From 5d8024887b132ed8a33e2aaf72f5efef752c652f Mon Sep 17 00:00:00 2001 From: sun Date: Thu, 16 Mar 2023 16:31:08 +0900 Subject: [PATCH] =?UTF-8?q?[refactor]=20NewNoteInputViewController=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20=EB=B0=8F=20=EA=B8=B0=EC=A1=B4=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=A0=9C=EA=B1=B0=20#299?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Happiggy-bank.xcodeproj/project.pbxproj | 16 +- .../UI/Controller/HomeTabViewController.swift | 5 +- .../UI/Controller/HomeViewController.swift | 9 +- .../NewNoteDatePickerViewController.swift | 12 +- .../NewNoteInputViewController.swift | 470 ++++++++++++++++++ .../NewNoteTextViewController.swift | 470 ------------------ .../ViewModel/NewNoteTextViewModel.swift | 106 ---- .../Happiggy-bank/Utils/Constants.swift | 86 ---- .../Protocol/NewNoteSavingDelegate.swift | 15 + 9 files changed, 503 insertions(+), 686 deletions(-) create mode 100644 Happiggy-bank/Happiggy-bank/HomeTab/NewNote/UI/Controller/NewNoteInputViewController.swift delete mode 100644 Happiggy-bank/Happiggy-bank/HomeTab/NewNote/UI/Controller/NewNoteTextViewController.swift delete mode 100644 Happiggy-bank/Happiggy-bank/HomeTab/NewNote/ViewModel/NewNoteTextViewModel.swift create mode 100644 Happiggy-bank/Happiggy-bank/Utils/Protocol/NewNoteSavingDelegate.swift diff --git a/Happiggy-bank/Happiggy-bank.xcodeproj/project.pbxproj b/Happiggy-bank/Happiggy-bank.xcodeproj/project.pbxproj index 887c3463..b15f8db5 100644 --- a/Happiggy-bank/Happiggy-bank.xcodeproj/project.pbxproj +++ b/Happiggy-bank/Happiggy-bank.xcodeproj/project.pbxproj @@ -68,6 +68,7 @@ A472C5F529C2DCEA00097432 /* NewNoteInputToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = A472C5F429C2DCEA00097432 /* NewNoteInputToolbar.swift */; }; A472C5F729C2DDFF00097432 /* NewNoteInputViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A472C5F629C2DDFF00097432 /* NewNoteInputViewModel.swift */; }; A472C5F929C2ED5000097432 /* HBError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A472C5F829C2ED5000097432 /* HBError.swift */; }; + A472C5FB29C2F21600097432 /* NewNoteSavingDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A472C5FA29C2F21600097432 /* NewNoteSavingDelegate.swift */; }; A47D83462966C0C60028AA1D /* NotificationSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D22ADF7E27F72F7300ECB77B /* NotificationSettingsViewModel.swift */; }; A47D83482966D3870028AA1D /* FontSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A47D83472966D3870028AA1D /* FontSelectionView.swift */; }; A47D834A296716BF0028AA1D /* FontCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A47D8349296716BF0028AA1D /* FontCell.swift */; }; @@ -117,7 +118,7 @@ A4A55A0928FFC675004ABE00 /* NewNoteInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4A55A0828FFC675004ABE00 /* NewNoteInputView.swift */; }; A4B285FD27D8A060008769EB /* Calendar+Duration.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4B285FC27D8A060008769EB /* Calendar+Duration.swift */; }; A4B2860527D9A546008769EB /* NewNoteDatePickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4B2860427D9A546008769EB /* NewNoteDatePickerViewController.swift */; }; - A4B2860927D9A56A008769EB /* NewNoteTextViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4B2860827D9A56A008769EB /* NewNoteTextViewController.swift */; }; + A4B2860927D9A56A008769EB /* NewNoteInputViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4B2860827D9A56A008769EB /* NewNoteInputViewController.swift */; }; A4B2860B27D9B434008769EB /* UIViewController+FadeInOut.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4B2860A27D9B434008769EB /* UIViewController+FadeInOut.swift */; }; A4B2860F27D9F539008769EB /* NewNoteDatePickerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4B2860E27D9F539008769EB /* NewNoteDatePickerViewModel.swift */; }; A4C1AFC027E477180096CD3E /* NSMutableAttributedString+ColorBold.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4C1AFBF27E477180096CD3E /* NSMutableAttributedString+ColorBold.swift */; }; @@ -125,7 +126,6 @@ A4C1AFC427E47DC50096CD3E /* String+NSMutableAttributedStringify.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4C1AFC327E47DC50096CD3E /* String+NSMutableAttributedStringify.swift */; }; A4C1AFC627E482E10096CD3E /* CGFloat+Values.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4C1AFC527E482E10096CD3E /* CGFloat+Values.swift */; }; A4C1AFCE27E4F8150096CD3E /* UIView+FadeInOut.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4C1AFCD27E4F8150096CD3E /* UIView+FadeInOut.swift */; }; - A4C1AFD227E5C60E0096CD3E /* NewNoteTextViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4C1AFD127E5C60E0096CD3E /* NewNoteTextViewModel.swift */; }; A4C1AFD427E5E6120096CD3E /* UITextView+ParagraphStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4C1AFD327E5E6120096CD3E /* UITextView+ParagraphStyle.swift */; }; A4CF2C7C27C71FF5001B01B1 /* CATransition+PopupAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4CF2C7B27C71FF5001B01B1 /* CATransition+PopupAnimation.swift */; }; A4CF2C8027C733FE001B01B1 /* ColorButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4CF2C7F27C733FE001B01B1 /* ColorButton.swift */; }; @@ -251,6 +251,7 @@ A472C5F429C2DCEA00097432 /* NewNoteInputToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewNoteInputToolbar.swift; sourceTree = ""; }; A472C5F629C2DDFF00097432 /* NewNoteInputViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewNoteInputViewModel.swift; sourceTree = ""; }; A472C5F829C2ED5000097432 /* HBError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HBError.swift; sourceTree = ""; }; + A472C5FA29C2F21600097432 /* NewNoteSavingDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewNoteSavingDelegate.swift; sourceTree = ""; }; A47D83472966D3870028AA1D /* FontSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontSelectionView.swift; sourceTree = ""; }; A47D8349296716BF0028AA1D /* FontCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontCell.swift; sourceTree = ""; }; A47D83532973A4860028AA1D /* FontPublishing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontPublishing.swift; sourceTree = ""; }; @@ -299,7 +300,7 @@ A4A55A0828FFC675004ABE00 /* NewNoteInputView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewNoteInputView.swift; sourceTree = ""; }; A4B285FC27D8A060008769EB /* Calendar+Duration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Calendar+Duration.swift"; sourceTree = ""; }; A4B2860427D9A546008769EB /* NewNoteDatePickerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewNoteDatePickerViewController.swift; sourceTree = ""; }; - A4B2860827D9A56A008769EB /* NewNoteTextViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewNoteTextViewController.swift; sourceTree = ""; }; + A4B2860827D9A56A008769EB /* NewNoteInputViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewNoteInputViewController.swift; sourceTree = ""; }; A4B2860A27D9B434008769EB /* UIViewController+FadeInOut.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+FadeInOut.swift"; sourceTree = ""; }; A4B2860E27D9F539008769EB /* NewNoteDatePickerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewNoteDatePickerViewModel.swift; sourceTree = ""; }; A4C1AFBF27E477180096CD3E /* NSMutableAttributedString+ColorBold.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSMutableAttributedString+ColorBold.swift"; sourceTree = ""; }; @@ -307,7 +308,6 @@ A4C1AFC327E47DC50096CD3E /* String+NSMutableAttributedStringify.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+NSMutableAttributedStringify.swift"; sourceTree = ""; }; A4C1AFC527E482E10096CD3E /* CGFloat+Values.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGFloat+Values.swift"; sourceTree = ""; }; A4C1AFCD27E4F8150096CD3E /* UIView+FadeInOut.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+FadeInOut.swift"; sourceTree = ""; }; - A4C1AFD127E5C60E0096CD3E /* NewNoteTextViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewNoteTextViewModel.swift; sourceTree = ""; }; A4C1AFD327E5E6120096CD3E /* UITextView+ParagraphStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITextView+ParagraphStyle.swift"; sourceTree = ""; }; A4CF2C7B27C71FF5001B01B1 /* CATransition+PopupAnimation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CATransition+PopupAnimation.swift"; sourceTree = ""; }; A4CF2C7F27C733FE001B01B1 /* ColorButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ColorButton.swift; path = "Happiggy-bank/HomeTab/NewNote/UI/View/ColorButton.swift"; sourceTree = SOURCE_ROOT; }; @@ -574,6 +574,7 @@ A4569CB7280FB979001E3FD6 /* Presenter.swift */, A4569CC528111EFA001E3FD6 /* InformationTextViewDataSource.swift */, A4EDFE7E2902D5CB0056C2DC /* ColorPickerDelegate.swift */, + A472C5FA29C2F21600097432 /* NewNoteSavingDelegate.swift */, ); path = Protocol; sourceTree = ""; @@ -796,7 +797,6 @@ isa = PBXGroup; children = ( A4B2860E27D9F539008769EB /* NewNoteDatePickerViewModel.swift */, - A4C1AFD127E5C60E0096CD3E /* NewNoteTextViewModel.swift */, A472C5F629C2DDFF00097432 /* NewNoteInputViewModel.swift */, ); path = ViewModel; @@ -815,7 +815,7 @@ isa = PBXGroup; children = ( A4B2860427D9A546008769EB /* NewNoteDatePickerViewController.swift */, - A4B2860827D9A56A008769EB /* NewNoteTextViewController.swift */, + A4B2860827D9A56A008769EB /* NewNoteInputViewController.swift */, ); path = Controller; sourceTree = ""; @@ -1383,7 +1383,6 @@ A467B5C827DA258700AC702D /* NewNoteDatePickerRowView.swift in Sources */, A484A3052958945E00A58312 /* BaseTextView.swift in Sources */, A484A327295EE22000A58312 /* String+SubstringsMatchingRegex.swift in Sources */, - A4C1AFD227E5C60E0096CD3E /* NewNoteTextViewModel.swift in Sources */, A819CFA127DE034F00DE8E72 /* NewBottle.swift in Sources */, A4569CBC2810455B001E3FD6 /* CustomerServiceViewController.swift in Sources */, A4EDFE812902D72A0056C2DC /* ColorPickerBarItem.swift in Sources */, @@ -1450,7 +1449,7 @@ A49AC5E52917C6E2009315BC /* UILabel+ChangeFontSize.swift in Sources */, A4A55A0928FFC675004ABE00 /* NewNoteInputView.swift in Sources */, A46B2E8329B5EBE6006A7870 /* NoteDetailListViewController.swift in Sources */, - A4B2860927D9A56A008769EB /* NewNoteTextViewController.swift in Sources */, + A4B2860927D9A56A008769EB /* NewNoteInputViewController.swift in Sources */, A843332127DA026D00A12A54 /* NewBottleDatePickerViewController.swift in Sources */, A4D6EB75282E2E6700553E43 /* VersionChecking.swift in Sources */, A8FC07CA27B3ECF00077A758 /* SceneDelegate.swift in Sources */, @@ -1458,6 +1457,7 @@ A466A31028018CD800D655F4 /* UIWindowScene+TopMostViewController.swift in Sources */, A4569CC428111E1D001E3FD6 /* LicenseViewModel.swift in Sources */, A466A30E28018BBD00D655F4 /* UIVIewController+TopMostViewController.swift in Sources */, + A472C5FB29C2F21600097432 /* NewNoteSavingDelegate.swift in Sources */, D2C48C0127E9DFA1006FC59E /* NoteView.swift in Sources */, A49B25E72812AC2800399630 /* FontSelectionViewController.swift in Sources */, A456657E27CC77A9007CF70A /* Date+Formatted.swift in Sources */, diff --git a/Happiggy-bank/Happiggy-bank/HomeTab/Home/UI/Controller/HomeTabViewController.swift b/Happiggy-bank/Happiggy-bank/HomeTab/Home/UI/Controller/HomeTabViewController.swift index b09ae705..7b55fa7c 100644 --- a/Happiggy-bank/Happiggy-bank/HomeTab/Home/UI/Controller/HomeTabViewController.swift +++ b/Happiggy-bank/Happiggy-bank/HomeTab/Home/UI/Controller/HomeTabViewController.swift @@ -92,9 +92,10 @@ final class HomeTabViewController: UIViewController { } if bottle.isEmtpyToday { - // NewNoteTextViewController + // NewNoteInputViewController + let viewModel = NewNoteInputViewModel(date: Date(), bottle: bottle) self.navigationController?.pushViewControllerWithFade( - to: UIViewController().then { $0.view.backgroundColor = .gray } + to: NewNoteInputViewController(viewModel: viewModel) ) } else { // NewNoteDatePickerViewController diff --git a/Happiggy-bank/Happiggy-bank/HomeTab/Home/UI/Controller/HomeViewController.swift b/Happiggy-bank/Happiggy-bank/HomeTab/Home/UI/Controller/HomeViewController.swift index c255c464..c16d3b4f 100644 --- a/Happiggy-bank/Happiggy-bank/HomeTab/Home/UI/Controller/HomeViewController.swift +++ b/Happiggy-bank/Happiggy-bank/HomeTab/Home/UI/Controller/HomeViewController.swift @@ -189,14 +189,7 @@ final class HomeViewController: UIViewController { let viewModel = NewNoteDatePickerViewModel(initialDate: Date(), bottle: bottle) dateViewController.viewModel = viewModel } - if segue.identifier == SegueIdentifier.presentNewNoteTextView { - guard let textViewController = segue.destination as? NewNoteTextViewController, - let bottle = self.viewModel.bottle - else { return } - - let viewModel = NewNoteTextViewModel(date: Date(), bottle: bottle) - textViewController.viewModel = viewModel - } + if segue.identifier == SegueIdentifier.presentBottleMessageView { guard let bottleMessageController = segue.destination as? BottleMessageViewController, let (fakeBackground, bottle) = sender as? (UIView, Bottle) diff --git a/Happiggy-bank/Happiggy-bank/HomeTab/NewNote/UI/Controller/NewNoteDatePickerViewController.swift b/Happiggy-bank/Happiggy-bank/HomeTab/NewNote/UI/Controller/NewNoteDatePickerViewController.swift index 22defff1..a940b848 100644 --- a/Happiggy-bank/Happiggy-bank/HomeTab/NewNote/UI/Controller/NewNoteDatePickerViewController.swift +++ b/Happiggy-bank/Happiggy-bank/HomeTab/NewNote/UI/Controller/NewNoteDatePickerViewController.swift @@ -232,12 +232,12 @@ final class NewNoteDatePickerViewController: UIViewController { override func prepare(for segue: UIStoryboardSegue, sender: Any?) { if segue.identifier == SegueIdentifier.presentNewNoteTextViewFromDatePicker { - - guard let textViewController = segue.destination as? NewNoteTextViewController - else { return } - - let viewModel = NewNoteTextViewModel(date: self.viewModel.selectedDate, bottle: self.viewModel.bottle) - textViewController.viewModel = viewModel +// +// guard let textViewController = segue.destination as? NewNoteTextViewController +// else { return } +// +// let viewModel = NewNoteTextViewModel(date: self.viewModel.selectedDate, bottle: self.viewModel.bottle) +// textViewController.viewModel = viewModel } } } diff --git a/Happiggy-bank/Happiggy-bank/HomeTab/NewNote/UI/Controller/NewNoteInputViewController.swift b/Happiggy-bank/Happiggy-bank/HomeTab/NewNote/UI/Controller/NewNoteInputViewController.swift new file mode 100644 index 00000000..0cea86c5 --- /dev/null +++ b/Happiggy-bank/Happiggy-bank/HomeTab/NewNote/UI/Controller/NewNoteInputViewController.swift @@ -0,0 +1,470 @@ +// +// NewNoteInputViewController.swift +// Happiggy-bank +// +// Created by sun on 2022/03/10. +// + +import PhotosUI +import UIKit + +import SnapKit +import Then + +/// 새로운 쪽지를 추가할 때 사용하는 뷰 컨트롤러 +/// 쪽지 추가 시 이를 알리기 위해 델리게이트 설정 필요 +final class NewNoteInputViewController: UIViewController { + + // MARK: - Properties + + weak var delegate: NewNoteSavingDelegate? + private let viewModel: NewNoteInputViewModel + private let noteInputView = NewNoteInputView() + /// 에러 로그 방지를 위해 임의의 초기값 설정 + private let toolbar = NewNoteInputToolbar(frame: .init(origin: .zero, size: .init(width: 1000, height: 50))) + private var showWarningLabel = false + private var noteInputViewBotttomConstraint: Constraint? + private var toolbarBottomConstraint: Constraint? + + + // MARK: - Init(s) + + init(viewModel: NewNoteInputViewModel) { + self.viewModel = viewModel + super.init(nibName: nil, bundle: nil) + + self.hidesBottomBarWhenPushed = true + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + + // MARK: - Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + + self.configureNavigationBar() + self.configureViews() + self.configureToolbar() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + self.noteInputView.textView.becomeFirstResponder() + self.updateCalendarButtonTitleText() + } + + + // MARK: - NavigationBar Configuration Functions + + private func configureNavigationBar() { + let cancelButton = UIBarButtonItem( + image: AssetImage.xmark, + primaryAction: .init { [weak self] _ in + self?.noteInputView.textView.endEditing(true) + self?.navigationController?.popToRootViewControllerWithFade() + } + ) + let saveButton = UIBarButtonItem( + image: AssetImage.checkmark, + primaryAction: .init { [weak self] _ in self?.saveButtonDidTap() } + ) + self.navigationItem.setLeftBarButton(cancelButton, animated: true) + self.navigationItem.setRightBarButton(saveButton, animated: true) + + self.navigationController?.navigationBar.standardAppearance.backgroundColor = .none + } + + private func saveButtonDidTap() { + let textView = self.noteInputView.textView + guard !textView.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + else { + HapticManager.instance.notification(type: .error) + self.showWarningLabel = true + self.noteInputView.warningLabel.fadeIn() + self.noteInputView.placeholderLabel.fadeOut() + return + } + + textView.endEditing(true) + self.present(makeConfirmationAlert(), animated: true) + } + + private func makeConfirmationAlert() -> UIAlertController { + let confirmAction = UIAlertAction.confirmAction(title: StringLiteral.confirmButtonTitle) { [weak self] _ in + guard let text = self?.noteInputView.textView.text, + let saveStatus = self?.viewModel.saveNote(withImage: self?.noteInputView.photo, text: text) + else { + return + } + + switch saveStatus { + case .success(let note): + self?.delegate?.newNoteDidSave(note) + self?.navigationController?.popToRootViewControllerWithFade() + case .failure(let error): + self?.presentSaveFailureAlert(withDescription: error.localizedDescription) + } + } + let cancelAction = UIAlertAction.cancelAction { [weak self] _ in + self?.noteInputView.textView.becomeFirstResponder() + } + + return .basic( + alertTitle: StringLiteral.saveConfirmationAlertTitle, + alertMessage: StringLiteral.saveConfirmationAlertMessage, + confirmAction: confirmAction, + cancelAction: cancelAction + ) + } + + private func presentSaveFailureAlert(withDescription description: String) { + let alert = UIAlertController.basic( + alertTitle: StringLiteral.saveFailureAlertTItle, + alertMessage: description, + preferredStyle: .alert, + confirmAction: .confirmAction() + ) + self.present(alert, animated: true) + } + + + // MARK: - View Configuration Functions + + private func configureViews() { + self.configureSubviews() + self.configureConstraints() + self.observeKeyboardAppearnce() + } + + private func configureSubviews() { + self.view.addSubview(self.noteInputView) + self.noteInputView.backgroundNoteImageView.addGestureRecognizer(UITapGestureRecognizer( + target: self, + action: #selector(backgroundDidTap(_:)) + )) + self.updateColor() + self.configureTextView() + self.configureCalendarButton() + self.updateLetterCountLabel(count: .zero) + self.noteInputView.removePhotoButton.addAction(UIAction(handler: { [weak self] _ in + self?.noteInputView.photo = nil + self?.viewModel.newNote.imageID = nil + self?.toolbar.photoButton.isEnabled = true + }), for: .touchUpInside) + } + + @objc private func backgroundDidTap(_ sender: UITapGestureRecognizer) { + self.noteInputView.textView.resignFirstResponder() + + self.toolbarBottomConstraint?.update(inset: Int.zero) + let toolbarHeight = self.toolbar.frame.height + + self.noteInputViewBotttomConstraint?.deactivate() + self.noteInputView.snp.makeConstraints { + self.noteInputViewBotttomConstraint = $0.bottom.equalTo(self.view.safeAreaLayoutGuide) + .inset(toolbarHeight) + .priority(.high) + .constraint + } + } + + private func updateColor() { + self.view.backgroundColor = self.viewModel.backgroundColor + self.noteInputView.backgroundNoteImageView.tintColor = self.viewModel.lineColor + self.noteInputView.calendarButton.tintColor = self.viewModel.textColor + self.noteInputView.letterCountLabel.textColor = self.viewModel.textColor + self.updateCalendarButtonTitleText() + } + + private func configureCalendarButton() { + let action = UIAction { [weak self] _ in + print("move to date select view controller ") + self?.navigationController?.pushViewControllerWithFade(to: UIViewController()) + self?.noteInputView.textView.resignFirstResponder() + } + self.noteInputView.calendarButton.addAction(action, for: .touchUpInside) + self.updateCalendarButtonTitleText() + } + + private func updateCalendarButtonTitleText() { + let calendarButton = self.noteInputView.calendarButton + let customFont = (calendarButton.customFont ?? .current) + let fontSize = calendarButton.titleLabel?.font.pointSize ?? FontSize.body3 + let font = UIFont(name: customFont.regular, size: fontSize) ?? .systemFont(ofSize: fontSize) + let boldFont = UIFont(name: customFont.bold, size: fontSize) ?? .boldSystemFont(ofSize: fontSize) + let title = " \(viewModel.yearString) \(viewModel.dateString)" + .nsMutableAttributedStringify() + .color(color: viewModel.textColor ?? .black) + .font(font) + .bold(font: boldFont, targetString: viewModel.yearString) + + self.noteInputView.calendarButton.setAttributedTitle(title, for: .normal) + } + + private func configureTextView() { + self.noteInputView.textView.delegate = self + self.noteInputView.textView.becomeFirstResponder() + } + + private func updateLetterCountLabel(count: Int) { + let label = self.noteInputView.letterCountLabel + let customFont = (label.customFont ?? .current) + let fontSize = label.font.pointSize + let font = UIFont(name: customFont.regular, size: fontSize) ?? .systemFont(ofSize: fontSize) + let boldFont = UIFont(name: customFont.bold, size: fontSize) ?? .boldSystemFont(ofSize: fontSize) + let isLongerThanLimit = self.noteInputView.textView.text.count > Metric.noteTextMaxLength + let countColor = isLongerThanLimit ? AssetColor.etcAlert : self.viewModel.textColor + + let title = "\(count) / \(Metric.noteTextMaxLength)" + .nsMutableAttributedStringify() + .color(color: self.viewModel.textColor ?? .black) + .color(targetString: count.description, color: countColor ?? .black) + .font(font) + .bold(font: boldFont, targetString: count.description) + + self.noteInputView.letterCountLabel.attributedText = title + } + + func attributedLetterCountString(count: Int) -> NSMutableAttributedString { + let label = self.noteInputView.letterCountLabel + let customFont = (label.customFont ?? .current) + let fontSize = label.font.pointSize + let font = UIFont(name: customFont.regular, size: fontSize) ?? .systemFont(ofSize: fontSize) + let boldFont = UIFont(name: customFont.bold, size: fontSize) ?? .boldSystemFont(ofSize: fontSize) + let color = viewModel.textColor ?? .black + + return "\(count) / \(Metric.noteTextMaxLength)" + .nsMutableAttributedStringify() + .color(color: color) + .font(font) + .bold(font: boldFont, targetString: count.description) + } + + private func configureConstraints() { + self.noteInputView.snp.makeConstraints { + $0.top.horizontalEdges.equalTo(self.view.safeAreaLayoutGuide) + self.noteInputViewBotttomConstraint = $0.bottom.equalTo(self.view.safeAreaLayoutGuide).constraint + } + } + + private func observeKeyboardAppearnce() { + NotificationCenter.default.addObserver( + forName: UIResponder.keyboardWillShowNotification, + object: nil, + queue: nil, + using: self.updateRelatedConstraints(notification:) + ) + } + + /// 키보드에 맞춰 noteInputView의 길이와 toolbar의 위치 변경 + private func updateRelatedConstraints(notification: Notification) { + guard let info = notification.userInfo, + let keyboardFrame = info[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect + else { return } + + let keyboardHeight = keyboardFrame.height - self.view.safeAreaInsets.bottom + self.toolbarBottomConstraint?.update(inset: keyboardHeight) + + let toolbarHeight = self.toolbar.frame.height + self.noteInputViewBotttomConstraint?.deactivate() + self.noteInputView.snp.makeConstraints { + self.noteInputViewBotttomConstraint = $0.bottom.equalTo(self.view.safeAreaLayoutGuide) + .inset(keyboardHeight + toolbarHeight) + .constraint + } + } + + + // MARK: - Toolbar Configuration Functions + + private func configureToolbar() { + self.view.addSubview(self.toolbar) + self.toolbar.snp.makeConstraints { + $0.horizontalEdges.equalTo(self.view.safeAreaLayoutGuide) + self.toolbarBottomConstraint = $0.bottom.equalTo(self.view.safeAreaLayoutGuide).constraint + } + self.toolbar.photoButton.addAction(UIAction { [weak self] _ in + self?.presentPhotoPicker() }, for: .touchUpInside) + self.toolbar.colorPicker.delegate = self + } + + private func presentPhotoPicker() { + var configuration = PHPickerConfiguration(photoLibrary: .shared()) + configuration.filter = .images + + let picker = PHPickerViewController(configuration: configuration) + picker.delegate = self + self.noteInputView.textView.resignFirstResponder() + self.present(picker, animated: true) + } +} + + +// MARK: - UITextViewDelegate +extension NewNoteInputViewController: UITextViewDelegate { + + func textViewDidEndEditing(_ textView: UITextView) { + + /// 100자 초과 시 초과분 삭제 + if textView.text.count > Metric.noteTextMaxLength { + textView.text = String(textView.text.prefix(Metric.noteTextMaxLength)) + self.updateLetterCountLabel(count: textView.text.count) + } + + /// 키보드 아래로 내리는 애니메이션 + textView.resignFirstResponder() + } + + func textView( + _ textView: UITextView, + shouldChangeTextIn range: NSRange, + replacementText text: String + ) -> Bool { + + /// 텍스트가 유효한지, 편집한 범위를 찾을 수 있는 지 확인 + guard let currentText = textView.text, + Range(range, in: currentText) != nil + else { return false } + + /// 경고 라벨이 나와 있으면 숨김처리 + if self.showWarningLabel { + self.showWarningLabel = false + self.noteInputView.warningLabel.fadeOut() + } + + let updatedTextLength = textView.text.count - range.length + text.count + let trimLength = updatedTextLength - Metric.krOverflowCap + + guard updatedTextLength > Metric.krOverflowCap, + text.count >= trimLength + else { + /// 내용이 빈 상태에서 백스페이스를 누르는 경우 + if textView.text.isEmpty, text.isEmpty { + self.noteInputView.placeholderLabel.fadeIn() + } + return true + } + + /// 새로 입력된 문자열의 초과분을 삭제 + let index = text.index(text.endIndex, offsetBy: -trimLength) + let trimmedReplacementText = text[.. Note { - Note.create( - id: self.viewModel.newNote.id, - date: self.viewModel.newNote.date, - color: self.viewModel.newNote.color, - content: self.newNoteInputView.textView.text, - imageURL: imageURL, - bottle: self.viewModel.newNote.bottle - ) - } - - /// 새로 생성한 노트 엔티티를 저장하고 성공 여부에 따라 불 리턴 - private func saveAndPostNewNote() -> Bool { - guard let (errorTitle, errorMessage) = PersistenceStore.shared.saveOld() - else { return true } - - let alert = PersistenceStore.shared.makeErrorAlert( - title: errorTitle, - message: errorMessage - ) { [weak self] _ in - self?.dismissWithAnimation() - } - self.present(alert, animated: true) - - return false - } - - /// 쪽지 저장 의사를 재확인 하는 알림을 띄움 - private func showNoteSavingConfirmationAlert() { - let alert = self.makeConfirmationAlert() - self.present(alert, animated: true) - } - - /// 쪽지 저장 의사를 재확인하는 알림 생성 - private func makeConfirmationAlert() -> UIAlertController { - let confirmAction = UIAlertAction.confirmAction( - title: StringLiteral.confirmButtonTitle - ) { [weak self] _ in - - var imageURL = String?.none - - if let image = self?.newNoteInputView.photo, - image != (.error ?? UIImage()) { - guard let url = self?.viewModel.saveImage(image) - else { - let alert = UIAlertController.basic( - alertTitle: "저장에 실패했습니다.", - preferredStyle: .alert, - confirmAction: .confirmAction() - ) - self?.present(alert, animated: true) - return - } - imageURL = url - } - - guard let note = self?.makeNewNote(withImageURL: imageURL) - else { - return - } - - guard self?.saveAndPostNewNote() == true - else { - PersistenceStore.shared.delete(note) - if let imageURL = note.imageURL { - self?.viewModel.deleteImage(withImageURL: imageURL) - } - return - } - - let noteAndDelay = (note: note, delay: CATransition.transitionDuration) - self?.post(name: .noteDidAdd, object: noteAndDelay) - self?.dismissWithAnimation() - } - - let cancelAction = UIAlertAction.cancelAction { [weak self] _ in - self?.newNoteInputView.textView.becomeFirstResponder() - } - - return UIAlertController.basic( - alertTitle: StringLiteral.alertTitle, - alertMessage: StringLiteral.message, - confirmAction: confirmAction, - cancelAction: cancelAction - ) - } - - /// 페이드아웃 효과와 함께 종료 - private func dismissWithAnimation() { - self.fadeOut() - self.performSegue( - withIdentifier: SegueIdentifier.unwindFromNoteTextViewToHomeView, - sender: self - ) - } - - - // MARK: - Photo Selecting Functions - - private func presentPhotoPicker() { - var configuration = PHPickerConfiguration(photoLibrary: .shared()) - configuration.filter = .images - - let picker = PHPickerViewController(configuration: configuration) - picker.delegate = self - self.newNoteInputView.textView.resignFirstResponder() - self.present(picker, animated: true) - } - - - - // MARK: - Navigation - - override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - if segue.identifier == SegueIdentifier.presentDatePickerFromNoteTextView { - guard let dateViewController = segue.destination as? NewNoteDatePickerViewController - else { return } - - let viewModel = NewNoteDatePickerViewModel( - initialDate: self.viewModel.newNote.date, - bottle: self.viewModel.newNote.bottle - ) - - dateViewController.viewModel = viewModel - dateViewController.isFromNoteTextView = true - } - } -} - - -// MARK: - UITextViewDelegate - -extension NewNoteTextViewController: UITextViewDelegate { - - func textViewDidEndEditing(_ textView: UITextView) { - - /// 100자 초과 시 초과분 삭제 - if textView.text.count > Metric.noteTextMaxLength { - textView.text = String(textView.text.prefix(Metric.noteTextMaxLength)) - self.updateLetterCountLabel(count: textView.text.count) - } - - /// 키보드 아래로 내리는 애니메이션 - textView.resignFirstResponder() - } - - func textView( - _ textView: UITextView, - shouldChangeTextIn range: NSRange, - replacementText text: String - ) -> Bool { - - /// 텍스트가 유효한지, 편집한 범위를 찾을 수 있는 지 확인 - guard let currentText = textView.text, - Range(range, in: currentText) != nil - else { return false } - - /// 경고 라벨이 나와 있으면 숨김처리 - if self.showWarningLabel { - self.showWarningLabel = false - self.newNoteInputView.warningLabel.fadeOut() - } - - var overflowCap = Metric.noteTextMaxLength - // if textView.textInputMode?.primaryLanguage == StringLiteral.korean { - // /// 한글의 경우 초성, 중성, 종성으로 이루어져 있어서 100자를 제대로 받기 위해 제한을 1글자 키움 - // overflowCap = Metric.krOverflowCap - // } - overflowCap = Metric.krOverflowCap - - let updatedTextLength = textView.text.count - range.length + text.count - let trimLength = updatedTextLength - overflowCap - - guard updatedTextLength > overflowCap, - text.count >= trimLength - else { - /// 내용이 빈 상태에서 백스페이스를 누르는 경우 - if textView.text.isEmpty, text.isEmpty { - self.newNoteInputView.placeholderLabel.fadeIn() - } - return true - } - - /// 새로 입력된 문자열의 초과분을 삭제 - let index = text.index(text.endIndex, offsetBy: -trimLength) - let trimmedReplacementText = text[.. NSMutableAttributedString { - let color = (count > NewNoteTextViewController.Metric.noteTextMaxLength) ? - UIColor.customWarningLabel : self.tintColor - - let countString = "\(count)" - .nsMutableAttributedStringify() - .bold() - .color(color: color) - - countString.append(StringLiteral.letterCountText.nsMutableAttributedStringify()) - - return countString - } - - /// 이미지를 저장하고, 성공한 경우 경로 엔드포인트를, 실패한 경우 nil 리턴 - func saveImage(_ image: UIImage) -> String? { - guard let imageID = newNote.imageID - else { - return nil - } - - return self.imageMananger.saveImage(image, noteID: newNote.id, imageID: imageID) - } - - /// 인자로 주어진 경로에 있는 이미지를 삭제 - /// - /// 삭제에 실패하는 경우 한 번 더 시도하고 리턴 - func deleteImage(withImageURL imageURL: String) { - guard !self.imageMananger.deleteImage(forNote: newNote.id, imageURL: imageURL) - else { - return - } - - self.imageMananger.deleteImage(forNote: newNote.id, imageURL: imageURL) - } -} diff --git a/Happiggy-bank/Happiggy-bank/Utils/Constants.swift b/Happiggy-bank/Happiggy-bank/Utils/Constants.swift index d1b254a9..6a88ba99 100644 --- a/Happiggy-bank/Happiggy-bank/Utils/Constants.swift +++ b/Happiggy-bank/Happiggy-bank/Utils/Constants.swift @@ -412,72 +412,6 @@ enum Asset: String { case settings } -extension NewNoteTextViewController { - - /// NewNoteTextViewController에서 사용하는 상수값 - enum Metric { - - /// 텍스트 뷰 컨테이너 인셋: (위: 16, 왼쪽: 24, 아래: 24, 오른쪽: 24) - static let textViewInset = UIEdgeInsets( - top: 16, - left: 24, - bottom: 24, - right: 24 - ) - - /// note 의 최대 작성 가능 길이 : 100 자 - static let noteTextMaxLength = 100 - - /// 한국 글자수 제한을 위한 오버플로우 cap 추가 값: 1 - static let krOverflowCap = noteTextMaxLength + 1 - - /// 애니메이션 지속 시간: 0.2 - static let animationDuration = CATransition.transitionDuration - - /// 내용 스택이 내비게이션바, safe area top inset, 키보드 크기를 제외한 나머지 영역을 다 차지할 수 있도록 높이를 계산해서 리턴 - static func contentStackHeight( - keyboardFrame: CGRect, - navigationBarFrame: CGRect - ) -> CGFloat { - let keyboardHeight = keyboardFrame.size.height - let navigationBarHeight = navigationBarFrame.size.height - let screenHeight = UIScreen.main.bounds.height - let safeAreaTopInset = navigationBarFrame.origin.y - let removingHeight = safeAreaTopInset + navigationBarHeight + keyboardHeight - - return screenHeight - removingHeight - } - - /// 컬러 버튼 컨테이너 뷰 페이드 인 지속 시간: 0.1 - static let colorButtonContainerViewFadeInDuration: TimeInterval = 0.1 - - /// 컬러 버튼 컨테이너 뷰 페이드 아웃 지속 시간: 0.1 - static let colorButtonContainerViewFadeOutDuration: TimeInterval = 0.1 - } - - /// NewNoteTextViewController 에서 설정하는 제목들 - enum StringLiteral { - - /// 키보드 언어 설정이 한글인 경우 - static let korean = "ko-KR" - - /// 저장 확인 알림 제목 - static let alertTitle = "쪽지를 추가하시겠어요?" - - /// 알림 내용 - static let message = """ -쪽지는 하루에 한 번 작성할 수 있고, -추가 후에는 수정/삭제가 불가능합니다 -""" - - /// 취소 버튼 제목: "취소" - static let cancelButtonTitle = "취소" - - /// 확인 버튼 제목: "추가" - static let confirmButtonTitle = "추가" - } -} - extension NewNoteDatePickerViewModel { /// NoteNoteDatePickerViewModel 에서 지정하는 폰트 크기 @@ -488,26 +422,6 @@ extension NewNoteDatePickerViewModel { } } -extension NewNoteTextViewModel { - - /// NewNoteTextViewModel 에서 사용하는 문자열 - enum StringLiteral { - - /// 글자수 라벨 텍스트를 반환 - static let letterCountText = " / \(NewNoteTextViewController.Metric.noteTextMaxLength)" - - /// 날짜 레이블 간격 - static let spacing = " " - } - - /// NewNoteTextViewModel 에서 사용하는 폰트 크기 - enum Font { - - /// 날짜 버튼과 글자수 라벨 폰트 크기: 15 - static let secondaryText: CGFloat = 15 - } -} - extension SettingsViewCell { /// 상수값 diff --git a/Happiggy-bank/Happiggy-bank/Utils/Protocol/NewNoteSavingDelegate.swift b/Happiggy-bank/Happiggy-bank/Utils/Protocol/NewNoteSavingDelegate.swift new file mode 100644 index 00000000..b7b3e0fe --- /dev/null +++ b/Happiggy-bank/Happiggy-bank/Utils/Protocol/NewNoteSavingDelegate.swift @@ -0,0 +1,15 @@ +// +// NewNoteSavingDelegate.swift +// Happiggy-bank +// +// Created by sun on 2023/03/16. +// + +import Foundation + +/// 새로운 쪽지를 추가했을 때 이를 알림받아야 하는 클래스가 채택하는 프로토콜 +protocol NewNoteSavingDelegate: AnyObject { + + /// 새로운 쪽지가 추가되었음을 전달 + func newNoteDidSave(_ note: Note) +}