SwiftUI PageTabView in iOS14.2 will Recall ChildView onAppear method Many times
Asked Answered
D

3

7

I use TabView PageTabViewStyle with SwiftUI to display a pageview, when I swipe this TabView I find child view will Recall onAppear method Many times, Can someone tell me why?

This is my code

import SwiftUI

struct Pageview: View {
    
    @StateObject var vm = PageViewModel()
    
    var body: some View {
        VStack {
            
            DragViewBar().padding(.top, 14)
            
            TabView(selection: $vm.selectTabIndex) {
                
                TextView(index: "0").tag(0)
                TextView(index: "1").tag(1)
                TextView(index: "2").tag(2)
                TextView(index: "3").tag(3)
                TextView(index: "4").tag(4)
                TextView(index: "5").tag(5)
                
            }
            .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
            
        }

    }
}

struct TextView: View {
    
    let index: String
    
    var body: some View {
        VStack {
            Text(index)
        }
        .onAppear { print(index) }
        
    }
}

struct DragViewBar: View {
    var body: some View {
        Rectangle()
            .frame(width:36.0,height:5.0).foregroundColor(Color.blue)
            .cornerRadius(100)
    }
}

class PageViewModel: ObservableObject {
    @Published var selectTabIndex = 0
}

The result of the console printing enter image description here

The correct case is to print only once per swipe

It just has a problem in ios14.2, 14.1 will be ok, you can load my code in Github: https://github.com/werbhelius/TabViewBug

Xcode version: 12.1 (12A7403)

Device: iPhone 6s iOS 14.2

I think you can reproduce this problem on any device in iOS 14.2

I look forward to your help to solve this problem. Thank you

Deutoplasm answered 2/11, 2020 at 8:0 Comment(0)
U
4

Views are preloaded at the discretion of SwiftUI. Sometimes more than others depending on the device's available resources. onAppear is called even if it has appeared out of view (pre-loaded)

import SwiftUI

struct PageView: View {
    
    @StateObject var vm = PageViewModel()
    
    var body: some View {
        VStack {
            
            DragViewBar().padding(.top, 14)
            
            TabView(selection: $vm.selectTabIndex) {
                
                TextView(index: "0").tag(0)
                TextView(index: "1").tag(1)
                TextView(index: "2").tag(2)
                TextView(index: "3").tag(3)
                TextView(index: "4").tag(4)
                TextView(index: "5").tag(5)
                
            }
            //This lets you perform an operation when the value has changed
            .onReceive(vm.$selectTabIndex, perform: { idx in
                print("PageView :: body :: onReceive" + idx.description)
            })
            .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
            
        }

    }
}

struct TextView: View {
    
    let index: String
    
    var body: some View {
        VStack {
            Text(index)
        }
        //Views are pre-loaded at the discretion of SwiftUI
        .onAppear { print(index) }
        
        .onReceive(index.publisher, perform: { idx in
            print("TextView :: body :: onReceive" + idx.description)
        })
        
    }
}

struct DragViewBar: View {
    var body: some View {
        Rectangle()
            .frame(width:36.0,height:5.0).foregroundColor(Color.blue)
            .cornerRadius(100)
    }
}

class PageViewModel: ObservableObject {
    @Published var selectTabIndex = 0
}


struct PageView_Previews: PreviewProvider {
    static var previews: some View {
        PageView()
    }
} 
Unmarked answered 2/11, 2020 at 16:57 Comment(5)
But I only found this problem with repeat calls onAppear on 14.2, which is normal for version 14.1, I suspect this is a bug in TabView on ios 14.2Deutoplasm
I have the same exact issue! My app relies on PageTabViewStyle and it completely broke on iOS 14.2 because of this.| Did you find a solution to this?Lemal
I don't know for sure but PageTabViewStyle likely just adopted this preload behavior from List for "efficiency" purposes. One popular workaround is the struct LazyView<Content: View>: View I haven't tried it for PageTabViewStyle but it might be worth the effort if onReceive doesn't work for your use case #58357914Unmarked
did either of you figure this out? @Lemal I'm still facing this issue in 14.3.Pathognomy
The answer above worked for me this is what I ended up doing: twitter.com/MaximeHeckel/status/1328381063287042048/photo/2Lemal
M
3

