diff --git a/dogether/Presentation/Base/BaseButton.swift b/dogether/Presentation/Base/BaseButton.swift index 91985efa..5135dbdd 100644 --- a/dogether/Presentation/Base/BaseButton.swift +++ b/dogether/Presentation/Base/BaseButton.swift @@ -6,8 +6,17 @@ // import UIKit +import RxSwift +import RxCocoa class BaseButton: UIButton { + /// 외부 노출용 Signal + var tap: Signal { tapRelay.asSignal() } + /// 내부 이벤트 스트림 + let tapRelay = PublishRelay() + + fileprivate let disposeBag = DisposeBag() + override init(frame: CGRect) { super.init(frame: frame) @@ -22,7 +31,9 @@ class BaseButton: UIButton { func configureView() { } /// 뷰의 동작 및 이벤트 처리를 설정하는 역할을 합니다 - func configureAction() { } + func configureAction() { + bindTap() + } /// 뷰 계층을 구성하는 역할을 합니다 func configureHierarchy() { } @@ -32,4 +43,12 @@ class BaseButton: UIButton { /// 뷰의 가변 요소들을 업데이트하는 역할을 합니다 func updateView(_ data: any BaseEntity) { } + + /// 공통 버튼 탭 이벤트 바인딩 + private func bindTap() { + rx.tap + .throttle(.milliseconds(500), scheduler: MainScheduler.instance) + .bind(to: tapRelay) + .disposed(by: disposeBag) + } } diff --git a/dogether/Presentation/Base/BaseViewController.swift b/dogether/Presentation/Base/BaseViewController.swift index 8c1e2a16..94296d22 100644 --- a/dogether/Presentation/Base/BaseViewController.swift +++ b/dogether/Presentation/Base/BaseViewController.swift @@ -15,7 +15,7 @@ class BaseViewController: UIViewController, CoordinatorDelegate { var datas: (any BaseEntity)? var pages: Array? - private let disposeBag = DisposeBag() + let disposeBag = DisposeBag() override func viewDidLoad() { super.viewDidLoad() @@ -31,6 +31,7 @@ class BaseViewController: UIViewController, CoordinatorDelegate { configurePages(pages) setViewDatas() + bindAction() } /// 뷰의 시각적인 속성을 설정하는 역할을 합니다 @@ -63,6 +64,9 @@ class BaseViewController: UIViewController, CoordinatorDelegate { /// View를 구성하는 필수 데이터를 세팅하고 바인딩하는 역할을 합니다 func setViewDatas() { } + /// 버튼이나 제스처, Rx 이벤트 바인딩 + func bindAction() { } + /// ViewDatas의 변화에 Page가 update 되도록 바인딩하는 역할을 합니다 func bind(_ relay: BehaviorRelay) { relay diff --git a/dogether/Presentation/Common/DogetherButton.swift b/dogether/Presentation/Common/DogetherButton.swift index 579a32a7..f63cfcac 100644 --- a/dogether/Presentation/Common/DogetherButton.swift +++ b/dogether/Presentation/Common/DogetherButton.swift @@ -7,9 +7,12 @@ import UIKit +import RxSwift +import RxCocoa + final class DogetherButton: BaseButton { private let title: String - + init(_ title: String) { self.title = title @@ -25,7 +28,9 @@ final class DogetherButton: BaseButton { layer.cornerRadius = 8 } - override func configureAction() { } + override func configureAction() { + super.configureAction() + } override func configureHierarchy() { } diff --git a/dogether/Presentation/Common/JoinCodeShareButton.swift b/dogether/Presentation/Common/JoinCodeShareButton.swift index 73cfdb1e..0057748a 100644 --- a/dogether/Presentation/Common/JoinCodeShareButton.swift +++ b/dogether/Presentation/Common/JoinCodeShareButton.swift @@ -28,7 +28,9 @@ final class JoinCodeShareButton: BaseButton { stackView.isUserInteractionEnabled = false } - override func configureAction() { } + override func configureAction() { + super.configureAction() + } override func configureHierarchy() { addSubview(stackView) diff --git a/dogether/Presentation/Features/Complete/CompleteViewController.swift b/dogether/Presentation/Features/Complete/CompleteViewController.swift index a59db885..2ef37c63 100644 --- a/dogether/Presentation/Features/Complete/CompleteViewController.swift +++ b/dogether/Presentation/Features/Complete/CompleteViewController.swift @@ -12,7 +12,6 @@ final class CompleteViewController: BaseViewController { private let viewModel = CompleteViewModel() override func viewDidLoad() { - completePage.delegate = self pages = [completePage] super.viewDidLoad() } @@ -24,25 +23,24 @@ final class CompleteViewController: BaseViewController { bind(viewModel.completeViewDatas) } -} - -protocol CompleteDelegate: AnyObject { - func goHomeAction() - func shareJoinCodeAction() -} - -extension CompleteViewController: CompleteDelegate { - func goHomeAction() { - coordinator?.setNavigationController(MainViewController()) - } - func shareJoinCodeAction() { - let data = viewModel.completeViewDatas.value + override func bindAction() { + completePage.homeTap + .emit(onNext: { [weak self] in + self?.coordinator?.setNavigationController(MainViewController()) + }) + .disposed(by: disposeBag) - let invite = SystemManager.inviteGroup( - groupName: data.groupInfo.name, - joinCode: data.joinCode - ) - present(UIActivityViewController(activityItems: invite, applicationActivities: nil), animated: true) + completePage.shareTap + .emit(onNext: { [weak self] in + guard let self else { return } + let data = viewModel.completeViewDatas.value + let invite = SystemManager.inviteGroup( + groupName: data.groupInfo.name, + joinCode: data.joinCode + ) + present(UIActivityViewController(activityItems: invite, applicationActivities: nil), animated: true) + }) + .disposed(by: disposeBag) } } diff --git a/dogether/Presentation/Features/Complete/Components/CompletePage.swift b/dogether/Presentation/Features/Complete/Components/CompletePage.swift index 476eaefc..6a9e00a8 100644 --- a/dogether/Presentation/Features/Complete/Components/CompletePage.swift +++ b/dogether/Presentation/Features/Complete/Components/CompletePage.swift @@ -7,26 +7,11 @@ import UIKit +import RxCocoa + final class CompletePage: BasePage { - weak var delegate: CompleteDelegate? { - didSet { - completeButton.addAction( - UIAction { [weak self] _ in - guard let self else { return } - delegate?.goHomeAction() - }, - for: .touchUpInside - ) - - joinCodeShareButton.addAction( - UIAction { [weak self] _ in - guard let self else { return } - delegate?.shareJoinCodeAction() - }, - for: .touchUpInside - ) - } - } + var homeTap: Signal { completeButton.tap } + var shareTap: Signal { joinCodeShareButton.tap } private let firecrackerImageView = UIImageView(image: .firecracker) private let titleLabel = UILabel() diff --git a/dogether/Presentation/Features/Main/Components/GroupInfoView.swift b/dogether/Presentation/Features/Main/Components/GroupInfoView.swift index 4732e13e..b49ea074 100644 --- a/dogether/Presentation/Features/Main/Components/GroupInfoView.swift +++ b/dogether/Presentation/Features/Main/Components/GroupInfoView.swift @@ -7,20 +7,40 @@ import UIKit -final class GroupInfoView: BaseView { - var delegate: MainDelegate? { - didSet { - groupNameStackView.addTapAction { [weak self] _ in - guard let self else { return } - delegate?.updateBottomSheetVisibleAction(isShowSheet: true) - } - - joinCodeStackView.addTapAction { [weak self] _ in - guard let self else { return } - delegate?.inviteAction() - } - } +import RxSwift +import RxRelay +import RxCocoa + +struct GroupInfoEvent { + enum Action { + case openGroupSelector + case invite } + + let action: Action + let group: ChallengeGroupInfo +} + +final class GroupInfoView: BaseView { +// var delegate: MainDelegate? { +// didSet { +// groupNameStackView.addTapAction { [weak self] in +// guard let self else { return } +// delegate?.updateBottomSheetVisibleAction(isShowSheet: true) +// } +// +// joinCodeStackView.addTapAction { [weak self] in +// guard let self else { return } +// delegate?.inviteAction() +// } +// } +// } + + // ✅ 외부 노출용 이벤트 스트림 + var event: Signal { _eventRelay.asSignal() } + + private let _eventRelay = PublishRelay() + private let disposeBag = DisposeBag() private let hasCopyImage: Bool private(set) var challengeGroupInfo: ChallengeGroupInfo @@ -128,7 +148,27 @@ final class GroupInfoView: BaseView { durationProgressView.transform = CGAffineTransform(translationX: 0, y: 1) // MARK: 디자인 디테일 반영 } - override func configureAction() { } + override func configureAction() { + groupNameStackView.addTapAction { [weak self] _ in + guard let self else { return } + _eventRelay.accept( + GroupInfoEvent( + action: .openGroupSelector, + group: challengeGroupInfo + ) + ) + } + + joinCodeStackView.addTapAction { [weak self] _ in + guard let self else { return } + _eventRelay.accept( + GroupInfoEvent( + action: .invite, + group: challengeGroupInfo + ) + ) + } + } override func configureHierarchy() { [nameLabel, changeGroupImageView, nameSpacerView].forEach { groupNameStackView.addArrangedSubview($0) } diff --git a/dogether/Presentation/Features/Main/Components/MainPage.swift b/dogether/Presentation/Features/Main/Components/MainPage.swift index 89d90120..16c1ae65 100644 --- a/dogether/Presentation/Features/Main/Components/MainPage.swift +++ b/dogether/Presentation/Features/Main/Components/MainPage.swift @@ -8,12 +8,16 @@ import UIKit import SnapKit +import RxSwift +import RxRelay +import RxCocoa + final class MainPage: BasePage { var delegate: MainDelegate? { didSet { bottomSheetView.delegate = delegate - groupInfoView.delegate = delegate +// groupInfoView.delegate = delegate rankingButton.delegate = delegate sheetHeaderView.delegate = delegate @@ -23,6 +27,10 @@ final class MainPage: BasePage { } } + var groupInfoEvent: Signal { _groupInfoEvent.asSignal() } + private let _groupInfoEvent = PublishRelay() + private let disposeBag = DisposeBag() + private let dogetherHeader = DogetherHeader() private let dosikCommentButton = DosikCommentButton() @@ -60,6 +68,10 @@ final class MainPage: BasePage { let dogetherPanGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:))) dogetherPanGesture.delegate = self dogetherSheet.addGestureRecognizer(dogetherPanGesture) + + groupInfoView.event + .emit(to: _groupInfoEvent) + .disposed(by: disposeBag) } override func configureHierarchy() { diff --git a/dogether/Presentation/Features/Main/MainViewController.swift b/dogether/Presentation/Features/Main/MainViewController.swift index b739221b..19067392 100644 --- a/dogether/Presentation/Features/Main/MainViewController.swift +++ b/dogether/Presentation/Features/Main/MainViewController.swift @@ -33,6 +33,32 @@ final class MainViewController: BaseViewController { bind(viewModel.sheetViewDatas) bind(viewModel.timerViewDatas) } + + override func bindAction() { + mainPage.groupInfoEvent + .emit(onNext: { [weak self] event in + guard let self else { return } + + switch event.action { + + case .openGroupSelector: + viewModel.bottomSheetViewDatas.update { + $0.isShowSheet = true + } + + case .invite: + let inviteGroup = SystemManager.inviteGroup( + groupName: event.group.name, + joinCode: event.group.joinCode + ) + present( + UIActivityViewController(activityItems: inviteGroup, applicationActivities: nil), + animated: true + ) + } + }) + .disposed(by: disposeBag) + } } extension MainViewController { @@ -114,7 +140,7 @@ protocol MainDelegate { func updateBottomSheetVisibleAction(isShowSheet: Bool) func selectGroupAction(index: Int) func addGroupAction() - func inviteAction() +// func inviteAction() func goPastAction() func goFutureAction() func startTimerAction() @@ -186,10 +212,10 @@ extension MainViewController: MainDelegate { coordinator?.pushViewController(startViewController, datas: startViewDatas) } - func inviteAction() { - let inviteGroup = SystemManager.inviteGroup(groupName: viewModel.currentGroup.name, joinCode: viewModel.currentGroup.joinCode) - present(UIActivityViewController(activityItems: inviteGroup, applicationActivities: nil), animated: true) - } +// func inviteAction() { +// let inviteGroup = SystemManager.inviteGroup(groupName: viewModel.currentGroup.name, joinCode: viewModel.currentGroup.joinCode) +// present(UIActivityViewController(activityItems: inviteGroup, applicationActivities: nil), animated: true) +// } func goPastAction() { viewModel.sheetViewDatas.update { diff --git a/dogether/Presentation/Features/TodoWrite/Components/TodoWritePage.swift b/dogether/Presentation/Features/TodoWrite/Components/TodoWritePage.swift index 15f7a9cc..3b028263 100644 --- a/dogether/Presentation/Features/TodoWrite/Components/TodoWritePage.swift +++ b/dogether/Presentation/Features/TodoWrite/Components/TodoWritePage.swift @@ -7,33 +7,22 @@ import UIKit +import RxSwift +import RxCocoa + final class TodoWritePage: BasePage { - var delegate: TodoWriteDelegate? { - didSet { - todoTextField.addAction( - UIAction { [weak self] _ in - guard let self else { return } - let todo = String((todoTextField.text ?? "").prefix(todoMaxLength)) - todoTextField.text = todo - delegate?.updateTodoAction(todo: todo) - }, for: .editingChanged - ) - - addButton.addAction( - UIAction { [weak self] _ in - guard let self else { return } - delegate?.addTodoAction(todoMaxCount: todoMaxCount) - }, for: .touchUpInside - ) - - saveButton.addAction( - UIAction { [weak self] _ in - guard let self else { return } - delegate?.saveTodoAction() - }, for: .touchUpInside - ) - } - } + var todoChanged: Signal<(String, Int)> { _todoChanged.asSignal() } + var addTap: Signal { _addTap.asSignal() } + var saveTap: Signal { _saveTap.asSignal() } + var removeTap: Signal { _removeTap.asSignal() } + var keyboardState: Signal { _keyboardState.asSignal() } + + private let _todoChanged = PublishRelay<(String, Int)>() + private let _addTap = PublishRelay() + private let _saveTap = PublishRelay() + private let _removeTap = PublishRelay() + private let _keyboardState = PublishRelay() + private let disposeBag = DisposeBag() private let navigationHeader = NavigationHeader(title: "투두 작성") @@ -137,15 +126,59 @@ final class TodoWritePage: BasePage { guard let self else { return } endEditing(true) } - - navigationHeader.delegate = coordinatorDelegate - todoTextField.delegate = self + navigationHeader.delegate = coordinatorDelegate todoTableView.delegate = self todoTableView.dataSource = self todoTableView.register(TodoWriteTableViewCell.self, forCellReuseIdentifier: TodoWriteTableViewCell.identifier) + todoTextField.rx.text + .orEmpty + .skip(1) + .do(onNext: { [weak self] _ in + guard let self else { return } + updateTextField() + }) + .map { ($0, self.todoMaxLength) } + .bind(to: _todoChanged) + .disposed(by: disposeBag) + + todoTextField.rx.controlEvent(.editingChanged) + .withLatestFrom(todoTextField.rx.text.orEmpty) + .subscribe(onNext: { [weak self] text in + guard let self else { return } + if text.count > self.todoMaxLength { + let limited = String(text.prefix(self.todoMaxLength)) + self.todoTextField.text = limited + self._todoChanged.accept((limited, self.todoMaxLength)) + } + }) + .disposed(by: disposeBag) + + addButton.rx.tap + .map { self.todoMaxCount } + .bind(to: _addTap) + .disposed(by: disposeBag) + + saveButton.tapRelay + .bind(to: _saveTap) + .disposed(by: disposeBag) + + todoTextField.rx.controlEvent(.editingDidEndOnExit) + .map { self.todoMaxCount } + .bind(to: _addTap) + .disposed(by: disposeBag) + + NotificationCenter.default.rx.notification(UIResponder.keyboardWillShowNotification) + .map { _ in true } + .bind(to: _keyboardState) + .disposed(by: disposeBag) + + NotificationCenter.default.rx.notification(UIResponder.keyboardWillHideNotification) + .map { _ in false } + .bind(to: _keyboardState) + .disposed(by: disposeBag) } override func configureHierarchy() { @@ -228,6 +261,7 @@ final class TodoWritePage: BasePage { currentTodos = datas.todos updateTodoLimitLabel() + updateTextField() emptyListView.isHidden = !datas.todos.isEmpty todoTableView.isHidden = datas.todos.isEmpty @@ -333,26 +367,9 @@ extension TodoWritePage: UITableViewDelegate, UITableViewDataSource { cell.setExtraInfo(todo: (currentTodos ?? [])[indexPath.row], index: indexPath.row) { [weak self] index in guard let self else { return } - delegate?.removeTodoAction(index: index) + _removeTap.accept(index) } return cell } } - -// MARK: - about keyboard (UITextFieldDelegate) -extension TodoWritePage: UITextFieldDelegate { - func textFieldShouldReturn(_ textField: UITextField) -> Bool { - guard addButton.isEnabled else { return false } - delegate?.addTodoAction(todoMaxCount: todoMaxCount) - return true - } - - func textFieldDidBeginEditing(_ textField: UITextField) { - delegate?.updateIsShowKeyboardAction(isShowKeyboard: true) - } - - func textFieldDidEndEditing(_ textField: UITextField) { - delegate?.updateIsShowKeyboardAction(isShowKeyboard: false) - } -} diff --git a/dogether/Presentation/Features/TodoWrite/TodoWriteViewController.swift b/dogether/Presentation/Features/TodoWrite/TodoWriteViewController.swift index e7cabc7f..e254ad73 100644 --- a/dogether/Presentation/Features/TodoWrite/TodoWriteViewController.swift +++ b/dogether/Presentation/Features/TodoWrite/TodoWriteViewController.swift @@ -15,8 +15,6 @@ final class TodoWriteViewController: BaseViewController { private let viewModel = TodoWriteViewModel() override func viewDidLoad() { - todoWritePage.delegate = self - pages = [todoWritePage] super.viewDidLoad() @@ -33,39 +31,49 @@ final class TodoWriteViewController: BaseViewController { bind(viewModel.todoWriteViewDatas) } -} - -// MARK: - delegate -protocol TodoWriteDelegate { - func updateIsShowKeyboardAction(isShowKeyboard: Bool) - func updateTodoAction(todo: String) - func addTodoAction(todoMaxCount: Int) - func removeTodoAction(index: Int) - func saveTodoAction() -} - -extension TodoWriteViewController: TodoWriteDelegate { - func updateIsShowKeyboardAction(isShowKeyboard: Bool) { - viewModel.updateIsShowKeyboard(isShowKeyboard: isShowKeyboard) - } - - func updateTodoAction(todo: String) { - viewModel.updateTodo(todo: todo) - } - - func addTodoAction(todoMaxCount: Int) { - viewModel.addTodo(todoMaxCount: todoMaxCount) - } - func removeTodoAction(index: Int) { - viewModel.removeTodo(index: index) - } - - func saveTodoAction() { - coordinator?.showPopup(self, type: .alert, alertType: .saveTodo) { [weak self] _ in - guard let self else { return } - trySaveTodo() - } + override func bindAction() { + todoWritePage.todoChanged + .emit(onNext: { [weak self] (text, maxLen) in + guard let self else { return } + viewModel.updateTodo(todo: text) + }) + .disposed(by: disposeBag) + + todoWritePage.addTap + .emit(onNext: { [weak self] maxCount in + guard let self else { return } + viewModel.addTodo(todoMaxCount: maxCount) + }) + .disposed(by: disposeBag) + + todoWritePage.removeTap + .emit(onNext: { [weak self] index in + guard let self else { return } + viewModel.removeTodo(index: index) + }) + .disposed(by: disposeBag) + + todoWritePage.saveTap + .emit(onNext: { [weak self] in + guard let self else { return } + coordinator?.showPopup( + self, + type: .alert, + alertType: .saveTodo + ) { [weak self] _ in + self?.trySaveTodo() + } + }) + .disposed(by: disposeBag) + + + todoWritePage.keyboardState + .emit(onNext: { [weak self] isShow in + guard let self else { return } + viewModel.updateIsShowKeyboard(isShowKeyboard: isShow) + }) + .disposed(by: disposeBag) } }