본문 바로가기

Swift 공부

MVVM RxSwift , Combine

지난글에 이어 

https://coding-rengar.tistory.com/23

 

Swift MVVM

1. MVVM 1. 개요 Model-View-ViewModel(MVVM) 패턴은 UI 디자인 패턴입니다. 그것은 MV라고 알려진 더 큰 패턴 모음중에 하나이며, Model View Controller(MVC), Model View Present(MVP), 그외 여러가지가 포함됩..

coding-rengar.tistory.com

 

 

오늘도 MVVM을 공부를 진행해볼려고한다. 

 

지난번에는 MVVM을 이용해 코드를 만들고 Bindging객체도 직접 만들어서 사용했다 

 

이번에는 코드를 개선하는데 Bindging 객체를 직접 구현하지않고 사람들이 많이 사용하는 RxSwift 

 

그리고 IOS13에서 나온 Combine 을 이용하여 구현을 진행해보겠다.

 

우선 RxSwift로 시작을 해보겠다.

 

RxSwift를 모르시는분들이 있을수도 있기때문에 추후 간단히 정리하여 글을 작성하겠다.

 

우선 RxSwift 와 관련된 라이브러리들을 설치를 진행해줘야한다.

 

Pod 가 깔려있지 않다 면 우선 Pod init 을 실행해 Podfile 을 셋팅해주고

 

  # Rx
  pod 'RxSwift'
  pod 'RxCocoa'

를 입력후 Pod install 을 진행해준다.

 

 

그럼 우선 ViewModel의 코드 부터 변경을 진행해볼려고한다.

 

지난번

import Foundation

class ViewModel {
    
    private var members : [MVVM_GFriendModel] = []
    
    var selectedMemeber : Observable<MVVM_GFriendModel?> = Observable(nil)
    
    private var index : Int = 0
    
    init(){
        self.members.append(MVVM_GFriendModel(name: "소원", imageName: "소원", memberType: "서브보컬 , 서브댄서 , 쏘리다", fancamURL: "https://www.youtube.com/watch?v=PeCsIDzTsb0"))
        self.members.append(MVVM_GFriendModel(name: "예린", imageName: "예린", memberType: "서브보컬 , 서브댄서 , 옌니", fancamURL: "https://www.youtube.com/watch?v=gw92213ow9s"))
        self.members.append(MVVM_GFriendModel(name: "은하", imageName: "은하", memberType: "서브보컬 , 최애캐 , 으나 , 짜냥이", fancamURL: "https://www.youtube.com/watch?v=G_-SNiuZrJA"))
        self.members.append(MVVM_GFriendModel(name: "유주", imageName: "유주", memberType: "메인보컬 , 가창력갑 , 바리스타 유주", fancamURL: "https://www.youtube.com/watch?v=Rk9nbLFm_uo"))
        self.members.append(MVVM_GFriendModel(name: "신비", imageName: "신비", memberType: "메인댄서 , 춤신춤왕 , 막내", fancamURL: "https://www.youtube.com/watch?v=L9U3hcCSKIA"))
        self.members.append(MVVM_GFriendModel(name: "엄지", imageName: "엄지", memberType: "서브댄서 , 서브보컬 , 막내", fancamURL: "https://www.youtube.com/watch?v=Rrm62tCrNVw"))
        
        self.selectedMemeber.value = members[index]
        
    
        
        
        
    }
    
    
    
    func tapButton(isPrevious : Bool) {
        if isPrevious {
            index = index > 0 ? index-1 : members.count - 1
        }else {
            index = index < members.count - 1 ? index + 1 : 0
        }
        self.selectedMemeber.value = members[index]
        
    }
    
    
}

코드에서

 

import Foundation

class ViewModel {
    
    private var members : [MVVM_GFriendModel] = []
    
    //var selectedMemeber : Observable<MVVM_GFriendModel?> = Observable(nil)
     var selectedMemeber = PublishSubject<MVVM_GFriendModel>()
    private var index : Int = 0
    
    init(){
        self.members.append(MVVM_GFriendModel(name: "소원", imageName: "소원", memberType: "서브보컬 , 서브댄서 , 쏘리다", fancamURL: "https://www.youtube.com/watch?v=PeCsIDzTsb0"))
        self.members.append(MVVM_GFriendModel(name: "예린", imageName: "예린", memberType: "서브보컬 , 서브댄서 , 옌니", fancamURL: "https://www.youtube.com/watch?v=gw92213ow9s"))
        self.members.append(MVVM_GFriendModel(name: "은하", imageName: "은하", memberType: "서브보컬 , 최애캐 , 으나 , 짜냥이", fancamURL: "https://www.youtube.com/watch?v=G_-SNiuZrJA"))
        self.members.append(MVVM_GFriendModel(name: "유주", imageName: "유주", memberType: "메인보컬 , 가창력갑 , 바리스타 유주", fancamURL: "https://www.youtube.com/watch?v=Rk9nbLFm_uo"))
        self.members.append(MVVM_GFriendModel(name: "신비", imageName: "신비", memberType: "메인댄서 , 춤신춤왕 , 막내", fancamURL: "https://www.youtube.com/watch?v=L9U3hcCSKIA"))
        self.members.append(MVVM_GFriendModel(name: "엄지", imageName: "엄지", memberType: "서브댄서 , 서브보컬 , 막내", fancamURL: "https://www.youtube.com/watch?v=Rrm62tCrNVw"))
        
        //self.selectedMemeber.value = members[index]
        self.selectedMemeber.onNext(members[index])
    
        
        
        
    }
    
    
    
