这是我参与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上面的实践。
如果大家有什么好的想法,欢迎交流。