λ³Έλ¬Έ λ°”λ‘œκ°€κΈ°
🍎 iOS/RxSwift

[iOS] MVVM & RxSwift μŠ€ν„°λ”” W2 - TableView bind items, modelSelected, cell prepareForReuse

by Danna 2021. 7. 15.
728x90
728x90
 

[iOS] MVVM & RxSwift μŠ€ν„°λ”” W2 - TableView λ§Œλ“€μ–΄λ³΄κΈ°

iamchiwon/RxSwift_In_4_Hours RxSwift, 4μ‹œκ°„ μ•ˆμ— λΉ λ₯΄κ²Œ μ΅ν˜€ 싀무에 μ‚¬μš©ν•˜κΈ°. Contribute to iamchiwon/RxSwift_In_4_Hours development by creating an account on GitHub. github.com κ³°νŠ€κΉ€λ‹˜ κ°•μ˜ μ˜μƒμ˜..

jellysong.tistory.com

κ³°νŠ€κΉ€λ‹˜ RxSwift κ°•μ˜ 쀑 step1 을 MVVM + TableView 둜 λ°”κΏ”λ³Έ μœ„μ˜ ν¬μŠ€νŒ…μ—μ„œ View Controller μ—μ„œμ˜ 데이터바인딩, TableViewCell μ—μ„œ 이미지 처리λ₯Ό λ”°λ‘œ μ •λ¦¬ν•œ κΈ€μž…λ‹ˆλ‹€!


1 ListViewController / 2 DetailViewController / 3 MemberCell 


 

1. ListViewController - 멀버 데이터λ₯Ό 리슀트(TableView) 둜 λ³΄μ—¬μ£ΌλŠ” ν™”λ©΄

  • ViewController λŠ” viewModel 을 κ°–κ³ , 이λ₯Ό 톡해 데이터λ₯Ό μ²˜λ¦¬ν•©λ‹ˆλ‹€ !
  • ViewController μ—μ„œ μ‹ κ²½μ¨μ•Όν•˜λŠ” μž‘μ—…μ€ 데이터가 λ³€κ²½λ˜μ—ˆμ„ λ•Œ, UI μ—…λ°μ΄νŠΈλ₯Ό μ–΄λ–»κ²Œ 해야할지? 에 λŒ€ν•œ μž‘μ—…μž…λ‹ˆλ‹€.
  • setBinding λ©”μ†Œλ“œμ— λͺ¨μ•„λ‘” λ‚΄μš©μ΄ ν•΅μ‹¬μž…λ‹ˆλ‹€ ✨

맨 μ²˜μŒμ— 데이터λ₯Ό λ°›μ•„μ˜€λŠ” λͺ¨μŠ΅!

 

πŸ‘€ setBinding λ©”μ†Œλ“œ

1. TableView 에 데이터 보여주기

viewModel.members -- (bind) --> tableView.rx.items(cellIdentifier: cellType:)

UITableViewDataSource ν”„λ‘œν† μ½œμ—μ„œ ν•„μˆ˜ λ©”μ†Œλ“œλ₯Ό κ΅¬ν˜„ν•΄ 데이터λ₯Ό ν‘œν˜„ν•˜λŠ” 것과 λ™μΌν•˜λ‹€. 

 

 

2. activity Indicator μ• λ‹ˆλ©”μ΄μ…˜ λ‚˜νƒ€λ‚΄κΈ°

viewModel.members -- map {$0.isEmpty} -- (subscribe) --> setVisibleWithAnimation(v: s:)

Behavior Subject 이기 λ•Œλ¬Έμ— ꡬ독을 ν•˜μžλ§ˆμž 빈 데이터가 λ°›μ•„μ™€μ§‘λ‹ˆλ‹€. μ•± μ‹€ν–‰μ‹œ μžλ™μœΌλ‘œ indicator κ°€ λ‚˜νƒ€λ‚˜λ„λ‘ ν•˜κ³ , 데이터가 λ°›μ•„μ™€μ‘Œμ„ λ•Œ indicator κ°€ 사라지도둝 ν•˜κΈ° μœ„ν•΄ map operator λ₯Ό μΆ”κ°€ν–ˆμŠ΅λ‹ˆλ‹€.

 

 

3. TableView Cell 선택 μ‹œ, Detail View Controller 둜 μ΄λ™ν•˜κΈ°

tableView.rx.modelSelected -- (subscribe) --> presentDetail(member)

