AppDelegateразделяй и властвуй
Вадим Смаль iOS разработчик Rambler&Co
AppDelegate
UIResponder
<UIApplicationDelegate>
• Реагирует на получение уведомлений
• Реагирует на ключевые изменения в состоянии вашего приложения
• Реагирует на события, которые нацелены на само приложение
• Управляет процессом сохранения и восстановления состояния приложения
Запуск приложения
Изменение состояния приложения
Восстановление состояния приложения
Загрузка данных в фоне
Локальные и удаленные уведомления
Пользовательская активность
WatchKit
Открытие URL’ов
HealthKitСистемные события
Разрешения для расширений
Геометрия интерфейса
Window CoreData
SharedInstance
Quick Actions
<UIApplicationDelegate>
import Shared import Storage import AVFoundation import XCGLogger import Breakpad import MessageUI import WebImage import SwiftKeychainWrapper import LocalAuthentication
private let log = Logger.browserLogger
let LatestAppVersionProfileKey = "latestAppVersion" let AllowThirdPartyKeyboardsKey = "settings.allowThirdPartyKeyboards"
class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? var browserViewController: BrowserViewController! var rootViewController: UINavigationController! weak var profile: BrowserProfile? var tabManager: TabManager! var adjustIntegration: AdjustIntegration?
weak var application: UIApplication? var launchOptions: [NSObject: AnyObject]?
let appVersion = NSBundle.mainBundle().objectForInfoDictionaryKey("CFBundleShortVersionString") as! String
var openInFirefoxURL: NSURL? = nil
func application(application: UIApplication, willFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { // Hold references to willFinishLaunching parameters for delayed app launch self.application = application self.launchOptions = launchOptions
log.debug("Configuring window…")
self.window = UIWindow(frame: UIScreen.mainScreen().bounds) self.window!.backgroundColor = UIConstants.AppBackgroundColor
// Short circuit the app if we want to email logs from the debug menu if DebugSettingsBundleOptions.launchIntoEmailComposer { self.window?.rootViewController = UIViewController() presentEmailComposerWithLogs() return true } else { return startApplication(application, withLaunchOptions: launchOptions) } }
Импорты
Зависимости
WINDOW
startApplication
private func startApplication(application: UIApplication, withLaunchOptions launchOptions: [NSObject: AnyObject]?) -> Bool { log.debug("Setting UA…") // Set the Firefox UA for browsing. setUserAgent()
log.debug("Starting keyboard helper…") // Start the keyboard helper to monitor and cache keyboard state. KeyboardHelper.defaultHelper.startObserving()
log.debug("Starting dynamic font helper…") // Start the keyboard helper to monitor and cache keyboard state. DynamicFontHelper.defaultHelper.startObserving()
log.debug("Setting custom menu items…") MenuHelper.defaultHelper.setItems()
log.debug("Creating Sync log file…") let logDate = NSDate() // Create a new sync log file on cold app launch. Note that this doesn't roll old logs. Logger.syncLogger.newLogWithDate(logDate)
log.debug("Creating corrupt DB logger…") Logger.corruptLogger.newLogWithDate(logDate)
log.debug("Creating Browser log file…") Logger.browserLogger.newLogWithDate(logDate)
log.debug("Getting profile…") let profile = getProfile(application)
if !DebugSettingsBundleOptions.disableLocalWebServer { log.debug("Starting web server…") // Set up a web server that serves us static content. Do this early so that it is ready when the UI is presented. setUpWebServer(profile) }
log.debug("Setting AVAudioSession category…") do { // for aural progress bar: play even with silent switch on, and do not stop audio from other apps (like music) try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback, withOptions: AVAudioSessionCategoryOptions.MixWithOthers) } catch _ { log.error("Failed to assign AVAudioSession category to allow playing with silent switch on for aural progress bar") }
Настройка приложения
let defaultRequest = NSURLRequest(URL: UIConstants.DefaultHomePage) let imageStore = DiskImageStore(files: profile.files, namespace: "TabManagerScreenshots", quality: UIConstants.ScreenshotQuality)
log.debug("Configuring tabManager…") self.tabManager = TabManager(defaultNewTabRequest: defaultRequest, prefs: profile.prefs, imageStore: imageStore) self.tabManager.stateDelegate = self
// Add restoration class, the factory that will return the ViewController we // will restore with. log.debug("Initing BVC…")
browserViewController = BrowserViewController(profile: self.profile!, tabManager: self.tabManager) browserViewController.restorationIdentifier = NSStringFromClass(BrowserViewController.self) browserViewController.restorationClass = AppDelegate.self browserViewController.automaticallyAdjustsScrollViewInsets = false
rootViewController = UINavigationController(rootViewController: browserViewController) rootViewController.automaticallyAdjustsScrollViewInsets = false rootViewController.delegate = self rootViewController.navigationBarHidden = true self.window!.rootViewController = rootViewController
log.debug("Configuring Breakpad…") activeCrashReporter = BreakpadCrashReporter(breakpadInstance: BreakpadController.sharedInstance()) configureActiveCrashReporter(profile.prefs.boolForKey("crashreports.send.always"))
log.debug("Adding observers…") NSNotificationCenter.defaultCenter().addObserverForName(FSReadingListAddReadingListItemNotification, object: nil, queue: nil) { (notification) -> Void in if let userInfo = notification.userInfo, url = userInfo["URL"] as? NSURL { let title = (userInfo["Title"] as? String) ?? "" profile.readingList?.createRecordWithURL(url.absoluteString, title: title, addedBy: UIDevice.currentDevice().name) } } // check to see if we started 'cos someone tapped on a notification. if let localNotification = launchOptions?[UIApplicationLaunchOptionsLocalNotificationKey] as? UILocalNotification { viewURLInNewTab(localNotification) } adjustIntegration = AdjustIntegration(profile: profile)
// We need to check if the app is a clean install to use for // preventing the What's New URL from appearing. if getProfile(application).prefs.intForKey(IntroViewControllerSeenProfileKey) == nil { getProfile(application).prefs.setString(AppInfo.appVersion, forKey: LatestAppVersionProfileKey) }
log.debug("Updating authentication keychain state to reflect system state") self.updateAuthenticationInfo()
log.debug("Done with setting up the application.") return true }
func applicationWillTerminate(application: UIApplication) { log.debug("Application will terminate.")
// We have only five seconds here, so let's hope this doesn't take too long. self.profile?.shutdown()
// Allow deinitializers to close our database connections. self.profile = nil self.tabManager = nil self.browserViewController = nil self.rootViewController = nil }
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject : AnyObject]?) -> Bool { // Override point for customization after application launch. var shouldPerformAdditionalDelegateHandling = true
log.debug("Did finish launching.") log.debug("Setting up Adjust") self.adjustIntegration?.triggerApplicationDidFinishLaunchingWithOptions(launchOptions) log.debug("Making window key and visible…") self.window!.makeKeyAndVisible()
// Now roll logs. log.debug("Triggering log roll.") dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)) { Logger.syncLogger.deleteOldLogsDownToSizeLimit() Logger.browserLogger.deleteOldLogsDownToSizeLimit() }
if #available(iOS 9, *) { // If a shortcut was launched, display its information and take the appropriate action if let shortcutItem = launchOptions?[UIApplicationLaunchOptionsShortcutItemKey] as? UIApplicationShortcutItem {
QuickActions.sharedInstance.launchedShortcutItem = shortcutItem // This will block "performActionForShortcutItem:completionHandler" from being called. shouldPerformAdditionalDelegateHandling = false } }
log.debug("Done with applicationDidFinishLaunching.")
return shouldPerformAdditionalDelegateHandling }
Quick Actions
func application(application: UIApplication, openURL url: NSURL, sourceApplication: String?, annotation: AnyObject) -> Bool { if let components = NSURLComponents(URL: url, resolvingAgainstBaseURL: false) { if components.scheme != "firefox" && components.scheme != "firefox-x-callback" { return false } var url: String? for item in (components.queryItems ?? []) as [NSURLQueryItem] { switch item.name { case "url": url = item.value default: () } }
if let url = url, newURL = NSURL(string: url.unescape()) { // If we are active then we can ask the BVC to open the new tab right away. Else we remember the // URL and we open it in applicationDidBecomeActive. if application.applicationState == .Active { if #available(iOS 9, *) { self.browserViewController.switchToPrivacyMode(isPrivate: false) } self.browserViewController.openURLInNewTab(newURL) } else { openInFirefoxURL = newURL } return true } } return false }
func application(application: UIApplication, shouldAllowExtensionPointIdentifier extensionPointIdentifier: String) -> Bool { if let thirdPartyKeyboardSettingBool = getProfile(application).prefs.boolForKey(AllowThirdPartyKeyboardsKey) where extensionPointIdentifier == UIApplicationKeyboardExtensionPointIdentifier { return thirdPartyKeyboardSettingBool }
return true }
Открытие URL’ов
Разрешения для расширений
func applicationDidBecomeActive(application: UIApplication) { guard !DebugSettingsBundleOptions.launchIntoEmailComposer else { return }
self.profile?.syncManager.applicationDidBecomeActive()
// We could load these here, but then we have to futz with the tab counter // and making NSURLRequests. self.browserViewController.loadQueuedTabs()
// handle quick actions is available if #available(iOS 9, *) { let quickActions = QuickActions.sharedInstance if let shortcut = quickActions.launchedShortcutItem { // dispatch asynchronously so that BVC is all set up for handling new tabs // when we try and open them quickActions.handleShortCutItem(shortcut, withBrowserViewController: browserViewController) quickActions.launchedShortcutItem = nil }
// we've removed the Last Tab option, so we should remove any quick actions that we already have that are last tabs // we do this after we've handled any quick actions that have been used to open the app so that we don't b0rk if // the user has opened the app for the first time after upgrade with a Last Tab quick action QuickActions.sharedInstance.removeDynamicApplicationShortcutItemOfType(ShortcutType.OpenLastTab, fromApplication: application) }
// If we have a URL waiting to open, switch to non-private mode and open the URL. if let url = openInFirefoxURL { openInFirefoxURL = nil // This needs to be scheduled so that the BVC is ready. dispatch_async(dispatch_get_main_queue()) { if #available(iOS 9, *) { self.browserViewController.switchToPrivacyMode(isPrivate: false) } self.browserViewController.switchToTabForURLOrOpen(url) } } }
func applicationWillEnterForeground(application: UIApplication) { // The reason we need to call this method here instead of `applicationDidBecomeActive` // is that this method is only invoked whenever the application is entering the foreground where as // `applicationDidBecomeActive` will get called whenever the Touch ID authentication overlay disappears. self.updateAuthenticationInfo() }
private func updateAuthenticationInfo() { if let authInfo = KeychainWrapper.authenticationInfo() { if !LAContext().canEvaluatePolicy(.DeviceOwnerAuthenticationWithBiometrics, error: nil) { authInfo.useTouchID = false KeychainWrapper.setAuthenticationInfo(authInfo) } } }
Quick Actions
func applicationDidEnterBackground(application: UIApplication) { self.profile?.syncManager.applicationDidEnterBackground()
var taskId: UIBackgroundTaskIdentifier = 0 taskId = application.beginBackgroundTaskWithExpirationHandler { _ in log.warning("Running out of background time, but we have a profile shutdown pending.") application.endBackgroundTask(taskId) }
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)) { self.profile?.shutdown() application.endBackgroundTask(taskId) }
// Workaround for crashing in the background when <select> popovers are visible (rdar://24571325). let jsBlurSelect = "if (document.activeElement && document.activeElement.tagName === 'SELECT') { document.activeElement.blur(); }" tabManager.selectedTab?.webView?.evaluateJavaScript(jsBlurSelect, completionHandler: nil) } func application(application: UIApplication, handleActionWithIdentifier identifier: String?, forLocalNotification notification: UILocalNotification, completionHandler: () -> Void) { if let actionId = identifier { if let action = SentTabAction(rawValue: actionId) { viewURLInNewTab(notification) switch(action) { case .Bookmark: addBookmark(notification) break case .ReadingList: addToReadingList(notification) break default: break } } else { print("ERROR: Unknown notification action received") } } else { print("ERROR: Unknown notification received") } }
func application(application: UIApplication, didReceiveLocalNotification notification: UILocalNotification) { viewURLInNewTab(notification) }
Загрузка данных в фоне
Локальные и удаленные уведомления
func application(application: UIApplication, continueUserActivity userActivity: NSUserActivity, restorationHandler: ([AnyObject]?) -> Void) -> Bool { if let url = userActivity.webpageURL { browserViewController.switchToTabForURLOrOpen(url) return true } return false }
@available(iOS 9.0, *) func application(application: UIApplication, performActionForShortcutItem shortcutItem: UIApplicationShortcutItem, completionHandler: Bool -> Void) { let handledShortCutItem = QuickActions.sharedInstance.handleShortCutItem(shortcutItem, withBrowserViewController: browserViewController)
completionHandler(handledShortCutItem) }
var activeCrashReporter: CrashReporter? func configureActiveCrashReporter(optedIn: Bool?) { if let reporter = activeCrashReporter { configureCrashReporter(reporter, optedIn: optedIn) } }
public func configureCrashReporter(reporter: CrashReporter, optedIn: Bool?) { let configureReporter: () -> () = { let addUploadParameterForKey: String -> Void = { key in if let value = NSBundle.mainBundle().objectForInfoDictionaryKey(key) as? String { reporter.addUploadParameter(value, forKey: key) } }
addUploadParameterForKey("AppID") addUploadParameterForKey("BuildID") addUploadParameterForKey("ReleaseChannel") addUploadParameterForKey("Vendor") }
if let optedIn = optedIn { // User has explicitly opted-in for sending crash reports. If this is not true, then the user has // explicitly opted-out of crash reporting so don't bother starting breakpad or stop if it was running if optedIn { reporter.start(true) configureReporter() reporter.setUploadingEnabled(true) } else { reporter.stop() } } // We haven't asked the user for their crash reporting preference yet. Log crashes anyways but don't send them. else { reporter.start(true) configureReporter() } }
Пользовательская активность
Пользовательская активность
ThirdParties
// MARK: - Root View Controller Animations extension AppDelegate: UINavigationControllerDelegate { func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? { if operation == UINavigationControllerOperation.Push { return BrowserToTrayAnimator() } else if operation == UINavigationControllerOperation.Pop { return TrayToBrowserAnimator() } else { return nil } } }
extension AppDelegate: TabManagerStateDelegate { func tabManagerWillStoreTabs(tabs: [Browser]) { // It is possible that not all tabs have loaded yet, so we filter out tabs with a nil URL. let storedTabs: [RemoteTab] = tabs.flatMap( Browser.toTab )
// Don't insert into the DB immediately. We tend to contend with more important // work like querying for top sites. let queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0) dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(ProfileRemoteTabsSyncDelay * Double(NSEC_PER_MSEC))), queue) { self.profile?.storeTabs(storedTabs) } } }
extension AppDelegate: MFMailComposeViewControllerDelegate { func mailComposeController(controller: MFMailComposeViewController, didFinishWithResult result: MFMailComposeResult, error: NSError?) { // Dismiss the view controller and start the app up controller.dismissViewControllerAnimated(true, completion: nil) startApplication(application!, withLaunchOptions: self.launchOptions) } }
Стек навигации
AppDelegate:
45 методов
~1000 строк кода
~ 30 зависимостей
Принцип единственной обязанности
Принцип разделения интерфейса
Тестируемость
Расширяемость
<UIApplicationDelegate> Логика
StartConfigurator
ThirdPartiesConfigurator
ApplicationConfigurator
AppStateConfigurator
HandoffHandler
SpotlightIndexer
QuickActionHandler
…
…
…
AppDelegate
didFinishLaunchingWithOptions {
[self.thirdPartiesConfigurator configure] [self.startConfigurator configure] … [self.applicationConfigurator configure]
[self.handoffHandler activate] [self.spotlightIndexer activate] … [self.quickActionHandler activate] …
}
@interface AppDelegate : UIResponder <UIApplicationDelegate>
@property UIWindow *window;
@property startConfigurator; @property thirdPartiesConfigurator; @property applicationConfigurator; ... @property handoffHandler; @property quickActionHandler ; @property NSArray *urlHandlers; @property forceLogoutHandler; ...
@end
AppDelegate:
45 методов
~200 строк кода
~20 зависимостей
Принцип единственной обязанности
Принцип разделения интерфейса
Тестируемость
God object
<UIApplicationDelegate>
Зависимости
Launching
Search
RemoteNotification
QuickAction
URLHandler
Handoff
ApplicationState BackgroundData
AppDelegateProxy AppDelegate
Array<UIApplicationDelegate>
@implementation RemoteNotificationAppDelegate
- didFinishLaunchingWithOptions { if (notification) { [self.pushNotificationCenter process:notification]; } return YES; }
- didRegisterForRemoteNotificationsWithDeviceToken: { [self.pushNotificationCenter didRegisterDeviceToken:token]; }
- didReceiveRemoteNotification { [self.pushNotificationCenter processWithUserInfo:userInfo]; }
@end
AppDelegate:
~ 3 метода
~50 строк кода
~2 зависимостей
Принцип единственной обязанности
Принцип разделения интерфейса
Тестируемость
Принципип “разделяй и властвуй”
<UIApplicationDelegate>
AppDelegateProxy
RemoteNotificationAppDelegateAppDelegateProxyPUSH NOTIFICATION
<PushNotificationCenter><StartUpConfigurator>
<NavigationStackBuilder> Present navigation stack
int main(int argc, char * argv[]) { @autoreleasepool { return UIApplicationMain(argc,
argv, nil, NSStringFromClass([RCMAppDelegateProxy class]));
} }
@interface RCMAppDelegateProxy : NSProxy
- (void)addAppDelegate:(id<UIApplicationDelegate>)delegate;
- (void)addAppDelegates:(NSArray *)delegates;
@end
Где еще использовать?<UITableViewDelegate>/<UITableViewDataSource>
<UICollectionViewDelegate>/
<UICollectionViewDataSource>
<UITextViewDelegate>
Спасибо!RamblerAppDelegateProxy
https://github.com/rambler-ios/RamblerAppDelegateProxy