For everyone who is still struggling to find a good workaround for this problem. I've managed to use this framework https://github.com/fermoya/SwiftUIPager which can be used to mimic the TabView .tabViewStyle(PageTabViewStyle()) style.

import SwiftUIPager
struct Dummy: View {
    @StateObject var page: Page = .first()
    let numberOfPages : Int = 3
    var body: some View {
        
        Pager(page: self.page,
              data: Array(0..<numberOfPages),
              id: \.self,
              content: { index in
                
                switch (index) {
                case 0:
                    ZStack{
                        Color.white
                        Text("Test")
                    }
                case 1:
                    ZStack{
                        Color.white
                        Text("Test2")
                    }
                default:
                    ZStack{
                        Color.white
                        Text("Test3")
                    }
                }
                
              }
        )
        .pagingPriority(.simultaneous)
        .onPageWillChange { (newIndex) in
            print("Page \(self.page.index) will change to \(newIndex) i.e. onDisappear")
            switch (self.page.index) {
            case 0:
                print("onDisappear of \(self.page.index)")
            case 1:
                print("onDisappear of \(self.page.index)")
            default:
                print("onDisappear of \(self.page.index)")
            }
        }
        .onPageChanged { (newIndex) in
            print("Has changed to page \(self.page.index) i.e. onAppear")
            switch (newIndex) {
            case 0:
                print("onAppear of \(newIndex)")
            case 1:
                print("onAppear of \(newIndex)")
            default:
                print("onAppear of \(newIndex)")
            }
        }
        
    }
}
Maggoty answered 23/1, 2021 at 9:33 Comment(0)
P
0

I've encountered similar problems due to bugs/features which have appeared with various 14.x releases. For example, on iOS 14.3 the code above prints this to the console at launch:

PageView :: body :: onReceive0 TextView :: body :: onReceive0 0 0 0 0 0 0 0 0 0 0 0

and this when swiping to index "1":

TextView :: body :: onReceive1 1 TextView :: body :: onReceive2 2 PageView :: body :: onReceive1 1 1 1 1 1 1

There appear to be two issues:

  1. .onAppear/.onDisappear may be called once, multiple times or not at all and therefore can't be used.
  2. Other modifiers like .onReceive and .onChange may be called repeatedly and therefore require debouncing.

Here's a simplified example of a workaround that allows detection and debouncing of TabView page changes without using .onAppear.

import SwiftUI

struct ContentView: View {
  @State private var currentView: Int = 0
  
  var body: some View {
    
    TabView(selection: $currentView) {
      ChildView1(currentView: $currentView).tag(1)
      ChildView2(currentView: $currentView).tag(2)
    }
    .tabViewStyle(PageTabViewStyle())
  }
}

struct ChildView1: View {
  @Binding var currentView: Int
  @State private var debouncer = 0
  let thisViewTag = 1
  
  var body: some View {
    Text("View 1")
      
      .onChange(of: currentView, perform: { value in
        if value == thisViewTag {
          debouncer += 1
          if debouncer == 1 {
            print ("view 1 appeared")
          }
        } else {
          debouncer = 0
        }
      })
  }
}

struct ChildView2: View {
  @Binding var currentView: Int
  @State private var debouncer = 0
  let thisViewTag = 2
  
  var body: some View {
    Text("View 2")
      
      .onChange(of: currentView, perform: { value in
        if value == thisViewTag {
          debouncer += 1
          if debouncer == 1 {
            print ("view 2 appeared")
          }
        } else {
          debouncer = 0
        }
      })
  }
}
Prefer answered 2/1, 2021 at 18:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.