
2023.10 기준 SwiftUI에서 life cycle updates modifier로 지정된 것은 4개이다. 여기서 마지막 task는 task만 따로 한번 파보기로 하고 onAppear, onDisappear, task 알아봅시다 다 드루와~
SwiftUI에서 화면을 전환하는 방법에는 여러가지가 있다. UIKit에서 ViewController를 사용했을 때는 화면 전환되는 View를 ViewController로 명확히 구분할 수 있는 단위가 있었지만 SwiftUI 에서는 view라는 단위가 명확하게 구분되어 있기 보다는 View가 View를 감싸고 있거나 다른 View의 일부분이 될 수도 있기 때문에 이를 고려해서 알아봐야 한다.
우선 각 modifire을 간단히 살펴보면
onAppear
func onAppear(perform action: (() -> Void)? = nil) -> some View
action 파라미터로 클로저를 전달한다. default 값이 nil이기 때문에 따로 action을 전달해 주지 않아도 된다.(action을 nil로 전달하는 onAppear()의 쓰임새가 따로 있는걸까?ㅎㅎㅎ)
특정 View의 유형에 따라 호출되는 정확한 시점이 다르지만 처음으로 렌더링 된 프레임이 나타나기 전에 액션 클로저가 완료된다고 한다.

ParentView에서 ChildView로 이동했을 때 ChildView가 나타나기 전에 onAppear에 action 이 호출되는걸 확인할 수 있다.
onAppear는 뷰가 화면에 나타나기 이전에 action 클로저를 실행한다 라고 정리하면 될것 같다.
onDisappear
func onDisappear(perform action: (() -> Void)? = nil) -> some View
onDisappear 또한 onAppear와 같이 action 파라미터로 클로저를 전달하고 default값은 nil이다. onAppear와 다르게 화면에서 View가 사라지기 전까지 action 클로저가 실행되지 않는다.

ParentView에서 ChildView로 이동한 뒤 dismiss 시키면 화면에서 ChildView가 사라지고 난 뒤 onDiasappear의 action 클로져가 호출된다.
task
func task(
priority: TaskPriority = .userInitiated,
_ action: @escaping () async -> Void
) -> some View
task는 onAppear, onDisappear 와는 달리 priority라는 파라미터를 가지고 있고, action 은 async로 되어 있다. task 관련해서 따로 한번 다루기로 하고 life cycle 부분만 살펴 보면
A closure that SwiftUI calls as an asynchronous task before the view appears. SwiftUI will automatically cancel the task at some point after the view disappears before the action completes. |
view appears 이전에 async task가 실행된다고 적혀 있어서 onAppear 이전에 호출되는 줄 알앗는데 한번도 onAppear 이전에 호출된거 못봤움 (혹시 appear의 개념이 onAppear와는 다른걸까 또륵... ㅠ)

뷰가 화면에 다 나타나고 난 뒤에 task의 action이 실행된다. 도대체 before the view appears 의 의미가 뭘까 🤔

테스트를 위해 작성한 코드의 구조를 살펴보면,
가장 상위뷰를 ContentView라 할 때, ContentView는 NavigationStack으로 ChildView 이동이 가능하고, ZStack으로 ContentView 전체를 덮을 수 있는 ChildView도 존재한다.
즉 NavigationStack으로 밀어 넣는 뷰랑 화면에 가려졌다 나타나는 뷰랑 차이가 있을까 알아보는 것. 생각해보니 탭바도 해야 한다.
body property & init
View 구조체도 View 프로토콜을 따르기 때문에 이닛 시점에서 appear을 붙여줄 수 있음
@main
struct LifeCycleTestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.task {
print("💩 ContentView 이닛: task Modifier before onAppear 🗂")
}
.onAppear {
print("💩 ContentView 이닛: ContentView onAppear 👻")
}
.task {
print("💩 ContentView 이닛: task Modifier after onAppear 🗂")
}
}
}
}
최상위 뷰는 ContentView이고 이닛 시점에서 onAppear와 task modifier을 사용할 수 있다.
struct ContentView: View {
@State private var isChildViewShow: Bool = false
var body: some View {
let _ = print("ContentView 바디프로퍼티 ")
NavigationStack {
let _ = print("NavigationStack 바디프로퍼티")
ZStack {
ParentView(isChildViewShow: $isChildViewShow)
if isChildViewShow {
ChildView(isSelfShow: $isChildViewShow)
}
} //: ZStack
}
.task {
print("🧡 ContentView task modifier before onAppear 🗂")
}
.onAppear {
print("🧡 ContentView NavigationStack onAppear 👻")
}
.task {
print("🧡 ContentView task modifier after onAppear 🗂")
}
.onDisappear {
print("🧡 ContentView disappear 🌪")
}
}
}
ContentView의 body property 내부의 가장 최상위 뷰인 NavigationonStack의 Appear와
ContentView의 이닛 시점의 onAppear 중에 어떤게 먼저 호출 될까?
var body: some View {
let _ = print("ContentView 바디프로퍼티 ")
}
우선 body 프로퍼티 내부에서 print문을 사용해서 body가 그려질 때 콘솔에 찍어볼 수 있다.
ContentView 바디프로퍼티
NavigationStack 바디프로퍼티
VStack 바디프로퍼티
🧡 ContentView NavigationStack onAppear 👻
💩 ContentView 이닛: ContentView onAppear 👻
콘솔에서 확인할 수 있는 것은
body 프로퍼티가 먼저 그려지고 그 후에 onAppear가 호출된다
🧡 ContentView NavigationStack onAppear 👻
💩 ContentView 이닛: ContentView onAppear 👻
그리고 볼 것이 바로 가장 아래 두 줄! 보면 ContentView의 최상위 뷰의 onAppear modifier가 호출되고 그 후에 이닛 시점에 걸어둔 onAppear가 호출 된다. body 프로퍼티 내부의 가장 최상위 뷰의 onAppear가 호출되고 그 후에 해당 body 프로퍼티가 포함된 View struct의 이닛 시점에 걸어둔 onAppear가 호출된다. (물론 이 모든건 공식적인 내용이 아니기 때문에 언제든지 바뀔 수 있다고 생각함)
NavigationLink {
ChildView(isSelfShow: $isChildViewShow)
.onAppear {
print("ChildView 이닛: ChildView onAppear")
}
} label: {
Text("🛼 go to ChildView with NavigationLink")
}
ContentView가 앱의 최상위 뷰라 특수한 경우로 호출된 걸 수도 있으니 한 개더 확인해 보면,
ParentView 내부에는 NavigationLink를 이용해서 ChildView로 이동하는 코드가 있다.
struct ChildView: View {
@Binding var isSelfShow: Bool
var body: some View {
let _ = print("칠드 바디프로퍼티")
VStack {
// 생략
}
.background(.yellow)
.onAppear {
print("🐤 ChildView onAppear 👻")
}
}
}
ChildView 내부에도 body 프로퍼티의 최상위 뷰에 onAppear를 걸어 두었다.
칠드 바디프로퍼티
🐤 ChildView onAppear 👻
ChildView 이닛: ChildView onAppear
ChildView의 body property가 먼저 그려지고 그 후 ChildView의 최상위 뷰의 onAppear가 호출된다. 그 후에 ChildView의 이닛 시점에 걸어둔 onAppear가 호출된다.
init Vs. onAppear
struct ParentView: View {
@Binding var isChildViewShow: Bool
init(isChildViewShow: Binding<Bool>) {
self._isChildViewShow = isChildViewShow
print("ParentView init!!!")
}
var body: some View {
VStack {
let _ = print("ParentView 바디프로퍼티 💪")
NavigationLink {
ChildView(isSelfShow: $isChildViewShow)
} label: {
Text("🛼 go to ChildView with NavigationLink")
}
Button {
print("🛼 go to ChildView with ZStack")
isChildViewShow = true
} label: {
Text("🛼 go to ChildView with ZStack")
}
.buttonStyle(.borderedProminent)
}
.onAppear {
print("🐔 ParentView onAppear 👻")
}
}
}
모든 View에다가 init을 붙여두었는데 ParentView만 확인해봅시다.
ParentView init!!!
ParentView 바디프로퍼티 💪
🐔 ParentView onAppear 👻
우선 init이 가장 먼저 실행된다. 그 후 body property가 그려지고, body의 최상위 뷰의 onAppear가 호출된다.
그 후 NavigationLink를 통해서 ChildView로 이동한 뒤 다시 Parent로 돌아오면 init과 onAppear의 차이를 알 수 있다.
🐔 ParentView onAppear 👻
onAppear는 호출되지만 init은 호출되지 않는다. init은 뷰의 초기화 시점에 한 번 호출되며 onAppear는 View가 화면에 보여질 때 호출된다. NavigationStack에 의해 화면에 나타나면 onAppear가 호출된다.
여기서 SwiftUI 방심하면 안된다. View가 화면에 나타날 때 꼭 호출되겠지 하고 onAppear에 맡겨 버리는 순간 예상치 못한 동작에 당황할 수 있다. NavigationStack이 아닌 ZStack을 사용해서 ChildView로 ParentView를 완전히 가린 뒤 ChildView를 화면에서 제거하면 어떻게 될까?
Button {
print("🛼 go to ChildView with ZStack")
isChildViewShow = true
} label: {
Text("🛼 go to ChildView with ZStack")
}
.buttonStyle(.borderedProminent)
ParentView에는 버튼이 있는데 이 버튼을 누르면 ContentView와 Binding 된 값인 isChildViewShow가 true가 된다.
NavigationStack {
ZStack {
ParentView(isChildViewShow: $isChildViewShow)
if isChildViewShow {
ChildView(isSelfShow: $isChildViewShow)
}
} //: ZStack
}
ContentView에는 isChildViewShow가 true가 되면 ZStack 전체를 덮는 ChildView가 화면에 나타나게 된다.


