UI = f(State),在Swift中的一点思考

1,862 阅读4分钟

这是我参与11月更文挑战的第7天,活动详情查看:2021最后一次更文挑战

前言

我很早之前写过一个文章Flutter:枚举的缺点,大概就是在吐槽Dart语言层面上的枚举孱弱,导致有些对于状态来改变UI的功能不能在Flutter上面大展拳脚。

这话虽然就这么搁在这里了,但是其实我在Swift中进行了一点探索与思考,不过本质上我还没有完全在RxSwift中进行实践。

将一些笔记写在这里。

通过泛型定义网络请求返回的数据模型

这一步,在目前的Swift中已经应用的非常广泛了,一般我们会根据后台给好的返回JSON的范式,总结一个HttpResponse,大概会是这个样子:

struct HttpRespone<T: Codable> {
    /// 业务状态码
    let code: String?
    /// 便于透传后台的错误信息
    let message: String?
    /// 实际的业务数据
    let data: T?
}

用协议统和UIKit中的UI组件

我尝试这样写一个空协议,并且让UIView去遵守

protocol Widget {}

extension UIView: Widget {}

大家都知道Any也是一个空协议,而且所有定义的类型都隐式的遵守了它,于是Any可以表示任何类型。

而我们知道,基本上所有的UI组件,都是通过继承UIView而来,让UIView去遵守Widget协议,你可以认为把所有的UIKit中的组件都进行一次展平。

注意:这里说基本上所有的UI组件,是因为确实有些UI组件没有继承UIView,例如UIBarButtonItem:

open class UIBarButtonItem : UIBarItem, NSCoding {}

open class UIBarItem : NSObject, NSCoding, UIAppearance {}

对于上面所说的UIBarItem,我们可以尝试单独处理一下:

extension UIBarItem: Widget {}

定义闭包,通过数据去构建页面

typealias BuilderWidget<T: Codable> = (HttpRespone<T>) -> Widget

这个闭包就是典型的通过数据去构建UI,这里指向的返回是Widget,而UIView遵守了Widget协议。

定义状态枚举

枚举主体

一般情况下,一个页面主要分为下面几个情况:

  • loading

  • error

  • success:

    • hasContent

    • noData

于是我们的枚举可以定义成下面的这种形式:

enum ViewState<T: Codable> {

    case loading

    case error

    case success(ViewSuccess)

    enum ViewSuccess {

        case noData

        case content(BuilderWidget<T>, HttpRespone<T>)

    }

}

这里我使用Swift枚举的特性——枚举带参,在success状态下带参了ViewSuccess,同时有在ViewSuccess.content中带参了构建UI的闭包和数据,便于调用。

枚举分类的扩展属性

在枚举分类中,我想通过状态值,来构建不同的UI组件,这里只是初步的验证情况,因为返回是自定义的Widget协议,所以编译没有报错:

extension ViewState {

    var view: Widget {
        switch self {
        case .error:
            return UIButton()

        case .loading:
            return UIActivityIndicatorView(style: .gray)

        case .success(let successState):
            switch successState {

            case .noData:
                return UILabel()

            case .content(let builderWidget, let response):
                return builderWidget(response)

            }
        }
    }
}

另外,我们也可以扩展data属性,专门获取枚举状态下的网络请求值:

extension ViewState {

    var data: T? {

        switch self {
        case .error:
            return nil

        case .loading:
            return nil

        case .success(let successState):
            switch successState {
            case .noData:
                return nil

            case .content(_, let response):
                return response.data
            }
        }
    }
}

应用方向思考

  • 我考虑在RxSwift框架下,使用MVVM模式进行编程,在ViewModel层,把获取的网络数据转换成为ViewState的序列,然后ViewModel再与ViewController产生化学反应。

  • 考虑在SwiftUI中进行这种枚举构建页面的思路。

SwiftUI

我尝试考虑在SwiftUI中通过这种形式去构建页面,当然也有可能是因为我的SwiftUI实在太菜了,目前还没有特别好的思路:

import SwiftUI

@available(iOS 13.0, *)
typealias BuilderView = () -> View

@available(iOS 13.0, *)
extension ViewState: View {

    /// 这个some View 返回的是some View 
    /// 但是必须是一个唯一确定的类型,比如你在.error中返回EmptyView(),那么就会马上报错,一旦确定是返回是Text,那么必须都是Text, 这也导致了BuilderView这闭包无法使用
    
    var body: some View {

        switch self {

        case .error:
            return Text("")

        case .loading:
            return Text("")

        case .success(let successState):

            switch successState {

            case .noData:
                return Text("")
                
            case .content(_):
                return Text("")
            }
        }
    }
}

首先碰到的老大难的问题就是定义的闭包typealias BuilderView = () -> View,其实我的想定义的闭包应该是这样:

typealias BuilderView = () -> some View

直接报错:

'some' types are only implemented for the declared type of properties and subscripts and the return type of functions

另外就是在var body: some View的返回中,你别看这里返回用的是some Viwe,其实最终所以的状态下返回的类型必须一致,比如.loading状态下,返回的是Text类型,那么其他状态下也必须返回Text类型,所以导致上的代码返回的都是Text类型。

我考虑,在这里封装一个中间容器层去把Text转成Container(Text),Button转成Container(Button),来保证var body: some View返回中的类型一致性,目前还在思考阶段,主要是如何定义一个入参构造函数来完成这件事情,抑或定义多个static函数?

下面这个例子与思路:

@available(iOS 13.0, *)
struct Container: View {

    let text: String

    var body: some View {

        return Text(text)

    }
}

参考文档

Flutter:枚举的缺点

总结

这篇文章可能有点跨界,抑或有点不靠谱,还有许多要边实践与思路的地方。

后续,我可能向大家介绍一下这个思路在Flutter上面的实践。

如果大家有什么好的想法,欢迎交流。