LAST CHANCE: Save 50% on all my Swift books and bundles! >>

SOLVED: iOS 17 Issue with Alerts that worked in iOS 16

Forums > SwiftUI

This code works in iOS16 but does not present the alerts or floating action button in iOS 17 with the error message below. I have tried rearranging the order of alerts etc, but I need to understand the root cause. Any help appreciated... Gerry

Attempt to present <SwiftUI.PlatformAlertController: 0x10b887600> on <SwiftUI.UIKitNavigationController: 0x11183e200> (from <_TtGC7SwiftUI32NavigationStackHostingControllerVS7AnyView: 0x111838800>) whose view is not in the window hierarchy.

import SwiftUI

struct RegattaView: View {
    @EnvironmentObject var regattaList : RegattaList
    @Environment(\.dismiss) var dismiss

    let dateFormatter = DateFormatter()
    @State  var showingAddScreen : Bool = false
    @State  var showingAddAMYA : Bool = false
    @State  var showingAddManual : Bool = false
    @State  var deleteWarn : Bool = false
    @State var offsets: IndexSet = []
    @State  var showingHelp : Bool = showRegattaListHelp

    var body: some View {
        NavigationStack {
            List {
                ForEach(regattaList.regattas, id: \.id) { regatta in
                    NavigationLink {
                        RegattaDetail(regatta: regatta)
                    } label: {
                        HStack {
                            Text(regatta.name)
                                    .font(.headline)
                            Spacer()
                            Text(regatta.startDate.formatted(date: .numeric, time: .omitted))                            
                        }
                        .swipeActions(edge: .leading) {
                            Button {
                                cloneRegatta(regatta: regatta)
                            } label: {
                                Label("Clone regatta", systemImage: "doc.on.doc.fill")
                            }
                            .tint(.indigo)
                        }
                    }
                }
                .onDelete { items in
                    offsets = items
                    deleteWarn = true
                }
            }
            .toolbar {
                ToolbarItem(placement: .topBarTrailing) {
                    Button {
                        showingAddScreen.toggle()
                    } label: {
                        Label("Add Regatta", systemImage: "plus")
                    }
                }

            }
            .floatingActionButton(color: .blue,
                                   image: Image(systemName: "questionmark.circle")
                                     .foregroundColor(.white)) {
               showingHelp = true
             }
            .sheet(isPresented: $showingAddAMYA) {
                AddAMYARegatta()
            }
            .sheet(isPresented: $showingAddManual) {
                AddRegatta()
            }
            .navigationTitle("Regatta List")
            .alert("Add a Regatta", isPresented: $showingAddScreen) {
                Button("Manually") {
                    showingAddManual = true
                }
                Button("Download from AMYA") { showingAddAMYA = true }
                Button("Cancel", role: .cancel) { }
            } message: {
                Text("How do you want to add the Regatta?")
            }
            .alert("Delete Regatta", isPresented: $deleteWarn) {
                Button("DELETE", role: .destructive) {
                    regattaList.regattas.remove(atOffsets: offsets)
                    regattaList.selectedRegatta = Regatta()
                    regattaList.save()
                }
                Button("Cancel", role: .cancel) { }
            } message: {
                Text("Are you sure you want to delete the Regatta?")
            }
            .alert("Regatta List Help", isPresented: $showingHelp) {
                Button("Don't show again") { showRegattaListHelp = false }
                Link("Get more Help", destination: URL(string: "https://ezregatta.freshdesk.com/support/home")!)
                Button("OK", role: .cancel) { }
            } message: {
                Text("**\(UIApplication.appName ?? "ezRegatta") Version: \(UIApplication.appVersion ?? "unknown")** \n\n")
                +
                Text("You can tap a regatta to view the details, or tap")
                +
                Text(" + ")
                +
                Text("at the top of the screen to add a regatta either manually or by downloading it from the AMYA")
                +
                Text("\nSwiping left will delete the regatta, and swiping right will CLONE it which is a great way to copy skipper details to your next regatta")
            }
        }

    }

2      

I eventually discovered the advice to only have one alert per view and was able to make this work. I still don't know if that is a hard and fast rule, and why it behaved differently in 17 vs 16.

2      

It seems like you're encountering an issue when presenting alerts in iOS 17. The error message suggests a problem with the view hierarchy. This could be due to changes in how views are managed in iOS 17.

I'd recommend checking if there have been any updates or changes in SwiftUI that might affect the way alerts are presented. Additionally, you might want to explore SwiftUI forums or Apple's developer community for specific solutions related to iOS 17.

Hope this helps you in troubleshooting the issue!

2      

If you would like a solution to alert management, try something like this :

ALERT OVERLAY VIEW

import SwiftUI
import Combine

struct AlertOverlay<Content: View>: View {
    var content: Content
    @ObservedObject var manager: AlertOverlayManager