TableView 의 Cell 을 μ„ νƒν–ˆμ„ λ•Œ, 싀행될 λ™μž‘λ“€μ€ 보톡 UITableViewDelegate ν”„ν† λ‘œμ½œμ˜ didSelectRowAt λ©”μ†Œλ“œμ— κ΅¬ν˜„ν•©λ‹ˆλ‹€. RxCocoa μ—μ„œ μ œκ³΅λ˜λŠ” tableView.rx.itemSelected ν˜Ήμ€ modelSelected λ₯Ό μ΄μš©ν•˜λ©΄ μœ„μ˜ λ©”μ†Œλ“œλ₯Ό κ΅¬ν˜„ν•¨κ³Ό λ™μΌν•©λ‹ˆλ‹€! 

 

  • tableView.rx.itemSelected : μ„ νƒλœ Cell μœ„μΉ˜μ˜ IndexPath λ₯Ό μ „λ‹¬ν•œλ‹€.
  • tableView.rx.modelSelected : μ„ νƒλœ Cell μœ„μΉ˜μ˜ Model 을 μ „λ‹¬ν•œλ‹€.

 

* μ°Έκ³  * 
* viewModel.members : BehaviorSubject, κΈ°λ³Έ κ°’ ν˜Ήμ€ 이전 값을 μ „λ‹¬λ°›λŠ” Subject.
* observe(on:) : λ‹€μŒ observable 이 μˆ˜ν–‰λ  μ“°λ ˆλ“œλ₯Ό μ§€μ •ν•œλ‹€. UI μž‘μ—…μ€ MainSchedular.instance μ—μ„œ.
* dispose(by: disposeBag) : ViewController κ°€ μ‚¬λΌμ§ˆ λ•Œ, disposable 듀도 ν•¨κ»˜ λ©”λͺ¨λ¦¬μ—μ„œ ν•΄μ œν•˜κΈ° μœ„ν•¨.
* bind : RxCocoa μ—μ„œ μ œκ³΅ν•˜λŠ” λ©”μ†Œλ“œ, subscribe 둜 값을 직접 λŒ€μž…ν•˜λŠ” 것 λŒ€μ‹  bind 둜 Outlet λ³€μˆ˜μ™€ μ—°κ²°κ°€λŠ₯ν•˜λ‹€. 

 

* 글이 λ„ˆλ¬΄ κΈΈμ–΄μ Έμ„œ 전체 μ½”λ“œλŠ” 링크둜 μ—°κ²°ν• κ²Œμš” 

class ViewController: UIViewController {
    
    @IBOutlet var timerLabel: UILabel!
    @IBOutlet var tableView: UITableView!
    @IBOutlet var activityIndicator: UIActivityIndicatorView!
    
    let viewModel = MemberViewModel.shared
    let disposeBag = DisposeBag()
    let cellId = "MemberCell"

    override func viewDidLoad() {
        super.viewDidLoad()
        setTimer()
        setBinding()
    }
    
    private func setBinding() {
        
        // tableView datasource -- 데이터 개수, 셀에 λ“€μ–΄κ°ˆ λ‚΄μš©
        viewModel.members
            .observe(on: MainScheduler.instance)
            .filter { !$0.isEmpty }
            .bind(to: tableView.rx.items(cellIdentifier: cellId, cellType: MemberCell.self)) { index, item, cell in
                cell.updateUI(member: item)
            }
            .disposed(by: disposeBag)

        // activityIndicator
        viewModel.members
            .observe(on: MainScheduler.instance)
            .map { $0.isEmpty }
            .subscribe(onNext: { [weak self] visible in
                self?.setVisibleWithAnimation(self?.activityIndicator, visible)
            })
            .disposed(by: disposeBag)
        
        // tableView delegate
        // tableView.rx.itemSelected -> indexPath
        tableView.rx.modelSelected(Member.self)
            .observe(on: MainScheduler.instance)
            .subscribe(onNext: { [weak self] member in
                self?.presentDetail(of: member)
            })
            .disposed(by: disposeBag)
    }
    
    // Other methods ...
}

μ—¬κΈ°μ„œ, bind + observe(on: Main) 의 μ‘°ν•© λŒ€μ‹  drive λ₯Ό μ‚¬μš©ν•  μˆ˜λ„ μžˆμŠ΅λ‹ˆλ‹€.


