SwiftUI NavigationLink loads destination view immediately, without clicking
예를 들어 아래와 같은 LazyVStack이 있다고 합니다. 리스트의 역할을 하고 있고요. 각각의 셀은 DetailView로 연결된 Navigation Link입니다.
struct ContentView: View {
var body: some View {
ScrollView {
LazyVStack {
ForEach(0..<100) { _ in
NavigationLink {
DetailView()
} label: {
Text("To Detail")
.font(.system(size: 50))
}
}
}
}
}
}
그런데 DetailView를 보니 해당 view를 init하기 위해서는 뭔가 무거운 네트워크 작업을 해야하는군요. 사용자가 DetailView를 들어오기 전까지는 필요 없는 네트워크 작업인데도 말이죠.
struct DetailView: View {
// init할 때 필요한 작업
init() {
heavyAPICall()
}
var body: some View {
Text("Detail View")
}
// 무거운 네트워크 작업
func heavyAPICall() {
print("Too Heavy API Called 😥")
}
}
위 코드를 실행해보면 아직 DetailView를 띄우지 않았는데도 미리 DetailView를 전부 init 해놓았습니다. 물론 VStack 이기 때문에 화면에 보이는 NavigationLink만 init했지만 여전히 성능 문제가 발생할 수 있습니다. 사용자가 보고자하는 Detail은 고작 몇개에 불과할텐데 전부다 무거운 네트워크 동작으로 init 해놓는다면 메모리의 낭비이자 네트워크 자원의 낭비입니다.
우리의 목표는 NavigationLink의 destination이 해당 View에 진입할 때 init되도록 만들고자 합니다. 따라서 destination에 해당하는 View를 아래 View로 한단계 감싸도록 합시다.
struct LazyView<Content: View>: View {
// 실제 View를 함수 형태로 받는다.
let build: () -> Content
// init할 때 build함수를 실행하지 않고 property에 저장만 한다.
init(_ build: @autoclosure @escaping() -> Content) {
self.build = build
}
//⭐️ 실제 SwiftUI가 View를 보여줄 때 접근하는 body property에서 build 함수를 실행한다.
//👉 이렇게 하면 실제 화면에 진입하기 전에는 View가 init되지 않는다!
var body: Content {
build()
}
}
코드의 원리는 다음으로 추정됩니다.
@autoclosure: 함수만 전달해도 자동으로 클로저를 만들어 줍니다.
//😱 @autoclosure가 없다면 아래와 같이 클로저의 형태로 전달해야 합니다.
LazyView({ DetailView() })
//😆 @autoclosure가 있다면 아래와 같이 함수의 형태로 전달해도 됩니다.
원하는 View를 LazyView로 한단계 감싸서 사용합니다.
이제 콘솔의 내용으로 보면 처음에는 어떤 DetailView도 init되지 않고 터치해서 화면에 띄운 DetailView만 init되는 것을 볼 수 있습니다.
struct ContentView: View {
var body: some View {
NavigationView {
ScrollView {
LazyVStack {
ForEach(0..<100) { i in
NavigationLink {
LazyView(DetailView(num: i))
} label: {
Text("To Detail")
.font(.system(size: 50))
}
}
}
}
}
}
}