    func tapButton(isPrevious : Bool) {
        if isPrevious {
            index = index > 0 ? index-1 : members.count - 1
        }else {
            index = index < members.count - 1 ? index + 1 : 0
        }
        //self.selectedMemeber.value = members[index]
        self.selectedMemeber.onNext(members[index])
    }
    
    
}

딱 3줄을 바꾸면 저희는 RxSwift와 MVVM을 이용해 ViewModel을 만들었습니다 짝짝짝 퍽

뭐... 만든건 맞긴한데.. RxSwift를 쓴거긴한데.. 차암 가볍게썼습니다.

 

그래도 여기서 주요하게 보실건 저희가 기존 Observable의 객체로 만들었던것을 PublishSubject로 변경해서 만들고

기존에 값을 직접 주입을 시켜주었다면 지금은 onNext라는 함수를 이용해 값을 넣어주는 것을 확일 할 수 있습니다.

해당 부분은 RxSwift의 글에서 좀 더 자세히 다뤄보기로 하고 지금은 RxSwift라는 라이브러리를

이용해 데이터를 바인딩시킨다 정도만 알면 될 것 같습니다.

 

그럼 바인딩을 받은 객체의 변화를 감지하여 사용하는쪽의 코드도 봐야하겠죠?

 

ViewController 로 넘어가서 확인해 보겠습니다. 

 

기존코드

import UIKit

class ViewController: UIViewController {

    let viewModel = ViewModel()
    @IBOutlet weak var NameLabel: UILabel!
    @IBOutlet weak var MainImageView: UIImageView!
    