ParentView init!!!
ParentView 바디프로퍼티 💪
🐔 ParentView onAppear 👻
-------- ChildView로 이동 --------
🛼 go to ChildView with ZStack
ParentView init!!!
ChildView init!!!
ParentView 바디프로퍼티 💪
ChildView init!!!
칠드 바디프로퍼티 💪
🐤 ChildView onAppear 👻
-------- ParentView 이동 --------
ParentView init!!!
ParentView 바디프로퍼티 💪
ChildView init!!!
칠드 바디프로퍼티 💪
NavigationStack이 아니라 이미 나타난 화면에 새로운 화면을 그려주기 때문에 onAppear는 한 번 호출되고, initializer는 화면에서 가려질 때, 화면에 나타날 때 호출된다. 정확히 말하면 dependency에 의해 body property가 새로 그려지기 때문인데
@State private var isChildViewShow: Bool = false
ContentView에 정의된 isChildViewShow의 값이 바뀌면서 ContentView의 body 프로퍼티를 새로 그리고, ParentView, ChildView 또한 isChildViewShow State와 Bind 되어 있어 body 프로퍼티가 업데이트 된다.
이해가 되지 않는건, ParentView의 id가 같기 때문에 init 또한 한번만 호출될 것으로 예상했는데 body property가 업데이트 되면서 이니셜라이저도 함께 호출되었다. (이유 아는 사람 있나요... 제발)
task and onAppear
struct ContentView: View {
@State private var isChildViewShow: Bool = false
init() {
print("ContentView init!!!")
}
var body: some View {
let _ = print("ContentView 바디프로퍼티 💪")
NavigationStack {
ZStack {
ParentView(isChildViewShow: $isChildViewShow)
if isChildViewShow {
ChildView(isSelfShow: $isChildViewShow)
}
} //: ZStack
}
.task {
print("🧡 ContentView task modifier before onAppear 🗂")
}
.onAppear {
print("🧡 ContentView NavigationStack onAppear 👻")
}
.task {
print("🧡 ContentView task modifier after onAppear 🗂")
}
.onDisappear {
print("🧡 ContentView disappear 🌪")
}
}
}
ContentView만 놓고 비교해 보면 아래와 같은 순서로 호출된다.
ContentView init!!!
ContentView 바디프로퍼티 💪
🧡 ContentView NavigationStack onAppear 👻
🧡 ContentView task modifier before onAppear 🗂
🧡 ContentView task modifier after onAppear 🗂
ContentView의 이니셜라이저가 호출되고 바디프로퍼티가 그려진다. body 프로퍼티의 최상위 뷰에는 task1, onAppear, task2, onDisappear 순으로 modifier가 붙어 있다.
가장 먼저 알 수 있는 것은 modifier가 붙은 순서 상관 없이 onAppear가 호출된 뒤 task가 호출된다. 여러 task가 붙어 있을 때에는 가장 위에 적힌 task가 먼저 실행되고, 가장 나중에 붙은 task가 마지막으로 실행된다.
공식적인 내용은 아니라 이 순서가 언제든 변할 수 있겠지만 여러번 실행해도 onAppear가 먼저 호출되고 위에서 아래 task 순으로 호출된다.
그렇다면 이제 ParentView를 통해 이닛 시점과 body property 내부 modifier을 비교해 보자
struct ContentView: View {
@State private var isChildViewShow: Bool = false
var body: some View {
NavigationStack {
ZStack {
ParentView(isChildViewShow: $isChildViewShow)
.task {
print("-> 🐔 ParentView task before onAppear 🗂")
}
.onAppear {
print("-> 🐔 ParentView onAppear 👻")
}
.task {
print("-> 🐔 ParentView task after onAppear 🗂")
}
.onDisappear {
print("-> 🐔 ParentView onDisappear 🌪")
}
if isChildViewShow {
ChildView(isSelfShow: $isChildViewShow)
}
} //: ZStack
}
}
}
struct ParentView: View {
@Binding var isChildViewShow: Bool
init(isChildViewShow: Binding<Bool>) {
self._isChildViewShow = isChildViewShow
print("ParentView init!!!")
}
var body: some View {
let _ = print("ParentView 바디프로퍼티 💪")
VStack {
}
.task {
print("🐔 ParentView task before onAppear 🗂")
}
.onAppear {
print("🐔 ParentView onAppear 👻")
}
.task {
print("🐔 ParentView task after onAppear 🗂")
}
.onDisappear {
print("🐔 ParentView onDisappear 🌪")
}
}
}
ParentView init!!!
ParentView 바디프로퍼티 💪
🐔 ParentView onAppear 👻
-> 🐔 ParentView onAppear 👻
🐔 ParentView task before onAppear 🗂
🐔 ParentView task after onAppear 🗂
-> 🐔 ParentView task before onAppear 🗂
-> 🐔 ParentView task after onAppear 🗂
순서를 보면 이닛시점에 붙은 modifier보다 body 프로퍼티의 최상위 뷰에서 호출된 modifier가 먼저 실행된다.
우선 순위 🔽 | 우선 순위 🔼 |
ParentView(isChildViewShow: $isChildViewShow) .onAppear {} .task {} |
var body: some View { VStack {} .onAppear {} .task {} } |
init 시점 또한 View이기 때문에 View life cycle에 사용할 수 있는 modifier을 사용할 수 있는데, body property 내부와 이닛 시점 두 부분을 혼용해서 쓰는 것 보다 하나를 정해 놓고 사용하는게 좋을 듯 하다.
개인적으로는 바디 프로퍼티의 최상위 뷰에서 onAppear나 task를 호출하는게 좋다고 생각한다. 왜냐하면 해당 뷰에서 사용할 비동기 로직이나 appear 시 호출되어야 하는 로직을 View struct 내부에서 함께 정의할 수 있기 때문이다. 만약 이닛 시점에서 onAppear나 task modifier을 사용한다면 중복되는 코드가 생기게 된다. 예를 들어서 ParentView를 하나의 화면에서 사용하고 이닛 시점에서 onAppear와 task를 정의했다면 다른 화면에서 ParentView를 사용할 때 또 다시 onAppear와 task를 정의해야 하기 때문이다.
onDisappear
struct ParentView: View {
@Binding var isChildViewShow: Bool
var body: some View {
VStack {
NavigationLink {
ChildView(isSelfShow: $isChildViewShow)
} label: {
Text("🛼 go to ChildView with NavigationLink")
}
Button {
print("🛼 go to ChildView with ZStack")
isChildViewShow = true
} label: {
Text("🛼 go to ChildView with ZStack")
}
.buttonStyle(.borderedProminent)
}
.onDisappear {
print("🐔 ParentView onDisappear 🌪")
}
}
}
struct ChildView: View {
@Binding var isSelfShow: Bool
var body: some View {
VStack {
Spacer()
Text("Child View")
.frame(maxWidth: .infinity)
Button {
isSelfShow = false
} label: {
Text("go to ContentView dismiss ZStack")
}
.buttonStyle(.borderedProminent)
Spacer()
}
.background(.yellow)
.onDisappear {
print("🐤 ChildView disappear 🌪")
}
}
}
NavigationStack으로 ChildView를 쌓았다가 pop 하면 다음과 같이 호출된다.
ChildView 이닛: ChildView onAppear
🐔 ParentView onDisappear 🌪
🐤 ChildView disappear 🌪
NavigationStack으로 ChildView가 들어오면 ParentView는 onDisappear 된다. 즉 NavigationStack에서 화면에 보이지 않게 되면 onDisappear가 호출된다.
ChildView 또한 NavigationStack에서 제거되면 onDisappear를 호출한다.
onAppear modifier을 사용하면 onDisappear의 시점을 더 명확히 알 수 있다.
🐔 ParentView onAppear 👻
🐤 ChildView onAppear 👻
🐔 ParentView onDisappear 🌪
🐔 ParentView onAppear 👻
🐤 ChildView disappear 🌪
처음 화면에 ParentView가 나타날 때 onAppear가 호출되고 NavigationStack으로 ChildView로 이동하면 ChildView의 onAppear가 호출된 뒤 ParentView는 화면에서 보이지 않으므로 onDisappear가 호출된다. 그 후 pop을 통해 ChildView를 화면에서 안보이게 하면 ParentView의 onAppear가 호출되고 ChildView의 onDisappear가 호출된다.
NavigationStack {
ZStack {
ParentView(isChildViewShow: $isChildViewShow)
if isChildViewShow {
ChildView(isSelfShow: $isChildViewShow)
}
} //: ZStack
}
이제 ZStack을 사용해서 ChildView로 ParentView를 완전히 가리면 어떻게 될까?
🐔 ParentView onAppear 👻
🛼 go to ChildView with ZStack
🐤 ChildView onAppear 👻
🐤 ChildView disappear 🌪
ParentView는 이미 화면에 나타난 상태이기 때문에 onAppear는 한번만 호출된다. ChildView는 화면에 나타나면 onAppear 를 호출하고 화면에서 사라지는 순간 onDisappear를 호출한다. Navigation과 화면에 단순 표시 및 제거할 때의 modifier 호출이 다르기 때문에 주의해서 사용해야 한다.
끝이 아니다 이제 TabView를 테스트 해보자 👏😌
TabView
struct TabBarTest: View {
var body: some View {
TabView {
FirstView()
.tabItem {
Image(systemName: "01.circle.fill")
Text("1")
}
SecondView()
.tabItem {
Image(systemName: "02.circle.fill")
Text("2")
}
ThirdView()
.tabItem {
Image(systemName: "03.circle.fill")
Text("3")
}
}
}
}
struct FirstView: View {
var body: some View {
Text("First")
.task {
print("1. before task")
}
.onAppear {
print("1. appear")
}
.task {
print("1. after task")
}
.onDisappear {
print("1. disappear")
}
}
}
각 탭에 해당 하는 뷰에는 task 두개와 appear, disappear modifier을 호출하도록 해뒀다. 예상으로는 1, 2, 3 번째 탭에 해당하는 모든 View의 appear가 호출될 것 같았는데 아니다!
1. appear
1. before task
1. after task
우선 처음에 화면에 탭뷰가 나타나면 첫 번째 탭의 appear와 task만 호출된다.
2. appear
1. disappear
2. before task
2. after task
이후 2번 탭을 누르면 2번 view의 appear가 호출되고 첫 번째 탭의 1번 뷰는 disappear를 호출한다. 화면에 다른 뷰가 appear 되면 -> 기존의 뷰가 disappear 되는 순서는 앞에서 확인한 것과 동일하다. 이후 2번 째 뷰의 task가 순서대로 호출된다.
3. appear
3. before task
3. after task
2. disappear
이번에는 2번 탭에서 3번 탭으로 이동할 때인데, 2번 뷰의 disappear 보다 3번 뷰의 task가 먼저 실행되었다. task와 다른 뷰에 정의된 disappear 간의 순서는 정해지지 않고 랜덤하게 호출되는 것으로 보인다.
1. appear
1. before task
1. after task
3. disappear
다시 3번에서 1번 탭으로 돌아오면 1번의 onAppear가 호출되고 사라진 3번 뷰는 disappear를 호출한다.
TabView 또한 NavigationStack과 마찬가지로 화면에 나타나면 onAppear를 호출하고 화면에서 사라지면 onDisappear를 호출한다. task 또한 화면에 나타날 때마다 호출하는 걸 알 수 있다.
struct SecondView: View {
init() {
print("2. init")
}
var body: some View {
let _ = print("2. body property")
Text("Second")
.task {
print("2. before task")
}
.onAppear {
print("2. appear")
}
.task {
print("2. after task")
}
.onDisappear {
print("2. disappear")
}
}
}
이번에는 각 탭에 해당되는 모든 뷰에 initializer를 추가하고 로그를 찍어주었다. body property가 그려지는 시점에도 로그를 찍었다.
1. init
2. init
3. init
1. body property
1. appear
1. before task
1. after task
예상한데로 init은 모두 호출되는 것을 알 수 있는데
struct TabBarTest: View {
var body: some View {
TabView {
FirstView()
.tabItem {
Image(systemName: "01.circle.fill")
Text("1")
}
SecondView()
.tabItem {
Image(systemName: "02.circle.fill")
Text("2")
}
ThirdView()
.tabItem {
Image(systemName: "03.circle.fill")
Text("3")
}
}
}
}
TabView를 그릴 때 init 되기 때문이다. 각 탭의 뷰가 화면에 나타날 때 비동기 코드를 실행해야 한다면 init이 아니라 task가 적당하다 (사실 그러라고 애플이 만들어 둔 modifier이기도 하다)
1. init
2. init
3. init
1. body property
1. appear
1. before task
1. after task
다시 살펴보면 appear 되기 이전에 1번 뷰의 body를 그린다. 1번 탭에서 2번 탭으로 이동하면
2. body property
2. appear
1. disappear
2. before task
2. after task
2번 또한 body property를 그리고 appear가 호출된다. 여기서 다시 1번 탭을 누르면
1. appear
2. disappear
1. before task
1. after task
1번의 body property 가 호출되지 않는다. 그 이유는 TabView에서 지정해 준 각 탭의 뷰가 이미 암시적 ID를 부여 받고, 업데이트 되는 dependency도 없기 때문에 body property를 새로 그려주지 않기 때문이다. 즉 탭을 눌러 뷰를 이동할 때 보여지는 뷰들은 한 번 그려지고 그 그려진 뷰를 다시 보여주고 있는 것이다. (만약 1번 탭에서 2번 탭으로, 다시 1번 탭으로 돌아온다고 했을 때 이전의 1번 뷰와 지금의 1번 뷰는 같은 뷰이다. SwiftUI에서 구조적으로 같은 ID를 가진 뷰로 인식하기 때문에 이전 뷰를 즉시 파괴하고 새로 그리는 것이 아니라, 이전 뷰를 다시 보여주고 있는 것이다)
+) TabView 자체에 걸어둔 onAppear, task, onDisappear 중 onAppear와 task는 화면에 보일 때 각각 한 번 호출된다.
custom tabbar를 사용하면 다른 동작을 하겠지만 이 정도 살펴봤으면 충분한 것 같다.
여기서 끝이 아니다! 하나 더 남았다 이제 진짜 마지막!! 💪🙃
modal
struct ModalTestView: View {
@State private var isShowModal = false
var body: some View {
VStack {
Button("모달") {
isShowModal.toggle()
}
}
.task {
print("🐔 ParentView task before onAppear 🗂")
}
.onAppear {
print("🐔 ParentView onAppear 👻")
}
.task {
print("🐔 ParentView task after onAppear 🗂")
}
.onDisappear {
print("🐔 ParentView onDisappear 🌪")
}
.sheet(isPresented: $isShowModal) {
ModalChildView()
}
}
}
struct ModalChildView: View {
@Environment(\.dismiss) var dismiss
var body: some View {
VStack {
Button("dismiss 버튼") {
dismiss()
}
}
.task {
print("🐤 ChildView task before onAppear 🗂")
}
.onAppear {
print("🐤 ChildView onAppear 👻")
}
.task {
print("🐤 ChildView task after onAppear 🗂")
}
.onDisappear {
print("🐤 ChildView disappear 🌪")
}
}
}
버튼 눌러서 ModalChildView를 화면에 띄우면
🐔 ParentView onAppear 👻
🐔 ParentView task after onAppear 🗂
🐔 ParentView task before onAppear 🗂
🐤 ChildView onAppear 👻
🐤 ChildView task before onAppear 🗂
🐤 ChildView task after onAppear 🗂
처음 세 줄은 처음 실행했을 때 ParentView가 화면에 보이는 순간 호출되는 modifier이다. 이후 버튼을 눌러 모달을 띄우면 아래 세 줄이 뜬다.
🐤 ChildView disappear 🌪
이후 버튼을 눌러 모달을 닫으면 childview의 onDisappear만 호출되고, ParentView의 onAppear는 호출되지 않는다. 스와이프 제스처도 마찬가지!
1. sheet으로 모달을 띄우면 모달을 띄운 ParentView는 onDisappear가 호출되지 않는다.
2. sheet을 닫아도 ParentView의 onAppear는 호출되지 않는다.
.fullScreenCover(isPresented: $isShowModal, content: {
ModalChildView()
})
.sheet modifier을 .fullScreenCover modifier로 바꿔도 동작은 동일하다.
.sheet(isPresented: $isShowModal) {
print("ParentView로 돌아옴")
} content: {
ModalChildView()
}
만약 ChildView에서 ParentView로 돌아왔을 때 실행할 코드가 있다면 onDismiss 파라미터를 사용할 수 있다. sheet, fullScreenCover modifier 모두 가지고 있는 파라미터이다.
ParentView로 돌아옴
🐤 ChildView disappear 🌪
ChildView의 onDisappear가 호출되기 이전에 onDismiss가 호출된다.
.sheet(isPresented: $isShowModal, content: {
ModalChildView()
.presentationDetents([.medium])
})
혹시 절반만 가리면 어떻게 될까 했는데 sheet, fullScreenCover에서 동작한 것과 동일하게 동작한다.
결론
life cycle modifier | 특징 |
onAppear | - 뷰가 화면에 나타나기 이전에 실행 - NavigationStack : 화면에 최초 나타날 때 호출, ChildView가 push되고 pop 될때 호출 - ZStack : 처음 화면에 나타날 때 한번만 호출, ChildView가 화면을 전체 다 가렸다가 사라져도 onAppear는 호출되지 않음 -TabView: 탭을 눌러 화면에서 나타날 때 마다 - sheet: 모달로 띄워졌을 때 호출, 모달을 띄우는 View라면 modal을 닫아도 호출되지 않음 |
onDisappear | - 화면에서 View가 사라지고 난 후에 실행 - NavigationStack에서 제거될 때 - TabView에서 탭을 바꿔 화면에서 사라질 때 마다 - sheet: 모달로 띄워졌다가 사라질 때, 모달을 띄우는 View라면 모달에 의해 화면에 가려져도 호출되지 않음 |
task | - 비공식적으로 onAppear 뒤에 호출됨 - 여러개의 task가 있는 경우 위에서 아래 순으로 실행 |
init -> body property 그려짐 -> onAppear 호출
(init과 body property update의 경우 depdency에 의해 호출될 수도, 되지 않을 수도 있기 때문에 주의)
이 정도면,,, 사용할 때마다 찍어보는게 가장 베스트이지 않을까 싶기도 하다 또륵...🥲

