trifork / TriforkSwiftExtensions

Generic Trifork Swift Extensions
MIT License
8 stars 6 forks source link

Globally queued UIAlertViewControllers #42

Closed ghost closed 5 years ago

ghost commented 5 years ago

Requested (and implemented) by @ktvtrifork :

//
//  GlobalDialog.swift
//  Created by Kasper Tvede on 18/02/2019.
//  Copyright Β© 2019 Trifork a/s. All rights reserved.
//

// inspiration drawn from https://github.com/agilityvision/FFGlobalAlertController
//  && https://github.com/lostatseajoshua/Alerts/blob/master/Alerts/AlertCoordinator.swift
// and further see https://stackoverflow.com/a/25428409
import Foundation
import UIKit

// Declare a global var to produce a unique address as the assoc object handle for the alert window variable
private var alertWindowObjectHandle: UInt8 = 0
// Declare a global var to produce a unique address as the assoc object handle for the dialog id variable
private var dialogIdObjectHandle: UInt8 = 1

//static variables for functionality; kept private as this is quite edgy already.
private var numOfGlobalDialogsShowing: Int = 0

private var dialogIdSet: Set<Int> = Set()

// MARK: - 
extension UIAlertController {

    /// This creates a property on a ui alert controller via objective C.
    private var alertWindow: UIWindow? {
        get {
            return objc_getAssociatedObject(self, &alertWindowObjectHandle) as? UIWindow
        }
        set {
            objc_setAssociatedObject(self, &alertWindowObjectHandle, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }
    /// This creates a property on a ui alert controller via objective C.
    private var dialogId: Int? {
        get {
            return objc_getAssociatedObject(self, &dialogIdObjectHandle) as? Int
        }
        set {
            objc_setAssociatedObject(self, &dialogIdObjectHandle, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }

    //can safely be called from any thread, and anywhere. multiple calls will just stack up.
    func showGlobally(animated: Bool = true) {
        DispatchQueue.main.async {
            numOfGlobalDialogsShowing += 1
            let window = UIWindow(frame: UIScreen.main.bounds)
            let rootController = UIViewController()
            window.rootViewController = rootController
            //above all else
            window.windowLevel = UIWindow.Level.alert + 1
            self.alertWindow = window
            window.makeKeyAndVisible()
            rootController.present(self, animated: animated)
        }
    }

    /// Only shows the igen dialog iff there are no global dialogs there already.
    ///
    /// - Parameter animated: true if the dialog should be animated in place
    func showGloballyIfNoGlobalIsPresented(animated: Bool = true) {
        if numOfGlobalDialogsShowing > 0 {
            return
        }
        showGlobally(animated: animated)
    }

    /// a more advanced editon; where we simply map each dialog to a given id, and iff that id is presented then skips displaying it (akk preventing dual
    ///   displaing these dialogs).
    ///
    /// - Parameters:
    ///   - id: the unique id, (so if mulitple dialogs contain this, then only the first one will be shown, prevents displaying multiple dialogs)
    ///   - animated: whenever the appearence should be animated
    func showGloballyWithId(id: Int, animated: Bool = true) {
        //force it though the main thread, thus ensuring no data raceing, since 1 thing can at max execute at the mainthread.
        DispatchQueue.main.async {
            if dialogIdSet.contains(id) {
                return
            }
            dialogIdSet.insert(id)
            self.dialogId = id
            self.showGlobally(animated: animated)
        }
    }
    //clean up when the dialog gets closed.
    override open func viewDidDisappear(_ animated: Bool) {
        super.viewDidAppear(animated)
        //cleanup
        numOfGlobalDialogsShowing -= 1
        if let id = dialogId {
            dialogIdSet.remove(id)
        }
        //dismiss all visible things.
        alertWindow?.isHidden = true
        //and make it possible to garbage collect
        alertWindow = nil
    }

}
ghost commented 5 years ago

I don't like that we override viewDidDisappear. There can be side effects that can be hard to find. If i create Custom UIAlertController overrides viewDidDisappear then this will nok work correctly.

I think this should be moved to its own little framework. Even it is only a couple if classes. Because this don't belongs to extensions. More like a final class GlobalyAlertController: UIAlertController { }

If we start to add those kind of classes here, there will fast come a lot of other classes and then TriforkSwiftExtensions will be bloated.

Rather 100 micro frameworks than one huge bloated.

ghost commented 5 years ago

Swift Package Manager and the Swift community is heavily using a lot of small packages with only one functionality.

Look at https://github.com/vapor

ghost commented 5 years ago

I agree with @kdvtrifork. I didn't see the override of viewDidDisappear at first. I like the concept of the global alert, but it should probably be its own framework as @kdvtrifork suggests.

What do you think about that @ktvtrifork ? πŸ˜„ πŸš€

ghost commented 5 years ago

We will of course make it open source. It is easier to integrate with Carthage then! πŸš€

ktvtrifork commented 5 years ago

Well that is a good point @kdvtrifork however, this would then concern multiple things in this repo as for example https://github.com/trifork/TriforkSwiftExtensions/tree/master/TriforkSwiftExtensions/TriforkSwiftExtensions/Reusable would be more in the "it should have its own micro framework", since it deviates from the "Extension" part as well as this does. Perhaps the question should then be; Should we create a "container" project with all these micro projects ( git sub project) and then create these smaller projects for themselves ?

the only options for this is really 1) integrate 2) not integrate, 3) its own framework 4) conditionally compile it in, but this kinda breaks a lot. 5) rewrite it to let us create a custom UIAlertViewController that we instead can do the magic on. (but that makes it hardly and easier to use or argue about)

ghost commented 5 years ago

I see your point @ktvtrifork with Reusable.swift I was also in doubt about this too. The main difference I see between those two is that we only use computed properties. So no side effects at all. We could also remove Reusable.swift and get same functionality I think, as we have default implementations on UITableViewCell and UITableViewHeaderFooterView.

Likewise i think that TSLogger.swift should be its own logger project πŸ˜„ But that is another talk!

I don't think a container framework would give any benefits. It take 2 sec to add a new framework to Carthage and I think it is also easier to control versions that way. Related to CI, when we have so many repos that it becomes a problem we should find a solution then. We don't wanna solve problems that don't exists yet. πŸ€–

I also agree with @tkctrifork if the framework depends to much on other there might be a problem. I know that we can't avoid it 100% but we can try to achieve it.

ghost commented 5 years ago

Removed reusable πŸ‘€πŸ€·β€β™‚οΈ https://github.com/trifork/TriforkSwiftExtensions/pull/45

ghost commented 5 years ago

We all agreed to create a "EnhancedSwiftPlatform" (working title) repository with a very strict policy for "platform only enhancements", such as this global UIAlertController stuff can be placed. Closing issue and continuing to enforce extension-stuff only in this repository.

ghost commented 5 years ago

https://github.com/Microsoft/azure-pipelines-agent/issues/1633#issuecomment-423611815