    @IBOutlet weak var TypeLabel: UILabel!
    @IBOutlet weak var FancamURL: UILabel!
    @IBOutlet weak var PreviousButton: UIButton!
    @IBOutlet weak var NextButton: UIButton!
    override func viewDidLoad() {
        super.viewDidLoad()
        ViewModelBinding()
        PreviousButton.addTarget(self, action: #selector(previousButtonAction), for: .touchUpInside)
        NextButton.addTarget(self, action: #selector(nextButtonAction), for: .touchUpInside)
        // Do any additional setup after loading the view.
    }

    func ViewModelBinding(){
        viewModel.selectedMemeber.bind{ member in
            self.NameLabel.text = member?.name
            self.MainImageView.image = UIImage(named: member?.imageName ?? "")
            self.TypeLabel.text = member?.memberType
        }
    }
    @objc func previousButtonAction(){
        viewModel.tapButton(isPrevious: true)
    }

    @objc func nextButtonAction(){
        viewModel.tapButton(isPrevious: false)
    }
}

 

기존 ViewModelBinding함수에서 .bind를 이용해 값을 변경하였는데

//
//  ViewController.swift
//  MVVMTestApp
//
//  Created by 강지윤 on 2022/03/09.
//

import UIKit
import RxCocoa
import RxSwift

class ViewController: UIViewController {

    let viewModel = ViewModel()
    @IBOutlet weak var NameLabel: UILabel!
    @IBOutlet weak var MainImageView: UIImageView!
    
    @IBOutlet weak var TypeLabel: UILabel!
    @IBOutlet weak var FancamURL: UILabel!
    @IBOutlet weak var PreviousButton: UIButton!
    @IBOutlet weak var NextButton: UIButton!
    override func viewDidLoad() {
        super.viewDidLoad()
        ViewModelBinding()
        PreviousButton.addTarget(self, action: #selector(previousButtonAction), for: .touchUpInside)
        NextButton.addTarget(self, action: #selector(nextButtonAction), for: .touchUpInside)
        // Do any additional setup after loading the view.
    }

    func ViewModelBinding(){
//        viewModel.selectedMemeber.bind{ member in
//            self.NameLabel.text = member?.name
//            self.MainImageView.image = UIImage(named: member?.imageName ?? "")
//            self.TypeLabel.text = member?.memberType
//        }
        
       viewModel.selectedMemeber
            .subscribe(onNext: { member in
                self.NameLabel.text = member.name
                self.MainImageView.image = UIImage(named: member.imageName)
                self.TypeLabel.text = member.memberType
            })
            .disposed(by: DisposeBag())
        
    }
    @objc func previousButtonAction(){
        viewModel.tapButton(isPrevious: true)
    }

    @objc func nextButtonAction(){
        viewModel.tapButton(isPrevious: false)
    }
}

이렇게 RxSwift 를 이용하면  우리가 직접 만들었던 bind함수가 아닌

RxSwift의 ObservableType객체 내장된 함수인 Subscribe의 함수를 통해 

이벤트를 구독하여 데이터를 변경해주는 것 을 확인 할 수 있습니다.

RxSwift는 이렇게 간단? 하지 않습니다. 지금은 진짜 가벼운 예시를 들기위해 정말 가볍게 사용하였고 혹시라도 이글을 보고 Rx 별거 아닌데 라는 생각은... 저도 RxSwift를 입문한지 얼마 되지 않아 매일 공부를 진행하고 실제로 적용하면서 많이 어려운 부분들이 많아 해매곤 합니다 쉽게 보시면 안됩니다.

 

이렇게 상당히 간단하게? RxSwift를 이용해서 MVVM의 코드를 완성해 봤습니다.

 

이번에는 Combine을 이용해서 구현을 해보겠습니다.

 


import Foundation
import Combine

class ViewModel {
    private var members : [MVVM_GFriendModel] = []
    @Published var selectedMemeber : MVVM_GFriendModel? = nil
    private var index : Int = 0
    
    init(){
        self.members.append(MVVM_GFriendModel(name: "소원", imageName: "소원", memberType: "서브보컬 , 서브댄서 , 쏘리다", fancamURL: "https://www.youtube.com/watch?v=PeCsIDzTsb0"))
        self.members.append(MVVM_GFriendModel(name: "예린", imageName: "예린", memberType: "서브보컬 , 서브댄서 , 옌니", fancamURL: "https://www.youtube.com/watch?v=gw92213ow9s"))
        self.members.append(MVVM_GFriendModel(name: "은하", imageName: "은하", memberType: "서브보컬 , 최애캐 , 으나 , 짜냥이", fancamURL: "https://www.youtube.com/watch?v=G_-SNiuZrJA"))
        self.members.append(MVVM_GFriendModel(name: "유주", imageName: "유주", memberType: "메인보컬 , 가창력갑 , 바리스타 유주", fancamURL: "https://www.youtube.com/watch?v=Rk9nbLFm_uo"))
        self.members.append(MVVM_GFriendModel(name: "신비", imageName: "신비", memberType: "메인댄서 , 춤신춤왕 , 막내", fancamURL: "https://www.youtube.com/watch?v=L9U3hcCSKIA"))
        self.members.append(MVVM_GFriendModel(name: "엄지", imageName: "엄지", memberType: "서브댄서 , 서브보컬 , 막내", fancamURL: "https://www.youtube.com/watch?v=Rrm62tCrNVw"))
        
        self.selectedMemeber = members[index]
        
    
        
        
        
    }
    
    
    
    func tapButton(isPrevious : Bool) {
        if isPrevious {
            index = index > 0 ? index-1 : members.count - 1
        }else {
            index = index < members.count - 1 ? index + 1 : 0
        }
        self.selectedMemeber = members[index]
        
    }
    
    
}

이쪽은 정말 별게 없습니다 @Published 를 변수 앞에 써주고  Observable 을 삭제해줍니다.

selectedMember 뒤의 value를 삭제해주고여

 

import UIKit
import Combine

class ViewController: UIViewController {

    let viewModel = ViewModel()
    @IBOutlet weak var NameLabel: UILabel!
    @IBOutlet weak var MainImageView: UIImageView!
    
    @IBOutlet weak var TypeLabel: UILabel!
    @IBOutlet weak var FancamURL: UILabel!
    @IBOutlet weak var PreviousButton: UIButton!
    @IBOutlet weak var NextButton: UIButton!
    
    var binding = Set<AnyCancellable>()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        ViewModelBinding()
        PreviousButton.addTarget(self, action: #selector(previousButtonAction), for: .touchUpInside)
        NextButton.addTarget(self, action: #selector(nextButtonAction), for: .touchUpInside)
        // Do any additional setup after loading the view.
    }

    func ViewModelBinding(){
        viewModel.$selectedMemeber
            .sink { member in
                self.NameLabel.text = member?.name
                self.MainImageView.image = UIImage(named: member?.imageName ?? "" )
                self.TypeLabel.text = member?.memberType
            }.store(in: &binding)
        
    }
    @objc func previousButtonAction(){
        viewModel.tapButton(isPrevious: true)
    }

    @objc func nextButtonAction(){
        viewModel.tapButton(isPrevious: false)
    }
}

 

여기는 subscribe가 아닌 sink 라는 값으로 변경해주고 나머지는 비슷 한 구조 입니다 &binding 으로 적은게 RxSwift의 

DisposeBag과 같은 역활을 한다고 보시면 됩니다.

 

 

이렇게 간단하게 RxSwift , Combine을 이용해 MVVM을 만들었는데 다음에 Combine + Uikit 이 RxSwift + Uikit 보다 조합이 별로인지 간단하게 예제로 만들어보겠습니다.

'Swift 공부' 카테고리의 다른 글

TableView Reusable  (0) 2022.03.25
GCD - (1)  (0) 2022.03.14
Swift MVVM  (0) 2022.03.09
Protocol Oriented Programming(POP) - With RxSwift Mvvm  (0) 2022.02.18
WKWebView MemoryLeak 해결  (0) 2021.11.12