使您现有的业务解决方案适应SwiftUI。第3部分。使用体系结构

祝大家有美好的一天!我和Usetech的领先移动开发人员Anna Zharkova一起,我们将

继续分解SwiftUI的复杂性。可以在链接上找到前面的部分:

第1
部分第2部分

今天,我们将讨论架构的功能,以及如何将现有的业务逻辑转移并嵌入到SwiftUI应用程序中。

SwiftUI中的标准数据流基于View与包含属性和状态变量(即状态变量)的某个模型的交互。因此,将MVVM推荐为SwiftUI应用程序的架构伙伴是合乎逻辑的。苹果建议将其与Combine框架结合使用,该框架引入了声明性的Api SwiftUI以随着时间的推移处理值。ViewModel实现ObservableObject协议,并作为ObservedObject连接到特定的View。



可修改的模型属性声明为@Published。

class NewsItemModel: ObservableObject {
    @Published var title: String = ""
    @Published var description: String = ""
    @Published var image: String = ""
    @Published var dateFormatted: String = ""
}

与传统的MVVM中一样,ViewModel与数据模型(即业务逻辑)进行通信,并以一种或另一种View形式传输数据。

struct NewsItemContentView: View {
    @ObservedObject var moder: NewsItemModel
    
    init(model: NewsItemModel) {
        self.model = model 
    }
    //... - 
}

就像几乎其他任何模式一样,MVVM也有拥塞和
冗余的趋势。重载的ViewModel始终取决于业务逻辑的突出显示和抽象程度。视图的负载取决于元素对状态变量的依赖以及转换到其他视图的复杂性。

在SwiftUI中,添加的是View是一个结构,而不是一个class,因此不支持继承,从而导致代码重复。
如果在小型应用程序中这不是很关键,那么随着功能的增加和逻辑的复杂性,过载变得至关重要,并且大量的复制粘贴会受到抑制。

在这种情况下,让我们尝试使用干净的代码和干净的体系结构的方法。毕竟,我们不能完全放弃MVVM,但DataFlow SwiftUI是基于它构建的,但是要重建的还很多。

警告!

如果您对有关体系结构的文章过敏,并且“干净代码”从短语中找出来,请向下滚动几段。
这不是Bob叔叔提供的Clean代码!


是的,我们不会采用鲍伯叔叔的“清洁代码”的最纯粹形式。对于我来说,其中包含过度工程。我们只会采取一个想法。

干净代码的主要思想是创建可读性最强的代码,然后可以轻松地对其进行扩展和修改。

建议遵循很多软件开发原则。



很多人都知道它们,但不是每个人都喜欢,也不是每个人都使用它们。这是针对holivar的单独主题。

为了确保代码的纯度,至少有必要将代码分为功能层和模块,使用问题的一般解决方案并实现组件之间交互的抽象。至少您需要将UI代码与所谓的业务逻辑分开。

无论选择哪种架构模式,使用数据库和网络,处理和存储数据的逻辑都与UI和应用程序本身的模块分开。同时,这些模块与服务或存储的实现一起工作,这些实现又访问网络请求的常规服务或常规数据存储。可以初始化某个应用程序模块(模块业务逻辑)最终访问的常规容器中的变量的初始化,您可以通过该变量来访问一项或另一项服务。



如果我们选择并抽象了业务逻辑,那么我们可以根据需要安排模块组件之间的交互。

原则上,所有现有的iOS应用程序模式都遵循相同的原理。



总有业务逻辑,有数据。还有一个呼叫管理器,负责显示和转换要输出的数据以及输出转换后的数据的位置。唯一的区别是角色在组件之间的分配方式。


因为我们努力使应用程序可读,以简化当前和将来的更改,将所有这些角色分开是合乎逻辑的。我们的业务逻辑已经被突出显示,数据总是分开的。保留调度员,演示者和视图。结果,我们得到了一个由View-Interactor-Presenter组成的体系结构,其中交互器与业务逻辑服务进行交互,演示者转换数据并将其作为ViewModel的一种提供给我们的View。以一种很好的方式,导航和配置也可以从View中提取到单独的组件中。



我们获得了VIP + R架构,并将有争议的角色划分为不同的组件。

