偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

TCA-SwiftUI 的救星之二

開發(fā) 后端
當(dāng)我們把某個(gè)狀態(tài)通過 Binding 交給其他 view 時(shí),這個(gè) view 就有能力改變?nèi)ブ苯痈淖儬顟B(tài)了,實(shí)際上這是違反了 TCA 中關(guān)于只能在 reducer 中更改狀態(tài)的規(guī)定的。

[[440914]]

前言

上一篇關(guān)于 TCA 的文章中,我們通過總覽的方式看到了 TCA 中一個(gè) Feature 的運(yùn)作方式,并嘗試實(shí)現(xiàn)了一個(gè)最小的 Feature 和它的測(cè)試。在這篇文章中,我們會(huì)繼續(xù)深入,看看 TCA 中對(duì) Binding 的處理,以及使用 Environment 來把依賴從 reducer 中解耦的方法。

如果你想要跟做,可以直接使用上一篇文章完成練習(xí)后最后的狀態(tài),或者從這里[1]獲取到起始代碼。

關(guān)于綁定

綁定和普通狀態(tài)的區(qū)別

在上一篇文章中,我們實(shí)現(xiàn)了“點(diǎn)擊按鈕” -> “發(fā)送 Action” -> “更新 State” -> “觸發(fā) UI 更新” 的流程,這解決了“狀態(tài)驅(qū)動(dòng) UI”這一課題。不過,除了單純的“通過狀態(tài)來更新 UI” 以外,SwiftUI 同時(shí)也支持在反方向使用 @Binding 的方式把某個(gè) State 綁定給控件,讓 UI 能夠不經(jīng)由我們的代碼,來更改某個(gè)狀態(tài)。在 SwiftUI 中,我們幾乎可以在所有既表示狀態(tài),又能接受輸入的控件上找到這種模式,比如 TextField 接受 String 的綁定 Binding,Toggle 接受 Bool 的綁定 Binding 等。

當(dāng)我們把某個(gè)狀態(tài)通過 Binding 交給其他 view 時(shí),這個(gè) view 就有能力改變?nèi)ブ苯痈淖儬顟B(tài)了,實(shí)際上這是違反了 TCA 中關(guān)于只能在 reducer 中更改狀態(tài)的規(guī)定的。對(duì)于綁定,TCA 中為 View Store 添加了將狀態(tài)轉(zhuǎn)換為一種“特殊綁定關(guān)系”的方法。我們來試試看把 Counter 例子中的顯示數(shù)字的 Text 改成可以接受直接輸入的 TextField。

在 TCA 中實(shí)現(xiàn)單個(gè)綁定

