안녕하세요.
그동안 대충은 알고 있었던 SwiftUI의 View Lifecycle에 대해서 정확하게 알고 가는 시간을 가져볼게요.
개인적으로 Lifecycle 개념도 중요하지만, Lifecycle이 정확하게 언제 호출되는지 (ex. 렌더링 전? 뷰 계층 제거된 후?)도 중요하다고 생각하기 때문에 로그 찍어보면서 하나씩 살펴보려고 합니다.
(관련 공식 문서가 별로 없더라구요.. 그래서 추측성 분석이 될 수도 있을 것 같아요ㅠ)
# Lifecycle method
## onAppear
View가 보이기 전에 action을 수행하는 함수입니다.
View가 첫 렌더링 되기 전에 action 클로저가 끝나는 것을 보장합니다.
그래서 아래처럼 onAppear에 수행시간이 오래 걸리는 action을 등록하면, 그만큼 화면이 늦게 출력됩니다.
(2초 sleep이 끝난 이후에 화면이 출력되는 것을 볼 수 있어요.)
struct OnAppearView: View { | |
var body: some View { | |
Text("Hello, World!") | |
.onAppear { | |
print("[OnAppearView] start onAppear") | |
sleep(2) | |
print("[OnAppearView] finish onAppear") | |
} | |
} | |
} |

## task
iOS 15 이상부터 사용할 수 있는 함수입니다.
View가 보이기 전에 비동기 task를 수행하는 함수입니다.
비동기 task이기 때문에 task가 끝나기 전에 View가 그려질 수 있습니다.
또한, task가 끝나기 전에 View가 사라지게 되면(disappear) 자동으로 task를 취소(cancel) 시킵니다.
아래 예시를 보면 View가 보이고 나서 task가 완료되는 것을 볼 수 있습니다.
struct TaskView: View { | |
var body: some View { | |
Text("Hello, World!") | |
.task { | |
print("[OnAppearView] start onAppear") | |
do { | |
try await Task.sleep(for: .seconds(2)) | |
} catch { | |
} | |
print("[OnAppearView] finish onAppear") | |
} | |
} | |
} |

추가로 task 함수에는 task(id:priority:_:) 라는 함수도 존재합니다.
이 함수는 View가 보이기 전 뿐만 아니라 Equatable 프로토콜을 준수하는 특정 변수의 값이 변할 때에도 task를 수행시킵니다.
(아래 예시를 보면 toggle 할 때마다 task가 수행됩니다.)
struct TaskView: View { | |
@State var flag = false | |
var body: some View { | |
Button("Toggle") { | |
flag.toggle() | |
} | |
.task(id: flag) { | |
print("[OnAppearView] start onAppear") | |
do { | |
try await Task.sleep(for: .seconds(2)) | |
} catch { | |
} | |
print("[OnAppearView] finish onAppear") | |
} | |
} | |
} |

그리고 만약 task가 완료되기 전에 값이 바뀌게 된 경우, 기존 task를 cancel 시키고 새로운 task를 수행합니다.

## onDisappear
View가 사라지고 난 이후에 action을 수행하는 함수입니다.
(예시는 생략할게요..ㅎ..)
# 여러 가지 궁금한 거 알아보기
이제부터, 개인적으로 궁금한 것들 직접 호출해 보면서 확인해 볼게요.
## onAppear와 task 중 먼저 호출되는 것은?
결론
- 판단할 수 없다.
- 테스트했을 땐 항상 onAppear가 먼저 호출되긴 하지만, onAppear가 먼저 호출된다고 장담할 수 없다.
struct ContentView: View { | |
var body: some View { | |
Text("Hello World") | |
.task { | |
print("[Content] task") | |
} | |
.onAppear { | |
print("[Content] onAppear") | |
} | |
} | |
} |

## Navigation 이동(push, pop)할 때 onAppear, onDisappear가 호출될까?
결론
- NavigationView 자체는 onAppear만 호출된다. (새로운 화면이 push 되어도 NavigationView의 onDisappear는 호출되지 않는다.) (만약 NavigationView 자체가 사라지는 경우에는 onDisappear가 호출된다.)
- 부모 View 위에 자식 View를 push 했을 때 자식 View의 onAppear가 호출되고 부모 View의 onDisappear가 호출된다.
- 자식 View가 pop 됐을 때, 부모 View의 onAppear가 호출되고 자식 View의 onDisappear가 호출된다.
struct ContentView: View { | |
var body: some View { | |
NavigationView { | |
VStack { | |
Text("Content View") | |
.onAppear { | |
print("[Content] onAppear") | |
} | |
.onDisappear { | |
print("[Content] onDisappear") | |
} | |
NavigationLink { | |
SubView() | |
} label: { | |
Text("Go To SubView") | |
} | |
} | |
} | |
.onAppear { | |
print("[NavigationView] onAppear") | |
} | |
.onDisappear { | |
print("[NavigationView] onDisappear") | |
} | |
} | |
} |
struct SubView: View { | |
var body: some View { | |
VStack { | |
Image(systemName: "globe") | |
.imageScale(.large) | |
.foregroundColor(.accentColor) | |
Text("Hello, world!") | |
} | |
.onAppear { | |
print("[SubView] onAppear") | |
} | |
.onDisappear { | |
print("[SubView] onDisappear") | |
} | |
} | |
} |

