当定义一个SwiftUI视图时,它会自动写入struct MyView: view,后跟var body: some view{…}.我们所有的views都是由其他views组成的。在某些时候,SwiftUI需要在屏幕上绘制一些东西: 什么时候结束? SwiftUI如何知道它已经到达视图层次结构的底部?

在本文中,让我们继续探索SwiftUI的内部工作原理。

1. The View protocol

每个SwiftUI视图都是符合view协议的类型:

public protocol View {
  associatedtype Body: View
  @ViewBuilder var body: Self.Body { get }
}

由于这是一个Swift协议,我们可以使任何类型都符合它,例如String:

extension String: View {
  public var body: some View {
    Text(self)
  }
}

这只是一个例子,不是建议,也不是最佳实践。

它允许我们直接将String写入视图声明:

struct ContentView: View {
  var body: some View {
    "Hello world!"
  }
}

当我们在String的主体中使用Text原语时,这将像预期的那样工作,但如果我们不这样做呢? 让我们构建一个不使用SwiftUI原语的新视图:

struct MyView: View {
  var body: some View {
    self
  }
}

在这个例子中,我们定义了一个名为MyView的新视图,我们在ContentView中使用它:

struct ContentView: View {
  var body: some View {
    MyView()
  }
}

构建完成。然而,由于MyView的主体是视图本身,我们陷入了无限递归,这将使SwiftUI终止我们的应用。

尽管View协议允许我们遵循它,但如果我们想在SwiftUI中使用这些声明,我们必须使用SwiftUI原语,否则我们的应用程序就会崩溃。

这些SwiftUI原语有什么独特之处,让我们能够打破我们所处的无限递归? 答案就在他们的body type:Never

2. Never

Swift 3.0给我们带来了Never,一种类型:Never是一种没有值的类型,因此我们不可能获得或创建实例。

我们也许见过 Never:

  • Combine's Publishers中,通常表示给定的publisher不能抛出错误
  • 作为**preconditionFailure(:file:line:)或fatalError(:file:line:)**的返回类型,表示一旦调用,就没有出路了

让我们声明一个body类型为Never的视图:

struct ImpossibleView: View {
  var body: Never
}

然而,我们不能使用它:我们不能实例化它而不传递类似**fatalError()**的东西。不管怎样,让我们实现body并运行我们的应用程序:

struct ImpossibleView: View {
  var body: Never {
    fatalError("This will make our app 💥")
  }
}

这是我们ContentView:

struct ContentView: View {
  var body: some View {
    ImpossibleView()
  }
}

不出意外,我们的应用程序将再次崩溃,然而,崩溃的原因是致命错误:ImpossibleView may not have Body == Never: file SwiftUI, line 0, 而不是 This will make our app 💥

在读取整个堆栈跟踪信息时,我们会看到SwiftUI的BodyAccessor.makeBody(container:inputs:fields:)方法内部有一个assertionFailure,它不喜欢我们的ImpossibleView体类型。

如果传递的是类实例而不是值类型,这个方法也会导致应用程序崩溃。

只有在SwiftUI中声明的视图才允许拥有body类型为Never。尽管不能访问BodyAccessor的代码,但很明显,这些视图要么传递这个断言,要么采取不同的、特殊的路径。

SwiftUI不能一直要求视图bodies:它需要一组特殊的视图,也就是一组原语,它可以在不要求其body的情况下绘制它们。这就是为什么Text, ZStack, Color等等,Never作为他们的body类型。

3. Is Never a View?

符合View的类型需要返回的主体也是View: Never是一个视图。

SwiftUI知道不要请求body类型为Never的视图的body,如果它不是一个原语或做其他事情,就会崩溃。然而,由于我们必须使代码能够编译,SwiftUI需要扩展Never to be a View:终极的、不可能的视图。

为了证实这一点,我们可以检查SwiftUI的头文件,在那里我们会发现以下声明(分散在几个地方):

extension Never: View {
  public typealias Body = Never
  public var body: Never { get }
}

SwiftUI团队本可以宣布使用另一种特殊类型而不是Never。然而,我发现这个解决方案非常优雅,非常适合用例。

4. Conclusions

在本文中,我们探讨了SwiftUI如何在绘制视图时打破无限递归的挑战,以及它如何使用特殊的Swift的Never类型来优雅地实现这一点。