UIButton에 Round와 Shadow 동시에 주기

21800 단어 UIkitUIkit

UIButton + Round + Shadow

문제 상황

둥근 UIButton에 Shadow를 넣어주고 싶었다! 프로젝트 상에서 이미지 리소스를 최대한 줄이기 위해 이미지는 사용하지 않고 코드로만 작성하고 싶었으나. 생각과는 달리 어려운 부분을 맞이하게 되는데.. 두둥!!

외부에서 버튼에 그림자를 넣어주는 방식이 아닌, UIButton 객체를 만들어 이 객체안에서 그림자, 라운드가 모두 해결되는걸 원했기 때문에 스택오버플로우에 있는 답변은 대부분 답이 되지 않았다.. ㅠㅠ

상황 설명

모두들 아는대로 UIView에 Round를 주는 것과 Shadow를 넣어주는 것은 상당히 쉽다.

  • cornerRadius
layer.cornerRadius = 10
layer.masksToBounds = true
  • shadow
layer.cornerRadius = 15
layer.shadowColor = UIColor.gray.cgColor
layer.shadowOpacity = 1.0
layer.shadowOffset = CGSize.zero
layer.shadowRadius = 6
layer.masksToBounds = false

masksToBounds 를 보면,

cornerRadius에서는 true를, shadow에서는 false를 설정해주게 된다.

즉, 서로가 상충하기 때문에 둥글고, 그림자가 있는 버튼이 나타나지 않는다.

cornerRadius나 shadow를 어디서 설정해주느냐에 따라 (뷰의 생명주기) 둥글기만 하거나, 그림자만 있는 버튼이 되어버렸다.

해결 방법

layer는 UIView에게 어떤 존재길래 masksToBounds 로 지지고 볶고 하는 걸까?

UIView는 CALayer 타입의 layer 라는 프로퍼티를 갖고 있다.

Core Animation 에서 제공하는 클래스로 UIView보다 한 단계 더 낮은 레벨의 인터페이스이다.

GPU에서 직접 그려지며 별도의 스레드에서 작동이된다.

Apple Developer Documentation

UIView에서 CALayer의 구성

UIView는 하나의 CALayer(Root)만 가지고 있다.

CALayer(Root)SubLayer를 여러개 둘 수 있다.

UIView의 SubViewUIView(Root)위에 얹혀지는 것이다.

CALayer 두개를 이용해 그림자가 있는 버튼을 만들어보자!

  • 전체코드
    public final class PostingButton: UIButton {
    
        private var shadowLayer: CALayer?
        private var backgroundLayer: CALayer?
    
        override public var isHighlighted: Bool {
            didSet {
                guard let layer = backgroundLayer else {
                    return
                }
    
                layer.backgroundColor = isHighlighted
                ? YDSColor.buttonPointPressed.cgColor
                : YDSColor.buttonPoint.cgColor
            }
        }
    
        public override init(frame: CGRect) {
            super.init(frame: frame)
            render()
            setConfiguration()
    
        }
    
        public override func draw(_ rect: CGRect) {
            configureLayers(rect)
        }
    
        private func render() {
            setImage(
                YDSIcon.commentFilled.withTintColor(
                    YDSColor.buttonReversed,
                    renderingMode: .alwaysOriginal
                ),
                for: .normal
            )
    
            tintColor = YDSColor.buttonReversed
        }
    
        private func setConfiguration() {
            if #available(iOS 15.0, *) {
                configuration = UIButton.Configuration.plain()
            } else {
                adjustsImageWhenHighlighted = false
            }
        }
    
        private func configureLayers(_ rect: CGRect) {
           if shadowLayer == nil {
               let shadowLayer = CALayer()
               shadowLayer.masksToBounds = false
               shadowLayer.shadowColor = YDSColor.shadowThin.cgColor
               shadowLayer.shadowOffset = CGSize(width: 0, height: 2)
               shadowLayer.shadowOpacity = 1
               shadowLayer.shadowRadius = rect.height/2
               shadowLayer.shadowPath = UIBezierPath(roundedRect: rect, cornerRadius: rect.height/2).cgPath
               layer.insertSublayer(shadowLayer, at: 0)
               self.shadowLayer = shadowLayer
           }
    
            if backgroundLayer == nil {
                let backgroundLayer = CALayer()
                backgroundLayer.masksToBounds = true
                backgroundLayer.frame = rect
                backgroundLayer.cornerRadius = rect.height/2
                backgroundLayer.backgroundColor = YDSColor.buttonPoint.cgColor
                layer.insertSublayer(backgroundLayer, at: 1)
                self.backgroundLayer = backgroundLayer
            }
       }
    
        required init?(coder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
    }

1️⃣ shadowLayer와 backgroundLayer CALayer? 타입으로 생성

private var shadowLayer: CALayer?
private var backgroundLayer: CALayer?

혹시나 draw가 다시 불려질 경우 (setNeedsDisplay등을 통해) 비효율적으로 shadowLayer나 backgroundLayer를 다시 그리는 일을 방지하기 위해서 옵셔널 타입으로 선언해준다.

2️⃣ 배경과 그림자 레이어 주입

private func configureLayers(_ rect: CGRect) {
       if shadowLayer == nil {
           let shadowLayer = CALayer()
           shadowLayer.masksToBounds = false
           shadowLayer.shadowColor = YDSColor.shadowThin.cgColor
           shadowLayer.shadowOffset = CGSize(width: 0, height: 2)
           shadowLayer.shadowOpacity = 1
           shadowLayer.shadowRadius = rect.height/2
           shadowLayer.shadowPath = UIBezierPath(roundedRect: rect, cornerRadius: rect.height/2).cgPath
           layer.insertSublayer(shadowLayer, at: 0)
           self.shadowLayer = shadowLayer
       }

        if backgroundLayer == nil {
            let backgroundLayer = CALayer()
            backgroundLayer.masksToBounds = true
            backgroundLayer.frame = rect
            backgroundLayer.cornerRadius = rect.height/2
            backgroundLayer.backgroundColor = YDSColor.buttonPoint.cgColor
            layer.insertSublayer(backgroundLayer, at: 1)
            self.backgroundLayer = backgroundLayer
        }
   }

shadowLayer 위에 원래라면 버튼의 background를 담당할 색상과 모양을 설정한 backgroundLayer를 설정해준다.

3️⃣ 이벤트 설정

override public var isHighlighted: Bool {
        didSet {
            guard let layer = backgroundLayer else {
                return
            }

            layer.backgroundColor = isHighlighted
            ? YDSColor.buttonPointPressed.cgColor
            : YDSColor.buttonPoint.cgColor
        }
    }

CA는 이벤트를 받지 못하기 때문에 UIView의 이벤트를 받아 layer의 색상을 바꾸어 처리해준다

결과

UIView를 addSubview 한게 아니라 layer를 insertSublayer했기 때문에 계층 하나에서 그림자까지 생성된걸 볼 수 있다!

좋은 웹페이지 즐겨찾기