본문 바로가기

iOS 개발/코로나검사소 앱(RxSwift)

코로나 검사소 - 1. 지역 선택 화면 구현하기

반응형

 

안녕하세요. Skillist입니다

오늘은 지역 선택 화면을 구현해볼게요

레이아웃은 navigationBar와 CollectionView로 간단하게 구현했습니다.

이미 복잡한 뷰는 저의 다른 프로젝트에서 진행했고, 이번에는 RxSwift에 집중했어요.

 

그럼 시작합니다!

 

———————————————————————————————————————————————————

 

우선 View와 레이아웃부터 구현합니다!

RxSwift관련 코드는 나~~중에 보시죠. 익숙한 코드부터 보죠.

 

14라인 : disposeBag입니다.

 

17라인 : 이전 프로젝트부터 함께 개발했던 collectionView입니다.

 

32라인 : 이니셜라이저입니다.

 

75라인 : view에 대한 설정입니다. 간단한 레이아웃이라 설정할게 별로 없었어요. title만 설정했습니다.

 

80라인 : collectionView를 추가하고, snapKit으로 제약사항 구성했습니다.

 

91라인 : cell Size를 설정합니다.

 

아직 RxSwift 코드는 안보이죠?

 

———————————————————————————————————————————————————

 

이번엔 CollectionView에서 사용할 Cell을 구성해봅니다.

다음 레이아웃을 구성할거에요.

11라인 : titleLabel입니다.

 

19라인 : 지역 검사소 count Label입니다.

 

30라인 : subView에 추가합니다.

 

37, 43라인 : label에 대한 제약사항을 snapKit으로 구성합니다.

 

58라인 : data를 설정합니다.

 

———————————————————————————————————————————————————

 

viewModel 을 구현해보겠습니다.

 

이제부터 RxSwift, RxCocoa를 사용합니다.

 

17라인 : collectionView에서 사용할 cellData입니다.

 

18라인 : errorMessage인 signal<String> 입니다.

 

20라인 : 이니셜라이저입니다. 

 

22라인 : "CenterNetwork().getCenters()"는 네트워크 통신 결과를 리턴합니다. 이를 옵저버블로 방출합니다.

 

26라인 : share()를 통해 하나의 시퀀스를 공유합니다. 여러개의 시퀀스 생성이 아닌 하나의 시퀀스를 공유하는거죠.

share를 통해 하나의 시퀀스로, 통신 성공(29라인)과 에러 발생(50라인) 처리를 하고 있습니다.

 

29라인 : 네트워크 통신 성공에 대한 처리입니다.

30라인 : 에러일때 nil을 반환하여 성공일때만 필터링을 합니다.

37라인 : sido라는 데이터로 그룹핑합니다.

40라인 : foreach를 통해 딕셔너리를 어레이로 변환했습니다.

 

50라인 : 네트워크 에러에 대한 처리입니다.

51라인 : 성공일때 nil을 반환하여 에러일때만 필터링합니다.

 

59라인 : 성공일때도 error를 발생하는 코드입니다.

 

———————————————————————————————————————————————————

 

그럼 다시 SelectRegionView로 돌아와서 viewModel 바인드 해볼게요.

46라인 : viewModel의 cellData로 collectionView를 구성합니다. RxSwift로 collectionView, tableView를 구성하는 방법이 따로 존재합니다. 

 

61라인 : collectionView에서 cell 터치시 model을 반환하는 rx.modelSelected를 사용했습니다.

cell 터치시 cell에 대한 data를 받아, 코로나 검사소 선택 화면으로 전달하고 이동합니다.

 

71라인 : viewModel의 errorMessage가 전달되면, alert을 보여줍니다. "self.rx.showErrorAlert"는 제가 Reactive를 확장하여 따로 구현했습니다.

 

104라인 : String을 받는 Binder입니다. 간단하게, errorMessage를 전달하고, Alert을 보여주도록 구현했습니다.

 

 

———————————————————————————————————————————————————

 

이렇게 지역 선택 화면을 구현해봤습니다.

코드로 구현하다가, 글로 설명하려니, 용어들이나, 상세 설명이 무척 어렵네요;;;;;;;;;

많이 미흡한데, 더 나아질수 있도록 노력하겠습니다.

 

———————————————————————————————————————————————————

 

잘못되거나 부족한 내용 등, 피드백 감사합니다!

 

https://github.com/DeveloperSkillist/CoronaCenterRxSwift

 

GitHub - DeveloperSkillist/CoronaCenterRxSwift

Contribute to DeveloperSkillist/CoronaCenterRxSwift development by creating an account on GitHub.

github.com

 

 

↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓  전체 코드  ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

 

import UIKit
import RxSwift
import RxCocoa
import SnapKit

class SelectRegionView: UIViewController {
    var disposeBag = DisposeBag()
    
    //지역을 보여줄 collectionView
    private lazy var collectionView: UICollectionView = {
        //collectionView lise, inset 설정
        let marginSize: CGFloat = 10
        let layout = UICollectionViewFlowLayout()
        layout.minimumLineSpacing = marginSize
        layout.minimumInteritemSpacing = marginSize
        layout.sectionInset = UIEdgeInsets(top: marginSize, left: marginSize, bottom: marginSize, right: marginSize)
        
        //collectionView 설정
        var collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
        collectionView.delegate = self
        collectionView.register(SelectRegionCollectionViewCell.self, forCellWithReuseIdentifier: "SelectRegionCollectionViewCell")
        return collectionView
    }()
    
    override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
        super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
        
