🍎 iOS/RxSwift

[RxSwift] RxSwift + MVVM + Moya 적용 (feat. tableview binding)

dev_zoe 2023. 1. 19. 21:28
반응형

Podfile

  # Moya with rx
  pod 'Moya/RxSwift', '~> 15.0'
  # RxSwift & Cocoa
  pod 'RxSwift', '~> 6.5.0'
  pod 'RxCocoa', '~> 6.5.0'
  # SwiftyJson
  pod 'SwiftyJSON', '~> 4.0'

 

1. 엮을 API의 구조

header

x-access-token : jwt 토큰

response

{
    "isSuccess": true,
    "code": 1000,
    "message": "성공",
  "result" : [
    {
      "profileImageUrl" : "이미지 링크",
      "roomId" : 255,
      "title" : "test",
      "repUserName" : "Runnber6421",
      "recentMessage" : "N"
    },
    {
      "profileImageUrl" : "이미지 링크",
      "roomId" : 256,
      "title" : "테스트",
      "repUserName" : "Runnber6421",
      "recentMessage" : "N"
    }
  ],
}

 

2. MessageAPI - API 통신에 필요한 Moya TargetType를 준수하는 enum 정의

import Foundation
import Moya

enum MessageAPI {
    case getMessageList(token: LoginToken) //사용자의 토큰정보가 담긴 구조체
}

enum APIResult<T> {
    case response(result: T)
    case error(alertMessage: String?)
}

extension MessageAPI: TargetType {
    var baseURL: URL { 
        return "API url 주소"
    }

    var path: String { //API 주소 적는곳
        switch self {
        case let .getMessageList:
            return "/message"
        }
    }

    var method: Moya.Method { // http method 정의
        switch self {
        case .getMessageList:
            return Method.get
        }
    }

    var task: Task {
        switch self {
        case .getMessageList: // request를 어떻게 요청할 것인가?
            return .requestPlain
        }
    }

    var header: [String: String]? {
  		var header: [String : String]? { ["Content-Type": "application/json"] }

        switch self {
        case let .getMessageList(token):
            header["x-access-token"] = "\(token.jwt)"
        }
        return header
    }
}

 

3. MessageAPIService - API 통신하여 observable response를 받아오는 클래스

class MessageAPIService {

    let provider: MoyaProvider<MessageAPI>
    let loginKeyChain: LoginKeyChainService

    init(provider: MoyaProvider<MessageAPI> = .init(), loginKeyChainService: LoginKeyChainService = BasicLoginKeyChainService.shared) {
        loginKeyChain = loginKeyChainService
        self.provider = provider
    }

    func getMessageList() -> Observable<APIResult<[MessageListItem]?>> {
        guard let token = loginKeyChain.token
        else {
            return .just(.error(alertMessage: nil))
        }

        return provider.rx.request(.getMessageList(token: token))
            .asObservable() // return type을 observable로
            .map { response in
            	try? JSON(data: response.data)
        	}
            .map { (try? $0?.json["result"].rawData()) ?? Data() } //result에 해당하는 raw Json Data가 필요함
            .decode(type: [MessageListItem]?.self, decoder: JSONDecoder()) //[MessageListItem]으로 디코딩
            .catch { error in
                Log.e("\(error)")
                return .just(nil)
            }
            .map { APIResult.response(result: $0 ?? []) } //APIResult에 response 담아서 매핑
            .catchAndReturn(.error(alertMessage: "네트워크 연결을 확인해 주세요"))
    }

 

4. MessageListItem - API 통신의 result를 감쌀 구조체

struct MessageListItem: Decodable {
    let roomId: Int?
    let title, repUserName: String?
    let profileImageUrl: String?
    let recentMessage: String?

    enum CodingKeys: String, CodingKey {
        case roomId
        case title, repUserName
        case profileImageUrl
        case recentMessage
    }
}

 

5. MessageViewModel

import Foundation
import RxSwift

class MessageViewModel: BaseViewModel {
    var messages: [MessageListItem] = []