'iOS 🍎 > SwiftUI' 카테고리의 다른 글
[SwiftUI] Custom Toggle (0) | 2023.11.13 |
---|---|
[SwiftUI] Shape path로 Tooltip 그리기 (0) | 2023.11.06 |
[SwiftUI] VStack 과 LazyVStack 뭐가 다른 걸까? (1) | 2023.10.08 |
[SwiftUI] NavigationView (1) | 2023.09.22 |
[SwiftUI] 가로모드, 세로모드 지원하기 with @ViewBuilder (0) | 2023.09.20 |

2023.10 기준 SwiftUI에서 life cycle updates modifier로 지정된 것은 4개이다. 여기서 마지막 task는 task만 따로 한번 파보기로 하고 onAppear, onDisappear, task 알아봅시다 다 드루와~
SwiftUI에서 화면을 전환하는 방법에는 여러가지가 있다. UIKit에서 ViewController를 사용했을 때는 화면 전환되는 View를 ViewController로 명확히 구분할 수 있는 단위가 있었지만 SwiftUI 에서는 view라는 단위가 명확하게 구분되어 있기 보다는 View가 View를 감싸고 있거나 다른 View의 일부분이 될 수도 있기 때문에 이를 고려해서 알아봐야 한다.
우선 각 modifire을 간단히 살펴보면
onAppear
func onAppear(perform action: (() -> Void)? = nil) -> some View
action 파라미터로 클로저를 전달한다. default 값이 nil이기 때문에 따로 action을 전달해 주지 않아도 된다.(action을 nil로 전달하는 onAppear()의 쓰임새가 따로 있는걸까?ㅎㅎㅎ)
특정 View의 유형에 따라 호출되는 정확한 시점이 다르지만 처음으로 렌더링 된 프레임이 나타나기 전에 액션 클로저가 완료된다고 한다.

