-
Notifications
You must be signed in to change notification settings - Fork 0
UIKit Observable Rendering
관련 기술: #UIKit #Observable #Swift6 #Performance 참고 자료:
@Observable을 class에 붙이면 컴파일러가 stored property를 computed property로 변환한다.
// 원본
@Observable class Model {
var name = ""
}
// ↓ 매크로 확장 (개념적)
class Model: Observable {
private let _$observationRegistrar = ObservationRegistrar()
private var _name = ""
var name: String {
get {
_$observationRegistrar.access(self, keyPath: \.name) // ← 읽기 추적
return _name
}
set {
_$observationRegistrar.withMutation(of: self, keyPath: \.name) { // ← 변경 알림
_name = newValue
}
}
}
}핵심 포인트:
-
getter:
access()를 호출해 "누가 이 프로퍼티를 읽었는지" 등록 -
setter:
withMutation()을 호출해 "이 프로퍼티가 바뀔 것임"을 알림 - 값이 같아도 setter를 호출하면 observation이 fire됨 — Equatable 체크 없음
withObservationTracking {
// apply 클로저: 여기서 읽은 프로퍼티만 추적 대상
print(model.name) // → name에 대한 observation 등록
} onChange: {
// name이 변경되면 이 클로저가 호출됨 (1회성!)
print("changed!")
}-
apply클로저 안에서access()가 호출된 프로퍼티만 추적 -
onChange는 1회만 호출됨 (one-shot). 계속 추적하려면 재귀적으로 다시 호출해야 함 - SwiftUI의
body와 UIKit의updateProperties()는 이 메커니즘을 자동으로 반복 호출함
@Observable은 class에만 적용 가능하다 (SE-0395).
- class는 참조 타입 → 메모리 위치가 안정적 →
ObservationRegistrar가 인스턴스에 고정 - struct는 값 타입 → 복사될 때마다 registrar도 복사 → 어떤 인스턴스에 대한 변경인지 추적 불가
Trait Update → updateProperties() → layoutSubviews() → Display
-
updateProperties()는 layout 이전에 실행되는 프로퍼티 설정 전용 메서드 - 텍스트, 색상, 가시성 등 geometry와 무관한 속성 변경에 사용
- WWDC25에서 도입 (iOS 26).
UIObservationTrackingEnabled키로 iOS 18부터 사용 가능
UIKit은 updateProperties() 실행 시 내부적으로 withObservationTracking을 감싼다.
// UIKit 내부 (개념적)
func _runUpdateProperties() {
withObservationTracking {
self.updateProperties() // ← 여기서 읽은 @Observable 프로퍼티 추적
} onChange: { [weak self] in
DispatchQueue.main.async {
self?.setNeedsUpdateProperties() // ← 변경 시 다음 사이클에 재호출
}
}
}결과: updateProperties() 안에서 읽은 @Observable 프로퍼티가 변경되면, UIKit이 자동으로 updateProperties()를 다시 호출한다. 이 과정이 자동으로 반복된다.
| 메서드 | 자동 tracking | 용도 |
|---|---|---|
updateProperties() |
✅ | 프로퍼티, 스타일, 콘텐츠 |
layoutSubviews() |
✅ | 위치, 크기, geometry |
updateConfiguration(using:) |
✅ | 셀 configuration |
viewDidLoad() 등 |
❌ | 초기 설정 (1회성) |
@Observable class ViewModel {
var state: State // ← State는 struct
}
struct State {
var analysisState: AnalysisState // 1회성 변화
var currentPlaybackState: AudioPlaybackState // 0.1초마다 변화
}state는 @Observable class의 단일 stored property다.
observation 시스템은 state 프로퍼티 단위로 추적한다.
// updateProperties() 안에서
viewModel.state.analysisState // → viewModel.state에 대한 observation 등록state.currentPlaybackState가 바뀌면 → struct 전체가 재할당 → state setter 호출 → observation fire → updateProperties() 재호출!
의도: analysisState만 추적하고 싶었음
현실: state의 어떤 프로퍼티든 바뀌면 트리거됨
override func updateProperties() {
switch analysisObservable.analysisState { // ← 이것만 추적? 아님!
case .completed:
applySnapshot() // ← 이 안에서 viewModel.state를 읽음!
}
}
func applySnapshot() {
// 이 코드는 updateProperties()의 tracking context 안에서 실행됨
let items = viewModel.state.keyPoints // ← viewModel.state observation 등록!
}updateProperties() → applySnapshot() → viewModel.state 읽기
→ ViewModel observation 등록 → state.currentPlaybackState 변경 시에도 updateProperties() 재호출
교훈: tracking context는 호출 체인 전체에 전파된다.
updateProperties() 안에서 호출하는 모든 함수의 모든 @Observable 프로퍼티 접근이 추적 대상이 된다.
@Observable class PlaybackHighlight {
var playingParagraphInfo: PlayingParagraphInfo?
}
// 0.1초마다 호출
highlight.playingParagraphInfo = sameValue // ← 값이 같아도 observation fire!@Observable의 setter(withMutation)는 Equatable 비교를 하지 않는다.
매번 setter가 호출되면 모든 observer에게 알림이 간다.
빠른 주기 (0.1초 간격)
currentPlaybackState ──→ AudioPlayerObservable ──→ AudioPlayerView.updateProperties()
느린 주기 (1회성/이벤트성)
analysisState ──→ AnalysisObservable ──→ VC.updateProperties()
errorMessage ──→ ErrorObservable ──→ VC.updateProperties()
struct State {
let analysisObservable = AnalysisObservable() // VC가 관찰
let errorObservable = ErrorObservable() // VC가 관찰
let audioPlayerObservable = AudioPlayerObservable() // PlayerView가 관찰
let playbackHighlight = PlaybackHighlight() // ScriptCell이 관찰
// 이 프로퍼티 변경이 위 Observable들을 오염시키지 않음
var currentPlaybackState: AudioPlaybackState { didSet { audioPlayerObservable.playbackState = currentPlaybackState } }
}// ❌ 잘못된 패턴
override func updateProperties() {
applySnapshot() // 내부에서 viewModel.state 읽음 → tracking 오염
}
// ✅ 올바른 패턴: 1회만 실행하거나, tracking context 밖에서 호출
override func updateProperties() {
if !hasAppliedCompletedSnapshot {
hasAppliedCompletedSnapshot = true
applySnapshot() // 이후 tracking에 viewModel이 포함되지 않음
}
}// ❌ 매번 fire
highlight.playingParagraphInfo = newInfo
// ✅ 변경 시에만 fire
guard playingParagraphInfo != newInfo else { return }
highlight.playingParagraphInfo = newInfoupdateProperties()가 과도하게 호출된다면:
-
updateProperties()안에서viewModel.state를 직접 읽고 있지 않은가? -
updateProperties()에서 호출하는 함수 안에서viewModel.state를 읽고 있지 않은가? -
@Observable프로퍼티에 같은 값을 반복 set하고 있지 않은가? - 고주기 상태(재생/타이머/센서)가 저주기 상태(콘텐츠/에러)와 같은
@Observable인스턴스에 있지 않은가? -
lastAppliedXxx같은 shadow state가 생기고 있지 않은가? → observation 분리 필요 신호
-
updateProperties()내에 재생/센서/타이머 등 고주기 상태와 일반 콘텐츠 상태가 혼재하는 경우 -
lastAppliedXxx패턴이 반복적으로 등장하는 경우 -
@Observableclass의 struct 프로퍼티 안에 변화 주기가 다른 상태가 혼재하는 경우
- 도메인-레이어-파라미터-설계:-URL-vs-Entity
- 도메인-에러-설계:-유스케이스별-vs-도메인(기능)별
- Swift-6.0-Typed-Throws-Guideline
- CoreData-loadPersistentStores-비동기-처리-전략
- stateless-Infrastructure-서비스의-struct-전환
- STT-Repository-동시-요청-큐잉-전환
- Coordinator-클래스-기반에서-프로토콜-기반으로-마이그레이션