使您现有的业务解决方案适应SwiftUI。第4部分。导航和配置

祝大家有美好的一天!有了你,我,安娜Zharkova,领先的移动开发者Usetech。
现在,让我们来谈谈在SwiftUI,导航另一个有趣的点。

如果您错过了该系列中的前几篇文章,可以在这里阅读:

第1
部分第2
部分第3部分

随着可视部分说明的更改和向声明性语法的过渡,SwiftUI应用程序中的导航控件也已更改。直接拒绝使用UIViewContoller;不直接使用UINavigationController。它由NavigationView代替。

@available(iOS 13.0, OSX 10.15, tvOS 13.0, *)
@available(watchOS, unavailable)
public struct NavigationView<Content> : View where Content : View {

    public init(@ViewBuilder content: () -> Content)

    //....
}

本质上是UINavigationController及其功能的包装。


主要的转换机制是NavigationLink(segue的类似物),它在主体View代码中立即设置。

public struct NavigationLink<Label, Destination> : View where Label : View, 
                                                                                           Destination : View {
.
    public init(destination: Destination, @ViewBuilder label: () -> Label)

    public init(destination: Destination, isActive: Binding<Bool>, 
                   @ViewBuilder label: () -> Label)

    public init<V>(destination: Destination, tag: V, selection: Binding<V?>,
                          @ViewBuilder label: () -> Label) where V : Hashable
//....
}

创建NavigationLink时,它指示进行过渡的视图以及NavigationLink包装的视图,即与之交互时,会激活NavigationLink。有关Apple文档中初始化NavigationLink的可能方式的更多信息,

但是,请记住,由于封装的原因,无法直接访问View堆栈,只能对导航进行编程,只能返回一级返回,然后通过封装的代码返回“后退”按钮



同样在SwiftUI中,没有动态软件导航。如果过渡不与触发事件(例如,单击按钮)相关,而是由于某种逻辑而跟随,则不要这样做。到下一个View的过渡必须绑定到NavigationLink机制,在描述包含它们的View时立即以声明方式设置。所有。

如果我们的屏幕应该包含到许多不同屏幕的过渡,那么代码变得很麻烦:

  NavigationView{
            NavigationLink(destination: ProfileView(), isActive: self.$isProfile) {
                Text("Profile")
            }
            NavigationLink(destination: Settings(), isActive: self.$isSettings) {
                           Text("Settings")
                       }
            NavigationLink(destination: Favorite(), isActive: self.$isFavorite) {
                Text("Favorite")
            }
            NavigationLink(destination: Login(), isActive: self.$isLogin) {
                Text("Login")
            }
            NavigationLink(destination: Search(), isActive: self.$isSearch) {
                Text("Search")
            }
}

我们可以
通过几种方式管理链接:-通过@Binding属性控制NavigationLink活动

 NavigationLink(destination: ProfileView(), isActive: self.$isProfile) {
                Text("Profile")
            }

-通过条件(状态变量)控制链接的创建

       if self.isProfile {
            NavigationLink(destination: ProfileView()) {
                Text("Profile")
            }
}

第一种方法为我们增加了监视控制变量状态的工作。
如果我们计划向前导航超过1个级别,那么这是一项非常困难的任务。

对于类似元素列表的屏幕,一切看起来都很紧凑:

 NavigationView{
        List(model.data) { item in
            NavigationLink(destination: NewsItemView(item:item)) {
            NewsItemRow(data: item)
            }
        }

我认为NavigationLink的最严重问题是指定的所有View链接都不是惰性的。它们不是在链接触发时创建的,而是在创建时创建的。如果我们有许多元素的列表或到许多不同的View大量内容的过渡,那么这不会以最佳方式影响我们应用程序的性能。如果我们仍然使用逻辑将ViewModel附加到这些View,在实现中未考虑或未正确考虑生命周期View,则情况将变得非常困难。

例如,我们有一个新闻列表,其中包含相同类型的元素。我们尚未浏览过单个新闻的单个屏幕,并且模型已经挂在我们的记忆中:



在这种情况下,我们可以做些什么来使我们的生活更轻松?

首先,回想一下View不存在于真空中,而是在UIHostingController中呈现。

open class UIHostingController<Content> : UIViewController where Content : View {

    public init(rootView: Content)

    public var rootView: Content
//...
}

这就是UIViewController。因此,我们可以执行以下操作。我们将转移到新UIHostingController内的下一个View的所有责任转移到当前View的控制器。让我们创建将在View中调用的导航和配置模块。

使用UIViewController的导航器将如下所示:

class Navigator {
    private init(){}
    
    static let shared = Navigator()
    
    private weak var view: UIViewController?
    
    internal weak var nc: UINavigationController?
        
   func setup(view: UIViewController) {
        self.view = view
    }
     
  internal func open<Content:View>(screen: Content.Type, _ data: Any? = nil) {
     if let vc = ModuleConfig.shared.config(screen: screen)?
        .createScreen(data) {
        self.nc?.pushViewController(vc, animated: true)
        }
   }

按照相同的原则,我们将创建一个配置器工厂,这将为我们提供特定模块的配置器的实现:

protocol IConfugator: class {
    func createScreen(_ data: Any?)->UIViewController
}

class ModuleConfig{
    private init(){}
    static let shared = ModuleConfig()
    
    func config<Content:View>(screen: Content.Type)->IConfugator? {
        if screen == NewsListView.self {
            return NewsListConfigurator.shared
        }
      // -
        return nil
    }
}

导航器根据屏幕类型询问特定模块的配置器,并将所有必要的信息传递给它。

class NewsListConfigurator: IConfugator {
    static let shared = NewsListConfigurator()
    
    func createScreen(_ data: Any?) -> UIViewController {
         var view = NewsListView()
        let presenter = NewsListPresenter()
         let interactor = NewsListInteractor()
        
        interactor.output = presenter
        presenter.output = view
        view.output = interactor
        
        let vc = UIHostingController<ContainerView<NewsListView>>
                    (rootView: ContainerView(content: view))
        return vc
    }
}

配置器提供了UIViewController,它是导航器,并将UINavigationController推送到共享堆栈上。



将代码中的NavigationLink替换为对Navigator的调用。作为触发,我们将有一个单击列表项的事件:

  List(model.data) { item in
            NewsItemRow(data: item)
                .onTapGesture {
                Navigator.shared.open(screen: NewsItemView.self, item)
            }
        }

没有什么可以阻止我们以相同的方式以任何View方法调用Navigator的。不只是在体内。

除了代码明显变得更清晰的事实之外,我们还卸载了内存。毕竟,使用这种方法,只有在调用时才创建视图。



现在,我们的SwiftUI应用程序更易于扩展和修改。代码干净漂亮。
您可以在此处找到示例代码

下次,我们将讨论更深层的Combine实施。

All Articles