[RxSWIFT]#3 UIコンポーネントとの併用
0.例示的なプログラムの説明
簡単な(?)アプリケーションが作成されました.
1.フォルダ構造
熊揚げgithub Step 3コードを見ればいいです.
Step 3:UIKETのみで作成したコード
Step3+Rx : RxSwift
Step3+Rx+MVVM : RxSwift + MVVM
完了したコードを使用します.
Step 3+emptyを開き、レッスンに沿って行います.
その中.
Domain
Menu > Model > Model.swift-menuのモデル
Menu > Service > APIService.swift-取得メニューのクラス
Pages
MenuList > VC, Cell
Order > VC
この構造からなる.
2. step3
1.プロジェクトの状況を仮定する
理論的には,クライアント側はAPI,設計後に開発を行うが,実際の作業ではそうではないことが多い.APIやデザインが分からないまま、企画書だけ読んで開発しています.APIは後で接続され、デザインも後で追加されます->最適構造MVVM~~
2.開発~
ViewController == VC
1. Menu.swift-Viewのモデルを作成する(ビューに表示されるモデルのみ)
struct Menu {
var name: String = ""
var price: Int = 0
var count: Int = 0
init(name: String, price: Int, count: Int) {
self.name = name
self.price = price
self.count = count
}
}
2.MenuVC-メニュー(ViewModelアレイ)を作成し、Table Viewに接続します。
3.MenuListViewModelを使用してメニューレイアウトを個別に管理-ビューに情報をインポートする場合は、VCでviewModelインスタンスを作成してインポートするだけです。
// MenuListViewModel
class MenuListViewModel {
let menus: [Menu] = [
Menu(name: "보아", price: 1105, count: 0),
Menu(name: "태연", price: 309, count: 0),
Menu(name: "효연", price: 922, count: 0),
Menu(name: "슬기", price: 210, count: 0),
Menu(name: "웬디", price: 224, count: 0),
Menu(name: "카리나", price: 411, count: 0),
Menu(name: "윈터", price: 101, count: 0)
]
var itemsCount: Int = 5
var totalPrice: Int = 10000
}
4.viewModelのtotalPrice、itemsCount情報を取得してみます。正しく導入されているかどうかはわかりませんが、onOrderボタンを押すと総価格が100増加します。
// MenuVC
class MenuViewController: UIViewController {
// MARK: - Life Cycle
// viewModel 인스턴스 생성해서 정보 받아오기
let viewModel = MenuListViewModel();
override func viewDidLoad() {
super.viewDidLoad()
updateUI()
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let identifier = segue.identifier ?? ""
if identifier == "OrderViewController",
let orderVC = segue.destination as? OrderViewController {
// TODO: pass selected menus
}
}
func showAlert(_ title: String, _ message: String) {
let alertVC = UIAlertController(title: title, message: message, preferredStyle: .alert)
alertVC.addAction(UIAlertAction(title: "OK", style: .default))
present(alertVC, animated: true, completion: nil)
}
// MARK: - InterfaceBuilder Links
@IBOutlet var activityIndicator: UIActivityIndicatorView!
@IBOutlet var tableView: UITableView!
@IBOutlet var itemCountLabel: UILabel!
@IBOutlet var totalPrice: UILabel!
@IBAction func onClear() {
}
@IBAction func onOrder(_ sender: UIButton) {
// TODO: no selection
// showAlert("Order Fail", "No Orders")
// performSegue(withIdentifier: "OrderViewController", sender: nil)
viewModel.totalPrice += 100
updateUI()
}
// updateUI로 따로 함수를 뺐음. 근데 우리가 이걸 매번 호출해줘야되냐..? 싫다.
func updateUI() {
totalPrice.text = "\(viewModel.totalPrice)"
itemCountLabel.text = "\(viewModel.itemsCount)"
}
}
extension MenuViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return viewModel.menus.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "MenuItemTableViewCell") as! MenuItemTableViewCell
let menu = viewModel.menus[indexPath.row]
cell.title.text = "\(menu.name)"
cell.price.text = "\(menu.price)"
cell.count.text = "\(menu.count)"
return cell
}
}
毎回updateUI()と呼びますか?やめて!viewModelが変わればviewも変わる!->観測性を利用する。
// 여기부터는 바뀌는 부분만 필기.
class MenuListViewModel {
let menus: [Menu] = [
Menu(name: "보아", price: 1105, count: 0),
Menu(name: "태연", price: 309, count: 0),
Menu(name: "효연", price: 922, count: 0),
Menu(name: "슬기", price: 210, count: 0),
Menu(name: "웬디", price: 224, count: 0),
Menu(name: "카리나", price: 411, count: 0),
Menu(name: "윈터", price: 101, count: 0)
]
var itemsCount: Int = 5
var totalPrice: Observable<Int> = Observable.just(10000)
}
// MenuVC
var viewModel = MenuListViewModel()
var disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
viewModel.totalPrice
.map { $0.currencyKR() }
.subscribe(onNext: {
self.totalPrice.text = $0
})
.disposed(by: disposeBag)
}
@IBAction func onOrder(_ sender: UIButton) {
// TODO: no selection
// showAlert("Order Fail", "No Orders")
// performSegue(withIdentifier: "OrderViewController", sender: nil)
// viewModel.totalPrice += 100
// 근데.. viewMode.totalPrice (Observable)에 값을 어떻게 보낼건데..?
// Subject를 사용하자.
// 근데 onNext 는 값을 보내기만 하는데 어떻게 쌓지? -> scan
viewModel.totalPrice.onNext(100)
}
6.Publish Subject:updateUIを価格変更の論理に書き込む必要はありません。ViewDidLoadで1回購読するだけで、自動的に値が変更されます。
subject:observerは値を受信してもよいし、値を送信してもよい// MenuListViewModel
class MenuListViewModel {
let menus: [Menu] = [
// 생략
]
var itemsCount: Int = 5
var totalPrice: PublishSubject<Int> = PublishSubject()
}
// MenuVC
override func viewDidLoad() {
super.viewDidLoad()
// 처음 불러오기.
viewModel.totalPrice
.scan(0, accumulator: +) // 100 보내기만 하는걸 쌓아줌.
.map { $0.currencyKR() }
.subscribe(onNext: {
self.totalPrice.text = $0
})
.disposed(by: disposeBag)
}
@IBAction func onOrder(_ sender: UIButton) {
// Subject가 등장!
viewModel.totalPrice.onNext(100) // 100을 보내기만 함
}
それではMenuListをSubjectに変えて見た時にItemsCountTotalPriceをそこから持ってきてくださいclass MenuListViewModel {
// 그러면..? 메뉴 리스트를 지켜보다가 값을 가지고 오면 되겠네
// lazy var menuObservable = Observable.just(menus)
// 근데 메뉴는 정보를 받아서 바뀔 수도 있어야지?
// init으로 menu array를 빼면서 lazy도 뺄 수 있게 되었다.
var menuObservable = PublishSubject<[Menu]>()
lazy var itemsCount = menuObservable.map { menu in
menu.map { $0.count }.reduce(0, +)
}
lazy var totalPrice = menuObservable.map { menu in
$0.map { $0.price * $0.count }.reduce(0, +)
}
// Subject : 값을 보내줄 수도 있고, 값을 받아올 수도 있음.
init() {
let menus: [Menu] = [
Menu(name: "오지지", price: 15000, count: 0),
Menu(name: "SMTOWN", price: 39000, count: 0),
Menu(name: "오지지", price: 12000, count: 0),
Menu(name: "오지지", price: 12000, count: 0),
Menu(name: "오지지", price: 12000, count: 0),
Menu(name: "오지지", price: 12000, count: 0)
]
menuObservable.onNext(menus)
}
}
このつながりをstreamと呼ぶ.
7.ラベルはRxCoCocoa、バインドTable View
1.上ではViewModelのデータをSubjectに変換し、ビューでもそれに応じてデータを受信して画面に表示します。
subscribeでデータを受信できます.// viewDidLoad
viewModel.itemsCount
.map { "\($0)" }
.subscribe(onNext: {
itemCountLabel.text = $0
})
.disposed(by: disposeBag)
RxCocoaを使ってbindでデータを受信できます!// RxCocoa : RxSiwft 기능을 Extension 으로 UI 에 추가한 것.
// 일반 text와의 차이점은 bind 인데,
// subscribe 대신 bind(to: itemCountLable.rx.text) 를 하면,
// 순환 참조가 안생긴다.
// 대신 UI 작업이기 때문에 꼭 MainScheduler로 바꾸고 동작하게 해야한다.
viewModel.itemsCount
.map { "\($0)" }
.observeOn(MainScheduler.instance)
.bind(to: itemCountLabel.rx.text)
.disposed(by: disposeBag)
2.tableView binding:tableView itemもバインドできます。
// viewDidLoad
// bind 할 것 이므로 dataSource 연결 끊고 관련 코드 없애도 됨.
tableView.dataSource = nil
viewModel.menuObservable
.bind(to: tableView.rx.items(cellIdentifier: "MenuItemTableViewCell", cellType: MenuItemTableViewCell.self)) { index, item, cell in
//앞의 bind를 실행하면서 tableView의 items에 cellId, cellType을 넣으면
// (ObservableType) -> ((@escaping (Int, Sequence.Element, Cell) -> Void) -> Disposable)
// 위와 같은 escaping closure 를 리턴하게 됨.
// 그러면 함수를 실행 한 다음에 index, item, cell 을 가지고 tableView의 item에 정보를 전달할 수 있다.
cell.title.text = item.name
cell.price.text = "\(item.price)"
cell.count.text = "\(item.count)"
}
でも…?実行できますが、table viewには何のデータもありません・・・menuobserverableはPublishSubjectだから!
8.Behavior Subject:データは生成されましたが、menuobserverableを購読したため、以前に生成されたデータは受信できません。Behavior Subjectに変更します。
// MenuListViewModel
var menuObservable = BehaviorSubject<[Menu]>(value: [])
正常に動作するように、orderボタンを押してtableViewプロジェクトを変更します.// MenuVC
@IBAction func onOrder(_ sender: UIButton) {
viewModel.menuObservable.onNext([
Menu(name: "a", price: Int.random(in: 0..<10) * 100, count: Int.random(in: 0..<10)),
Menu(name: "b", price: Int.random(in: 0..<10) * 100, count: Int.random(in: 0..<10)),
Menu(name: "c", price: Int.random(in: 0..<10) * 100, count: Int.random(in: 0..<10)),
])
}
menuObserverableは配列のみ変更され、itemsCount、totalPriceも自動的に変更されます.
9.ボタンを有効にする
1.clearボタン
// clear 버튼을 누르면 count가 다 0으로 바뀌어야 한다.
// MenuVC
@IBAction func onClear() {
viewModel.clearAllItemSelections()
}
// MenuListViewModel
func clearAllItemSelections() {
// menu를 바꿔야하니까 menuObservable을 가져와서 menu count 0 으로 바꾼다.
menuObservable
.map { menus in
menus.map { m in
Menu(name: m.name, price: m.price, count: 0)
}
}
.take(1) // 버튼 눌렀을 때 한번 하고 죽어야함. 계속 남아있으면서 clear 하면 안됨.
.subscribe(onNext: {
self.menuObservable.onNext($0)
})
}
2+-ボタン:cellではcellで処理する必要があります。
方法viewModelを変数としてセルに渡し、受信して処理します.
メソッド2.+-ボタンのIBACTIONで実行する関数をTable View cell bindで処理する
方法delegateclass MenuItemTableViewCell: UITableViewCell {
@IBOutlet var title: UILabel!
@IBOutlet var count: UILabel!
@IBOutlet var price: UILabel!
// 방법1. viewModel을 넘겨주고 받아서 처리하던가.
var viewModel: MenuListViewModel! //을 받아서 처리하거나.
// 방법2. 쉬운 방법은 onChange를 넘겨서 처리.
var onChange: ((Int) -> Void)?
@IBAction func onIncreaseCount() {
onChange?(+1)
}
@IBAction func onDecreaseCount() {
onChange?(-1)
}
}
// MenuVC > viewDidLoad
viewModel.menuObservable
.bind(to: tableView.rx.items(cellIdentifier: "MenuItemTableViewCell", cellType: MenuItemTableViewCell.self)) { index, item, cell in
cell.title.text = item.name
cell.price.text = "\(item.price)"
cell.count.text = "\(item.count)"
cell.onChange = { [weak self] increase in
self?.viewModel.changeCount(item: item, increase: increase)
}
}
.disposed(by: disposeBag)
// MenuListViewModel
func changeCount(item: Menu, increase: Int) {
_ = menuObservable
.map { menus in
menus.map { menu in
// menu에 id 추가해서 관련된 부분 바꾸자.
if menu.id == item.id {
return Menu(id: menu.id, name: menu.name, price: menu.price, count: menu.count + increase)
} else {
return Menu(id: menu.id, name: menu.name, price: menu.price, count: menu.count)
}
}
}
.take(1)
.subscribe(onNext: {
self.menuObservable.onNext($0)
})
}
10. fetch
これまでのようにコードを記述する場合は、viewModelを使用してテストするだけです.testcaseの作成が容易になりました.前述のように、APIが出る前にアプリを作成してから降りる場合は、model->viewModelで一度ページングすれば良いのですが、ロジックが修正されている場合はviewModelに触れるだけで良いのです.// APIService
import Foundation
let MenuUrl = "https://firebasestorage.googleapis.com/v0/b/rxswiftin4hours.appspot.com/o/fried_menus.json?alt=media&token=42d5cb7e-8ec4-48f9-bf39-3049e796c936"
class APIService {
// 기존 fetch 코드
static func fetchAllMenus(onComplete: @escaping (Result<Data, Error>) -> Void) {
URLSession.shared.dataTask(with: URL(string: MenuUrl)!) { data, res, err in
if let err = err {
onComplete(.failure(err))
return
}
guard let data = data else {
let httpResponse = res as! HTTPURLResponse
onComplete(.failure(NSError(domain: "no data",
code: httpResponse.statusCode,
userInfo: nil)))
return
}
onComplete(.success(data))
}.resume()
}
// 근데 그러면 기존 프로젝트를 Rx로 리팩토링할 때 아예 다 새로만드나요?
// 아니요! 기존 fetch 함수를 사용하면 됩니다.
// legacy를 rx로 감싸서 리팩토링하면 된다.
static func fetchAllMenusRx() -> Observable<Data> {
return Observable.create() { emmiter in
fetchAllMenus { result in
switch result {
case .success(let data):
emitter.onNext(data)
emitter.onCompleted()
case .failure(let err):
emitter.onError(err)
}
return Disposables.create()
}
}
}
// MenuListViewModel
init() {
_ = APIService.fetchAllMenusRx()
.map { data -> [MenuItem] in
struct Response: Decodable {
let menus: [MenuItem]
}
let response = try! JSONDecoder().decode(Response.self, from: data)
return response.menus
}
// menuItem -> menu
// menu에 extension 작성해서 바꾼다.
.map { menuItems in
var menus: [Menu] = []
// 원래 여기도 map으로 하려고 했으나 id에 index 를 사용하기 위해서 enumerated().forEach 사용
menuItems.enumerated().forEach { (index, menuItem) in
let menu = Menu.fromMenuItems(id: index, item: menuItem)
menus.append(menu)
}
return menus
}
.take(1)
.bind(to: menuObservable)
}
// model -> view를 위한 model 로 바꾸는 코드.
extension Menu {
static func fromMenuItems(id: Int, item: MenuItem) -> Menu {
return Menu(id: id, name: item.name, price: item.price, count: 0)
}
}
これにより、コードを作成し、APIを取得した後、fetch、init、extensionだけを追加して、正常なアプリケーションを作成することができます.
ソース
4時間終了
https://youtu.be/iHKBNYMWd5I
RxSwift 4時間でgithub終了
https://github.com/iamchiwon/RxSwift_In_4_Hours
Reference
この問題について([RxSWIFT]#3 UIコンポーネントとの併用), 我々は、より多くの情報をここで見つけました
https://velog.io/@ddosang/RxSwift-3-UI-컴포넌트와-연동
テキストは自由に共有またはコピーできます。ただし、このドキュメントのURLは参考URLとして残しておいてください。
Collection and Share based on the CC Protocol
1.プロジェクトの状況を仮定する
理論的には,クライアント側はAPI,設計後に開発を行うが,実際の作業ではそうではないことが多い.APIやデザインが分からないまま、企画書だけ読んで開発しています.APIは後で接続され、デザインも後で追加されます->最適構造MVVM~~
2.開発~
ViewController == VC
1. Menu.swift-Viewのモデルを作成する(ビューに表示されるモデルのみ)
struct Menu {
var name: String = ""
var price: Int = 0
var count: Int = 0
init(name: String, price: Int, count: Int) {
self.name = name
self.price = price
self.count = count
}
}
2.MenuVC-メニュー(ViewModelアレイ)を作成し、Table Viewに接続します。
3.MenuListViewModelを使用してメニューレイアウトを個別に管理-ビューに情報をインポートする場合は、VCでviewModelインスタンスを作成してインポートするだけです。
// MenuListViewModel
class MenuListViewModel {
let menus: [Menu] = [
Menu(name: "보아", price: 1105, count: 0),
Menu(name: "태연", price: 309, count: 0),
Menu(name: "효연", price: 922, count: 0),
Menu(name: "슬기", price: 210, count: 0),
Menu(name: "웬디", price: 224, count: 0),
Menu(name: "카리나", price: 411, count: 0),
Menu(name: "윈터", price: 101, count: 0)
]
var itemsCount: Int = 5
var totalPrice: Int = 10000
}
4.viewModelのtotalPrice、itemsCount情報を取得してみます。正しく導入されているかどうかはわかりませんが、onOrderボタンを押すと総価格が100増加します。
// MenuVC
class MenuViewController: UIViewController {
// MARK: - Life Cycle
// viewModel 인스턴스 생성해서 정보 받아오기
let viewModel = MenuListViewModel();
override func viewDidLoad() {
super.viewDidLoad()
updateUI()
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let identifier = segue.identifier ?? ""
if identifier == "OrderViewController",
let orderVC = segue.destination as? OrderViewController {
// TODO: pass selected menus
}
}
func showAlert(_ title: String, _ message: String) {
let alertVC = UIAlertController(title: title, message: message, preferredStyle: .alert)
alertVC.addAction(UIAlertAction(title: "OK", style: .default))
present(alertVC, animated: true, completion: nil)
}
// MARK: - InterfaceBuilder Links
@IBOutlet var activityIndicator: UIActivityIndicatorView!
@IBOutlet var tableView: UITableView!
@IBOutlet var itemCountLabel: UILabel!
@IBOutlet var totalPrice: UILabel!
@IBAction func onClear() {
}
@IBAction func onOrder(_ sender: UIButton) {
// TODO: no selection
// showAlert("Order Fail", "No Orders")
// performSegue(withIdentifier: "OrderViewController", sender: nil)
viewModel.totalPrice += 100
updateUI()
}
// updateUI로 따로 함수를 뺐음. 근데 우리가 이걸 매번 호출해줘야되냐..? 싫다.
func updateUI() {
totalPrice.text = "\(viewModel.totalPrice)"
itemCountLabel.text = "\(viewModel.itemsCount)"
}
}
extension MenuViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return viewModel.menus.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "MenuItemTableViewCell") as! MenuItemTableViewCell
let menu = viewModel.menus[indexPath.row]
cell.title.text = "\(menu.name)"
cell.price.text = "\(menu.price)"
cell.count.text = "\(menu.count)"
return cell
}
}
毎回updateUI()と呼びますか?やめて!viewModelが変わればviewも変わる!->観測性を利用する。
// 여기부터는 바뀌는 부분만 필기.
class MenuListViewModel {
let menus: [Menu] = [
Menu(name: "보아", price: 1105, count: 0),
Menu(name: "태연", price: 309, count: 0),
Menu(name: "효연", price: 922, count: 0),
Menu(name: "슬기", price: 210, count: 0),
Menu(name: "웬디", price: 224, count: 0),
Menu(name: "카리나", price: 411, count: 0),
Menu(name: "윈터", price: 101, count: 0)
]
var itemsCount: Int = 5
var totalPrice: Observable<Int> = Observable.just(10000)
}
// MenuVC
var viewModel = MenuListViewModel()
var disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
viewModel.totalPrice
.map { $0.currencyKR() }
.subscribe(onNext: {
self.totalPrice.text = $0
})
.disposed(by: disposeBag)
}
@IBAction func onOrder(_ sender: UIButton) {
// TODO: no selection
// showAlert("Order Fail", "No Orders")
// performSegue(withIdentifier: "OrderViewController", sender: nil)
// viewModel.totalPrice += 100
// 근데.. viewMode.totalPrice (Observable)에 값을 어떻게 보낼건데..?
// Subject를 사용하자.
// 근데 onNext 는 값을 보내기만 하는데 어떻게 쌓지? -> scan
viewModel.totalPrice.onNext(100)
}
6.Publish Subject:updateUIを価格変更の論理に書き込む必要はありません。ViewDidLoadで1回購読するだけで、自動的に値が変更されます。
subject:observerは値を受信してもよいし、値を送信してもよい
// MenuListViewModel
class MenuListViewModel {
let menus: [Menu] = [
// 생략
]
var itemsCount: Int = 5
var totalPrice: PublishSubject<Int> = PublishSubject()
}
// MenuVC
override func viewDidLoad() {
super.viewDidLoad()
// 처음 불러오기.
viewModel.totalPrice
.scan(0, accumulator: +) // 100 보내기만 하는걸 쌓아줌.
.map { $0.currencyKR() }
.subscribe(onNext: {
self.totalPrice.text = $0
})
.disposed(by: disposeBag)
}
@IBAction func onOrder(_ sender: UIButton) {
// Subject가 등장!
viewModel.totalPrice.onNext(100) // 100을 보내기만 함
}
それではMenuListをSubjectに変えて見た時にItemsCountTotalPriceをそこから持ってきてくださいclass MenuListViewModel {
// 그러면..? 메뉴 리스트를 지켜보다가 값을 가지고 오면 되겠네
// lazy var menuObservable = Observable.just(menus)
// 근데 메뉴는 정보를 받아서 바뀔 수도 있어야지?
// init으로 menu array를 빼면서 lazy도 뺄 수 있게 되었다.
var menuObservable = PublishSubject<[Menu]>()
lazy var itemsCount = menuObservable.map { menu in
menu.map { $0.count }.reduce(0, +)
}
lazy var totalPrice = menuObservable.map { menu in
$0.map { $0.price * $0.count }.reduce(0, +)
}
// Subject : 값을 보내줄 수도 있고, 값을 받아올 수도 있음.
init() {
let menus: [Menu] = [
Menu(name: "오지지", price: 15000, count: 0),
Menu(name: "SMTOWN", price: 39000, count: 0),
Menu(name: "오지지", price: 12000, count: 0),
Menu(name: "오지지", price: 12000, count: 0),
Menu(name: "오지지", price: 12000, count: 0),
Menu(name: "오지지", price: 12000, count: 0)
]
menuObservable.onNext(menus)
}
}
このつながりをstreamと呼ぶ.7.ラベルはRxCoCocoa、バインドTable View
1.上ではViewModelのデータをSubjectに変換し、ビューでもそれに応じてデータを受信して画面に表示します。
subscribeでデータを受信できます.
// viewDidLoad
viewModel.itemsCount
.map { "\($0)" }
.subscribe(onNext: {
itemCountLabel.text = $0
})
.disposed(by: disposeBag)
RxCocoaを使ってbindでデータを受信できます!// RxCocoa : RxSiwft 기능을 Extension 으로 UI 에 추가한 것.
// 일반 text와의 차이점은 bind 인데,
// subscribe 대신 bind(to: itemCountLable.rx.text) 를 하면,
// 순환 참조가 안생긴다.
// 대신 UI 작업이기 때문에 꼭 MainScheduler로 바꾸고 동작하게 해야한다.
viewModel.itemsCount
.map { "\($0)" }
.observeOn(MainScheduler.instance)
.bind(to: itemCountLabel.rx.text)
.disposed(by: disposeBag)
2.tableView binding:tableView itemもバインドできます。
// viewDidLoad
// bind 할 것 이므로 dataSource 연결 끊고 관련 코드 없애도 됨.
tableView.dataSource = nil
viewModel.menuObservable
.bind(to: tableView.rx.items(cellIdentifier: "MenuItemTableViewCell", cellType: MenuItemTableViewCell.self)) { index, item, cell in
//앞의 bind를 실행하면서 tableView의 items에 cellId, cellType을 넣으면
// (ObservableType) -> ((@escaping (Int, Sequence.Element, Cell) -> Void) -> Disposable)
// 위와 같은 escaping closure 를 리턴하게 됨.
// 그러면 함수를 실행 한 다음에 index, item, cell 을 가지고 tableView의 item에 정보를 전달할 수 있다.
cell.title.text = item.name
cell.price.text = "\(item.price)"
cell.count.text = "\(item.count)"
}
でも…?実行できますが、table viewには何のデータもありません・・・menuobserverableはPublishSubjectだから!8.Behavior Subject:データは生成されましたが、menuobserverableを購読したため、以前に生成されたデータは受信できません。Behavior Subjectに変更します。
// MenuListViewModel
var menuObservable = BehaviorSubject<[Menu]>(value: [])
正常に動作するように、orderボタンを押してtableViewプロジェクトを変更します.// MenuVC
@IBAction func onOrder(_ sender: UIButton) {
viewModel.menuObservable.onNext([
Menu(name: "a", price: Int.random(in: 0..<10) * 100, count: Int.random(in: 0..<10)),
Menu(name: "b", price: Int.random(in: 0..<10) * 100, count: Int.random(in: 0..<10)),
Menu(name: "c", price: Int.random(in: 0..<10) * 100, count: Int.random(in: 0..<10)),
])
}
menuObserverableは配列のみ変更され、itemsCount、totalPriceも自動的に変更されます.
9.ボタンを有効にする
1.clearボタン
// clear 버튼을 누르면 count가 다 0으로 바뀌어야 한다.
// MenuVC
@IBAction func onClear() {
viewModel.clearAllItemSelections()
}
// MenuListViewModel
func clearAllItemSelections() {
// menu를 바꿔야하니까 menuObservable을 가져와서 menu count 0 으로 바꾼다.
menuObservable
.map { menus in
menus.map { m in
Menu(name: m.name, price: m.price, count: 0)
}
}
.take(1) // 버튼 눌렀을 때 한번 하고 죽어야함. 계속 남아있으면서 clear 하면 안됨.
.subscribe(onNext: {
self.menuObservable.onNext($0)
})
}
2+-ボタン:cellではcellで処理する必要があります。
方法viewModelを変数としてセルに渡し、受信して処理します.
メソッド2.+-ボタンのIBACTIONで実行する関数をTable View cell bindで処理する
方法delegate
class MenuItemTableViewCell: UITableViewCell {
@IBOutlet var title: UILabel!
@IBOutlet var count: UILabel!
@IBOutlet var price: UILabel!
// 방법1. viewModel을 넘겨주고 받아서 처리하던가.
var viewModel: MenuListViewModel! //을 받아서 처리하거나.
// 방법2. 쉬운 방법은 onChange를 넘겨서 처리.
var onChange: ((Int) -> Void)?
@IBAction func onIncreaseCount() {
onChange?(+1)
}
@IBAction func onDecreaseCount() {
onChange?(-1)
}
}
// MenuVC > viewDidLoad
viewModel.menuObservable
.bind(to: tableView.rx.items(cellIdentifier: "MenuItemTableViewCell", cellType: MenuItemTableViewCell.self)) { index, item, cell in
cell.title.text = item.name
cell.price.text = "\(item.price)"
cell.count.text = "\(item.count)"
cell.onChange = { [weak self] increase in
self?.viewModel.changeCount(item: item, increase: increase)
}
}
.disposed(by: disposeBag)
// MenuListViewModel
func changeCount(item: Menu, increase: Int) {
_ = menuObservable
.map { menus in
menus.map { menu in
// menu에 id 추가해서 관련된 부분 바꾸자.
if menu.id == item.id {
return Menu(id: menu.id, name: menu.name, price: menu.price, count: menu.count + increase)
} else {
return Menu(id: menu.id, name: menu.name, price: menu.price, count: menu.count)
}
}
}
.take(1)
.subscribe(onNext: {
self.menuObservable.onNext($0)
})
}
10. fetch
これまでのようにコードを記述する場合は、viewModelを使用してテストするだけです.testcaseの作成が容易になりました.前述のように、APIが出る前にアプリを作成してから降りる場合は、model->viewModelで一度ページングすれば良いのですが、ロジックが修正されている場合はviewModelに触れるだけで良いのです.
// APIService
import Foundation
let MenuUrl = "https://firebasestorage.googleapis.com/v0/b/rxswiftin4hours.appspot.com/o/fried_menus.json?alt=media&token=42d5cb7e-8ec4-48f9-bf39-3049e796c936"
class APIService {
// 기존 fetch 코드
static func fetchAllMenus(onComplete: @escaping (Result<Data, Error>) -> Void) {
URLSession.shared.dataTask(with: URL(string: MenuUrl)!) { data, res, err in
if let err = err {
onComplete(.failure(err))
return
}
guard let data = data else {
let httpResponse = res as! HTTPURLResponse
onComplete(.failure(NSError(domain: "no data",
code: httpResponse.statusCode,
userInfo: nil)))
return
}
onComplete(.success(data))
}.resume()
}
// 근데 그러면 기존 프로젝트를 Rx로 리팩토링할 때 아예 다 새로만드나요?
// 아니요! 기존 fetch 함수를 사용하면 됩니다.
// legacy를 rx로 감싸서 리팩토링하면 된다.
static func fetchAllMenusRx() -> Observable<Data> {
return Observable.create() { emmiter in
fetchAllMenus { result in
switch result {
case .success(let data):
emitter.onNext(data)
emitter.onCompleted()
case .failure(let err):
emitter.onError(err)
}
return Disposables.create()
}
}
}
// MenuListViewModel
init() {
_ = APIService.fetchAllMenusRx()
.map { data -> [MenuItem] in
struct Response: Decodable {
let menus: [MenuItem]
}
let response = try! JSONDecoder().decode(Response.self, from: data)
return response.menus
}
// menuItem -> menu
// menu에 extension 작성해서 바꾼다.
.map { menuItems in
var menus: [Menu] = []
// 원래 여기도 map으로 하려고 했으나 id에 index 를 사용하기 위해서 enumerated().forEach 사용
menuItems.enumerated().forEach { (index, menuItem) in
let menu = Menu.fromMenuItems(id: index, item: menuItem)
menus.append(menu)
}
return menus
}
.take(1)
.bind(to: menuObservable)
}
// model -> view를 위한 model 로 바꾸는 코드.
extension Menu {
static func fromMenuItems(id: Int, item: MenuItem) -> Menu {
return Menu(id: id, name: item.name, price: item.price, count: 0)
}
}
これにより、コードを作成し、APIを取得した後、fetch、init、extensionだけを追加して、正常なアプリケーションを作成することができます.ソース
4時間終了
https://youtu.be/iHKBNYMWd5I
RxSwift 4時間でgithub終了
https://github.com/iamchiwon/RxSwift_In_4_Hours
Reference
この問題について([RxSWIFT]#3 UIコンポーネントとの併用), 我々は、より多くの情報をここで見つけました https://velog.io/@ddosang/RxSwift-3-UI-컴포넌트와-연동テキストは自由に共有またはコピーできます。ただし、このドキュメントのURLは参考URLとして残しておいてください。
Collection and Share based on the CC Protocol