bind 와 drive λͺ¨λ‘ λ‚΄λΆ€μ μœΌλ‘œ subscribe λ₯Ό μ“°κ³ μžˆμœΌλ©°, drive λŠ” 무쑰건 MainThread μ—μ„œ λ™μž‘ν•œλ‹€.
이와 λ”λΆˆμ–΄ asDriver(onErrorJustReturn:) λ“±μ„ μ΄μš©ν•˜λ©΄ μ—λŸ¬κ°€ 났을 λ•Œ, μ²˜λ¦¬ν•  기본값을 μ„€μ •ν•  수 μžˆλ‹€.
λ˜ν•œ Subject λŒ€μ‹  λŠμ–΄μ§€μ§€ μ•ŠλŠ” νƒ€μž…μ„ Relay λΌκ³  ν•œλ‹€. μ—λŸ¬κ°€ λ‚˜κ±°λ‚˜ completed λ˜μ§€ μ•Šκ³ , 항상 onNext 만 있기 λ•Œλ¬Έμ— onNext κ°€ μ•„λ‹Œ accept 둜 ν‘œν˜„ν•œλ‹€.
UI μž‘μ—…μ€ ν•­μƒ UI Thread (= Main) μ—μ„œ λŒμ•„κ°€μ•Ό ν•˜κ³ , μ—λŸ¬κ°€ λ‚œλ‹€κ³  μ—°κ²°μ΄ λŠμ–΄μ§€λ©΄ μ•ˆλ˜λŠ” νŠΉμ§•μ΄ μžˆλ‹€.
* RxCocoa κ΄€λ ¨λ˜μ–΄μ„œλŠ” μ°¨μ°¨ λ”°λ‘œ μ •λ¦¬ν•˜κ³ , ν•„μš”ν•˜λ‹€λ©΄ μ½”λ“œλ₯Ό μˆ˜μ •ν•΄λ³΄κ² μŠ΅λ‹ˆλ‹€! πŸ₯Ί

μ •λ¦¬ν•˜μžλ©΄, UI 와 κ΄€λ ¨λœ μ½”λ“œλŠ” Relay + drive 의 μ‘°ν•©μœΌλ‘œ νŽΈν•˜κ³  μ•ˆμ „ν•˜κ²Œ μ‚¬μš©ν•  수 μžˆλ‹€.
λ‹¨μˆœνžˆ 값을 μ‚¬μš©ν•˜κ±°λ‚˜ μ €μž₯ν•˜κΈ° μœ„ν•΄μ„œλŠ” Subject + bind/ subscribe 의 쑰합을 μ΄μš©ν•œλ‹€. 

2. DetailViewController

  • DetailViewController μ—μ„œλ„ λ™μΌν•œ ViewModel 을 μ΄μš©ν–ˆμŠ΅λ‹ˆλ‹€.
  • ListVC μ—μ„œ TableView 의 셀이 ν΄λ¦­λ˜μ—ˆμ„ λ•Œ, μ„ νƒλœ model μ„ 전달받아 ViewModel λ‚΄μ— selectedMember λ³€μˆ˜μ˜ κ°’μœΌλ‘œ ν• λ‹Ήν•©λ‹ˆλ‹€! 
  • DetailVC μ—μ„œλŠ” viewModel.selectedMember λ₯Ό μ΄μš©ν•΄ UI λ₯Ό μ—…λ°μ΄νŠΈν•©λ‹ˆλ‹€.
  • String -> UILabel.text 둜의 λ³€ν™˜μ€ κ°„λ‹¨ν•˜μ§€λ§Œ, avatar μ΄λ―Έμ§€μ˜ 경우 url 을 톡해 μ΄λ―Έμ§€λ‘œ λ³€ν™˜ν•΄μ€˜μ•Όν•©λ‹ˆλ‹€.

List View > Detail View

 

πŸ’‘ DetailVC μ—μ„œ 이미지 λ‹€μš΄λ‘œλ“œ μž‘μ—…

Observable<Data> -- (bind) --> avatarImage.rx.image

JSON data λ₯Ό λ‹€μš΄λ°›μ„ λ•Œ μ΄μš©ν•œ APIManger.fetchData(url:) λ©”μ†Œλ“œλ₯Ό 톡해, 이미지λ₯Ό λΉ„λ™κΈ°λ‘œ λ‹€μš΄λ‘œλ“œ ν•˜λŠ” μž‘μ—…λ„ μˆ˜ν–‰ν•΄μ€λ‹ˆλ‹€. λ©”μ†Œλ“œλ₯Ό 톡해 전달받은 Observable 에 λŒ€ν•΄ subscribe (bind) λ₯Ό ν•΄μ£Όμ–΄ 이미지λ₯Ό λ³€κ²½ν•΄μ£ΌλŠ” κ²ƒμž…λ‹ˆλ‹€!

class MemberDetailViewController: UIViewController {
	
    // UILabels ... 
    @IBOutlet weak var avatarImage: UIImageView!
    
    let viewModel = MemberViewModel.shared
    var disposeBag = DisposeBag()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // UILabel.text 처리
        setImage(url: member.avatar) // binding
    }

    func setImage(url: String) {
        let orginalImageURL = url.replacingOccurrences(of: "size=50x50&", with: "")
        // Observable<Data> -> UIImage(data: )
        APIManager.fetchData(url: orginalImageURL)
            .observe(on: MainScheduler.instance)
            .map { UIImage(data: $0)! }
            .bind(to: avatarImage.rx.image)
            .disposed(by: disposeBag)
    }
}

 