ParentView에서 ChildView로 이동했을 때 ChildView가 나타나기 전에 onAppear에 action 이 호출되는걸 확인할 수 있다.
onAppear는 뷰가 화면에 나타나기 이전에 action 클로저를 실행한다 라고 정리하면 될것 같다.
onDisappear
func onDisappear(perform action: (() -> Void)? = nil) -> some View
onDisappear 또한 onAppear와 같이 action 파라미터로 클로저를 전달하고 default값은 nil이다. onAppear와 다르게 화면에서 View가 사라지기 전까지 action 클로저가 실행되지 않는다.

ParentView에서 ChildView로 이동한 뒤 dismiss 시키면 화면에서 ChildView가 사라지고 난 뒤 onDiasappear의 action 클로져가 호출된다.
task
func task(
priority: TaskPriority = .userInitiated,
_ action: @escaping () async -> Void
) -> some View
task는 onAppear, onDisappear 와는 달리 priority라는 파라미터를 가지고 있고, action 은 async로 되어 있다. task 관련해서 따로 한번 다루기로 하고 life cycle 부분만 살펴 보면
A closure that SwiftUI calls as an asynchronous task before the view appears. SwiftUI will automatically cancel the task at some point after the view disappears before the action completes. |
view appears 이전에 async task가 실행된다고 적혀 있어서 onAppear 이전에 호출되는 줄 알앗는데 한번도 onAppear 이전에 호출된거 못봤움 (혹시 appear의 개념이 onAppear와는 다른걸까 또륵... ㅠ)

