iOS 18에 숨겨진 보석
SwiftUI가 처음 나왔을 때,
@Published
속성이 바뀔 때마다 뷰가 자동으로 업데이트되는 모습에 모두 감탄했죠? 애플이 조용히 UIKit과 AppKit에도 같은 마법을 심어두고 있었습니다. 더 놀라운 점은, 이 기능이 iOS 18/macOS 15에 이미 포함되어 있다는 사실. Xcode 26 같은 최신 도구도 필요하지 않고, 오직 간단한 Info.plist 설정만 해주면 됩니다.
UIKit에서 데이터 모델과 UI를 동기화하는 일은 항상 번거로운 일이었습니다. 흔히 아래와 같은 코드를 쓰면서 UI가 최신 상태를 유지하도록 신경 써야 했죠.
class ProfileViewController: UIViewController {
var user: User? {
didSet {
updateUI()
}
}
func updateUI() {
nameLabel.text = user?.name
avatarImageView.image = user?.avatar
// … 추가로 여러 줄의 수동 업데이트 코드
setNeedsLayout()
}
}
updateUI()
호출을 깜빡하면 UI는 “멈춘” 상태가 되고, 너무 자주 호출하면 성능에 문제가 발생할 수 있습니다. 귀찮고 실수하기 쉬운 방식이죠.
이제는 모든 게 바뀝니다. 새로운 관찰 프레임워크를 도입하면 위의 패턴이 필요 없어집니다. 아래처럼 작성만 하면 끝입니다:
import Observation
@Observable
class User {
var name: String = ""
var avatar: UIImage?
var unreadCount = 0
var hasUnread: Bool {
unreadCount > 0
}
}
class ProfileViewController: UIViewController {
let user = User()
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
// UIKit이 여기에 접근한 속성을 자동으로 추적합니다!
nameLabel.text = user.name
avatarImageView.image = user.avatar
badgeView.isHidden = !user.hasUnread
}
}
이제 앱 어디에서든 user.name
을 변경하면, 해당 변경이 자동으로 반영되어 라벨이 갱신됩니다. 수동 호출, 누락된 업데이트, 불필요한 성능 부담 없이 “그냥 작동”합니다.
자동 관찰 트래킹은 다양한 UIKit과 AppKit 메서드에서 지원됩니다. 주로 아래 메서드들이 사용됩니다:
updateProperties()
(iOS 26+)layoutSubviews()
updateConstraints()
draw(_:)
updateProperties()
(iOS 26+)viewWillLayoutSubviews()
viewDidLayoutSubviews()
updateViewConstraints()
updateContentUnavailableConfiguration(using:)
containerViewWillLayoutSubviews()
containerViewDidLayoutSubviews()
updateConfiguration()
configurationUpdateHandler
실행 시updateConfiguration(using:)
configurationUpdateHandler
실행 시updateConstraints()
layout()
draw(_:)
updateLayer()
updateViewConstraints()
viewWillLayout()
viewDidLayout()
이 기능은 기본으로 활성화되어있지는 않습니다. 수동으로 Info.plist에 다음 키를 추가해야 합니다.
UIKit (iOS 18+)
<key>UIObservationTrackingEnabled</key>
<true/>
AppKit (macOS 15+)
<key>NSObservationTrackingEnabled</key>
<true/>
iOS 26/macOS 26부터는 이 키가 없어도 기본적으로 활성화되어, 이 설정은 무시됩니다. ([steipete.me][1])
iOS 26에서는 updateProperties()
라는 신규 메서드를 통해 더 효율적인 업데이트 루프를 지원합니다. layoutSubviews()
보다 먼저 실행되며, 다음과 같은 기능을 제공하죠: ([steipete.me][1])
setNeedsUpdateProperties()
로 수동 플래그 호출 가능updatePropertiesIfNeeded()
로 즉각적 실행 가능이 기능들을 테스트 해볼 수 있는 예제 프로젝트도 공개되어 있습니다: ObservationTrackingExample
지원 플랫폼: iOS 18+/macOS 15+ (수동 활성화 필요), iOS 26+/macOS 26에서는 기본 활성화
모델 예시:
@Observable
@MainActor
final class SharedDataModel: Sendable {
var message = "Hello!"
var counter = 0
// ... 기타 속성
}
UIKit 예시:
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
textLabel.text = model.message
counterLabel.text = "\(model.counter)"
}
AppKit 예시:
override func viewWillLayout() {
super.viewWillLayout()
textField.stringValue = model.message
label.intValue = model.counter
}
이 프로젝트는 SwiftUI, UIKit, AppKit 간에 Observable 모델을 공유하며, 수동 코드 없이 UI가 자동 동기화되는 사례를 보여줍니다.
위 예제를 참고하여 버튼 클릭에 따라 UILabel 이 바뀌는 간단한 기능을 구현해보았다!
@Observable
class User {
var name: String = "테스트 이름"
}
class ViewController: UIViewController {
@IBOutlet weak var nameLabel: UILabel!
let user = User()
var testFlag: Bool = true
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
// UIKit이 여기에 접근한 속성을 자동으로 추적합니다!
nameLabel.text = user.name
}
override func viewDidLoad() {
super.viewDidLoad()
}
@IBAction func updateBtnClicked(_ sender: Any) {
testFlag.toggle()
user.name = testFlag ? "테스트 이름" : "조이"
}
}
UIObservationTrackingEnabled
또는 NSObservationTrackingEnabled
키를 추가하면 활성화할 수 있습니다.updateProperties()
메서드도 지원됩니다.출처
https://steipete.me/posts/2025/automatic-observation-tracking-uikit-appkit