    init(messageAPIService: MessageAPIService = MessageAPIService()) {
        super.init()

        routeInputs.needUpdate //routeInputs.needUpdate 값이 observe되면
            .flatMap { _ in
                messageAPIService.getMessageList() //API 호출하여 해당 결과값을 observable로 보냄
            }
            .map { [weak self] result -> [MessageListItem]? in
                switch result {
                case let .response(result: data):
                    return data
                case let .error(alertMessage):
                    if let alertMessage = alertMessage {
                        self?.toast.onNext(alertMessage)
                    }
                    return nil
                }
            }
            .subscribe(onNext: { [weak self] result in
                guard let self = self else { return }
                self.messages.removeAll()

                if let result = result {
                    self.messages = result

                    if !self.messages.isEmpty {
                        self.outputs.messageLists.onNext(self.messages)
                    } else {
                        self.outputs.messageLists.onNext([])
                    }
                }

            })
            .disposed(by: disposeBag)
    }

    struct Input { // View에서 ViewModel로 전달되는 이벤트 정의
    	생략
    }

    struct Output { // ViewModel에서 View로의 데이터 전달이 정의되어있는 구조체
        var messageLists = ReplaySubject<[MessageListItem]>.create(bufferSize: 1)
    }

    struct Route { // 화면 전환이 필요한 경우 해당 이벤트를 Coordinator에 전달하는 구조체
		생략
    }

    struct RouteInput { // 자식화면이 해제되면서 전달되어야하느 정보가 있을 경우, 전달되어야할 이벤트가 정의되어있는 구조체
        var needUpdate = PublishSubject<Bool>()
    }

    private var disposeBag = DisposeBag()
    var inputs = Input()
    var outputs = Output()
    var routes = Route()
    var routeInputs = RouteInput()
}

 

6. MessageViewController - 테이블뷰를 뿌려줄 ViewController

import Kingfisher
import RxCocoa
import RxGesture
import RxSwift
import SnapKit
import Then
import UIKit

class MessageViewController: BaseViewController, UIScrollViewDelegate {
    let cellID = "MessageTableViewCell"

    override func viewDidLoad() {
        super.viewDidLoad()
        
        viewModelOutput()
        viewModel.routeInputs.needUpdate.onNext(true) //여기서 true값을 주게되면, ViewModel 안의 needUpdate가 동작하여 API를 호출함 
    }

    init(viewModel: MessageViewModel) {
        self.viewModel = viewModel
        super.init()
    }

    private var viewModel: MessageViewModel
    
    ...중략

    private func viewModelOutput() {
        tableView.rx.setDelegate(self).disposed(by: disposeBag)

        viewModel.outputs.messageLists
            .filter { [weak self] array in
                if array.isEmpty {
                    self!.tableView.isHidden = true
                    return false
                } else {
                    self!.tableView.isHidden = false
                    return true
                }
            }
            .bind(to: tableView.rx.items(cellIdentifier: cellID, cellType: MessageTableViewCell.self)) { _, item, cell in

                cell.selectionStyle = .none
//                반짝임 효과 제거
                if item.profileImageUrl != nil {
                    cell.messageProfile.kf.setImage(with: URL(string: item.profileImageUrl!), placeholder: Asset.profileEmptyIcon.uiImage)
                } else {
                    cell.messageProfile.image = Asset.profileEmptyIcon.uiImage
                }
                cell.postTitle.text = item.title
                cell.nameLabel.text = item.repUserName

                if item.recentMessage == "Y" { // 안읽은 메시지 여부 : 있음
                    cell.backgroundColor = .primaryBestDark
                } else {
                    cell.backgroundColor = .clear
                }
            }
            .disposed(by: disposeBag)

        viewModel.toast
            .subscribe(onNext: { message in
                AppContext.shared.makeToast(message)
            })
            .disposed(by: disposeBag)
    } // 뷰모델에서 뷰로 데이터가 전달되어 뷰의 변화가 반영되는 부분

    private var tableView = UITableView().then { view in
        view.separatorColor = .clear
        view.register(MessageTableViewCell.self, forCellReuseIdentifier: MessageTableViewCell.id) //셀 등록 
    }
}
... 후략

-> 추후에 RxDatasource를 사용해서 테이블뷰에 바인딩해보는것도 할 예정!

 

 

실행결과

 

반응형