Информационный портал по безопасности » Программирование » Организация навигации в iOS-приложениях с помощью Root Controller

 

Организация навигации в iOS-приложениях с помощью Root Controller

Автор: admin от 4-07-2018, 17:05, посмотрело: 203

Организация навигации в iOS-приложениях с помощью Root Controller


Большинство мобильных приложений содержит не один десяток экранов, сложные переходы, а также части приложения, разделенные по смыслу и назначению. Следовательно, возникает потребность в организации правильной структуры навигации приложения, которая будет гибкой, удобной, расширяемой, обеспечит комфортный доступ к различным частям приложения, а также будет бережно относиться к ресурсам системы.



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



У большинства приложений есть как минимум две части: аутентификации(pre-login) и закрытая часть(post-login). У некоторых приложений может быть и более сложная структура, множественные профили с одним логином, условные переходы после запуска приложения(deeplinks) и т.д.



Для перемещения по приложению на практике в основном используют два подхода:




  • Один навигационный стек и для контроллеров представления(present) и для контроллеров навигации(push), без возможности вернуться назад. Такой подход приводит к тому, что все предыдущие ViewController'ы остаются в памяти.

  • Используется переключение window.rootViewController. При таком подходе все предыдущие ViewController'ы уничтожаются в памяти, но это выглядит не лучшим образом с точки зрения UI. Также это не позволяет перемещаться вперед-назад при необходимости.



  • А теперь давайте посмотрим, как можно сделать легко поддерживаемую структуру, которая позволяет без проблем переключаться между различными частями приложения, без спагетти-кода и с удобной навигацией.



    Давайте представим, что мы пишем приложение, состоящее из:




    • Первичный экран(Splash screen): это самый первый экран, который вы видите, как только запускается приложение, туда можно добавить, например, анимацию или сделать какие-либо первичные API-запросы.

    • Экраны аутентификации(Authentification part): экраны логина, регистрации, сброса пароля, подтверждения email и т.д. Рабочая сессия пользователя обычно сохраняется, поэтому нет необходимости вводить логин каждый раз при запуске приложения.

    • Основное приложение(Main part): бизнес-логика основного приложения



    Все эти части приложения изолированы друг от друга и существуют каждая в своем навигационном стеке. Таким образом нам могут потребоваться следующие переходы:




    • Splash screen Authentication screen, в случае если текущая сессия активного пользователя отсутствует.

    • Splash screen Main screen, в случае если пользователь уже совершил ранее вход в приложение и есть активная сессия.

    • Main screen Authentication screen, в случае если пользователь разлогинился





    Базовая настройка



    Когда приложение запускается, нам необходимо инициализировать RootViewController, который будет загружаться в первую очередь. Это можно сделать как кодом, так и через Interface Builder. Создайте в xCode новый проект и все это уже будет сделано по умолчанию: main.storyboard уже привязана к window.rootViewController.



    Но для того чтобы сфокусироваться на основной теме статьи мы не будем использовать сториборды в нашем проекте. Поэтому удалите main.storyboard, а также очистите поле «Main Interface» в пункте Targets General Deployment info:



    Организация навигации в iOS-приложениях с помощью Root Controller


    Теперь давайте изменим метод didFinishLaunchingWithOptions в AppDelegate чтобы он выглядел следующим образом:



    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?)  Bool {
       window = UIWindow(frame: UIScreen.main.bounds)
       window?.rootViewController = RootViewController()
       window?.makeKeyAndVisible()
       return true
    }
    


    Теперь приложение в первую очередь запустит RootViewController. Переименуйте базовый ViewController в RootViewController:



    class RootViewController: UIViewController {
    
    }


    Это будет основной контроллер, ответственный за все переходы между различными разделами приложения. Поэтому нам будет нужна ссылка на него каждый раз, когда мы захотим совершить переход. Для этого добавим расширение в AppDelegate:



    extension AppDelegate {
       static var shared: AppDelegate {
          return UIApplication.shared.delegate as! AppDelegate
       }
    var rootViewController: RootViewController {
          return window!.rootViewController as! RootViewController 
       }
    }


    Принудительное извлечение опционала в данном случае оправдано, потому что RootViewController не меняется, и если это вдруг случайно произойдет, то падение приложения при этом является нормальной ситуацией.



    Итак, теперь у нас есть ссылка на RootViewController из любой точки приложения:



    let rootViewController = AppDelegate.shared.rootViewController


    Теперь давайте создадим еще несколько контроллеров, которые нам понадобятся: SplashViewController, LoginViewController, и MainViewController.



    Splash Screen это первый экран, который увидит пользователь после запуска приложения. В это время обычно производятся все необходимые API-запросы, проверяется активность сессии пользователя и т.д. Для отображения происходящих фоновых действий используем UIActivityIndicatorView:



    class SplashViewController: UIViewController {
       private let activityIndicator = UIActivityIndicatorView(activityIndicatorStyle: .whiteLarge)
       override func viewDidLoad() {
          super.viewDidLoad()
          view.backgroundColor = UIColor.white
          view.addSubview(activityIndicator)
          activityIndicator.frame = view.bounds
          activityIndicator.backgroundColor = UIColor(white: 0, alpha: 0.4)
          makeServiceCall()
       }
       private func makeServiceCall() {
       
       }
    }


    Для того чтобы симулировать API-запросы добавим метод DispatchQueue.main.asyncAfter с задержкой 3 секунды:



    private func makeServiceCall() {
       activityIndicator.startAnimating()
       DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .seconds(3)) {
          self.activityIndicator.stopAnimating()
       }
    }


    Полагаем, что в этих запросах также устанавливается сессия пользователя. В нашем приложении мы используем для этого UserDefaults:



    private func makeServiceCall() {
       activityIndicator.startAnimating()
       DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .seconds(3)) {
          self.activityIndicator.stopAnimating()
          
          if UserDefaults.standard.bool(forKey: “LOGGED_IN”) {
             // navigate to protected page
          } else {
             // navigate to login screen
          }
       }
    }
    


    Вы определенно не будете использовать UserDefaults для сохранения состояния сессии пользователя в релизной версии программы. Мы используем локальные настройки в нашем проекте для упрощения понимания и чтобы не выходить сильно за основную тему статьи.



    Создайте LoginViewController. Он будет использоваться для аутентификации пользователя, в том случае, если текущая сессия пользователя неактивна. Вы можете добавить в контроллер свой кастомный UI, но я добавлю сюда только заголовок экрана и кнопку логина в Navigation Bar.



    class LoginViewController: UIViewController {
       override func viewDidLoad() {
          super.viewDidLoad()
          view.backgroundColor = UIColor.white
          title = "Login Screen"
          let loginButton = UIBarButtonItem(title: "Log In", style: .plain, target: self, action: #selector(login))
          navigationItem.setLeftBarButton(loginButton, animated: true)
       }
    @objc
       private func login() {
          // store the user session (example only, not for the production)
          UserDefaults.standard.set(true, forKey: "LOGGED_IN")
          // navigate to the Main Screen
       }
    }


    И, наконец, создадим основной контроллер приложения MainViewController:



    class MainViewController: UIViewController {
       override func viewDidLoad() {
          super.viewDidLoad()
          view.backgroundColor = UIColor.lightGray // to visually distinguish the protected part
          title = “Main Screen”
          let logoutButton = UIBarButtonItem(title: “Log Out”, style: .plain, target: self, action: #selector(logout))
          navigationItem.setLeftBarButton(logoutButton, animated: true)
       }
       @objc
       private func logout() {
          // clear the user session (example only, not for the production)
          UserDefaults.standard.set(false, forKey: “LOGGED_IN”)
          // navigate to the Main Screen
       }
    }


    Root Navigation



    Теперь вернемся к RootViewController.

    Как мы говорили ранее, RootViewController это единственный объект, который отвечает за переходы между различными независимыми стеками контроллеров. Для того, чтобы быть в курсе о текущем состоянии приложения, мы создадим переменную, в которой будем хранить текущий ViewController:



    class RootViewController: UIViewController {
       private var current: UIViewController
    }


    Добавим инициализатор класса и создадим первый ViewController, который мы хотим загрузить при запуске приложения. В нашем случае это будет SplashViewController:



    class RootViewController: UIViewController {
       private var current: UIViewController
       init() {
          self.current = SplashViewController()
          super.init(nibName: nil, bundle: nil)
       }
    }


    В viewDidLoad добавим текущий viewController в RootViewController:



    class RootViewController: UIViewController {
       ...
       override func viewDidLoad() {
          super.viewDidLoad()
          
          addChildViewController(current)               // 1
          current.view.frame = view.bounds              // 2             
          view.addSubview(current.view)                 // 3
          current.didMove(toParentViewController: self) // 4
       }
    }


    Как только мы добавляем childViewController (1), мы настраиваем его размер, присваивая current.view.frame значение view.bounds (2).



    Если мы пропустим эту строку, viewController все равно будет размещен правильно в большинстве случаев, но могут появиться проблемы, если размер frame изменится.



    Добавляем новый subview(3) и вызываем метод didMove(toParentViewController:). Это завершит операцию добавления контроллера. Как только загрузится RootViewController, сразу же после этого отобразится SplashViewController.



    Теперь можно добавить несколько методов для навигации в приложении. Мы будем отображать LoginViewController без какоц-либо анимации, MainViewController будет использовать анимацию с плавным затемнением, и переход экранов при разлогинивании пользователя будет иметь эффект слайда.



    class RootViewController: UIViewController {
       ...
    func showLoginScreen() {
      
          let new = UINavigationController(rootViewController: LoginViewController())                               // 1
          addChildViewController(new)                    // 2
          new.view.frame = view.bounds                   // 3
          view.addSubview(new.view)                      // 4
          new.didMove(toParentViewController: self)      // 5
          current.willMove(toParentViewController: nil)  // 6
          current.view.removeFromSuperview()]            // 7
          current.removeFromParentViewController()       // 8
          current = new                                  // 9
    }
    


    Создайте LoginViewController(1), добавьте как дочерний контроллер(2), установите frame(3). Добавьте view LoginController'а как subview(4) и вызовите метод didMove(5). Далее, подготовим текущий контроллер к удалению методом willMove(6). Наконец, удалим текущий view из superview(7), и удалим текущий контроллер из RootViewController(8). Не забудьте обновить значение текущего контроллера(9).



    Теперь давайте создадим метод switchToMainScreen:



    func switchToMainScreen() {   
       let mainViewController = MainViewController()
       let new = UINavigationController(rootViewController: mainViewController)
       ...
    }


    Для анимации перехода потребуется другой метод:



    private func animateFadeTransition(to new: UIViewController, completion: (()  Void)? = nil) {
       current.willMove(toParentViewController: nil)
       addChildViewController(new)
       
       transition(from: current, to: new, duration: 0.3, options: [.transitionCrossDissolve, .curveEaseOut], animations: {
       }) { completed in
            self.current.removeFromParentViewController()
            new.didMove(toParentViewController: self)
            self.current = new
            completion?()  //1
       }
    }


    Этот метод очень похож на showLoginScreen, но все последние шаги выполняются после завершения анимации. Для того чтобы уведомить вызывающий метод об окончании перехода, мы в самом конце вызываем замыкание(1).



    Теперь конечный вариант метода switchToMainScreen будет выглядеть следующим образом:



    func switchToMainScreen() {   
       let mainViewController = MainViewController()
       let new = UINavigationController(rootViewController: mainViewController)
       animateFadeTransition(to: mainScreen)
    }


    И, наконец, давайте создадим последний метод, который будет отвечать за переход из MainViewController в LoginViewController:



    func switchToLogout() {
       let loginViewController = LoginViewController()
       let logoutScreen = UINavigationController(rootViewController: loginViewController)
       animateDismissTransition(to: logoutScreen)
    }


    Метод AnimateDismissTransition обеспечивает слайд-анимацию:



    private func animateDismissTransition(to new: UIViewController, completion: (()  Void)? = nil) {
       let initialFrame = CGRect(x: -view.bounds.width, y: 0, width: view.bounds.width, height: view.bounds.height)
       current.willMove(toParentViewController: nil)
       addChildViewController(new)
       transition(from: current, to: new, duration: 0.3, options: [], animations: {
          new.view.frame = self.view.bounds
       }) { completed in
          self.current.removeFromParentViewController()
          new.didMove(toParentViewController: self)
          self.current = new
          completion?()
       }
    }
    


    Это только два примера анимации, используя тот же подход можно создать любые сложные анимации, которые вам требуются



    Для завершения настройки добавим вызовы методов с анимациией из SplashViewController, LoginViewController, и MainViewController:



    class SplashViewController: UIViewController {
       ...
       private func makeServiceCall() {
          if UserDefaults.standard.bool(forKey: “LOGGED_IN”) {
             // navigate to protected page
             AppDelegate.shared.rootViewController.switchToMainScreen()
          } else {
             // navigate to login screen
             AppDelegate.shared.rootViewController.switchToLogout()
          }
       }
    }
    
    class LoginViewController: UIViewController {
       ...
       
       @objc
       private func login() {
          ...
          AppDelegate.shared.rootViewController.switchToMainScreen()
       }
    }
    
    class MainViewController: UIViewController {
       ...
       @objc
       private func logout() {
          ...
          AppDelegate.shared.rootViewController.switchToLogout()
       }
    }
    


    Скомпилируйте, запустите приложение и проверьте его работу в двух вариантах:



    — когда пользователь уже имеет активную текущую сессию(залогинен)

    — когда активной сессии нет и необходима аутентификация



    И в том и в другом случае вы должны увидеть переход на нужный экран, сразу после загрузки SplashScreen.



    Организация навигации в iOS-приложениях с помощью Root Controller


    В итоге мы создали небольшую тестовую модель приложения, с навигацией по ее основным модулям. В случае, если требуется расширение возможностей приложения, добавление дополнительных модулей и переходов между ними, вы всегда можете быстро и удобно расширять и масштабировать данную систему навигации.

    Источник: Хабр / Интересные публикации

    Категория: Программирование / Веб-разработка / iOS

    Уважаемый посетитель, Вы зашли на сайт как незарегистрированный пользователь.
    Мы рекомендуем Вам зарегистрироваться либо войти на сайт под своим именем.

    Добавление комментария

    Имя:*
    E-Mail:
    Комментарий:
    • bowtiesmilelaughingblushsmileyrelaxedsmirk
      heart_eyeskissing_heartkissing_closed_eyesflushedrelievedsatisfiedgrin
      winkstuck_out_tongue_winking_eyestuck_out_tongue_closed_eyesgrinningkissingstuck_out_tonguesleeping
      worriedfrowninganguishedopen_mouthgrimacingconfusedhushed
      expressionlessunamusedsweat_smilesweatdisappointed_relievedwearypensive
      disappointedconfoundedfearfulcold_sweatperseverecrysob
      joyastonishedscreamtired_faceangryragetriumph
      sleepyyummasksunglassesdizzy_faceimpsmiling_imp
      neutral_faceno_mouthinnocent