🤣

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
}

这是一个协议, 并且 iOSAndroid 都继承这个协议, 于是写一个这样的方法:

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 逻辑返回了不同的类型, 是不可以的.


在我们一生中,命运赐予我们每个人三个导师,三个朋友,三名敌人,三个挚爱。但这十二人总是不以真面目示人,总要等到我们爱上他们、离开他们、或与他们对抗时,才能知道他们是其中哪种角色。