精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

TCA-SwiftUI 的救星之二

開發 后端
當我們把某個狀態通過 Binding 交給其他 view 時,這個 view 就有能力改變去直接改變狀態了,實際上這是違反了 TCA 中關于只能在 reducer 中更改狀態的規定的。

[[440914]]

前言

上一篇關于 TCA 的文章中,我們通過總覽的方式看到了 TCA 中一個 Feature 的運作方式,并嘗試實現了一個最小的 Feature 和它的測試。在這篇文章中,我們會繼續深入,看看 TCA 中對 Binding 的處理,以及使用 Environment 來把依賴從 reducer 中解耦的方法。

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

關于綁定

綁定和普通狀態的區別

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

當我們把某個狀態通過 Binding 交給其他 view 時,這個 view 就有能力改變去直接改變狀態了,實際上這是違反了 TCA 中關于只能在 reducer 中更改狀態的規定的。對于綁定,TCA 中為 View Store 添加了將狀態轉換為一種“特殊綁定關系”的方法。我們來試試看把 Counter 例子中的顯示數字的 Text 改成可以接受直接輸入的 TextField。

在 TCA 中實現單個綁定

首先,為 CounterAction 和 counterReducer 添加對應的接受一個字符串值來設定 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 兩個參數,它們都是和當前 View Store 及綁定 view 類型相關的泛型函數。在特化 (將泛型在這個上下文中轉換為具體類型) 后:

  • get: (Counter) -> String 負責為對象 View (這里的 TextField) 提供數據。
  • send: (String) -> CounterAction 負責將 View 新發送的值轉換為 View Store 可以理解的 action,并發送它來觸發 counterReducer。 在 counterReducer 接到 binding 給出的 setCount 事件后,我們就回到使用 reducer 進行狀態更新,并驅動 UI 的標準 TCA 循環中了。

傳統的 SwiftUI 中,我們在通過 $ 符號獲取一個狀態的 Binding 時,實際上是調用了它的 projectedValue。而 viewStore.binding 在內部通過將 View Store 自己包裝到一個 ObservedObject 里,然后通過自定義的 projectedValue 來把輸入的 get 和 send 設置給 Binding 使用中。對內,它通過內部存儲維持了狀態,并把這個細節隱藏起來;對外,它通過 action 來把狀態的改變發送出去。捕獲這個改變,并對應地更新它,最后再把新的狀態再次通過 get 設置給 binding,是開發者需要保證的事情。

簡化代碼

做一點重構:現在 binding 的 get 是從 $0.count 生成的 String,reducer 中對 state.count 的設定也需要先從 String 轉換為 Int。我們把這部分 Mode 和 View 表現形式相關的部分抽取出來,放到 Counter 的一個 extension 中,作為 View Model 使用:

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

把 reducer 中轉換 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 已經可以被當作函數使用了,因此我們可以把 \Counter.countString 的類型看作 (Counter) -> String。同時,Swift 5.3 中 enum case 也可以當作函數[2],可以認為 CounterAction.setCount 具有類型 (String) -> CounterAction。兩者恰好滿足 binding 的兩個參數的要求,所以可以進一步將創建綁定的部分簡化:

  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 添加測試!

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

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

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

在 Reducer 中處理這個 .binding,并添加 .binding() 調用。

直接用代碼說明會更快:

  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 里用類似標準 SwiftUI 的做法,使用 $ 取 projected value 來進行 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.   } 

這樣一來,即使有多個 binding 值,我們也只需要用一個 .binding action 就能對應了。這段代碼能夠工作,是因為 BindableAction 要求一個簽名為 BindingAction -> Self 且名為 binding 的函數:

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

再一次,利用了將 enum case 作為函數使用的 Swift 新特性,代碼可以變得非常簡單優雅。

環境值

猜數字游戲

回到 Counter 的例子來。既然已經有輸入數字的方式了,那不如來做一個猜數字的小游戲吧!

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

最簡單的方法,是在 Counter 中添加一個屬性,用來持有這個隨機數:

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

檢查 count 和 secret 的關系,返回答案:

  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.   } 

有了這個模型,我們就可以通過使用 checkResult 來在 view 中顯示一個代表結果的 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:

外部依賴

當我們用這個 UI “蒙對”答案后,Reset 按鈕雖然可以把猜測歸零,但它并不能為我們重開一局,這當然有點無聊。我們來試試看把 Reset 按鈕改成 New Game 按鈕。

