Created
August 14, 2018 17:11
-
-
Save voxels/2249bbb2fad243ebf8d91c320c0d58fa to your computer and use it in GitHub Desktop.
LaunchController
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// LaunchController.swift | |
// ToyPhotoGallery | |
// | |
// Created by Voxels on 7/2/18. | |
// Copyright © 2018 Michael Edgcumbe. All rights reserved. | |
// | |
import UIKit | |
/// Class that launches potentially asynchronous launch services and signals when the expected services | |
/// have been successfully launched, or sends a failed to launch notification if the time out is reached | |
class LaunchController { | |
/// The resource model we use to create the gallery model | |
var resourceModelController:ResourceModelController | |
/// An array of notifications we need to receive before confirming that launch is complete | |
var waitForNotifications = Set<Notification.Name>() | |
/// An array of notification names for those we have already received | |
var receivedNotifications = Set<Notification.Name>() | |
/// The duration, in seconds, that the launch controller waits before timing out | |
var timeoutDuration:TimeInterval = 60 | |
/// A timer used to push launch forward if a service is not reached | |
var timeoutTimer:Timer? | |
/// Flag to indicate of the error reporting service has been launched | |
var didLaunchErrorHandler = false | |
/// We use the debug error handler until we have the Bugsnag service | |
var currentErrorHandler:ErrorHandlerDelegate { | |
return didLaunchErrorHandler ? BugsnagInterface() : DebugErrorHandler() | |
} | |
deinit { | |
NotificationCenter.default.removeObserver(self) | |
} | |
init(with modelController:ResourceModelController) { | |
resourceModelController = modelController | |
#if DEBUG | |
if FeaturePolice.showAPIKeys { | |
show(hidden: []) | |
} | |
#endif | |
} | |
/** | |
Calls the launch method for each service, retains any services that need to stay alive, | |
and assigns the notification names we need to receive before posting a *DidCompleteLaunch* notification | |
- parameter services: An array of *LaunchService* that need to be launched | |
- parameter center: the *NotificationCenter* to deregister and post *DidCompleteLaunch* on | |
- Returns: void | |
*/ | |
func launch(services:[LaunchService], with center:NotificationCenter = NotificationCenter.default) { | |
startTimeOutTimer(duration:timeoutDuration, with:center) | |
waitForLaunchNotifications(for: services, with:center) | |
attempt(services, with:center) | |
} | |
} | |
// MARK: - Launch Control | |
extension LaunchController { | |
/** | |
Registers for *DidCompleteLaunch* notification for each service in the given array | |
- parameter services: an array of *LaunchService* that should be checked for waiting to complete the launch | |
- parameter center: the *NotificationCenter* to deregister and post *DidCompleteLaunch* on | |
- Returns: void | |
*/ | |
func waitForLaunchNotifications(for services:[LaunchService], with center:NotificationCenter = NotificationCenter.default) { | |
services.forEach { (service) in | |
waitIfNecessary(service, with:center) | |
} | |
} | |
/** | |
Attempts to launch each of the services in the given array and handle the error if the launch fails | |
- parameter services: an array of *LaunchService* that need to be launched | |
- parameter center: the *NotificationCenter* used to post the *DidLaunch...* notification | |
- Returns: void | |
*/ | |
func attempt(_ services:[LaunchService], with center:NotificationCenter = NotificationCenter.default) { | |
services.forEach { (service) in | |
do { | |
try service.launch(with:service.launchControlKey?.decoded(), with:center) | |
} catch { | |
let errorHandler:ErrorHandlerDelegate = didLaunchErrorHandler ? BugsnagInterface() : DebugErrorHandler() | |
handle(error: error, with:errorHandler) | |
} | |
} | |
} | |
/** | |
Adds a check for services that the controller should wait for before sending a final *DidCompleteLaunch* notification | |
- parameter service: A *LaunchService* that needs to be checked for delaying the final *DidCompleteLaunch* notification | |
- parameter center: the *NotificationCenter* to deregister and post *DidCompleteLaunch* on | |
- Returns: void | |
*/ | |
func waitIfNecessary(_ service: LaunchService, with center:NotificationCenter = NotificationCenter.default) { | |
var shouldWaitForDidCompleteNotification = false | |
if service is RemoteStoreController { | |
shouldWaitForDidCompleteNotification = true | |
} | |
if service is ErrorHandlerDelegate { | |
shouldWaitForDidCompleteNotification = true | |
} | |
if service is BucketHandlerDelegate { | |
shouldWaitForDidCompleteNotification = true | |
} | |
if shouldWaitForDidCompleteNotification, let name = didLaunchNotificationName(for: service) { | |
waitForNotifications.insert(name) | |
register(for: name, with:center) | |
} | |
} | |
/** | |
Adds a notification to the set of received notifications and compares the set to the notifications we are waiting for to the notifications we have received. If the *receivedNotifications* are verified against the *waitForNotifications*, the *galleryModel* is constructed. | |
- parameter notification: The notification received | |
- parameter center: The notification center to post a *DidFailLaunch* notification to, if necessary | |
- Returns: void | |
*/ | |
func checkLaunchComplete(with notification:Notification, for center:NotificationCenter = NotificationCenter.default) { | |
receivedNotifications.insert(notification.name) | |
if verify(received: receivedNotifications, with: waitForNotifications) { | |
resourceModelController.delegate = self | |
let fetchQueue = DispatchQueue(label: "com.secretatomics.launchcontroller.fetch", qos: .userInteractive, attributes: [.concurrent], autoreleaseFrequency: .inherit, target: nil) | |
fetchQueue.async { [weak self] in | |
guard let strongSelf = self else { | |
return | |
} | |
strongSelf.resourceModelController.build(using: strongSelf.resourceModelController.remoteStoreController, for: ImageResource.self, on:fetchQueue, with: strongSelf.resourceModelController.errorHandler, timeoutDuration:strongSelf.timeoutDuration) | |
} | |
} | |
} | |
/** | |
Verifies that we have received all of the expected *DidCompleteLaunch* notifications | |
- parameter receivedNotifications: a set of the notifications the class has received since launch | |
- parameter expectedNotifications: a set of the notifications that we expect to receive before launch is complete | |
- Returns: True if all the expected notifications have been received, false if an expected notification hasn't been received | |
*/ | |
func verify(received receivedNotifications:Set<Notification.Name>, with expectedNotifications:Set<Notification.Name>)->Bool { | |
for expected in expectedNotifications { | |
if !receivedNotifications.contains(expected) { | |
return false | |
} | |
} | |
return true | |
} | |
/** | |
Signals that launch is complete with the *DidCompleteLaunch* notification. Resets the notification registration and time out timer for self | |
- parameter center: the *NotificationCenter* to deregister and post *DidCompleteLaunch* on | |
- Returns: void | |
*/ | |
func signalLaunchComplete(with center:NotificationCenter = NotificationCenter.default) { | |
reset(with: center) | |
center.post(name: Notification.Name.DidCompleteLaunch, object: nil) | |
} | |
/** | |
Signals that launch has failed with the *DidFailLaunch* notification. Resets the notification registration and time out timer for self | |
- parameter reason: an optional *String* to include as the reason for failure | |
- parameter center: the *NotificationCenter* to deregister and post *DidCompleteLaunch* on | |
- Returns: void | |
*/ | |
func signalLaunchFailed(reason:String?, with center:NotificationCenter = NotificationCenter.default) { | |
reset(with: center) | |
guard let reason = reason else { | |
center.post(name: Notification.Name.DidFailLaunch, object: nil) | |
return | |
} | |
let notification = Notification(name: Notification.Name.DidFailLaunch, object: nil, userInfo: [NSLocalizedFailureReasonErrorKey:reason]) | |
center.post(notification) | |
} | |
/** | |
Resets the notification sets and *timeOutTimer*, removes self from the notification center | |
- parameter center: the center to remove observer status from | |
- Returns: void | |
*/ | |
func reset(with center:NotificationCenter = NotificationCenter.default) { | |
center.removeObserver(self) | |
receivedNotifications = Set<Notification.Name>() | |
waitForNotifications = Set<Notification.Name>() | |
timeoutTimer?.invalidate() | |
timeoutTimer = nil | |
} | |
} | |
// MARK: - Launch Time Out | |
extension LaunchController { | |
/** | |
Starts the time out timer that posts a *DidFailLaunch* notification after the duration has elapsed | |
- parameter duration: the TimeInterval that the class should wait for before posting the failure notification | |
- parameter center: the *NotificationCenter* to deregister and post *DidCompleteLaunch* on | |
- Returns: void | |
*/ | |
func startTimeOutTimer(duration:TimeInterval, with center:NotificationCenter = NotificationCenter.default) { | |
timeoutTimer = Timer.scheduledTimer(withTimeInterval: duration, repeats: false, block: { [weak self] (timer) in | |
let reason = "Launch timed out after \(String(describing:self?.timeoutDuration)) seconds" | |
self?.signalLaunchFailed(reason: reason) | |
}) | |
} | |
} | |
// MARK: - Error Handling | |
extension LaunchController { | |
/** | |
Handles the error with the *errorHandlerDelegate* if one is present or an instance of *DebugErrorHandler* if the error handler hasn't been init | |
- parameter error: The error that needs to be handled | |
- parameter handler: The error handler reporting the error | |
- Returns: void | |
*/ | |
func handle(error:Error, with handler:ErrorHandlerDelegate?) { | |
guard let handler = handler else { | |
let debugHandler = DebugErrorHandler() | |
debugHandler.report(error) | |
if FeaturePolice.shouldAssertWarnings { | |
assert(false) | |
} | |
return | |
} | |
handler.report(error) | |
} | |
} | |
// MARK: - Notifications | |
extension LaunchController { | |
/** | |
Removes self from the notification center observers | |
- parameter name: The notification name to deregister | |
- parameter center: The notification center to deregister from | |
- Returns: void | |
*/ | |
func deregisterForNotification(_ name:Notification.Name, with center:NotificationCenter = NotificationCenter.default) { | |
center.removeObserver(self, name: name, object: nil) | |
} | |
/** | |
Registers self for the given notification names and assigns the handle(notification:) selector | |
- parameter name: The notification name to register | |
- parameter center: The notification center to register with | |
- Returns: void | |
*/ | |
func register(for name:Notification.Name, with center:NotificationCenter = NotificationCenter.default) { | |
deregisterForNotification(name, with:center) | |
center.addObserver(self, selector:#selector(handle(notification:)), name: name, object: nil) | |
} | |
/** | |
Assigns a *didLaunch...* notification name to a LaunchService | |
- parameter service: the *LaunchService* that needs to be checked for completion | |
- Returns: a Notification.Name for the *LaunchService* or nil if none is assigned | |
*/ | |
func didLaunchNotificationName(for service:LaunchService)->Notification.Name? { | |
if service is RemoteStoreController { | |
return Notification.Name.DidLaunchRemoteStore | |
} else if service is ErrorHandlerDelegate { | |
return Notification.Name.DidLaunchErrorHandler | |
} else if service is BucketHandlerDelegate { | |
return Notification.Name.DidLaunchBucketHandler | |
} | |
return nil | |
} | |
/** | |
Handles incoming notifications | |
- parameter notification: the notification received from the default *NotificationCenter* | |
- Returns: void | |
*/ | |
@objc func handle(notification:Notification) { | |
switch notification.name { | |
case Notification.Name.DidLaunchErrorHandler: | |
didLaunchErrorHandler = true | |
fallthrough | |
case Notification.Name.DidLaunchRemoteStore: | |
fallthrough | |
case Notification.Name.DidLaunchBucketHandler: | |
fallthrough | |
case Notification.Name.DidLaunchSharedCached: | |
checkLaunchComplete(with: notification) | |
default: | |
handle(error: LaunchError.UnexpectedLaunchNotification, with:currentErrorHandler) | |
} | |
} | |
} | |
// MARK: - GalleryViewModelDelegate | |
extension LaunchController : ResourceModelControllerDelegate { | |
/** | |
Delegate method called when the *ResourceModelController* successfully updated | |
- Returns: void | |
*/ | |
func didUpdateModel() { | |
resourceModelController.delegate = nil | |
signalLaunchComplete() | |
} | |
/** | |
Delegate method called when the *ResourceModelController* failed to update | |
- parameter reason: an optional *String* to include as the reason why the update failed | |
- Returns: void | |
*/ | |
func didFailToUpdateModel(with reason:String?) { | |
resourceModelController.delegate = nil | |
signalLaunchFailed(reason: reason) | |
} | |
} | |
// MARK: - View | |
extension LaunchController { | |
func showGalleryView(in rootViewController:UINavigationController, with resourceModelController:ResourceModelController) throws { | |
let galleryViewModel = GalleryViewModel(with: resourceModelController) | |
guard let galleryViewController = UIStoryboard.init(name: StoryboardSchemaMap.Main.rawValue, bundle: .main).instantiateViewController(withIdentifier: StoryboardSchemaMap.ViewController.GalleryViewController.rawValue) as? GalleryViewController else { | |
throw ViewError.MissingViewController | |
} | |
galleryViewController.refresh(with: galleryViewModel, for: .vertical) | |
rootViewController.pushViewController(galleryViewController, animated: false) | |
} | |
func showReachabilityView(in rootViewController:UINavigationController) { | |
// TODO: show reachability view | |
print("show reachability view") | |
} | |
static func showLockoutViewController(with window:UIWindow?, message:String?) { | |
let lockoutViewController = LockoutViewController(nibName: nil, bundle: nil) | |
lockoutViewController.message = message | |
window?.rootViewController = lockoutViewController | |
showFatalAlert(with: message ?? "Please contact the developer if you see this message.", in: lockoutViewController) | |
} | |
static func showFatalAlert(with message:String, in viewController:UIViewController?) { | |
guard let viewController = viewController else { | |
// No further recourse. The app is dead. | |
fatalError("Missing root window view controller") | |
} | |
let alertController = UIAlertController(title: "Fatal Error", message: message, preferredStyle: .alert) | |
let okAction = UIAlertAction(title: "OK", style: .default) { (action) in | |
fatalError("turtles") | |
} | |
alertController.addAction(okAction) | |
alertController.show(viewController, sender: nil) | |
} | |
} | |
// MARK: - API Key Security | |
private extension LaunchController { | |
#if DEBUG | |
/** | |
Debug method used to print the bytes for an array of *LaunchControllerKey* encrypted by the Obfuscator class | |
- parameter keys: an array of *LaunchControllerKey* to print to the console | |
- parameter handler: The *LogHandlerDelegate* responsible for displaying the string | |
*/ | |
func show(hidden keys:[LaunchControlKey], with handler:LogHandlerDelegate = DebugLogHandler()) { | |
for key in keys { | |
let bytes = key.generate(with:Obfuscator.saltObjects()) | |
handler.console("Key for \(key):") | |
handler.console("\t\(String(describing:bytes))") | |
handler.console("Decoded string:") | |
handler.console(key.decoded()) | |
handler.console("\n\n") | |
} | |
} | |
#endif | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment