Home > OS >  SwiftUI Pop To Root DeInit Class
SwiftUI Pop To Root DeInit Class

Time:02-18

UPDATE: Added more code examples so that a working minimal example was provided.


On the main screen of my app the user can select from a few different options. One of which is to create a new user account, which is a multi-page form utilizing a NavigationView and various NavigationLinks.

On the first page of the form an observable class is initialized and the rest of the form's pages observe that class.

If I go through the form and then tap the back arrow all the way out of the form, the class deinitializes as it should. However, I have a timeout alert set to go off after x seconds with an OK button that takes them back to the main screen of the app (i.e. out of the form and out of the nav view).

All of this works great, except that when tapping ok on the alert and then being sent back to the main screen, the class is not being deinitialized, thus a memory leak.

This is also causing another major problem.

If I am on the first page of the form and wait for the alert, no problem. However, if I'm on any consecutive page of the form and the alert appears, I get console errors stating:

[Presentation] Attempt to present <SwiftUI.PlatformAlertController: 0x7f850606ba00> on <_TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentVS_7AnyViewVS_12RootModifier__: 0x7f84f680a410> (from <_TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentVS_7AnyViewVS_24NavigationColumnModifier__: 0x7f84f690e800>) which is already presenting <SwiftUI.PlatformAlertController: 0x7f8515829a00>

This leads me to believe that the alerts are being shown on every page of the form, not just the page the user is on when the alert is triggered.

Also, when tapping OK to go back to the main screen, the alert is shown again with another error stating:

[Presentation] Presenting view controller <SwiftUI.PlatformAlertController: 0x7f84f5881200> from detached view controller <_TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentVS_7AnyViewVS_24NavigationColumnModifier__: 0x7f84f69236a0> is discouraged.

I've been researching these issues for a while but can't seem to find definitive answers as to how to resolve them.

Registration Expired Class:

import Foundation

class RegExpired: ObservableObject {
    @Published var navToHome: Bool = false
}

Timer Class:

import Combine
import Foundation

class TimerClass: ObservableObject {
    
// Timeout Timer
    private var receive = [AnyCancellable]()
    let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
    
    init() {
        timeoutTimer()
        print("Initialized")
    }
    
    deinit {
        timerCancel()
        print("DeInitialized")
    }
    
// Storing User Activity Time
    @Published var userActivity = Date.now
    @Published var userActivityAlert = false
    @Published var userTimeout = false
    
// CreateUser Timer
    func timeoutTimer() {
        timer
            .receive(on: DispatchQueue.main)
            .sink { [weak self] _ in
                self?.warning()
                self?.timeout()
            }.store(in: &receive)
    }
    
// Cancel Timer
    func timerCancel() {
        timer.upstream.connect().cancel()
        userActivityAlert = false
        userTimeout = false
    }
    
// Timeout Warning
    func warning() {
        if Date.now >= userActivity.addingTimeInterval(10*1) {
            userActivityAlert = true
        }
    }
    
// Timeout Expired
    func timeout() {
        if Date.now >= userActivity.addingTimeInterval(15*1) {
            userActivityAlert = false
            DispatchQueue.main.asyncAfter(deadline: .now()   0.5) {
                self.userTimeout = true
            }
        }
    }
}

Timeout Alert:

import SwiftUI

struct TimeoutAlertView: View {
    
    @EnvironmentObject var regExpired: RegExpired
    @ObservedObject var timerClass: TimerClass
    
    var body: some View {
        if timerClass.userActivityAlert == true {
            VStack {
                EmptyView()
            }
            .alert("Warning", isPresented: $timerClass.userActivityAlert, actions: {
                Button("I'm Here") { timerClass.userActivity = Date.now }}, message: { Text("Are you still there?") })
        } else if timerClass.userTimeout == true {
            VStack {
                EmptyView()
            }
            .alert("Expired", isPresented: $timerClass.userTimeout, actions: {
                Button("OK") {
                    self.regExpired.navToHome = true
                }}, message: { Text("Your session has expired.") })
        }
        
    }
}

App:

import SwiftUI

@main
struct MyApp: App {
    @StateObject var regExpired: RegExpired
    
    init() {
        let regExpired = RegExpired()
        _regExpired = StateObject(wrappedValue: regExpired)
    }
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(regExpired)
        }
    }
}

ContentView:

import SwiftUI

struct ContentView: View {
    @EnvironmentObject var regExpired: RegExpired
    @State var isMainViewActive: Bool = false
    
    var body: some View {
        NavigationView {
            VStack {
                NavigationLink(destination: WelcomeView(), isActive: $isMainViewActive) {
                    Text("Reg Form")
                }
                .isDetailLink(false)
            }
            .onReceive(self.regExpired.$navToHome) { navToHome in
                if navToHome {
                    self.isMainViewActive = false
                    self.regExpired.navToHome = false
                }
            }
        } // End NavigationView
        .navigationViewStyle(.stack)
    }
}

Welcome & Next Views:

import SwiftUI

struct WelcomeView: View {

    @StateObject var timerClass = TimerClass()
    @State private var btnHover = false
    @State private var isBtnActive = false
    
    var body: some View {

        VStack {                
            List {
                Section {
                    Text("Welcome")
                }               

                Section {
                    Spacer()
                    HStack {
                        Spacer()
                        Image(systemName: btnHover == true ? "chevron.right.circle.fill" : "chevron.forward.circle")
                            .pressAction {
                                btnHover = true
                            } onRelease: {
                                btnHover = false
                                isBtnActive = true
                            }
                            .background(
                                NavigationLink(destination: NextView(timerClass: timerClass), isActive: $isBtnActive) {}
                                    .opacity(0)
                            )
                            Spacer()
                    }
                }
            }
    }
    .navigationBarTitle("", displayMode: .inline)
    TimeoutAlertView(timerClass: timerClass)
    }
}

struct NextView: View {

    @ObservedObject var timerClass: TimerClass
    @State private var btnHover = false
    @State private var isBtnActive = false
    
    var body: some View {

        VStack {                
            List {
                Section {
                    Text("Welcome")
                }               

                Section {
                    Spacer()
                    HStack {
                        Spacer()
                        Image(systemName: btnHover == true ? "chevron.right.circle.fill" : "chevron.forward.circle")
                            .pressAction {
                                btnHover = true
                            } onRelease: {
                                btnHover = false
                                isBtnActive = true
                            }
                            .background(
                                NavigationLink(destination: AnotherView(timerClass: timerClass), isActive: $isBtnActive) {}
                                    .opacity(0)
                            )
                            Spacer()
                    }
                }
            }
    }
    .navigationBarTitle("", displayMode: .inline)
    TimeoutAlertView(timerClass: timerClass)
    }
}

WelcomeView is where the timerClass is initialized via @StateObject. All other form pages use @ObservedObject to share that class. However, upon tapping OK on the alert, the user is taken back to ContentView and the class stays initialized, and all the errors mentioned above occur.

I appreciate it! Thanks.

CodePudding user response:

OK, I solved it.

I honestly didn't need all the code I posted. You need to detect the current view. I did this in each page of the form by adding:

@State private var isViewDisplayed = false

and then:

.onAppear() {
    self.isViewDisplayed = true
}

.onDisappear() {
    self.isViewDisplayed = false
}

and then to call to the TimeoutAlertView

if self.isViewDisplayed {
    TimeoutAlertView(timerClass: timerClass)
}

Now, each page will only run the TimeoutAlertView if isViewDisplayed is true, which only happens if the current view has appeared.

Hope this helps others!

  • Related