본문 바로가기
iOS/UIKit

[UIKit] UIView와 CALayer 사용하기

by Kelly Chui 2025. 7. 17.

개발자 문서에 적혀있는 UIView의 역할은 '화면의 직사각형 영역의 콘텐츠를 관리하는 객체' 이다.

  • 화면의 사각형 영역을 나타내며, 내부에 다른 뷰를 포함할 수 있다. 대부분의 UI 요소가 UIView를 상속받는다.
  • 콘텐츠를 화면에 나타내고, 사용자의 터치나 제스처를 감지한다.

UIView로 콘텐츠 표시하기

UIView는 UIKit에서 제공하는 메소드 혹은 Core Graphics를 직접 이용해서 직사각형 영역에 콘텐츠를 그릴 수 있다. 배경 색만 존재하는 간단한 UIView를 그려보자.

var magentaRectView: UIView = {
    let magentaRectView = UIView()
    magentaRectView.backgroundColor = .magenta
    return magentaRectView
}()

오토레이아웃 제약은 임의적으로 정했다.

UIView는 내부에 다른 UIView를 포함할 수 있다. 이 때 상위 뷰에 포함된 뷰를 서브 뷰라고 한다. 사실 UIViewControllerview 프로퍼티에 UIView가 할당되어 있으므로, 위 마젠타 사각형도 UIView 내부의 UIView이다. 하지만 좀 더 직관적으로 확인하기 위해, 마젠타 사각형 안에 검은색 사각형을 포함시켜보자.

var blackRectSubView: UIView = {
    let blackRectSubView = UIView()
    blackRectSubView.backgroundColor = .black
    return blackRectSubView
}()

UIView에 서브 뷰를 추가하기 위해서는 addSubView(\_:) 메소드를 사용하면 된다. 마찬가지로 오토레이아웃 제약은 임의적으로 정했다.

    private func setupConstraints() {
        magentaRectView.addSubview(blackRectSubView) // 마젠타 사각형 뷰에 서브 뷰 추가
        view.addSubview(magentaRectView) // 뷰 컨트롤러 기본 뷰에 마젠타 사각형 뷰 추가
        magentaRectView.translatesAutoresizingMaskIntoConstraints = false
        blackRectSubView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            // 오토레이아웃 제약
        ])
    }

UIKit에서 기본적으로 제공해주는 UIStackView, UITableView, UIImageView 등 대다수의 뷰들이 UIView 의 서브 클래스이다. UIKit에서 시각적으로 콘텐츠를 보여주는 책임을 UIView가 가지고 있기 때문이다.

CALayer

개발자 문서에서 CALayer는 '이미지 기반 콘텐츠를 관리하고, 해당 콘텐츠에 애니메이션을 수행할 수 있는 객체' 라고 설명한다.

UIView는 화면에 컨텐츠를 보여주는 책임을 가지고 있지만, 직접 그 컨텐츠를 그리는 것은 CALayer라는 클래스가 한다. UIView의 자체적인 역할과 CALayer의 역할을 구분해보자.

  • CALayer는 그래픽 렌더링, 애니메이션을 처리한다.
  • UIView는 그 외의 레이아웃 작업, 터치 이벤트를 처리한다.

UIViewControllerview 프로퍼티에 UIView 인스턴스를 가지고 있는 것 처럼, UIViewlayer 프로퍼티에 CALayer 인스턴스를 기본적으로 가지고 있다.

계층 구조

UIView가 서브 뷰를 가질 수 있는 것 처럼 CALayer도 서브 레이어를 가질 수 있다. 위에서 만든 사각형 안의 사각형 도형을 서브 뷰 없이 레이어 계층 구조만으로 그려보겠다.

let blackRectSublayer: CALayer = {
    let blackRectSublayer = CALayer()
    blackRectSublayer.frame = CGRect(x: 25, y: 25, width: 50, height: 50)
    blackRectSublayer.backgroundColor = UIColor.black.cgColor
    return blackRectSublayer
}()

레이어를 하나 생성한 다음에 addSublayer(\_:) 메소드를 호출해서 간단하게 서브레이어를 추가할 수 있다.

magentaRectView.layer.addSublayer(blackRectSubLayer)

서브 뷰를 추가하지 않고서도 똑같은 형태를 구현할 수 있다.

CALayer와 UIView의 관계

UIView 인스턴스는 layer 프로퍼티에 CALayer 인스턴스를 가지고 있다. 그리고 UIViewCALayer의 동작을 위임받는 형태로 델리게이트 패턴으로 연결되어 있다. 대표적으로 draw(\_:) 메소드가 있다. UIKit은 오픈 소스가 아니므로 코드로 직접 확인하기 힘들다. 따라서 직접 instrument에서 확인해보자.

우선 커스텀 뷰를 하나 만들자.

let log = OSLog(subsystem: Bundle.main.bundleIdentifier ?? "com.kelly.uikitexample", category: .pointsOfInterest)

final class LayerView: UIView {
    override init(frame: CGRect) {
        super.init(frame: frame)
        backgroundColor = .white
    }

    @available(*, unavailable)
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func draw(_ rect: CGRect) {
        os_signpost(.event, log: log, name: "draw(_:)")
    }
}

instruments에서 해당 호출 부분을 찾기 쉽도록 signpost를 지정했다. 이제 이 뷰를 뷰 컨트롤러에 추가하고, 실제 콜 트리를 확인해보자.

draw(_:) 메소드가 실행되면, CALayer가 Objective-C 브릿징을 한번 거친 후, 델리게이트 메소드를 호출하는 과정을 확인할 수 있다.

CALayer에서 그래픽, 애니메이션 처리하기

이제 우리가 보는 UIView 인스턴스의 시각적인 부분은 CALayer가 처리한다는 것을 알게 되었다. 그러면 UIView 말고 실제 CALayer 인스턴스를 조작해서 그래픽을 처리해보자.

그림자 넣어보기

CALayer는 그림자를 추가하는 메소드를 직접 제공한다.

layerView.backgroundColor = .magenta
let layer = layerView.layer
layer.shadowColor = UIColor.black.cgColor
layer.shadowOpacity = 1
layer.shadowOffset = CGSize(width: 5, height: 5)
layer.shadowRadius = 10
layer.cornerRadius = 12

각 프로퍼티들에 대한 자세한 설명은 필요하지 않다고 느낀다. 핵심은 UIView에서 시각적인 부분을 처리하는 것이 CALayer라는 것을 확인하는 것이다.

애니메이션 처리하기

UIView에서도 애니메이션을 처리할 수 있지만, 실제론 CALayer가 처리하는 것을 래핑하는 것 뿐이다. CALayer에 직접 애니메이션을 추가해보자.

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    let startPosition = layerView.layer.position
    let animation = CABasicAnimation(keyPath: "position.x")
    animation.fromValue = startPosition.x
    animation.toValue = startPosition.x + 100
    animation.duration = 1.0
    animation.autoreverses = true
    animation.repeatCount = .infinity
    layerView.layer.add(animation, forKey: "horizontalMove")
}

잘 동작하는 것을 확인할 수 있다.

'iOS > UIKit' 카테고리의 다른 글

[UIKit] UIResponder + Responder chain 흐름  (0) 2025.07.21
[UIKit] UIStackView 레이아웃  (0) 2025.07.17