본문 바로가기

iOS/Deep dive!!!

[Swift] UI Compoenet에 Shadow 적용 방법 | shadowPath | 쉐도우가 적용되지 않았던 이유...

 
안녕하세요. 이번 포스트는 shadow 적용하는 방법과 shadowPath를 사용한 shadow rendering 최적화 방법, 그리고 쉐도우가 적용이 안됬던 제 경험을 글로 작성했습니다.
 

1. UIView에 shadow 적용하는 방법

UIView에 쉐도우를 적용하는 방법은 기본적으로 layer 속성을 이용하면 됩니다.
 

let view = UIView()
...
view.layer.shadowColor = UIColor.black.cgColor
view.layer.shadowOpacity = 0.5
view.layer.shadowOffset = CGSize(width: 2.0, height: 2.0)
view.layer.shadowRadius = 4.0

 
1. layer에서 보여질 shadow color를 지정합니다.
2. shadow의 투명도를 지정합니다.
3. shadow가 보여질 offset(특정 지점에서 얼만큼 떨어지는가)를 지정합니다. 값이 -일 경우 왼 or 위로 이동하고 +는 오른쪽 or 아래로 이동합니다.
 
그 예를들어 CGSize(width: 2.0, height: 0)으로 했습니다.  이 경우 그림자가 수평방향으로 2 포인트 만큼 이동해서 생깁니다.
반대로 CGSize(width:0.0, height: 2.0)으로 하면 수직(아래)로만 2포인트 이동해서 생성됩니다. ( 아래 방향으로만 생성 )
 
4. shadowRadius로 블러(흐림) 반경을 지정합니다. 값이 커질수록 더 흐리게 됩니다.
 
shadow는 기본적으로 UIView의 layer 겉에 나타나게 됩니다. 그래서 뷰의 배경 색에 따라 잘 안보일 수 있습니다. 그리고 기본적으로 view의 밖에 생기기 때문에 layer.masksToBounds 를 false로 해야합니다.
 
masksToBounds는 view의 bounds를 벗어나는  부분을 자르기 때문입니다. 
 

2. layer.shadowPath

 

하지만 그림자는 형태의 위치, 크기 render할 때 계산 리소스가 많이 필요됩니다. 그래서 동적으로 계산될 때는 위와 같은 경고가 나타날 수 있습니다. 동적으로 render 하기 때문입니다. (= shadowPath로 성능 최적화를 하라는 뜻) 그래서 shadowPath를 지정해주는 게 좋습니다.
shadowPath를 통해 쉐도우가 보여질 현재 뷰의 크기를 감싸서 쉐도우가 나올 영역을 지정해주는 것입니다. 
 

let view = UIView()
...
view.layer.shadowColor = UIColor.black.cgColor
view.layer.shadowOpacity = 0.5
view.layer.shadowOffset = CGSize(width: 2.0, height: 2.0)
view.layer.shadowRadius = 4.0
let shadowRect = CGRect(x: -10, y: -10, width: layer.bounds.width + 20, height: layer.bounds.height + 20)
layer.shadowPath = UIBezierPath(rect: shadowRect).cgPath

 
여기서 shadowRect는 결국 현재 뷰보다 가로로 30, 세로로 30 더 큰 뷰를 의미할 것 같지만 이는 아닙니다. view의 실제 x,y 좌표가 어딜 지 모르기 때문에...
 

let shadowRect = CGRect(
    x: layer.bounds.origin.x - 10,
    y: layer.bounds.origin.y - 10,
    width: layer.bounds.width + 20,
    height: layer.bounds.height + 20)

 
요롷게 해야 view보다 가로로 30, 세로로 30 큰 영역을 정의할 수 있습니다. 이렇게 현재 쉐도우를 적용할 view를 감쌀 영역을 정의하고  ( 여기까지 쉐도우가 그려질 수 있다) UIBezierPath(rect:).cgPath를 통해 shadowPath를 설정할 수 있습니다.
 

https://developer.apple.com/documentation/uikit/uibezierpath

UIBezierPath는 직선 또는 curved line으로 구성된 Path입니다. 선!! 이를 통해 custom view에서 rendering할 수 있습니다. 여기선 이를 통해 Path를 shadowPath로 적용할 것입니다.
 