        attribute()
        layout()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    //view와 viewModel 바인드
    func bind(_ viewModel: SelectRegionViewModel) {
        //지역들과 collectionView 바인드
        viewModel.cellData
            .drive(collectionView.rx.items) { cv, row, data in
                guard let cell = cv.dequeueReusableCell(
                    withReuseIdentifier: "SelectRegionCollectionViewCell",
                    for: IndexPath(row: row, section: 0)) as? SelectRegionCollectionViewCell else {
                    return UICollectionViewCell()
                }
                
                //cell data 설정
                cell.setupData(title: data.first?.sido.rawValue ?? "-", number: String("(\(data.count))"))
                return cell
            }
            .disposed(by: disposeBag)
        
        //cell 선택 시, 선택한 cell의 model을 리턴합니다.
        collectionView.rx.modelSelected([Center].self)
            .bind(onNext: {
                //cell 선택하여 지역 선택 화면으로 이동합니다.
                let selectCenterView = SelectCenterView()
                selectCenterView.bind(SelectCenterViewModel(centers: $0))
                self.navigationController?.pushViewController(selectCenterView, animated: true)
            })
            .disposed(by: disposeBag)
        
        //에러 메세지 발생 시 alert show
        viewModel.errorMessage
            .emit(to: self.rx.showErrorAlert)
            .disposed(by: disposeBag)
    }
    
    //view에 대한 속성 설정
    private func attribute() {
        navigationItem.title = "지역"
    }
    
    //view 레이아웃 설정
    private func layout() {
        view.addSubview(collectionView)
        
        collectionView.snp.makeConstraints {
            $0.edges.equalToSuperview()
        }
    }
}

extension SelectRegionView: UICollectionViewDelegateFlowLayout {
    
    //cell size 설정
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        return CGSize(
            width: ((collectionView.frame.width - (10 * 2))/2) - 5,
            height: 100
        )
    }
}

extension Reactive where Base: SelectRegionView {
    //alert show
    var showErrorAlert: Binder<String> {
        return Binder(base) { base, message in
            let alert = UIAlertController(title: "error", message: message, preferredStyle: .alert)
            let okAction = UIAlertAction(title: "확인", style: .default, handler: nil)
            alert.addAction(okAction)
            base.present(alert, animated: true, completion: nil)
        }
    }
}
import Foundation
import RxSwift
import RxCocoa

struct SelectRegionViewModel {
    //view -> viewModel
    //없습니다.
    
    //viewModel -> view
    var cellData: Driver<[[Center]]>    //지역별로 그룹핑한 [[Center]]을 받습니다.
    var errorMessage: Signal<String>
    
    init() {
        //네트워크 통신으로 데이터 가져오기
        let centersDataResult = Observable.just(CenterNetwork().getCenters())
            .flatMapLatest {
                $0
            }
            .share()
        
        //데이터 성공인 경우 목록 전달
        cellData = centersDataResult
            .compactMap { data -> CenterAPIResponse? in
                guard case .success(let value) = data else {
                    return nil
                }
                return value    //CenterAPIResponse? 리턴
            }
            .map { $0.data }    //data만 리턴
            .map { centers in
                Dictionary(grouping: centers) { $0.sido }   //sido를 기준으로 그룹핑
            }
            .map {  //딕셔너리를 어레이로 변경
                var groupedCenter: [[Center]] = []
                $0.forEach {
                    groupedCenter.append($0.value)
                }
                return groupedCenter
            }
            .asDriver(onErrorJustReturn: [[]])
        
        //에러 발생 시 error message
        errorMessage = centersDataResult
            .compactMap { data -> String? in
                guard case .failure(_) = data else {
                    return nil
                }
                return "네트워크 에러가 발생했습니다.\n잠시 후 시도 해주세요."
            }
            .asSignal(onErrorJustReturn: "잠시 후 시도 해주세요.")
        
        //통신 성공 시에도, error message 강제 발생
//        errorMessage = centersDataResult
//            .compactMap { data -> String? in
//                guard case .success(_) = data else {
//                    return nil
//                }
//                return "네트워크 에러가 발생했습니다.\n잠시 후 시도 해주세요."
//            }
//            .asSignal(onErrorJustReturn: "잠시 후 시도 해주세요.")
    }
}
import UIKit

class SelectRegionCollectionViewCell: UICollectionViewCell {
    private lazy var titleLabel: UILabel = {
        var label = UILabel()
        label.textColor = .purple
        label.font = .systemFont(ofSize: 25)
        label.textAlignment = .center
        return label
    }()
    
    private lazy var numberLabel: UILabel = {
        var label = UILabel()
        label.textColor = .darkGray
        label.font = .systemFont(ofSize: 20)
        label.textAlignment = .center
        return label
    }()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        
        [
            titleLabel,
            numberLabel
        ].forEach {
            addSubview($0)
        }
        
        titleLabel.snp.makeConstraints {
            $0.top.equalToSuperview().inset(15)
            $0.leading.trailing.equalToSuperview().inset(15)
            $0.centerX.equalToSuperview()
        }
        
        numberLabel.snp.makeConstraints {
            $0.leading.trailing.equalToSuperview().inset(15)
            $0.bottom.equalToSuperview().inset(15)
            $0.centerX.equalToSuperview()
        }
        
        self.backgroundColor = .lightGray
        self.layer.cornerRadius = 10
        self.clipsToBounds = true
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func setupData(title: String, number: String) {
        titleLabel.text = title
        numberLabel.text = number
    }
}
반응형