Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI's onAppear() and onDisappear() called multiple times and inconsistently on Xcode 12.1

I've come across some strange behavior with SwiftUI's onAppear() and onDisappear() events. I need to be able to reliably track when a view is visible to the user, disappears, and any other subsequent appear/disappear events (the use case is tracking impressions for mobile analytics).

I was hoping to leverage the onAppear() and onDisappear() events associated with swiftUI views, but I'm not seeing consistent behavior when using those events. The behavior can change depending on view modifiers as well as the simulator on which I run the app.

In the example code listed below, I would expect that when ItemListView2 appears, I would see the following printed out in the console:

button init
button appear

And on the iPhone 8 simulator, I see exactly that.

However, on an iPhone 12 simulator, I see:

button init
button appear
button disappear
button appear

Things get even weirder when I enable the listStyle view modifier:

button init
button appear
button disappear
button appear
button disappear
button appear
button appear

The iPhone 8, however remains consistent and produces the expected result.

I should also note that in no case, did the Button ever seem to disappear and re-appear to the eye.

These inconsistencies are also not simulator only issues, i noticed them on devices as well.

I need to reliably track these appear/disappear events. For example I'd need to know when a cell in a list appears (scrolled into view) or disappears (scrolled out of view) or when, say a user switches tabs.

Has anyone else noticed this behavior? To me this seems like a bug in SwiftUI, but I'm not certain as I've not used SwiftUI enough to trust myself to discern a programmer error from an SDK error. If any of you have noticed this, did you find a good work-around / fix?

Thanks,

  • Norm
// Sample code referenced in explanation
// Using Xcode Version 12.1 (12A7403) and iOS 14.1 for all simulators
import SwiftUI

struct ItemListView2: View {

    let items = ["Cell 1", "Cell 2", "Cell 3", "Cell 4"]

    var body: some View {
        ListingView(items: items)
    }
}

private struct ListingView: View {
    let items: [String]

    var body: some View {
        List {
            Section(
                footer:
                    FooterButton()
                    .onAppear { print("button appear") }
                    .onDisappear { print("button disappear") }
            ) {
                ForEach(items) { Text($0) }
            }
        }
//      .listStyle(GroupedListStyle())
    }
}

private struct FooterButton: View {
    init() {
        print("button init")
    }
    var body: some View {
        Button(action: {}) { Text("Button")  }
    }
}
like image 537
Norm Barnard Avatar asked Nov 12 '20 22:11

Norm Barnard


2 Answers

In SwiftUI you don't control when items in a List appear or disappear. The view graph is managed internally by SwiftUI and views may appear/disappear at any time.

You can, however, attach the onAppear / onDisappear modifiers to the outermost view:

List {
    Section(footer: FooterButton()) {
        ForEach(items, id: \.self) { 
            Text($0) 
        }
    }
}
.listStyle(GroupedListStyle())
.onAppear { print("list appear") }
.onDisappear { print("list disappear") }
like image 184
pawello2222 Avatar answered Sep 21 '22 02:09

pawello2222


Try this UIKit approach. Similar behavior continues to exist under iOS 14.

protocol RemoteActionRepresentable: AnyObject {
    func remoteAction()
}

struct UIKitAppear: UIViewControllerRepresentable {
    let action: () -> Void
    
    func makeUIViewController(context: Context) -> UIAppearViewController {
       let vc = UIAppearViewController()
        vc.delegate = context.coordinator
        return vc
    }
    
    func updateUIViewController(_ controller: UIAppearViewController, context: Context) {}
    
    func makeCoordinator() -> Coordinator {
        Coordinator(action: self.action)
    }
    
    class Coordinator: RemoteActionRepresentable {
        
        var action: () -> Void
        
        init(action: @escaping () -> Void) {
            self.action = action
        }
        
        func remoteAction() {
            action()
        }
    }
}

class UIAppearViewController: UIViewController {
    weak var delegate: RemoteActionRepresentable?
    
    override func viewDidLoad() {
        view.addSubview(UILabel())
    }
    override func viewDidAppear(_ animated: Bool) {
        delegate?.remoteAction()
    }
}

extension View {
    func onUIKitAppear(_ perform: @escaping () -> Void) -> some View {
        self.background(UIKitAppear(action: perform))
    }
}

Example:

var body: some View {
    MyView().onUIKitAppear {
        print("UIViewController did appear")
   }
}
like image 35
Peter Kreinz Avatar answered Sep 19 '22 02:09

Peter Kreinz