层级响应器

Social Banner

tests codecov

License

HierarchyResponder 是一个框架,允许您使用 SwiftUI 视图层级结构作为事件和错误处理的响应链。通过使用视图层级结构来报告错误和触发事件,视图可以变得更加独立,而不会牺牲与其他视图的通信。

要报告错误或触发事件,视图从环境中读取一个闭包,并使用事件或错误作为参数调用该闭包。

视图层级结构中上层的视图可以注册不同的响应器,这些响应器将使用事件或错误作为参数来执行。

struct ItemSelectionEvent: Event {
  let item: Item
}

struct ParentView: View {
  @State var selection: Item?
  let items: [Item] = ...
  
  var body: some View {
    GridView(items: items)
      // The handleEvent modifier registers a responder closure to be executed when
      // an ItemSelectionEvent is triggered in any descendant view
      .handleEvent(ItemSelectionEvent.self) { event in
        selection = event.item
      }
      .sheet(item: selection) { ItemDetail($0) }
  }
}

struct GridView: View {
  let items: [Item]
  
  var body: some View {
    ForEach(items) { item in
      ItemView(item: item)
    }
  }
}

struct ItemView: View {
  @Environment(\.triggerEvent) var triggerEvent
  let item: Item
  
  var body: some View {
    ItemPreview(item)
      .onTapGesture {
        // The triggerEvent object, called as a closure, triggers an event which
        // will be received by any ancestor views that registered a responder
        triggerEvent(ItemSelectionEvent(item: item))
      }
  }
}

入门指南

理解如何使用层级响应器模式的基础知识。

事件协议

Event 是一个无要求的协议,用于标识一个类型为可以向上发送到 SwiftUI 视图层级结构的事件。

它可以是任何类型,并包含任何类型的附加信息。它的存在是为了能够轻松地将类型标识为对象,并避免必须将此框架中方法使用的类型注释为 Any

触发事件

事件使用可以从 Environment 中读取的 triggerEvent 对象触发。由于此对象实现了 callAsFunction,因此可以像闭包一样调用它。

struct MyEvent: Event {}

struct TriggerView: View {
  @Environment(\.triggerEvent) var triggerEvent
  
  var body: some View {
    Button("Trigger") {
      triggerEvent(MyEvent())
    }
  }
}

报告错误

与事件类似,错误使用 reportError 闭包触发。由于此对象实现了 callAsFunction,因此可以像闭包一样调用它。

struct MyError: Error {}

struct TriggerView: View {
  @Environment(\.reportError) var reportError
  
  var body: some View {
    Button("Trigger") {
      reportError(MyError())
    }
  }
}

处理事件

事件和错误使用多个响应器之一来处理。例如,下面的 .handleEvent 响应器仅接收类型为 MyEvent 的事件。

struct ContentView: View {
  var body: some View {
    TriggerView()
      .handleEvent(MyEvent.self) { event in
        //  Do something with event
      }
  }
}

理解响应器

使用响应器来接收或处理视图层级结构中下层视图触发或报告的事件和错误。

什么是响应器?

响应器是“响应”视图层级结构中下层视图触发或报告的事件或错误的闭包,响应方式各不相同。

struct ContentView: View {
  var body: some View {
    TriggerView()
      .handleEvent(MyEvent.self) { event in
      //  Do something with event
    }
  }
}

注册响应器

注册响应器是使用修饰符语法完成的,就像 SwiftUI 中的任何其他修饰符一样,它们的执行顺序至关重要

简单来说,响应器将按照它们添加到视图的顺序被调用,这与它们在视图层级结构中的位置相反。

为了更好地理解视图层级结构,您可以阅读这篇文章

struct ContentView: View {
  var body: some View {
    TriggerView()
      .handleEvent(MyEvent.self) {
      //  Will be called first and absorb `MyEvent` objects
      }
      .handleEvent {
      //  Will be called second, will not receive any `MyEvent` objects
      }
  }
}

不同类型的响应器

有几种类型的响应器,每种响应器都有两个版本,一个版本将响应任何类型的事件或错误,另一个版本将事件或错误的类型作为第一个参数接收,并且仅对该类型的值起作用。

建议使用显式响应器,也称为指定它们将接收的事件或错误类型的响应器。将此与安全修饰符结合使用将执行运行时检查,并在事件或错误没有关联的响应器时警告您。

接收事件或错误

