Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[refactor] 준비 정보 입력 뷰 Rx 리팩토링 #405

Open
wants to merge 10 commits into
base: suyeon
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -253,16 +253,14 @@ extension ReadyStatusViewController {
promiseID: viewModel.promiseID,
promiseTime: promiseTime,
promiseName: promiseName,
storedReadyHour: (preparationTime / 60).description,
storedReadyMinute: (preparationTime % 60).description,
storedMoveHour: (travelTime / 60).description,
storedMoveMinute: (travelTime % 60).description,
service: PromiseService()
)

viewModel.storedReadyHour = preparationTime / 60
viewModel.storedReadyMinute = preparationTime % 60
viewModel.storedMoveHour = travelTime / 60
viewModel.storedMoveMinute = travelTime % 60

let viewController = SetReadyInfoViewController(viewModel: viewModel)

navigationController?.pushViewController(viewController, animated: true)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,19 @@

import UIKit

import RxCocoa
import RxSwift

final class SetReadyInfoViewController: BaseViewController {


// MARK: - Property

private let rootView = SetReadyInfoView()

private let viewModel: SetReadyInfoViewModel
private let viewWillAppearRelay = PublishRelay<Void>()
private let disposeBag = DisposeBag()


// MARK: - Initializer
Expand All @@ -31,108 +37,152 @@ final class SetReadyInfoViewController: BaseViewController {
// MARK: - LifeCycle

override func loadView() {
self.view = rootView
view = rootView
}

override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white

setupNavigationBarBackButton()
setupNavigationBarTitle(with: "준비 정보 입력하기")

setupBinding()
setupTapGesture()
setupTextField()
bindViewModel()
}

override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)

navigationController?.isNavigationBarHidden = false
viewWillAppearRelay.accept(())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

}

override func setupView() {
setupNavigationBarBackButton()
setupNavigationBarTitle(with: "준비 정보 입력하기")
}

override func setupDelegate() {
setTextFieldDelegate()
}

override func setupAction() {
rootView.readyHourTextField.addTarget(
self,
action: #selector(textFieldDidChange),
for: .editingChanged
)
rootView.readyMinuteTextField.addTarget(
self,
action: #selector(textFieldDidChange),
for: .editingChanged
)
rootView.moveHourTextField.addTarget(
self,
action: #selector(textFieldDidChange),
for: .editingChanged
)
rootView.moveMinuteTextField.addTarget(
self,
action: #selector(textFieldDidChange),
for: .editingChanged
)
rootView.doneButton.addTarget(
self,
action: #selector(doneButtonDidTap),
for: .touchUpInside
)
setupTextField(textField: rootView.readyHourTextField)
setupTextField(textField: rootView.readyMinuteTextField)
setupTextField(textField: rootView.moveHourTextField)
setupTextField(textField: rootView.moveMinuteTextField)

let tapGesture = UITapGestureRecognizer()
view.addGestureRecognizer(tapGesture)
tapGesture.rx.event
.subscribe(with: self) { owner, _ in
owner.view.endEditing(true)
}
.disposed(by: disposeBag)
Comment on lines +72 to +78
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

}

