探索 SwiftUI:面向协议的编程技巧与应用

发表时间: 2022-06-02 23:33

SwiftUI 展示了一种新的、更快、更高效的视图构建方式。 声明式编程是一项了不起的技术,SwiftUI 以及 Android 上的 Jetpack Compose 和 Flutter 的 Widget 正在使 w 视图构建变得愉快和主动。 但是,构建视图并不是移动开发人员生活的唯一部分。 在瞬息万变的环境中设计一个良好的、可扩展的和有效的 UI 可以很好地与任何类型的模型配合使用是一个相当大的挑战。


一般是怎么处理的?

在最常见和最简单的情况下,有两个对象:

  • 模型:从存储库中获取,可以是来自 API 的数据、来自数据库的记录或来自多种服务的其他类型的数据。 模型通常公开公开字段,很少公开方法。
  • View:实现 View 协议的结构,在最好的情况下只有一个依赖。

依赖关系图如下所示:

和代码:

struct UserListView: View {    var viewModel: UserViewModel        var body: some View {        switch viewModel.state {        case .loading:            LoaderView()        case .loaded(let users):            List(users) {                UserRow(user: $0)            }        case .failure(let error):            Text(error.localzableDescriptioin)        }    }}struct UserRow: View {    let user: User        var body: some View {        VStack(alignment: .leading) {            Text("\(user.id): \(user.name)")            Text(user.email)        }    }}

将模型直接注入视图是一个......有效的想法。 不要理解我的错误,我并不是说这是错误或错误的方法。 我相信当模型是原始类型时,这些解决方案运行良好,例如

  • 细绳
  • 布尔值
  • 数字

将上述类型注入视图可以避免多余的样板代码,如额外的抽象层或仅为满足多余需求而创建的 ViewModel 类。 此外,当一个大视图被分成多个小视图时,更改特定的视图很容易,特别是当它们不依赖于模型,而是依赖于原始类型时。

第二个,也是非常流行的方法来自 MVVM 架构。 在这种架构中,大多数视图都有自己的 ViewModel。

MVVM 在模型和视图之间实施了一个额外的层,称为 ViewModel。 模型和视图对象与前面的例子没有什么不同,一个重要的区别是如何将可显示的数据传递给视图。

struct UserListView: View {    @ObservedObject var viewModel: UserViewModel    var body: some View {        VStack {            List(viewModel.elements) {                UserRow(viewModel: $0)            }        }        .onAppear {            viewModel.fetchData()        }    }}class UserViewModel: ObservableObject {    @Published var elements: [UserRowViewModel] = []    private let userRepository: UserRepository    init(userRepository: UserRepository) {        self.userRepository = userRepository    }    func fetchData() {        elements = userRepository.fetch().map { UserRowViewModel(user: $0) }    }}struct UserRow: View {    let viewModel: UserRowViewModel    var body: some View {        VStack(alignment: .leading) {            Text(viewModel.header)            Text(viewModel.description)        }    }}struct UserRowViewModel: Identifiable {    var id: String {        user.id    }    var header: String {        "\(user.id): \(user.name)"    }    var description: String {        user.email    }    let user: User}

这通常是解决视图层和模型层之间的依赖问题的好方法。在开发人员中广为人知。这种方法的最大优点是

  • 层分离:抽象负责什么一目了然,领域模型和UI没有依赖关系
  • 可测试性:视图模型通常很容易测试
  • 视图模型通信:发布者或委托可以从父视图模型传递到子视图模型并在上层处理

但我也看到了一些缺点:

  • 不同类型的来源:ViewModel 只接受一个模型。意味着一个类不可扩展,如果出现新的业务案例,则必须重写它。例如。

出现了附加要求,在用户列表中,它不仅必须是用户,还必须是组,这是一个完全不同的模型。

  • 样板代码和 ViewModel 实际上只是模型的包装。除了公开一个字段以供查看外,别无他法。
  • 在大多数情况下,为每个视图创建一个新的视图模型似乎是一种过度工程

知道了这些问题,我们可以顺利地继续:


面向协议的方法

我推荐介于 MVVM 和原始模型之间的东西——视图依赖。为了避免:

  • 很多视图模型文件,通常只为私有 MVVM 架构而创建,什么都不做,只描述如何将模型转换为视图。

并确保:

  • 良好的可测试性水平
  • 轻松采用不同类型的资源
  • 在需要时轻松从协议转换为视图模型

协议方法的主要目标是创建一个易于适应和灵活的环境,以适应任何类型的业务需求。

它的关键部分是一个通常称为 DisplayableModel 的协议,它描述了应该在单个视图上确切显示的内容。

它可能看起来像这样

protocol UserRowDisplayableModel {    var name: String { get }    var avatar: ImageSource { get }}

可显示协议定义了视图所需的所有数据。 在这里,它是一个名称变量,描述每个用户行视图都有一个名称文本元素和图像源,以便在屏幕上显示用户头像。 可显示模型内部没有模型,适用于任何类型的业务逻辑。 其中的属性对于正确显示视图至关重要。 里面没有更多的处理。 DisplayableModel 的目的是尽可能的清晰和小巧。

视图接受 DisplayableModel 协议作为入口点。

struct UserRow: View {    let model: UserRowDisplayableModel    var body: some View {        // Body    }}

由 UserRow 处理的每个模型都必须实现 DisplayableModel 协议,例如

接下来,必须在 UserView 上显示的每个模型都必须实现 DisplayableModel 协议,例如

extension User: UserRowDisplayableModel {    var name: String {        "\(firstName), \(lastName)"    }    var avatar: ImageSource {        ImageSource(url: avatarUrl)    }}extension Group: UserRowDisplayableModel {    var name: String {        "\(groupName)"    }    var avatar: ImageSource {        MixedAvatarImageSource(urls: users.map { $0.avatar })    }}

剩下的就看开发商了。 可显示的模型可以来自 ViewModel、observable store 或您希望的任何来源。


使用这种技术,您可以构建快速、可扩展且易于采用的视图。


视图模型示例

我想结合最著名的架构 MVVM 来展示它的外观。 此外,当您只下载一个模型时,我还展示了一个比普通案例复杂一点的商业案例示例。 要求与上述相同:

您不仅要显示用户,还要在同一个列表视图中显示组

class ViewModel: ObservableObject {    @Published var elements: [UserRowDisplayableModel] = []    func fetch() {        Task {            async let users = await fetchUsers() // Return [User]            async let group = await fetchGroups() // Return [Group]            elements = await users + group        }    }    /// Private methods}
  1. 可显示模型存储在数组中并标记为已发布
  2. 当视图出现时,或者当它需要时,方法 fetch 被调用
  3. 在其中,ViewModel 异步获取两个不同的模型:Group 和 User
  4. 因为两者都实现了 UserRowDisplayableModel 它可以很容易地传递给已发布的元素。


结论

协议是迄今为止 Swift 拥有的最好的特性之一。 它允许编写一个简单、描述良好且清晰的代码,该代码具有一个且只有一个易于理解的目的。

我强烈建议所有开发人员将大部分代码封装到协议中,不仅因为团队中的其他工程师清晰易懂,而且更容易测试。