首先,為 CounterAction 和 counterReducer 添加對(duì)應(yīng)的接受一個(gè)字符串值來設(shè)定 count 的能力:

  1. enum CounterAction { 
  2.   case increment 
  3.   case decrement 
  4. case setCount(String) 
  5.   case reset 
  6.  
  7. let counterReducer = Reducer<Counter, CounterAction, CounterEnvironment> { 
  8.   state, action, _ in 
  9.   switch action { 
  10.   // ... 
  11. case .setCount(let text): 
  12. +   if let value = Int(text) { 
  13. +     state.count = value 
  14. +   } 
  15. +   return .none 
  16.   // ... 
  17. }.debug() 

接下來,把 body 中原來的 Text 替換為下面的 TextField:

  1. var body: some View { 
  2.   WithViewStore(store) { viewStore in 
  3.     // ... 
  4. -   Text("\(viewStore.count)"
  5. +   TextField( 
  6. +     String(viewStore.count), 
  7. +     text: viewStore.binding( 
  8. +       get: { String($0.count) }, 
  9. +       send: { CounterAction.setCount($0) } 
  10. +     ) 
  11. +   ) 
  12. +     .frame(width: 40) 
  13. +     .multilineTextAlignment(.center) 
  14.       .foregroundColor(colorOfCount(viewStore.count)) 
  15.   } 

viewStore.binding 方法接受 get 和 send 兩個(gè)參數(shù),它們都是和當(dāng)前 View Store 及綁定 view 類型相關(guān)的泛型函數(shù)。在特化 (將泛型在這個(gè)上下文中轉(zhuǎn)換為具體類型) 后:

  • get: (Counter) -> String 負(fù)責(zé)為對(duì)象 View (這里的 TextField) 提供數(shù)據(jù)。
  • send: (String) -> CounterAction 負(fù)責(zé)將 View 新發(fā)送的值轉(zhuǎn)換為 View Store 可以理解的 action,并發(fā)送它來觸發(fā) counterReducer。 在 counterReducer 接到 binding 給出的 setCount 事件后,我們就回到使用 reducer 進(jìn)行狀態(tài)更新,并驅(qū)動(dòng) UI 的標(biāo)準(zhǔn) TCA 循環(huán)中了。

傳統(tǒng)的 SwiftUI 中,我們?cè)谕ㄟ^ $ 符號(hào)獲取一個(gè)狀態(tài)的 Binding 時(shí),實(shí)際上是調(diào)用了它的 projectedValue。而 viewStore.binding 在內(nèi)部通過將 View Store 自己包裝到一個(gè) ObservedObject 里,然后通過自定義的 projectedValue 來把輸入的 get 和 send 設(shè)置給 Binding 使用中。對(duì)內(nèi),它通過內(nèi)部存儲(chǔ)維持了狀態(tài),并把這個(gè)細(xì)節(jié)隱藏起來;對(duì)外,它通過 action 來把狀態(tài)的改變發(fā)送出去。捕獲這個(gè)改變,并對(duì)應(yīng)地更新它,最后再把新的狀態(tài)再次通過 get 設(shè)置給 binding,是開發(fā)者需要保證的事情。

簡(jiǎn)化代碼

做一點(diǎn)重構(gòu):現(xiàn)在 binding 的 get 是從 $0.count 生成的 String,reducer 中對(duì) state.count 的設(shè)定也需要先從 String 轉(zhuǎn)換為 Int。我們把這部分 Mode 和 View 表現(xiàn)形式相關(guān)的部分抽取出來,放到 Counter 的一個(gè) extension 中,作為 View Model 使用:

  1. extension Counter { 
  2.   var countString: String { 
  3.     get { String(count) } 
  4.     set { count = Int(newValue) ?? count } 
  5.   } 

把 reducer 中轉(zhuǎn)換 String 的部分替換成 countString:

  1. let counterReducer = Reducer<Counter, CounterAction, CounterEnvironment> { 
  2.   state, action, _ in 
  3.   switch action { 
  4.   // ... 
  5.   case .setCount(let text): 
  6. -   if let value = Int(text) { 
  7. -     state.count = value 
  8. -   } 
  9. +   state.countString = text 
  10.     return .none 
  11.   // ... 
  12. }.debug() 

在 Swift 5.2 中,KeyPath 已經(jīng)可以被當(dāng)作函數(shù)使用了,因此我們可以把 \Counter.countString 的類型看作 (Counter) -> String。同時(shí),Swift 5.3 中 enum case 也可以當(dāng)作函數(shù)[2],可以認(rèn)為 CounterAction.setCount 具有類型 (String) -> CounterAction。兩者恰好滿足 binding 的兩個(gè)參數(shù)的要求,所以可以進(jìn)一步將創(chuàng)建綁定的部分簡(jiǎn)化:

  1. // ... 
  2.   TextField( 
  3.     String(viewStore.count), 
  4.     text: viewStore.binding( 
  5. -     get: { String($0.count) }, 
  6. +     get: \.countString, 
  7. -     send: { CounterAction.setCount($0) } 
  8. +     send: CounterAction.setCount 
  9.     ) 
  10.   ) 
  11. // ... 

最后,別忘了為 .setCount 添加測(cè)試!

多個(gè)綁定值 如果在一個(gè) Feature 中,有多個(gè)綁定值的話,使用例子中這樣的方式,每次我們都會(huì)需要添加一個(gè) action,然后在 binding 中 send 它。這是千篇一律的模板代碼,TCA 中設(shè)計(jì)了 @BindableState 和 BindableAction,讓多個(gè)綁定的寫法簡(jiǎn)單一些。具體來說,分三步:

為 State 中的需要和 UI 綁定的變量添加 @BindableState。

將 Action 聲明為 BindableAction,然后添加一個(gè)“特殊”的 case binding(BindingAction) 。

在 Reducer 中處理這個(gè) .binding,并添加 .binding() 調(diào)用。

直接用代碼說明會(huì)更快:

  1. // 1 
  2. struct MyState: Equatable { 
  3. + @BindableState var foo: Bool = false 
  4. + @BindableState var bar: String = "" 
  5.  
  6. // 2 
  7. - enum MyAction { 
  8. + enum MyAction: BindableAction { 
  9. +   case binding(BindingAction<MyState>) 
  10.  
  11. // 3 
  12. let myReducer = //... 
  13.   // ... 
  14. case .binding: 
  15. +   return .none 
  16. + .binding() 

這樣一番操作后,我們就可以在 View 里用類似標(biāo)準(zhǔn) SwiftUI 的做法,使用 $ 取 projected value 來進(jìn)行 Binding 了:

  1. struct MyView: View { 
  2.   let store: Store<MyState, MyAction> 
  3.   var body: some View { 
  4.     WithViewStore(store) { viewStore in 
  5. +     Toggle("Toggle!", isOn: viewStore.binding(\.$foo)) 
  6. +     TextField("Text Field!", text: viewStore.binding(\.$bar)) 
  7.     } 
  8.   } 

這樣一來,即使有多個(gè) binding 值,我們也只需要用一個(gè) .binding action 就能對(duì)應(yīng)了。這段代碼能夠工作,是因?yàn)? BindableAction 要求一個(gè)簽名為 BindingAction -> Self 且名為 binding 的函數(shù):

  1. public protocol BindableAction { 
  2.   static func binding(_ action: BindingAction<State>) -> Self 

再一次,利用了將 enum case 作為函數(shù)使用的 Swift 新特性,代碼可以變得非常簡(jiǎn)單優(yōu)雅。

環(huán)境值

猜數(shù)字游戲

回到 Counter 的例子來。既然已經(jīng)有輸入數(shù)字的方式了,那不如來做一個(gè)猜數(shù)字的小游戲吧!

猜數(shù)字:程序隨機(jī)選擇 -100 到 100 之間的數(shù)字,用戶輸入一個(gè)數(shù)字,程序判斷這個(gè)數(shù)字是否就是隨機(jī)選擇的數(shù)字。如果不是,返回“太大”或者“太小”作為反饋,并要求用戶繼續(xù)嘗試輸入下一個(gè)數(shù)字進(jìn)行猜測(cè)。

最簡(jiǎn)單的方法,是在 Counter 中添加一個(gè)屬性,用來持有這個(gè)隨機(jī)數(shù):

  1. struct Counter: Equatable { 
  2.   var countInt = 0 
  3. + let secret = Int.random(in: -100 ... 100) 

檢查 count 和 secret 的關(guān)系,返回答案:

  1. extension Counter { 
  2.   enum CheckResult { 
  3.     case lower, equal, higher 
  4.   } 
  5.    
  6.   var checkResult: CheckResult { 
  7.     if count < secret { return .lower } 
  8.     if count > secret { return .higher } 
  9.     return .equal 
  10.   } 

有了這個(gè)模型,我們就可以通過使用 checkResult 來在 view 中顯示一個(gè)代表結(jié)果的 Label 了:

  1. struct CounterView: View { 
  2.   let store: Store<Counter, CounterAction> 
  3.   var body: some View { 
  4.     WithViewStore(store) { viewStore in 
  5.       VStack { 
  6. +       checkLabel(with: viewStore.checkResult) 
  7.         HStack { 
  8.           Button("-") { viewStore.send(.decrement) } 
  9.           // ... 
  10.   } 
  11.    
  12.   func checkLabel(with checkResult: Counter.CheckResult) -> some View { 
  13.     switch checkResult { 
  14.     case .lower
  15.       return Label("Lower", systemImage: "lessthan.circle"
  16.         .foregroundColor(.red) 
  17.     case .higher: 
  18.       return Label("Higher", systemImage: "greaterthan.circle"
  19.         .foregroundColor(.red) 
  20.     case .equal: 
  21.       return Label("Correct", systemImage: "checkmark.circle"
  22.         .foregroundColor(.green) 
  23.     } 
  24.   } 

最終,我們可以得到這樣的 UI:

外部依賴

當(dāng)我們用這個(gè) UI “蒙對(duì)”答案后,Reset 按鈕雖然可以把猜測(cè)歸零,但它并不能為我們重開一局,這當(dāng)然有點(diǎn)無聊。我們來試試看把 Reset 按鈕改成 New Game 按鈕。

在 UI 和 CounterAction 里我們已經(jīng)定義了 .reset 行為了,進(jìn)行一些重命名的工作:

  1. enum CounterAction { 
  2.   // ... 
  3. case reset 
  4. case playNext 
  5.  
  6. struct CounterView: View { 
  7.   // ... 
  8.   var body: some View { 
  9.     // ... 
  10. -   Button("Reset") { viewStore.send(.reset) } 
  11. +   Button("Next") { viewStore.send(.playNext) } 
  12.   } 

然后在 counterReducer 里處理這個(gè)情況,

  1. struct Counter: Equatable { 
  2.   var countInt = 0 
  3. - let secret = Int.random(in: -100 ... 100) 
  4. + var secret = Int.random(in: -100 ... 100) 
  5.  
  6. let counterReducer = Reducer<Counter, CounterAction, CounterEnvironment> { 
  7.   // ... 
  8. case .reset: 
  9. case .playNext: 
  10.     state.count = 0 
  11. +   state.secret = Int.random(in: -100 ... 100) 
  12.     return .none 
  13.   // ... 
  14. }.debug() 

運(yùn)行 app,觀察 reducer debug() 的輸出,可以看到一切正常!太好了。

隨時(shí) Cmd + U 運(yùn)行測(cè)試是大家都應(yīng)該養(yǎng)成的習(xí)慣,這時(shí)候我們可以發(fā)現(xiàn)測(cè)試編譯失敗了。最后的任務(wù)就是修正原來的 .reset 測(cè)試,這也很簡(jiǎn)單:

  1. func testReset() throws { 
  2. - store.send(.reset) { state in 
  3. + store.send(.playNext) { state in 
  4.     state.count = 0 
  5.   } 

但是,測(cè)試的運(yùn)行結(jié)果大概率會(huì)失敗!

這是因?yàn)?.playNext 現(xiàn)在不僅重置 count,也會(huì)隨機(jī)生成新的 secret。而 TestStore 會(huì)把 send 閉包結(jié)束時(shí)的 state 和真正的由 reducer 操作的 state 進(jìn)行比較并斷言:前者沒有設(shè)置合適的 secret,導(dǎo)致它們并不相等,所以測(cè)試失敗了。

我們需要一種穩(wěn)定的方式,來保證測(cè)試成功。

使用環(huán)境值解決依賴

在 TCA 中,為了保證可測(cè)試性,reducer 必須是純函數(shù):也就是說,相同的輸入 (state, action 和 environment) 的組合,必須能給出相同的輸入 (在這里輸出是 state 和 effect,我們會(huì)在后面的文章再接觸 effect 角色)。

  1. let counterReducer = // ... { 
  2.  
  3.   state, action, _ in  
  4.   // ... 
  5.   case .playNext: 
  6.     state.count = 0 
  7.     state.secret = Int.random(in: -100 ... 100) 
  8.     return .none 
  9.   //... 
  10. }.debug() 

在處理 .playNext 時(shí),Int.random 顯然無法保證每次調(diào)用都給出同樣結(jié)果,它也是導(dǎo)致 reducer 變得無法測(cè)試的原因。TCA 中環(huán)境 (Environment) 的概念,就是為了對(duì)應(yīng)這類外部依賴的情況。如果在 reducer 內(nèi)部出現(xiàn)了依賴外部狀態(tài)的情況 (比如說這里的 Int.random,使用的是自動(dòng)選擇隨機(jī)種子的 SystemRandomNumberGenerator),我們可以把這個(gè)狀態(tài)通過 Environment 進(jìn)行注入,讓實(shí)際 app 和單元測(cè)試能使用不同的環(huán)境。

首先,更新 CounterEnvironment,加入一個(gè)屬性,用它來持有隨機(jī)生成 Int 的方法。

  1. struct CounterEnvironment { 
  2. + var generateRandom: (ClosedRange<Int>) -> Int 

現(xiàn)在編譯器需要我們?yōu)樵瓉?CounterEnvironment() 的地方加上 generateRandom 的設(shè)定。我們可以直接在生成時(shí)用 Int.random 來創(chuàng)建一個(gè) CounterEnvironment:

  1. CounterView( 
  2.   store: Store( 
  3.     initialState: Counter(), 
  4.     reducer: counterReducer, 
  5. -   environment: CounterEnvironment() 
  6. +   environment: CounterEnvironment( 
  7. +     generateRandom: { Int.random(in: $0) } 
  8. +   ) 
  9.   ) 

一種更加常見和簡(jiǎn)潔的做法,是為 CounterEnvironment 定義一組環(huán)境,然后把它們傳到相應(yīng)的地方:

  1. struct CounterEnvironment { 
  2.   var generateRandom: (ClosedRange<Int>) -> Int 
  3.    
  4. static let live = CounterEnvironment( 
  5. +   generateRandom: Int.random 
  6. + ) 
  7.  
  8. CounterView( 
  9.   store: Store( 
  10.     initialState: Counter(), 
  11.     reducer: counterReducer, 
  12. -   environment: CounterEnvironment() 
  13. +   environment: .live 
  14.   ) 

現(xiàn)在,在 reducer 中,就可以使用注入的環(huán)境值來達(dá)到和原來等效的結(jié)果了:

  1. let counterReducer = // ... { 
  2. - state, action, _ in 
  3. + state, action, environment in 
  4.   // ... 
  5.   case .playNext: 
  6.     state.count = 0 
  7. -   state.secret = Int.random(in: -100 ... 100) 
  8. +   state.secret = environment.generateRandom(-100 ... 100) 
  9.     return .none 
  10.   // ... 
  11. }.debug() 

萬事俱備,回到最開始的目的 - 保證測(cè)試能順利通過。在 test target 中,用類似的方法創(chuàng)建一個(gè) .test 環(huán)境:

  1. extension CounterEnvironment { 
  2.   static let test = CounterEnvironment(generateRandom: { _ in 5 }) 

現(xiàn)在,在生成 TestStore 的時(shí)候,使用 .test,然后在斷言時(shí)生成合適的 Counter 作為新的 state,測(cè)試就能順利通過了:

  1. store = TestStore( 
  2.   initialState: Counter(countInt.random(in: -100...100)), 
  3.   reducer: counterReducer, 
  4. - environment: CounterEnvironment() 
  5. + environment: .test 
  6.  
  7. store.send(.playNext) { state in 
  8. - state.count = 0 
  9. + state = Counter(count: 0, secret: 5) 

在 store.send 的閉包里,我們現(xiàn)在直接為 state 設(shè)置了一個(gè)新的 Counter,并明確了所有期望的屬性。這里也可以分開兩行,寫成 state.count = 0 以及 state.secret = 5,測(cè)試也可以通過。選擇哪種方式都可以,但在涉及到復(fù)雜的情況下,會(huì)傾向于選擇完整的賦值:在測(cè)試中,我們希望的是通過斷言來比較期望 state 和實(shí)際 state 的差別,而不是重新去實(shí)現(xiàn)一次 reducer 中的邏輯。這可能引入混亂,因?yàn)樵跍y(cè)試失敗時(shí)你需要去排查到底是 reducer 本身的問題,還是測(cè)試代碼中操作狀態(tài)造成的問題。

其他常見依賴

除了像是 random 系列以外,凡是會(huì)隨著調(diào)用環(huán)境的變化 (包括時(shí)間,地點(diǎn),各種外部狀態(tài)等等) 而打破 reducer 純函數(shù)特性的外部依賴,都應(yīng)該被納入 Environment 的范疇。常見的像是 UUID 的生成,當(dāng)前 Date 的獲取,獲取某個(gè)運(yùn)行隊(duì)列 (比如 main queue),使用 Core Location 獲取現(xiàn)在的位置信息,負(fù)責(zé)發(fā)送網(wǎng)絡(luò)請(qǐng)求的網(wǎng)絡(luò)框架等等。

它們之中有一些是可以同步完成的,比如例子中的 Int.random;有一些則是需要一定時(shí)間才能得到結(jié)果,比如獲取位置信息和發(fā)送網(wǎng)絡(luò)請(qǐng)求。對(duì)于后者,我們往往會(huì)把它轉(zhuǎn)換為一個(gè) Effect。我們會(huì)在下一篇文章中再討論 Effect。

練習(xí)

如果你沒有跟隨本文更新代碼,你可以在這里[3]找到下面練習(xí)的起始代碼。參考實(shí)現(xiàn)可以在這里[4]找到。

添加一個(gè) Slider

用鍵盤和加減號(hào)來控制 Counter 已經(jīng)不錯(cuò)了,但是添加一個(gè) Slider 會(huì)更有趣。請(qǐng)為 CounterView 添加一個(gè) Slider,用來來和 TextField 以及 “+” “-“ Button 一起,控制我們的猜數(shù)字游戲。

期望的 UI 大概是這樣:

別忘了寫測(cè)試!

完善 Counter,記錄更多信息

為了后面功能的開發(fā),我們需要更新一下 Counter 模型。首先,每個(gè)謎題添加一些元信息,比如謎題 ID:

在 Counter 中加上下面的屬性,然后讓它滿足 Identifiable:

  1. - struct Counter: Equatable { 
  2. + struct Counter: Equatable, Identifiable { 
  3.     var countInt = 0 
  4.     var secret = Int.random(in: -100 ... 100) 
  5.    
  6. +   var id: UUID = UUID() 
  7.   } 

 

在開始新一輪游戲的時(shí)候,記得更新 id。還有,別忘了寫測(cè)試!

 

責(zé)任編輯:武曉燕 來源: Swift社區(qū)
相關(guān)推薦

2021-12-15 08:26:03

TCASwiftUIUIKit

2009-03-20 08:54:16

Windows 7微軟

2019-11-08 08:16:12

區(qū)塊鏈數(shù)據(jù)存儲(chǔ)去中心化

2021-10-18 08:28:03

Kafka架構(gòu)主從架構(gòu)

2021-01-26 14:31:04

IPv6物聯(lián)網(wǎng)IOT

2017-10-26 10:25:07

數(shù)據(jù)恢復(fù)服務(wù)

2022-05-09 11:52:38

Java卡片服務(wù)卡片

2022-03-04 15:43:36

文件管理模塊Harmony鴻蒙

2021-12-01 07:02:16

虛擬化LinuxCPU

2021-10-11 11:58:41

Channel原理recvq

2010-10-28 11:25:34

應(yīng)聘

2023-06-29 08:32:41

Bean作用域

2012-03-15 16:27:13

JavaHashMap

2021-10-28 19:27:08

C++指針內(nèi)存

2018-04-19 14:11:50

2021-06-29 08:28:12

算法順序表數(shù)據(jù)

2021-01-18 05:33:08

機(jī)器學(xué)習(xí)前端算法

2021-02-15 15:36:20

Vue框架數(shù)組

2020-10-15 14:10:51

網(wǎng)絡(luò)攻擊溯源

2012-01-16 11:05:22

紅帽PaaS 開源
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)