在 UI 和 CounterAction 里我們已經定義了 .reset 行為了,進行一些重命名的工作:

  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 里處理這個情況,

  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() 

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

隨時 Cmd + U 運行測試是大家都應該養成的習慣,這時候我們可以發現測試編譯失敗了。最后的任務就是修正原來的 .reset 測試,這也很簡單:

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

但是,測試的運行結果大概率會失敗!

這是因為 .playNext 現在不僅重置 count,也會隨機生成新的 secret。而 TestStore 會把 send 閉包結束時的 state 和真正的由 reducer 操作的 state 進行比較并斷言:前者沒有設置合適的 secret,導致它們并不相等,所以測試失敗了。

我們需要一種穩定的方式,來保證測試成功。

使用環境值解決依賴

在 TCA 中,為了保證可測試性,reducer 必須是純函數:也就是說,相同的輸入 (state, action 和 environment) 的組合,必須能給出相同的輸入 (在這里輸出是 state 和 effect,我們會在后面的文章再接觸 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 時,Int.random 顯然無法保證每次調用都給出同樣結果,它也是導致 reducer 變得無法測試的原因。TCA 中環境 (Environment) 的概念,就是為了對應這類外部依賴的情況。如果在 reducer 內部出現了依賴外部狀態的情況 (比如說這里的 Int.random,使用的是自動選擇隨機種子的 SystemRandomNumberGenerator),我們可以把這個狀態通過 Environment 進行注入,讓實際 app 和單元測試能使用不同的環境。

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

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

現在編譯器需要我們為原來 CounterEnvironment() 的地方加上 generateRandom 的設定。我們可以直接在生成時用 Int.random 來創建一個 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.   ) 

一種更加常見和簡潔的做法,是為 CounterEnvironment 定義一組環境,然后把它們傳到相應的地方:

  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.   ) 

現在,在 reducer 中,就可以使用注入的環境值來達到和原來等效的結果了:

  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() 

萬事俱備,回到最開始的目的 - 保證測試能順利通過。在 test target 中,用類似的方法創建一個 .test 環境:

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

現在,在生成 TestStore 的時候,使用 .test,然后在斷言時生成合適的 Counter 作為新的 state,測試就能順利通過了:

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

其他常見依賴

除了像是 random 系列以外,凡是會隨著調用環境的變化 (包括時間,地點,各種外部狀態等等) 而打破 reducer 純函數特性的外部依賴,都應該被納入 Environment 的范疇。常見的像是 UUID 的生成,當前 Date 的獲取,獲取某個運行隊列 (比如 main queue),使用 Core Location 獲取現在的位置信息,負責發送網絡請求的網絡框架等等。

它們之中有一些是可以同步完成的,比如例子中的 Int.random;有一些則是需要一定時間才能得到結果,比如獲取位置信息和發送網絡請求。對于后者,我們往往會把它轉換為一個 Effect。我們會在下一篇文章中再討論 Effect。

練習

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

添加一個 Slider

用鍵盤和加減號來控制 Counter 已經不錯了,但是添加一個 Slider 會更有趣。請為 CounterView 添加一個 Slider,用來來和 TextField 以及 “+” “-“ Button 一起,控制我們的猜數字游戲。

期望的 UI 大概是這樣:

別忘了寫測試!

完善 Counter,記錄更多信息

為了后面功能的開發,我們需要更新一下 Counter 模型。首先,每個謎題添加一些元信息,比如謎題 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.   } 

 

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

 

責任編輯:武曉燕 來源: Swift社區
相關推薦

2021-12-15 08:26:03

TCASwiftUIUIKit

2009-03-20 08:54:16

Windows 7微軟

2019-11-08 08:16:12

區塊鏈數據存儲去中心化

2021-10-18 08:28:03

Kafka架構主從架構

2021-10-11 11:58:41

Channel原理recvq

2021-12-01 07:02:16

虛擬化LinuxCPU

2021-01-26 14:31:04

IPv6物聯網IOT

2022-05-09 11:52:38

Java卡片服務卡片

2022-03-04 15:43:36

文件管理模塊Harmony鴻蒙

2017-10-26 10:25:07

數據恢復服務

2023-06-29 08:32:41

Bean作用域

2010-10-28 11:25:34

