<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          SwiftUI Hooks,教你如何在 SwiftUI 中使用 React Hooks

          共 7120字,需瀏覽 15分鐘

           ·

          2021-03-10 16:41

          最近,Github 基友 ra1028 基于 React Hooks 的思想,開發(fā)了一套 SwiftUI Hooks 并將其開源出來,倉庫地址是 https://github.com/ra1028/SwiftUI-Hooks 。

          SwiftUI Hooks 將狀態(tài)和生命周期引入視圖,而不必依賴于類似 @State 或 @ObservedObject 這些僅允許在視圖中使用的元素。它還允許我們通過構(gòu)建由多個鉤子組成的自定義鉤子在視圖之間重用狀態(tài)邏輯。此外,諸如 useEffect 之類的鉤子也解決了 SwiftUI 中缺乏生命周期的問題。

          支持的 Hook API

          SwiftUI Hooks 的 API 和行為規(guī)范完全基于 React Hooks,所以如果熟悉 React 的話,了解起來會相當(dāng)容易。我們簡單介紹一下幾個主要的 API。

          useState

          這個 hook 使用 Binding 包裝當(dāng)前狀態(tài),并將新狀態(tài)設(shè)置為 wrapperValue。更改狀態(tài)后觸發(fā)視圖更新。

          func useState<State>(_ initialState: State) -> Binding<State>

          let count = useState(0) // Binding<Int>
          count.wrappedValue = 123

          useEffect

          這個 hook 會調(diào)用一個副作用函數(shù),該函數(shù)通過 computation 指定。另外,當(dāng)從視圖樹中卸載這個 hook 或再次調(diào)用副作用函數(shù)時,可以取消該函數(shù)。

          func useEffect(_ computation: HookComputation, _ effect: @escaping () -> (() -> Void)?)

          useEffect(.once) {
          print("View is mounted")

          return {
          print("View is unmounted")
          }
          }

          useLayoutEffect

          這個 hook 與 useEffect 相同,但會在調(diào)用 hook 時同步觸發(fā)操作。

          func useLayoutEffect(_ computation: HookComputation, _ effect: @escaping () -> (() -> Void)?)

          useLayoutEffect(.always) {
          print("View is being evaluated")
          return nil
          }

          useMemo

          這個 hook 會使用保留的記憶值,直到在計算指定的時間重新計算記憶值為止。

          func useMemo<Value>(_ computation: HookComputation, _ makeValue: @escaping () -> Value) -> Value

          let random = useMemo(.once) {
          Int.random(in: 0...100)
          }

          useRef

          這個 hook 使用可變引用對象來存儲任意值的,這個 hook 的本質(zhì)是將值設(shè)置為 current 不會觸發(fā)視圖更新。

          func useRef<T>(_ initialValue: T) -> RefObject<T>

          let value = useRef("text") // RefObject<String>
          value.current = "new text"

          useReducer

          這個 hook 使用傳遞的 reduce 來計算當(dāng)前狀態(tài),并通過 dispatch 來分發(fā)一個操作以更新狀態(tài)。更改狀態(tài)后全觸發(fā)視圖更新。

          func useReducer<State, Action>(_ reducer: @escaping (State, Action) -> State, initialState: State) -> (state: State, dispatch: (Action) -> Void)

          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)

          useEnvironment

          這個 hook 可以在不有 @Environment 屬性包裝器的情況下通過視圖樹傳遞的環(huán)境值。

          func useEnvironment<Value>(_ keyPath: KeyPath<EnvironmentValues, Value>) -> Value

          let colorScheme = useEnvironment(\.colorScheme) // ColorScheme

          usePublisher

          這個 hook 使用傳遞的發(fā)布者的異步操作的最新狀態(tài)。

          func usePublisher<P: Publisher>(_ computation: HookComputation, _ makePublisher: @escaping () -> P) -> AsyncStatus<P.Output, P.Failure>

          let status = usePublisher(.once) {
          URLSession.shared.dataTaskPublisher(for: url)
          }

          usePublisherSubscribe

          這個 hook 與 usePublisher 相同,并會啟動一個 subscribe 來訂閱任意事件。

          func usePublisherSubscribe<P: Publisher>(_ makePublisher: @escaping () -> P) -> (status: AsyncStatus<P.Output, P.Failure>, subscribe: () -> Void)

          let (status, subscribe) = usePublisherSubscribe {
          URLSession.shared.dataTaskPublisher(for: url)
          }

          useContext

          這個 hook 使用 Context.Provider提供的當(dāng)前上下文值

          func useContext<T>(_ context: Context<T>.Type) -> T

          let value = useContext(Context<Int>.self) // Int

          Hook 規(guī)則

          為了充分利用 Hooks 的能力,SwiftUI Hooks 也必須遵循與 React 鉤子相同的規(guī)則。

          僅在函數(shù)頂層調(diào)用 Hook

          不要在條件或循環(huán)內(nèi)調(diào)用 Hook。Hook 的調(diào)用順序很重要,因為 Hook 使用LinkedList 跟蹤其狀態(tài)。

          ?? 正確的做法

          @ViewBuilder
          var counterButton: some View {
          let count = useState(0) // Uses hook at the top level

          Button("You clicked \(count.wrappedValue) times") {
          count.wrappedValue += 1
          }
          }

          ?? 錯誤做法

          @ViewBuilder
          var counterButton: some View {
          if condition {
          let count = useState(0) // Uses hook inside condition.

          Button("You clicked \(count.wrappedValue) times") {
          count.wrappedValue += 1
          }
          }
          }

          僅在 HookScope 或 HookView.hookBody 中調(diào)用 Hook

          為了保存狀態(tài),必須在 HookScope 內(nèi)調(diào)用鉤子。

          符合 HookView 協(xié)議的視圖將自動包含在 HookScope 中。

          ?? 正確的做法

          struct ContentView: HookView {  // `HookView` is used.
          var hookBody: some View {
          let count = useState(0)

          Button("You clicked \(count.wrappedValue) times") {
          count.wrappedValue += 1
          }
          }
          }
          struct ContentView: View {
          var body: some View {
          HookScope { // `HookScope` is used.
          let count = useState(0)

          Button("You clicked \(count.wrappedValue) times") {
          count.wrappedValue += 1
          }
          }
          }
          }

          ?? 錯誤做法

          struct ContentView: View {
          var body: some View { // Neither `HookScope` nor `HookView` is used.
          let count = useState(0)

          Button("You clicked \(count.wrappedValue) times") {
          count.wrappedValue += 1
          }
          }
          }

          自定義 Hook 及測試

          構(gòu)建自己的 Hook 可以使將狀態(tài)邏輯提取到可重用的函數(shù)中。

          Hook 是可組合的,因為它們是有狀態(tài)的函數(shù)。因此,它們可以與其他鉤子組合在一起以創(chuàng)建自己的自定義 Hook。

          在以下示例中,最基本的 useState 和 useEffect 使函數(shù)提供具有指定間隔的當(dāng)前 Date。如果更改了指定的時間間隔,則將調(diào)用 Timer.invalidate(),然后將激活一個新的計時器。

          這樣,可以使用 Hooks 將有狀態(tài)邏輯作為函數(shù)提取出來。

          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 重構(gòu)前面的 Example 視圖。

          struct Example: HookView {
          var hookBody: some View {
          let time = useTimer(interval: 1)

          Text("Now: \(time)")
          }
          }

          這樣更簡單易讀,且代碼更少!

          當(dāng)然,有狀態(tài)自定義鉤子可以由任意視圖調(diào)用。

          如何測試自定義掛鉤

          withTemporaryHookScope 這個 API 可以創(chuàng)建一個獨立于 SwiftUI 視圖的臨時 Hook 作用域。在 withTemporaryHookScope 函數(shù)中,可以多次啟動 Hook 作用域,以測試諸如多次評估 SwiftUI 視圖時的狀態(tài)轉(zhuǎn)換。

          例如:

          withTemporaryHookScope { scope in
          scope {
          let count = useState(0)
          count.wrappedValue = 1
          }

          scope {
          let count = useState(0)
          XCTAssertEqual(count.wrappedValue, 1) // The previous state is preserved.
          }
          }

          上下文

          React 有一種通過組件樹傳遞數(shù)據(jù)而無需手動傳遞數(shù)據(jù)的方法,這稱為Context。

          類似地,SwiftUI 具有實現(xiàn)相同的 EnvironmentValues,但是定義自定義環(huán)境值有點麻煩,因此 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
          }
          }
          }
          }

          當(dāng)然,可以使用 useContext 代替 Context.Consumer 來檢索提供的值。

          @ViewBuilder
          var darkModeButton: some View {
          let colorScheme = useContext(ColorSchemeContext.self)

          Button("Use dark mode") {
          colorScheme.wrappedValue = .dark
          }
          }

          系統(tǒng)要求及使用

          SwiftUI Hooks 需要以下支持:

          • Swift 5.3+

          • Xcode 12.4.0+

          • iOS 13.0+

          • macOS 10.15+

          • tvOS 13.0+

          • watchOS 6.0+

          安裝的話支持 SPM、Cocoapod 和 Carthage 三種方式。

          • SPM

          Repository: https://github.com/ra1028/SwiftUI-Hooks
          • CocoaPods

          pod 'Hooks' :git => 'https://github.com/ra1028/SwiftUI-Hooks.git'
          • Carthage

          github "ra1028/SwiftUI-Hooks"

          小結(jié)

          SwiftUI Hooks 是 React Hooks 開發(fā)的一套狀態(tài)管理庫,其 API 和行為規(guī)范完全基于React Hooks,所以想了解 SwiftUI Hooks 的能力,也可以參考 React Hooks 的相關(guān)文檔。


          瀏覽 31
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  精品av| 99热在线免费 | 高清无码免费观看 | 91久久夜色精品国产九色 | 99无码人妻一区二区三区色 |