    init(
        _ manager: AlertOverlayManager,
        @ViewBuilder content: @escaping () -> Content
    ) {
        self.manager = manager
        self.content = content()
    }

    var body: some View {
        ZStack {
            content.blur(
                radius: manager.showingAlert
                    ? Constants.UserInterfaceDefaults.blurRadius
                    : 0
            )

            // Show a toast alert if the current settings are to show the alert as a toast
            if manager.showingToast, let alert = manager.currentAlertModel {
                VStack {
                    Spacer()
                    ToastView(
                        alert: alert,
                        dismissAction: { manager.dismiss() }
                    )
                        .padding()
                }
            }
        }
        .alert(isPresented: $manager.showingAlert) {
            guard let alertModel = manager.currentAlertModel else {
                return Alert(title: Text("Unexected internal error as occured."))
            }

            // Support only one of each
            let primaryAction = alertModel.actions.first(where: { $0.style == .primary }) ??
                AlertModel.defaultPrimaryAction
            let secondaryAction = alertModel.actions.first(where: { $0.style == .secondary })

            let primaryButton = Alert.Button.default(Text(primaryAction.title)) {
                primaryAction.handler()
                self.manager.dismiss()
            }

            let titleText = Text(alertModel.presentedTitle)
                .font(.title)

            // Select the alert type based on the number of actions
            let alert: Alert
            if let secondaryAction {
                let textView = Text(secondaryAction.title)
                let secondaryButton = Alert.Button.default(textView) {
                    secondaryAction.handler()
                    self.manager.dismiss()
                }
                alert = Alert(
                    title: titleText,
                    message: Text(alertModel.message),
                    primaryButton: primaryButton,
                    secondaryButton: secondaryButton
                )
            } else {
                alert = Alert(
                    title: titleText,
                    message: Text(alertModel.message),
                    dismissButton: primaryButton
                )
            }

            return alert
        }
    }
}

OVERLAY MANAGER

import Foundation
import Combine

class AlertOverlayManager: ObservableObject {

    @Published var currentAlertModel: AlertModel? = nil
    @Published var showingAlert: Bool = false
    @Published var showingToast: Bool = false
    @Published private var alertStack = [AlertModel]()

    private var currentTimeToastTask: Task<(), Never>? = nil
    private var cancellables = Set<AnyCancellable>()

    init(alertState: SafePublisher<AlertModel>) {
        alertState
            .receive(on: RunLoop.main)
            .sink(receiveValue: { [weak self] model in
                guard let self else { return }
                Logger.verbose(topic: .appState, message: "Alert displayed with model \(model)")
                if case .clearAll = model.style {
                    self.alertStack.removeAll()
                    self.showingAlert = false
                    self.showingToast = false
                } else {
                    self.alertStack.append(model)
                    self.presentAlert(with: model)
                }

            })
            .store(in: &cancellables)
    }

    @MainActor
    func dismiss() {
        showingAlert = false
        showingToast = false
        currentAlertModel = nil
        currentTimeToastTask = nil

        guard alertStack.popLast() != nil,
            let nextAlertModel = alertStack.last else {
            return
        }

        Task {
            // Note, we have to sleep here otherwise the alert won't appear if two are present simultaneously.
            try? await Task.sleep(seconds: 0.1)
            presentAlert(with: nextAlertModel)
        }
    }

