Search code examples
swiftuiuialertviewcore-bluetoothswift5

Multiple Alerts in one view can only the last alert works always in swiftui


I have two alert which is called if the boolean is true.

Alert - 1 - It is called if there is any issues with the bluetooth state other than powered on.This is called directly from a swift package named BLE. The code snippet is below.

Alert - 2 - It is called when you want to unpair the peripheral giving the user two options.Unpair or remain on the same page.

Issue : Both the alert seems to be working fine but if they are not placed in the same view. When I place the alert in the same view the last displayed alert is called from the sequence top to bottom.

The OS reads the first alert but only activates the second alert if it's called.

Is there a way to make both alert functional if they are called. I referred to below solution but i was getting the same results.

Solution 1 and Solution 2

There are 2 Code snippets

1. Main Application

import SwiftUI
import BLE

struct Dashboard: View {

    @EnvironmentObject var BLE: BLE
    @State private var showUnpairAlert: Bool = false

    private var topLayer: HeatPeripheral {
        self.BLE.peripherals.baseLayer.top
    }

    var body: some View {
        VStack(alignment: .center, spacing: 0) {
            // MARK: - Menu Bar
            VStack(alignment: .center, spacing: 4) {
                Button(action: {
                    print("Unpair tapped!")
                    self.showUnpairAlert = true
                }) {
                    HStack {
                        Text("Unpair")
                            .fontWeight(.bold)
                            .font(.body)
                    }
                    .frame(minWidth: 85, minHeight: 35)
                    .cornerRadius(30)
                }
            }

        }
        .onAppear(perform: {
            self.BLE.update()
        })

            // Alert 1 - It is called if it meets one of the cases and returns the alert
            // It is presented in the function centralManagerDidUpdateState
            .alert(isPresented: $BLE.showStateAlert, content: { () -> Alert in

                let state = self.BLE.centralManager!.state
                var message = ""

                switch state {
                case .unknown:
                    message = "Bluetooth state is unknown"
                case .resetting:
                    message = "Bluetooth is resetting..."
                case .unsupported:
                    message = "This device doesn't have a bluetooth radio."
                case .unauthorized:
                    message = "Turn On Bluetooth In The Settings App to Allow Battery to Connect to App."
                case .poweredOff:
                    message = "Turn On Bluetooth to Allow Battery to Connect to App."
                    break
                @unknown default:
                    break
                }

                return Alert(title: Text("Bluetooth is \(self.BLE.getStateString())"), message: Text(message), dismissButton: .default(Text("OK")))
            })

            // Alert 2 - It is called when you tap the unpair button

            .alert(isPresented: $showUnpairAlert) {
                Alert(title: Text("Unpair from \(checkForDeviceInformation())"), message: Text("*Your peripheral command will stay on."), primaryButton: .destructive(Text("Unpair")) {
                    self.unpairAndSetDefaultDeviceInformation()
                    }, secondaryButton: .cancel())
        }
    }
    func unpairAndSetDefaultDeviceInformation() {
        defaults.set(defaultDeviceinformation, forKey: Keys.deviceInformation)
        disconnectPeripheral()
        print("Pod unpaired and view changed to Onboarding")
        self.presentationMode.wrappedValue.dismiss()
        DispatchQueue.main.async {
            self.activateLink = true
        }

    }
    func disconnectPeripheral(){
        if skiinBLE.peripherals.baseLayer.top.cbPeripheral != nil {
            self.skiinBLE.disconnectPeripheral()
        }
    }

}

2. BLE Package

import SwiftUI
import Combine
import CoreBluetooth

public class BLE: NSObject, ObservableObject {

    public var centralManager: CBCentralManager? = nil
    public let baseLayerServices = "XXXXXXXXXXXXXXX"
    let defaults = UserDefaults.standard
    @Published public var showStateAlert: Bool = false

    public func start() {
        self.centralManager = CBCentralManager(delegate: self, queue: nil, options: nil)
        self.centralManager?.delegate = self
    }

    public func getStateString() -> String {
        guard let state = self.centralManager?.state else { return String() }
        switch state {
        case .unknown:
            return "Unknown"
        case .resetting:
            return "Resetting"
        case .unsupported:
            return "Unsupported"
        case .unauthorized:
            return "Unauthorized"
        case .poweredOff:
            return "Powered Off"
        case .poweredOn:
            return "Powered On"
        @unknown default:
            return String()
        }
    }

}

extension BLE: CBCentralManagerDelegate {

    public func centralManagerDidUpdateState(_ central: CBCentralManager) {
        print("state: \(self.getStateString())")
        if central.state == .poweredOn {
            self.showStateAlert = false

            if let connectedPeripherals =  self.centralManager?.retrieveConnectedPeripherals(withServices: self.baseLayerServices), connectedPeripherals.count > 0 {
                print("Already connected: \(connectedPeripherals.map{$0.name}), self.peripherals: \(self.peripherals)")
                self.centralManager?.stopScan()

            }
            else {
                print("scanForPeripherals")
                self.centralManager?.scanForPeripherals(withServices: self.baseLayerServices, options: nil)
            }
        }
        else {
            self.showStateAlert = true // Alert is called if there is any issue with the state.
        }
    }
}

Thank You !!!


Solution

  • The thing to remember is that view modifiers don't really just modify a view, they return a whole new view. So the first alert modifier returns a new view that handles alerts in the first way. The second alert modifier returns a new view that modifies alerts the second way (overwriting the first method) and that's the only one that ultimately is in effect. The outermost modifier is what matters.

    There are couple things you can try, first try attaching the different alert modifiers to two different view, not the same one.

    Second you can try the alternate form of alert that takes a Binding of an optional Identifiable and passes that on to the closure. When value is nil, nothing happens. When the state of changes to something other than nil, the alert should appear.

    Here's an example using the alert(item:) form as opposed to the Bool based alert(isPresented:).

    enum Selection: Int, Identifiable {
      case a, b, c
      var id: Int { rawValue }
    }
    
    
    struct MultiAlertView: View {
    
      @State private var selection: Selection? = nil
    
      var body: some View {
    
        HStack {
          Button(action: {
            self.selection = .a
          }) { Text("a") }
    
          Button(action: {
            self.selection = .b
          }) { Text("b") }
    
        }.alert(item: $selection) { (s: Selection) -> Alert in
          Alert(title: Text("selection: \(s.rawValue)"))
        }
      }
    }