使用根控制器在iOS应用程序中组织导航



大多数移动应用程序包含十几个屏幕,复杂的过渡以及应用程序的各个部分,并按含义和目的分开。 因此,需要为应用程序组织正确的导航结构,该结构将是灵活,方便,可扩展的,可以舒适地访问应用程序的各个部分,并且还要注意系统的资源。

在本文中,我们将以某种方式设计应用程序中的导航,以避免导致内存泄漏,破坏体系结构并破坏导航结构的最常见错误。

大多数应用程序至少包含两个部分:身份验证(登录前)和私有部分(登录后)。 某些应用程序可能具有更复杂的结构,具有一次登录的多个配置文件,启动应用程序后的条件转换(深链接)等。

实际上,主要使用两种方法来导航应用程序:

  1. 演示控制器(present)和导航控制器(push)的一个导航堆栈,无法返回。 这种方法导致所有以前的ViewController保留在内存中。
  2. 使用window.rootViewController切换。 使用这种方法,以前的所有ViewController都会在内存中被破坏,但是从UI的角度来看,这看起来并不是最好的。 另外,如果有必要,它也不允许您来回移动。

现在,让我们看看如何创建一个易于维护的结构,该结构允许您在应用程序的不同部分之间无缝切换,而无需意大利面条式代码和轻松导航。

假设我们正在编写一个包含以下内容的应用程序:

  • 主屏幕( 启动屏幕 ):这是您看到的第一个屏幕,应用程序启动后,您可以添加例如动画或发出任何主API请求。
  • 认证部分 :登录,注册,密码重置,电子邮件确认等 通常会保存用户的工作会话,因此无需在每次应用程序启动时都输入登录名。
  • 主要部分 :主要应用程序的业务逻辑

应用程序的所有这些部分都相互隔离,并且各自存在于自己的导航堆栈中。 因此,我们可能需要以下转换:

  • 初始屏幕->认证屏幕 ,如果当前活动用户的当前会话不存在。
  • 启动屏幕->主屏幕,如果用户已经较早登录到该应用程序并且存在活动会话。
  • 主屏幕->身份验证屏幕 ,以防用户注销


基本设定

当应用程序启动时,我们需要初始化RootViewController ,它将首先被加载。 既可以使用代码也可以通过Interface Builder来完成。 在xCode中创建一个新项目,默认情况下所有这些操作都已经完成: main.storyboard已绑定到window.rootViewController

但是,为了专注于本文的主题,我们将不在项目中使用情节提要。 因此,删除main.storyboard ,并清除Targets-> General- > Deployment info下的“ Main Interface”字段:



现在,让我们在AppDelegate中更改didFinishLaunchingWithOptions方法,使其看起来像这样:

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,LoginViewControllerMainViewController

启动屏幕是用户启动应用程序后将看到的第一个屏幕。 此时,通常会发出所有必要的API请求,检查用户会话活动,等等。 要显示正在进行的后台操作,请使用UIActivityIndi​​catorView

 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添加到控制器,但是我将在此处仅在导航栏中添加屏幕标题和登录按钮。

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

根导航

现在回到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仍将正确放置,但是如果大小发生更改,则可能会出现问题。

添加一个新的子视图(3)并调用didMove(toParentViewController :)方法。 这将完成添加控制器操作。 RootViewController启动后 ,将立即显示SplashViewController

现在,您可以在应用程序中添加几种导航方法。 我们将显示没有任何动画的LoginViewControllerMainViewController将使用具有平滑调光效果的动画,并且在断开用户连接时的屏幕过渡将具有幻灯片效果。

 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),设置框架(3)。 将LoginController的视图添加为子视图(4),然后调用didMove(5)方法。 接下来,使用willMove(6)方法准备要卸下的当前控制器。 最后,从超级视图(7)中删除当前视图,并从RootViewController (8)中删除当前控制器。 切记更新当前控制器的值(9)。

现在让我们创建switchToMainScreen方法:

 func switchToMainScreen() { let mainViewController = MainViewController() let mainScreen = 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 mainScreen = UINavigationController(rootViewController: mainViewController) animateFadeTransition(to: mainScreen) } 

最后,让我们创建最后一个方法,该方法将负责从MainViewControllerLoginViewController的过渡:

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

AnimateDismissTransition方法提供幻灯片动画:

 private func animateDismissTransition(to new: UIViewController, completion: (() -> Void)? = nil) { new.view.frame = 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,LoginViewControllerMainViewController动画的方法调用:

 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之后立即看到到所需屏幕的过渡。



结果,我们创建了该应用程序的小型测试模型,并在其主要模块中进行了导航。 如果您需要扩展应用程序的功能,添加其他模块并在它们之间进行转换,则始终可以快速,方便地扩展和缩放此导航系统。

Source: https://habr.com/ru/post/zh-CN416147/


All Articles