안녕하세요. 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
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ 전체 코드 ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
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
}
}
'iOS 개발 > 코로나검사소 앱(RxSwift)' 카테고리의 다른 글
코로나 검사소 - 5. SceneDelegate (0) | 2022.01.22 |
---|---|
코로나 검사소 - 4. MapView (0) | 2022.01.22 |
코로나 검사소 - 3. 코로나 센터 선택 화면 (0) | 2022.01.21 |
코로나 검사소 - 2. 네트워크 통신 (0) | 2022.01.21 |
코로나 검사소 - 0. 프리뷰 (0) | 2022.01.21 |