3. MemberCell 

  • Cell μ—μ„œ UI 와 κ΄€λ ¨λœ, 이미지 λ‹€μš΄λ‘œλ“œ μž‘μ—…λ“±μ€ DetailVC 와 λΉ„μŠ·ν•˜μ§€λ§Œ, Cell 은 μž¬μ‚¬μš©λ˜λŠ” νŠΉμ„±μ΄ 있기 λ•Œλ¬Έμ— 이λ₯Ό κ³ λ €ν•΄μ£Όμ–΄μ•Όν•©λ‹ˆλ‹€!
  • TableView μ—μ„œ Cell 은 ν•œλ²ˆμ— λ³΄μ—¬μ§€λŠ” 개수만큼 init 되고, 슀크둀 될 λ•Œμ—λŠ” 이전에 μƒμ„±λλ˜ Cell 이 μž¬μ‚¬μš©λ©λ‹ˆλ‹€. κ·Έλ•Œλ§ˆλ‹€ ν˜ΈμΆœλ˜λŠ” λ©”μ†Œλ“œκ°€ prepareForReuse() 인 κ²ƒμ΄κ΅¬μš”!
  • cell λ§ˆλ‹€ 이미지λ₯Ό λ‹€μš΄λ‘œλ“œ λ°›κ³ , 이λ₯Ό disposeBag 에 λ‹΄κ³ μžˆλŠ” μž‘μ—…μ€ DetailVC 와 λ™μΌν•œλ°μš”. cell 이 μž¬μ‚¬μš©λ λ•Œλ§ˆλ‹€ DisposeBag 을 μž¬ν• λ‹Ήν•΄μ£Όμ–΄ disposables 을 λ²„λ €μ£ΌλŠ” μž‘μ—…μ„ ν•΄μ€λ‹ˆλ‹€. λ§Œμ•½ DisposeBag 을 μž¬ν• λ‹Ήν•˜μ§€ μ•ŠλŠ”λ‹€λ©΄, μž¬μ‚¬μš©λ˜λŠ” μ…€λ“€μ—μ„œ subscribe(bind) 이후 λ§Œλ“€μ–΄μ§„ disposables λ₯Ό μ „λΆ€ λ‹€ κ°€μ§€κ³ μžˆκ²Œ λ˜μ–΄ λ©”λͺ¨λ¦¬μ— λ‚¨μ•„μžˆμ„κ²λ‹ˆλ‹€ 😡‍πŸ’«
  • 그리고, cell 이 μž¬μ‚¬μš©λ˜κΈ° λ•Œλ¬Έμ— image κ°€ λ‹€μš΄λ‘œλ“œλ˜κΈ° μ „μ—λŠ” 이전 cell 의 image λ₯Ό λ³΄μ—¬μ€λ‹ˆλ‹€. 이λ₯Ό λ°©μ§€ν•˜κΈ° μœ„ν•΄μ„œ image = nil 으둜 ν• λ‹Ήν–ˆμŠ΅λ‹ˆλ‹€! (μ•„λž˜ 이미지 μ°Έκ³ )
class MemberCell: UITableViewCell {
    
    // IBOutlets
    var disposeBag = DisposeBag()

    override func prepareForReuse() {
        super.prepareForReuse()
        // cell reuse μ‹œμ— μ΄ˆκΈ°ν™”λ˜λ„λ‘ μž‘μ—…ν•˜κΈ°
        avatarImage.image = nil
        disposeBag = DisposeBag()
    }
    
    // Other methods ...
}

이미지 λ‹€μš΄λ‘œλ”© μ „μ—λŠ” nil > λ‹€μš΄λ‘œλ“œ ν›„ image ! 


 

 

 

songda515/MVVM_RxSwift

MVVM νŒ¨ν„΄κ³Ό RxSwift λ₯Ό ν•™μŠ΅ν•˜κ³ , 이λ₯Ό μ μš©ν•œ ν”„λ‘œμ νŠΈλ₯Ό κ°œλ°œν•˜κΈ° μœ„ν•œ μŠ€ν„°λ””λͺ¨μž„μž…λ‹ˆλ‹€. - songda515/MVVM_RxSwift

github.com

참고 링크

 

πŸ‘‡ ν˜Ήμ‹œ λΆ€μ‘±ν•˜κ±°λ‚˜ 잘λͺ»λœ λ‚΄μš©μ΄ 있으면 λŒ“κΈ€λ‘œ μ•Œλ €μ£Όμ„Έμš” 🧐

728x90
728x90