    func presentAlert(with model: AlertModel) {
        if let currentTimeToastTask {
            currentTimeToastTask.cancel()
        }

        currentAlertModel = model

        switch model.style {
        case .blockingModal:
            showingToast = false
            showingAlert = true
        case let .timedToast(timeInterval):
            currentTimeToastTask = Task {
                try? await Task.sleep(seconds: timeInterval)

                if !Task.isCancelled {
                    await dismiss()
                }
            }
            fallthrough
        case .blockingToast:
            showingAlert = false
            showingToast = true
        default:
            Logger.info(topic: .appState, message: "AlertOverlayManager - Unhandled model")
            break //
        }
    }
}

Useage

struct AlertOverlay_Previews: PreviewProvider {
    static let publisher = PassthroughSubject<AlertModel, Never>()
    static var internalView: some View {
        VStack {
            Spacer()
            Group {
                Text("Some Test Text")
                Text("Some Test Text")
                Text("Some Test Text")
            }
            Spacer()
            Group {
                Text("Some Test Text")
                Text("Some Test Text")
                Text("Some Test Text")
            }
            Spacer()
            Group {
                Text("Some Test Text")
                Text("Some Test Text")
                Text("Some Test Text")
            }
            //Spacer()
        }
    }

    static var previews: some View {
        let manager = AlertOverlayManager(
            alertState: publisher.eraseToAnyPublisher()
        )

        // Alert Test
        AlertOverlay(
            manager
        ) {
            internalView
        }
        .task {
            let firstState = AlertModel.blocking(withTitle: "First!", message: "Warning!")
            publisher.send(firstState)
            try? await Task.sleep(seconds: 2)

            let delayedState = AlertModel.blocking(withTitle: "Delayed Number 1", message: "With message...")
            publisher.send(delayedState)
            try? await Task.sleep(seconds: 2)

            let actionState = AlertModel.blocking(
                withTitle: "Delayed Options Number 2",
                message: "With message...",
                actions: [
                    .init(title: "Ok", handler: { /* do nothing */ }, style: .primary),
                    .init(title: "Cancel", handler: { /* do nothing */ }, style: .secondary)
                ]
            )
            publisher.send(actionState)
            try? await Task.sleep(seconds: 2)

            let blockingToastState = AlertModel.blockingToast(withMessage: "Blocking toast")
            publisher.send(blockingToastState)
            try? await Task.sleep(seconds: 2)

            let nonBlockingToastState = AlertModel.timedToast(
                withMessage: "Non-Blocking toast",
                interval: 4
            )
            publisher.send(nonBlockingToastState)
            try? await Task.sleep(seconds: 2)

            let blockingState = AlertModel.blocking(
                withTitle: "Delayed Options Number 3",
                message: "With message...",
                actions: [
                    .init(title: "Ok", handler: { /* do nothing */ }, style: .primary),
                    .init(title: "Cancel", handler: { /* do nothing */ }, style: .secondary)
                ]
            )
            publisher.send(blockingState)
        }

        // Toast Test
        AlertOverlay(
            manager
        ) {
            internalView
        }
        .task {
            let firstState = AlertModel.blockingToast(withMessage: "Blocking 1")
            publisher.send(firstState)
            try? await Task.sleep(seconds: 2)

            let secondState = AlertModel.timedToast(withMessage: "Time Toast", interval: 5)
            publisher.send(secondState)
            try? await Task.sleep(seconds: 2)

            let thirdState = AlertModel.blockingToast(withMessage: "Blocking 2")
            publisher.send(thirdState)
        }

        // Clear all alerts test
        AlertOverlay(
            manager
        ) {
            internalView
        }
        .task {
            let firstState = AlertModel.blockingToast(withMessage: "Blocking 1")
            publisher.send(firstState)
            try? await Task.sleep(seconds: 2)

            let actionState = AlertModel.blocking(
                withTitle: "Delayed Options Number 2",
                message: "With message...",
                actions: [
                    .init(title: "Ok", handler: { /* do nothing */ }, style: .primary),
                    .init(title: "Cancel", handler: { /* do nothing */ }, style: .secondary)
                ]
            )
            publisher.send(actionState)
            try? await Task.sleep(seconds: 2)

            let secondState = AlertModel.timedToast(withMessage: "Time Toast", interval: 5)
            publisher.send(secondState)
            try? await Task.sleep(seconds: 2)

            let thirdState = AlertModel.blockingToast(withMessage: "Blocking 2")
            publisher.send(thirdState)
            try? await Task.sleep(seconds: 2)

            let clearState = AlertModel.clearAllAlerts()
            publisher.send(clearState)
        }
    }

}

2      

Hacking with Swift is sponsored by Essential Developer.

SPONSORED Join a FREE crash course for mid/senior iOS devs who want to achieve an expert level of technical and practical skills – it’s the fast track to being a complete senior developer! Hurry up because it'll be available only until July 28th.

Click to save your free spot now

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.