應聘

2022-04-14 11:35:01

HarmonyOS手表Demo操作系統

2011-11-28 12:55:37

JavaJVM

2012-03-15 16:27:13

JavaHashMap

2018-04-19 14:11:50

2021-01-18 05:33:08

機器學習前端算法

2020-10-15 14:10:51

網絡攻擊溯源

2021-10-28 19:27:08

C++指針內存

2021-02-15 15:36:20

Vue框架數組
點贊
收藏

51CTO技術棧公眾號

国产精品女主播视频| 亚洲黄一区二区| 在线视频不卡国产| 国产www免费观看| 1024日韩| 中文字幕日韩综合av| 在线视频观看一区二区| 69av成人| 国产精品久久久久影院色老大| 亚洲综合在线中文字幕| 综合激情网五月| 99久久婷婷| 日韩精品久久久久| 高清在线观看av| 日韩久久视频| 欧美videofree性高清杂交| 国产二区视频在线播放| 日本黄色片在线观看| 成人免费视频视频在线观看免费 | 超碰成人免费在线| 精品无码m3u8在线观看| 亚洲精品小区久久久久久| 欧美午夜一区二区三区 | 91香蕉视频在线观看视频| 一女被多男玩喷潮视频| 日本高清中文字幕二区在线| 久久成人18免费观看| 91精品国产乱码久久久久久久久 | …久久精品99久久香蕉国产| 啪啪一区二区三区| 国产伦精品一区二区三区千人斩| 欧美一区二区在线观看| 不要播放器的av网站| 粗大黑人巨茎大战欧美成人| 国产拍揄自揄精品视频麻豆| 精品免费一区二区三区蜜桃| 国产草草影院ccyycom| 麻豆精品在线播放| 欧美在线亚洲一区| 国产一级性生活| 7777久久香蕉成人影院| 中文欧美在线视频| 丰满少妇在线观看资源站| 五月天色综合| 欧美在线|欧美| 国产激情在线观看视频| 一级毛片久久久| 午夜私人影院久久久久| 国产成a人亚洲精v品在线观看| caoporm免费视频在线| 中文字幕欧美一| 小说区视频区图片区| av在线电影观看| 国产欧美一区二区三区在线老狼| 蜜桃导航-精品导航| 性xxxxbbbb| 91小视频在线观看| 快播亚洲色图| 国内在线精品| 欧美国产激情一区二区三区蜜月| 日本一区高清不卡| 黄色av免费在线看| 久久久久久免费网| 日本一区二区免费看| 北条麻妃在线| 国产精品国产精品国产专区不片| 一个色的综合| 日韩va亚洲va欧美va久久| 91精品国产91| 国语对白永久免费| 老司机精品视频网站| 国产不卡在线观看| 在线免费看av片| 国产一区二区三区蝌蚪| 99热在线播放| 神马午夜在线观看| 久久亚洲综合色| 涩涩日韩在线| bestiality新另类大全| 依依成人综合视频| 奇米精品一区二区三区| 超碰超碰人人人人精品| 欧美在线观看你懂的| 国产aⅴ爽av久久久久| 欧美在线在线| 精品视频在线观看日韩| 青娱乐国产视频| 亚洲国产成人精品女人| 欧美极品少妇全裸体| 免费av网站在线| 久久成人综合网| 高清一区二区三区视频| 免费在线视频你懂得| 国产精品久久久久久亚洲伦 | 天堂在线观看免费视频| 久久精品一区二区三区不卡| 自拍偷拍亚洲色图欧美| 波多野结衣中文字幕久久| 一本一道综合狠狠老| 黄色小视频免费网站| 久久99国产精品久久99大师| 综合av色偷偷网| 久久久久久久久久久网| 美女视频黄久久| 国产欧美日韩视频一区二区三区| a视频网址在线观看| 亚洲高清视频的网址| 日本888xxxx| 国产欧美啪啪| 亚洲欧洲美洲在线综合| 男人操女人的视频网站| 激情婷婷久久| 欧美一区三区四区| 中文字幕乱视频| 日韩成人激情| 性欧美在线看片a免费观看 | 欧美系列亚洲系列| 完美搭档在线观看| 久久精品亚洲欧美日韩精品中文字幕| 7m第一福利500精品视频| 91丨porny丨在线中文 | 国产精品一二| 亚洲综合中文字幕在线| 2021av在线| 日韩欧美亚洲成人| 亚洲少妇一区二区| 欧美肥老太太性生活| 秋霞av国产精品一区| 亚洲男人天堂久久| 亚洲人成在线观看一区二区| 已婚少妇美妙人妻系列| 欧美丝袜美腿| 欧美精品电影在线| 国产特黄一级片| 国产精品久久久久影院亚瑟| 国产亚洲天堂网| 国产精品115| 欧美成人精品在线播放| 91影院在线播放| 中文字幕精品一区二区三区精品| 国产成人无码精品久久久性色| 91成人午夜| 久操成人在线视频| 国产欧美久久久精品免费| 国产精品三级av| 精品日韩久久久| 日韩大片在线观看| 国产精品久久久久77777| 国家队第一季免费高清在线观看| 精品久久久久久电影| 国产免费一区二区三区最新6| 欧美成人69av| 99三级在线| 黑人另类精品××××性爽| 日韩欧美成人午夜| 国产一级片久久| youjizz久久| 日韩中字在线观看| 日韩一级电影| 日本中文字幕久久看| 免费人成在线观看网站| 日本黄色一区二区| 老头老太做爰xxx视频| 日本美女一区二区| 一区二区三区免费看| 亚洲人成777| 欧美理论片在线观看| 亚洲精品久久久久久无码色欲四季| 亚洲男人天堂av| 特级特黄刘亦菲aaa级| 尤物在线精品| 久久综合一区| 精品美女一区| 欧美日韩福利电影| 日本黄色三级视频| 在线中文字幕一区二区| 三级黄色片在线观看| 国产精品2024| 男人天堂999| 欧美gayvideo| 国产厕所精品在线观看| xxxxxx欧美| 色香阁99久久精品久久久| 精品国产亚洲AV| 五月婷婷另类国产| 久久视频一区二区三区| 国产高清精品网站| 日本一道本久久| 日韩在线观看电影完整版高清免费悬疑悬疑| 国产主播喷水一区二区| 牛牛精品在线视频| 亚洲欧洲国产伦综合| 欧美激情麻豆| 欧美乱大交做爰xxxⅹ性3| 丰满岳乱妇国产精品一区| 福利二区91精品bt7086| 91ts人妖另类精品系列| 成人午夜免费视频| 青青青在线视频免费观看| 888久久久| 欧美一进一出视频| 亚洲国产高清在线观看| 欧美在线一区二区视频| 丝袜美腿av在线| 亚洲三级黄色在线观看| 性一交一乱一伧老太| 一本久道中文字幕精品亚洲嫩| 希岛爱理中文字幕| 成年人国产精品| 五月天婷婷亚洲| 美女被久久久| 欧美又粗又长又爽做受| 日韩毛片视频| 久久久久se| 88久久精品| 91人人爽人人爽人人精88v| 在线观看特色大片免费视频| 精品中文字幕视频| 亚洲视频tv| 亚洲人成电影在线观看天堂色| www.日本在线观看| 欧美日韩国产成人在线免费| wwwwww国产| 亚洲成人免费电影| 一级黄色录像视频| 国产精品视频在线看| 中文字幕国产专区| 成人a免费在线看| 免费不卡av网站| 麻豆成人久久精品二区三区小说| 无码人妻精品一区二区三区在线| 欧美777四色影| 亚洲永久激情精品| 欧美中文一区二区| 欧美午夜欧美| 亚洲素人在线| 精品视频一区二区三区四区| jazzjazz国产精品久久| 51国偷自产一区二区三区的来源| 日韩国产一二三区| 国产欧美一区二区三区视频| 3d性欧美动漫精品xxxx软件| 4438全国成人免费| 操人在线观看| 97成人超碰免| 91www在线| 77777少妇光屁股久久一区| 俄罗斯一级**毛片在线播放 | 国产av自拍一区| 91麻豆精东视频| 97超碰在线资源| 国产亚洲成年网址在线观看| 无码国产69精品久久久久同性| 2欧美一区二区三区在线观看视频 337p粉嫩大胆噜噜噜噜噜91av | av在线不卡观看| 天堂精品在线视频| 俄罗斯精品一区二区三区| 波多野结衣在线一区二区| 国产91视觉| 老司机凹凸av亚洲导航| 精品久久久久久综合日本| 日韩福利视频一区| 日本黑人久久| 成人在线免费观看视频| 正在播放一区| 欧美视频日韩| 秋霞无码一区二区| 久久成人免费| 2025韩国理伦片在线观看| 久久99久久99精品免视看婷婷| 网站在线你懂的| 国产福利一区在线| 欧美无人区码suv| 国产日产欧美一区二区三区| 一二三四在线观看视频| 亚洲精品写真福利| 国产无精乱码一区二区三区| 黑人巨大精品欧美一区二区一视频| 区一区二在线观看| 欧美日韩不卡在线| 精品久久久久久亚洲综合网站| 亚洲第一精品夜夜躁人人躁| 激情小视频在线| 北条麻妃99精品青青久久| 日本天码aⅴ片在线电影网站| 欧美一级大片在线观看| 国产乱子精品一区二区在线观看| 51精品国产人成在线观看| 欧美久久精品| 亚洲草草视频| 国语自产精品视频在线看8查询8| 99精品视频在线看| 国产综合色产在线精品| 7788色淫网站小说| 中国色在线观看另类| 国产一级片播放| 欧美天天综合网| 神马午夜在线观看| 日韩网站免费观看| 午夜av不卡| 亚洲资源在线看| 中文字幕av一区二区三区人| 中文字幕日韩一区二区三区 | 欧美激情视频在线观看| 欧美xx视频| 国产精品yjizz| 日韩欧美精品综合| 自慰无码一区二区三区| 国产一区二区三区免费播放| 久久久久久久久久久久久久久| 一区二区视频在线看| 69视频免费看| 亚洲国产精品字幕| 国产激情在线视频| 国产精品嫩草视频| 欧美日韩一本| 日韩视频免费播放| 国产一区二区网址| 天天操天天摸天天舔| 日韩欧美在线播放| 高清国产mv在线观看| 美女视频久久黄| 久久久成人av毛片免费观看| 精品蜜桃一区二区三区| 激情久久久久| 无人码人妻一区二区三区免费| 中文字幕精品一区二区精品绿巨人 | 午夜美女久久久久爽久久| 国产精品777777在线播放| 日韩一区二区三区高清| 久久免费高清| 亚洲一区二区三区四区五区六区| 一区二区三区在线看| 国产一区二区在线视频观看| 国产一区二区三区在线视频| 久久久男人天堂| 国产一区二区精品在线| 欧美色一级片| 麻豆免费在线观看视频| 一区二区三区国产精品| 国产伦精品一区二区三区四区 | 91精品久久久| 成人免费黄色网| 欧美电影三区| 伊人网在线综合| 中文字幕一区av| 在线观看国产成人| 日韩在线观看你懂的| av在线播放一区| 亚洲一区二区不卡视频| 免费看精品久久片| 99精品中文字幕| 91精品免费观看| 亚洲区欧洲区| 国产精品久久久久免费| 在线精品亚洲| 网站免费在线观看| 岛国视频午夜一区免费在线观看| 香蕉视频黄在线观看| 日韩免费av片在线观看| 精品国产a一区二区三区v免费| 色婷婷综合久久久久中文字幕 | 欧美卡一卡二卡三| 精品欧美乱码久久久久久| 97人人在线视频| 免费一区二区三区| 麻豆一区二区99久久久久| 亚洲色偷偷综合亚洲av伊人| 日韩欧美一区二区在线视频| 激情影院在线| 蜜桃视频日韩| 免费成人美女在线观看| 2018天天弄| 亚洲国产欧美精品| 国产一区二区三区朝在线观看| 亚洲永久激情精品| 国产91色综合久久免费分享| 日韩黄色三级视频| 国产亚洲激情在线| 国产精品亚洲四区在线观看| www.国产在线视频| 91麻豆6部合集magnet| 在线观看免费观看在线| 欧美丰满少妇xxxx| 亚洲自拍都市欧美小说| 日韩精品aaa| 午夜亚洲国产au精品一区二区| 国产三级电影在线观看| 99电影网电视剧在线观看| 国产精品试看| 午夜剧场免费在线观看| 亚洲国产一区二区三区在线观看| 成人交换视频| 97碰在线视频| 国产精品久久久久桃色tv| 婷婷av一区二区三区| 成人激情视频小说免费下载| 亚洲精品看片| 精品人妻伦九区久久aaa片| 精品视频久久久久久|