SwiftUI 版本的 React Hooks。
增强了状态逻辑的可复用性,并为函数式视图赋予状态和生命周期。
func timer() -> some View {
let time = useState(Date())
useEffect(.once) {
let timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) {
time.wrappedValue = $0.fireDate
}
return {
timer.invalidate()
}
}
return Text("Time: \(time.wrappedValue)")
}
SwiftUI Hooks 是 React Hooks 的 SwiftUI 实现。它将状态和生命周期引入函数式视图,而无需依赖只能在结构体视图中使用的元素,例如 @State
或 @ObservedObject
。
它允许你通过构建由多个 hooks 组成的自定义 hooks,在视图之间复用状态逻辑。
此外,诸如 useEffect
之类的 hooks 也解决了 SwiftUI 中缺少生命周期的问题。
SwiftUI Hooks 的 API 和行为规范完全基于 React Hooks,因此你可以利用你的 Web 应用程序知识。
已经有很多关于 React Hooks 的文档,你可以参考它们并了解更多关于 Hooks 的信息。
最低版本 | |
---|---|
Swift | 5.6 |
Xcode | 13.3 |
iOS | 13.0 |
macOS | 10.15 |
tvOS | 13.0 |
watchOS | 6.0 |
该软件包的模块名称为 Hooks
。选择以下说明之一进行安装,并将以下 import 语句添加到你的源代码中。
import Hooks
从 Xcode 菜单:File
> Swift Packages
> Add Package Dependency
https://github.com/ra1028/swiftui-hooks
在你的 Package.swift
文件中,首先将以下内容添加到 package 的 dependencies
中
.package(url: "https://github.com/ra1028/swiftui-hooks"),
然后,将 "Hooks" 作为你目标的依赖项包含进来
.target(name: "<target>", dependencies: [
.product(name: "Hooks", package: "swiftui-hooks"),
]),
👇 点击以展开描述。
useState
func useState<State>(_ initialState: State) -> Binding<State>
func useState<State>(_ initialState: @escaping () -> State) -> Binding<State>
一个 hook,用于使用 Binding<State>
包裹当前状态,通过将新状态设置为 wrappedValue
来更新状态。
当状态发生更改时,触发视图更新。
let count = useState(0) // Binding<Int>
Button("Increment") {
count.wrappedValue += 1
}
如果初始状态是昂贵计算的结果,你可以提供一个闭包来代替。该闭包将在初始渲染期间执行一次。
let count = useState {
let initialState = expensiveComputation() // Int
return initialState
} // Binding<Int>
Button("Increment") {
count.wrappedValue += 1
}
useEffect
func useEffect(_ updateStrategy: HookUpdateStrategy? = nil, _ effect: @escaping () -> (() -> Void)?)
一个 hook,用于使用副作用函数,该函数根据 updateStrategy
指定的策略调用多次。
可选地,当此 hook 被释放或副作用函数再次被调用时,可以取消该函数。
请注意,执行会延迟到其他 hooks 更新之后。
useEffect {
print("Do side effects")
return {
print("Do cleanup")
}
}
useLayoutEffect
func useLayoutEffect(_ updateStrategy: HookUpdateStrategy? = nil, _ effect: @escaping () -> (() -> Void)?)
一个 hook,用于使用副作用函数,该函数根据 updateStrategy
指定的策略调用多次。
可选地,当此 hook 从视图树中卸载或副作用函数再次被调用时,可以取消该函数。
签名与 useEffect
相同,但它在 hook 被调用时同步触发。
useLayoutEffect {
print("Do side effects")
return nil
}
useMemo
func useMemo<Value>(_ updateStrategy: HookUpdateStrategy, _ makeValue: @escaping () -> Value) -> Value
一个 hook,用于使用记忆化的值,该值被保留直到在给定的 updateStrategy
确定的时机更新。
let random = useMemo(.once) {
Int.random(in: 0...100)
}
useRef
func useRef<T>(_ initialValue: T) -> RefObject<T>
一个 hook,用于使用可变 ref 对象存储任意值。
此 hook 的本质是,将值设置为 current
不会触发视图更新。
let value = useRef("text") // RefObject<String>
Button("Save text") {
value.current = "new text"
}
useReducer
func useReducer<State, Action>(_ reducer: @escaping (State, Action) -> State, initialState: State) -> (state: State, dispatch: (Action) -> Void)
一个 hook,用于使用由传递的 reducer
返回的状态,以及一个 dispatch
函数来发送 action 以更新状态。
当状态发生更改时,触发视图更新。
enum Action {
case increment, decrement
}
func reducer(state: Int, action: Action) -> Int {
switch action {
case .increment:
return state + 1
case .decrement:
return state - 1
}
}
let (count, dispatch) = useReducer(reducer, initialState: 0)
useAsync
func useAsync<Output>(_ updateStrategy: HookUpdateStrategy, _ operation: @escaping () async -> Output) -> AsyncPhase<Output, Never>
func useAsync<Output>(_ updateStrategy: HookUpdateStrategy, _ operation: @escaping () async throws -> Output) -> AsyncPhase<Output, Error>
一个 hook,用于使用传递的异步操作函数的最新阶段。
该函数将在首次更新时执行,并将根据给定的 updateStrategy
重新执行。
let phase = useAsync(.once) {
try await URLSession.shared.data(from: url)
}
useAsyncPerform
func useAsyncPerform<Output>(_ operation: @escaping @MainActor () async -> Output) -> (phase: AsyncPhase<Output, Never>, perform: @MainActor () async -> Void)
func useAsyncPerform<Output>(_ operation: @escaping @MainActor () async throws -> Output) -> (phase: AsyncPhase<Output, Error>, perform: @MainActor () async -> Void)
一个 hook,用于使用传递的异步操作的最新阶段,以及一个 perform
函数,用于在任意时机调用它。
let (phase, perform) = useAsyncPerform {
try await URLSession.shared.data(from: url)
}
usePublisher
func usePublisher<P: Publisher>(_ updateStrategy: HookUpdateStrategy, _ makePublisher: @escaping () -> P) -> AsyncPhase<P.Output, P.Failure>
一个 hook,用于使用传递的 publisher 的异步操作的最新阶段。
publisher 将在首次更新时被订阅,并将根据给定的 updateStrategy
重新订阅。
let phase = usePublisher(.once) {
URLSession.shared.dataTaskPublisher(for: url)
}
usePublisherSubscribe
func usePublisherSubscribe<P: Publisher>(_ makePublisher: @escaping () -> P) -> (phase: AsyncPhase<P.Output, P.Failure>, subscribe: () -> Void)
一个 hook,用于使用传递的 publisher 的异步操作的最新阶段,以及一个 subscribe
函数,用于在任意时机订阅它。
let (phase, subscribe) = usePublisherSubscribe {
URLSession.shared.dataTaskPublisher(for: url)
}
useEnvironment
func useEnvironment<Value>(_ keyPath: KeyPath<EnvironmentValues, Value>) -> Value
一个 hook,用于使用通过视图树传递的环境值,而无需 @Environment
属性包装器。
let colorScheme = useEnvironment(\.colorScheme) // ColorScheme
useContext
func useContext<T>(_ context: Context<T>.Type) -> T
一个 hook,用于使用由 Context<T>.Provider
提供的当前 context 值。
目的与使用 Context<T>.Consumer
相同。
有关更多详细信息,请参阅 Context 部分。
let value = useContext(Context<Int>.self) // Int
另请参阅:React Hooks API 参考
为了利用 Hooks 精彩的接口,SwiftUI Hooks 也必须遵循 React hooks 相同的规则。
[免责声明]:这些规则不是 SwiftUI Hooks 特有的技术约束,而是基于 Hooks 本身的设计所必需的。你可以 这里 了解更多关于为 React Hooks 定义的规则。
* 在 -Onone 构建中,如果检测到违反这些规则的行为,它会通过内部健全性检查来断言,以帮助开发人员注意到 hooks 使用中的错误。但是,hooks 也具有 disableHooksRulesAssertion
修饰符,以防你想禁用断言。
不要在条件或循环中调用 Hooks。调用 hook 的顺序很重要,因为 Hooks 使用 LinkedList 来跟踪其状态。
🟢 推荐做法
@ViewBuilder
func counterButton() -> some View {
let count = useState(0) // 🟢 Uses hook at the top level
Button("You clicked \(count.wrappedValue) times") {
count.wrappedValue += 1
}
}
🔴 避免做法
@ViewBuilder
func counterButton() -> some View {
if condition {
let count = useState(0) // 🔴 Uses hook inside condition.
Button("You clicked \(count.wrappedValue) times") {
count.wrappedValue += 1
}
}
}
为了保持状态,hooks 必须在 HookScope
内调用。
符合 HookView
协议的视图将自动被 HookScope
包裹。
🟢 推荐做法
struct CounterButton: HookView { // 🟢 `HookView` is used.
var hookBody: some View {
let count = useState(0)
Button("You clicked \(count.wrappedValue) times") {
count.wrappedValue += 1
}
}
}
func counterButton() -> some View {
HookScope { // 🟢 `HookScope` is used.
let count = useState(0)
Button("You clicked \(count.wrappedValue) times") {
count.wrappedValue += 1
}
}
}
struct ContentView: HookView {
var hookBody: some View {
counterButton()
}
// 🟢 Called from `HookView.hookBody` or `HookScope`.
@ViewBuilder
var counterButton: some View {
let count = useState(0)
Button("You clicked \(count.wrappedValue) times") {
count.wrappedValue += 1
}
}
}
🔴 避免做法
// 🔴 Neither `HookScope` nor `HookView` is used, and is not called from them.
@ViewBuilder
func counterButton() -> some View {
let count = useState(0)
Button("You clicked \(count.wrappedValue) times") {
count.wrappedValue += 1
}
}
另请参阅:React Hooks 的规则
构建你自己的 hooks 允许你将状态逻辑提取到可复用的函数中。
Hooks 是可组合的,因为它们充当有状态的函数。因此,它们可以与其他 hooks 组合以创建你自己的自定义 hook。
在以下示例中,最基本的 useState
和 useEffect
被用来创建一个函数,该函数以指定的间隔提供当前的 Date
。如果指定的间隔发生更改,将调用 Timer.invalidate()
,然后激活一个新的 timer。
像这样,有状态的逻辑可以使用 Hooks 作为函数提取出来。
func useTimer(interval: TimeInterval) -> Date {
let time = useState(Date())
useEffect(.preserved(by: interval)) {
let timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) {
time.wrappedValue = $0.fireDate
}
return {
timer.invalidate()
}
}
return time.wrappedValue
}
让我们使用这个自定义 hook 重构 README 开头的 Example
视图。
struct Example: HookView {
var hookBody: some View {
let time = useTimer(interval: 1)
Text("Now: \(time)")
}
}
现在更容易阅读,代码也更少了!
当然,有状态的自定义 hook 可以被任意视图调用。
另请参阅:构建你自己的 React Hooks
到目前为止,我们已经解释了 hooks 应该在 HookScope
或 HookView
中调用。那么,你创建的自定义 hook 如何进行测试呢?
为了使你的自定义 hooks 的单元测试变得容易,SwiftUI Hooks 提供了一个简单而完整的测试实用程序库。
HookTester
通过模拟给定 hook 的视图行为并管理结果值,使自定义 hooks 的单元测试独立于 UI。
示例
// Your custom hook.
func useCounter() -> (count: Int, increment: () -> Void) {
let count = useState(0)
func increment() {
count.wrappedValue += 1
}
return (count: count.wrappedValue, increment: increment)
}
let tester = HookTester {
useCounter()
}
XCTAssertEqual(tester.value.count, 0)
tester.value.increment()
XCTAssertEqual(tester.value.count, 1)
tester.update() // Simulates view's update.
XCTAssertEqual(tester.value.count, 1)
React 有一种方法可以在组件树中传递数据,而无需手动向下传递,它被称为 Context
。
类似地,SwiftUI 具有 EnvironmentValues
来实现相同的目的,但是定义自定义环境值有点麻烦,因此 SwiftUI Hooks 提供了 Context API,它更加用户友好。
这是 EnvironmentValues
周围的一个简单包装器。
typealias ColorSchemeContext = Context<Binding<ColorScheme>>
struct ContentView: HookView {
var hookBody: some View {
let colorScheme = useState(ColorScheme.light)
ColorSchemeContext.Provider(value: colorScheme) {
darkModeButton
.background(Color(.systemBackground))
.colorScheme(colorScheme.wrappedValue)
}
}
var darkModeButton: some View {
ColorSchemeContext.Consumer { colorScheme in
Button("Use dark mode") {
colorScheme.wrappedValue = .dark
}
}
}
}
当然,还有一个 useContext
hook 可以代替 Context.Consumer
来检索提供的值。
@ViewBuilder
var darkModeButton: some View {
let colorScheme = useContext(ColorSchemeContext.self)
Button("Use dark mode") {
colorScheme.wrappedValue = .dark
}
}
另请参阅:React Context