iOS

iOS) CALayer, Shadow, Border 그리고 CornerRadius

kangwook 2024. 11. 15. 15:11

UI 작업을 하다보면 shadow와 border, cornerRadius 작업을 해야하는 경우가 종종 있다.

그런데 예를 들어 StackView의 경우 기본적으로 shadow를 넣을 수 없고 일반적인 뷰에 shadow를 적용하려고 해도 복잡해서 항상 찾아보게 되는 이런 상황이 싫어서 정리해보고자 한다.


가장 먼저 Shadow를 적용하는 Extension 함수 만들기

가장 고질적인 문제인 Shadow를 적용하는 방법을 가장 쉽게 하기 위해 CALayer Extension에 새로운 함수를 정의해준다.

기본적인 함수 외형은 

https://baechukim.tistory.com/112

 

[iOS] swift shadow 그림자 적용 (x, y, blur, spread) feat. Zeplin

제플린 등에서 제공하는 그림자 요소들을 쉽게 swiftf로 만들어주는 extension입니다. extension CALayer { func applySketchShadow( color: UIColor, alpha: Float, x: CGFloat, y: CGFloat, blur: CGFloat, spread: CGFloat ) { masksToBounds

baechukim.tistory.com

해당 블로그를 참고해서 만들었고, 이 함수는 재플린을 주로 사용하는 사람들을 위해서 만들어졌다.

extension CALayer {
    func applyShadow(color: UIColor = .black, 
    				 alpha: Float = 0.5, 
                     x: CGFloat = 0, 
                     y: CGFloat = 2, 
                     blur: CGFloat = 4, 
                     spread: CGFloat = 0, 
                     roundedCorners: UIRectCorner = [], 
                     cornerRadius: CGFloat = 0) {

        // If spread is 0 and no specific corners are rounded, set shadowPath to nil
        if spread == 0 && roundedCorners.isEmpty {
            shadowPath = nil
        } else {
            var rect = bounds
            if spread != 0 {
                let dx = -spread
                rect = rect.insetBy(dx: dx, dy: dx)
            }
            let path = UIBezierPath(roundedRect: rect, byRoundingCorners: roundedCorners, cornerRadii: CGSize(width: cornerRadius, height: cornerRadius))
            shadowPath = path.cgPath
        }
        
        shadowColor = color.cgColor
        shadowOpacity = alpha
        shadowOffset = CGSize(width: x, height: y)
        shadowRadius = blur / 2.0
    }
}

 

나는 여기서 추가적으로 뷰가 cornerRadius가 있을 경우에 대비해 roundedCorners와 cornerRadius 파라미터를 추가해서 적용했다.

여기서 명확히 알아야 할 것이 있는데, 레이어라는 것은 뷰를 올려놓는 어떠한 상판이라고 생각하면 된다.

이 상판의 위치를 x, y(shadowOffset)만큼 옮길 것인가, 어떤 색(shadowColor)을 적용하고 투명도(shadowOpacity)를 어느정도 적용할 것인가, 얼마나 퍼지게 할 것인가(shadowRadius)등을 설정한다고 생각하면 쉽다.

그런데 여기서 중요한건 spread인데, spread는 rect.insetBy를 사용해서 레이어의 사각형 크기를 조절한다고 보면된다.

예를 들어 spread를 10으로 했을 경우, 상하좌우 -10만큼 inset이 들어가기 때문에 결론적으로 기존 뷰보다 상하좌우 10씩 증가된 사각형레이어가 그려지게 되는 것이다.

그리고 roundedCorners는 배열형태로, 친절하게 .topLeft, .topRight, .bottomLeft, .bottomRight, .allCorners가 있기 때문에 기호에 따라 적용하면 된다.

그리고 이 shadow는 중요한게, 뷰가 그려지기 전에 적용하면 적용이 안된다.

왜냐하면 bounds를 인식하지 못하기 때문에..

그래서 viewDidLayoutSubviews에 넣어서 구현해야하는걸 권장한다.

var currentLocationButton: UIButton!

...

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    if let currentLocationButton = currentLocationButton {
        currentLocationButton.layer.applyShadow(color: .black, alpha: 0.1, x: 0, y: 1, blur: 3, spread: 1, roundedCorners: [.allCorners], cornerRadius: 24)
    }
}

...

 

이렇게 하면 정상적으로 shadow가 그려지는 것을 볼 수 있다. 근데 또 문제가 몇 가지 있음

StackView에 CornerRadius, Shadow, Border 적용하기

이 스택뷰는 viewless 뷰라고 하는데, 쉽게 말하자면 비 렌더링 뷰라고 생각하면된다.

그래서 cornerRadius, Shadow, Border 다 안들어간다. 걍 background color 이런거만 들어간다..

근데 필연적으로 우리는 스택뷰를 사용할때도 layer를 이용해야할 일이 있는데, 이 때는 어떻게 하냐..

 

결론적으로 말하면 Wrapper View를 2개 더 추가해야한다.

나는 그냥 만들 때 보통 outerView, innerView로 만들고 그 안에 스택뷰를 넣는 방식으로 한다.

이유는 쉽게말하면 한 뷰에 cornerRadius와 shadow를 동시에 적용하면 masksToBounds 속성 때문에 문제가 발생하기 때문에, masksToBounds를 true로 설정하면 그림자가 잘리고, false로 설정하면 cornerRadius이 적용되지 않기 때문이다.

뭐 여튼 이유는 이런데 진짜 쉽게 기억하기 위해서 깔끔하게 정리를 하고자 한다.

 

중요한건 이 4가지다

1. innerView에 cornerRadius

2. innerView에 border, masksToBounds = true

3. outerView에 applyShadow

4. outerView에 clear backgroundColor

 

솔직히 이 4개만 기억하면 stackView에 적용하는건 정말 쉽다. 맨날 까먹어서 다시 찾아봐서 그렇지

이렇게 적용하면 스택뷰든 뭐든 죽었다 깨어나도 무조건 적용된다. 


항상 디자인을 보고 UI에 구현을 하려고 하면 막히는 부분이 있기 마련이고, 이에 따라 시간을 들여서 찾아보고, 새로운 함수를 만들고 하는 과정이 무조건적으로 필요해지는 것 같다.

그러한 시간을 조금이라도 줄이고 효율적인 코딩을 하기 위해서 정리하는건데, 이왕이면 꼭 유용하게 이용해보도록 하자.

안된다 싶으면 masksToBounds를 빼먹었는지, viewDidLayoutSubviews에 구현하지 않고 다른 부분(예를 들어 viewDidLoad)에 구현하지 않았는지 꼭 확인해보도록 하자.

 

혹시 틀린 부분이나, 불필요한 코드같은게 있으면 피드백 주시면 감사하겠습니다