@objc
private func textFieldDidChange(_ textField: UITextField) {
let text = textField.text ?? ""
viewModel.updateTime(textField: textField.accessibilityIdentifier ?? "", time: text)
viewModel.checkValid(
readyHourText: rootView.readyHourTextField.text ?? "",
readyMinuteText: rootView.readyMinuteTextField.text ?? "",
moveHourText: rootView.moveHourTextField.text ?? "",
moveMinuteText: rootView.moveMinuteTextField.text ?? ""
private func bindViewModel() {
let input = SetReadyInfoViewModel.Input(
viewWillAppear: viewWillAppearRelay,
readyHourText: rootView.readyHourTextField.rx.text.orEmpty.asObservable(),
readyMinuteText: rootView.readyMinuteTextField.rx.text.orEmpty.asObservable(),
moveHourText: rootView.moveHourTextField.rx.text.orEmpty.asObservable(),
moveMinuteText: rootView.moveMinuteTextField.rx.text.orEmpty.asObservable(),
doneButtonDidTap: rootView.doneButton.rx.tap.asObservable()
)

let output = viewModel.transform(input: input, disposeBag: disposeBag)

output.readyHourText
.drive(with: self) { owner, text in
owner.rootView.readyHourTextField.text = text
}
.disposed(by: disposeBag)

output.readyMinuteText
.drive(with: self) { owner, text in
owner.rootView.readyMinuteTextField.text = text
}
.disposed(by: disposeBag)

output.moveHourText
.drive(with: self) { owner, text in
owner.rootView.moveHourTextField.text = text
}
.disposed(by: disposeBag)

output.moveMinuteText
.drive(with: self) { owner, text in
owner.rootView.moveMinuteTextField.text = text
}
.disposed(by: disposeBag)
Comment on lines +93 to +115
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bind를 이용하여 구현할 수도 있을 것 같아요.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

근데 binddrive 모두 메인 스레드에서 동작하는 명령어인데 어떨 때 어떤 명령어를 사용해야 하는지 감이 잘 안잡힙니다 ㅠㅠ
정확히 어떤 상황에서 사용하는지 예시 하나만 들어주실 수 있을까요 🥹


output.errMessage
.drive(with: self) { owner, err in
owner.showToast(err)
}
Comment on lines +118 to +120
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

가능하면 축약형이 아닌 error로 표현해주세요.

.disposed(by: disposeBag)

output.doneButtonIsEnabled
.drive(with: self) { owner, isEnabled in
owner.rootView.doneButton.backgroundColor = isEnabled ? .maincolor : .gray2
owner.rootView.doneButton.isEnabled = isEnabled
}
.disposed(by: disposeBag)

output.isSucceed
.drive(with: self) { owner, isSucceed in
if isSucceed {
owner.navigateToSetReadyCompleted()
}
}
.disposed(by: disposeBag)
Comment on lines +130 to +136
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아래와 같이 구현할 수도 있을 것 같네요.

output.isSucceed
    .filter { $0 } // 성공 조건 필터링
    .drive(with: self) { owner, _ in
        owner.navigateToSetReadyCompleted()
    }
    .disposed(by: disposeBag)

}

@objc
private func doneButtonDidTap(_ sender: UIButton) {
viewModel.updateReadyInfo()
private func setupTextField(textField: UITextField) {
let textFieldEvent = Observable.merge(
textField.rx.controlEvent(.editingDidBegin).map { UIColor.maincolor.cgColor },
textField.rx.controlEvent(.editingDidEnd).map { UIColor.gray3.cgColor },
textField.rx.controlEvent(.editingDidEndOnExit).map { UIColor.gray3.cgColor }
)
Comment on lines +140 to +144
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

와 진짜 깔끔하네요 ... 수야미 짱


textFieldEvent
.bind { borderColor in
textField.layer.borderColor = borderColor
}
.disposed(by: disposeBag)
Comment on lines +139 to +150
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3가지의 이벤트에 대해 위와 같이 구현하여, 반복되는 코드를 줄인 것이 대단한 것 같습니다. 👍

}

private func navigateToSetReadyCompleted() {
let setReadyCompletedViewController = SetReadyCompletedViewController()
self.navigationController?.pushViewController(
setReadyCompletedViewController,
animated: true
)
}

// MARK: - Keyboard Dismissal

private func setupTapGesture() {
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard))
view.addGestureRecognizer(tapGesture)
private func showToast(_ message: String, bottomInset: CGFloat = 128) {
guard let view else { return }
Comment on lines +161 to +162
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

guard let view else { return }을 한 이유가 있을까요?

Toast().show(message: message, view: view, position: .bottom, inset: bottomInset)
}

@objc private func dismissKeyboard() {
view.endEditing(true)
private func setTextFieldDelegate() {
let textFields: [(UITextField, String)] = [
(rootView.readyHourTextField, "readyHour"),
(rootView.readyMinuteTextField, "readyMinute"),
(rootView.moveHourTextField, "moveHour"),
(rootView.moveMinuteTextField, "moveMinute")
]

textFields.forEach { (textField, identifier) in
textField.delegate = self
textField.keyboardType = .numberPad
textField.accessibilityIdentifier = identifier
}
Comment on lines +176 to +178
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아래의 두 내용은 View 클래스에서 선언되어도 되지 않나 하는 생각이 있네요.
UI를 설정하는 관점에서 바라본다면 말이죠.

}
}


// MARK: - UITextFieldDelegate

extension SetReadyInfoViewController: UITextFieldDelegate {
func textFieldDidBeginEditing(_ textField: UITextField) {
textField.layer.borderColor = UIColor.maincolor.cgColor
}

func textFieldDidEndEditing(_ textField: UITextField) {
textField.layer.borderColor = UIColor.gray3.cgColor

if let text = textField.text, !text.isEmpty {
viewModel.updateTime(
textField: textField.accessibilityIdentifier ?? "",
time: textField.text ?? ""
)
}
}

func textField(
_ textField: UITextField,
shouldChangeCharactersIn range: NSRange,
Expand All @@ -143,91 +193,3 @@ extension SetReadyInfoViewController: UITextFieldDelegate {
return allowedCharacters.isSuperset(of: characterSet)
}
}


// MARK: - Function

private extension SetReadyInfoViewController {
func setupTextField() {
/// 저장된 준비 시간이 0이 아니면 텍스트 필드에 설정
if viewModel.storedReadyHour != 0 || viewModel.storedReadyMinute != 0 {
rootView.readyHourTextField.text = String(viewModel.storedReadyHour)
rootView.readyMinuteTextField.text = String(viewModel.storedReadyMinute)
}

/// 저장된 이동 시간이 0이 아니면 텍스트 필드에 설정
if viewModel.storedMoveHour != 0 || viewModel.storedMoveMinute != 0 {
rootView.moveHourTextField.text = String(viewModel.storedMoveHour)
rootView.moveMinuteTextField.text = String(viewModel.storedMoveMinute)
}

viewModel.checkValid(
readyHourText: rootView.readyHourTextField.text ?? "",
readyMinuteText: rootView.readyMinuteTextField.text ?? "",
moveHourText: rootView.moveHourTextField.text ?? "",
moveMinuteText: rootView.moveMinuteTextField.text ?? ""
)
}

func setTextFieldDelegate() {
let textFields: [(UITextField, String)] = [
(rootView.readyHourTextField, "readyHour"),
(rootView.readyMinuteTextField, "readyMinute"),
(rootView.moveHourTextField, "moveHour"),
(rootView.moveMinuteTextField, "moveMinute")
]

textFields.forEach { (textField, identifier) in
textField.delegate = self
textField.keyboardType = .numberPad
textField.accessibilityIdentifier = identifier
}
}

func showToast(_ message: String, bottomInset: CGFloat = 128) {
guard let view else { return }
Toast().show(message: message, view: view, position: .bottom, inset: bottomInset)
}

// MARK: - Data Bind

func setupBinding() {
viewModel.readyHour.bind { [weak self] readyHour in
self?.rootView.readyHourTextField.text = readyHour
}

viewModel.readyMinute.bind { [weak self] readyMinute in
self?.rootView.readyMinuteTextField.text = readyMinute
}

viewModel.moveHour.bind { [weak self] moveHour in
self?.rootView.moveHourTextField.text = moveHour
}

viewModel.moveMinute.bind { [weak self] moveMinute in
self?.rootView.moveMinuteTextField.text = moveMinute
}

viewModel.isValid.bind { [weak self] isValid in
self?.rootView.doneButton.isEnabled = isValid
}

viewModel.errMessage.bind { [weak self] err in
if !err.isEmpty {
self?.showToast(err)
}
}

viewModel.isSucceedToSave.bind { [weak self] _ in
if self?.viewModel.isSucceedToSave.value == true {
DispatchQueue.main.async {
let viewController = SetReadyCompletedViewController()
self?.navigationController?.pushViewController(
viewController,
animated: true
)
}
}
}
}
}
Loading