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!