Swift: Notification permissions pop up affecting class functionality

Viewed 40

This is a hard one to explain as I'm not entirely sure what's going wrong here. In my AppDelegate I add an array of services to the app:

    override var services: [ApplicationService] {
    if UIApplication.isRunningUnitTests() {
        return []
    }
    
    return [
        SettingsService(),
        TrackingApplicationService(),
        NotificationApplicationService(),
        AudioApplicationService(),
        ConnectionService()
    ]
}

The connection service is the one I am interested in here. It looks like this:

   import PluggableAppDelegate
import Connectivity

final class ConnectionService: NSObject, ApplicationService {
    
    // MARK: - Helpers
    
    private struct Constants {
        static let containerHeight: CGFloat = 20
        static let containerPadding: CGFloat = 20
        static let statusCenterAdjustment: CGFloat = -1
        static let statusFont: UIFont = .systemFont(ofSize: 15, weight: .semibold)
        static let labelLines = 1
        static let radiusDenominator: CGFloat = 2.0
        static let animationDuration = 0.2
        static let defaultAnchorConstant: CGFloat = 0
    }
    
    internal enum ConnectionStatus {
        case connected
        case disconnected
    }
    
    // MARK: - Properties
    
    private let connectivity = Connectivity()
    private let queue = DispatchQueue.global(qos: .utility)
    internal var currentStatus = ConnectionStatus.connected
    
    private var statusTopAnchor: NSLayoutConstraint?
    
    private lazy var statusContainer: UIView = {
        let container = UIView()
        container.translatesAutoresizingMaskIntoConstraints = false
        container.backgroundColor = UIColor.red
        container.layer.cornerRadius = Constants.containerHeight / Constants.radiusDenominator
        container.layer.maskedCorners = [.layerMaxXMaxYCorner, .layerMinXMaxYCorner]
        container.isHidden = true
        container.addSubview(statusLabel)

        NSLayoutConstraint.activate([
            statusLabel.centerYAnchor.constraint(equalTo: container.centerYAnchor, constant: Constants.statusCenterAdjustment),
            statusLabel.leftAnchor.constraint(equalTo: container.leftAnchor, constant: Constants.containerPadding),
            statusLabel.rightAnchor.constraint(equalTo: container.rightAnchor, constant: -Constants.containerPadding)
            ])

        return container
    }()

    private lazy var statusLabel: UILabel = {
        let label = UILabel()
        label.translatesAutoresizingMaskIntoConstraints = false
        label.numberOfLines = Constants.labelLines
        label.font = Constants.statusFont
        label.textAlignment = .center
        label.textColor = .white
        label.text = Strings.common.user.noInternetConnection.localized
        return label
    }()
    
    // MARK: - Lifecycle
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
        DispatchQueue.main.async { [weak self] in
            self?.prepareUI()
        }
        prepareConnectivityListener()
        updateConnectionStatus(connectivity.status)
        