注册接收响应器时,处理闭包将确定事件或错误是否已被处理。

如果事件或错误已被处理,则闭包应返回 .handled,否则应返回 .unhandled

未处理的事件将继续在视图层级结构中向上传播。

struct ContentView: View {
  var body: some View {
    TriggerView()
      .receiveEvent { event in
        if canHandle(event) {
          //  Do something
          return .handled
        }
        return .notHandled
      }
  }
}

处理事件或错误

处理响应器将消耗它们接收的事件或错误,这将阻止它在视图层级结构中向上传播。这等效于使用始终返回 .handledreceiveEvent 闭包。

转换事件或错误

转换函数可用于将接收到的值替换为另一个值。

struct ContentView: View {
  var body: some View {
    TriggerView()
      .transformEvent(MyEvent.self) {
        return AnotherEvent()
      }
  }
}

可失败的响应器

所有事件响应器以及 catchError 响应器都接收一个抛出异常的闭包。在此闭包内抛出的任何错误都将像使用 reportError 闭包报告一样在视图层级结构中向上传播。

struct ContentView: View {
  var body: some View {
    TriggerView()
      .handleEvent { event in
        guard canHandle(event) else {
          throw AnError()
        }
        //  Handle Event
      }
  }
}

捕获错误

捕获响应器允许您接收错误并将其转换为将改为传播的事件。

struct UnauthenticatedError: Error {}
struct ShowSignInEvent: Event {}

struct ContentView: View {
  var body: some View {
    TriggerView()
      .catchError(UnauthenticatedError.self) {
        ShowSignInEvent()
      }
  }
}

外部事件

处理源自视图层级结构外部的事件

triggerEvent 可用于处理源自视图层级结构内部的事件,但某些事件,例如菜单栏操作、意图、深度链接、导航事件、摇动事件等,可能源自视图层级结构外部,并且很难确保它们被传递到正确的视图。

.publisher 视图修饰符生成一个 EventPublisher 对象,该对象可用于发布一个将“向下”遍历视图层级结构的事件,从而允许我们默认情况下找到事件的最后一个订阅者。

例如,假设您有多个视图通过 NotificationCenter 监听摇动事件,但当用户在应用程序中导航时,其中一些视图可能不在屏幕上,但仍然存在于视图层级结构中。您可以在应用程序的根目录监听 NotificationCenter 事件并发布一个事件,该事件将仅传递给最后一个订阅者,即当前在屏幕上的视图。

安全性

避免未处理事件和错误的最佳实践

这种模式提供的灵活性的一个权衡是,很容易迷失事件的生成和处理位置。包含了一些修饰符来缓解这种情况。

包含复杂层级结构(如屏幕视图)的视图可以使用 .triggers.reports 修饰符来公开一种“API”,供任何使用它的视图使用。

struct FeedView: View {
  var body: some View {
    FeedList()
      .triggers(ShowPostEvent.self, ShowProfileEvent.self)
      .reports(AuthenticationError.self)
  }
}

这些修饰符不仅对使用 FeedView 对象的开发人员有用,而且它们还充当运行时检查,如果使用了列表中未包含的事件或错误,则会发出警告,并且当需要显式响应器(默认启用)时,如果层级结构上方没有针对这些特定事件和错误的响应器,则会发出警告。

.responderSafetyLevel.requireExplicitResponders 修饰符允许自定义安全检查的行为。默认响应器安全级别将抛出控制台警告,严格安全级别将触发 fatalError,禁用安全级别将忽略任何违规行为。所有这些检查在发布版本中都已禁用,以避免导致用户崩溃。

实用工具

使事情更轻松的小工具。

EventButton

EventButton 本质上是 Button 的包装器,它接收一个 Event 而不是动作闭包,该 Event 在每次调用底层 Button 的动作时触发。

// Before:
Button("Tap Me") {
  //  Perform action
}

// After:
EventButton("Tap Me", event: MyEvent())

onTapGesture(trigger:)

onTapGesture(trigger:) 修饰符的工作方式与 onTapGesture(perform:) 类似,但它触发一个事件而不是执行闭包。

AlertableErrors

AlertableError 是一个符合 Error 协议的协议,表示带有消息和可选标题的用户友好错误。

通过使用 .handleAlertErrors() 修饰符,符合 AlertableError 协议的错误将通过显示带有错误提供的标题和消息的警报来处理。