Funny SwiftUI
开发 SwiftUI 应用有一段时间了, 感觉完全没有像 js 那样的动态语言灵活, Swift 写起来感觉是在推车, 而 js 写起来是在开车. 写的时候不是在抱怨 Swift 难用就是在抱怨 Xcode 垃圾!
所以从现在开始收集一下遇到的坑. 可能是因为自己还是太菜, 希望之后回过头来看这篇文章能打脸现在的我.
例子1
struct ContentView: View {
var body: some View {
NavigationStack {
NavigationLink {
ChildView()
} label {
Text("GO Child")
}
}
}
}
struct ChildView: View {
var body: some View {
Text("I am ChildView")
}
}
上面代码看起来没问题, 但是我要告诉你, 在页面还没进子页面之前, ChildView
就已经执行过了, 你觉得震惊不? 不信你可以在 ChildView
添加 init
方法打一下 log 看看.
这就是 SwiftUI 的优势, 完全数据驱动, 页面刚开始时候所有层级都已经展开了, 当 push 新页面时候, 新页面的 body 会被执行, 相应的在 onAppear
时候请求网络数据等, 使用 @State
将数据绑定页面, 数据变化会导致页面变化.
但是将上面代码增加一个方法:
extension ChildView: Equatable {
static func ==(lhs: ChildView, rhs: ChildView) -> Bool {
return true
}
}
这样即使父组件再怎么更新, 也不会刷新 ChildView
, 比如如果想传一个可以变化的数据到 ChildView
, 即使在父组件中数值发生了变化, 但是进入子页面, 还是一开始的数据.
例子2
这个例子比较难懂, 开发了这么久, 我仍然没有完全搞明白类型协议和 Opaque Type.
protocol MobileOS {
associatedtype Version
var version: Version { get }
init(version: Version)
}
struct iOS: MobileOS {
var version: Float
}
struct Android: MobileOS {
var version: String
}
这是一个协议, 并且 iOS
和 Android
都继承这个协议, 于是写一个这样的方法:
func buildOS() -> MobileOS {
return iOS(version: 16.1)
}
这个代码会报错, 为什么呢? 因为 MobileOS
是有 associatedtype
的, Swift 编译器无法推断出函数里的 associatedtype
.
我就奇了怪了, 为什么它能推断出来结构体 iOS 的 associatedtype
却无法推断出这个函数的呢? 所以应该怎么改呢?
func buildOS<T: MobileOS>(version: T.Version) -> T {
return T(version: version)
}
let o1: Android = buildOS(version: "XiaoMi")
let o2: iOS = buildOS(version: 16.1)
哎, 贼麻烦, 有没有简单点的呢? 有, 可以用 Opaque Type:
func buildOS() -> some MobileOS {
return iOS(version: 16.1)
}
好的, 那来吧:
func buildOS() -> some MobileOS {
let isEven = Int.random(in: 0...10) % 2 == 0
return isEven ? iOS(version: 16.1) : Android(version: "Pie")
}
// Compiler ERROR 😭
// Cannot convert return expression of type 'iOS' to return type 'some MobileOS'
哎, 又不行了, 编译器又报错. 这个我始终无法理解. 但是这么写就可以:
func buildOS() -> some MobileOS {
let isEven = Int.random(in: 0...10) % 2 == 0
return isEven ? iOS(version: 16.1) : iOS(version: 16.2)
}
总结下这奇葩的逻辑, 函数返回一个 protocol 是可以的, 但是如果 protocol 里面有 associatedtype
是不行的, 编译器不会推断;
如果添加上 some
又可以了, 因为这个 some
让编译器不管了, 只要返回值遵守协议即可; 但是如果用 if else
逻辑返回不同类型但遵守协议的又不行了, 但用 if else
返回多个相同类型的它又行了..
上面是理论的东西, 结合 SwiftUI 来看下:
struct FolderInfoView: View {
@Binding var folder: Folder
var isEditable: Bool
var body: some View {
HStack {
Image(systemName: "folder")
textView
}
}
private var textView: some View {
// Error: Function declares an opaque return type, but
// the return statements in its body do not have matching
// underlying types.
if isEditable {
return TextField("Name", text: $folder.name)
} else {
return Text(folder.name)
}
}
}
这个代码会报错和上面理论中的一样问题, 用 if else
返回不同的类型, 虽然它都遵守 View 协议.
但这段 if else
逻辑直接拿到 body 里面, 它又行了…??? 或者将 textView 添加一个 @ViewBuilder
.
总的来说, 编译器能够推断出函数的返回值的类型和计算属性的类型, 但如果被一个 if else
逻辑返回了不同的类型, 是不可以的.