diff --git a/CMC/Sources/Presenter/Auth/Coordinators/AuthCoordinator.swift b/CMC/Sources/Presenter/Auth/Coordinators/AuthCoordinator.swift index 5f87833..14a4a38 100644 --- a/CMC/Sources/Presenter/Auth/Coordinators/AuthCoordinator.swift +++ b/CMC/Sources/Presenter/Auth/Coordinators/AuthCoordinator.swift @@ -55,12 +55,12 @@ class AuthCoordinator: CoordinatorType { } case .signUp: let signUpViewController = SignUpViewController( -// viewModel: EmailSignUpViewModel( -// coordinator: self, -// userSignUpUsecase: DefaultUserSignUpUsecase( -// userRepository: DefaultUserRepository() -// ) -// ) + viewModel: SignUpViewModel( + coordinator: self, + authUsecase: DefaultAuthUsecase( + authRepository: DefaultAuthRepository() + ) + ) ) if self.navigationController.viewControllers.contains(where: {$0 is SignUpViewController}) { self.navigationController.popViewController(animated: true) diff --git a/CMC/Sources/Presenter/Auth/SignUp/ScrollPages/TermsAndConditions/TermsAndConditionsView.swift b/CMC/Sources/Presenter/Auth/SignUp/ScrollPages/TermsAndConditions/TermsAndConditionsView.swift index 084f9b0..7aa1b00 100644 --- a/CMC/Sources/Presenter/Auth/SignUp/ScrollPages/TermsAndConditions/TermsAndConditionsView.swift +++ b/CMC/Sources/Presenter/Auth/SignUp/ScrollPages/TermsAndConditions/TermsAndConditionsView.swift @@ -5,5 +5,266 @@ // Created by Siri on 10/28/23. // Copyright © 2023 com.centralMakeusChallenge. All rights reserved. // - import Foundation + +import RxCocoa +import RxGesture +import RxSwift + +import DesignSystem +import SnapKit + +import UIKit + +final class TermsAndConditionsView: BaseView { + // MARK: - UI + + private lazy var titleLabel: UILabel = { + let label = UILabel() + label.text = "약관동의" + label.font = DesignSystemFontFamily.Pretendard.bold.font(size: 26) + label.textColor = DesignSystemAsset.gray50.color + return label + }() + + private lazy var buttonStackViews: [UIStackView] = { + var stackViews: [UIStackView] = [] + buttons.enumerated().forEach { index, button in + let stackView = UIStackView() + stackView.axis = .horizontal + stackView.alignment = .center + stackView.addArrangedSubview(button) + stackView.addArrangedSubview(buttonLabels[index]) + stackViews.append(stackView) + } + return stackViews + }() + + private lazy var buttons: [CMCTouchArea] = { + let selectedImage = CMCAsset._24x24abled.image + let normalImage = CMCAsset._24x24disabled.image + let buttons = [ + CMCTouchArea(image: normalImage), + CMCTouchArea(image: normalImage), + CMCTouchArea(image: normalImage), + CMCTouchArea(image: normalImage), + CMCTouchArea(image: normalImage) + ] + buttons.forEach{ $0.setImage(selectedImage, for: .selected)} + return buttons + }() + + private lazy var accessoryDetailButton : [CMCTouchArea] = { + let image = CMCAsset._16x16arrowLeft.image + let buttons = [ + CMCTouchArea(image: image), + CMCTouchArea(image: image), + CMCTouchArea(image: image), + CMCTouchArea(image: image) + ] + return buttons + }() + + private lazy var buttonLabels: [UILabel] = { + var labels: [UILabel] = [] + let texts = [ + "전체 동의", + "서비스 이용약관(필수)", + "개인정보 수집/이용 (필수)", + "위치정보 이용 동의(선택)", + "마케팅 정보 수신 동의(선택)" + ] + texts.enumerated().forEach { index, text in + let label = UILabel() + if index == 0 { + label.font = CMCFontFamily.Pretendard.bold.font(size: 18) + } else { + label.font = CMCFontFamily.Pretendard.bold.font(size: 14) + } + label.textColor = CMCAsset.gray50.color + label.text = text + labels.append(label) + } + return labels + }() + + private lazy var separeteBar: UIView = { + let view = UIView() + view.backgroundColor = DesignSystemAsset.gray800.color + return view + }() + + private lazy var rowStackViews: [UIStackView] = { + var stackViews: [UIStackView] = [] + accessoryDetailButton.enumerated().forEach { index, button in + let stackView = UIStackView() + stackView.addArrangedSubview(buttonStackViews[index+1]) + stackView.addArrangedSubview(button) + stackView.axis = .horizontal + stackViews.append(stackView) + } + return stackViews + }() + + private lazy var nextButton: CMCButton = { + let button = CMCButton( + isRound: false, + iconTitle: nil, + type: .login(.disabled), + title: "다음") + return button + }() + + // MARK: - Properties + private var viewModel: TermsAndConditionsViewModel + + // MARK: - Initializers + init( + viewModel: TermsAndConditionsViewModel + ) { + self.viewModel = viewModel + super.init(frame: .zero) + self.backgroundColor = CMCAsset.background.color + } + + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - LifeCycle + // MARK: - Methods + + override func setAddSubView() { + self.addSubview(titleLabel) + self.addSubview(buttonStackViews[0]) + self.addSubview(separeteBar) + self.addSubview(rowStackViews[0]) + self.addSubview(rowStackViews[1]) + self.addSubview(rowStackViews[2]) + self.addSubview(rowStackViews[3]) + self.addSubview(nextButton) + } + + override func setConstraint() { + titleLabel.snp.makeConstraints{ make in + make.top.equalToSuperview().offset(30) + make.leading.equalToSuperview().offset(24) + } + + buttons.forEach { button in + button.snp.makeConstraints { make in + make.width.height.equalTo(44) + } + } + + accessoryDetailButton.forEach { button in + button.snp.makeConstraints { make in + make.width.height.equalTo(44) + } + } + + buttonStackViews[0].snp.makeConstraints{ make in + make.top.equalTo(titleLabel.snp.bottom).offset(30) + make.leading.equalToSuperview().offset(14) + } + + separeteBar.snp.makeConstraints{ make in + make.top.equalTo(buttonStackViews[0].snp.bottom).offset(10) + make.leading.trailing.equalToSuperview() + make.height.equalTo(1) + } + + rowStackViews[0].snp.makeConstraints { make in + make.top.equalTo(separeteBar.snp.bottom).offset(10) + make.leading.equalToSuperview().offset(14) + make.trailing.equalToSuperview().offset(-5) + } + + rowStackViews[1].snp.makeConstraints { make in + make.top.equalTo(rowStackViews[0].snp.bottom).offset(2) + make.leading.trailing.equalTo(rowStackViews[0]) + } + + rowStackViews[2].snp.makeConstraints { make in + make.top.equalTo(rowStackViews[1].snp.bottom).offset(2) + make.leading.trailing.equalTo(rowStackViews[0]) + } + + rowStackViews[3].snp.makeConstraints { make in + make.top.equalTo(rowStackViews[2].snp.bottom).offset(2) + make.leading.trailing.equalTo(rowStackViews[0]) + } + + nextButton.snp.makeConstraints { make in + make.bottom.equalTo(self.safeAreaLayoutGuide.snp.bottom).offset(-20) + make.leading.equalToSuperview().offset(14) + make.trailing.equalToSuperview().offset(-14) + make.height.equalTo(50) + } + + } + + override func bind() { + + let input = TermsAndConditionsViewModel.Input( + allAgreeBtnTapped: buttons[0].rx.tapped().asObservable(), + termsBtnTapped: buttons[1].rx.tapped().asObservable(), + conditionBtnTapped: buttons[2].rx.tapped().asObservable(), + locateBtnTapped: buttons[3].rx.tapped().asObservable(), + eventBtnTapped: buttons[4].rx.tapped().asObservable(), + + nextBtnTapped: nextButton.rx.tap.asObservable() + ) + + let output = viewModel.transform(input: input) + + output.allAgreeBtnState + .observe(on: MainScheduler.instance) + .withUnretained(self) + .subscribe(onNext: { owner, state in + owner.buttons[0].makeCustomState(type: state ? .selected : .normal) + }) + .disposed(by: disposeBag) + + output.termsBtnState + .observe(on: MainScheduler.instance) + .withUnretained(self) + .subscribe(onNext: { owner, state in + owner.buttons[1].makeCustomState(type: state ? .selected : .normal) + }) + .disposed(by: disposeBag) + + output.conditionBtnState + .observe(on: MainScheduler.instance) + .withUnretained(self) + .subscribe(onNext: { owner, state in + owner.buttons[2].makeCustomState(type: state ? .selected : .normal) + }) + .disposed(by: disposeBag) + + output.locateBtnState + .observe(on: MainScheduler.instance) + .withUnretained(self) + .subscribe(onNext: { owner, state in + owner.buttons[3].makeCustomState(type: state ? .selected : .normal) + }) + .disposed(by: disposeBag) + + output.eventBtnState + .observe(on: MainScheduler.instance) + .withUnretained(self) + .subscribe(onNext: { owner, state in + owner.buttons[4].makeCustomState(type: state ? .selected : .normal) + }) + .disposed(by: disposeBag) + + output.nextButtonState + .withUnretained(self) + .subscribe(onNext: { owner, state in + owner.nextButton.rxType.accept(state ? .login(.inactive) : .login(.disabled)) + }) + .disposed(by: disposeBag) + + } +} diff --git a/CMC/Sources/Presenter/Auth/SignUp/ScrollPages/TermsAndConditions/TermsAndConditionsViewModel.swift b/CMC/Sources/Presenter/Auth/SignUp/ScrollPages/TermsAndConditions/TermsAndConditionsViewModel.swift index e1a57d6..20e8619 100644 --- a/CMC/Sources/Presenter/Auth/SignUp/ScrollPages/TermsAndConditions/TermsAndConditionsViewModel.swift +++ b/CMC/Sources/Presenter/Auth/SignUp/ScrollPages/TermsAndConditions/TermsAndConditionsViewModel.swift @@ -7,3 +7,106 @@ // import Foundation + +import RxCocoa +import RxSwift + +import UIKit + +final class TermsAndConditionsViewModel: ViewModelType { + + struct Input { + let allAgreeBtnTapped: Observable + let termsBtnTapped: Observable + let conditionBtnTapped: Observable + let locateBtnTapped: Observable + let eventBtnTapped: Observable + + let nextBtnTapped: Observable + } + + struct Output { + let allAgreeBtnState: Observable + let termsBtnState: Observable + let conditionBtnState: Observable + let locateBtnState: Observable + let eventBtnState: Observable + + let nextButtonState: Observable + } + + var disposeBag: DisposeBag = DisposeBag() + var parentViewModel: SignUpViewModel + + init( + parentViewModel: SignUpViewModel + ) { + self.parentViewModel = parentViewModel + } + + private var allAgreeBtnRelay = BehaviorRelay(value: false) + private var termsBtnRelay = BehaviorRelay(value: false) + private var conditionBtnRelay = BehaviorRelay(value: false) + private var locateBtnRelay = BehaviorRelay(value: false) + private var eventBtnRelay = BehaviorRelay(value: false) + + func transform(input: Input) -> Output { + input.termsBtnTapped.bind { [unowned self] in + termsBtnRelay.accept(!termsBtnRelay.value) + updateAllAgreeState() + }.disposed(by: disposeBag) + + input.conditionBtnTapped.bind { [unowned self] in + conditionBtnRelay.accept(!conditionBtnRelay.value) + updateAllAgreeState() + }.disposed(by: disposeBag) + + input.locateBtnTapped.bind { [unowned self] in + locateBtnRelay.accept(!locateBtnRelay.value) + updateAllAgreeState() + }.disposed(by: disposeBag) + + input.eventBtnTapped.bind { [unowned self] in + eventBtnRelay.accept(!eventBtnRelay.value) + updateAllAgreeState() + }.disposed(by: disposeBag) + + input.allAgreeBtnTapped.bind { [unowned self] in + let newState = !allAgreeBtnRelay.value + allAgreeBtnRelay.accept(newState) + termsBtnRelay.accept(newState) + conditionBtnRelay.accept(newState) + locateBtnRelay.accept(newState) + eventBtnRelay.accept(newState) + }.disposed(by: disposeBag) + + input.nextBtnTapped + .withUnretained(self) + .subscribe(onNext: { owner, _ in + owner.parentViewModel.nextBtnTapped.accept(()) + }) + .disposed(by: disposeBag) + + let moveToNext = Observable.combineLatest(termsBtnRelay, conditionBtnRelay) + .map { $0 && $1 } + + return Output( + allAgreeBtnState: allAgreeBtnRelay.asObservable(), + termsBtnState: termsBtnRelay.asObservable(), + conditionBtnState: conditionBtnRelay.asObservable(), + locateBtnState: locateBtnRelay.asObservable(), + eventBtnState: eventBtnRelay.asObservable(), + nextButtonState: moveToNext.asObservable() + ) + } + +} + +// MARK: - Private Methods +extension TermsAndConditionsViewModel { + + private func updateAllAgreeState() { + let allSelected = termsBtnRelay.value && conditionBtnRelay.value && locateBtnRelay.value && eventBtnRelay.value + allAgreeBtnRelay.accept(allSelected) + } +} diff --git a/CMC/Sources/Presenter/Auth/SignUp/SignUpViewController.swift b/CMC/Sources/Presenter/Auth/SignUp/SignUpViewController.swift index 3b86ea9..c6e24ff 100644 --- a/CMC/Sources/Presenter/Auth/SignUp/SignUpViewController.swift +++ b/CMC/Sources/Presenter/Auth/SignUp/SignUpViewController.swift @@ -28,9 +28,12 @@ class SignUpViewController: BaseViewController { return navigationBar }() - private lazy var termsAndConditionsView: UIView = { - let view = UIView() - view.backgroundColor = .blue + private lazy var termsAndConditionsView: TermsAndConditionsView = { + let view = TermsAndConditionsView( + viewModel: TermsAndConditionsViewModel( + parentViewModel: self.viewModel + ) + ) return view }() @@ -55,19 +58,16 @@ class SignUpViewController: BaseViewController { return progressPager }() - private lazy var nextButton: CMCButton = { - let button = CMCButton( - isRound: false, - iconTitle: nil, - type: .login(.inactive), - title: "다음" - ) - return button - }() - // MARK: - Properties + private let viewModel: SignUpViewModel // MARK: - Initializers + init( + viewModel: SignUpViewModel + ) { + self.viewModel = viewModel + super.init() + } // MARK: - LifeCycle @@ -77,7 +77,6 @@ class SignUpViewController: BaseViewController { override func setAddSubView() { self.view.addSubview(navigationBar) self.view.addSubview(cmcPager) - self.view.addSubview(nextButton) } override func setConstraint() { @@ -89,40 +88,45 @@ class SignUpViewController: BaseViewController { cmcPager.snp.makeConstraints{ cmcPager in cmcPager.top.equalTo(navigationBar.snp.bottom) - cmcPager.leading.trailing.equalToSuperview() - cmcPager.bottom.equalToSuperview() + cmcPager.leading.trailing.bottom.equalToSuperview() } - nextButton.snp.makeConstraints { make in - make.bottom.equalTo(view.safeAreaLayoutGuide.snp.bottom).offset(-20) - make.leading.equalToSuperview().offset(24) - make.trailing.equalToSuperview().offset(-24) - make.height.equalTo(56) - } } override func bind() { - nextButton.rx.tap + + let input = SignUpViewModel.Input( + backButtonTapped: navigationBar.backButton.rx.tapped().asObservable(), + nowPage: cmcPager.getCurrentPage(), + totalPage: cmcPager.totalPages() + ) + + let output = viewModel.transform(input: input) + + output.nextBtnTapped + .observe(on: MainScheduler.instance) .withUnretained(self) .subscribe(onNext: { owner, _ in owner.cmcPager.nextPage() }) .disposed(by: disposeBag) - cmcPager.getCurrentPage() + output.backButtonHidden .observe(on: MainScheduler.instance) .withUnretained(self) - .subscribe(onNext: { owner, page in - owner.navigationBar.accessoryLabel.text = "\(page)/\(owner.cmcPager.totalPages())" + .subscribe(onNext: { owner, hidden in + owner.navigationBar.backButton.isHidden = hidden }) .disposed(by: disposeBag) - navigationBar.backButton.rx.tapped() + output.navigationAccessoryText + .observe(on: MainScheduler.instance) .withUnretained(self) - .subscribe(onNext: { owner, _ in - owner.cmcPager.previousPage() + .subscribe(onNext: { owner, text in + owner.navigationBar.accessoryLabel.text = text }) .disposed(by: disposeBag) + } } diff --git a/CMC/Sources/Presenter/Auth/SignUp/SignUpViewModel.swift b/CMC/Sources/Presenter/Auth/SignUp/SignUpViewModel.swift index 8fe9094..a7a3886 100644 --- a/CMC/Sources/Presenter/Auth/SignUp/SignUpViewModel.swift +++ b/CMC/Sources/Presenter/Auth/SignUp/SignUpViewModel.swift @@ -7,3 +7,74 @@ // import Foundation + +import RxCocoa +import RxSwift + +import DesignSystem +import SnapKit + +import UIKit + +class SignUpViewModel: ViewModelType{ + + struct Input { + let backButtonTapped: Observable + let nowPage: Observable + let totalPage: Int + } + + struct Output { + let nextBtnTapped: Observable + let backButtonHidden: Observable + let navigationAccessoryText: Observable + } + + // MARK: - Properties + private var authUsecase: AuthUsecase + + var disposeBag: DisposeBag = DisposeBag() + weak var coordinator: AuthCoordinator? + + let nextBtnTapped = PublishRelay() + + private let backButtonHidden = BehaviorSubject(value: false) + + // MARK: - Initializers + init( + coordinator: AuthCoordinator, + authUsecase: AuthUsecase + ) { + self.coordinator = coordinator + self.authUsecase = authUsecase + } + + // MARK: - Methods + func transform(input: Input) -> Output { + + input.backButtonTapped + .withUnretained(self) + .subscribe(onNext: { owner, _ in + owner.coordinator?.popViewController() + }) + .disposed(by: disposeBag) + + input.nowPage + .withUnretained(self) + .subscribe(onNext: { owner, page in + owner.backButtonHidden.onNext(page >= 3) + }) + .disposed(by: disposeBag) + + let navigationAccessoryText = input.nowPage + .map { page in + return "\(page)/\(input.totalPage)" + } + + return Output( + nextBtnTapped: nextBtnTapped.asObservable(), + backButtonHidden: backButtonHidden.asObservable(), + navigationAccessoryText: navigationAccessoryText + ) + } +} diff --git a/DesignSystem/Sources/CMCProgressPager.swift b/DesignSystem/Sources/CMCProgressPager.swift index f2a7365..55b2268 100644 --- a/DesignSystem/Sources/CMCProgressPager.swift +++ b/DesignSystem/Sources/CMCProgressPager.swift @@ -111,7 +111,7 @@ public final class CMCProgressPager: UIView { currentPage.asObservable() .subscribe(onNext: { [weak self] page in guard let self = self else { return } - let xOffset = CGFloat(page) * self.frame.size.width + let xOffset = CGFloat(page - 1) * self.frame.size.width self.pagerScrollView.setContentOffset(CGPoint(x: xOffset, y: 0), animated: true) }) .disposed(by: disposeBag) diff --git a/DesignSystem/Sources/CMCTouchArea.swift b/DesignSystem/Sources/CMCTouchArea.swift index b4a9247..7e41155 100644 --- a/DesignSystem/Sources/CMCTouchArea.swift +++ b/DesignSystem/Sources/CMCTouchArea.swift @@ -24,14 +24,14 @@ public final class CMCTouchArea: UIView{ // MARK: - UI private lazy var imageView: UIImageView = { let imageView = UIImageView() - imageView.image = image[style.rawValue] + imageView.image = image[style.value.rawValue] return imageView }() // MARK: - Properties private var disposeBag = DisposeBag() - private var style: TouchAreaStyle = .normal + private var style = BehaviorRelay(value: .normal) private var image: [Int:UIImage] = [:] /// 터치 영역의 `image`를 설정합니다. @@ -70,11 +70,18 @@ public final class CMCTouchArea: UIView{ } private func bind() { + + self.style + .withUnretained(self) + .subscribe(onNext: { owner, style in + owner.imageView.image = owner.image[style.rawValue] + }) + .disposed(by: disposeBag) + self.rx.tapped() .withUnretained(self) .subscribe(onNext: { owner, _ in - owner.style = owner.style == .normal ? .selected : .normal - owner.imageView.image = owner.image[owner.style.rawValue] + owner.style.accept(owner.style.value == .normal ? .selected : .normal) }) .disposed(by: disposeBag) } @@ -85,6 +92,10 @@ public final class CMCTouchArea: UIView{ } } + public func makeCustomState(type: TouchAreaStyle) { + style.accept(type) + } + } // MARK: - CMCTouchArea+RxSwift