둥근 UIButton에 Shadow를 넣어주고 싶었다! 프로젝트 상에서 이미지 리소스를 최대한 줄이기 위해 이미지는 사용하지 않고 코드로만 작성하고 싶었으나. 생각과는 달리 어려운 부분을 맞이하게 되는데.. 두둥!!
외부에서 버튼에 그림자를 넣어주는 방식이 아닌, UIButton 객체를 만들어 이 객체안에서 그림자, 라운드가 모두 해결되는걸 원했기 때문에 스택오버플로우에 있는 답변은 대부분 답이 되지 않았다.. ㅠㅠ
모두들 아는대로 UIView에 Round를 주는 것과 Shadow를 넣어주는 것은 상당히 쉽다.
layer.cornerRadius = 10
layer.masksToBounds = true
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에서 직접 그려지며 별도의 스레드에서 작동이된다.
UIView에서 CALayer의 구성
UIView
는 하나의 CALayer(Root)
만 가지고 있다.
CALayer(Root)
는 SubLayer를 여러개
둘 수 있다.
UIView의 SubView
는 UIView(Root)
위에 얹혀지는 것이다.
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")
}
}
CALayer?
타입으로 생성private var shadowLayer: CALayer?
private var backgroundLayer: CALayer?
혹시나 draw가 다시 불려질 경우 (setNeedsDisplay등을 통해) 비효율적으로 shadowLayer나 backgroundLayer를 다시 그리는 일을 방지하기 위해서 옵셔널 타입으로 선언해준다.
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를 설정해준다.
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했기 때문에 계층 하나에서 그림자까지 생성된걸 볼 수 있다!