针对UIKit开发人员的UIKit和SwiftUI应用程序中基于组合的MVVM



我们知道ObservableObject类及其@Published属性是Combine专门为View Modelin 创建的SwiftUI。但是在架构 View ModelUIKit实现中  可以使用完全相同的方法MVVM,尽管在这种情况下,我们将必须手动将元素“绑定”(bindUIl @Published 属性View Mode。您会感到惊讶,但是在此帮助下,Combine可以执行几行代码。此外,在设计UIKit应用程序时秉承这种思想,您随后将轻松切换到SwiftUI

这篇文章的目的是用古朴例子来告诉你如何能实现优雅的MVVM建筑UIKit Combine。相比之下,我们展示了相同的用法View Model c SwiftUI

本文将讨论两个简单的应用程序,这些应用程序使您可以从OpenWeatherMa p 网站选择特定城市的最新天气信息。但是 UI其中一个将通过应用程序创建,SwiftUI另一个将通过帮助创建UIKit。对于用户而言,这些应用程序看起来几乎相同。



代码在Github上

用户界面(UI)将仅包含2个UI 元素:用于输入城市的文本字段和用于显示温度的标签。用于输入城市的文本框是主动INPUT(Input),温度标签是被动EXIT(Output)。   架构中

的角色是,它从(或获取INPUT ,实现应用程序的业务逻辑,然后将OUTPUTS传递回  (或),并可能以所需的格式呈现此数据。 创建  同步或异步- -如果你用一个很简单的无论什么业务逻辑的一种使用它的类属性。View Model MVVMViewViewControllerUIKitViewViewControllerUIKit

View ModelCombineObservableObject @Published

API OpenWeatherMap


尽管OpenWeatherMap服务    允许选择非常广泛的天气信息,但是我们感兴趣的数据的模型将非常简单,它提供了WeatherDetail有关所选城市当前天气的详细信息  ,并且位于Model.swift文件中 



尽管在此特定任务中,我们仅对温度感兴趣temp,在结构中  Main,该模型作为根结构提供了有关当前天气的完整详细信息  WeatherDetail,认为将来您将希望扩展此应用程序的功能。该结构WeatherDetail 是可编码的,这使我们仅用两行代码即可将JSON数据按字面意义解码  为模型。

结构  WeatherDetail 也应该是Identifiable如果我们要使它更容易为自己在未来的显示的天气预报阵列  [WeatherDetail] 事先以列表的形式了好几天  List 的SwiftUI。对于将来更复杂的当前天气应用程序来说,这也是空白。该协议Identifiable要求存在 id,我们已经拥有的财产,因此不需要我们做出额外的努力。

通常,包括OpenWeatherMap服务在内的服务  都提供各种服务URLs 来获取我们所需的资源。OpenWeatherMap服务  使我们URLs能够获取有关某个城市5天当前天气或天气预报的详细信息city。在此应用中,我们仅对当前天气信息感兴趣,在这种情况下URL使用函数计算absoluteURL (city: String)



API 对于OpenWeatherMap服务, 我们将其放置在WeatherAPI.swift文件中。其中心部分将是一种选择WeatherDetail城市中  详细天气信息的方法  city

  • fetchWeather (for city: String) -> AnyPublisher<WeatherDetail, Never>

在框架的上下文中,Combine 此方法不仅返回详细的天气信息WeatherDetail,而且还返回  相应的“发布者” Publisher我们的“发布者” AnyPublisher<WeatherDetail, Never>不会返回任何错误- Never如果仍然发生采样或编码错误,则代理人返回时  WeatherDetail.placeholder不会包含任何其他有关错误原因的消息。 

更详细地考虑  fetchWeather (for city: String) -> AnyPublisher<WeatherDetail, Never>OpenWeatherMap网站选择 城市详细天气信息 city并且不返回任何错误的方法Never



  1. 根据城市的名字,我们  city 的形式URL 使用功能absoluteURL(city:city)要求的详细天气信息  WeatherDetail
  2. «» dataTaskPublisher(for:), Output (data: Data, response: URLResponse),   Failure - URLError,
  3. map { } (data: Data, response: URLResponse)  data
  4. JSON  data ,  WeatherDetail, ,
  5. - «»  catch (error ... )  «» WeatherDetail.placeholder,
  6. main , UI,
  7. «» «» eraseToAnyPublisher() AnyPublisher.

如此获得的异步“发布者” AnyPublisher本身“不会起飞”;直到有人“订阅”它之前,它不会交付任何东西。我们将在  同时在和中ObservableObject 起作用类中使用它  。 View ModelSwiftUIUIKit

创建视图模型


View Model创建一个非常简单的类  TempViewModel,它实现的协议ObservableObject 有两个  @Published 属性:  



  1. 一个  @Published var city: String是城市(您可以有条件地将其称为“入口”,因为其值由用户控制View),  
  2. 第二个  @Published var currentWeather = WeatherDetail.placeholder 是当前该城市的天气(我们可以有条件地将该属性称为EXIT,因为它是通过从OpenWeatherMap网站获取数据获得的  )。

设置  @Published 属性后  city,我们就可以将其用作简单属性  city和“发布者”  $city

在一个类中  TempViewModel,您不仅可以声明我们感兴趣的属性,还可以规定它们交互的业务逻辑。为此,在初始化类实例  TempViewModel 时,init?我们可以创建一个“订阅”,该订阅将在类实例的整个“生命周期”中运行,  TempViewModel并再现当前天气currentWeather 对城市  的依赖性  city

为此,Combine我们将链从输入“发布者” $city 延伸到输出“发布者” AnyPublisher<WeatherDetail, Never>,其值为当前天气。随后,我们在“订阅者”的帮助下“订阅”它 assign (to: \.currentWeather, on: self) 并获得当前天气的期望值  currentWeather 作为“输出”  @Published 属性。

我们不仅必须从属性中拉出链条city,也不仅仅是从$city将要参与创建的“发布者” 那里拉出链UI 那里我们将对其进行更改。

我们将如何做?

我们 fetchWeather (for city: String)的类库中  已经有一个函数,该函数可以 根据城市WeatherAPI返回AnyPublisher<WeatherDetail, Never>带有详细天气信息  的“发布者” city,并且只能以某种方式使用“发布者”的值  $city将其转换为该函数的参数。

 转到合适的发行商  fetchWeather (for city: String) 以  Combine帮助我们运营商  flatMap



运营商flatMap根据从先前“发布者”接收到的数据创建一个新的“发布者”。

接下来,我们借助非常简单的“订阅者”来“订阅”这个新接收的“发布者”,  assign (to: \.currentWeather, on: self)并将从“发布者”接收到的值分配给@Published属性  currentWeather



我们刚刚创建了一个init( )异步“发布者”并对其进行“订阅”,从而得到了AnyCancellable“订阅” ”。

AnyCancellable ``订阅''允许调用者随时取消``订阅'',并且不再从``发布者''接收值,但是,一旦``  AnyCancellable订阅''离开其作用域,``发布者''所占用的内存就会被释放。因此,一旦init( ) 完成,系统将删除此“订阅” ARC,并且没有时间分配有关接收到的当前天气的异步信息,并有时间延迟  currentWeather。为了保存这样的“订阅”,有必要创建一个OUTSIDE init()变量var cancellableSetAnyCancellable该变量将在类实例的整个“生命周期” 中将我们的“订阅” 保持在该变量中  TempViewMode。  使用操作符  存储变量中

AnyCancellable“订阅” 结果,将在类实例的整个“生命周期”中保留“订阅”  。我们可以根据需要更改发布者的价值,而 该城市的当前天气将始终由我们支配 为了减少键入城市时的服务器呼叫次数 cancellableSetstore ( in: &self.cancellableSet)



TempViewModel$citycurrentWeather

city,我们不应该直接使用带有城市名称的行的“发布者”  $city,而是使用带有debounce和的修改版:和操作符removeDuplicates



操作符  debounce 用于等待用户完成在键盘上键入必要的信息,然后才执行一次资源密集型任务。

同样,运算符removeDuplicates仅在它们与任何先前值不同的情况下才会发布值。例如,如果用户先输入john,然后输入joe然后再次输入john,我们将john收到一次。这有助于UI提高我们的效率。

使用SwiftUI创建UI


现在我们有了它View Model让我们开始吧UI。首先在SwiftUI,然后在UIKit

在我们Xcode创建一个新项目时SwiftUIContentView  将其View Model作为 @ObservedObject 变量放置  model。用Text ("Hello, World!") 标题  替换  Text ("WeatherApp"),添加一个文本框以输入城市  TextField ("City", text: self.$model.city) 和一个标签以显示温度:



我们直接使用了变量的值model: TempViewModel()。我们在文本框中输入城市$model.city,并在标签中显示温度- model.currentWeather.main?.temp

现在,对@Published 属性的任何更改  都将导致“重绘” View



这是通过以下事实确保的View Model @ObservedObject,即执行我们的属性和用户界面元素()的自动“绑定”(binding这种自动“绑定”仅在中可能@PublishedView ModelUISwiftUI

使用UIKit创建UI


该怎么办UIKit不在那里  @ObservedObject在其中,  UIKit 我们将binding手动执行“绑定”()。有很多方法可以实现这种“手动绑定”:

  • Key-Value Observing KVO:用于key paths监视属性并接收更改通知的机制 
  • 功能反应式编程或FRP:使用框架Combine
  • Delegation:使用委托方法发送属性值已更改的通知。
  • Boxing: didSet { } , .

有了文章的标题,我们自然会在该领域工作Combine。在该UIKit应用程序中,我们将展示使用进行“手动绑定”有多么容易Combine

UIKit应用程序中,我们还将有两个UI 元素:UITextField用于输入城市和 UILabel显示温度。在ViewController我们自然会有Outlet这些元素:





在一个普通变量的形式viewModel,我们有相同的一个是View Model在上一节:



做“手工绑定”与之前Combine,让我们的文本字段UITextField我们的内容是我们的盟友和“出版人” text



这将使我们能够viewDidLoad使用以下功能轻松实现“手动绑定”binding ()



的确,我们“订阅”的“出版商” cityTextField.textPublisher使用非常简单的“用户”  assign (to: \.city, on: viewModel),并指定由用户在文本框中键入的文本cityTextField,以我们的“输入”  @Published的性质city我们View Model

此外,我们在另一个方向上进行了更改: 在“用户” 及其闭包的帮助下“订阅” “输出”  @Published属性  ,形成温度值并将其分配给标签“订阅”中接收的  是存储在变量中。创建它们一次之后,我们允许它们在类实例的整个“生命周期”中以及在我们的“订阅”中行动 $currentWeathersink receiveValue temperatureLabel

viewDidLoad var cancellableSetViewControllerView Model实现应用程序的所有业务逻辑。

顺便说一句,该协议ObservableObject不适用于UIKit,但不会造成干扰。UIKit 完全不关心协议,ObservableObject并且原则上可以View ModelUIKit 应用程序



中将其删除  但是我们不会这样做,因为我们希望对于View Model当前的应用程序 UIKit以及将来的将来的应用程序保持不变SwiftUI

就这样。代码在Github上

结论

功能性反应的框架,Combine可以让你非常简单和简洁地实现 MVVM二者的架构SwiftUIUIKit,并在理解的,可读的代码的形式。

链接:

组合+ UIKit + MVVM
使用Combine
iOS MVVM教程:使用适用
于iOS

PS的Combine教程的MVC MVVM进行重构如果您想查看一些天气信息,则需要在OpenWeatherMap上注册 并获取 API key此过程将花费您不超过2分钟的时间。

All Articles