        return true
    }
    
    // MARK: - Connectivity
    
    func updateConnectionStatus(_ status: Connectivity.Status) {
        switch status {
        case .connected, .connectedViaWiFi, .connectedViaCellular:
            updateLabelWith(connectionStatus: .connected)
        case .notConnected, .connectedViaWiFiWithoutInternet, .connectedViaCellularWithoutInternet:
            updateLabelWith(connectionStatus: .disconnected)
        }
        
        NotificationCenter.default.post(name: .connectivityStatusChanged, object: self.currentStatus)
    }
    
    
    // MARK: - Configuration
    
    private func prepareUI() {
        guard let window = UIApplication.shared.keyWindow else {
            assertionFailure("We require a Window")
            return // Don't setup in release if we hit this, no point
        }
        
        window.addSubview(statusContainer)
        statusTopAnchor = statusContainer.topAnchor.constraint(equalTo: window.topAnchor, constant: -Constants.containerHeight)
        
        guard let statusTopAnchor = statusTopAnchor else {
            assertionFailure("Top anchor could not be build")
            return
        }
        
        NSLayoutConstraint.activate([
            statusTopAnchor,
            statusContainer.centerXAnchor.constraint(equalTo: window.centerXAnchor),
            statusContainer.heightAnchor.constraint(equalToConstant: Constants.containerHeight)
            ])
    }
    
    private func prepareConnectivityListener() {
        let connectivityChanged: (Connectivity) -> Void = { [weak self] connectivity in
            self?.updateConnectionStatus(connectivity.status)
        }
        
        connectivity.whenConnected = connectivityChanged
        connectivity.whenDisconnected = connectivityChanged
        connectivity.startNotifier(queue: queue)
    }
    
    private func updateLabelWith(connectionStatus: ConnectionStatus) {
        if currentStatus == connectionStatus {
            return // No need to update if we are the same state
        } else {
            currentStatus = connectionStatus
        }
        
        let topAnchorConstant: CGFloat = (connectionStatus == .connected) ? -Constants.containerHeight : Constants.defaultAnchorConstant
        
        DispatchQueue.main.async { [weak self] in
            DispatchQueue.main.async { [weak self] in
                if connectionStatus != .connected {
                    self?.statusContainer.isHidden = false
                }

                self?.statusTopAnchor?.constant = topAnchorConstant

                UIView.animate(withDuration: Constants.animationDuration,
                               animations: { [weak self] in
                                   self?.statusContainer.superview?.layoutIfNeeded()
                               },
                               completion: { _ in
                                    if connectionStatus == .connected {
                                        self?.statusContainer.isHidden = true
                                    }
                })
            }
        }
    }
}

So I'm using this service to check the connection status of the device and on the login view controller I display different warnings depending on the connection status.

However, my problem is is that the connection service is not functioning properly when the permissions pop up appears. As you can see from the ConnectionService class, an alert should be generated when the app is offline. Now because I am adding this connection service before the permissions are requested, the window briefly shows the alert, but as soon as the permissions alert appears, it disappears.

Furthermore, you can see in ConnectionService.swift I have a listener set up:

        NotificationCenter.default.post(name: .connectivityStatusChanged, object: self.currentStatus)

I'm listening for this in the subsequent viewController which triggers this method:

    @objc private func connectivityChanged(notification: Notification) {
    if notification.object as? ConnectionService.ConnectionStatus == .connected {
        deviceConnected = true
    } else {
        deviceConnected = false
    }
}

This in turn triggers a UI change:

    private var deviceConnected: Bool = true {
    willSet {
        DispatchQueue.main.async {
            if newValue == true {
                self.tabBarItem.image = #imageLiteral(resourceName: "retrieve_completed")
                self.errorLabel?.isHidden = true
            } else {
                self.tabBarItem.image = #imageLiteral(resourceName: "retrieve_offline")
                self.errorLabel?.isHidden = false
                self.errorLabel?.text = Strings.eHandshake.warning.deviceOffline.title.localized
            }
        }
    }
}

When permissions have already been granted and therefore the app launches without requesting permission, this works fine, but when the permissions alert appears, this seems to break this part of the logic.

To be clear the services var is initially declared here:

public protocol ApplicationService: UIApplicationDelegate {}

extension ApplicationService {
public var window: UIWindow? {
    return UIApplication.shared.delegate?.window ?? nil
}
}

open class PluggableApplicationDelegate: UIResponder, 
UIApplicationDelegate {

public var window: UIWindow?

open var services: [ApplicationService] { return [] }

internal lazy var _services: [ApplicationService] = {
    return self.services
}()

@discardableResult
internal func apply<T, S>(_ work: (ApplicationService, @escaping (T) -> Void) -> S?, completionHandler: @escaping ([T]) -> Swift.Void) -> [S] {
    let dispatchGroup = DispatchGroup()
    var results: [T] = []
    var returns: [S] = []

    for service in _services {
        dispatchGroup.enter()
        let returned = work(service, { result in
            results.append(result)
            dispatchGroup.leave()
        })
        if let returned = returned {
            returns.append(returned)
        } else { // delegate doesn't implement method
            dispatchGroup.leave()
        }
    }

    dispatchGroup.notify(queue: .main) {
        completionHandler(results)
    }

    return returns
}
}

Any help with this would be much appreciated.

0 Answers
Related