뷰가 화면에 다 나타나고 난 뒤에 task의 action이 실행된다. 도대체 before the view appears 의 의미가 뭘까 🤔

테스트를 위해 작성한 코드의 구조를 살펴보면,
가장 상위뷰를 ContentView라 할 때, ContentView는 NavigationStack으로 ChildView 이동이 가능하고, ZStack으로 ContentView 전체를 덮을 수 있는 ChildView도 존재한다.
즉 NavigationStack으로 밀어 넣는 뷰랑 화면에 가려졌다 나타나는 뷰랑 차이가 있을까 알아보는 것. 생각해보니 탭바도 해야 한다.
body property & init
View 구조체도 View 프로토콜을 따르기 때문에 이닛 시점에서 appear을 붙여줄 수 있음
@main
struct LifeCycleTestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.task {
print("💩 ContentView 이닛: task Modifier before onAppear 🗂")
}
.onAppear {
print("💩 ContentView 이닛: ContentView onAppear 👻")
}
.task {
print("💩 ContentView 이닛: task Modifier after onAppear 🗂")
}
}
}
}
최상위 뷰는 ContentView이고 이닛 시점에서 onAppear와 task modifier을 사용할 수 있다.
struct ContentView: View {
@State private var isChildViewShow: Bool = false
var body: some View {
let _ = print("ContentView 바디프로퍼티 ")
NavigationStack {
let _ = print("NavigationStack 바디프로퍼티")
ZStack {
ParentView(isChildViewShow: $isChildViewShow)
if isChildViewShow {
ChildView(isSelfShow: $isChildViewShow)
}
} //: ZStack
}
.task {
print("🧡 ContentView task modifier before onAppear 🗂")
}
.onAppear {
print("🧡 ContentView NavigationStack onAppear 👻")
}
.task {
print("🧡 ContentView task modifier after onAppear 🗂")
}
.onDisappear {
print("🧡 ContentView disappear 🌪")
}
}
}
ContentView의 body property 내부의 가장 최상위 뷰인 NavigationonStack의 Appear와
ContentView의 이닛 시점의 onAppear 중에 어떤게 먼저 호출 될까?
var body: some View {
let _ = print("ContentView 바디프로퍼티 ")
}
우선 body 프로퍼티 내부에서 print문을 사용해서 body가 그려질 때 콘솔에 찍어볼 수 있다.
ContentView 바디프로퍼티
NavigationStack 바디프로퍼티
VStack 바디프로퍼티
🧡 ContentView NavigationStack onAppear 👻
💩 ContentView 이닛: ContentView onAppear 👻
콘솔에서 확인할 수 있는 것은
body 프로퍼티가 먼저 그려지고 그 후에 onAppear가 호출된다
🧡 ContentView NavigationStack onAppear 👻
💩 ContentView 이닛: ContentView onAppear 👻
그리고 볼 것이 바로 가장 아래 두 줄! 보면 ContentView의 최상위 뷰의 onAppear modifier가 호출되고 그 후에 이닛 시점에 걸어둔 onAppear가 호출 된다. body 프로퍼티 내부의 가장 최상위 뷰의 onAppear가 호출되고 그 후에 해당 body 프로퍼티가 포함된 View struct의 이닛 시점에 걸어둔 onAppear가 호출된다. (물론 이 모든건 공식적인 내용이 아니기 때문에 언제든지 바뀔 수 있다고 생각함)
NavigationLink {
ChildView(isSelfShow: $isChildViewShow)
.onAppear {
print("ChildView 이닛: ChildView onAppear")
}
} label: {
Text("🛼 go to ChildView with NavigationLink")
}
ContentView가 앱의 최상위 뷰라 특수한 경우로 호출된 걸 수도 있으니 한 개더 확인해 보면,
ParentView 내부에는 NavigationLink를 이용해서 ChildView로 이동하는 코드가 있다.
struct ChildView: View {
@Binding var isSelfShow: Bool
var body: some View {
let _ = print("칠드 바디프로퍼티")
VStack {
// 생략
}
.background(.yellow)
.onAppear {
print("🐤 ChildView onAppear 👻")
}
}
}
ChildView 내부에도 body 프로퍼티의 최상위 뷰에 onAppear를 걸어 두었다.
칠드 바디프로퍼티
🐤 ChildView onAppear 👻
ChildView 이닛: ChildView onAppear
ChildView의 body property가 먼저 그려지고 그 후 ChildView의 최상위 뷰의 onAppear가 호출된다. 그 후에 ChildView의 이닛 시점에 걸어둔 onAppear가 호출된다.
init Vs. onAppear
struct ParentView: View {
@Binding var isChildViewShow: Bool
init(isChildViewShow: Binding<Bool>) {
self._isChildViewShow = isChildViewShow
print("ParentView init!!!")
}
var body: some View {
VStack {
let _ = print("ParentView 바디프로퍼티 💪")
NavigationLink {
ChildView(isSelfShow: $isChildViewShow)
} label: {
Text("🛼 go to ChildView with NavigationLink")
}
Button {
print("🛼 go to ChildView with ZStack")
isChildViewShow = true
} label: {
Text("🛼 go to ChildView with ZStack")
}
.buttonStyle(.borderedProminent)
}
.onAppear {
print("🐔 ParentView onAppear 👻")
}
}
}
모든 View에다가 init을 붙여두었는데 ParentView만 확인해봅시다.
ParentView init!!!
ParentView 바디프로퍼티 💪
🐔 ParentView onAppear 👻
우선 init이 가장 먼저 실행된다. 그 후 body property가 그려지고, body의 최상위 뷰의 onAppear가 호출된다.
그 후 NavigationLink를 통해서 ChildView로 이동한 뒤 다시 Parent로 돌아오면 init과 onAppear의 차이를 알 수 있다.
🐔 ParentView onAppear 👻
onAppear는 호출되지만 init은 호출되지 않는다. init은 뷰의 초기화 시점에 한 번 호출되며 onAppear는 View가 화면에 보여질 때 호출된다. NavigationStack에 의해 화면에 나타나면 onAppear가 호출된다.
여기서 SwiftUI 방심하면 안된다. View가 화면에 나타날 때 꼭 호출되겠지 하고 onAppear에 맡겨 버리는 순간 예상치 못한 동작에 당황할 수 있다. NavigationStack이 아닌 ZStack을 사용해서 ChildView로 ParentView를 완전히 가린 뒤 ChildView를 화면에서 제거하면 어떻게 될까?
Button {
print("🛼 go to ChildView with ZStack")
isChildViewShow = true
} label: {
Text("🛼 go to ChildView with ZStack")
}
.buttonStyle(.borderedProminent)
ParentView에는 버튼이 있는데 이 버튼을 누르면 ContentView와 Binding 된 값인 isChildViewShow가 true가 된다.
NavigationStack {
ZStack {
ParentView(isChildViewShow: $isChildViewShow)
if isChildViewShow {
ChildView(isSelfShow: $isChildViewShow)
}
} //: ZStack
}
ContentView에는 isChildViewShow가 true가 되면 ZStack 전체를 덮는 ChildView가 화면에 나타나게 된다.


