swiftuipreloadtabview

SwiftUI: How can I preload next page when use TabView with page style


I'm using TabView to show pages of image:

TabView(selection: self.$index) {
  ForEach(0..<100) { index in
    PageImage(id: pageIds[index]).tag(index)
  }
}
  .tabViewStyle(.page(indexDisplayMode: .never))

Those images are asyncornized loaded from web and I have PageImage like this:

@StateObject private var pageModel = PageImageModel()

...

var body: some View {
  if let imageData = pageModel.imageData {
    Image(uiImage: UIImage(data: imageData)!)
  } else {
    ProgressView(value: pageModel.progress)
      .onAppear {
        imageModel.load(id: id)
      }
  }
}

It works except the image only load after I reached that page. Is there anyway I could tell the TabView to preload pages (i.e. trigger the imageModel.load(id: id)) before I reach that page? Thanks.

Edit: Something a bit unrelated. Currently, if your page view use if else to return different view, then TabView will call body to render ALL pages when it is shown. My workaround is to wrap if else in a ZStack, now TabView will only render the page that is on screen.


Solution

  • It is already call next page to appear when you swipe. So only the second page of TabView won't be loaded because you haven't perform any swipe yet

    There're many solution to overcome this, like put an onAppear on TabView to load second page, or handle it properly in your PageImageModel

    Incase you want to switch your page index without swipe gesture (buttons, events, ...), there're some solutions as well

    1. preload next page when current page already has image: it will load the next page then the page after that when you swipe (better more than less)
    2. send current page index of TabView to your PageView so it knows exactly what next page is (no more redundant)
    3. send current page index to your PageImageModel to handle it properly

    Here's minimize sample code for your case that can be run on preview

    struct PageView: View {
        @Binding var data: [Int:String]
        @Binding var currentIndex: Int
        var id: Int
        var body: some View {
            if let value = data[id] {
                Text(value).onAppear {
                    if id == currentIndex {
                        DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) {
                            data[id+1] = "\(id+1)"
                        }
                    }
                }
            } else{
                ProgressView().onAppear {
                    DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) {
                        data[id] = "\(id)"
                    }
                }
            }
        }
    }
    
    struct TestNextTabView: View {
        @State var data: [Int:String] = [:]
        @State var next = 0
        @State var index = 0
        var body: some View {
            VStack {
                Text("preload: \(next)")
                TabView(selection: $index) {
                    ForEach(0..<100) { i in
                        PageView(data: $data, currentIndex: $index, id: i)
                    }
                }
                .tabViewStyle(.page(indexDisplayMode: .never))
                Button("next") { index += 1 }
            }
        }
    }
    
    struct TestOtherTestViews_Previews: PreviewProvider {
        static var previews: some View {
            TestNextTabView()
        }
    }