
大多数移动应用程序包含十几个屏幕,复杂的过渡以及应用程序的各个部分,并按含义和目的分开。 因此,需要为应用程序组织正确的导航结构,该结构将是灵活,方便,可扩展的,可以舒适地访问应用程序的各个部分,并且还要注意系统的资源。
在本文中,我们将以某种方式设计应用程序中的导航,以避免导致内存泄漏,破坏体系结构并破坏导航结构的最常见错误。
大多数应用程序至少包含两个部分:身份验证(登录前)和私有部分(登录后)。 某些应用程序可能具有更复杂的结构,具有一次登录的多个配置文件,启动应用程序后的条件转换(深链接)等。
实际上,主要使用两种方法来导航应用程序:
- 演示控制器(present)和导航控制器(push)的一个导航堆栈,无法返回。 这种方法导致所有以前的ViewController保留在内存中。
- 使用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,LoginViewController和
MainViewController 。
启动屏幕是用户启动应用程序后将看到的第一个屏幕。 此时,通常会发出所有必要的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”) {
您绝对不会使用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() {
最后,创建
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)
添加
childViewController (1)后,我们通过将
current.view.frame设置为
view.bounds (2)来调整其大小。
如果我们跳过这一行,在大多数情况下,
viewController仍将正确放置,但是如果
帧大小发生更改,则可能会出现问题。
添加一个新的子视图(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),设置框架(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?()
此方法与
showLoginScreen非常相似,但是所有最后的步骤都是在动画完成后执行的。 为了通知转换结束的调用方法,我们在最后调用了闭包(1)。
现在,
switchToMainScreen方法的最终版本将如下所示:
func switchToMainScreen() { let mainViewController = MainViewController() let mainScreen = 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) { 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,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之后立即看到到所需屏幕的过渡。

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