여러 함수들이 있는데
간단하게
move(to:) 는 선이 그려질 시작점 정의
addLine(to:)는 move의 끝 점을 정의!! close는 그리는 것을 끝냅니다. 요고로 도형도 그릴 수 있습니다. +_+ 
 
다시 돌아와서 shadow가 그려질 Path를 UIBezierPath로 사각형이든 ... 다양한 형태로 Path를 정할 수 있습니다. ( 그림자의 모양을 다양하게 정할 수 있다는 말) 그림자를 더 정교하게 조작이 가능하고, 그림자를 사전에 rendering할 수 있게 됩니다. 일반적인 CALayer의 그림자 이미지는 동적으로 계산됩니다. Rendering 과정에서 그림자 또한 매번 계산될 수 있지만 view.layer.shadowPath를 지정하면 그림자를 사전에 정의 가능합니다. ( 동적으로 그림자를 그려야 할 상황이 아니면 rendering 과정에서 그림자 다시 계산 필요 x)
 

3. 뷰의 shadow가 적용이 안되는 이유

 
이렇게 기본적으로 뷰에 shadow를 넣는 것은 간단한데.. 제 경우에 이상하게 뷰의 쉐도우가 절때 나타나지 않았습니다.
 

 
지금 화면은 subview 두 개로 구성되어 있습니다.
 

 
위에 있는 뷰는 categoryView. 아래의 gray는 categoryDetailView입니다.
 

 
그리고 제가 쉐도우를 줘야 할 위치는 초록색 구간 categoryView의 아래 부분입니다.  
 

 
 
그래서 categoryView에서 super.init(frame:) 시점이 끝난 이후 configureUI()에서
 

 
이 함수를 통해 쉐도우를 적용했습니다.. 그런데 아무리 해도.. 적용이 안됬습니다. bounds.height 사용 시점에 print를 찍어보니 0이더라구요.. auto layout를 사용할 경우엔 layoutSubviews() 함수가 호출되지 않는 한 subviews의 layout 중이기 때문에 뷰들의 레이아웃이 계산되지 않습니다. 그래서 0으로 출력됩니다.
그런데 제 경우 이 컴포넌트가 화면의 최상단에 위치하고, 이 함수 호출할 시점에는 해당 뷰의 레이아웃을 지정했다면 layoutIfNeeded()함수를 bounds사용하기 전에 넣을 수 있습니다. (그럼 layoutIfNeeded()에 의해 bounds가 지정됩니다)
 
layoutIfNeeded()를 사용하게 될 경우 이를 호출한 시점에서 강제적으로 이 뷰와 하위 뷰들의 레이아웃을 갱신해서 최신 크기, x,y를 갖을 수 있습니다. +_+ 그래서 103번라인 전에 추가하면!!!!
 
 
 
그래도 안됩니다.
저의 경우에 왜 안됬냐면 categoryView 바로 아래에 categoryDetailView가 위치해있기 때문이고 categoryDetailView의 top anchor의 equalTo: categoryView, constraint는 0입니다. !! categoryView와 딱 붙어있기 때문에 이들의 컨테이너 뷰인 categoryPageView에서 categoryView를 먼저 subview로 추가한 후 그 다음에 categoryDetailView를 추가했다면 뷰의 겉 부분에서 보여지는 쉐도우가 가려질 수 있습니다. ( 이게 제 경우.. )
 

 
그래서 categoryView를 서브뷰로 추가할 때 마지막 해 해줌으로 이를 해결했습니다. 
 
 

 
짜잔!!! 이렇게 할 경우 원했던 쉐도우를 다시 볼 수 있었습니다.. 후.. 
 


새롭게 알게 된 개념

  1. layoutIfNeeded() 를 사용하면 초기화 시점에 해당 뷰와 subviews의 frame를 명확히 얻어올 수 있다.
  2. shadowPath는 UIBezierPath를 통해 뷰의 겉 부분에서 그려질 shadow의 위치, 모양 등을 정할 수 있다. bezierPath는 펜과 같다. 점과 점사이를 이용해 선을 만들고, 선과 선을 이어 모형을 만들면 그 안 영역을 다룰 수 있다.
  3. shadow는 UIView의 겉 부분에 render됨으로 view와 view 사이에 spacing이 0일 경우 가려질 수 있다.