[G1-Sync] Manual knowledge update
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
---
|
||||
id: ios-swiftui-lifecycle-view-identity
|
||||
title: SwiftUI Lifecycle 와 View Identity
|
||||
category: Coding
|
||||
status: draft
|
||||
source_trust_level: B
|
||||
verification_status: conceptual
|
||||
created_at: 2026-05-09
|
||||
updated_at: 2026-05-09
|
||||
tags: [ios, swiftui, identity, lifecycle, vibe-coding]
|
||||
tech_stack: { language: "Swift / SwiftUI", applicable_to: ["iOS 16+"] }
|
||||
applied_in: []
|
||||
aliases: [view identity, .id(), onAppear, onChange]
|
||||
---
|
||||
|
||||
# SwiftUI Lifecycle 와 View Identity
|
||||
|
||||
> SwiftUI 는 view 를 **identity** 로 추적. identity 가 같으면 같은 view (state 보존), 다르면 새 view (state 리셋). `if/else` 분기, `.id()` modifier, `ForEach id`, 그리고 위치 — 이 4가지가 identity 를 결정.
|
||||
|
||||
## 📖 핵심 개념
|
||||
- View 는 struct (value type) — render 마다 다시 만들어짐.
|
||||
- 그러나 `@State` 는 identity 가 같으면 보존.
|
||||
- identity 가 바뀌면 onAppear → onDisappear → 새 onAppear.
|
||||
|
||||
## 💻 코드 패턴
|
||||
|
||||
### `.id()` 로 강제 재생성
|
||||
```swift
|
||||
struct UserView: View {
|
||||
let userId: String
|
||||
@State private var draft = ""
|
||||
var body: some View {
|
||||
TextField("draft", text: $draft)
|
||||
}
|
||||
}
|
||||
|
||||
// 부모
|
||||
UserView(userId: id)
|
||||
.id(id) // userId 바뀌면 새 view → draft 리셋
|
||||
```
|
||||
|
||||
### if/else 가 identity 만듦
|
||||
```swift
|
||||
if isLoggedIn {
|
||||
HomeView() // identity A
|
||||
} else {
|
||||
LoginView() // identity B
|
||||
}
|
||||
// 전환 시 onDisappear/onAppear 발생
|
||||
```
|
||||
|
||||
### ForEach 의 id 가 핵심
|
||||
```swift
|
||||
ForEach(items, id: \.id) { item in
|
||||
Row(item: item)
|
||||
}
|
||||
// id 가 같으면 같은 row (애니메이션 부드러움)
|
||||
// index 를 id 로 쓰면 reorder 시 사고
|
||||
```
|
||||
|
||||
### onAppear vs onChange vs task
|
||||
```swift
|
||||
struct Detail: View {
|
||||
let id: String
|
||||
@State private var data: Data?
|
||||
|
||||
var body: some View {
|
||||
Text("...")
|
||||
.onAppear { /* 매 등장마다 호출 */ }
|
||||
.task(id: id) {
|
||||
// id 바뀌면 이전 task cancel + 새 task. async 가능.
|
||||
self.data = try? await load(id: id)
|
||||
}
|
||||
.onChange(of: id) { oldId, newId in
|
||||
// id 바뀐 시점만
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🤔 의사결정 기준
|
||||
| 의도 | 도구 |
|
||||
|---|---|
|
||||
| 비동기 작업 + id 변경 시 cancel/restart | `.task(id:)` |
|
||||
| 매 등장마다 1회 작업 | `.onAppear` |
|
||||
| 특정 값 변경에 반응 | `.onChange(of:)` |
|
||||
| sheet/navigation 등장만 | `.task` (lifecycle 묶임) |
|
||||
| 강제 view 리셋 | `.id(value)` |
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **ForEach 에 `id: \.self` + 같은 값 가진 항목**: 충돌. unique id.
|
||||
- **`.task` 에 cancellation 무시 + 긴 loop**: id 변경 후에도 옛 task 가 setState. 새 task 가 자동 cancel 하므로 within task에서 checkCancellation.
|
||||
- **onAppear 에서 fetch 후 setState**: id 변경 시 cancel 안 됨. `.task(id:)` 권장.
|
||||
- **`.id()` 남발**: 모든 view 강제 재생성 → 애니메이션 깨짐 + 성능 저하.
|
||||
- **부모 rebuild 마다 자식 `@StateObject` 새로**: identity 다른 경우. 위치/조건 점검.
|
||||
- **Animation 안에서 ForEach id 가 index**: 항목 추가/삭제 시 잘못된 row animate.
|
||||
|
||||
## 🤖 LLM 활용 힌트
|
||||
- "이 view 는 어떤 값에 의해 identity 가 바뀌어야 하는가?" 질문 후 .id() 또는 .task(id:) 결정.
|
||||
- 비동기 + cancel 필요 = `.task(id:)`.
|
||||
|
||||
## 🔗 관련 문서
|
||||
- [[iOS_SwiftUI_State_Property_Wrappers]]
|
||||
- [[iOS_Swift_Concurrency_async_await]]
|
||||
Reference in New Issue
Block a user