Rambler.iOS #6: App delegate - разделяй и властвуй

32
AppDelegate разделяй и властвуй Вадим Смаль iOS разработчик Rambler&Co

Transcript of Rambler.iOS #6: App delegate - разделяй и властвуй

Page 1: Rambler.iOS #6: App delegate - разделяй и властвуй

AppDelegateразделяй и властвуй

Вадим Смаль iOS разработчик Rambler&Co

Page 2: Rambler.iOS #6: App delegate - разделяй и властвуй

AppDelegate

UIResponder

<UIApplicationDelegate>

Page 3: Rambler.iOS #6: App delegate - разделяй и властвуй

• Реагирует на получение уведомлений

• Реагирует на ключевые изменения в состоянии вашего приложения

• Реагирует на события, которые нацелены на само приложение

• Управляет процессом сохранения и восстановления состояния приложения

Page 4: Rambler.iOS #6: App delegate - разделяй и властвуй

Запуск приложения

Изменение состояния приложения

Восстановление состояния приложения

Загрузка данных в фоне

Локальные и удаленные уведомления

Пользовательская активность

WatchKit

Открытие URL’ов

HealthKitСистемные события

Разрешения для расширений

Геометрия интерфейса

Window CoreData

SharedInstance

Quick Actions

<UIApplicationDelegate>

Page 5: Rambler.iOS #6: App delegate - разделяй и властвуй

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

Page 6: Rambler.iOS #6: App delegate - разделяй и властвуй

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") }

Настройка приложения

Page 7: Rambler.iOS #6: App delegate - разделяй и властвуй

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 }

Page 8: Rambler.iOS #6: App delegate - разделяй и властвуй

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

Page 9: Rambler.iOS #6: App delegate - разделяй и властвуй

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’ов

Разрешения для расширений

Page 10: Rambler.iOS #6: App delegate - разделяй и властвуй

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

Page 11: Rambler.iOS #6: App delegate - разделяй и властвуй

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) }

Загрузка данных в фоне

Локальные и удаленные уведомления

Page 12: Rambler.iOS #6: App delegate - разделяй и властвуй

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

Page 13: Rambler.iOS #6: App delegate - разделяй и властвуй

// 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) } }

Стек навигации

Page 14: Rambler.iOS #6: App delegate - разделяй и властвуй

AppDelegate:

45 методов

~1000 строк кода

~ 30 зависимостей

Page 15: Rambler.iOS #6: App delegate - разделяй и властвуй

Принцип единственной обязанности

Принцип разделения интерфейса

Тестируемость

Расширяемость

Page 16: Rambler.iOS #6: App delegate - разделяй и властвуй

<UIApplicationDelegate> Логика

Page 17: Rambler.iOS #6: App delegate - разделяй и властвуй

StartConfigurator

ThirdPartiesConfigurator

ApplicationConfigurator

AppStateConfigurator

HandoffHandler

SpotlightIndexer

QuickActionHandler

AppDelegate

Page 18: Rambler.iOS #6: App delegate - разделяй и властвуй

didFinishLaunchingWithOptions {

[self.thirdPartiesConfigurator configure] [self.startConfigurator configure] … [self.applicationConfigurator configure]

[self.handoffHandler activate] [self.spotlightIndexer activate] … [self.quickActionHandler activate] …

}

Page 19: Rambler.iOS #6: App delegate - разделяй и властвуй

@interface AppDelegate : UIResponder <UIApplicationDelegate>

@property UIWindow *window;

@property startConfigurator; @property thirdPartiesConfigurator; @property applicationConfigurator; ... @property handoffHandler; @property quickActionHandler ; @property NSArray *urlHandlers; @property forceLogoutHandler; ...

@end

Page 20: Rambler.iOS #6: App delegate - разделяй и властвуй

AppDelegate:

45 методов

~200 строк кода

~20 зависимостей

Page 21: Rambler.iOS #6: App delegate - разделяй и властвуй

Принцип единственной обязанности

Принцип разделения интерфейса

Тестируемость

God object

Page 22: Rambler.iOS #6: App delegate - разделяй и властвуй

<UIApplicationDelegate>

Зависимости

Page 23: Rambler.iOS #6: App delegate - разделяй и властвуй

Launching

Search

RemoteNotification

QuickAction

URLHandler

Handoff

ApplicationState BackgroundData

AppDelegateProxy AppDelegate

Array<UIApplicationDelegate>

Page 24: Rambler.iOS #6: App delegate - разделяй и властвуй

@implementation RemoteNotificationAppDelegate

- didFinishLaunchingWithOptions { if (notification) { [self.pushNotificationCenter process:notification]; } return YES; }

- didRegisterForRemoteNotificationsWithDeviceToken: { [self.pushNotificationCenter didRegisterDeviceToken:token]; }

- didReceiveRemoteNotification { [self.pushNotificationCenter processWithUserInfo:userInfo]; }

@end

Page 25: Rambler.iOS #6: App delegate - разделяй и властвуй

AppDelegate:

~ 3 метода

~50 строк кода

~2 зависимостей

Page 26: Rambler.iOS #6: App delegate - разделяй и властвуй

Принцип единственной обязанности

Принцип разделения интерфейса

Тестируемость

Принципип “разделяй и властвуй”

Page 27: Rambler.iOS #6: App delegate - разделяй и властвуй

<UIApplicationDelegate>

AppDelegateProxy

Page 28: Rambler.iOS #6: App delegate - разделяй и властвуй

RemoteNotificationAppDelegateAppDelegateProxyPUSH NOTIFICATION

<PushNotificationCenter><StartUpConfigurator>

<NavigationStackBuilder> Present navigation stack

Page 29: Rambler.iOS #6: App delegate - разделяй и властвуй

int main(int argc, char * argv[]) { @autoreleasepool { return UIApplicationMain(argc,

argv, nil, NSStringFromClass([RCMAppDelegateProxy class]));

} }

Page 30: Rambler.iOS #6: App delegate - разделяй и властвуй

@interface RCMAppDelegateProxy : NSProxy

- (void)addAppDelegate:(id<UIApplicationDelegate>)delegate;

- (void)addAppDelegates:(NSArray *)delegates;

@end

Page 31: Rambler.iOS #6: App delegate - разделяй и властвуй

Где еще использовать?<UITableViewDelegate>/<UITableViewDataSource>

<UICollectionViewDelegate>/

<UICollectionViewDataSource>

<UITextViewDelegate>

Page 32: Rambler.iOS #6: App delegate - разделяй и властвуй

Спасибо!RamblerAppDelegateProxy

https://github.com/rambler-ios/RamblerAppDelegateProxy