18

I would like to know if the behavior of onAppear and onDisappear in SwiftUI (Xcode 11 beta 6 when I wrote this) is what a developer would find more useful or it is just being more a problem than a feature.

Right now, if we use a cascaded navigation as you will find in the sample code I attach (that compiles and runs fine in Xcode 11b6), a console output of a user navigating back and forth would just trigger onAppear only in the case of a new view load in the forward direction (meaning going deeper).

In the navigation: Root -> NestedView1 -> NestedView2 -> NestedView3 , when adding a debug helper to each view stage,

  .onAppear(perform: {print("onAppear level N")})
  .onDisappear(perform: {print("onDisappear level N")})

debug console would show

onAppear root level 0
onAppear level 1
onAppear level 2
onAppear level 3

(No onDisappear triggering)

but travelling back Root <- NestedView1 <- NestedView2 <- NestedView3

debug console would show ... nothing

(No onAppear or onDisappear triggering)

struct NestedViewLevel3: View {

    @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>

    var body: some View {
        VStack {
            Spacer()
            Text("Level 3")
            Spacer()
            Button(action: {
                self.presentationMode.wrappedValue.dismiss()
            }) {
                Text("Back")
                    .padding(.horizontal, 15)
                    .padding(.vertical, 2)
                    .foregroundColor(Color.white)
                    .clipped(antialiased: true)
                    .background(
                        RoundedRectangle(cornerRadius: 20)
                            .foregroundColor(Color.blue)
                            .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: 40, alignment: .center)
                )
            }
            Spacer()
        }
        .navigationBarBackButtonHidden(false)
        .navigationBarTitle("Level 3", displayMode: .inline)
        .onAppear(perform: {print("onAppear level 3")})
        .onDisappear(perform: {print("onDisappear level 3")})

    }
}

struct NestedViewLevel2: View {

    @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>

    var body: some View {
        VStack {
            Spacer()
            NavigationLink(destination: NestedViewLevel3()) {
                Text("To level 3")
                    .padding(.horizontal, 15)
                    .padding(.vertical, 2)
                    .foregroundColor(Color.white)
                    .clipped(antialiased: true)
                    .background(
                        RoundedRectangle(cornerRadius: 20)
                            .foregroundColor(Color.gray)
                            .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: 40, alignment: .center)
                )
                    .shadow(radius: 10)
            }
            Spacer()
            Text("Level 2")
            Spacer()
            Button(action: {
                self.presentationMode.wrappedValue.dismiss()
            }) {
                Text("Back")
                    .padding(.horizontal, 15)
                    .padding(.vertical, 2)
                    .foregroundColor(Color.white)
                    .clipped(antialiased: true)
                    .background(
                        RoundedRectangle(cornerRadius: 20)
                            .foregroundColor(Color.blue)
                            .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: 40, alignment: .center)
                )
            }
            Spacer()
        }
        .navigationBarBackButtonHidden(false)
        .navigationBarTitle("Level 2", displayMode: .inline)
        .onAppear(perform: {print("onAppear level 2")})
        .onDisappear(perform: {print("onDisappear level 2")})
    }
}

struct NestedViewLevel1: View {

    @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>

    var body: some View {
        VStack {
            Spacer()
            NavigationLink(destination: NestedViewLevel2()) {
                Text("To level 2")
                    .padding(.horizontal, 15)
                    .padding(.vertical, 2)
                    .foregroundColor(Color.white)
                    .clipped(antialiased: true)
                    .background(
                        RoundedRectangle(cornerRadius: 20)
                            .foregroundColor(Color.gray)
                            .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: 40, alignment: .center)
                )
                    .shadow(radius: 10)
            }
            Spacer()
            Text("Level 1")
            Spacer()
            Button(action: {
                self.presentationMode.wrappedValue.dismiss()
            }) {
                Text("Back")
                    .padding(.horizontal, 15)
                    .padding(.vertical, 2)
                    .foregroundColor(Color.white)
                    .clipped(antialiased: true)
                    .background(
                        RoundedRectangle(cornerRadius: 20)
                            .foregroundColor(Color.blue)
                            .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: 40, alignment: .center)
                )
            }
            Spacer()
        }
        .navigationBarBackButtonHidden(false)
        .navigationBarTitle("Level 1", displayMode: .inline)
        .onAppear(perform: {print("onAppear level 1")})
        .onDisappear(perform: {print("onDisappear level 1")})
    }
}

