diff --git a/DailyQuest/DailyQuest/Domain/Entities/User.swift b/DailyQuest/DailyQuest/Domain/Entities/User.swift index 797a57f..e61c60b 100644 --- a/DailyQuest/DailyQuest/Domain/Entities/User.swift +++ b/DailyQuest/DailyQuest/Domain/Entities/User.swift @@ -44,6 +44,15 @@ struct User { } extension User { + func setAllow(allow: Bool) -> User { + return User(uuid: self.uuid, + nickName: self.nickName, + profileURL: self.profileURL, + backgroundImageURL: self.backgroundImageURL, + introduce: self.introduce, + allow: allow) + } + func setProfileImageURL(profileURL: String) -> User { return User(uuid: self.uuid, nickName: self.nickName, diff --git a/DailyQuest/DailyQuest/Domain/UseCases/Settings/DefaultSettingsUseCase.swift b/DailyQuest/DailyQuest/Domain/UseCases/Settings/DefaultSettingsUseCase.swift index 89ef700..e6ae64d 100644 --- a/DailyQuest/DailyQuest/Domain/UseCases/Settings/DefaultSettingsUseCase.swift +++ b/DailyQuest/DailyQuest/Domain/UseCases/Settings/DefaultSettingsUseCase.swift @@ -37,4 +37,18 @@ extension DefaultSettingsUseCase: SettingsUseCase { .catchAndReturn(false) .asObservable() } + + func updateAllow(allow: Bool) -> Single { + userRepository.readUser() + .map { $0.setAllow(allow: allow) } + .flatMap(userRepository.updateUser(by:)) + .map { _ in true } + .catchAndReturn(false) + } + + func fetchAllow() -> Single { + userRepository.readUser() + .map { $0.allow } + .catchAndReturn(nil) + } } diff --git a/DailyQuest/DailyQuest/Domain/UseCases/Settings/Protocols/SettingsUseCase.swift b/DailyQuest/DailyQuest/Domain/UseCases/Settings/Protocols/SettingsUseCase.swift index 2f4d42d..c944fe9 100644 --- a/DailyQuest/DailyQuest/Domain/UseCases/Settings/Protocols/SettingsUseCase.swift +++ b/DailyQuest/DailyQuest/Domain/UseCases/Settings/Protocols/SettingsUseCase.swift @@ -12,4 +12,7 @@ import RxSwift protocol SettingsUseCase { func isLoggedIn() -> Observable func signOut() -> Observable + + func updateAllow(allow: Bool) -> Single + func fetchAllow() -> Single } diff --git a/DailyQuest/DailyQuest/Presentation/Browse/Flow/BrowseCoordinator.swift b/DailyQuest/DailyQuest/Presentation/Browse/Flow/BrowseCoordinator.swift index d62587f..d5ee4f2 100644 --- a/DailyQuest/DailyQuest/Presentation/Browse/Flow/BrowseCoordinator.swift +++ b/DailyQuest/DailyQuest/Presentation/Browse/Flow/BrowseCoordinator.swift @@ -30,6 +30,7 @@ final class DefaultBrowseCoordinator: BrowseCoordinator { func start() { let browseViewController = browseSceneDIContainer.makeBrowseViewController() navigationController.pushViewController(browseViewController, animated: false) + navigationController.isNavigationBarHidden = true browseViewController .coordinatorPublisher diff --git a/DailyQuest/DailyQuest/Presentation/Browse/View/UserInfoView.swift b/DailyQuest/DailyQuest/Presentation/Browse/View/UserInfoView.swift index ba221a6..91385ff 100644 --- a/DailyQuest/DailyQuest/Presentation/Browse/View/UserInfoView.swift +++ b/DailyQuest/DailyQuest/Presentation/Browse/View/UserInfoView.swift @@ -6,7 +6,7 @@ // import UIKit - +import Kingfisher import SnapKit final class UserInfoView: UIStackView { @@ -59,5 +59,6 @@ final class UserInfoView: UIStackView { func setup(with user: User) { welcomeLabel.text = user.nickName + "님의 퀘스트" + userImage.setImage(with: user.profileURL) } } diff --git a/DailyQuest/DailyQuest/Presentation/Browse/ViewController/BrowseViewController.swift b/DailyQuest/DailyQuest/Presentation/Browse/ViewController/BrowseViewController.swift index 6146964..917cd97 100644 --- a/DailyQuest/DailyQuest/Presentation/Browse/ViewController/BrowseViewController.swift +++ b/DailyQuest/DailyQuest/Presentation/Browse/ViewController/BrowseViewController.swift @@ -17,6 +17,18 @@ final class BrowseViewController: UITableViewController { private var viewModel: BrowseViewModel! private var disposableBag = DisposeBag() + lazy var activityIndicator: UIActivityIndicatorView = { + // Create an indicator. + let activityIndicator = UIActivityIndicatorView() + activityIndicator.color = .maxDarkYellow + + let transfrom = CGAffineTransform.init(scaleX: 2, y: 2) + activityIndicator.transform = transfrom + + activityIndicator.startAnimating() + return activityIndicator + }() + // MARK: - Life Cycle static func create(with viewModel: BrowseViewModel) -> BrowseViewController { let view = BrowseViewController() @@ -28,10 +40,18 @@ final class BrowseViewController: UITableViewController { super.viewDidLoad() configure() - + configureIndicatorBar() bind() } + private func configureIndicatorBar() { + self.view.addSubview(activityIndicator) + activityIndicator.snp.makeConstraints { make in + make.width.height.equalTo(50) + make.centerX.centerY.equalToSuperview() + } + } + /** table view의 기본 정보를 설정합니다. */ @@ -50,6 +70,9 @@ final class BrowseViewController: UITableViewController { output .data + .do{ [weak self] _ in + self?.activityIndicator.stopAnimating() + } .drive(tableView.rx.items(cellIdentifier: BrowseCell.reuseIdentifier, cellType: BrowseCell.self)) { row, item, cell in cell.setup(with: item) } diff --git a/DailyQuest/DailyQuest/Presentation/Common/View/QuestViewHeader.swift b/DailyQuest/DailyQuest/Presentation/Common/View/QuestViewHeader.swift index 003f70e..10dfc7b 100644 --- a/DailyQuest/DailyQuest/Presentation/Common/View/QuestViewHeader.swift +++ b/DailyQuest/DailyQuest/Presentation/Common/View/QuestViewHeader.swift @@ -18,7 +18,7 @@ final class QuestViewHeader: UIStackView { // MARK: - Components private(set) lazy var titleLabel: UILabel = { let titleLabel = UILabel() - titleLabel.text = "Today Quests" + titleLabel.text = "오늘의 퀘스트" titleLabel.textColor = .maxViolet titleLabel.font = UIFont.boldSystemFont(ofSize: 32) diff --git a/DailyQuest/DailyQuest/Presentation/Home/ViewModel/HomeViewModel.swift b/DailyQuest/DailyQuest/Presentation/Home/ViewModel/HomeViewModel.swift index 22b895d..fccefb0 100644 --- a/DailyQuest/DailyQuest/Presentation/Home/ViewModel/HomeViewModel.swift +++ b/DailyQuest/DailyQuest/Presentation/Home/ViewModel/HomeViewModel.swift @@ -94,14 +94,14 @@ final class HomeViewModel { .do(onNext: { [weak self] date in self?.currentDate = date }) - .flatMap(questUseCase.fetch(by:)) - .asDriver(onErrorJustReturn: []) + .flatMap(questUseCase.fetch(by:)) + .asDriver(onErrorJustReturn: []) - let userNotification = NotificationCenter - .default - .rx - .notification(.userUpdated) - .map { _ in Date() } + let userNotification = NotificationCenter + .default + .rx + .notification(.userUpdated) + .map { _ in Date() } let userData = Observable .merge( @@ -194,13 +194,10 @@ final class HomeViewModel { private extension HomeViewModel { func calculateRelative(_ date: Date) -> String { let today = Date() - - if today.startOfDay > date.startOfDay { - return "Previous Quests" - } else if today.startOfDay < date.startOfDay { - return "Upcomming Quests" + if today.startOfDay == date.startOfDay { + return "오늘의 퀘스트" } else { - return "Today Quests" + return "\(date.toFormatMonthDay)의 퀘스트 " } } } diff --git a/DailyQuest/DailyQuest/Presentation/Settings/View/ToggleField/ToggleCell.swift b/DailyQuest/DailyQuest/Presentation/Settings/View/ToggleField/ToggleCell.swift index 81e786d..0f4f381 100644 --- a/DailyQuest/DailyQuest/Presentation/Settings/View/ToggleField/ToggleCell.swift +++ b/DailyQuest/DailyQuest/Presentation/Settings/View/ToggleField/ToggleCell.swift @@ -6,9 +6,15 @@ // import UIKit +import RxSwift +import RxCocoa final class ToggleCell: UITableViewCell { static let reuseIdentifier = "ToggleCell" + private var viewModel: ToggleItemViewModel! + + var toggleItemDidClicked = PublishSubject() + private var disposableBag = DisposeBag() private let padding = 20 @@ -45,9 +51,9 @@ final class ToggleCell: UITableViewCell { } private func configureUI() { - addSubview(icon) - addSubview(title) - addSubview(toggle) + contentView.addSubview(icon) + contentView.addSubview(title) + contentView.addSubview(toggle) icon.snp.makeConstraints { make in make.leading.top.bottom.equalToSuperview().inset(padding) @@ -67,5 +73,40 @@ final class ToggleCell: UITableViewCell { func setup(with viewModel: ToggleItemViewModel) { icon.image = UIImage(systemName: viewModel.imageName) title.text = viewModel.title + self.viewModel = viewModel + bind() + } + + func bind() { + toggle.rx.tapGesture() + .when(.ended) + .do(onNext: { _ in + print(self.toggle.isOn) + }) + .bind(onNext: {_ in + self.toggleItemDidClicked.onNext(!self.toggle.isOn) + }) + .disposed(by: disposableBag) + + let output = viewModel.transform(input: ToggleItemViewModel.Input( + toggleItemDidClicked: toggleItemDidClicked + )) + + output.toggleItemResult + .subscribe(onNext: { isOn in + guard let isOn = isOn else { + self.toggle.isOn = false + self.toggle.isEnabled = false + return + } + + if !self.toggle.isEnabled { + self.toggle.isEnabled = true + } + DispatchQueue.main.async { + self.toggle.isOn = isOn + } + }) + .disposed(by: disposableBag) } } diff --git a/DailyQuest/DailyQuest/Presentation/Settings/View/ToggleField/ToggleItemViewModel.swift b/DailyQuest/DailyQuest/Presentation/Settings/View/ToggleField/ToggleItemViewModel.swift index ac45b9d..4c6f5c2 100644 --- a/DailyQuest/DailyQuest/Presentation/Settings/View/ToggleField/ToggleItemViewModel.swift +++ b/DailyQuest/DailyQuest/Presentation/Settings/View/ToggleField/ToggleItemViewModel.swift @@ -6,8 +6,34 @@ // import Foundation +import RxSwift struct ToggleItemViewModel { let title: String let imageName: String + let settingsUseCase: SettingsUseCase! + + struct Input { + let toggleItemDidClicked: Observable + } + + struct Output { + let toggleItemResult: Observable + } + + func transform(input: Input) -> Output { + let fetchAllow = settingsUseCase.isLoggedIn() + .flatMap { _ in settingsUseCase.fetchAllow() } + .asObservable() + + let changeAllow = input.toggleItemDidClicked + .flatMap { isOn in + settingsUseCase.updateAllow(allow: isOn).asObservable() + .map { result in result ? isOn : nil } + } + + let toggleItemResult = Observable.merge(fetchAllow, changeAllow) + + return Output(toggleItemResult: toggleItemResult) + } } diff --git a/DailyQuest/DailyQuest/Presentation/Settings/ViewController/LoginViewController.swift b/DailyQuest/DailyQuest/Presentation/Settings/ViewController/LoginViewController.swift index 7be2470..cba59e0 100644 --- a/DailyQuest/DailyQuest/Presentation/Settings/ViewController/LoginViewController.swift +++ b/DailyQuest/DailyQuest/Presentation/Settings/ViewController/LoginViewController.swift @@ -60,6 +60,17 @@ final class LoginViewController: UIViewController { return UIButton(configuration: config) }() + lazy var activityIndicator: UIActivityIndicatorView = { + // Create an indicator. + let activityIndicator = UIActivityIndicatorView() + activityIndicator.color = .maxDarkYellow + + let transfrom = CGAffineTransform.init(scaleX: 2, y: 2) + activityIndicator.transform = transfrom + + return activityIndicator + }() + // MARK: Life Cycle static func create(with viewModel: LoginViewModel) -> LoginViewController { let vc = LoginViewController() @@ -68,6 +79,10 @@ final class LoginViewController: UIViewController { return vc } + override func viewWillAppear(_ animated: Bool) { + navigationController?.isNavigationBarHidden = false + } + override func viewDidLoad() { super.viewDidLoad() @@ -85,11 +100,17 @@ final class LoginViewController: UIViewController { container.addArrangedSubview(signUpButton) view.addSubview(container) + view.addSubview(activityIndicator) container.snp.makeConstraints { make in make.center.equalToSuperview() make.width.equalToSuperview().multipliedBy(0.8) } + + activityIndicator.snp.makeConstraints { make in + make.width.height.equalTo(50) + make.centerX.centerY.equalToSuperview() + } } private func setup(with authViewModel: LoginViewModel) { @@ -104,10 +125,16 @@ extension LoginViewController { self.itemDidClick.onNext(.showSignUpFlow) }).disposed(by: disposableBag) + let submitButtonDidTapEvent = submitButton.rx.tap + .asObservable() + .do(onNext: { [weak self] _ in + self?.activityIndicator.startAnimating() + }) + let input = LoginViewModel.Input( emailFieldDidEditEvent: emailField.rx.text.orEmpty.asObservable(), passwordFieldDidEditEvent: passwordField.rx.text.orEmpty.asObservable(), - submitButtonDidTapEvent: submitButton.rx.tap.asObservable() + submitButtonDidTapEvent: submitButtonDidTapEvent ) let output = viewModel.transform(input: input, disposeBag: disposableBag) @@ -126,6 +153,7 @@ extension LoginViewController { extension LoginViewController: Alertable { private func analyse(result: Bool) { + activityIndicator.stopAnimating() if result { itemDidClick.onNext(.back) } else { diff --git a/DailyQuest/DailyQuest/Presentation/Settings/ViewController/SettingsViewController.swift b/DailyQuest/DailyQuest/Presentation/Settings/ViewController/SettingsViewController.swift index ff11c38..1fd8015 100644 --- a/DailyQuest/DailyQuest/Presentation/Settings/ViewController/SettingsViewController.swift +++ b/DailyQuest/DailyQuest/Presentation/Settings/ViewController/SettingsViewController.swift @@ -15,6 +15,7 @@ final class SettingsViewController: UITableViewController { private var disposableBag = DisposeBag() var itemDidClick = PublishSubject() + var toggleButtonDidClick = PublishSubject() // MARK: - Life Cycle static func create(with viewModel: SettingsViewModel) -> SettingsViewController { @@ -26,6 +27,7 @@ final class SettingsViewController: UITableViewController { override func viewDidLoad() { super.viewDidLoad() + navigationController?.isNavigationBarHidden = true tableView.separatorStyle = .singleLine register() diff --git a/DailyQuest/DailyQuest/Presentation/Settings/ViewController/SignUpViewController.swift b/DailyQuest/DailyQuest/Presentation/Settings/ViewController/SignUpViewController.swift index 406df9f..006d0f7 100644 --- a/DailyQuest/DailyQuest/Presentation/Settings/ViewController/SignUpViewController.swift +++ b/DailyQuest/DailyQuest/Presentation/Settings/ViewController/SignUpViewController.swift @@ -15,92 +15,113 @@ final class SignUpViewController: UIViewController { enum Event { case back } - + private var viewModel: SignUpViewModel! private var disposableBag = DisposeBag() - + var itemDidClick = PublishSubject() - + private lazy var container: UIStackView = { let container = UIStackView() container.axis = .vertical container.spacing = 10 - + return container }() - + private lazy var emailField: TextFieldForm = { let emailField = TextFieldForm() emailField.placeholder = "email" emailField.autocapitalizationType = .none - + return emailField }() - + private lazy var passwordField: TextFieldForm = { let passwordField = TextFieldForm() passwordField.placeholder = "password (6글자 이상)" passwordField.isSecureTextEntry = true - + return passwordField }() - + private lazy var passwordConfirmField: TextFieldForm = { let passwordConfirmField = TextFieldForm() passwordConfirmField.placeholder = "password 확인" passwordConfirmField.isSecureTextEntry = true return passwordConfirmField }() - + private lazy var nickNameField: TextFieldForm = { let nickNameField = TextFieldForm() nickNameField.placeholder = "닉네임" emailField.autocapitalizationType = .none return nickNameField }() - + private lazy var submitButton: UIButton = { var config = UIButton.Configuration.filled() config.baseBackgroundColor = .maxYellow config.title = "회원가입" - + return UIButton(configuration: config) }() - + + lazy var activityIndicator: UIActivityIndicatorView = { + // Create an indicator. + let activityIndicator = UIActivityIndicatorView() + activityIndicator.color = .maxDarkYellow + + let transfrom = CGAffineTransform.init(scaleX: 2, y: 2) + activityIndicator.transform = transfrom + + return activityIndicator + }() + // MARK: Life Cycle static func create(with viewModel: SignUpViewModel) -> SignUpViewController { let vc = SignUpViewController() vc.setup(with: viewModel) return vc } - + + override func viewWillAppear(_ animated: Bool) { + navigationController?.isNavigationBarHidden = false + } + override func viewDidLoad() { super.viewDidLoad() - + configureUI() - + bind() } - + private func configureUI() { view.backgroundColor = .white - + [emailField, - passwordField, - passwordConfirmField, - nickNameField, - submitButton].forEach { field in + passwordField, + passwordConfirmField, + nickNameField, + submitButton].forEach { field in container.addArrangedSubview(field) } - + view.addSubview(container) - + view.addSubview(activityIndicator) + container.snp.makeConstraints { make in make.center.equalToSuperview() make.width.equalToSuperview().multipliedBy(0.8) } + + activityIndicator.snp.makeConstraints { make in + make.width.height.equalTo(50) + make.centerX.centerY.equalToSuperview() + } } - + private func setup(with authViewModel: SignUpViewModel) { viewModel = authViewModel } @@ -108,21 +129,26 @@ final class SignUpViewController: UIViewController { extension SignUpViewController { private func bind() { + let submitButtonDidTapEvent = submitButton.rx.tap + .asObservable() + .do(onNext: { [weak self] _ in + self?.activityIndicator.startAnimating() + }) + let input = SignUpViewModel.Input( emailFieldDidEditEvent: emailField.rx.text.orEmpty.asObservable(), passwordFieldDidEditEvent: passwordField.rx.text.orEmpty.asObservable(), passwordConfirmFieldDidEditEvent: passwordConfirmField.rx.text.orEmpty.asObservable(), nickNameFieldDidEditEvent: nickNameField.rx.text.orEmpty.asObservable(), - submitButtonDidTapEvent: submitButton.rx.tap.asObservable() - ) - + submitButtonDidTapEvent: submitButtonDidTapEvent) + let output = viewModel.transform(input: input, disposeBag: disposableBag) - + output .buttonEnabled .drive(submitButton.rx.isEnabled) .disposed(by: disposableBag) - + output .signUpResult .bind(onNext: analyse(result:)) @@ -132,6 +158,7 @@ extension SignUpViewController { extension SignUpViewController: Alertable { private func analyse(result: Bool) { + activityIndicator.stopAnimating() if result { itemDidClick.onNext(.back) } else { diff --git a/DailyQuest/DailyQuest/Presentation/Settings/ViewModel/SettingsViewModel.swift b/DailyQuest/DailyQuest/Presentation/Settings/ViewModel/SettingsViewModel.swift index 886cd80..95a1409 100644 --- a/DailyQuest/DailyQuest/Presentation/Settings/ViewModel/SettingsViewModel.swift +++ b/DailyQuest/DailyQuest/Presentation/Settings/ViewModel/SettingsViewModel.swift @@ -19,8 +19,10 @@ final class SettingsViewModel { init(settingsUseCase: SettingsUseCase) { self.settingsUseCase = settingsUseCase - let toggleField = ToggleField(viewModel: .init(title: "둘러보기 허용", imageName: "person.crop.circle.badge.checkmark")) - let plainField = PlainField(viewModel: .init(title: "앱 버전", info: "0.0.1", imageName: "exclamationmark.transmission")) + let toggleField = ToggleField(viewModel: .init(title: "둘러보기 허용", + imageName: "person.crop.circle.badge.checkmark", + settingsUseCase: settingsUseCase)) + let plainField = PlainField(viewModel: .init(title: "앱 버전", info: "1.1", imageName: "exclamationmark.transmission")) self.navigateField = NavigateField(viewModel: .init(title: "로그인", imageName: "person.circle.fill", viewType: .login)) self.fields = [ diff --git a/DailyQuest/DailyQuest/Utils/Date+.swift b/DailyQuest/DailyQuest/Utils/Date+.swift index e9afae1..b04c5a5 100644 --- a/DailyQuest/DailyQuest/Utils/Date+.swift +++ b/DailyQuest/DailyQuest/Utils/Date+.swift @@ -19,6 +19,12 @@ extension Date { dateFormatter.dateFormat = "yyyy년 MM월" return dateFormatter.string(from: self) } + + var toFormatMonthDay: String { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "MM월 dd일" + return dateFormatter.string(from: self) + } } extension Date {