Diese Präsentation wurde erfolgreich gemeldet.
Die SlideShare-Präsentation wird heruntergeladen. ×

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

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

YouTube-Videos werden auf SlideShare nicht mehr unterstützt.

Original auf YouTube ansehen

AppDelegate
UIResponder
<UIApplicationDelegate>
Wird geladen in …3
×

Hier ansehen

1 von 33 Anzeige

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

Herunterladen, um offline zu lesen

Вадим Смаль объяснил, что плохого в содержании одного огромного класса и как решить эту проблему с помощью разработанного им компонента.

Rambler.iOS - митапы iOS-разработчиков, организуемые компанией RAMBLER&Co.

Вадим Смаль объяснил, что плохого в содержании одного огромного класса и как решить эту проблему с помощью разработанного им компонента.

Rambler.iOS - митапы iOS-разработчиков, организуемые компанией RAMBLER&Co.

Anzeige
Anzeige

Weitere Verwandte Inhalte

Diashows für Sie (20)

Andere mochten auch (20)

Anzeige

Ähnlich wie Rambler.iOS #6: App delegate - разделяй и властвуй (20)

Weitere von RAMBLER&Co (20)

Anzeige

Aktuellste (20)

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

  1. 1. AppDelegate разделяй и властвуй Вадим Смаль iOS разработчик Rambler&Co
  2. 2. AppDelegate UIResponder <UIApplicationDelegate>
  3. 3. • Реагирует на получение уведомлений • Реагирует на ключевые изменения в состоянии вашего приложения • Реагирует на события, которые нацелены на само приложение • Управляет процессом сохранения и восстановления состояния приложения
  4. 4. Запуск приложения Изменение состояния приложения Восстановление состояния приложения Загрузка данных в фоне Локальные и удаленные уведомления Пользовательская активность WatchKit Открытие URL’ов HealthKit Системные события Разрешения для расширений Геометрия интерфейса Window CoreData SharedInstance Quick Actions <UIApplicationDelegate>
  5. 5. 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
  6. 6. 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") } Настройка приложения
  7. 7. 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 }
  8. 8. 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
  9. 9. 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’ов Разрешения для расширений
  10. 10. 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
  11. 11. 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) } Загрузка данных в фоне Локальные и удаленные уведомления
  12. 12. 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
  13. 13. // 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) } } Стек навигации
  14. 14. AppDelegate: 45 методов ~1000 строк кода ~ 30 зависимостей
  15. 15. Принцип единственной обязанности Принцип разделения интерфейса Тестируемость Расширяемость
  16. 16. <UIApplicationDelegate> Логика
  17. 17. StartConfigurator ThirdPartiesConfigurator ApplicationConfigurator AppStateConfigurator HandoffHandler SpotlightIndexer QuickActionHandler … … … AppDelegate
  18. 18. didFinishLaunchingWithOptions { [self.thirdPartiesConfigurator configure] [self.startConfigurator configure] … [self.applicationConfigurator configure] [self.handoffHandler activate] [self.spotlightIndexer activate] … [self.quickActionHandler activate] … }
  19. 19. @interface AppDelegate : UIResponder <UIApplicationDelegate> @property UIWindow *window; @property startConfigurator; @property thirdPartiesConfigurator; @property applicationConfigurator; ... @property handoffHandler; @property quickActionHandler ; @property NSArray *urlHandlers; @property forceLogoutHandler; ... @end
  20. 20. AppDelegate: 45 методов ~200 строк кода ~20 зависимостей
  21. 21. Принцип единственной обязанности Принцип разделения интерфейса Тестируемость God object
  22. 22. <UIApplicationDelegate> Зависимости
  23. 23. Launching Search RemoteNotification QuickAction URLHandler Handoff ApplicationState BackgroundData AppDelegateProxy AppDelegate Array<UIApplicationDelegate>
  24. 24. @implementation RemoteNotificationAppDelegate - didFinishLaunchingWithOptions { if (notification) { [self.pushNotificationCenter process:notification]; } return YES; } - didRegisterForRemoteNotificationsWithDeviceToken: { [self.pushNotificationCenter didRegisterDeviceToken:token]; } - didReceiveRemoteNotification { [self.pushNotificationCenter processWithUserInfo:userInfo]; } @end
  25. 25. AppDelegate: ~ 3 метода ~50 строк кода ~2 зависимостей
  26. 26. Принцип единственной обязанности Принцип разделения интерфейса Тестируемость Принципип “разделяй и властвуй”
  27. 27. <UIApplicationDelegate> AppDelegateProxy
  28. 28. RemoteNotificationAppDelegateAppDelegateProxyPUSH NOTIFICATION <PushNotificationCenter><StartUpConfigurator> <NavigationStackBuilder> Present navigation stack
  29. 29. int main(int argc, char * argv[]) { @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([RCMAppDelegateProxy class])); } }
  30. 30. @interface RCMAppDelegateProxy : NSProxy - (void)addAppDelegate:(id<UIApplicationDelegate>)delegate; - (void)addAppDelegates:(NSArray *)delegates; @end
  31. 31. Где еще использовать? <UITableViewDelegate>/<UITableViewDataSource> <UICollectionViewDelegate>/ <UICollectionViewDataSource> <UITextViewDelegate>
  32. 32. Спасибо! RamblerAppDelegateProxy https://github.com/rambler-ios/RamblerAppDelegateProxy

×