struct RootViewLevel0: View {

    var body: some View {
        NavigationView {
        VStack {
            Spacer()
            NavigationLink(destination: NestedViewLevel1()) {
            Text("To level 1")
                .padding(.horizontal, 15)
                .padding(.vertical, 2)
                .foregroundColor(Color.white)
                .clipped(antialiased: true)
                .background(
                    RoundedRectangle(cornerRadius: 20)
                    .foregroundColor(Color.gray)
                    .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: 40, alignment: .center)
                )
                .shadow(radius: 10)
        }

            Spacer()

            }
    }

        .navigationBarTitle("Root level 0", displayMode: .inline)
        .navigationBarBackButtonHidden(false)
        .navigationViewStyle(StackNavigationViewStyle())
        .onAppear(perform: {print("onAppear root level 0")})
        .onDisappear(perform: {print("onDisappear root level 0")})

    }

}


struct ContentView: View {
    var body: some View {
        RootViewLevel0()
    }
}


}

Now, would a developer rather have onAppear and onDisappear:

1) Triggered for the purpose of launching actions that need to be performed only once and only when the user travels forward, like in the current observed behavior.

2) Triggered each time the view appears, more like the action name seems to mean, be it backwards, forwards and any number of times.

I'd take the option 2, simple and brutal (and what I currently need), but I am quite a naive newbie at NavigationView, and option 2 may break a lot of established paradigms I'm not taking into account.

Your feedback will help me find out if the corresponding Feedback Assistant case for SwiftUI is on legitimate grounds.

Jonas Deichelmann
  • 3,513
  • 1
  • 30
  • 45
jpelayo
  • 403
  • 1
  • 5
  • 13
  • 2
    I would like to have the exact same functioning as we had with ViewWillAppear and ViewWillDisappear. Your console would show each one appearing once (when they are first loaded), then disappearing once. – ShadowDES Aug 26 '19 at 20:30
  • Definitely, seems to be a bug and not a feature, so I'd say that onAppear/onDisappear is broken as of Xcode11b6. – jpelayo Aug 27 '19 at 08:54
  • As of XCODE11b7 testing with a watchOS app, onAppear works but onDisappear does not. Bug most likely. – Sid Aug 30 '19 at 04:54

3 Answers3

7

It was a bug on Apple's end.

.onAppear() now works as it is supposed to in iOS 13.1 and Xcode 11 Beta 7.

When navigating forwards and backwards to the NavigationView, .onAppear() will trigger.

pkamb
  • 33,281
  • 23
  • 160
  • 191
txagPman
  • 284
  • 1
  • 2
  • 8
  • Works as we both like (mode 2) on XCode 11 (11A420a) and iOS 13.1 (17A844). Indeed, was a bug. Thanks! – jpelayo Sep 25 '19 at 12:22
  • 4
    `onDisappear()` does not predictably get called even on iOS 13.5! – Zorayr Jun 26 '20 at 22:11
  • 3
    Not even close to working yet, search onAppear on the dev forums https://developer.apple.com/forums/search/?q=onappear – malhal Sep 14 '20 at 21:07
  • Weirdly enough, someone mentioned that `.onAppear(perform: viewModel.onAppear)` seems to work better than `.onAppear { viewModel.onAppear() }`, and you know what, indeed it does. At least for the issue that I was hitting, switching to the `.onAppear(perform:)` seems to be working. Source: https://developer.apple.com/forums/thread/655338?answerId=650030022 – Manav Feb 05 '21 at 12:09
  • 1
    onAppear is often not called when navigating forwards and backwards within a NavigationView. The reason might have something to do with internal caching in iOS. – Karlth Jun 30 '22 at 11:31
7

Starting Xcode 11.2 beta (released 02/10/2019), both methods (including .onDisappear()) are being correctly triggered.

Note that .onDisappear() was not being triggered until this beta release.

pkamb
  • 33,281
  • 23
  • 160
  • 191
Arshia
  • 334
  • 2
  • 13
1

Both commands: .onDisappear(perform: ) & .onAppear(perform: )

are bugging the NavigationView (.navigationBarTitle) feature:

    .navigationBarTitle("Opciones", displayMode: .inline)

When used the navigationBarTitle is removed or not working in the View where you use invoke it. This happens using BOTH: .onDisappear(perform: ) .onAppear(perform: )

Using Xcode Version 12.2 (12B45b)