ParentView init!!!
ParentView 바디프로퍼티 💪
🐔 ParentView onAppear 👻
-------- ChildView로 이동 --------
🛼 go to ChildView with ZStack
ParentView init!!!
ChildView init!!!
ParentView 바디프로퍼티 💪
ChildView init!!!
칠드 바디프로퍼티 💪
🐤 ChildView onAppear 👻
-------- ParentView 이동 --------
ParentView init!!!
ParentView 바디프로퍼티 💪
ChildView init!!!
칠드 바디프로퍼티 💪
NavigationStack이 아니라 이미 나타난 화면에 새로운 화면을 그려주기 때문에 onAppear는 한 번 호출되고, initializer는 화면에서 가려질 때, 화면에 나타날 때 호출된다. 정확히 말하면 dependency에 의해 body property가 새로 그려지기 때문인데
@State private var isChildViewShow: Bool = false
ContentView에 정의된 isChildViewShow의 값이 바뀌면서 ContentView의 body 프로퍼티를 새로 그리고, ParentView, ChildView 또한 isChildViewShow State와 Bind 되어 있어 body 프로퍼티가 업데이트 된다.
이해가 되지 않는건, ParentView의 id가 같기 때문에 init 또한 한번만 호출될 것으로 예상했는데 body property가 업데이트 되면서 이니셜라이저도 함께 호출되었다. (이유 아는 사람 있나요... 제발)
task and onAppear
struct ContentView: View {
@State private var isChildViewShow: Bool = false
init() {
print("ContentView init!!!")
}
var body: some View {
let _ = print("ContentView 바디프로퍼티 💪")
NavigationStack {
ZStack {
ParentView(isChildViewShow: $isChildViewShow)
if isChildViewShow {
ChildView(isSelfShow: $isChildViewShow)
}
} //: ZStack
}
.task {
print("🧡 ContentView task modifier before onAppear 🗂")
}
.onAppear {
print("🧡 ContentView NavigationStack onAppear 👻")
}
.task {
print("🧡 ContentView task modifier after onAppear 🗂")
}
.onDisappear {
print("🧡 ContentView disappear 🌪")
}
}
}
ContentView만 놓고 비교해 보면 아래와 같은 순서로 호출된다.
ContentView init!!!
ContentView 바디프로퍼티 💪
🧡 ContentView NavigationStack onAppear 👻
🧡 ContentView task modifier before onAppear 🗂
🧡 ContentView task modifier after onAppear 🗂
ContentView의 이니셜라이저가 호출되고 바디프로퍼티가 그려진다. body 프로퍼티의 최상위 뷰에는 task1, onAppear, task2, onDisappear 순으로 modifier가 붙어 있다.
가장 먼저 알 수 있는 것은 modifier가 붙은 순서 상관 없이 onAppear가 호출된 뒤 task가 호출된다. 여러 task가 붙어 있을 때에는 가장 위에 적힌 task가 먼저 실행되고, 가장 나중에 붙은 task가 마지막으로 실행된다.
공식적인 내용은 아니라 이 순서가 언제든 변할 수 있겠지만 여러번 실행해도 onAppear가 먼저 호출되고 위에서 아래 task 순으로 호출된다.
그렇다면 이제 ParentView를 통해 이닛 시점과 body property 내부 modifier을 비교해 보자
struct ContentView: View {
@State private var isChildViewShow: Bool = false
var body: some View {
NavigationStack {
ZStack {
ParentView(isChildViewShow: $isChildViewShow)
.task {
print("-> 🐔 ParentView task before onAppear 🗂")
}
.onAppear {
print("-> 🐔 ParentView onAppear 👻")
}
.task {
print("-> 🐔 ParentView task after onAppear 🗂")
}
.onDisappear {
print("-> 🐔 ParentView onDisappear 🌪")
}
if isChildViewShow {
ChildView(isSelfShow: $isChildViewShow)
}
} //: ZStack
}
}
}
struct ParentView: View {
@Binding var isChildViewShow: Bool
init(isChildViewShow: Binding<Bool>) {
self._isChildViewShow = isChildViewShow
print("ParentView init!!!")
}
var body: some View {
let _ = print("ParentView 바디프로퍼티 💪")
VStack {
}
.task {
print("🐔 ParentView task before onAppear 🗂")
}
.onAppear {
print("🐔 ParentView onAppear 👻")
}
.task {
print("🐔 ParentView task after onAppear 🗂")
}
.onDisappear {
print("🐔 ParentView onDisappear 🌪")
}
}
}
ParentView init!!!
ParentView 바디프로퍼티 💪
🐔 ParentView onAppear 👻
-> 🐔 ParentView onAppear 👻
🐔 ParentView task before onAppear 🗂
🐔 ParentView task after onAppear 🗂
-> 🐔 ParentView task before onAppear 🗂
-> 🐔 ParentView task after onAppear 🗂
순서를 보면 이닛시점에 붙은 modifier보다 body 프로퍼티의 최상위 뷰에서 호출된 modifier가 먼저 실행된다.
우선 순위 🔽 | 우선 순위 🔼 |
ParentView(isChildViewShow: $isChildViewShow) .onAppear {} .task {} |
var body: some View { VStack {} .onAppear {} .task {} } |
init 시점 또한 View이기 때문에 View life cycle에 사용할 수 있는 modifier을 사용할 수 있는데, body property 내부와 이닛 시점 두 부분을 혼용해서 쓰는 것 보다 하나를 정해 놓고 사용하는게 좋을 듯 하다.
개인적으로는 바디 프로퍼티의 최상위 뷰에서 onAppear나 task를 호출하는게 좋다고 생각한다. 왜냐하면 해당 뷰에서 사용할 비동기 로직이나 appear 시 호출되어야 하는 로직을 View struct 내부에서 함께 정의할 수 있기 때문이다. 만약 이닛 시점에서 onAppear나 task modifier을 사용한다면 중복되는 코드가 생기게 된다. 예를 들어서 ParentView를 하나의 화면에서 사용하고 이닛 시점에서 onAppear와 task를 정의했다면 다른 화면에서 ParentView를 사용할 때 또 다시 onAppear와 task를 정의해야 하기 때문이다.
onDisappear
struct ParentView: View {
@Binding var isChildViewShow: Bool
var body: some View {
VStack {
NavigationLink {
ChildView(isSelfShow: $isChildViewShow)
} label: {
Text("🛼 go to ChildView with NavigationLink")
}
Button {
print("🛼 go to ChildView with ZStack")
isChildViewShow = true
} label: {
Text("🛼 go to ChildView with ZStack")
}
.buttonStyle(.borderedProminent)
}
.onDisappear {
print("🐔 ParentView onDisappear 🌪")
}
}
}
struct ChildView: View {
@Binding var isSelfShow: Bool
var body: some View {
VStack {
Spacer()
Text("Child View")
.frame(maxWidth: .infinity)
Button {
isSelfShow = false
} label: {
Text("go to ContentView dismiss ZStack")
}
.buttonStyle(.borderedProminent)
Spacer()
}
.background(.yellow)
.onDisappear {
print("🐤 ChildView disappear 🌪")
}
}
}
NavigationStack으로 ChildView를 쌓았다가 pop 하면 다음과 같이 호출된다.
ChildView 이닛: ChildView onAppear
🐔 ParentView onDisappear 🌪
🐤 ChildView disappear 🌪
NavigationStack으로 ChildView가 들어오면 ParentView는 onDisappear 된다. 즉 NavigationStack에서 화면에 보이지 않게 되면 onDisappear가 호출된다.
ChildView 또한 NavigationStack에서 제거되면 onDisappear를 호출한다.
onAppear modifier을 사용하면 onDisappear의 시점을 더 명확히 알 수 있다.
🐔 ParentView onAppear 👻
🐤 ChildView onAppear 👻
🐔 ParentView onDisappear 🌪
🐔 ParentView onAppear 👻
🐤 ChildView disappear 🌪
처음 화면에 ParentView가 나타날 때 onAppear가 호출되고 NavigationStack으로 ChildView로 이동하면 ChildView의 onAppear가 호출된 뒤 ParentView는 화면에서 보이지 않으므로 onDisappear가 호출된다. 그 후 pop을 통해 ChildView를 화면에서 안보이게 하면 ParentView의 onAppear가 호출되고 ChildView의 onDisappear가 호출된다.
NavigationStack {
ZStack {
ParentView(isChildViewShow: $isChildViewShow)
if isChildViewShow {
ChildView(isSelfShow: $isChildViewShow)
}
} //: ZStack
}
이제 ZStack을 사용해서 ChildView로 ParentView를 완전히 가리면 어떻게 될까?
🐔 ParentView onAppear 👻
🛼 go to ChildView with ZStack
🐤 ChildView onAppear 👻
🐤 ChildView disappear 🌪
ParentView는 이미 화면에 나타난 상태이기 때문에 onAppear는 한번만 호출된다. ChildView는 화면에 나타나면 onAppear 를 호출하고 화면에서 사라지는 순간 onDisappear를 호출한다. Navigation과 화면에 단순 표시 및 제거할 때의 modifier 호출이 다르기 때문에 주의해서 사용해야 한다.
끝이 아니다 이제 TabView를 테스트 해보자 👏😌
TabView
struct TabBarTest: View {
var body: some View {
TabView {
FirstView()
.tabItem {
Image(systemName: "01.circle.fill")
Text("1")
}
SecondView()
.tabItem {
Image(systemName: "02.circle.fill")
Text("2")
}
ThirdView()
.tabItem {
Image(systemName: "03.circle.fill")
Text("3")
}
}
}
}
struct FirstView: View {
var body: some View {
Text("First")
.task {
print("1. before task")
}
.onAppear {
print("1. appear")
}
.task {
print("1. after task")
}
.onDisappear {
print("1. disappear")
}
}
}
각 탭에 해당 하는 뷰에는 task 두개와 appear, disappear modifier을 호출하도록 해뒀다. 예상으로는 1, 2, 3 번째 탭에 해당하는 모든 View의 appear가 호출될 것 같았는데 아니다!
1. appear
1. before task
1. after task
우선 처음에 화면에 탭뷰가 나타나면 첫 번째 탭의 appear와 task만 호출된다.
2. appear
1. disappear
2. before task
2. after task
이후 2번 탭을 누르면 2번 view의 appear가 호출되고 첫 번째 탭의 1번 뷰는 disappear를 호출한다. 화면에 다른 뷰가 appear 되면 -> 기존의 뷰가 disappear 되는 순서는 앞에서 확인한 것과 동일하다. 이후 2번 째 뷰의 task가 순서대로 호출된다.
3. appear
3. before task
3. after task
2. disappear
이번에는 2번 탭에서 3번 탭으로 이동할 때인데, 2번 뷰의 disappear 보다 3번 뷰의 task가 먼저 실행되었다. task와 다른 뷰에 정의된 disappear 간의 순서는 정해지지 않고 랜덤하게 호출되는 것으로 보인다.
1. appear
1. before task
1. after task
3. disappear
다시 3번에서 1번 탭으로 돌아오면 1번의 onAppear가 호출되고 사라진 3번 뷰는 disappear를 호출한다.
TabView 또한 NavigationStack과 마찬가지로 화면에 나타나면 onAppear를 호출하고 화면에서 사라지면 onDisappear를 호출한다. task 또한 화면에 나타날 때마다 호출하는 걸 알 수 있다.
struct SecondView: View {
init() {
print("2. init")
}
var body: some View {
let _ = print("2. body property")
Text("Second")
.task {
print("2. before task")
}
.onAppear {
print("2. appear")
}
.task {
print("2. after task")
}
.onDisappear {
print("2. disappear")
}
}
}
이번에는 각 탭에 해당되는 모든 뷰에 initializer를 추가하고 로그를 찍어주었다. body property가 그려지는 시점에도 로그를 찍었다.
1. init
2. init
3. init
1. body property
1. appear
1. before task
1. after task
예상한데로 init은 모두 호출되는 것을 알 수 있는데
struct TabBarTest: View {
var body: some View {
TabView {
FirstView()
.tabItem {
Image(systemName: "01.circle.fill")
Text("1")
}
SecondView()
.tabItem {
Image(systemName: "02.circle.fill")
Text("2")
}
ThirdView()
.tabItem {
Image(systemName: "03.circle.fill")
Text("3")
}
}
}
}
TabView를 그릴 때 init 되기 때문이다. 각 탭의 뷰가 화면에 나타날 때 비동기 코드를 실행해야 한다면 init이 아니라 task가 적당하다 (사실 그러라고 애플이 만들어 둔 modifier이기도 하다)
1. init
2. init
3. init
1. body property
1. appear
1. before task
1. after task
다시 살펴보면 appear 되기 이전에 1번 뷰의 body를 그린다. 1번 탭에서 2번 탭으로 이동하면
2. body property
2. appear
1. disappear
2. before task
2. after task
2번 또한 body property를 그리고 appear가 호출된다. 여기서 다시 1번 탭을 누르면
1. appear
2. disappear
1. before task
1. after task
1번의 body property 가 호출되지 않는다. 그 이유는 TabView에서 지정해 준 각 탭의 뷰가 이미 암시적 ID를 부여 받고, 업데이트 되는 dependency도 없기 때문에 body property를 새로 그려주지 않기 때문이다. 즉 탭을 눌러 뷰를 이동할 때 보여지는 뷰들은 한 번 그려지고 그 그려진 뷰를 다시 보여주고 있는 것이다. (만약 1번 탭에서 2번 탭으로, 다시 1번 탭으로 돌아온다고 했을 때 이전의 1번 뷰와 지금의 1번 뷰는 같은 뷰이다. SwiftUI에서 구조적으로 같은 ID를 가진 뷰로 인식하기 때문에 이전 뷰를 즉시 파괴하고 새로 그리는 것이 아니라, 이전 뷰를 다시 보여주고 있는 것이다)
+) TabView 자체에 걸어둔 onAppear, task, onDisappear 중 onAppear와 task는 화면에 보일 때 각각 한 번 호출된다.
custom tabbar를 사용하면 다른 동작을 하겠지만 이 정도 살펴봤으면 충분한 것 같다.
여기서 끝이 아니다! 하나 더 남았다 이제 진짜 마지막!! 💪🙃
modal
struct ModalTestView: View {
@State private var isShowModal = false
var body: some View {
VStack {
Button("모달") {
isShowModal.toggle()
}
}
.task {
print("🐔 ParentView task before onAppear 🗂")
}
.onAppear {
print("🐔 ParentView onAppear 👻")
}
.task {
print("🐔 ParentView task after onAppear 🗂")
}
.onDisappear {
print("🐔 ParentView onDisappear 🌪")
}
.sheet(isPresented: $isShowModal) {
ModalChildView()
}
}
}
struct ModalChildView: View {
@Environment(\.dismiss) var dismiss
var body: some View {
VStack {
Button("dismiss 버튼") {
dismiss()
}
}
.task {
print("🐤 ChildView task before onAppear 🗂")
}
.onAppear {
print("🐤 ChildView onAppear 👻")
}
.task {
print("🐤 ChildView task after onAppear 🗂")
}
.onDisappear {
print("🐤 ChildView disappear 🌪")
}
}
}
버튼 눌러서 ModalChildView를 화면에 띄우면
🐔 ParentView onAppear 👻
🐔 ParentView task after onAppear 🗂
🐔 ParentView task before onAppear 🗂
🐤 ChildView onAppear 👻
🐤 ChildView task before onAppear 🗂
🐤 ChildView task after onAppear 🗂
처음 세 줄은 처음 실행했을 때 ParentView가 화면에 보이는 순간 호출되는 modifier이다. 이후 버튼을 눌러 모달을 띄우면 아래 세 줄이 뜬다.
🐤 ChildView disappear 🌪
이후 버튼을 눌러 모달을 닫으면 childview의 onDisappear만 호출되고, ParentView의 onAppear는 호출되지 않는다. 스와이프 제스처도 마찬가지!
1. sheet으로 모달을 띄우면 모달을 띄운 ParentView는 onDisappear가 호출되지 않는다.
2. sheet을 닫아도 ParentView의 onAppear는 호출되지 않는다.
.fullScreenCover(isPresented: $isShowModal, content: {
ModalChildView()
})
.sheet modifier을 .fullScreenCover modifier로 바꿔도 동작은 동일하다.
.sheet(isPresented: $isShowModal) {
print("ParentView로 돌아옴")
} content: {
ModalChildView()
}
만약 ChildView에서 ParentView로 돌아왔을 때 실행할 코드가 있다면 onDismiss 파라미터를 사용할 수 있다. sheet, fullScreenCover modifier 모두 가지고 있는 파라미터이다.
ParentView로 돌아옴
🐤 ChildView disappear 🌪
ChildView의 onDisappear가 호출되기 이전에 onDismiss가 호출된다.
.sheet(isPresented: $isShowModal, content: {
ModalChildView()
.presentationDetents([.medium])
})
혹시 절반만 가리면 어떻게 될까 했는데 sheet, fullScreenCover에서 동작한 것과 동일하게 동작한다.
결론
life cycle modifier | 특징 |
onAppear | - 뷰가 화면에 나타나기 이전에 실행 - NavigationStack : 화면에 최초 나타날 때 호출, ChildView가 push되고 pop 될때 호출 - ZStack : 처음 화면에 나타날 때 한번만 호출, ChildView가 화면을 전체 다 가렸다가 사라져도 onAppear는 호출되지 않음 -TabView: 탭을 눌러 화면에서 나타날 때 마다 - sheet: 모달로 띄워졌을 때 호출, 모달을 띄우는 View라면 modal을 닫아도 호출되지 않음 |
onDisappear | - 화면에서 View가 사라지고 난 후에 실행 - NavigationStack에서 제거될 때 - TabView에서 탭을 바꿔 화면에서 사라질 때 마다 - sheet: 모달로 띄워졌다가 사라질 때, 모달을 띄우는 View라면 모달에 의해 화면에 가려져도 호출되지 않음 |
task | - 비공식적으로 onAppear 뒤에 호출됨 - 여러개의 task가 있는 경우 위에서 아래 순으로 실행 |
init -> body property 그려짐 -> onAppear 호출
(init과 body property update의 경우 depdency에 의해 호출될 수도, 되지 않을 수도 있기 때문에 주의)
이 정도면,,, 사용할 때마다 찍어보는게 가장 베스트이지 않을까 싶기도 하다 또륵...🥲

'iOS 🍎 > SwiftUI' 카테고리의 다른 글
[SwiftUI] Custom Toggle (0) | 2023.11.13 |
---|---|
[SwiftUI] Shape path로 Tooltip 그리기 (0) | 2023.11.06 |
[SwiftUI] VStack 과 LazyVStack 뭐가 다른 걸까? (1) | 2023.10.08 |
[SwiftUI] NavigationView (1) | 2023.09.22 |
[SwiftUI] 가로모드, 세로모드 지원하기 with @ViewBuilder (0) | 2023.09.20 |