## TabBar 이동했을 때 onAppear, onDisappear가 호출될까?
결론
- TabBar 터치로 인해 화면 이동했을 때 이전 View에선 onDisappear가, 보여질 View에선 onAppear가 호출된다.
struct ContentView: View { | |
var body: some View { | |
TabView { | |
SubView1() | |
.tabItem { | |
Label("SubView1", systemImage: "house") | |
} | |
SubView2() | |
.tabItem { | |
Label("SubView2", systemImage: "person") | |
} | |
} | |
} | |
} |
struct SubView1: View { | |
var body: some View { | |
Text("SubView1") | |
.onAppear { | |
print("[SubView1] onAppear") | |
} | |
.onDisappear { | |
print("[SubView1] onDisappear") | |
} | |
} | |
} |
struct SubView2: View { | |
var body: some View { | |
Text("SubView2") | |
.onAppear { | |
print("[SubView2] onAppear") | |
} | |
.onDisappear { | |
print("[SubView2] onDisappear") | |
} | |
} | |
} |

## fullScreenCover로 View를 덮으면 onDisappear가 호출될까?
결론
- fullScreenCover로 View가 덮여도 onDisappear는 호출되지 않는다.
- fullScreenCover로 인해 View가 다시 보여도 onAppear는 호출되지 않는다.
struct ContentView: View { | |
@State var showAlert = false | |
var body: some View { | |
VStack { | |
Text("ContentView") | |
Button("Show Alert") { | |
showAlert = true | |
} | |
} | |
.onAppear { | |
print("[ContentView] onAppear") | |
} | |
.onDisappear { | |
print("[ContentView] onDisappear") | |
} | |
.fullScreenCover(isPresented: $showAlert) { | |
Color.red | |
.onTapGesture { | |
showAlert = false | |
} | |
} | |
} | |
} |

## UIKit의 viewWillAppear와 SwiftUI의 onAppear를 같이 사용하면 어떻게 될까?
결론
- viewDidLoad가 제일 먼저 호출된다.
- viewWillAppear, onAppear 모두 호출된다. (순서는 보장할 수 없을 듯..)
struct ContentView: View { | |
var body: some View { | |
CenteredLabelView() | |
.onAppear { | |
print("[SwiftUI] onAppear") | |
} | |
} | |
} |
struct CenteredLabelView: UIViewControllerRepresentable { | |
func makeUIViewController(context: Context) -> UIViewController { | |
return CenteredLabelViewController() | |
} | |
func updateUIViewController(_ uiViewController: UIViewController, context: Context) { | |
} | |
} | |
class CenteredLabelViewController: UIViewController { | |
override func viewWillAppear(_ animated: Bool) { | |
print("[UIKit] viewWillAppear") | |
super.viewWillAppear(animated) | |
} | |
override func viewDidLoad() { | |
print("[UIKit] viewDidLoad") | |
super.viewDidLoad() | |
let label = UILabel() | |
label.text = "This is ViewController" | |
label.textAlignment = .center | |
self.view.addSubview(label) | |
label.translatesAutoresizingMaskIntoConstraints = false | |
NSLayoutConstraint.activate([ | |
label.centerXAnchor.constraint(equalTo: self.view.centerXAnchor), | |
label.centerYAnchor.constraint(equalTo: self.view.centerYAnchor) | |
]) | |
} | |
} |

# 참고
- https://developer.apple.com/documentation/swiftui/view/onappear(perform:)
- https://developer.apple.com/documentation/swiftui/view/task(priority:_:)
- https://developer.apple.com/documentation/swiftui/view/ondisappear(perform:)
이번 글은 여기서 마무리.
'SwiftUI' 카테고리의 다른 글
TCA(1) : ReducerProtocol, StoreOf, WithViewStore (0) | 2023.06.13 |
---|---|
TCA(0) : The Composable Architecture 개요 (0) | 2023.06.07 |
overlay + matchedGeometryEffect로 Hero Anmiation 비슷하게 만들기 (0) | 2023.05.24 |
커스텀 LabelStyle (0) | 2023.03.28 |
RoundedCornerStyle (0) | 2023.03.26 |