让我们尝试看一个例子。我们有一个
用SwiftUI和MVVM编写的小型新闻聚合器应用程序



该应用程序具有3个独立的屏幕,具有自己的逻辑,即3个模块:


  • 新闻列表模块;
  • 新闻屏幕模块;
  • 新闻搜索模块。

每个模块都包含一个ViewModel和一个View,该ViewModel与选定的业务逻辑进行交互,该ViewModel显示了ViewModel广播给它的内容。



我们努力确保ViewModel仅与存储准备显示的数据有关。现在,他从事访问服务和处理结果的工作。

我们将这些角色转移给为每个
模块设置的演示者和交互者



交互器将从服务接收的数据传递给演示者,该演示者用准备好的数据填充绑定到View的现有ViewModel。原则上,关于模块业务逻辑的分离,一切都很简单。 


现在去查看。让我们尝试处理强制代码重复。如果我们正在处理某种控件,则可能是他的样式或设置。如果我们正在谈论屏幕上的视图,那么:

  • 屏幕样式
  • 常用的UI元素(LoadingView);
  • 信息警报;
  • 一些通用方法。

我们不能使用继承,但可以完全使用composition正是基于这一原则,在SwiftUI中创建了所有自定义视图。

因此,我们创建了一个View容器,在其中传输了所有相同的逻辑,然后将屏幕上的View传递给容器初始化程序,然后将其用作正文中的内容View。

struct ContainerView<Content>: IContainer, View where Content: View {
    @ObservedObject var containerModel = ContainerModel()
    private var content: Content

    public init(content: Content) {
        self.content = content
    }

    var body : some View {
        ZStack {
            content
            if (self.containerModel.isLoading) {
                LoaderView()
            }
        }.alert(isPresented: $containerModel.hasError){
            Alert(title: Text(""), message: Text(containerModel.errorText),
                 dismissButton: .default(Text("OK")){
                self.containerModel.errorShown()
                })
        }
    }

屏幕上的视图嵌入在ContainerView内部的ZStack中,该视图还包含用于显示LoadingView的代码和用于显示信息警报的代码。

我们还需要ContainerView从内部View的ViewModel接收信号并更新其状态。我们无法通过@Observed订阅
与内部View 相同的模型,因为我们将拖动其信号。



因此,我们通过委托模式与其建立通信,对于容器的当前状态,我们使用其自己的ContainerModel。

class ContainerModel:ObservableObject {
    @Published var hasError: Bool = false
    @Published var errorText: String = ""
    @Published var isLoading: Bool = false
    
    func setupError(error: String){
     //....
       }
    
    func errorShown() {
     //...
    }
    
    func showLoading() {
        self.isLoading = true
    }
    
    func hideLoading() {
        self.isLoading = false
    }
}

ContainerView实现IContainer协议;实例引用分配给嵌入式View模型。

protocol  IContainer {
    func showError(error: String)
    
    func showLoading()
    
    func hideLoading()
}

struct ContainerView<Content>: IContainer, View where Content: View&IModelView {
    @ObservedObject var containerModel = ContainerModel()
    private var content: Content

    public init(content: Content) {
        self.content = content
        self.content.viewModel?.listener = self
    }
    //- 
}

View实现IModelView协议以封装模型访问并统一一些逻辑。出于相同目的的模型实现了IModel协议:

protocol IModelView {
    var viewModel: IModel? {get}
}

protocol  IModel:class {
   //....
    var listener:IContainer? {get set}
}

然后,已经在该模型中,必要时调用了委托方法,例如,显示带有错误的警报,其中容器模型的状态变量发生了变化。

struct ContainerView<Content>: IContainer, View where Content: View&IModelView {
    @ObservedObject var containerModel = ContainerModel()
    private var content: Content

    //- 
    func showError(error: String) {
        self.containerModel.setupError(error: error)
    }
    
    func showLoading() {
        self.containerModel.showLoading()
    }
    
    func hideLoading() {
        self.containerModel.hideLoading()
    }
}

现在,我们可以通过切换到ContainerView来统一View的工作。
在配置以下模块和导航时,这将极大地便利我们的生活。
如何在SwiftUI中配置导航并进行干净的配置,我们将在下一部分中讨论

您可以在此处找到示例的源代码

All Articles