From 381677f4b67b555807f2e51d2a6783d8c78f6a84 Mon Sep 17 00:00:00 2001 From: Jaewon Yun Date: Wed, 7 Feb 2024 12:45:30 +0900 Subject: [PATCH 01/24] Make launch arguments verifiable --- .../UserSettingsRepositoryAssemblyDev.swift | 6 ++- .../DI/WordRepositoryAssemblyDev.swift | 10 +++-- Sources/Utility/LaunchArgument.swift | 45 +++++++++++++++++++ Sources/Utility/LaunchArguments.swift | 18 -------- .../XCUIApplication+launchEnvironment.swift | 2 +- 5 files changed, 56 insertions(+), 25 deletions(-) create mode 100644 Sources/Utility/LaunchArgument.swift delete mode 100644 Sources/Utility/LaunchArguments.swift diff --git a/Sources/Infrastructure/DI/UserSettingsRepositoryAssemblyDev.swift b/Sources/Infrastructure/DI/UserSettingsRepositoryAssemblyDev.swift index 8636b424..8f84e304 100644 --- a/Sources/Infrastructure/DI/UserSettingsRepositoryAssemblyDev.swift +++ b/Sources/Infrastructure/DI/UserSettingsRepositoryAssemblyDev.swift @@ -17,8 +17,10 @@ final class UserSettingsRepositoryAssemblyDev: Assembly { func assemble(container: Container) { container.register(UserSettingsRepositoryProtocol.self) { _ in let userDefaults: ExtendedUserDefaults = .init(suiteName: "Dev")! - let arguments = ProcessInfo.processInfo.arguments - if arguments.contains(LaunchArguments.initUserDefaults.rawValue) { + + LaunchArgument.verify() + + if LaunchArgument.contains(.initUserDefaults) { userDefaults.removeAllObject(forKeyType: UserDefaultsKey.self) } diff --git a/Sources/Infrastructure/DI/WordRepositoryAssemblyDev.swift b/Sources/Infrastructure/DI/WordRepositoryAssemblyDev.swift index ae185c13..303a5af9 100644 --- a/Sources/Infrastructure/DI/WordRepositoryAssemblyDev.swift +++ b/Sources/Infrastructure/DI/WordRepositoryAssemblyDev.swift @@ -16,14 +16,16 @@ final class WordRepositoryAssemblyDev: Assembly { func assemble(container: Container) { container.register(WordRepositoryProtocol.self) { _ in - let arguments = ProcessInfo.processInfo.arguments - // TODO: Insert [LaunchArguments 검증 code](상호배타적인것들) - if arguments.contains(LaunchArguments.useInMemoryDatabase.rawValue) { + LaunchArgument.verify() + + if LaunchArgument.contains(.useInMemoryDatabase) { return self.makeInMemoryWordRepository() } - if arguments.contains(LaunchArguments.sampledDatabase.rawValue) { + + if LaunchArgument.contains(.sampledDatabase) { return self.makeSampledWordRepository() } + return self.makePersistenceWordRepository() } .inObjectScope(.container) diff --git a/Sources/Utility/LaunchArgument.swift b/Sources/Utility/LaunchArgument.swift new file mode 100644 index 00000000..ceda59b8 --- /dev/null +++ b/Sources/Utility/LaunchArgument.swift @@ -0,0 +1,45 @@ +// +// LaunchArgument.swift +// WordChecker +// +// Created by Jaewon Yun on 2023/08/27. +// + +import Foundation + +public final class LaunchArgument { + + private init() {} + + /// 사용 가능한 launch argument 목록입니다. + public enum Arguments: String { + + /// 인메모리 데이터베이스를 사용합니다. + case useInMemoryDatabase = "-useInMemoryDatabase" + + /// 샘플 데이터를 적용합니다. + case sampledDatabase = "-sampledDatabase" + + /// UserDefaults 를 초기화합니다. + case initUserDefaults = "-initUserDefaults" + + } + + public static func contains(_ launchArguments: Arguments) -> Bool { + let arguments = ProcessInfo.processInfo.arguments + return arguments.contains(launchArguments.rawValue) + } + + public static func contains(_ launchArguments: String) -> Bool { + let arguments = ProcessInfo.processInfo.arguments + return arguments.contains(launchArguments) + } + + /// Launch arguments 가 올바르게 적용되었는지 검증합니다. + public static func verify() { + if self.contains(.useInMemoryDatabase) && self.contains(.sampledDatabase) { + fatalError("\(Arguments.useInMemoryDatabase) and \(Arguments.sampledDatabase) should not be used together.") + } + } + +} diff --git a/Sources/Utility/LaunchArguments.swift b/Sources/Utility/LaunchArguments.swift deleted file mode 100644 index 0343eea9..00000000 --- a/Sources/Utility/LaunchArguments.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// LaunchArguments.swift -// WordChecker -// -// Created by Jaewon Yun on 2023/08/27. -// - -import Foundation - -public enum LaunchArguments: String { - - case useInMemoryDatabase = "-useInMemoryDatabase" - - case sampledDatabase = "-sampledDatabase" - - case initUserDefaults = "-initUserDefaults" - -} diff --git a/Tests/WordCheckerUITests/Utilities/XCUIApplication+launchEnvironment.swift b/Tests/WordCheckerUITests/Utilities/XCUIApplication+launchEnvironment.swift index 9de20e5e..efa367c0 100644 --- a/Tests/WordCheckerUITests/Utilities/XCUIApplication+launchEnvironment.swift +++ b/Tests/WordCheckerUITests/Utilities/XCUIApplication+launchEnvironment.swift @@ -11,7 +11,7 @@ import XCTest extension XCUIApplication { - func setLaunchArguments(_ arguments: [LaunchArguments]) { + func setLaunchArguments(_ arguments: [LaunchArgument.Arguments]) { self.launchArguments = arguments.map(\.rawValue) } From ef27127b7f9a865d8b9ea208c1a5eda614999a09 Mon Sep 17 00:00:00 2001 From: Jaewon Yun Date: Wed, 7 Feb 2024 13:14:24 +0900 Subject: [PATCH 02/24] Fix typo of file name --- .../{iPhoneCoorinator.stencil => iPhoneCoordinator.stencil} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Tuist/Templates/{iPhoneCoorinator.stencil => iPhoneCoordinator.stencil} (100%) diff --git a/Tuist/Templates/iPhoneCoorinator.stencil b/Tuist/Templates/iPhoneCoordinator.stencil similarity index 100% rename from Tuist/Templates/iPhoneCoorinator.stencil rename to Tuist/Templates/iPhoneCoordinator.stencil From 35bb5372b13d7d20dd2e2e6e1097b526dad87f74 Mon Sep 17 00:00:00 2001 From: Jaewon Yun Date: Wed, 7 Feb 2024 13:26:21 +0900 Subject: [PATCH 03/24] Add ThemeSetting module --- Project.swift | 24 +++++++++++ .../ThemeSetting/ThemeSettingAssembly.swift | 21 ++++++++++ .../ThemeSetting/ThemeSettingReactor.swift | 35 ++++++++++++++++ .../ThemeSettingViewController.swift | 22 ++++++++++ .../ThemeSettingCoordinator.swift | 41 +++++++++++++++++++ TestPlans/ThemeSetting.xctestplan | 12 ++++++ .../ThemeSettingTests/ThemeSettingTests.swift | 28 +++++++++++++ 7 files changed, 183 insertions(+) create mode 100644 Sources/iOSScenes/ThemeSetting/ThemeSettingAssembly.swift create mode 100644 Sources/iOSScenes/ThemeSetting/ThemeSettingReactor.swift create mode 100644 Sources/iOSScenes/ThemeSetting/ThemeSettingViewController.swift create mode 100644 Sources/iPhoneDriver/Coordinators/ThemeSettingCoordinator.swift create mode 100644 TestPlans/ThemeSetting.xctestplan create mode 100644 Tests/iOSScenesTests/ThemeSettingTests/ThemeSettingTests.swift diff --git a/Project.swift b/Project.swift index f51d686f..f5f71aa0 100644 --- a/Project.swift +++ b/Project.swift @@ -284,6 +284,29 @@ func targets() -> [Target] { ], appendSchemeTo: &schemes ) + + Target.module( + name: "ThemeSetting", + sourcesPrefix: "iOSScenes", + resourceOptions: [.additional("Resources/iOSSupport/**")], + dependencies: [ + .target(name: "Domain"), + .target(name: "iOSSupport"), + .external(name: ExternalDependencyName.rxSwift), + .external(name: ExternalDependencyName.rxCocoa), + .external(name: ExternalDependencyName.rxUtilityDynamic), + .external(name: ExternalDependencyName.reactorKit), + .external(name: ExternalDependencyName.snapKit), + .external(name: ExternalDependencyName.then), + .external(name: ExternalDependencyName.swinject), + .external(name: ExternalDependencyName.swinjectExtension), + ], + hasTests: true, + additionalTestDependencies: [ + .target(name: "DomainTesting"), + .external(name: ExternalDependencyName.rxBlocking), + ], + appendSchemeTo: &schemes + ) + Target.module( name: "PushNotificationSettings", sourcesPrefix: "iOSScenes", @@ -348,6 +371,7 @@ func targets() -> [Target] { .target(name: "PushNotificationSettings"), .target(name: "GeneralSettings"), .target(name: "Infrastructure"), + .target(name: "ThemeSetting"), .external(name: ExternalDependencyName.swinject), .external(name: ExternalDependencyName.swinjectDIContainer), .external(name: ExternalDependencyName.sfSafeSymbols), diff --git a/Sources/iOSScenes/ThemeSetting/ThemeSettingAssembly.swift b/Sources/iOSScenes/ThemeSetting/ThemeSettingAssembly.swift new file mode 100644 index 00000000..be28aae4 --- /dev/null +++ b/Sources/iOSScenes/ThemeSetting/ThemeSettingAssembly.swift @@ -0,0 +1,21 @@ +import Swinject +import SwinjectExtension +import Then + +public final class ThemeSettingAssembly: Assembly { + + public init() {} + + public func assemble(container: Container) { + container.register(ThemeSettingReactor.self) { _ in + return ThemeSettingReactor.init() + } + + container.register(ThemeSettingViewControllerProtocol.self) { resolver in + return ThemeSettingViewController().then { + $0.reactor = resolver.resolve() + } + } + } + +} diff --git a/Sources/iOSScenes/ThemeSetting/ThemeSettingReactor.swift b/Sources/iOSScenes/ThemeSetting/ThemeSettingReactor.swift new file mode 100644 index 00000000..0a2bc07d --- /dev/null +++ b/Sources/iOSScenes/ThemeSetting/ThemeSettingReactor.swift @@ -0,0 +1,35 @@ +import ReactorKit + +final class ThemeSettingReactor: Reactor { + + enum Action { + + } + + enum Mutation { + + } + + struct State { + + } + + var initialState: State = .init() + + func mutate(action: Action) -> Observable { + switch action { + + } + } + + func reduce(state: State, mutation: Mutation) -> State { + var state = state + + switch mutation { + + } + + return state + } + +} diff --git a/Sources/iOSScenes/ThemeSetting/ThemeSettingViewController.swift b/Sources/iOSScenes/ThemeSetting/ThemeSettingViewController.swift new file mode 100644 index 00000000..712c32fe --- /dev/null +++ b/Sources/iOSScenes/ThemeSetting/ThemeSettingViewController.swift @@ -0,0 +1,22 @@ +import iOSSupport +import ReactorKit +import UIKit + +public protocol ThemeSettingViewControllerDelegate: AnyObject { + // An example if you use navigation stack. + func willPopView() +} + +public protocol ThemeSettingViewControllerProtocol: UIViewController { + var delegate: ThemeSettingViewControllerDelegate? { get set } +} + +final class ThemeSettingViewController: RxBaseViewController, View, ThemeSettingViewControllerProtocol { + + weak var delegate: ThemeSettingViewControllerDelegate? + + func bind(reactor: ThemeSettingReactor) { + + } + +} diff --git a/Sources/iPhoneDriver/Coordinators/ThemeSettingCoordinator.swift b/Sources/iPhoneDriver/Coordinators/ThemeSettingCoordinator.swift new file mode 100644 index 00000000..f23ebd17 --- /dev/null +++ b/Sources/iPhoneDriver/Coordinators/ThemeSettingCoordinator.swift @@ -0,0 +1,41 @@ +import iOSSupport +import SwinjectDIContainer +import SwinjectExtension +import UIKit +import ThemeSetting + +final class ThemeSettingCoordinator: Coordinator { + + weak var parentCoordinator: Coordinator? + var childCoordinators: [Coordinator] = [] + + let navigationController: UINavigationController + + init(navigationController: UINavigationController) { + self.navigationController = navigationController + } + + func start() { + // An example if you use navigation stack. + let viewController: ThemeSettingViewControllerProtocol = DIContainer.shared.resolver.resolve() + viewController.delegate = self + navigationController.pushViewController(viewController, animated: true) + } + + /* If you want start to coordinator with arguments. + func start(with argument: Arg1) { + + } + */ + +} + +extension ThemeSettingCoordinator: ThemeSettingViewControllerDelegate { + + // An example if you use navigation stack. + func willPopView() { + navigationController.popViewController(animated: true) + parentCoordinator?.childCoordinators.removeAll(where: { $0 === self }) + } + +} diff --git a/TestPlans/ThemeSetting.xctestplan b/TestPlans/ThemeSetting.xctestplan new file mode 100644 index 00000000..bff262de --- /dev/null +++ b/TestPlans/ThemeSetting.xctestplan @@ -0,0 +1,12 @@ +{ + "configurations" : [ + + ], + "defaultOptions" : { + "testTimeoutsEnabled" : true + }, + "testTargets" : [ + + ], + "version" : 1 +} diff --git a/Tests/iOSScenesTests/ThemeSettingTests/ThemeSettingTests.swift b/Tests/iOSScenesTests/ThemeSettingTests/ThemeSettingTests.swift new file mode 100644 index 00000000..52801d12 --- /dev/null +++ b/Tests/iOSScenesTests/ThemeSettingTests/ThemeSettingTests.swift @@ -0,0 +1,28 @@ +@testable import ThemeSetting + +import XCTest + +final class ThemeSettingTests: XCTestCase { + + var sut: ThemeSettingReactor! + + override func setUpWithError() throws { + try super.setUpWithError() + sut + } + + override func tearDownWithError() throws { + try super.tearDownWithError() + sut = nil + } + + func test_example() { + // Given + + // When + + // Then + XCTAssertEqual("ThemeSettingKit", "ThemeSettingKit") + } + +} From 772abff18cbf84e96472d3edca9d132b429ab4cc Mon Sep 17 00:00:00 2001 From: Jaewon Yun Date: Wed, 7 Feb 2024 13:26:56 +0900 Subject: [PATCH 04/24] Update tuist scaffold templates --- Tuist/Templates/Assembly.stencil | 4 ++++ Tuist/Templates/Reactor.stencil | 6 +++--- Tuist/Templates/UnitTests.stencil | 12 +++++------- Tuist/Templates/iPhoneCoordinator.stencil | 4 ++-- 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/Tuist/Templates/Assembly.stencil b/Tuist/Templates/Assembly.stencil index 6aec4c96..625e07a0 100644 --- a/Tuist/Templates/Assembly.stencil +++ b/Tuist/Templates/Assembly.stencil @@ -7,6 +7,10 @@ public final class {{ name }}Assembly: Assembly { public init() {} public func assemble(container: Container) { + container.register({{ name }}Reactor.self) { _ in + return {{ name }}Reactor.init() + } + container.register({{ name }}ViewControllerProtocol.self) { resolver in return {{ name }}ViewController().then { $0.reactor = resolver.resolve() diff --git a/Tuist/Templates/Reactor.stencil b/Tuist/Templates/Reactor.stencil index 26bc13be..4ab37f40 100644 --- a/Tuist/Templates/Reactor.stencil +++ b/Tuist/Templates/Reactor.stencil @@ -18,15 +18,15 @@ final class {{ name }}Reactor: Reactor { func mutate(action: Action) -> Observable { switch action { - + } } func reduce(state: State, mutation: Mutation) -> State { var state = state - + switch mutation { - + } return state diff --git a/Tuist/Templates/UnitTests.stencil b/Tuist/Templates/UnitTests.stencil index be775c69..b369e3b9 100644 --- a/Tuist/Templates/UnitTests.stencil +++ b/Tuist/Templates/UnitTests.stencil @@ -4,7 +4,7 @@ import XCTest final class {{ name }}Tests: XCTestCase { - var sut: <#Code#>! + var sut: {{ name }}Reactor! override func setUpWithError() throws { try super.setUpWithError() @@ -15,16 +15,14 @@ final class {{ name }}Tests: XCTestCase { try super.tearDownWithError() sut = nil } - + func test_example() { // Given - - + // When - - + // Then XCTAssertEqual("{{ name }}Kit", "{{ name }}Kit") } - + } diff --git a/Tuist/Templates/iPhoneCoordinator.stencil b/Tuist/Templates/iPhoneCoordinator.stencil index e5564921..df44a5b6 100644 --- a/Tuist/Templates/iPhoneCoordinator.stencil +++ b/Tuist/Templates/iPhoneCoordinator.stencil @@ -21,10 +21,10 @@ final class {{ name }}Coordinator: Coordinator { viewController.delegate = self navigationController.pushViewController(viewController, animated: true) } - + /* If you want start to coordinator with arguments. func start(with argument: Arg1) { - + } */ From a7e76e64d71528581fc17d1d2d066666f140c6ce Mon Sep 17 00:00:00 2001 From: Jaewon Yun Date: Wed, 7 Feb 2024 13:41:10 +0900 Subject: [PATCH 05/24] Make method call timing clarity --- .../LanguageSetting/LanguageSettingViewController.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/iOSScenes/LanguageSetting/LanguageSettingViewController.swift b/Sources/iOSScenes/LanguageSetting/LanguageSettingViewController.swift index 4113d44f..7dc4af1f 100644 --- a/Sources/iOSScenes/LanguageSetting/LanguageSettingViewController.swift +++ b/Sources/iOSScenes/LanguageSetting/LanguageSettingViewController.swift @@ -95,8 +95,8 @@ final class LanguageSettingViewController: RxBaseViewController, LanguageSetting applyInitialSnapshotIfNoSections() } - override func viewDidDisappear(_ animated: Bool) { - super.viewDidDisappear(animated) + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) if self.isMovingFromParent { delegate?.viewMustPop() From 3857da91bc75b0278c6ccc7a928a6b68b02bd96b Mon Sep 17 00:00:00 2001 From: Jaewon Yun Date: Wed, 7 Feb 2024 13:54:13 +0900 Subject: [PATCH 06/24] Remove dead code --- .../iOSScenes/UserSettings/UserSettingsViewController.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Sources/iOSScenes/UserSettings/UserSettingsViewController.swift b/Sources/iOSScenes/UserSettings/UserSettingsViewController.swift index 274abdb8..914d19ca 100644 --- a/Sources/iOSScenes/UserSettings/UserSettingsViewController.swift +++ b/Sources/iOSScenes/UserSettings/UserSettingsViewController.swift @@ -110,10 +110,6 @@ final class UserSettingsViewController: RxBaseViewController, View, UserSettings } override func bindAction() { - guard let reactor = self.reactor else { - preconditionFailure("After initialization, reactor is not assigned.") - } - let itemSelectedEvent = settingsTableView.rx.itemSelected.asSignal() .doOnNext { [weak self] in self?.settingsTableView.deselectRow(at: $0, animated: true) } From 8a10606d463e85e98e6f5532d0e9b3af3eec55ce Mon Sep 17 00:00:00 2001 From: Jaewon Yun Date: Wed, 7 Feb 2024 14:01:59 +0900 Subject: [PATCH 07/24] Fix typo --- Resources/iOSSupport/Localization/en.lproj/Localizable.strings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Resources/iOSSupport/Localization/en.lproj/Localizable.strings b/Resources/iOSSupport/Localization/en.lproj/Localizable.strings index 301a3fc5..4a9910c3 100644 --- a/Resources/iOSSupport/Localization/en.lproj/Localizable.strings +++ b/Resources/iOSSupport/Localization/en.lproj/Localizable.strings @@ -66,7 +66,7 @@ dailyReminderFooter = "Sends a daily push notification at the time you set."; general = "General"; haptics = "Haptics"; hapticsSettingsFooterTextWhenHapticsIsOn = "Enable haptics for interactions."; -hapticsSettingsFooterTextWhenHapticsIsOff = "Disable haptics for interactions"; +hapticsSettingsFooterTextWhenHapticsIsOff = "Disable haptics for interactions."; more_menu = "More menu"; memorize_words = "Memorize words"; From a56040ebe994a5373d64030e974740567f0c1df7 Mon Sep 17 00:00:00 2001 From: Jaewon Yun Date: Wed, 7 Feb 2024 14:16:49 +0900 Subject: [PATCH 08/24] Enhance clarity of logic --- .../LanguageSettingViewController.swift | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/Sources/iOSScenes/LanguageSetting/LanguageSettingViewController.swift b/Sources/iOSScenes/LanguageSetting/LanguageSettingViewController.swift index 7dc4af1f..6c633aee 100644 --- a/Sources/iOSScenes/LanguageSetting/LanguageSettingViewController.swift +++ b/Sources/iOSScenes/LanguageSetting/LanguageSettingViewController.swift @@ -103,8 +103,8 @@ final class LanguageSettingViewController: RxBaseViewController, LanguageSetting } } - /// 현재 Snapshot 에 추가된 Section 이 없다면 초기 snapshot 을 적용합니다. - /// - Returns: 현재 Snapshot 에 추가된 Section 이 있다면 현재 snapshot 을 반환, 없다면 새로 적용한 snapshot 을 반환합니다. + /// 현재 DataSource 에 적용된 Snapshot 이 없다면 초기 Snapshot 을 적용합니다. + /// - Returns: 현재 DataSource 에 적용된 Snapshot 이 있다면 해당 Snapshot 을 반환, 없다면 새로 적용한 Snapshot 을 반환합니다. @discardableResult func applyInitialSnapshotIfNoSections() -> NSDiffableDataSourceSnapshot { let currenetSnapshot = dataSource.snapshot() @@ -144,13 +144,14 @@ final class LanguageSettingViewController: RxBaseViewController, LanguageSetting var snapshot = owner.dataSource.snapshot() snapshot = owner.applyInitialSnapshotIfNoSections() - guard let sectionIdentifier = SectionIdentifier.init(rawValue: selectedIndexPath.section) else { + guard + let selectedSectionIdentifier = SectionIdentifier.init(rawValue: selectedIndexPath.section), + let selectedItemIdentifier = owner.dataSource.itemIdentifier(for: selectedIndexPath) + else { assertionFailure("Out of selectable cell.") return } - let selectedItemIdentifier = snapshot.itemIdentifiers(inSection: sectionIdentifier)[selectedIndexPath.row] - if case .language = selectedItemIdentifier { owner.itemModels = owner.defaultItemModels var selectedItem = owner.itemModels[selectedItemIdentifier] @@ -158,7 +159,7 @@ final class LanguageSettingViewController: RxBaseViewController, LanguageSetting owner.itemModels[selectedItemIdentifier] = selectedItem } - snapshot.reconfigureItems(snapshot.itemIdentifiers(inSection: sectionIdentifier)) + snapshot.reconfigureItems(snapshot.itemIdentifiers(inSection: selectedSectionIdentifier)) owner.dataSource.apply(snapshot) } .disposed(by: self.disposeBag) From ca1c43c4925523c3555c75959f59b1e3cfd2c7cd Mon Sep 17 00:00:00 2001 From: Jaewon Yun Date: Wed, 7 Feb 2024 17:28:08 +0900 Subject: [PATCH 09/24] Implement ThemeSetting module --- Changelog/next.md | 3 +- Project.swift | 5 +- .../Localization/en.lproj/Localizable.strings | 5 + .../Localization/ko.lproj/Localizable.strings | 5 + .../UserSettingsUseCaseProtocol.swift | 2 + .../Domain/UseCases/UserSettingsUseCase.swift | 14 +- Sources/Domain/ValueObjects/ThemeStyle.swift | 21 +++ .../Domain/ValueObjects/UserSettings.swift | 6 +- .../UserSettingsUseCaseFake.swift | 8 +- .../DomainImpl/UserSettingsRepository.swift | 11 +- .../UserDefaults/UserDefaultsKey.swift | 2 + Sources/WordCheckerDev/AppDelegate.swift | 2 + .../GeneralSettingsViewController.swift | 27 +++- .../AppDelegate.swift | 2 +- .../ThemeSetting/ThemeSettingAssembly.swift | 7 +- .../ThemeSetting/ThemeSettingReactor.swift | 38 ++++- .../ThemeSettingViewController.swift | 133 +++++++++++++++++- .../UserSettingsItemModel.swift | 2 +- .../Common/Domain.ThemeStyle+mapping.swift | 43 ++++++ Sources/iOSSupport/Common/GlobalState.swift | 9 +- .../iOSSupport/Localization/WCString.swift | 5 + .../UIKitExtension}/Cells/ButtonCell.swift | 12 +- .../Cells/DisclosureIndicatorCell.swift | 12 +- .../GeneralSettingsCoordinator.swift | 7 + .../ThemeSettingCoordinator.swift | 8 -- Sources/iPhoneDriver/SceneDelegate.swift | 27 +++- Sources/iPhoneDriver/iPhoneAppDelegate.swift | 23 +-- .../UserSettingsRepositoryTests.swift | 6 +- .../UserSettingsReactorTests.swift | 2 +- 29 files changed, 391 insertions(+), 56 deletions(-) create mode 100644 Sources/Domain/ValueObjects/ThemeStyle.swift create mode 100644 Sources/iOSSupport/Common/Domain.ThemeStyle+mapping.swift rename Sources/{iOSScenes/UserSettings => iOSSupport/UIKitExtension}/Cells/ButtonCell.swift (75%) rename Sources/{iOSScenes/UserSettings => iOSSupport/UIKitExtension}/Cells/DisclosureIndicatorCell.swift (72%) diff --git a/Changelog/next.md b/Changelog/next.md index e666edce..1a1f33c4 100644 --- a/Changelog/next.md +++ b/Changelog/next.md @@ -1,3 +1,4 @@ -## Enhancements +## Added +- Added to change theme feature. ## Fixed diff --git a/Project.swift b/Project.swift index f5f71aa0..4a1e65c6 100644 --- a/Project.swift +++ b/Project.swift @@ -102,8 +102,9 @@ func targets() -> [Target] { .external(name: ExternalDependencyName.reactorKit), .external(name: ExternalDependencyName.snapKit), .external(name: ExternalDependencyName.then), + .package(product: ExternalDependencyName.swiftCollections), ], - appendSchemeTo: &disposedSchemes + appendSchemeTo: &schemes ) + Target.module( name: "WordChecking", @@ -274,7 +275,6 @@ func targets() -> [Target] { .external(name: ExternalDependencyName.toast), .external(name: ExternalDependencyName.swinject), .external(name: ExternalDependencyName.swinjectExtension), - .package(product: ExternalDependencyName.swiftCollections), ], hasTests: true, additionalTestDependencies: [ @@ -291,6 +291,7 @@ func targets() -> [Target] { dependencies: [ .target(name: "Domain"), .target(name: "iOSSupport"), + .target(name: "FoundationExtension"), .external(name: ExternalDependencyName.rxSwift), .external(name: ExternalDependencyName.rxCocoa), .external(name: ExternalDependencyName.rxUtilityDynamic), diff --git a/Resources/iOSSupport/Localization/en.lproj/Localizable.strings b/Resources/iOSSupport/Localization/en.lproj/Localizable.strings index 4a9910c3..00de3e20 100644 --- a/Resources/iOSSupport/Localization/en.lproj/Localizable.strings +++ b/Resources/iOSSupport/Localization/en.lproj/Localizable.strings @@ -68,6 +68,11 @@ haptics = "Haptics"; hapticsSettingsFooterTextWhenHapticsIsOn = "Enable haptics for interactions."; hapticsSettingsFooterTextWhenHapticsIsOff = "Disable haptics for interactions."; +theme = "Theme"; +system_mode = "System mode"; +light_mode = "Light mode"; +dark_mode = "Dark mode"; + more_menu = "More menu"; memorize_words = "Memorize words"; next_word = "Next word"; diff --git a/Resources/iOSSupport/Localization/ko.lproj/Localizable.strings b/Resources/iOSSupport/Localization/ko.lproj/Localizable.strings index 59a2ee5a..4c8fc113 100644 --- a/Resources/iOSSupport/Localization/ko.lproj/Localizable.strings +++ b/Resources/iOSSupport/Localization/ko.lproj/Localizable.strings @@ -68,6 +68,11 @@ haptics = "진동"; hapticsSettingsFooterTextWhenHapticsIsOn = "상호 작용에 대한 진동을 사용합니다."; hapticsSettingsFooterTextWhenHapticsIsOff = "상호 작용에 대한 진동을 사용하지 않습니다."; +theme = "테마"; +system_mode = "시스템 설정 모드"; +light_mode = "라이트 모드"; +dark_mode = "다크 모드"; + more_menu = "메뉴 더보기"; memorize_words = "단어 암기"; next_word = "다음 단어"; diff --git a/Sources/Domain/Interfaces/UseCases/UserSettingsUseCaseProtocol.swift b/Sources/Domain/Interfaces/UseCases/UserSettingsUseCaseProtocol.swift index 1051c888..f4e4fb8c 100644 --- a/Sources/Domain/Interfaces/UseCases/UserSettingsUseCaseProtocol.swift +++ b/Sources/Domain/Interfaces/UseCases/UserSettingsUseCaseProtocol.swift @@ -21,4 +21,6 @@ public protocol UserSettingsUseCaseProtocol { func offHaptics() -> Single + func updateThemeStyle(_ style: ThemeStyle) -> Single + } diff --git a/Sources/Domain/UseCases/UserSettingsUseCase.swift b/Sources/Domain/UseCases/UserSettingsUseCase.swift index 6a130187..8e904ad1 100644 --- a/Sources/Domain/UseCases/UserSettingsUseCase.swift +++ b/Sources/Domain/UseCases/UserSettingsUseCase.swift @@ -100,10 +100,20 @@ public final class UserSettingsUseCase: UserSettingsUseCaseProtocol { translationTargetLocale = .english } - let userSettings: UserSettings = .init(translationSourceLocale: .english, translationTargetLocale: translationTargetLocale, hapticsIsOn: true) // FIXME: 처음에 Source Locale 설정 가능하게 (현재 .english 고정) + let initialUserSettings: UserSettings = .init(translationSourceLocale: .english, translationTargetLocale: translationTargetLocale, hapticsIsOn: true, themeStyle: .system) // FIXME: 처음에 Source Locale 설정 가능하게 (현재 .english 고정) - return self.userSettingsRepository.saveUserSettings(userSettings) + return self.userSettingsRepository.saveUserSettings(initialUserSettings) } } + public func updateThemeStyle(_ style: ThemeStyle) -> Single { + return userSettingsRepository.getUserSettings() + .map { currentSettings in + var newSettings = currentSettings + newSettings.themeStyle = style + return newSettings + } + .flatMap { self.userSettingsRepository.saveUserSettings($0) } + } + } diff --git a/Sources/Domain/ValueObjects/ThemeStyle.swift b/Sources/Domain/ValueObjects/ThemeStyle.swift new file mode 100644 index 00000000..2306231c --- /dev/null +++ b/Sources/Domain/ValueObjects/ThemeStyle.swift @@ -0,0 +1,21 @@ +// +// ThemeStyle.swift +// Domain +// +// Created by Jaewon Yun on 2/7/24. +// Copyright © 2024 woin2ee. All rights reserved. +// + +/// 적용 가능한 테마 스타일. +public enum ThemeStyle: Codable { + + /// 시스템 설정을 따르는 스타일 + case system + + /// Light 스타일 + case light + + /// Dark 스타일 + case dark + +} diff --git a/Sources/Domain/ValueObjects/UserSettings.swift b/Sources/Domain/ValueObjects/UserSettings.swift index 206526ee..3db6ed81 100644 --- a/Sources/Domain/ValueObjects/UserSettings.swift +++ b/Sources/Domain/ValueObjects/UserSettings.swift @@ -19,10 +19,14 @@ public struct UserSettings { /// 진동 사용 여부 public var hapticsIsOn: Bool - public init(translationSourceLocale: TranslationLanguage, translationTargetLocale: TranslationLanguage, hapticsIsOn: Bool) { + /// 테마 스타일 + public var themeStyle: ThemeStyle + + public init(translationSourceLocale: TranslationLanguage, translationTargetLocale: TranslationLanguage, hapticsIsOn: Bool, themeStyle: ThemeStyle) { self.translationSourceLocale = translationSourceLocale self.translationTargetLocale = translationTargetLocale self.hapticsIsOn = hapticsIsOn + self.themeStyle = themeStyle } } diff --git a/Sources/DomainTesting/UserSettingsUseCaseFake.swift b/Sources/DomainTesting/UserSettingsUseCaseFake.swift index 01d05eca..7ffbf7ed 100644 --- a/Sources/DomainTesting/UserSettingsUseCaseFake.swift +++ b/Sources/DomainTesting/UserSettingsUseCaseFake.swift @@ -16,7 +16,8 @@ public final class UserSettingsUseCaseFake: UserSettingsUseCaseProtocol { public var currentUserSettings: Domain.UserSettings = .init( translationSourceLocale: .english, translationTargetLocale: .korean, - hapticsIsOn: true + hapticsIsOn: true, + themeStyle: .system ) public init() {} @@ -48,4 +49,9 @@ public final class UserSettingsUseCaseFake: UserSettingsUseCaseProtocol { return .just(()) } + public func updateThemeStyle(_ style: ThemeStyle) -> Single { + currentUserSettings.themeStyle = style + return .just(()) + } + } diff --git a/Sources/Infrastructure/DomainImpl/UserSettingsRepository.swift b/Sources/Infrastructure/DomainImpl/UserSettingsRepository.swift index 42655bff..8c78072c 100644 --- a/Sources/Infrastructure/DomainImpl/UserSettingsRepository.swift +++ b/Sources/Infrastructure/DomainImpl/UserSettingsRepository.swift @@ -31,7 +31,8 @@ final class UserSettingsRepository: UserSettingsRepositoryProtocol { userSettings.translationTargetLocale, forKey: UserDefaultsKey.translationTargetLocale ), - userDefaults.rx.setValue(userSettings.hapticsIsOn, forKey: UserDefaultsKey.hapticsIsOn) + userDefaults.rx.setValue(userSettings.hapticsIsOn, forKey: UserDefaultsKey.hapticsIsOn), + userDefaults.rx.setCodable(userSettings.themeStyle, forKey: UserDefaultsKey.themeStyle) ) .mapToVoid() } @@ -40,13 +41,15 @@ final class UserSettingsRepository: UserSettingsRepositoryProtocol { return Single.zip( userDefaults.rx.object(TranslationLanguage.self, forKey: UserDefaultsKey.translationSourceLocale), userDefaults.rx.object(TranslationLanguage.self, forKey: UserDefaultsKey.translationTargetLocale), - userDefaults.rx.bool(forKey: UserDefaultsKey.hapticsIsOn) + userDefaults.rx.bool(forKey: UserDefaultsKey.hapticsIsOn), + userDefaults.rx.object(ThemeStyle.self, forKey: UserDefaultsKey.themeStyle) ) - .map { sourceLocale, targetLocale, hapticsIsOn -> Domain.UserSettings in + .map { sourceLocale, targetLocale, hapticsIsOn, themeStyle -> Domain.UserSettings in return .init( translationSourceLocale: sourceLocale, translationTargetLocale: targetLocale, - hapticsIsOn: hapticsIsOn + hapticsIsOn: hapticsIsOn, + themeStyle: themeStyle ) } } diff --git a/Sources/Infrastructure/UserDefaults/UserDefaultsKey.swift b/Sources/Infrastructure/UserDefaults/UserDefaultsKey.swift index 322a6ca8..99dfbcc0 100644 --- a/Sources/Infrastructure/UserDefaults/UserDefaultsKey.swift +++ b/Sources/Infrastructure/UserDefaults/UserDefaultsKey.swift @@ -19,6 +19,8 @@ enum UserDefaultsKey: UserDefaultsKeyProtocol, CaseIterable { case hapticsIsOn + case themeStyle + /// 테스트용 Key 입니다. case test diff --git a/Sources/WordCheckerDev/AppDelegate.swift b/Sources/WordCheckerDev/AppDelegate.swift index e03d50cf..704b39ae 100644 --- a/Sources/WordCheckerDev/AppDelegate.swift +++ b/Sources/WordCheckerDev/AppDelegate.swift @@ -13,6 +13,7 @@ import Infrastructure import GeneralSettings import LanguageSetting import PushNotificationSettings +import ThemeSetting import UserSettings import WordAddition import WordChecking @@ -42,6 +43,7 @@ class AppDelegate: iPhoneAppDelegate { LanguageSettingAssembly(), PushNotificationSettingsAssemblyDev(), GeneralSettingsAssembly(), + ThemeSettingAssembly(), ]) } diff --git a/Sources/iOSScenes/GeneralSettings/GeneralSettingsViewController.swift b/Sources/iOSScenes/GeneralSettings/GeneralSettingsViewController.swift index ab030be2..f70b9d09 100644 --- a/Sources/iOSScenes/GeneralSettings/GeneralSettingsViewController.swift +++ b/Sources/iOSScenes/GeneralSettings/GeneralSettingsViewController.swift @@ -14,6 +14,7 @@ import UIKit public protocol GeneralSettingsViewControllerDelegate: AnyObject { func willPopView() + func didTapThemeSetting() } public protocol GeneralSettingsViewControllerProtocol: UIViewController { @@ -24,16 +25,19 @@ final class GeneralSettingsViewController: RxBaseViewController, View, GeneralSe enum SectionIdentifier: Int { case hapticsSettings = 0 + case themeSetting } enum ItemIdentifier { case hapticsOnOffSwitch + case themeSetting } weak var delegate: GeneralSettingsViewControllerDelegate? lazy var rootView: UITableView = .init(frame: .zero, style: .insetGrouped).then { $0.registerCell(ManualSwitchCell.self) + $0.registerCell(DisclosureIndicatorCell.self) $0.registerHeaderFooterView(TextFooterView.self) $0.delegate = self } @@ -60,6 +64,11 @@ final class GeneralSettingsViewController: RxBaseViewController, View, GeneralSe .bind(to: reactor.action) .disposed(by: cell.disposeBag) return cell + + case .themeSetting: + let cell = tableView.dequeueReusableCell(DisclosureIndicatorCell.self, for: indexPath) + cell.bind(model: .init(title: WCString.theme)) + return cell } } @@ -88,11 +97,27 @@ final class GeneralSettingsViewController: RxBaseViewController, View, GeneralSe func applyInitialSnapshot() { var snapshot = dataSource.snapshot() - snapshot.appendSections([.hapticsSettings]) + snapshot.appendSections([ + .hapticsSettings, + .themeSetting, + ]) snapshot.appendItems([.hapticsOnOffSwitch], toSection: .hapticsSettings) + snapshot.appendItems([.themeSetting], toSection: .themeSetting) dataSource.applySnapshotUsingReloadData(snapshot) } + override func bindAction() { + let itemSelectedEvent = rootView.rx.itemSelected.asSignal() + .doOnNext { [weak self] in self?.rootView.deselectRow(at: $0, animated: true) } + + itemSelectedEvent + .filter { self.dataSource.itemIdentifier(for: $0) == .themeSetting } + .emit(with: self, onNext: { owner, _ in + owner.delegate?.didTapThemeSetting() + }) + .disposed(by: self.disposeBag) + } + func bind(reactor: GeneralSettingsReactor) { // Action self.rx.sentMessage(#selector(viewDidLoad)) diff --git a/Sources/iOSScenes/PushNotificationSettingsExample/AppDelegate.swift b/Sources/iOSScenes/PushNotificationSettingsExample/AppDelegate.swift index c494e4a8..d1b5ea45 100644 --- a/Sources/iOSScenes/PushNotificationSettingsExample/AppDelegate.swift +++ b/Sources/iOSScenes/PushNotificationSettingsExample/AppDelegate.swift @@ -12,7 +12,7 @@ import UIKit class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - GlobalState.shared.initialize(hapticsIsOn: true) + GlobalState.shared.initialize(hapticsIsOn: true, themeStyle: .unspecified) return true } diff --git a/Sources/iOSScenes/ThemeSetting/ThemeSettingAssembly.swift b/Sources/iOSScenes/ThemeSetting/ThemeSettingAssembly.swift index be28aae4..1a78b6d0 100644 --- a/Sources/iOSScenes/ThemeSetting/ThemeSettingAssembly.swift +++ b/Sources/iOSScenes/ThemeSetting/ThemeSettingAssembly.swift @@ -7,8 +7,11 @@ public final class ThemeSettingAssembly: Assembly { public init() {} public func assemble(container: Container) { - container.register(ThemeSettingReactor.self) { _ in - return ThemeSettingReactor.init() + container.register(ThemeSettingReactor.self) { resolver in + return ThemeSettingReactor.init( + userSettingsUseCase: resolver.resolve(), + globalState: .shared + ) } container.register(ThemeSettingViewControllerProtocol.self) { resolver in diff --git a/Sources/iOSScenes/ThemeSetting/ThemeSettingReactor.swift b/Sources/iOSScenes/ThemeSetting/ThemeSettingReactor.swift index 0a2bc07d..789832cd 100644 --- a/Sources/iOSScenes/ThemeSetting/ThemeSettingReactor.swift +++ b/Sources/iOSScenes/ThemeSetting/ThemeSettingReactor.swift @@ -1,24 +1,49 @@ +import Domain +import iOSSupport import ReactorKit +import UIKit final class ThemeSettingReactor: Reactor { enum Action { - + case viewDidLoad + case selectStyle(UIUserInterfaceStyle) } enum Mutation { - + case setStyle(UIUserInterfaceStyle) } struct State { - + var selectedStyle: UIUserInterfaceStyle } - var initialState: State = .init() + var initialState: State = .init(selectedStyle: .unspecified) + + let userSettingsUseCase: UserSettingsUseCaseProtocol + let globalState: GlobalState + + init(userSettingsUseCase: UserSettingsUseCaseProtocol, globalState: GlobalState) { + self.userSettingsUseCase = userSettingsUseCase + self.globalState = globalState + } func mutate(action: Action) -> Observable { switch action { - + case .viewDidLoad: + return userSettingsUseCase.getCurrentUserSettings() + .asObservable() + .map(\.themeStyle) + .map { $0.toUIKit() } + .map(Mutation.setStyle) + + case .selectStyle(let selectedStyle): + return userSettingsUseCase.updateThemeStyle(selectedStyle.toDomain()) + .asObservable() + .doOnNext { + self.globalState.themeStyle.accept(selectedStyle) + } + .map { Mutation.setStyle(selectedStyle) } } } @@ -26,7 +51,8 @@ final class ThemeSettingReactor: Reactor { var state = state switch mutation { - + case .setStyle(let selectedStyle): + state.selectedStyle = selectedStyle } return state diff --git a/Sources/iOSScenes/ThemeSetting/ThemeSettingViewController.swift b/Sources/iOSScenes/ThemeSetting/ThemeSettingViewController.swift index 712c32fe..cb745db7 100644 --- a/Sources/iOSScenes/ThemeSetting/ThemeSettingViewController.swift +++ b/Sources/iOSScenes/ThemeSetting/ThemeSettingViewController.swift @@ -1,9 +1,10 @@ +import FoundationExtension import iOSSupport +import OrderedCollections import ReactorKit import UIKit public protocol ThemeSettingViewControllerDelegate: AnyObject { - // An example if you use navigation stack. func willPopView() } @@ -13,10 +14,140 @@ public protocol ThemeSettingViewControllerProtocol: UIViewController { final class ThemeSettingViewController: RxBaseViewController, View, ThemeSettingViewControllerProtocol { + enum SectionIdentifier { + case theme + } + + enum ItemIdentifier: Hashable { + case theme(UIUserInterfaceStyle) + } + + /// `ItemIdentifier` 로 인해 구분되는 Item 들을 보여주기 위한 값을 가지고 있는 OrderedDictionary 타입 Model 입니다. + /// + /// `TableView` 에 보여지는 순서대로 값을 가지고 있습니다. + lazy var itemModels: OrderedDictionary = defaultItemModels + + var defaultItemModels: OrderedDictionary { + [ + ItemIdentifier.theme(.unspecified), + ItemIdentifier.theme(.light), + ItemIdentifier.theme(.dark), + ] + .reduce(into: OrderedDictionary()) { partialResult, itemIdentifier in + if case let .theme(style) = itemIdentifier { + partialResult[itemIdentifier] = .init(title: localizedCellTitle(by: style), isChecked: false) + } + } + } + + lazy var dataSource: UITableViewDiffableDataSource = .init(tableView: rootView) { [weak self] tableView, indexPath, itemIdentifier -> UITableViewCell? in + guard let self = self else { return nil } + guard let itemModel = itemModels[itemIdentifier] else { return nil } + + switch itemIdentifier { + case .theme(let style): + let cell = tableView.dequeueReusableCell(CheckmarkCell.self, for: indexPath) + cell.bind(model: .init(title: localizedCellTitle(by: style), isChecked: itemModel.isChecked)) + return cell + } + } + weak var delegate: ThemeSettingViewControllerDelegate? + lazy var rootView: UITableView = .init(frame: .zero, style: .insetGrouped).then { + $0.registerCell(CheckmarkCell.self) + } + + override func loadView() { + self.view = rootView + } + + override func viewDidLoad() { + super.viewDidLoad() + + self.view.backgroundColor = .systemGroupedBackground + self.navigationItem.title = WCString.theme + + applyInitialSnapshotIfNoSections() + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + + if self.isMovingFromParent { + delegate?.willPopView() + } + } + + /// 현재 DataSource 에 적용된 Snapshot 이 없다면 초기 Snapshot 을 적용합니다. + /// - Returns: 현재 DataSource 에 적용된 Snapshot 이 있다면 해당 Snapshot 을 반환, 없다면 새로 적용한 Snapshot 을 반환합니다. + @discardableResult + func applyInitialSnapshotIfNoSections() -> NSDiffableDataSourceSnapshot { + let currenetSnapshot = dataSource.snapshot() + + if currenetSnapshot.sectionIdentifiers.isNotEmpty { + return currenetSnapshot + } + + var snapshot: NSDiffableDataSourceSnapshot = .init() + let itemIdentifiers = Array(itemModels.keys) + + snapshot.appendSections([.theme]) + snapshot.appendItems(itemIdentifiers, toSection: .theme) + dataSource.applySnapshotUsingReloadData(snapshot) + return snapshot + } + func bind(reactor: ThemeSettingReactor) { + // Action + self.rx.sentMessage(#selector(viewDidLoad)) + .map { _ in Reactor.Action.viewDidLoad } + .bind(to: reactor.action) + .disposed(by: self.disposeBag) + + rootView.rx.itemSelected + .doOnNext { [weak self] in self?.rootView.deselectRow(at: $0, animated: false) } + .compactMap { [weak self] indexPath in + guard case .theme(let style) = self?.dataSource.itemIdentifier(for: indexPath) else { + assertionFailure("Selected invalid item.") + return nil + } + return style + } + .map(Reactor.Action.selectStyle) + .bind(to: reactor.action) + .disposed(by: self.disposeBag) + + // State + reactor.state + .map(\.selectedStyle) + .skip(1) // skip initialState + .asDriverOnErrorJustComplete() + .drive(with: self) { owner, selectedStyle in + var snapshot = owner.dataSource.snapshot() + snapshot = owner.applyInitialSnapshotIfNoSections() + + owner.itemModels = owner.defaultItemModels + owner.itemModels[.theme(selectedStyle)] = .init(title: owner.localizedCellTitle(by: selectedStyle), isChecked: true) + + snapshot.reconfigureItems(snapshot.itemIdentifiers(inSection: .theme)) + owner.dataSource.apply(snapshot) + } + .disposed(by: self.disposeBag) + } + func localizedCellTitle(by style: UIUserInterfaceStyle) -> String { + switch style { + case .unspecified: + return WCString.system_mode + case .light: + return WCString.light_mode + case .dark: + return WCString.dark_mode + @unknown default: + assertionFailure("unkown cases.") + return "" + } } } diff --git a/Sources/iOSScenes/UserSettings/UserSettingsDataSource/UserSettingsItemModel.swift b/Sources/iOSScenes/UserSettings/UserSettingsDataSource/UserSettingsItemModel.swift index 8c7ce7bf..591c9498 100644 --- a/Sources/iOSScenes/UserSettings/UserSettingsDataSource/UserSettingsItemModel.swift +++ b/Sources/iOSScenes/UserSettings/UserSettingsDataSource/UserSettingsItemModel.swift @@ -6,7 +6,7 @@ // Copyright © 2023 woin2ee. All rights reserved. // -import Foundation +import iOSSupport enum UserSettingsItemModel { case disclosureIndicator(DisclosureIndicatorCell.Model) diff --git a/Sources/iOSSupport/Common/Domain.ThemeStyle+mapping.swift b/Sources/iOSSupport/Common/Domain.ThemeStyle+mapping.swift new file mode 100644 index 00000000..8e0b0649 --- /dev/null +++ b/Sources/iOSSupport/Common/Domain.ThemeStyle+mapping.swift @@ -0,0 +1,43 @@ +// +// Domain.ThemeStyle+mapping.swift +// iOSSupport +// +// Created by Jaewon Yun on 2/7/24. +// Copyright © 2024 woin2ee. All rights reserved. +// + +import Domain +import UIKit + +extension Domain.ThemeStyle { + + public func toUIKit() -> UIUserInterfaceStyle { + switch self { + case .system: + return .unspecified + case .light: + return .light + case .dark: + return .dark + } + } + +} + +extension UIUserInterfaceStyle { + + public func toDomain() -> Domain.ThemeStyle { + switch self { + case .unspecified: + return .system + case .light: + return .light + case .dark: + return .dark + @unknown default: + assertionFailure("unkown cases.") + return .system + } + } + +} diff --git a/Sources/iOSSupport/Common/GlobalState.swift b/Sources/iOSSupport/Common/GlobalState.swift index d355de0a..7395fa50 100644 --- a/Sources/iOSSupport/Common/GlobalState.swift +++ b/Sources/iOSSupport/Common/GlobalState.swift @@ -6,7 +6,9 @@ // Copyright © 2024 woin2ee. All rights reserved. // -import Foundation +import UIKit +import RxSwift +import RxRelay /// 앱의 전역 상태를 가지는 객체입니다. /// @@ -19,9 +21,12 @@ public final class GlobalState { public var hapticsIsOn: Bool! + public var themeStyle: BehaviorRelay! + /// 전역 상태를 초기화 합니다. - public func initialize(hapticsIsOn: Bool) { + public func initialize(hapticsIsOn: Bool, themeStyle: UIUserInterfaceStyle) { self.hapticsIsOn = hapticsIsOn + self.themeStyle = .init(value: themeStyle) } } diff --git a/Sources/iOSSupport/Localization/WCString.swift b/Sources/iOSSupport/Localization/WCString.swift index 02b91c99..668a6aeb 100644 --- a/Sources/iOSSupport/Localization/WCString.swift +++ b/Sources/iOSSupport/Localization/WCString.swift @@ -77,6 +77,11 @@ public struct WCString { public static let hapticsSettingsFooterTextWhenHapticsIsOn = NSLocalizedString("hapticsSettingsFooterTextWhenHapticsIsOn", bundle: Bundle.module, comment: "") public static let hapticsSettingsFooterTextWhenHapticsIsOff = NSLocalizedString("hapticsSettingsFooterTextWhenHapticsIsOff", bundle: Bundle.module, comment: "") + public static let theme = NSLocalizedString("theme", bundle: Bundle.module, comment: "") + public static let system_mode = NSLocalizedString("system_mode", bundle: Bundle.module, comment: "") + public static let light_mode = NSLocalizedString("light_mode", bundle: Bundle.module, comment: "") + public static let dark_mode = NSLocalizedString("dark_mode", bundle: Bundle.module, comment: "") + public static let more_menu = NSLocalizedString("more_menu", bundle: Bundle.module, comment: "") public static let memorize_words = NSLocalizedString("memorize_words", bundle: Bundle.module, comment: "") public static let previous_word = NSLocalizedString("previous_word", bundle: Bundle.module, comment: "") diff --git a/Sources/iOSScenes/UserSettings/Cells/ButtonCell.swift b/Sources/iOSSupport/UIKitExtension/Cells/ButtonCell.swift similarity index 75% rename from Sources/iOSScenes/UserSettings/Cells/ButtonCell.swift rename to Sources/iOSSupport/UIKitExtension/Cells/ButtonCell.swift index 60536acf..5cfb0be2 100644 --- a/Sources/iOSScenes/UserSettings/Cells/ButtonCell.swift +++ b/Sources/iOSSupport/UIKitExtension/Cells/ButtonCell.swift @@ -6,17 +6,21 @@ // Copyright © 2023 woin2ee. All rights reserved. // -import iOSSupport import UIKit /// 버튼 역할을 하기 위한 Cell 클래스 입니다. /// /// 버튼 가이드라인에 따른 텍스트 색상을 사용하세요. -final class ButtonCell: UITableViewCell, ReusableIdentifying { +public final class ButtonCell: UITableViewCell, ReusableIdentifying { - struct Model { + public struct Model { let title: String let textColor: UIColor + + public init(title: String, textColor: UIColor) { + self.title = title + self.textColor = textColor + } } override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { @@ -29,7 +33,7 @@ final class ButtonCell: UITableViewCell, ReusableIdentifying { fatalError("init(coder:) has not been implemented") } - func bind(model: Model) { + public func bind(model: Model) { var config: UIListContentConfiguration = .cell() config.text = model.title config.textProperties.color = model.textColor diff --git a/Sources/iOSScenes/UserSettings/Cells/DisclosureIndicatorCell.swift b/Sources/iOSSupport/UIKitExtension/Cells/DisclosureIndicatorCell.swift similarity index 72% rename from Sources/iOSScenes/UserSettings/Cells/DisclosureIndicatorCell.swift rename to Sources/iOSSupport/UIKitExtension/Cells/DisclosureIndicatorCell.swift index 5054dd46..fd1717c9 100644 --- a/Sources/iOSScenes/UserSettings/Cells/DisclosureIndicatorCell.swift +++ b/Sources/iOSSupport/UIKitExtension/Cells/DisclosureIndicatorCell.swift @@ -6,14 +6,18 @@ // Copyright © 2023 woin2ee. All rights reserved. // -import iOSSupport import UIKit -final class DisclosureIndicatorCell: UITableViewCell, ReusableIdentifying { +public final class DisclosureIndicatorCell: UITableViewCell, ReusableIdentifying { - struct Model { + public struct Model { let title: String let value: String? + + public init(title: String, value: String? = nil) { + self.title = title + self.value = value + } } override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { @@ -26,7 +30,7 @@ final class DisclosureIndicatorCell: UITableViewCell, ReusableIdentifying { fatalError("init(coder:) has not been implemented") } - func bind(model: Model) { + public func bind(model: Model) { var config: UIListContentConfiguration = .valueCell() config.text = model.title config.secondaryText = model.value diff --git a/Sources/iPhoneDriver/Coordinators/GeneralSettingsCoordinator.swift b/Sources/iPhoneDriver/Coordinators/GeneralSettingsCoordinator.swift index abaedfd3..802435a6 100644 --- a/Sources/iPhoneDriver/Coordinators/GeneralSettingsCoordinator.swift +++ b/Sources/iPhoneDriver/Coordinators/GeneralSettingsCoordinator.swift @@ -37,4 +37,11 @@ extension GeneralSettingsCoordinator: GeneralSettingsViewControllerDelegate { parentCoordinator?.childCoordinators.removeAll(where: { $0 === self }) } + func didTapThemeSetting() { + let coordinator: ThemeSettingCoordinator = .init(navigationController: navigationController) + coordinator.parentCoordinator = self + self.childCoordinators.append(coordinator) + coordinator.start() + } + } diff --git a/Sources/iPhoneDriver/Coordinators/ThemeSettingCoordinator.swift b/Sources/iPhoneDriver/Coordinators/ThemeSettingCoordinator.swift index f23ebd17..380bb691 100644 --- a/Sources/iPhoneDriver/Coordinators/ThemeSettingCoordinator.swift +++ b/Sources/iPhoneDriver/Coordinators/ThemeSettingCoordinator.swift @@ -16,23 +16,15 @@ final class ThemeSettingCoordinator: Coordinator { } func start() { - // An example if you use navigation stack. let viewController: ThemeSettingViewControllerProtocol = DIContainer.shared.resolver.resolve() viewController.delegate = self navigationController.pushViewController(viewController, animated: true) } - /* If you want start to coordinator with arguments. - func start(with argument: Arg1) { - - } - */ - } extension ThemeSettingCoordinator: ThemeSettingViewControllerDelegate { - // An example if you use navigation stack. func willPopView() { navigationController.popViewController(animated: true) parentCoordinator?.childCoordinators.removeAll(where: { $0 === self }) diff --git a/Sources/iPhoneDriver/SceneDelegate.swift b/Sources/iPhoneDriver/SceneDelegate.swift index 2bc6638f..3c81cdac 100644 --- a/Sources/iPhoneDriver/SceneDelegate.swift +++ b/Sources/iPhoneDriver/SceneDelegate.swift @@ -6,26 +6,28 @@ // import iOSSupport +import RxSwift import UIKit class SceneDelegate: UIResponder, UIWindowSceneDelegate { + let disposeBag: DisposeBag = .init() + var window: UIWindow? var appCoordinator: AppCoordinator? let globalAction: GlobalReactorAction = .shared + let globalState: GlobalState = .shared func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { guard let windowScene = (scene as? UIWindowScene) else { return } window = .init(windowScene: windowScene) window?.makeKeyAndVisible() - let rootTabBarController: RootTabBarController = .shared - window?.rootViewController = rootTabBarController + setRootViewController() - appCoordinator = .init(rootTabBarController: rootTabBarController) - appCoordinator?.start() + subscribeGlobalAction() } func sceneWillEnterForeground(_ scene: UIScene) { @@ -36,4 +38,21 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { globalAction.sceneDidBecomeActive.accept(()) } + func setRootViewController() { + let rootTabBarController: RootTabBarController = .shared + window?.rootViewController = rootTabBarController + + appCoordinator = .init(rootTabBarController: rootTabBarController) + appCoordinator?.start() + } + + func subscribeGlobalAction() { + globalState.themeStyle + .asDriver() + .drive(with: self) { owner, userInterfaceStyle in + owner.window?.overrideUserInterfaceStyle = userInterfaceStyle + } + .disposed(by: disposeBag) + } + } diff --git a/Sources/iPhoneDriver/iPhoneAppDelegate.swift b/Sources/iPhoneDriver/iPhoneAppDelegate.swift index 842e0c59..09e2ba2f 100644 --- a/Sources/iPhoneDriver/iPhoneAppDelegate.swift +++ b/Sources/iPhoneDriver/iPhoneAppDelegate.swift @@ -7,23 +7,28 @@ // import Domain -import GeneralSettings import GoogleSignIn import Infrastructure import iOSSupport -import LanguageSetting -import PushNotificationSettings import RxSwift -import Swinject -import SwinjectDIContainer import UIKit -import UserSettings import Utility + +// Scenes +import GeneralSettings +import LanguageSetting +import PushNotificationSettings +import ThemeSetting +import UserSettings import WordAddition import WordChecking import WordDetail import WordList +// DI +import Swinject +import SwinjectDIContainer + // swiftlint:disable type_name open class iPhoneAppDelegate: UIResponder, UIApplicationDelegate { @@ -90,14 +95,16 @@ open class iPhoneAppDelegate: UIResponder, UIApplicationDelegate { LanguageSettingAssembly(), PushNotificationSettingsAssembly(), GeneralSettingsAssembly(), + ThemeSettingAssembly(), ]) } func initGlobalState() { let userSettingsUseCase: UserSettingsUseCaseProtocol = DIContainer.shared.resolver.resolve() _ = userSettingsUseCase.getCurrentUserSettings() - .map(\.hapticsIsOn) - .doOnSuccess(GlobalState.shared.initialize) + .doOnSuccess { + GlobalState.shared.initialize(hapticsIsOn: $0.hapticsIsOn, themeStyle: $0.themeStyle.toUIKit()) + } .subscribe(on: ConcurrentMainScheduler.instance) .subscribe() } diff --git a/Tests/InfrastructureTests/UserDefaults/UserSettingsRepositoryTests.swift b/Tests/InfrastructureTests/UserDefaults/UserSettingsRepositoryTests.swift index f1402b6d..49d551f8 100644 --- a/Tests/InfrastructureTests/UserDefaults/UserSettingsRepositoryTests.swift +++ b/Tests/InfrastructureTests/UserDefaults/UserSettingsRepositoryTests.swift @@ -35,7 +35,7 @@ final class UserSettingsRepositoryTests: XCTestCase { func testSaveAndGetUserSettings() throws { do { // Given - let userSettings: UserSettings = .init(translationSourceLocale: .english, translationTargetLocale: .korean, hapticsIsOn: true) + let userSettings: UserSettings = .init(translationSourceLocale: .english, translationTargetLocale: .korean, hapticsIsOn: true, themeStyle: .system) // When try sut.saveUserSettings(userSettings) @@ -50,11 +50,12 @@ final class UserSettingsRepositoryTests: XCTestCase { XCTAssertEqual(result.translationSourceLocale, .english) XCTAssertEqual(result.translationTargetLocale, .korean) XCTAssertEqual(result.hapticsIsOn, true) + XCTAssertEqual(result.themeStyle, .system) } do { // Given - let userSettings: UserSettings = .init(translationSourceLocale: .korean, translationTargetLocale: .english, hapticsIsOn: false) + let userSettings: UserSettings = .init(translationSourceLocale: .korean, translationTargetLocale: .english, hapticsIsOn: false, themeStyle: .dark) // When try sut.saveUserSettings(userSettings) @@ -69,6 +70,7 @@ final class UserSettingsRepositoryTests: XCTestCase { XCTAssertEqual(result.translationSourceLocale, .korean) XCTAssertEqual(result.translationTargetLocale, .english) XCTAssertEqual(result.hapticsIsOn, false) + XCTAssertEqual(result.themeStyle, .dark) } } diff --git a/Tests/iOSScenesTests/UserSettingsTests/UserSettingsReactorTests.swift b/Tests/iOSScenesTests/UserSettingsTests/UserSettingsReactorTests.swift index 4ee5a153..c28c779c 100644 --- a/Tests/iOSScenesTests/UserSettingsTests/UserSettingsReactorTests.swift +++ b/Tests/iOSScenesTests/UserSettingsTests/UserSettingsReactorTests.swift @@ -109,7 +109,7 @@ final class UserSettingsReactorTests: RxBaseTestCase { func test_viewDidLoad() { // Given let userSettingsUseCase: UserSettingsUseCaseFake = .init() - userSettingsUseCase.currentUserSettings = .init(translationSourceLocale: .german, translationTargetLocale: .italian, hapticsIsOn: true) + userSettingsUseCase.currentUserSettings = .init(translationSourceLocale: .german, translationTargetLocale: .italian, hapticsIsOn: true, themeStyle: .system) sut = .init( userSettingsUseCase: userSettingsUseCase, googleDriveUseCase: GoogleDriveUseCaseFake(scheduler: testScheduler), From c48023fb13b748b54a3e9270b44b0bab88f190a9 Mon Sep 17 00:00:00 2001 From: Jaewon Yun Date: Thu, 8 Feb 2024 16:12:04 +0900 Subject: [PATCH 10/24] Resolve issue that view controller popped twice Declare BasicCoordinator that has default behavior of view controller --- .../GeneralSettingsViewController.swift | 9 ++- .../LanguageSettingViewController.swift | 8 +-- ...shNotificationSettingsViewController.swift | 9 ++- .../ThemeSettingViewController.swift | 9 ++- .../WordAdditionViewController.swift | 11 ++-- .../WordDetail/WordDetailViewController.swift | 20 +++---- .../WordSearchResultsController.swift | 10 ---- .../Foundations/ViewControllerDelegate.swift | 25 +++++++++ .../Coordinators/BasicCoordinator.swift | 56 +++++++++++++++++++ .../GeneralSettingsCoordinator.swift | 18 +----- .../LanguageSettingCoordinator.swift | 19 +------ .../PushNotificationSettingsCoordinator.swift | 22 +------- .../ThemeSettingCoordinator.swift | 19 +------ .../UserSettingsCoordinator.swift | 14 +---- .../WordAdditionCoordinator.swift | 19 +------ .../WordCheckingCoordinator.swift | 13 +---- .../Coordinators/WordDetailCoordinator.swift | 19 +------ .../Coordinators/WordListCoordinator.swift | 13 +---- 18 files changed, 128 insertions(+), 185 deletions(-) create mode 100644 Sources/iOSSupport/Foundations/ViewControllerDelegate.swift create mode 100644 Sources/iPhoneDriver/Coordinators/BasicCoordinator.swift diff --git a/Sources/iOSScenes/GeneralSettings/GeneralSettingsViewController.swift b/Sources/iOSScenes/GeneralSettings/GeneralSettingsViewController.swift index f70b9d09..cc72712c 100644 --- a/Sources/iOSScenes/GeneralSettings/GeneralSettingsViewController.swift +++ b/Sources/iOSScenes/GeneralSettings/GeneralSettingsViewController.swift @@ -12,8 +12,7 @@ import RxUtility import Then import UIKit -public protocol GeneralSettingsViewControllerDelegate: AnyObject { - func willPopView() +public protocol GeneralSettingsViewControllerDelegate: AnyObject, ViewControllerDelegate { func didTapThemeSetting() } @@ -87,11 +86,11 @@ final class GeneralSettingsViewController: RxBaseViewController, View, GeneralSe applyInitialSnapshot() } - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) if self.isMovingFromParent { - delegate?.willPopView() + delegate?.viewControllerDidPop(self) } } diff --git a/Sources/iOSScenes/LanguageSetting/LanguageSettingViewController.swift b/Sources/iOSScenes/LanguageSetting/LanguageSettingViewController.swift index 6c633aee..6e955c29 100644 --- a/Sources/iOSScenes/LanguageSetting/LanguageSettingViewController.swift +++ b/Sources/iOSScenes/LanguageSetting/LanguageSettingViewController.swift @@ -15,11 +15,7 @@ import RxSwift import Then import UIKit -public protocol LanguageSettingViewControllerDelegate: AnyObject { - - /// ViewController 가 Pop 해야될때 호출되는 Delegate method 입니다. - func viewMustPop() - +public protocol LanguageSettingViewControllerDelegate: AnyObject, ViewControllerDelegate { } public protocol LanguageSettingViewControllerProtocol: UIViewController { @@ -99,7 +95,7 @@ final class LanguageSettingViewController: RxBaseViewController, LanguageSetting super.viewWillDisappear(animated) if self.isMovingFromParent { - delegate?.viewMustPop() + delegate?.viewControllerDidPop(self) } } diff --git a/Sources/iOSScenes/PushNotificationSettings/PushNotificationSettingsViewController.swift b/Sources/iOSScenes/PushNotificationSettings/PushNotificationSettingsViewController.swift index 47220865..87ba158c 100644 --- a/Sources/iOSScenes/PushNotificationSettings/PushNotificationSettingsViewController.swift +++ b/Sources/iOSScenes/PushNotificationSettings/PushNotificationSettingsViewController.swift @@ -3,8 +3,7 @@ import ReactorKit import Then import UIKit -public protocol PushNotificationSettingsDelegate: AnyObject { - func willPopView() +public protocol PushNotificationSettingsDelegate: AnyObject, ViewControllerDelegate { } public protocol PushNotificationSettingsViewControllerProtocol: UIViewController { @@ -96,11 +95,11 @@ class PushNotificationSettingsViewController: RxBaseViewController, View, PushNo isViewAppeared = true } - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) if self.isMovingFromParent { - delegate?.willPopView() + delegate?.viewControllerDidPop(self) } } diff --git a/Sources/iOSScenes/ThemeSetting/ThemeSettingViewController.swift b/Sources/iOSScenes/ThemeSetting/ThemeSettingViewController.swift index cb745db7..7a2b7689 100644 --- a/Sources/iOSScenes/ThemeSetting/ThemeSettingViewController.swift +++ b/Sources/iOSScenes/ThemeSetting/ThemeSettingViewController.swift @@ -4,8 +4,7 @@ import OrderedCollections import ReactorKit import UIKit -public protocol ThemeSettingViewControllerDelegate: AnyObject { - func willPopView() +public protocol ThemeSettingViewControllerDelegate: AnyObject, ViewControllerDelegate { } public protocol ThemeSettingViewControllerProtocol: UIViewController { @@ -71,11 +70,11 @@ final class ThemeSettingViewController: RxBaseViewController, View, ThemeSetting applyInitialSnapshotIfNoSections() } - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) if self.isMovingFromParent { - delegate?.willPopView() + delegate?.viewControllerDidPop(self) } } diff --git a/Sources/iOSScenes/WordAddition/WordAdditionViewController.swift b/Sources/iOSScenes/WordAddition/WordAdditionViewController.swift index 0776ba52..09051299 100644 --- a/Sources/iOSScenes/WordAddition/WordAdditionViewController.swift +++ b/Sources/iOSScenes/WordAddition/WordAdditionViewController.swift @@ -13,10 +13,7 @@ import SnapKit import Then import UIKit -public protocol WordAdditionViewControllerDelegate: AnyObject { - - func didFinishInteration() - +public protocol WordAdditionViewControllerDelegate: AnyObject, ViewControllerDelegate { } public protocol WordAdditionViewControllerProtocol: UIViewController { @@ -90,19 +87,19 @@ final class WordAdditionViewController: RxBaseViewController, WordAdditionViewCo [ output.saveComplete .emit(with: self, onNext: { owner, _ in - owner.delegate?.didFinishInteration() + owner.delegate?.viewControllerMustBeDismissed(self) }), output.wordTextIsNotEmpty .drive(doneBarButton.rx.isEnabled), output.reconfirmDismiss .emit(with: self, onNext: { owner, _ in owner.presentDismissActionSheet { - owner.delegate?.didFinishInteration() + owner.delegate?.viewControllerMustBeDismissed(self) } }), output.dismissComplete .emit(with: self, onNext: { owner, _ in - owner.delegate?.didFinishInteration() + owner.delegate?.viewControllerMustBeDismissed(self) }), ] .forEach { $0.disposed(by: disposeBag) } diff --git a/Sources/iOSScenes/WordDetail/WordDetailViewController.swift b/Sources/iOSScenes/WordDetail/WordDetailViewController.swift index db0d8476..101914fa 100644 --- a/Sources/iOSScenes/WordDetail/WordDetailViewController.swift +++ b/Sources/iOSScenes/WordDetail/WordDetailViewController.swift @@ -13,10 +13,7 @@ import Then import UIKit import Utility -public protocol WordDetailViewControllerDelegate: AnyObject { - - func willFinishInteraction() - +public protocol WordDetailViewControllerDelegate: AnyObject, ViewControllerDelegate { } public protocol WordDetailViewControllerProtocol: UIViewController { @@ -106,10 +103,10 @@ final class WordDetailViewController: RxBaseViewController, WordDetailViewContro .drive(with: self) { owner, _ in if owner.reactor!.currentState.hasChanges { owner.presentDismissActionSheet { - owner.delegate?.willFinishInteraction() + owner.delegate?.viewControllerMustBeDismissed(owner) } } else { - owner.delegate?.willFinishInteraction() + owner.delegate?.viewControllerMustBeDismissed(owner) } } .disposed(by: self.disposeBag) @@ -129,7 +126,10 @@ extension WordDetailViewController: View { .disposed(by: self.disposeBag) doneBarButton.rx.tap - .doOnNext { [weak self] _ in self?.delegate?.willFinishInteraction() } + .doOnNext { [weak self] _ in + guard let self = self else { return } + self.delegate?.viewControllerMustBeDismissed(self) + } .map { Reactor.Action.doneEditing } .bind(to: reactor.action) .disposed(by: self.disposeBag) @@ -185,12 +185,12 @@ extension WordDetailViewController: UIAdaptivePresentationControllerDelegate { func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) { self.presentDismissActionSheet { - self.delegate?.willFinishInteraction() + self.delegate?.viewControllerMustBeDismissed(self) } } - func presentationControllerWillDismiss(_ presentationController: UIPresentationController) { - delegate?.willFinishInteraction() + func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { + delegate?.viewControllerDidDismiss(self) } } diff --git a/Sources/iOSScenes/WordList/WordSearchResultsController.swift b/Sources/iOSScenes/WordList/WordSearchResultsController.swift index 2e59d6b3..af9912db 100644 --- a/Sources/iOSScenes/WordList/WordSearchResultsController.swift +++ b/Sources/iOSScenes/WordList/WordSearchResultsController.swift @@ -152,13 +152,3 @@ extension WordSearchResultsController: UISearchResultsUpdating { } } - -// MARK: - WordDetailViewControllerDelegate - -extension WordSearchResultsController: WordDetailViewControllerDelegate { - - func willFinishInteraction() { - self.presentingViewController?.tabBarController?.dismiss(animated: true) - } - -} diff --git a/Sources/iOSSupport/Foundations/ViewControllerDelegate.swift b/Sources/iOSSupport/Foundations/ViewControllerDelegate.swift new file mode 100644 index 00000000..0c6d7805 --- /dev/null +++ b/Sources/iOSSupport/Foundations/ViewControllerDelegate.swift @@ -0,0 +1,25 @@ +// +// ViewControllerDelegate.swift +// iPhoneDriver +// +// Created by Jaewon Yun on 2/7/24. +// Copyright © 2024 woin2ee. All rights reserved. +// + +import UIKit + +/// `UIViewController` 를 위한 기본적인 Delegate methods 가 정의되어 있는 Protocol 입니다. +public protocol ViewControllerDelegate { + + /// ViewController 가 Pop 되었음을 Delegate 에게 알립니다. + func viewControllerDidPop(_ viewController: UIViewController) + + /// ViewController 가 Dismiss 되어야 함을 Delegate 에게 알립니다. + /// + /// 이 Delegate method 를 구현하는 Subclass 는 직접 ViewController 에 대한 Dismiss 처리를 해야합니다. + func viewControllerMustBeDismissed(_ viewController: UIViewController) + + /// ViewController 가 Dismiss 되었음을 Delegate 에게 알립니다. + func viewControllerDidDismiss(_ viewController: UIViewController) + +} diff --git a/Sources/iPhoneDriver/Coordinators/BasicCoordinator.swift b/Sources/iPhoneDriver/Coordinators/BasicCoordinator.swift new file mode 100644 index 00000000..ff08268a --- /dev/null +++ b/Sources/iPhoneDriver/Coordinators/BasicCoordinator.swift @@ -0,0 +1,56 @@ +// +// BasicCoordinator.swift +// iPhoneDriver +// +// Created by Jaewon Yun on 2/7/24. +// Copyright © 2024 woin2ee. All rights reserved. +// + +import iOSSupport +import UIKit +import Utility + +/// iOS 환경에서 동작하는 Coordinator 를 위한 Parent coordinator 클래스 +/// +/// - warning: Do not use instance of this class directly. Some methods in this class cause of fatal error. +class BasicCoordinator: Coordinator { + + weak var parentCoordinator: iOSSupport.Coordinator? + var childCoordinators: [iOSSupport.Coordinator] = [] + + let navigationController: UINavigationController + + init(navigationController: UINavigationController) { + self.navigationController = navigationController + } + + func start() { + abstractMethod() + } + + func start(with argument: Arg1) { + abstractMethod() + } + + func start(with arguments: Arg1, _ arg2: Arg2) { + abstractMethod() + } + +} + +extension BasicCoordinator: ViewControllerDelegate { + + func viewControllerDidPop(_ viewController: UIViewController) { + parentCoordinator?.childCoordinators.removeAll(where: { $0 === self }) + } + + func viewControllerMustBeDismissed(_ viewController: UIViewController) { + navigationController.dismiss(animated: true) // In this case, `navigationController` property is represented the presented view controller. + parentCoordinator?.childCoordinators.removeAll(where: { $0 === self }) + } + + func viewControllerDidDismiss(_ viewController: UIViewController) { + parentCoordinator?.childCoordinators.removeAll(where: { $0 === self }) + } + +} diff --git a/Sources/iPhoneDriver/Coordinators/GeneralSettingsCoordinator.swift b/Sources/iPhoneDriver/Coordinators/GeneralSettingsCoordinator.swift index 802435a6..795a707b 100644 --- a/Sources/iPhoneDriver/Coordinators/GeneralSettingsCoordinator.swift +++ b/Sources/iPhoneDriver/Coordinators/GeneralSettingsCoordinator.swift @@ -11,18 +11,9 @@ import iOSSupport import UIKit import SwinjectDIContainer -final class GeneralSettingsCoordinator: Coordinator { +final class GeneralSettingsCoordinator: BasicCoordinator { - weak var parentCoordinator: iOSSupport.Coordinator? - var childCoordinators: [iOSSupport.Coordinator] = [] - - let navigationController: UINavigationController - - init(navigationController: UINavigationController) { - self.navigationController = navigationController - } - - func start() { + override func start() { let viewController: GeneralSettingsViewControllerProtocol = DIContainer.shared.resolver.resolve() viewController.delegate = self navigationController.pushViewController(viewController, animated: true) @@ -32,11 +23,6 @@ final class GeneralSettingsCoordinator: Coordinator { extension GeneralSettingsCoordinator: GeneralSettingsViewControllerDelegate { - func willPopView() { - navigationController.popViewController(animated: true) - parentCoordinator?.childCoordinators.removeAll(where: { $0 === self }) - } - func didTapThemeSetting() { let coordinator: ThemeSettingCoordinator = .init(navigationController: navigationController) coordinator.parentCoordinator = self diff --git a/Sources/iPhoneDriver/Coordinators/LanguageSettingCoordinator.swift b/Sources/iPhoneDriver/Coordinators/LanguageSettingCoordinator.swift index a1cded96..a5547622 100644 --- a/Sources/iPhoneDriver/Coordinators/LanguageSettingCoordinator.swift +++ b/Sources/iPhoneDriver/Coordinators/LanguageSettingCoordinator.swift @@ -12,18 +12,9 @@ import SwinjectDIContainer import SwinjectExtension import UIKit -final class LanguageSettingCoordinator: Coordinator { +final class LanguageSettingCoordinator: BasicCoordinator { - weak var parentCoordinator: Coordinator? - var childCoordinators: [Coordinator] = [] - - let navigationController: UINavigationController - - init(navigationController: UINavigationController) { - self.navigationController = navigationController - } - - func start(with argument: Arg1) { + override func start(with argument: Arg1) { let viewController: LanguageSettingViewControllerProtocol = DIContainer.shared.resolver.resolve(argument: argument) viewController.delegate = self navigationController.pushViewController(viewController, animated: true) @@ -32,10 +23,4 @@ final class LanguageSettingCoordinator: Coordinator { } extension LanguageSettingCoordinator: LanguageSettingViewControllerDelegate { - - func viewMustPop() { - navigationController.popViewController(animated: true) - parentCoordinator?.childCoordinators.removeAll(where: { $0 === self }) - } - } diff --git a/Sources/iPhoneDriver/Coordinators/PushNotificationSettingsCoordinator.swift b/Sources/iPhoneDriver/Coordinators/PushNotificationSettingsCoordinator.swift index 32f966d6..0595fe93 100644 --- a/Sources/iPhoneDriver/Coordinators/PushNotificationSettingsCoordinator.swift +++ b/Sources/iPhoneDriver/Coordinators/PushNotificationSettingsCoordinator.swift @@ -6,36 +6,20 @@ // Copyright © 2023 woin2ee. All rights reserved. // -import iOSSupport import PushNotificationSettings import SwinjectDIContainer import SwinjectExtension import UIKit -final class PushNotificationSettingsCoordinator: Coordinator { +final class PushNotificationSettingsCoordinator: BasicCoordinator { - weak var parentCoordinator: Coordinator? - var childCoordinators: [Coordinator] = [] - - let navigationController: UINavigationController - - init(navigationController: UINavigationController) { - self.navigationController = navigationController - } - - func start() { + override func start() { let viewController: PushNotificationSettingsViewControllerProtocol = DIContainer.shared.resolver.resolve() viewController.delegate = self - navigationController.pushViewController(viewController, animated: true) + self.navigationController.pushViewController(viewController, animated: true) } } extension PushNotificationSettingsCoordinator: PushNotificationSettingsDelegate { - - func willPopView() { - navigationController.popViewController(animated: true) - parentCoordinator?.childCoordinators.removeAll(where: { $0 === self }) - } - } diff --git a/Sources/iPhoneDriver/Coordinators/ThemeSettingCoordinator.swift b/Sources/iPhoneDriver/Coordinators/ThemeSettingCoordinator.swift index 380bb691..dc3d42ad 100644 --- a/Sources/iPhoneDriver/Coordinators/ThemeSettingCoordinator.swift +++ b/Sources/iPhoneDriver/Coordinators/ThemeSettingCoordinator.swift @@ -4,18 +4,9 @@ import SwinjectExtension import UIKit import ThemeSetting -final class ThemeSettingCoordinator: Coordinator { +final class ThemeSettingCoordinator: BasicCoordinator { - weak var parentCoordinator: Coordinator? - var childCoordinators: [Coordinator] = [] - - let navigationController: UINavigationController - - init(navigationController: UINavigationController) { - self.navigationController = navigationController - } - - func start() { + override func start() { let viewController: ThemeSettingViewControllerProtocol = DIContainer.shared.resolver.resolve() viewController.delegate = self navigationController.pushViewController(viewController, animated: true) @@ -24,10 +15,4 @@ final class ThemeSettingCoordinator: Coordinator { } extension ThemeSettingCoordinator: ThemeSettingViewControllerDelegate { - - func willPopView() { - navigationController.popViewController(animated: true) - parentCoordinator?.childCoordinators.removeAll(where: { $0 === self }) - } - } diff --git a/Sources/iPhoneDriver/Coordinators/UserSettingsCoordinator.swift b/Sources/iPhoneDriver/Coordinators/UserSettingsCoordinator.swift index 5a51a2d4..261b7357 100644 --- a/Sources/iPhoneDriver/Coordinators/UserSettingsCoordinator.swift +++ b/Sources/iPhoneDriver/Coordinators/UserSettingsCoordinator.swift @@ -6,7 +6,6 @@ // Copyright © 2023 woin2ee. All rights reserved. // -import Domain import iOSSupport import LanguageSetting import SwinjectDIContainer @@ -14,18 +13,9 @@ import SwinjectExtension import UIKit import UserSettings -final class UserSettingsCoordinator: Coordinator { +final class UserSettingsCoordinator: BasicCoordinator { - weak var parentCoordinator: Coordinator? - var childCoordinators: [Coordinator] = [] - - let navigationController: UINavigationController - - init(navigationController: UINavigationController) { - self.navigationController = navigationController - } - - func start() { + override func start() { let viewController: UserSettingsViewControllerProtocol = DIContainer.shared.resolver.resolve() viewController.delegate = self navigationController.setViewControllers([viewController], animated: false) diff --git a/Sources/iPhoneDriver/Coordinators/WordAdditionCoordinator.swift b/Sources/iPhoneDriver/Coordinators/WordAdditionCoordinator.swift index b74b6472..010d1a1b 100644 --- a/Sources/iPhoneDriver/Coordinators/WordAdditionCoordinator.swift +++ b/Sources/iPhoneDriver/Coordinators/WordAdditionCoordinator.swift @@ -12,18 +12,9 @@ import SwinjectExtension import UIKit import WordAddition -final class WordAdditionCoordinator: Coordinator { +final class WordAdditionCoordinator: BasicCoordinator { - weak var parentCoordinator: Coordinator? - var childCoordinators: [Coordinator] = [] - - let navigationController: UINavigationController - - init(navigationController: UINavigationController) { - self.navigationController = navigationController - } - - func start() { + override func start() { let viewController: WordAdditionViewControllerProtocol = DIContainer.shared.resolver.resolve() viewController.delegate = self navigationController.setViewControllers([viewController], animated: true) @@ -32,10 +23,4 @@ final class WordAdditionCoordinator: Coordinator { } extension WordAdditionCoordinator: WordAdditionViewControllerDelegate { - - func didFinishInteration() { - navigationController.dismiss(animated: true) - parentCoordinator?.childCoordinators.removeAll(where: { $0 === self }) - } - } diff --git a/Sources/iPhoneDriver/Coordinators/WordCheckingCoordinator.swift b/Sources/iPhoneDriver/Coordinators/WordCheckingCoordinator.swift index cc5e7ab1..15cc61ae 100644 --- a/Sources/iPhoneDriver/Coordinators/WordCheckingCoordinator.swift +++ b/Sources/iPhoneDriver/Coordinators/WordCheckingCoordinator.swift @@ -12,18 +12,9 @@ import SwinjectExtension import UIKit import WordChecking -final class WordCheckingCoordinator: Coordinator { +final class WordCheckingCoordinator: BasicCoordinator { - weak var parentCoordinator: Coordinator? - var childCoordinators: [Coordinator] = [] - - let navigationController: UINavigationController - - init(navigationController: UINavigationController) { - self.navigationController = navigationController - } - - func start() { + override func start() { let viewController: WordCheckingViewControllerProtocol = DIContainer.shared.resolver.resolve() navigationController.setViewControllers([viewController], animated: false) } diff --git a/Sources/iPhoneDriver/Coordinators/WordDetailCoordinator.swift b/Sources/iPhoneDriver/Coordinators/WordDetailCoordinator.swift index f815c34c..3af01e18 100644 --- a/Sources/iPhoneDriver/Coordinators/WordDetailCoordinator.swift +++ b/Sources/iPhoneDriver/Coordinators/WordDetailCoordinator.swift @@ -12,18 +12,9 @@ import SwinjectExtension import UIKit import WordDetail -final class WordDetailCoordinator: Coordinator { +final class WordDetailCoordinator: BasicCoordinator { - weak var parentCoordinator: Coordinator? - var childCoordinators: [Coordinator] = [] - - let navigationController: UINavigationController - - init(navigationController: UINavigationController) { - self.navigationController = navigationController - } - - func start(with argument: Arg1) { + override func start(with argument: Arg1) { let viewController: WordDetailViewControllerProtocol = DIContainer.shared.resolver.resolve(argument: argument) viewController.delegate = self navigationController.setViewControllers([viewController], animated: false) @@ -32,10 +23,4 @@ final class WordDetailCoordinator: Coordinator { } extension WordDetailCoordinator: WordDetailViewControllerDelegate { - - func willFinishInteraction() { - navigationController.dismiss(animated: true) - parentCoordinator?.childCoordinators.removeAll(where: { $0 === self }) - } - } diff --git a/Sources/iPhoneDriver/Coordinators/WordListCoordinator.swift b/Sources/iPhoneDriver/Coordinators/WordListCoordinator.swift index 44427ece..40f3eb21 100644 --- a/Sources/iPhoneDriver/Coordinators/WordListCoordinator.swift +++ b/Sources/iPhoneDriver/Coordinators/WordListCoordinator.swift @@ -12,18 +12,9 @@ import SwinjectExtension import UIKit import WordList -final class WordListCoordinator: Coordinator { +final class WordListCoordinator: BasicCoordinator { - weak var parentCoordinator: Coordinator? - var childCoordinators: [Coordinator] = [] - - let navigationController: UINavigationController - - init(navigationController: UINavigationController) { - self.navigationController = navigationController - } - - func start() { + override func start() { let viewController: WordListViewControllerProtocol = DIContainer.shared.resolver.resolve() viewController.delegate = self navigationController.setViewControllers([viewController], animated: false) From 70bf18447bab67665cb18ec115f5c892d4f0042f Mon Sep 17 00:00:00 2001 From: Jaewon Yun Date: Thu, 8 Feb 2024 16:26:25 +0900 Subject: [PATCH 11/24] Fix broken example app --- Sources/iOSScenes/WordCheckingExample/AppDelegate.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/iOSScenes/WordCheckingExample/AppDelegate.swift b/Sources/iOSScenes/WordCheckingExample/AppDelegate.swift index 7010d807..00a6970f 100644 --- a/Sources/iOSScenes/WordCheckingExample/AppDelegate.swift +++ b/Sources/iOSScenes/WordCheckingExample/AppDelegate.swift @@ -5,6 +5,7 @@ // Created by Jaewon Yun on 2023/08/23. // +import iOSSupport import UIKit import Utility @@ -13,6 +14,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { NetworkMonitor.start() + GlobalState.shared.initialize(hapticsIsOn: true, themeStyle: .unspecified) return true } From 24a26d76fa08637b0e496056213c3380d0992826 Mon Sep 17 00:00:00 2001 From: Jaewon Yun Date: Thu, 8 Feb 2024 17:08:46 +0900 Subject: [PATCH 12/24] Resolve duplecate dependencies --- Project.swift | 98 +----------------- .../UseCases/NotificationsUseCase.swift | 8 +- .../WordSearchResultsController.swift | 1 - graph.png | Bin 176187 -> 140488 bytes 4 files changed, 8 insertions(+), 99 deletions(-) diff --git a/Project.swift b/Project.swift index 4a1e65c6..b0cda6bd 100644 --- a/Project.swift +++ b/Project.swift @@ -23,7 +23,6 @@ func targets() -> [Target] { .external(name: ExternalDependencyName.rxUtilityDynamic), .external(name: ExternalDependencyName.swinject), .external(name: ExternalDependencyName.swinjectExtension), - .external(name: ExternalDependencyName.then), ], hasTests: true, additionalTestDependencies: [ @@ -72,7 +71,6 @@ func targets() -> [Target] { .package(product: ExternalDependencyName.googleSignIn), .external(name: ExternalDependencyName.rxSwift), .external(name: ExternalDependencyName.rxUtilityDynamic), - .external(name: ExternalDependencyName.extendedUserDefaults), .external(name: ExternalDependencyName.extendedUserDefaultsRxExtension), .external(name: ExternalDependencyName.swinject), .external(name: ExternalDependencyName.swinjectExtension), @@ -96,12 +94,17 @@ func targets() -> [Target] { dependencies: [ .target(name: "Domain"), .target(name: "Utility"), + .target(name: "FoundationExtension"), .external(name: ExternalDependencyName.rxSwift), .external(name: ExternalDependencyName.rxCocoa), .external(name: ExternalDependencyName.rxUtilityDynamic), .external(name: ExternalDependencyName.reactorKit), .external(name: ExternalDependencyName.snapKit), .external(name: ExternalDependencyName.then), + .external(name: ExternalDependencyName.toast), + .external(name: ExternalDependencyName.sfSafeSymbols), + .external(name: ExternalDependencyName.swinject), + .external(name: ExternalDependencyName.swinjectExtension), .package(product: ExternalDependencyName.swiftCollections), ], appendSchemeTo: &schemes @@ -111,18 +114,7 @@ func targets() -> [Target] { sourcesPrefix: "iOSScenes", resourceOptions: [.additional("Resources/iOSSupport/**")], dependencies: [ - .target(name: "Domain"), .target(name: "iOSSupport"), - .external(name: ExternalDependencyName.rxSwift), - .external(name: ExternalDependencyName.rxCocoa), - .external(name: ExternalDependencyName.rxUtilityDynamic), - .external(name: ExternalDependencyName.reactorKit), - .external(name: ExternalDependencyName.snapKit), - .external(name: ExternalDependencyName.then), - .external(name: ExternalDependencyName.toast), - .external(name: ExternalDependencyName.sfSafeSymbols), - .external(name: ExternalDependencyName.swinject), - .external(name: ExternalDependencyName.swinjectExtension), ], hasTests: true, additionalTestDependencies: [ @@ -148,19 +140,7 @@ func targets() -> [Target] { sourcesPrefix: "iOSScenes", resourceOptions: [.additional("Resources/iOSSupport/**")], dependencies: [ - .target(name: "Domain"), .target(name: "iOSSupport"), - .target(name: "WordDetail"), - .external(name: ExternalDependencyName.rxSwift), - .external(name: ExternalDependencyName.rxCocoa), - .external(name: ExternalDependencyName.rxUtilityDynamic), - .external(name: ExternalDependencyName.reactorKit), - .external(name: ExternalDependencyName.snapKit), - .external(name: ExternalDependencyName.then), - .external(name: ExternalDependencyName.toast), - .external(name: ExternalDependencyName.sfSafeSymbols), - .external(name: ExternalDependencyName.swinject), - .external(name: ExternalDependencyName.swinjectExtension), ], hasTests: true, additionalTestDependencies: [ @@ -175,17 +155,7 @@ func targets() -> [Target] { sourcesPrefix: "iOSScenes", resourceOptions: [.additional("Resources/iOSSupport/**")], dependencies: [ - .target(name: "Domain"), .target(name: "iOSSupport"), - .external(name: ExternalDependencyName.rxSwift), - .external(name: ExternalDependencyName.rxCocoa), - .external(name: ExternalDependencyName.rxUtilityDynamic), - .external(name: ExternalDependencyName.reactorKit), - .external(name: ExternalDependencyName.snapKit), - .external(name: ExternalDependencyName.then), - .external(name: ExternalDependencyName.toast), - .external(name: ExternalDependencyName.swinject), - .external(name: ExternalDependencyName.swinjectExtension), ], hasTests: true, additionalTestDependencies: [ @@ -200,18 +170,7 @@ func targets() -> [Target] { sourcesPrefix: "iOSScenes", resourceOptions: [.additional("Resources/iOSSupport/**")], dependencies: [ - .target(name: "Domain"), .target(name: "iOSSupport"), - .target(name: "FoundationExtension"), - .external(name: ExternalDependencyName.rxSwift), - .external(name: ExternalDependencyName.rxCocoa), - .external(name: ExternalDependencyName.rxUtilityDynamic), - .external(name: ExternalDependencyName.reactorKit), - .external(name: ExternalDependencyName.snapKit), - .external(name: ExternalDependencyName.then), - .external(name: ExternalDependencyName.toast), - .external(name: ExternalDependencyName.swinject), - .external(name: ExternalDependencyName.swinjectExtension), ], hasTests: true, additionalTestDependencies: [ @@ -226,17 +185,7 @@ func targets() -> [Target] { sourcesPrefix: "iOSScenes", resourceOptions: [.additional("Resources/iOSSupport/**")], dependencies: [ - .target(name: "Domain"), .target(name: "iOSSupport"), - .external(name: ExternalDependencyName.rxSwift), - .external(name: ExternalDependencyName.rxCocoa), - .external(name: ExternalDependencyName.rxUtilityDynamic), - .external(name: ExternalDependencyName.reactorKit), - .external(name: ExternalDependencyName.snapKit), - .external(name: ExternalDependencyName.then), - .external(name: ExternalDependencyName.toast), - .external(name: ExternalDependencyName.swinject), - .external(name: ExternalDependencyName.swinjectExtension), ], hasTests: true, additionalTestDependencies: [ @@ -263,18 +212,7 @@ func targets() -> [Target] { sourcesPrefix: "iOSScenes", resourceOptions: [.additional("Resources/iOSSupport/**")], dependencies: [ - .target(name: "Domain"), .target(name: "iOSSupport"), - .target(name: "FoundationExtension"), - .external(name: ExternalDependencyName.rxSwift), - .external(name: ExternalDependencyName.rxCocoa), - .external(name: ExternalDependencyName.rxUtilityDynamic), - .external(name: ExternalDependencyName.reactorKit), - .external(name: ExternalDependencyName.snapKit), - .external(name: ExternalDependencyName.then), - .external(name: ExternalDependencyName.toast), - .external(name: ExternalDependencyName.swinject), - .external(name: ExternalDependencyName.swinjectExtension), ], hasTests: true, additionalTestDependencies: [ @@ -289,17 +227,7 @@ func targets() -> [Target] { sourcesPrefix: "iOSScenes", resourceOptions: [.additional("Resources/iOSSupport/**")], dependencies: [ - .target(name: "Domain"), .target(name: "iOSSupport"), - .target(name: "FoundationExtension"), - .external(name: ExternalDependencyName.rxSwift), - .external(name: ExternalDependencyName.rxCocoa), - .external(name: ExternalDependencyName.rxUtilityDynamic), - .external(name: ExternalDependencyName.reactorKit), - .external(name: ExternalDependencyName.snapKit), - .external(name: ExternalDependencyName.then), - .external(name: ExternalDependencyName.swinject), - .external(name: ExternalDependencyName.swinjectExtension), ], hasTests: true, additionalTestDependencies: [ @@ -314,12 +242,6 @@ func targets() -> [Target] { resourceOptions: [.additional("Resources/iOSSupport/**")], dependencies: [ .target(name: "iOSSupport"), - .external(name: ExternalDependencyName.rxSwift), - .external(name: ExternalDependencyName.rxCocoa), - .external(name: ExternalDependencyName.rxUtilityDynamic), - .external(name: ExternalDependencyName.reactorKit), - .external(name: ExternalDependencyName.swinject), - .external(name: ExternalDependencyName.swinjectExtension), ], hasTests: true, additionalTestDependencies: [ @@ -345,12 +267,6 @@ func targets() -> [Target] { resourceOptions: [.additional("Resources/iOSSupport/**")], dependencies: [ .target(name: "iOSSupport"), - .external(name: ExternalDependencyName.rxSwift), - .external(name: ExternalDependencyName.rxCocoa), - .external(name: ExternalDependencyName.rxUtilityDynamic), - .external(name: ExternalDependencyName.reactorKit), - .external(name: ExternalDependencyName.swinject), - .external(name: ExternalDependencyName.swinjectExtension), ], hasTests: true, additionalTestDependencies: [ @@ -373,11 +289,7 @@ func targets() -> [Target] { .target(name: "GeneralSettings"), .target(name: "Infrastructure"), .target(name: "ThemeSetting"), - .external(name: ExternalDependencyName.swinject), .external(name: ExternalDependencyName.swinjectDIContainer), - .external(name: ExternalDependencyName.sfSafeSymbols), - .external(name: ExternalDependencyName.then), - ], appendSchemeTo: &disposedSchemes ) diff --git a/Sources/Domain/UseCases/NotificationsUseCase.swift b/Sources/Domain/UseCases/NotificationsUseCase.swift index de712324..2c511418 100644 --- a/Sources/Domain/UseCases/NotificationsUseCase.swift +++ b/Sources/Domain/UseCases/NotificationsUseCase.swift @@ -9,7 +9,6 @@ import Foundation import RxSwift import RxUtility -import Then import UserNotifications enum NotificationsUseCaseError: Error { @@ -59,10 +58,9 @@ final class NotificationsUseCase: NotificationsUseCaseProtocol { let setDailyReminderSequence: Single = .create { observer in let unmemorizedWordCount = self.wordRepository.getUnmemorizedList().count - var content: UNMutableNotificationContent = .init().then { - $0.title = DomainString.daily_reminder - $0.sound = .default - } + var content: UNMutableNotificationContent = .init() + content.title = DomainString.daily_reminder + content.sound = .default if unmemorizedWordCount == 0 { content.body = DomainString.daily_reminder_body_message_when_no_words_to_memorize diff --git a/Sources/iOSScenes/WordList/WordSearchResultsController.swift b/Sources/iOSScenes/WordList/WordSearchResultsController.swift index af9912db..9db0be8d 100644 --- a/Sources/iOSScenes/WordList/WordSearchResultsController.swift +++ b/Sources/iOSScenes/WordList/WordSearchResultsController.swift @@ -10,7 +10,6 @@ import iOSSupport import ReactorKit import SwinjectExtension import UIKit -import WordDetail public protocol WordSearchResultsControllerDelegate: AnyObject { diff --git a/graph.png b/graph.png index 0875392861ef365444ff4f5a3984261099805152..9479a285c24732851ec1f1730de0c8422b22d684 100644 GIT binary patch literal 140488 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ⅇ1 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
v+0RT4c~K7nNq9T+#l_P{5{2edddI&NMHQi< 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 Date: Thu, 8 Feb 2024 17:20:29 +0900 Subject: [PATCH 13/24] Update tuist template files --- Tuist/Templates/ViewController.stencil | 4 +--- Tuist/Templates/iPhoneCoordinator.stencil | 23 ++++------------------- 2 files changed, 5 insertions(+), 22 deletions(-) diff --git a/Tuist/Templates/ViewController.stencil b/Tuist/Templates/ViewController.stencil index 81bc5333..27e9f11a 100644 --- a/Tuist/Templates/ViewController.stencil +++ b/Tuist/Templates/ViewController.stencil @@ -2,9 +2,7 @@ import iOSSupport import ReactorKit import UIKit -public protocol {{ name }}ViewControllerDelegate: AnyObject { - // An example if you use navigation stack. - func willPopView() +public protocol {{ name }}ViewControllerDelegate: AnyObject, ViewControllerDelegate { } public protocol {{ name }}ViewControllerProtocol: UIViewController { diff --git a/Tuist/Templates/iPhoneCoordinator.stencil b/Tuist/Templates/iPhoneCoordinator.stencil index df44a5b6..444d1e82 100644 --- a/Tuist/Templates/iPhoneCoordinator.stencil +++ b/Tuist/Templates/iPhoneCoordinator.stencil @@ -2,21 +2,13 @@ import iOSSupport import SwinjectDIContainer import SwinjectExtension import UIKit -import {{ name }} - -final class {{ name }}Coordinator: Coordinator { - weak var parentCoordinator: Coordinator? - var childCoordinators: [Coordinator] = [] - - let navigationController: UINavigationController +import {{ name }} - init(navigationController: UINavigationController) { - self.navigationController = navigationController - } +final class {{ name }}Coordinator: BasicCoordinator { - func start() { - // An example if you use navigation stack. + override func start() { + // An example if you push view controller. let viewController: {{ name }}ViewControllerProtocol = DIContainer.shared.resolver.resolve() viewController.delegate = self navigationController.pushViewController(viewController, animated: true) @@ -31,11 +23,4 @@ final class {{ name }}Coordinator: Coordinator { } extension {{ name }}Coordinator: {{ name }}ViewControllerDelegate { - - // An example if you use navigation stack. - func willPopView() { - navigationController.popViewController(animated: true) - parentCoordinator?.childCoordinators.removeAll(where: { $0 === self }) - } - } From 6dfa27f0d309d2e35ed9ddf7323b1beefae5ac0a Mon Sep 17 00:00:00 2001 From: Jaewon Yun Date: Thu, 8 Feb 2024 17:46:25 +0900 Subject: [PATCH 14/24] Add default value to GlobalState --- .../AppDelegate.swift | 1 - .../WordCheckingExample/AppDelegate.swift | 2 -- Sources/iOSSupport/Common/GlobalState.swift | 18 +++++++++++++----- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/Sources/iOSScenes/PushNotificationSettingsExample/AppDelegate.swift b/Sources/iOSScenes/PushNotificationSettingsExample/AppDelegate.swift index d1b5ea45..8d0edda5 100644 --- a/Sources/iOSScenes/PushNotificationSettingsExample/AppDelegate.swift +++ b/Sources/iOSScenes/PushNotificationSettingsExample/AppDelegate.swift @@ -12,7 +12,6 @@ import UIKit class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - GlobalState.shared.initialize(hapticsIsOn: true, themeStyle: .unspecified) return true } diff --git a/Sources/iOSScenes/WordCheckingExample/AppDelegate.swift b/Sources/iOSScenes/WordCheckingExample/AppDelegate.swift index 00a6970f..7010d807 100644 --- a/Sources/iOSScenes/WordCheckingExample/AppDelegate.swift +++ b/Sources/iOSScenes/WordCheckingExample/AppDelegate.swift @@ -5,7 +5,6 @@ // Created by Jaewon Yun on 2023/08/23. // -import iOSSupport import UIKit import Utility @@ -14,7 +13,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { NetworkMonitor.start() - GlobalState.shared.initialize(hapticsIsOn: true, themeStyle: .unspecified) return true } diff --git a/Sources/iOSSupport/Common/GlobalState.swift b/Sources/iOSSupport/Common/GlobalState.swift index 7395fa50..e4f51054 100644 --- a/Sources/iOSSupport/Common/GlobalState.swift +++ b/Sources/iOSSupport/Common/GlobalState.swift @@ -12,18 +12,26 @@ import RxRelay /// 앱의 전역 상태를 가지는 객체입니다. /// -/// - Warning: 앱의 전역 상태를 나타내므로 반드시 Singleton 으로 사용해야 합니다. +/// 전역 상태를 초기화하기 위해 `initialize()` 인스턴스 함수를 호출하는것이 권장됩니다. +/// +/// - Note: 앱의 전역 상태를 나타내므로 공유 객체로 정의되어 있습니다. public final class GlobalState { public static let shared: GlobalState = .init() - private init() {} + /// 고정된 상태 값으로 객체를 초기화합니다. + private init() { + self.hapticsIsOn = true + self.themeStyle = .init(value: .unspecified) + } - public var hapticsIsOn: Bool! + public var hapticsIsOn: Bool - public var themeStyle: BehaviorRelay! + public var themeStyle: BehaviorRelay - /// 전역 상태를 초기화 합니다. + /// 전역 상태를 초기화합니다. + /// + /// 전달된 파라미터를 이용하여 전역 상태를 초기화합니다. 이 함수를 호출하여 초기화하지 않으면, 내부 생성자에서 고정된 값으로 전역 상태가 초기화됩니다. public func initialize(hapticsIsOn: Bool, themeStyle: UIUserInterfaceStyle) { self.hapticsIsOn = hapticsIsOn self.themeStyle = .init(value: themeStyle) From 126e3c65a5ee4d07f392f306dd25ddcdf5d070c6 Mon Sep 17 00:00:00 2001 From: Jaewon Yun Date: Fri, 9 Feb 2024 15:52:54 +0900 Subject: [PATCH 15/24] Rename GlobalAction --- .../iOSScenes/LanguageSetting/LanguageSettingReactor.swift | 4 ++-- .../PushNotificationSettingsReactor.swift | 4 ++-- Sources/iOSScenes/UserSettings/UserSettingsReactor.swift | 4 ++-- Sources/iOSScenes/WordAddition/WordAdditionViewModel.swift | 2 +- Sources/iOSScenes/WordChecking/WordCheckingReactor.swift | 4 ++-- Sources/iOSScenes/WordDetail/WordDetailReactor.swift | 4 ++-- Sources/iOSScenes/WordList/WordListReactor.swift | 4 ++-- .../Common/{GlobalReactorAction.swift => GlobalAction.swift} | 4 ++-- Sources/iPhoneDriver/SceneDelegate.swift | 2 +- 9 files changed, 16 insertions(+), 16 deletions(-) rename Sources/iOSSupport/Common/{GlobalReactorAction.swift => GlobalAction.swift} (88%) diff --git a/Sources/iOSScenes/LanguageSetting/LanguageSettingReactor.swift b/Sources/iOSScenes/LanguageSetting/LanguageSettingReactor.swift index b750a9b3..f0ddde78 100644 --- a/Sources/iOSScenes/LanguageSetting/LanguageSettingReactor.swift +++ b/Sources/iOSScenes/LanguageSetting/LanguageSettingReactor.swift @@ -32,12 +32,12 @@ final class LanguageSettingReactor: Reactor { let userSettingsUseCase: UserSettingsUseCaseProtocol - let globalAction: GlobalReactorAction + let globalAction: GlobalAction init( translationDirection: TranslationDirection, userSettingsUseCase: UserSettingsUseCaseProtocol, - globalAction: GlobalReactorAction + globalAction: GlobalAction ) { self.initialState = .init(translationDirection: translationDirection, selectedCell: .init()) self.userSettingsUseCase = userSettingsUseCase diff --git a/Sources/iOSScenes/PushNotificationSettings/PushNotificationSettingsReactor.swift b/Sources/iOSScenes/PushNotificationSettings/PushNotificationSettingsReactor.swift index 75447f1f..b993e3e5 100644 --- a/Sources/iOSScenes/PushNotificationSettings/PushNotificationSettingsReactor.swift +++ b/Sources/iOSScenes/PushNotificationSettings/PushNotificationSettingsReactor.swift @@ -40,9 +40,9 @@ final class PushNotificationSettingsReactor: Reactor { ) let notificationsUseCase: NotificationsUseCaseProtocol - let globalAction: GlobalReactorAction + let globalAction: GlobalAction - init(notificationsUseCase: NotificationsUseCaseProtocol, globalAction: GlobalReactorAction) { + init(notificationsUseCase: NotificationsUseCaseProtocol, globalAction: GlobalAction) { self.notificationsUseCase = notificationsUseCase self.globalAction = globalAction } diff --git a/Sources/iOSScenes/UserSettings/UserSettingsReactor.swift b/Sources/iOSScenes/UserSettings/UserSettingsReactor.swift index ca22cdc9..f3e2838a 100644 --- a/Sources/iOSScenes/UserSettings/UserSettingsReactor.swift +++ b/Sources/iOSScenes/UserSettings/UserSettingsReactor.swift @@ -47,12 +47,12 @@ final class UserSettingsReactor: Reactor { let userSettingsUseCase: UserSettingsUseCaseProtocol let googleDriveUseCase: ExternalStoreUseCaseProtocol - let globalAction: GlobalReactorAction + let globalAction: GlobalAction init( userSettingsUseCase: UserSettingsUseCaseProtocol, googleDriveUseCase: ExternalStoreUseCaseProtocol, - globalAction: GlobalReactorAction + globalAction: GlobalAction ) { self.userSettingsUseCase = userSettingsUseCase self.googleDriveUseCase = googleDriveUseCase diff --git a/Sources/iOSScenes/WordAddition/WordAdditionViewModel.swift b/Sources/iOSScenes/WordAddition/WordAdditionViewModel.swift index 0f32cad4..e62c288c 100644 --- a/Sources/iOSScenes/WordAddition/WordAdditionViewModel.swift +++ b/Sources/iOSScenes/WordAddition/WordAdditionViewModel.swift @@ -36,7 +36,7 @@ final class WordAdditionViewModel: ViewModelType { .asSignalOnErrorJustComplete() } .doOnNext { _ in - GlobalReactorAction.shared.didAddWord.accept(()) + GlobalAction.shared.didAddWord.accept(()) } let wordTextIsNotEmpty = input.wordText.map(\.isNotEmpty) diff --git a/Sources/iOSScenes/WordChecking/WordCheckingReactor.swift b/Sources/iOSScenes/WordChecking/WordCheckingReactor.swift index f5f55610..d0644cd6 100644 --- a/Sources/iOSScenes/WordChecking/WordCheckingReactor.swift +++ b/Sources/iOSScenes/WordChecking/WordCheckingReactor.swift @@ -43,12 +43,12 @@ final class WordCheckingReactor: Reactor { let wordUseCase: WordUseCaseProtocol let userSettingsUseCase: UserSettingsUseCaseProtocol - let globalAction: GlobalReactorAction + let globalAction: GlobalAction init( wordUseCase: WordUseCaseProtocol, userSettingsUseCase: UserSettingsUseCaseProtocol, - globalAction: GlobalReactorAction + globalAction: GlobalAction ) { self.wordUseCase = wordUseCase self.userSettingsUseCase = userSettingsUseCase diff --git a/Sources/iOSScenes/WordDetail/WordDetailReactor.swift b/Sources/iOSScenes/WordDetail/WordDetailReactor.swift index 77d84d86..a80bc71f 100644 --- a/Sources/iOSScenes/WordDetail/WordDetailReactor.swift +++ b/Sources/iOSScenes/WordDetail/WordDetailReactor.swift @@ -39,12 +39,12 @@ final class WordDetailReactor: Reactor { /// 편집되기 전 원래 단어입니다. viewDidLoad 가 호출될 때 초기화됩니다. private(set) var originWord: String? - let globalAction: GlobalReactorAction + let globalAction: GlobalAction let wordUseCase: WordUseCaseProtocol init( uuid: UUID, - globalAction: GlobalReactorAction, + globalAction: GlobalAction, wordUseCase: WordUseCaseProtocol ) { self.uuid = uuid diff --git a/Sources/iOSScenes/WordList/WordListReactor.swift b/Sources/iOSScenes/WordList/WordListReactor.swift index df8245aa..3cf5bddc 100644 --- a/Sources/iOSScenes/WordList/WordListReactor.swift +++ b/Sources/iOSScenes/WordList/WordListReactor.swift @@ -33,10 +33,10 @@ final class WordListReactor: Reactor { var initialState: State = State(listType: .all, wordList: []) - let globalAction: GlobalReactorAction + let globalAction: GlobalAction let wordUseCase: WordUseCaseProtocol - init(globalAction: GlobalReactorAction, wordUseCase: WordUseCaseProtocol) { + init(globalAction: GlobalAction, wordUseCase: WordUseCaseProtocol) { self.globalAction = globalAction self.wordUseCase = wordUseCase } diff --git a/Sources/iOSSupport/Common/GlobalReactorAction.swift b/Sources/iOSSupport/Common/GlobalAction.swift similarity index 88% rename from Sources/iOSSupport/Common/GlobalReactorAction.swift rename to Sources/iOSSupport/Common/GlobalAction.swift index 7573acd0..8c788be6 100644 --- a/Sources/iOSSupport/Common/GlobalReactorAction.swift +++ b/Sources/iOSSupport/Common/GlobalAction.swift @@ -11,9 +11,9 @@ import Foundation import RxSwift import RxCocoa -public final class GlobalReactorAction { +public final class GlobalAction { - public static let shared: GlobalReactorAction = .init() + public static let shared: GlobalAction = .init() private init() {} diff --git a/Sources/iPhoneDriver/SceneDelegate.swift b/Sources/iPhoneDriver/SceneDelegate.swift index 3c81cdac..082c1c0e 100644 --- a/Sources/iPhoneDriver/SceneDelegate.swift +++ b/Sources/iPhoneDriver/SceneDelegate.swift @@ -17,7 +17,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { var appCoordinator: AppCoordinator? - let globalAction: GlobalReactorAction = .shared + let globalAction: GlobalAction = .shared let globalState: GlobalState = .shared func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { From 27037e372309a8681936b75f5a02cd2130a4833d Mon Sep 17 00:00:00 2001 From: Jaewon Yun Date: Sat, 10 Feb 2024 19:10:06 +0900 Subject: [PATCH 16/24] Make FoundationExtension module external --- Project.swift | 7 +---- .../UseCases/NotificationsUseCase.swift | 1 + .../FoundationExtension/AllCases+index.swift | 23 --------------- .../Collection+isNotEmpty.swift | 16 ----------- .../Concurrency/Sequence+asyncForEach.swift | 19 ------------- Sources/Utility/Concurrency/Task+.swift | 28 ------------------- .../LanguageSettingReactor.swift | 2 +- .../LanguageSettingViewController.swift | 2 +- .../ThemeSettingViewController.swift | 2 +- .../WordAddition/WordAdditionViewModel.swift | 2 +- TestPlans/FoundationExtension.xctestplan | 24 ---------------- TestPlans/IntergrationTests.xctestplan | 14 +++++----- .../FoundationExtensionTests.swift | 18 ------------ Tuist/Dependencies.swift | 3 ++ .../ExternalDependencyName.swift | 1 + 15 files changed, 17 insertions(+), 145 deletions(-) delete mode 100644 Sources/FoundationExtension/AllCases+index.swift delete mode 100644 Sources/FoundationExtension/Collection+isNotEmpty.swift delete mode 100644 Sources/Utility/Concurrency/Sequence+asyncForEach.swift delete mode 100644 Sources/Utility/Concurrency/Task+.swift delete mode 100644 TestPlans/FoundationExtension.xctestplan delete mode 100644 Tests/FoundationExtensionTests/FoundationExtensionTests.swift diff --git a/Project.swift b/Project.swift index b0cda6bd..183033e1 100644 --- a/Project.swift +++ b/Project.swift @@ -41,11 +41,6 @@ func targets() -> [Target] { ], appendSchemeTo: &disposedSchemes ) - + Target.module( - name: "FoundationExtension", - hasTests: true, - appendSchemeTo: &schemes - ) + Target.module( name: "Utility", scripts: [ @@ -94,7 +89,7 @@ func targets() -> [Target] { dependencies: [ .target(name: "Domain"), .target(name: "Utility"), - .target(name: "FoundationExtension"), + .external(name: ExternalDependencyName.foundationPlus), .external(name: ExternalDependencyName.rxSwift), .external(name: ExternalDependencyName.rxCocoa), .external(name: ExternalDependencyName.rxUtilityDynamic), diff --git a/Sources/Domain/UseCases/NotificationsUseCase.swift b/Sources/Domain/UseCases/NotificationsUseCase.swift index 2c511418..3dcddb94 100644 --- a/Sources/Domain/UseCases/NotificationsUseCase.swift +++ b/Sources/Domain/UseCases/NotificationsUseCase.swift @@ -7,6 +7,7 @@ // import Foundation +import FoundationPlus import RxSwift import RxUtility import UserNotifications diff --git a/Sources/FoundationExtension/AllCases+index.swift b/Sources/FoundationExtension/AllCases+index.swift deleted file mode 100644 index 9f129706..00000000 --- a/Sources/FoundationExtension/AllCases+index.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// AllCases+index.swift -// iOSSupport -// -// Created by Jaewon Yun on 2/5/24. -// Copyright © 2024 woin2ee. All rights reserved. -// - -import Foundation - -extension Array where Element: CaseIterable & Equatable { - - /// AllCases 배열에서 주어진 열거형 값에 해당하는 index 를 반환합니다. - /// - Parameter element: 열거형 값 - /// - Returns: 주어진 열거형 값에 해당하는 index - public func index(of element: Element) -> Int { - guard let index = self.firstIndex(of: element) else { - fatalError("This method was used where it was not appropriate.") - } - return index - } - -} diff --git a/Sources/FoundationExtension/Collection+isNotEmpty.swift b/Sources/FoundationExtension/Collection+isNotEmpty.swift deleted file mode 100644 index 3f0c91e6..00000000 --- a/Sources/FoundationExtension/Collection+isNotEmpty.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// Collection+isNotEmpty.swift -// ItsME -// -// Created by Jaewon Yun on 2023/03/24. -// - -import Foundation - -extension Collection { - - /// A Boolean value indicating whether the collection is not empty. - public var isNotEmpty: Bool { - !isEmpty - } -} diff --git a/Sources/Utility/Concurrency/Sequence+asyncForEach.swift b/Sources/Utility/Concurrency/Sequence+asyncForEach.swift deleted file mode 100644 index 39143c4b..00000000 --- a/Sources/Utility/Concurrency/Sequence+asyncForEach.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// Sequence+asyncForEach.swift -// iOSCore -// -// Created by Jaewon Yun on 2023/10/06. -// Copyright © 2023 woin2ee. All rights reserved. -// - -import Foundation - -extension Sequence { - - public func asyncForEach(_ body: (Self.Element) async throws -> Void) async rethrows { - for element in self { - try await body(element) - } - } - -} diff --git a/Sources/Utility/Concurrency/Task+.swift b/Sources/Utility/Concurrency/Task+.swift deleted file mode 100644 index 0bce10bb..00000000 --- a/Sources/Utility/Concurrency/Task+.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// Task+.swift -// iOSCore -// -// Created by Jaewon Yun on 2023/10/06. -// Copyright © 2023 woin2ee. All rights reserved. -// - -import Foundation - -extension Task where Success == Void, Failure == Never { - - @discardableResult - public init( - priority: TaskPriority? = nil, - operation: @escaping (() async throws -> Void), - catch: @escaping ((Error) -> Void) - ) { - self.init(priority: priority) { - do { - try await operation() - } catch { - `catch`(error) - } - } - } - -} diff --git a/Sources/iOSScenes/LanguageSetting/LanguageSettingReactor.swift b/Sources/iOSScenes/LanguageSetting/LanguageSettingReactor.swift index f0ddde78..fb774532 100644 --- a/Sources/iOSScenes/LanguageSetting/LanguageSettingReactor.swift +++ b/Sources/iOSScenes/LanguageSetting/LanguageSettingReactor.swift @@ -8,7 +8,7 @@ import Domain import Foundation -import FoundationExtension +import FoundationPlus import iOSSupport import ReactorKit diff --git a/Sources/iOSScenes/LanguageSetting/LanguageSettingViewController.swift b/Sources/iOSScenes/LanguageSetting/LanguageSettingViewController.swift index 6e955c29..29504319 100644 --- a/Sources/iOSScenes/LanguageSetting/LanguageSettingViewController.swift +++ b/Sources/iOSScenes/LanguageSetting/LanguageSettingViewController.swift @@ -7,7 +7,7 @@ // import Domain -import FoundationExtension +import FoundationPlus import iOSSupport import OrderedCollections import ReactorKit diff --git a/Sources/iOSScenes/ThemeSetting/ThemeSettingViewController.swift b/Sources/iOSScenes/ThemeSetting/ThemeSettingViewController.swift index 7a2b7689..cbc9e2bb 100644 --- a/Sources/iOSScenes/ThemeSetting/ThemeSettingViewController.swift +++ b/Sources/iOSScenes/ThemeSetting/ThemeSettingViewController.swift @@ -1,4 +1,4 @@ -import FoundationExtension +import FoundationPlus import iOSSupport import OrderedCollections import ReactorKit diff --git a/Sources/iOSScenes/WordAddition/WordAdditionViewModel.swift b/Sources/iOSScenes/WordAddition/WordAdditionViewModel.swift index e62c288c..812d08fb 100644 --- a/Sources/iOSScenes/WordAddition/WordAdditionViewModel.swift +++ b/Sources/iOSScenes/WordAddition/WordAdditionViewModel.swift @@ -7,7 +7,7 @@ import Domain import Foundation -import FoundationExtension +import FoundationPlus import iOSSupport import RxSwift import RxCocoa diff --git a/TestPlans/FoundationExtension.xctestplan b/TestPlans/FoundationExtension.xctestplan deleted file mode 100644 index 6c803c28..00000000 --- a/TestPlans/FoundationExtension.xctestplan +++ /dev/null @@ -1,24 +0,0 @@ -{ - "configurations" : [ - { - "id" : "8F663F64-D2ED-466D-B509-5E41EF98731F", - "name" : "Configuration 1", - "options" : { - - } - } - ], - "defaultOptions" : { - "testTimeoutsEnabled" : true - }, - "testTargets" : [ - { - "target" : { - "containerPath" : "container:WordChecker.xcodeproj", - "identifier" : "0D0A048C4E8A1F30CBB8F2BE", - "name" : "FoundationExtensionTests" - } - } - ], - "version" : 1 -} diff --git a/TestPlans/IntergrationTests.xctestplan b/TestPlans/IntergrationTests.xctestplan index 17d9cfe5..c04a423c 100644 --- a/TestPlans/IntergrationTests.xctestplan +++ b/TestPlans/IntergrationTests.xctestplan @@ -148,13 +148,6 @@ "name" : "WordListTests" } }, - { - "target" : { - "containerPath" : "container:WordChecker.xcodeproj", - "identifier" : "0D0A048C4E8A1F30CBB8F2BE", - "name" : "FoundationExtensionTests" - } - }, { "target" : { "containerPath" : "container:WordChecker.xcodeproj", @@ -175,6 +168,13 @@ "identifier" : "7C78BC2C06BF23C70DEE14AE", "name" : "InfrastructureTests" } + }, + { + "target" : { + "containerPath" : "container:WordChecker.xcodeproj", + "identifier" : "E5AF7A03E6E737AD9E4136ED", + "name" : "ThemeSettingTests" + } } ], "version" : 1 diff --git a/Tests/FoundationExtensionTests/FoundationExtensionTests.swift b/Tests/FoundationExtensionTests/FoundationExtensionTests.swift deleted file mode 100644 index 7fce248d..00000000 --- a/Tests/FoundationExtensionTests/FoundationExtensionTests.swift +++ /dev/null @@ -1,18 +0,0 @@ -@testable import FoundationExtension - -import XCTest - -final class FoundationExtensionTests: XCTestCase { - - func test_isNotEmpty() { - // Given - let string: String = "" - - // When - let isNotEmpty = string.isNotEmpty - - // Then - XCTAssertEqual(isNotEmpty, false) - } - -} diff --git a/Tuist/Dependencies.swift b/Tuist/Dependencies.swift index bc6f36d4..4b574472 100644 --- a/Tuist/Dependencies.swift +++ b/Tuist/Dependencies.swift @@ -43,6 +43,9 @@ let dependencies = Dependencies( // ReactorKit .remote(url: "https://github.com/ReactorKit/ReactorKit.git", requirement: .upToNextMajor(from: "3.0.0")), + // ReactorKit + .remote(url: "https://github.com/Woin2ee-Modules/FoundationPlus.git", + requirement: .upToNextMajor(from: "1.0.0")), ], platforms: [.iOS] ) diff --git a/Tuist/ProjectDescriptionHelpers/ExternalDependencyName.swift b/Tuist/ProjectDescriptionHelpers/ExternalDependencyName.swift index c061ab3b..1607f572 100644 --- a/Tuist/ProjectDescriptionHelpers/ExternalDependencyName.swift +++ b/Tuist/ProjectDescriptionHelpers/ExternalDependencyName.swift @@ -23,5 +23,6 @@ public struct ExternalDependencyName { public static let googleAPIClientForRESTCore = "GoogleAPIClientForRESTCore" public static let reactorKit = "ReactorKit" public static let swiftCollections = "Collections" + public static let foundationPlus = "FoundationPlus" } From 03cd9da041ddc04d63feecbbf0e8b49e18ed9b2a Mon Sep 17 00:00:00 2001 From: Jaewon Yun Date: Sun, 11 Feb 2024 01:25:22 +0900 Subject: [PATCH 17/24] Rename modules --- Project.swift | 72 +++++++++---------- .../IPhoneAppDelegate.swift} | 8 +-- Sources/WordChecker/AppDelegate.swift | 4 +- Sources/WordCheckerDev/AppDelegate.swift | 4 +- .../GeneralSettingsReactor.swift | 2 +- .../GeneralSettingsViewController.swift | 2 +- .../LanguageSettingReactor.swift | 2 +- .../LanguageSettingViewController.swift | 2 +- .../Cells/DatePickerCell.swift | 2 +- .../PushNotificationSettingsReactor.swift | 2 +- .../PushNotificationSettingsView.swift | 2 +- ...shNotificationSettingsViewController.swift | 2 +- .../AppDelegate.swift | 2 +- .../ThemeSetting/ThemeSettingReactor.swift | 2 +- .../ThemeSettingViewController.swift | 2 +- .../UserSettingsItemModel.swift | 2 +- .../UserSettings/UserSettingsReactor.swift | 2 +- .../UserSettingsViewController.swift | 2 +- .../WordAdditionViewController.swift | 2 +- .../WordAddition/WordAdditionViewModel.swift | 2 +- .../WordChecking+ChangeWordButton.swift | 2 +- .../TranslationWebViewController.swift | 2 +- .../WordChecking/WordCheckingReactor.swift | 2 +- .../WordChecking/WordCheckingView.swift | 2 +- .../WordCheckingViewController.swift | 2 +- .../WordDetail/WordDetailReactor.swift | 2 +- .../WordDetail/WordDetailViewController.swift | 2 +- .../iOSScenes/WordList/WordListReactor.swift | 2 +- .../WordList/WordListViewController.swift | 2 +- .../WordSearchResultsController.swift | 2 +- .../Coordinators/AppCoordinator.swift | 2 +- .../Coordinators/BasicCoordinator.swift | 6 +- .../GeneralSettingsCoordinator.swift | 2 +- .../LanguageSettingCoordinator.swift | 2 +- .../ThemeSettingCoordinator.swift | 2 +- .../UserSettingsCoordinator.swift | 2 +- .../WordAdditionCoordinator.swift | 2 +- .../WordCheckingCoordinator.swift | 2 +- .../Coordinators/WordDetailCoordinator.swift | 2 +- .../Coordinators/WordListCoordinator.swift | 2 +- .../iPhoneDriver/RootTabBarController.swift | 2 +- Sources/iPhoneDriver/SceneDelegate.swift | 2 +- .../WordCheckerUITests.swift | 2 +- .../IOSScene.swift} | 14 ++-- Tuist/Templates/ViewController.stencil | 2 +- Tuist/Templates/iPhoneCoordinator.stencil | 2 +- 46 files changed, 94 insertions(+), 94 deletions(-) rename Sources/{iPhoneDriver/iPhoneAppDelegate.swift => IPhoneDriver/IPhoneAppDelegate.swift} (95%) rename Tuist/Templates/{iOSScene/iOSScene.swift => IOSScene/IOSScene.swift} (68%) diff --git a/Project.swift b/Project.swift index 183033e1..28c76ca5 100644 --- a/Project.swift +++ b/Project.swift @@ -84,7 +84,7 @@ func targets() -> [Target] { appendSchemeTo: &disposedSchemes ) + Target.module( - name: "iOSSupport", + name: "IOSSupport", resourceOptions: [.own], dependencies: [ .target(name: "Domain"), @@ -106,10 +106,10 @@ func targets() -> [Target] { ) + Target.module( name: "WordChecking", - sourcesPrefix: "iOSScenes", - resourceOptions: [.additional("Resources/iOSSupport/**")], + sourcesPrefix: "IOSScenes", + resourceOptions: [.additional("Resources/IOSSupport/**")], dependencies: [ - .target(name: "iOSSupport"), + .target(name: "IOSSupport"), ], hasTests: true, additionalTestDependencies: [ @@ -123,7 +123,7 @@ func targets() -> [Target] { name: "WordCheckingExample", product: .app, infoPlist: .file(path: "Resources/InfoPlist/InfoExample.plist"), - sourcesPrefix: "iOSScenes", + sourcesPrefix: "IOSScenes", dependencies: [ .target(name: "WordChecking"), .target(name: "DomainTesting"), @@ -132,10 +132,10 @@ func targets() -> [Target] { ) + Target.module( name: "WordList", - sourcesPrefix: "iOSScenes", - resourceOptions: [.additional("Resources/iOSSupport/**")], + sourcesPrefix: "IOSScenes", + resourceOptions: [.additional("Resources/IOSSupport/**")], dependencies: [ - .target(name: "iOSSupport"), + .target(name: "IOSSupport"), ], hasTests: true, additionalTestDependencies: [ @@ -147,10 +147,10 @@ func targets() -> [Target] { ) + Target.module( name: "WordDetail", - sourcesPrefix: "iOSScenes", - resourceOptions: [.additional("Resources/iOSSupport/**")], + sourcesPrefix: "IOSScenes", + resourceOptions: [.additional("Resources/IOSSupport/**")], dependencies: [ - .target(name: "iOSSupport"), + .target(name: "IOSSupport"), ], hasTests: true, additionalTestDependencies: [ @@ -162,10 +162,10 @@ func targets() -> [Target] { ) + Target.module( name: "WordAddition", - sourcesPrefix: "iOSScenes", - resourceOptions: [.additional("Resources/iOSSupport/**")], + sourcesPrefix: "IOSScenes", + resourceOptions: [.additional("Resources/IOSSupport/**")], dependencies: [ - .target(name: "iOSSupport"), + .target(name: "IOSSupport"), ], hasTests: true, additionalTestDependencies: [ @@ -177,10 +177,10 @@ func targets() -> [Target] { ) + Target.module( name: "UserSettings", - sourcesPrefix: "iOSScenes", - resourceOptions: [.additional("Resources/iOSSupport/**")], + sourcesPrefix: "IOSScenes", + resourceOptions: [.additional("Resources/IOSSupport/**")], dependencies: [ - .target(name: "iOSSupport"), + .target(name: "IOSSupport"), ], hasTests: true, additionalTestDependencies: [ @@ -195,7 +195,7 @@ func targets() -> [Target] { name: "UserSettingsExample", product: .app, infoPlist: .file(path: "Resources/InfoPlist/InfoExample.plist"), - sourcesPrefix: "iOSScenes", + sourcesPrefix: "IOSScenes", dependencies: [ .target(name: "UserSettings"), .target(name: "DomainTesting"), @@ -204,10 +204,10 @@ func targets() -> [Target] { ) + Target.module( name: "LanguageSetting", - sourcesPrefix: "iOSScenes", - resourceOptions: [.additional("Resources/iOSSupport/**")], + sourcesPrefix: "IOSScenes", + resourceOptions: [.additional("Resources/IOSSupport/**")], dependencies: [ - .target(name: "iOSSupport"), + .target(name: "IOSSupport"), ], hasTests: true, additionalTestDependencies: [ @@ -219,10 +219,10 @@ func targets() -> [Target] { ) + Target.module( name: "ThemeSetting", - sourcesPrefix: "iOSScenes", - resourceOptions: [.additional("Resources/iOSSupport/**")], + sourcesPrefix: "IOSScenes", + resourceOptions: [.additional("Resources/IOSSupport/**")], dependencies: [ - .target(name: "iOSSupport"), + .target(name: "IOSSupport"), ], hasTests: true, additionalTestDependencies: [ @@ -233,10 +233,10 @@ func targets() -> [Target] { ) + Target.module( name: "PushNotificationSettings", - sourcesPrefix: "iOSScenes", - resourceOptions: [.additional("Resources/iOSSupport/**")], + sourcesPrefix: "IOSScenes", + resourceOptions: [.additional("Resources/IOSSupport/**")], dependencies: [ - .target(name: "iOSSupport"), + .target(name: "IOSSupport"), ], hasTests: true, additionalTestDependencies: [ @@ -249,7 +249,7 @@ func targets() -> [Target] { name: "PushNotificationSettingsExample", product: .app, infoPlist: .file(path: "Resources/InfoPlist/InfoExample.plist"), - sourcesPrefix: "iOSScenes", + sourcesPrefix: "IOSScenes", dependencies: [ .target(name: "PushNotificationSettings"), .target(name: "DomainTesting"), @@ -258,10 +258,10 @@ func targets() -> [Target] { ) + Target.module( name: "GeneralSettings", - sourcesPrefix: "iOSScenes", - resourceOptions: [.additional("Resources/iOSSupport/**")], + sourcesPrefix: "IOSScenes", + resourceOptions: [.additional("Resources/IOSSupport/**")], dependencies: [ - .target(name: "iOSSupport"), + .target(name: "IOSSupport"), ], hasTests: true, additionalTestDependencies: [ @@ -271,9 +271,9 @@ func targets() -> [Target] { appendSchemeTo: &schemes ) + Target.module( - name: "iPhoneDriver", + name: "IPhoneDriver", dependencies: [ - .target(name: "iOSSupport"), + .target(name: "IOSSupport"), .target(name: "WordChecking"), .target(name: "WordList"), .target(name: "WordAddition"), @@ -298,7 +298,7 @@ func targets() -> [Target] { .additional("Resources/InfoPlist/Product/**"), ], dependencies: [ - .target(name: "iPhoneDriver"), + .target(name: "IPhoneDriver"), ], settings: .settings(), appendSchemeTo: &schemes @@ -313,7 +313,7 @@ func targets() -> [Target] { .additional("Resources/InfoPlist/Dev/**"), ], dependencies: [ - .target(name: "iPhoneDriver"), + .target(name: "IPhoneDriver"), ], settings: .settings(), appendSchemeTo: &schemes @@ -338,7 +338,7 @@ func targets() -> [Target] { sources: "Tests/\(PROJECT_NAME)UITests/**", dependencies: [ .target(name: "\(PROJECT_NAME)Dev"), - .target(name: "iOSSupport"), + .target(name: "IOSSupport"), .target(name: "Utility"), .package(product: "Realm"), ] diff --git a/Sources/iPhoneDriver/iPhoneAppDelegate.swift b/Sources/IPhoneDriver/IPhoneAppDelegate.swift similarity index 95% rename from Sources/iPhoneDriver/iPhoneAppDelegate.swift rename to Sources/IPhoneDriver/IPhoneAppDelegate.swift index 09e2ba2f..195835fb 100644 --- a/Sources/iPhoneDriver/iPhoneAppDelegate.swift +++ b/Sources/IPhoneDriver/IPhoneAppDelegate.swift @@ -1,5 +1,5 @@ // -// iPhoneAppDelegate.swift +// IPhoneAppDelegate.swift // iPhoneDriver // // Created by Jaewon Yun on 1/30/24. @@ -9,7 +9,7 @@ import Domain import GoogleSignIn import Infrastructure -import iOSSupport +import IOSSupport import RxSwift import UIKit import Utility @@ -30,7 +30,7 @@ import Swinject import SwinjectDIContainer // swiftlint:disable type_name -open class iPhoneAppDelegate: UIResponder, UIApplicationDelegate { +open class IPhoneAppDelegate: UIResponder, UIApplicationDelegate { public func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { initDIContainer() @@ -111,7 +111,7 @@ open class iPhoneAppDelegate: UIResponder, UIApplicationDelegate { } -extension iPhoneAppDelegate: UNUserNotificationCenterDelegate { +extension IPhoneAppDelegate: UNUserNotificationCenterDelegate { public func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse) async { RootTabBarController.shared.selectedViewController = RootTabBarController.shared.wordCheckingNC diff --git a/Sources/WordChecker/AppDelegate.swift b/Sources/WordChecker/AppDelegate.swift index d56f43d0..166e3df6 100644 --- a/Sources/WordChecker/AppDelegate.swift +++ b/Sources/WordChecker/AppDelegate.swift @@ -5,8 +5,8 @@ // Created by Jaewon Yun on 2023/08/23. // -import iPhoneDriver +import IPhoneDriver @main -class AppDelegate: iPhoneAppDelegate { +class AppDelegate: IPhoneAppDelegate { } diff --git a/Sources/WordCheckerDev/AppDelegate.swift b/Sources/WordCheckerDev/AppDelegate.swift index 704b39ae..2b532165 100644 --- a/Sources/WordCheckerDev/AppDelegate.swift +++ b/Sources/WordCheckerDev/AppDelegate.swift @@ -6,7 +6,7 @@ // import Domain -import iPhoneDriver +import IPhoneDriver import Infrastructure // Scenes @@ -25,7 +25,7 @@ import Swinject import SwinjectDIContainer @main -class AppDelegate: iPhoneAppDelegate { +class AppDelegate: IPhoneAppDelegate { override func restoreGoogleSignInState() { // No restore for dev. diff --git a/Sources/iOSScenes/GeneralSettings/GeneralSettingsReactor.swift b/Sources/iOSScenes/GeneralSettings/GeneralSettingsReactor.swift index f53e993b..c64273a1 100644 --- a/Sources/iOSScenes/GeneralSettings/GeneralSettingsReactor.swift +++ b/Sources/iOSScenes/GeneralSettings/GeneralSettingsReactor.swift @@ -7,7 +7,7 @@ // import Domain -import iOSSupport +import IOSSupport import ReactorKit final class GeneralSettingsReactor: Reactor { diff --git a/Sources/iOSScenes/GeneralSettings/GeneralSettingsViewController.swift b/Sources/iOSScenes/GeneralSettings/GeneralSettingsViewController.swift index cc72712c..8db35b03 100644 --- a/Sources/iOSScenes/GeneralSettings/GeneralSettingsViewController.swift +++ b/Sources/iOSScenes/GeneralSettings/GeneralSettingsViewController.swift @@ -6,7 +6,7 @@ // Copyright © 2024 woin2ee. All rights reserved. // -import iOSSupport +import IOSSupport import ReactorKit import RxUtility import Then diff --git a/Sources/iOSScenes/LanguageSetting/LanguageSettingReactor.swift b/Sources/iOSScenes/LanguageSetting/LanguageSettingReactor.swift index fb774532..93770e5f 100644 --- a/Sources/iOSScenes/LanguageSetting/LanguageSettingReactor.swift +++ b/Sources/iOSScenes/LanguageSetting/LanguageSettingReactor.swift @@ -9,7 +9,7 @@ import Domain import Foundation import FoundationPlus -import iOSSupport +import IOSSupport import ReactorKit final class LanguageSettingReactor: Reactor { diff --git a/Sources/iOSScenes/LanguageSetting/LanguageSettingViewController.swift b/Sources/iOSScenes/LanguageSetting/LanguageSettingViewController.swift index 29504319..ffe4085a 100644 --- a/Sources/iOSScenes/LanguageSetting/LanguageSettingViewController.swift +++ b/Sources/iOSScenes/LanguageSetting/LanguageSettingViewController.swift @@ -8,7 +8,7 @@ import Domain import FoundationPlus -import iOSSupport +import IOSSupport import OrderedCollections import ReactorKit import RxSwift diff --git a/Sources/iOSScenes/PushNotificationSettings/Cells/DatePickerCell.swift b/Sources/iOSScenes/PushNotificationSettings/Cells/DatePickerCell.swift index 6834bea8..0c31dc58 100644 --- a/Sources/iOSScenes/PushNotificationSettings/Cells/DatePickerCell.swift +++ b/Sources/iOSScenes/PushNotificationSettings/Cells/DatePickerCell.swift @@ -6,7 +6,7 @@ // Copyright © 2023 woin2ee. All rights reserved. // -import iOSSupport +import IOSSupport import UIKit final class DatePickerCell: RxBaseReusableCell { diff --git a/Sources/iOSScenes/PushNotificationSettings/PushNotificationSettingsReactor.swift b/Sources/iOSScenes/PushNotificationSettings/PushNotificationSettingsReactor.swift index b993e3e5..9bb9c048 100644 --- a/Sources/iOSScenes/PushNotificationSettings/PushNotificationSettingsReactor.swift +++ b/Sources/iOSScenes/PushNotificationSettings/PushNotificationSettingsReactor.swift @@ -8,7 +8,7 @@ import Domain import Foundation -import iOSSupport +import IOSSupport import ReactorKit final class PushNotificationSettingsReactor: Reactor { diff --git a/Sources/iOSScenes/PushNotificationSettings/PushNotificationSettingsView.swift b/Sources/iOSScenes/PushNotificationSettings/PushNotificationSettingsView.swift index fc5580a9..2eafe11e 100644 --- a/Sources/iOSScenes/PushNotificationSettings/PushNotificationSettingsView.swift +++ b/Sources/iOSScenes/PushNotificationSettings/PushNotificationSettingsView.swift @@ -6,7 +6,7 @@ // Copyright © 2023 woin2ee. All rights reserved. // -import iOSSupport +import IOSSupport import Then import UIKit diff --git a/Sources/iOSScenes/PushNotificationSettings/PushNotificationSettingsViewController.swift b/Sources/iOSScenes/PushNotificationSettings/PushNotificationSettingsViewController.swift index 87ba158c..2e9884a9 100644 --- a/Sources/iOSScenes/PushNotificationSettings/PushNotificationSettingsViewController.swift +++ b/Sources/iOSScenes/PushNotificationSettings/PushNotificationSettingsViewController.swift @@ -1,4 +1,4 @@ -import iOSSupport +import IOSSupport import ReactorKit import Then import UIKit diff --git a/Sources/iOSScenes/PushNotificationSettingsExample/AppDelegate.swift b/Sources/iOSScenes/PushNotificationSettingsExample/AppDelegate.swift index 8d0edda5..66305b16 100644 --- a/Sources/iOSScenes/PushNotificationSettingsExample/AppDelegate.swift +++ b/Sources/iOSScenes/PushNotificationSettingsExample/AppDelegate.swift @@ -5,7 +5,7 @@ // Created by Jaewon Yun on 2023/08/23. // -import iOSSupport +import IOSSupport import UIKit @main diff --git a/Sources/iOSScenes/ThemeSetting/ThemeSettingReactor.swift b/Sources/iOSScenes/ThemeSetting/ThemeSettingReactor.swift index 789832cd..8b15ef38 100644 --- a/Sources/iOSScenes/ThemeSetting/ThemeSettingReactor.swift +++ b/Sources/iOSScenes/ThemeSetting/ThemeSettingReactor.swift @@ -1,5 +1,5 @@ import Domain -import iOSSupport +import IOSSupport import ReactorKit import UIKit diff --git a/Sources/iOSScenes/ThemeSetting/ThemeSettingViewController.swift b/Sources/iOSScenes/ThemeSetting/ThemeSettingViewController.swift index cbc9e2bb..6947532f 100644 --- a/Sources/iOSScenes/ThemeSetting/ThemeSettingViewController.swift +++ b/Sources/iOSScenes/ThemeSetting/ThemeSettingViewController.swift @@ -1,5 +1,5 @@ import FoundationPlus -import iOSSupport +import IOSSupport import OrderedCollections import ReactorKit import UIKit diff --git a/Sources/iOSScenes/UserSettings/UserSettingsDataSource/UserSettingsItemModel.swift b/Sources/iOSScenes/UserSettings/UserSettingsDataSource/UserSettingsItemModel.swift index 591c9498..cee35ab3 100644 --- a/Sources/iOSScenes/UserSettings/UserSettingsDataSource/UserSettingsItemModel.swift +++ b/Sources/iOSScenes/UserSettings/UserSettingsDataSource/UserSettingsItemModel.swift @@ -6,7 +6,7 @@ // Copyright © 2023 woin2ee. All rights reserved. // -import iOSSupport +import IOSSupport enum UserSettingsItemModel { case disclosureIndicator(DisclosureIndicatorCell.Model) diff --git a/Sources/iOSScenes/UserSettings/UserSettingsReactor.swift b/Sources/iOSScenes/UserSettings/UserSettingsReactor.swift index f3e2838a..a65ee0eb 100644 --- a/Sources/iOSScenes/UserSettings/UserSettingsReactor.swift +++ b/Sources/iOSScenes/UserSettings/UserSettingsReactor.swift @@ -7,7 +7,7 @@ // import Domain -import iOSSupport +import IOSSupport import ReactorKit final class UserSettingsReactor: Reactor { diff --git a/Sources/iOSScenes/UserSettings/UserSettingsViewController.swift b/Sources/iOSScenes/UserSettings/UserSettingsViewController.swift index 914d19ca..f437f530 100644 --- a/Sources/iOSScenes/UserSettings/UserSettingsViewController.swift +++ b/Sources/iOSScenes/UserSettings/UserSettingsViewController.swift @@ -7,7 +7,7 @@ // import Domain -import iOSSupport +import IOSSupport import ReactorKit import RxSwift import RxUtility diff --git a/Sources/iOSScenes/WordAddition/WordAdditionViewController.swift b/Sources/iOSScenes/WordAddition/WordAdditionViewController.swift index 09051299..e1f8085e 100644 --- a/Sources/iOSScenes/WordAddition/WordAdditionViewController.swift +++ b/Sources/iOSScenes/WordAddition/WordAdditionViewController.swift @@ -5,7 +5,7 @@ // Created by Jaewon Yun on 2023/09/09. // -import iOSSupport +import IOSSupport import RxSwift import RxCocoa import RxUtility diff --git a/Sources/iOSScenes/WordAddition/WordAdditionViewModel.swift b/Sources/iOSScenes/WordAddition/WordAdditionViewModel.swift index 812d08fb..7ea95686 100644 --- a/Sources/iOSScenes/WordAddition/WordAdditionViewModel.swift +++ b/Sources/iOSScenes/WordAddition/WordAdditionViewModel.swift @@ -8,7 +8,7 @@ import Domain import Foundation import FoundationPlus -import iOSSupport +import IOSSupport import RxSwift import RxCocoa import RxUtility diff --git a/Sources/iOSScenes/WordChecking/Subviews/WordChecking+ChangeWordButton.swift b/Sources/iOSScenes/WordChecking/Subviews/WordChecking+ChangeWordButton.swift index eb4c190b..e0b71be6 100644 --- a/Sources/iOSScenes/WordChecking/Subviews/WordChecking+ChangeWordButton.swift +++ b/Sources/iOSScenes/WordChecking/Subviews/WordChecking+ChangeWordButton.swift @@ -5,7 +5,7 @@ // Created by Jaewon Yun on 2023/09/11. // -import iOSSupport +import IOSSupport import UIKit extension WordCheckingView { diff --git a/Sources/iOSScenes/WordChecking/TranslationWeb/TranslationWebViewController.swift b/Sources/iOSScenes/WordChecking/TranslationWeb/TranslationWebViewController.swift index df633280..293d3fc2 100644 --- a/Sources/iOSScenes/WordChecking/TranslationWeb/TranslationWebViewController.swift +++ b/Sources/iOSScenes/WordChecking/TranslationWeb/TranslationWebViewController.swift @@ -7,7 +7,7 @@ // import Domain -import iOSSupport +import IOSSupport import Then import UIKit import WebKit diff --git a/Sources/iOSScenes/WordChecking/WordCheckingReactor.swift b/Sources/iOSScenes/WordChecking/WordCheckingReactor.swift index d0644cd6..dfab6e3d 100644 --- a/Sources/iOSScenes/WordChecking/WordCheckingReactor.swift +++ b/Sources/iOSScenes/WordChecking/WordCheckingReactor.swift @@ -8,7 +8,7 @@ import Domain import Foundation -import iOSSupport +import IOSSupport import ReactorKit final class WordCheckingReactor: Reactor { diff --git a/Sources/iOSScenes/WordChecking/WordCheckingView.swift b/Sources/iOSScenes/WordChecking/WordCheckingView.swift index 5166ce0b..cfe85bc8 100644 --- a/Sources/iOSScenes/WordChecking/WordCheckingView.swift +++ b/Sources/iOSScenes/WordChecking/WordCheckingView.swift @@ -6,7 +6,7 @@ // Copyright © 2023 woin2ee. All rights reserved. // -import iOSSupport +import IOSSupport import SnapKit import Then import UIKit diff --git a/Sources/iOSScenes/WordChecking/WordCheckingViewController.swift b/Sources/iOSScenes/WordChecking/WordCheckingViewController.swift index a2828ea0..a5bfb60e 100644 --- a/Sources/iOSScenes/WordChecking/WordCheckingViewController.swift +++ b/Sources/iOSScenes/WordChecking/WordCheckingViewController.swift @@ -5,7 +5,7 @@ // Created by Jaewon Yun on 2023/08/23. // -import iOSSupport +import IOSSupport import ReactorKit import RxSwift import RxCocoa diff --git a/Sources/iOSScenes/WordDetail/WordDetailReactor.swift b/Sources/iOSScenes/WordDetail/WordDetailReactor.swift index a80bc71f..67db11cb 100644 --- a/Sources/iOSScenes/WordDetail/WordDetailReactor.swift +++ b/Sources/iOSScenes/WordDetail/WordDetailReactor.swift @@ -8,7 +8,7 @@ import Domain import Foundation -import iOSSupport +import IOSSupport import ReactorKit final class WordDetailReactor: Reactor { diff --git a/Sources/iOSScenes/WordDetail/WordDetailViewController.swift b/Sources/iOSScenes/WordDetail/WordDetailViewController.swift index 101914fa..89bf7d43 100644 --- a/Sources/iOSScenes/WordDetail/WordDetailViewController.swift +++ b/Sources/iOSScenes/WordDetail/WordDetailViewController.swift @@ -6,7 +6,7 @@ // import Domain -import iOSSupport +import IOSSupport import ReactorKit import SnapKit import Then diff --git a/Sources/iOSScenes/WordList/WordListReactor.swift b/Sources/iOSScenes/WordList/WordListReactor.swift index 3cf5bddc..6d0a83e9 100644 --- a/Sources/iOSScenes/WordList/WordListReactor.swift +++ b/Sources/iOSScenes/WordList/WordListReactor.swift @@ -8,7 +8,7 @@ import Domain import Foundation -import iOSSupport +import IOSSupport import ReactorKit final class WordListReactor: Reactor { diff --git a/Sources/iOSScenes/WordList/WordListViewController.swift b/Sources/iOSScenes/WordList/WordListViewController.swift index 43cc629f..90306825 100644 --- a/Sources/iOSScenes/WordList/WordListViewController.swift +++ b/Sources/iOSScenes/WordList/WordListViewController.swift @@ -5,7 +5,7 @@ // Created by Jaewon Yun on 2023/08/25. // -import iOSSupport +import IOSSupport import ReactorKit import RxUtility import SFSafeSymbols diff --git a/Sources/iOSScenes/WordList/WordSearchResultsController.swift b/Sources/iOSScenes/WordList/WordSearchResultsController.swift index 9db0be8d..23f776dc 100644 --- a/Sources/iOSScenes/WordList/WordSearchResultsController.swift +++ b/Sources/iOSScenes/WordList/WordSearchResultsController.swift @@ -6,7 +6,7 @@ // import Domain -import iOSSupport +import IOSSupport import ReactorKit import SwinjectExtension import UIKit diff --git a/Sources/iPhoneDriver/Coordinators/AppCoordinator.swift b/Sources/iPhoneDriver/Coordinators/AppCoordinator.swift index 17fb93c2..51ce5516 100644 --- a/Sources/iPhoneDriver/Coordinators/AppCoordinator.swift +++ b/Sources/iPhoneDriver/Coordinators/AppCoordinator.swift @@ -6,7 +6,7 @@ // Copyright © 2023 woin2ee. All rights reserved. // -import iOSSupport +import IOSSupport import SFSafeSymbols import Then import UIKit diff --git a/Sources/iPhoneDriver/Coordinators/BasicCoordinator.swift b/Sources/iPhoneDriver/Coordinators/BasicCoordinator.swift index ff08268a..1205b477 100644 --- a/Sources/iPhoneDriver/Coordinators/BasicCoordinator.swift +++ b/Sources/iPhoneDriver/Coordinators/BasicCoordinator.swift @@ -6,7 +6,7 @@ // Copyright © 2024 woin2ee. All rights reserved. // -import iOSSupport +import IOSSupport import UIKit import Utility @@ -15,8 +15,8 @@ import Utility /// - warning: Do not use instance of this class directly. Some methods in this class cause of fatal error. class BasicCoordinator: Coordinator { - weak var parentCoordinator: iOSSupport.Coordinator? - var childCoordinators: [iOSSupport.Coordinator] = [] + weak var parentCoordinator: IOSSupport.Coordinator? + var childCoordinators: [IOSSupport.Coordinator] = [] let navigationController: UINavigationController diff --git a/Sources/iPhoneDriver/Coordinators/GeneralSettingsCoordinator.swift b/Sources/iPhoneDriver/Coordinators/GeneralSettingsCoordinator.swift index 795a707b..10abc12d 100644 --- a/Sources/iPhoneDriver/Coordinators/GeneralSettingsCoordinator.swift +++ b/Sources/iPhoneDriver/Coordinators/GeneralSettingsCoordinator.swift @@ -7,7 +7,7 @@ // import GeneralSettings -import iOSSupport +import IOSSupport import UIKit import SwinjectDIContainer diff --git a/Sources/iPhoneDriver/Coordinators/LanguageSettingCoordinator.swift b/Sources/iPhoneDriver/Coordinators/LanguageSettingCoordinator.swift index a5547622..48fe5741 100644 --- a/Sources/iPhoneDriver/Coordinators/LanguageSettingCoordinator.swift +++ b/Sources/iPhoneDriver/Coordinators/LanguageSettingCoordinator.swift @@ -6,7 +6,7 @@ // Copyright © 2023 woin2ee. All rights reserved. // -import iOSSupport +import IOSSupport import LanguageSetting import SwinjectDIContainer import SwinjectExtension diff --git a/Sources/iPhoneDriver/Coordinators/ThemeSettingCoordinator.swift b/Sources/iPhoneDriver/Coordinators/ThemeSettingCoordinator.swift index dc3d42ad..d23377d4 100644 --- a/Sources/iPhoneDriver/Coordinators/ThemeSettingCoordinator.swift +++ b/Sources/iPhoneDriver/Coordinators/ThemeSettingCoordinator.swift @@ -1,4 +1,4 @@ -import iOSSupport +import IOSSupport import SwinjectDIContainer import SwinjectExtension import UIKit diff --git a/Sources/iPhoneDriver/Coordinators/UserSettingsCoordinator.swift b/Sources/iPhoneDriver/Coordinators/UserSettingsCoordinator.swift index 261b7357..12edd6d8 100644 --- a/Sources/iPhoneDriver/Coordinators/UserSettingsCoordinator.swift +++ b/Sources/iPhoneDriver/Coordinators/UserSettingsCoordinator.swift @@ -6,7 +6,7 @@ // Copyright © 2023 woin2ee. All rights reserved. // -import iOSSupport +import IOSSupport import LanguageSetting import SwinjectDIContainer import SwinjectExtension diff --git a/Sources/iPhoneDriver/Coordinators/WordAdditionCoordinator.swift b/Sources/iPhoneDriver/Coordinators/WordAdditionCoordinator.swift index 010d1a1b..ee4f613d 100644 --- a/Sources/iPhoneDriver/Coordinators/WordAdditionCoordinator.swift +++ b/Sources/iPhoneDriver/Coordinators/WordAdditionCoordinator.swift @@ -6,7 +6,7 @@ // Copyright © 2023 woin2ee. All rights reserved. // -import iOSSupport +import IOSSupport import SwinjectDIContainer import SwinjectExtension import UIKit diff --git a/Sources/iPhoneDriver/Coordinators/WordCheckingCoordinator.swift b/Sources/iPhoneDriver/Coordinators/WordCheckingCoordinator.swift index 15cc61ae..3138bfb0 100644 --- a/Sources/iPhoneDriver/Coordinators/WordCheckingCoordinator.swift +++ b/Sources/iPhoneDriver/Coordinators/WordCheckingCoordinator.swift @@ -6,7 +6,7 @@ // Copyright © 2023 woin2ee. All rights reserved. // -import iOSSupport +import IOSSupport import SwinjectDIContainer import SwinjectExtension import UIKit diff --git a/Sources/iPhoneDriver/Coordinators/WordDetailCoordinator.swift b/Sources/iPhoneDriver/Coordinators/WordDetailCoordinator.swift index 3af01e18..48c81afa 100644 --- a/Sources/iPhoneDriver/Coordinators/WordDetailCoordinator.swift +++ b/Sources/iPhoneDriver/Coordinators/WordDetailCoordinator.swift @@ -6,7 +6,7 @@ // Copyright © 2023 woin2ee. All rights reserved. // -import iOSSupport +import IOSSupport import SwinjectDIContainer import SwinjectExtension import UIKit diff --git a/Sources/iPhoneDriver/Coordinators/WordListCoordinator.swift b/Sources/iPhoneDriver/Coordinators/WordListCoordinator.swift index 40f3eb21..8f4a7f04 100644 --- a/Sources/iPhoneDriver/Coordinators/WordListCoordinator.swift +++ b/Sources/iPhoneDriver/Coordinators/WordListCoordinator.swift @@ -6,7 +6,7 @@ // Copyright © 2023 woin2ee. All rights reserved. // -import iOSSupport +import IOSSupport import SwinjectDIContainer import SwinjectExtension import UIKit diff --git a/Sources/iPhoneDriver/RootTabBarController.swift b/Sources/iPhoneDriver/RootTabBarController.swift index d3ec8f56..68fbba18 100644 --- a/Sources/iPhoneDriver/RootTabBarController.swift +++ b/Sources/iPhoneDriver/RootTabBarController.swift @@ -6,7 +6,7 @@ // Copyright © 2024 woin2ee. All rights reserved. // -import iOSSupport +import IOSSupport import SFSafeSymbols import Then import UIKit diff --git a/Sources/iPhoneDriver/SceneDelegate.swift b/Sources/iPhoneDriver/SceneDelegate.swift index 082c1c0e..978ceaf2 100644 --- a/Sources/iPhoneDriver/SceneDelegate.swift +++ b/Sources/iPhoneDriver/SceneDelegate.swift @@ -5,7 +5,7 @@ // Created by Jaewon Yun on 2023/08/23. // -import iOSSupport +import IOSSupport import RxSwift import UIKit diff --git a/Tests/WordCheckerUITests/WordCheckerUITests.swift b/Tests/WordCheckerUITests/WordCheckerUITests.swift index 9a0b071c..ae0a8d0b 100644 --- a/Tests/WordCheckerUITests/WordCheckerUITests.swift +++ b/Tests/WordCheckerUITests/WordCheckerUITests.swift @@ -7,7 +7,7 @@ @testable import Domain -import iOSSupport +import IOSSupport import XCTest final class WordCheckerUITests: XCTestCase { diff --git a/Tuist/Templates/iOSScene/iOSScene.swift b/Tuist/Templates/IOSScene/IOSScene.swift similarity index 68% rename from Tuist/Templates/iOSScene/iOSScene.swift rename to Tuist/Templates/IOSScene/IOSScene.swift index c65cce74..5a5609df 100644 --- a/Tuist/Templates/iOSScene/iOSScene.swift +++ b/Tuist/Templates/IOSScene/IOSScene.swift @@ -3,30 +3,30 @@ import ProjectDescription let nameAttribute: Template.Attribute = .required("name") let template = Template( - description: "Create default files for iOSScene.", + description: "Create default files for IOSScene.", attributes: [ nameAttribute, .optional("platform", default: "ios"), ], items: [ .file( - path: "Sources/iOSScenes/\(nameAttribute)/\(nameAttribute)ViewController.swift", + path: "Sources/IOSScenes/\(nameAttribute)/\(nameAttribute)ViewController.swift", templatePath: .relativeToCurrentFile("../ViewController.stencil") ), .file( - path: "Sources/iOSScenes/\(nameAttribute)/\(nameAttribute)Reactor.swift", + path: "Sources/IOSScenes/\(nameAttribute)/\(nameAttribute)Reactor.swift", templatePath: .relativeToCurrentFile("../Reactor.stencil") ), .file( - path: "Sources/iOSScenes/\(nameAttribute)/\(nameAttribute)Assembly.swift", + path: "Sources/IOSScenes/\(nameAttribute)/\(nameAttribute)Assembly.swift", templatePath: .relativeToCurrentFile("../Assembly.stencil") ), .file( - path: "Sources/iPhoneDriver/Coordinators/\(nameAttribute)Coordinator.swift", - templatePath: .relativeToCurrentFile("../iPhoneCoordinator.stencil") + path: "Sources/IPhoneDriver/Coordinators/\(nameAttribute)Coordinator.swift", + templatePath: .relativeToCurrentFile("../IPhoneCoordinator.stencil") ), .file( - path: "Tests/iOSScenesTests/\(nameAttribute)Tests/\(nameAttribute)Tests.swift", + path: "Tests/IOSScenesTests/\(nameAttribute)Tests/\(nameAttribute)Tests.swift", templatePath: .relativeToCurrentFile("../UnitTests.stencil") ), .file( diff --git a/Tuist/Templates/ViewController.stencil b/Tuist/Templates/ViewController.stencil index 27e9f11a..7e1ece88 100644 --- a/Tuist/Templates/ViewController.stencil +++ b/Tuist/Templates/ViewController.stencil @@ -1,4 +1,4 @@ -import iOSSupport +import IOSSupport import ReactorKit import UIKit diff --git a/Tuist/Templates/iPhoneCoordinator.stencil b/Tuist/Templates/iPhoneCoordinator.stencil index 444d1e82..22dbac7b 100644 --- a/Tuist/Templates/iPhoneCoordinator.stencil +++ b/Tuist/Templates/iPhoneCoordinator.stencil @@ -1,4 +1,4 @@ -import iOSSupport +import IOSSupport import SwinjectDIContainer import SwinjectExtension import UIKit From 508c3cf450a99e06d2c05baa92bd163ceaeb0ff7 Mon Sep 17 00:00:00 2001 From: Jaewon Yun Date: Mon, 12 Feb 2024 17:27:21 +0900 Subject: [PATCH 18/24] Structuring domain errors --- .../ExternalStoreUseCaseError.swift | 17 +++++++++ .../ExternalStoreUseCaseProtocol.swift | 0 .../NotificationsUseCaseError.swift | 13 +++++++ .../NotificationsUseCaseProtocol.swift | 0 .../UserSettingsUseCaseError.swift | 14 +++++++ .../UserSettingsUseCaseProtocol.swift | 0 .../UseCases/Word/WordUseCaseError.swift | 38 +++++++++++++++++++ .../{ => Word}/WordUseCaseProtocol.swift | 2 + .../UseCases/ExternalStoreUseCase.swift | 8 ---- .../UseCases/NotificationsUseCase.swift | 4 -- .../Domain/UseCases/UserSettingsUseCase.swift | 5 --- Sources/Domain/UseCases/WordUseCase.swift | 19 ++-------- Sources/DomainTesting/WordUseCaseFake.swift | 2 +- 13 files changed, 88 insertions(+), 34 deletions(-) create mode 100644 Sources/Domain/Interfaces/UseCases/ExternalStore/ExternalStoreUseCaseError.swift rename Sources/Domain/Interfaces/UseCases/{ => ExternalStore}/ExternalStoreUseCaseProtocol.swift (100%) create mode 100644 Sources/Domain/Interfaces/UseCases/Notification/NotificationsUseCaseError.swift rename Sources/Domain/Interfaces/UseCases/{ => Notification}/NotificationsUseCaseProtocol.swift (100%) create mode 100644 Sources/Domain/Interfaces/UseCases/UserSettings/UserSettingsUseCaseError.swift rename Sources/Domain/Interfaces/UseCases/{ => UserSettings}/UserSettingsUseCaseProtocol.swift (100%) create mode 100644 Sources/Domain/Interfaces/UseCases/Word/WordUseCaseError.swift rename Sources/Domain/Interfaces/UseCases/{ => Word}/WordUseCaseProtocol.swift (85%) diff --git a/Sources/Domain/Interfaces/UseCases/ExternalStore/ExternalStoreUseCaseError.swift b/Sources/Domain/Interfaces/UseCases/ExternalStore/ExternalStoreUseCaseError.swift new file mode 100644 index 00000000..e5a6b8b0 --- /dev/null +++ b/Sources/Domain/Interfaces/UseCases/ExternalStore/ExternalStoreUseCaseError.swift @@ -0,0 +1,17 @@ +// +// ExternalStoreUseCaseError.swift +// Domain +// +// Created by Jaewon Yun on 2/12/24. +// Copyright © 2024 woin2ee. All rights reserved. +// + +import Foundation + +public enum ExternalStoreUseCaseError: Error { + + case noCurrentUser + + case noPresentingConfiguration + +} diff --git a/Sources/Domain/Interfaces/UseCases/ExternalStoreUseCaseProtocol.swift b/Sources/Domain/Interfaces/UseCases/ExternalStore/ExternalStoreUseCaseProtocol.swift similarity index 100% rename from Sources/Domain/Interfaces/UseCases/ExternalStoreUseCaseProtocol.swift rename to Sources/Domain/Interfaces/UseCases/ExternalStore/ExternalStoreUseCaseProtocol.swift diff --git a/Sources/Domain/Interfaces/UseCases/Notification/NotificationsUseCaseError.swift b/Sources/Domain/Interfaces/UseCases/Notification/NotificationsUseCaseError.swift new file mode 100644 index 00000000..1a104b23 --- /dev/null +++ b/Sources/Domain/Interfaces/UseCases/Notification/NotificationsUseCaseError.swift @@ -0,0 +1,13 @@ +// +// NotificationsUseCaseError.swift +// Domain +// +// Created by Jaewon Yun on 2/12/24. +// Copyright © 2024 woin2ee. All rights reserved. +// + +import Foundation + +public enum NotificationsUseCaseError: Error { + case noWordsToMemorize +} diff --git a/Sources/Domain/Interfaces/UseCases/NotificationsUseCaseProtocol.swift b/Sources/Domain/Interfaces/UseCases/Notification/NotificationsUseCaseProtocol.swift similarity index 100% rename from Sources/Domain/Interfaces/UseCases/NotificationsUseCaseProtocol.swift rename to Sources/Domain/Interfaces/UseCases/Notification/NotificationsUseCaseProtocol.swift diff --git a/Sources/Domain/Interfaces/UseCases/UserSettings/UserSettingsUseCaseError.swift b/Sources/Domain/Interfaces/UseCases/UserSettings/UserSettingsUseCaseError.swift new file mode 100644 index 00000000..b51ebcba --- /dev/null +++ b/Sources/Domain/Interfaces/UseCases/UserSettings/UserSettingsUseCaseError.swift @@ -0,0 +1,14 @@ +// +// UserSettingsUseCaseError.swift +// Domain +// +// Created by Jaewon Yun on 2/12/24. +// Copyright © 2024 woin2ee. All rights reserved. +// + +import Foundation + +public enum UserSettingsUseCaseError: Error { + case noPendingDailyReminder + case noNotificationAuthorization +} diff --git a/Sources/Domain/Interfaces/UseCases/UserSettingsUseCaseProtocol.swift b/Sources/Domain/Interfaces/UseCases/UserSettings/UserSettingsUseCaseProtocol.swift similarity index 100% rename from Sources/Domain/Interfaces/UseCases/UserSettingsUseCaseProtocol.swift rename to Sources/Domain/Interfaces/UseCases/UserSettings/UserSettingsUseCaseProtocol.swift diff --git a/Sources/Domain/Interfaces/UseCases/Word/WordUseCaseError.swift b/Sources/Domain/Interfaces/UseCases/Word/WordUseCaseError.swift new file mode 100644 index 00000000..c4c034e4 --- /dev/null +++ b/Sources/Domain/Interfaces/UseCases/Word/WordUseCaseError.swift @@ -0,0 +1,38 @@ +// +// WordUseCaseError.swift +// Domain +// +// Created by Jaewon Yun on 2/12/24. +// Copyright © 2024 woin2ee. All rights reserved. +// + +import Foundation + +public enum WordUseCaseError: Error { + + /// `saveFailed` 에러가 발생한 이유입니다. + public enum SaveFailureReason { + + /// 저장하려는 단어가 이미 암기 완료 상태입니다. + case wordStateInvalid + + } + + /// `retrieveFailed` 에러가 발생한 이유입니다. + public enum RetrieveFailureReason { + + /// 해당 UUID 와 일치하는 단어가 없습니다. + case uuidInvaild(uuid: UUID) + + } + + /// 단어 저장 실패 + case saveFailed(reason: SaveFailureReason) + + /// 단어 검색 실패 + case retrieveFailed(reason: RetrieveFailureReason) + + /// 현재 암기중인 단어가 없음 + case noMemorizingWords + +} diff --git a/Sources/Domain/Interfaces/UseCases/WordUseCaseProtocol.swift b/Sources/Domain/Interfaces/UseCases/Word/WordUseCaseProtocol.swift similarity index 85% rename from Sources/Domain/Interfaces/UseCases/WordUseCaseProtocol.swift rename to Sources/Domain/Interfaces/UseCases/Word/WordUseCaseProtocol.swift index 958fe3e6..8787b9fa 100644 --- a/Sources/Domain/Interfaces/UseCases/WordUseCaseProtocol.swift +++ b/Sources/Domain/Interfaces/UseCases/Word/WordUseCaseProtocol.swift @@ -11,6 +11,8 @@ import RxSwift public protocol WordUseCaseProtocol { /// 새 단어를 추가합니다. + /// + /// - Returns: 단어 추가에 성공하면 Next 이벤트를, 어떠한 이유로 인해 실패하면 `WordUseCaseError` 타입의 Error 이벤트를 방출하는 Sequence 를 반환합니다. func addNewWord(_ word: Word) -> Single /// 단어를 삭제합니다. diff --git a/Sources/Domain/UseCases/ExternalStoreUseCase.swift b/Sources/Domain/UseCases/ExternalStoreUseCase.swift index bef269fd..6383662a 100644 --- a/Sources/Domain/UseCases/ExternalStoreUseCase.swift +++ b/Sources/Domain/UseCases/ExternalStoreUseCase.swift @@ -172,11 +172,3 @@ public final class ExternalStoreUseCase: ExternalStoreUseCaseProtocol { } } - -enum ExternalStoreUseCaseError: Error { - - case noCurrentUser - - case noPresentingConfiguration - -} diff --git a/Sources/Domain/UseCases/NotificationsUseCase.swift b/Sources/Domain/UseCases/NotificationsUseCase.swift index 3dcddb94..483d476c 100644 --- a/Sources/Domain/UseCases/NotificationsUseCase.swift +++ b/Sources/Domain/UseCases/NotificationsUseCase.swift @@ -12,10 +12,6 @@ import RxSwift import RxUtility import UserNotifications -enum NotificationsUseCaseError: Error { - case noWordsToMemorize -} - final class NotificationsUseCase: NotificationsUseCaseProtocol { /// Notification request 의 고유 ID diff --git a/Sources/Domain/UseCases/UserSettingsUseCase.swift b/Sources/Domain/UseCases/UserSettingsUseCase.swift index 8e904ad1..1075d24f 100644 --- a/Sources/Domain/UseCases/UserSettingsUseCase.swift +++ b/Sources/Domain/UseCases/UserSettingsUseCase.swift @@ -12,11 +12,6 @@ import RxRelay import RxUtility import Utility -enum UserSettingsUseCaseError: Error { - case noPendingDailyReminder - case noNotificationAuthorization -} - public final class UserSettingsUseCase: UserSettingsUseCaseProtocol { let userSettingsRepository: UserSettingsRepositoryProtocol diff --git a/Sources/Domain/UseCases/WordUseCase.swift b/Sources/Domain/UseCases/WordUseCase.swift index b531b416..840269fb 100644 --- a/Sources/Domain/UseCases/WordUseCase.swift +++ b/Sources/Domain/UseCases/WordUseCase.swift @@ -23,7 +23,7 @@ public final class WordUseCase: WordUseCaseProtocol { public func addNewWord(_ word: Word) -> RxSwift.Single { return .create { single in guard word.memorizedState != .memorized else { - single(.failure(WordUseCaseError.canNotSaveWord(reason: "Can only add word with a memorization state of `.memorizing`."))) + single(.failure(WordUseCaseError.saveFailed(reason: .wordStateInvalid))) return Disposables.create() } @@ -85,7 +85,7 @@ public final class WordUseCase: WordUseCaseProtocol { if let word = self.wordRepository.getWord(by: uuid) { single(.success(word)) } else { - single(.failure(WordUseCaseError.invalidUUID(uuid))) + single(.failure(WordUseCaseError.retrieveFailed(reason: .uuidInvaild(uuid: uuid)))) } return Disposables.create() @@ -151,7 +151,7 @@ public final class WordUseCase: WordUseCaseProtocol { public func markCurrentWordAsMemorized(uuid: UUID) -> RxSwift.Single { return .create { single in guard let currentWord = self.wordRepository.getWord(by: uuid) else { - single(.failure(WordUseCaseError.invalidUUID(uuid))) + single(.failure(WordUseCaseError.retrieveFailed(reason: .uuidInvaild(uuid: uuid)))) return Disposables.create() } @@ -175,16 +175,3 @@ public final class WordUseCase: WordUseCaseProtocol { } } - -enum WordUseCaseError: Error { - - /// 해당되는 단어가 없는 UUID - case invalidUUID(UUID) - - /// 단어를 저장할 수 없음 - case canNotSaveWord(reason: String) - - /// 현재 암기중인 단어가 없음 - case noMemorizingWords - -} diff --git a/Sources/DomainTesting/WordUseCaseFake.swift b/Sources/DomainTesting/WordUseCaseFake.swift index f4c800c3..8ff0e6f9 100644 --- a/Sources/DomainTesting/WordUseCaseFake.swift +++ b/Sources/DomainTesting/WordUseCaseFake.swift @@ -51,7 +51,7 @@ public final class WordUseCaseFake: WordUseCaseProtocol { public func getWord(by uuid: UUID) -> Single { guard let word = _wordList.first(where: { $0.uuid == uuid }) else { - return .error(WordUseCaseError.invalidUUID(uuid)) + return .error(WordUseCaseError.retrieveFailed(reason: .uuidInvaild(uuid: uuid))) } return .just(word) } From 3ccbd667197f2f245f8117bc884b858f6fb15649 Mon Sep 17 00:00:00 2001 From: Jaewon Yun Date: Mon, 12 Feb 2024 17:27:29 +0900 Subject: [PATCH 19/24] Fix typo --- .../Domain/Interfaces/Services/LocalNotificationService.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Domain/Interfaces/Services/LocalNotificationService.swift b/Sources/Domain/Interfaces/Services/LocalNotificationService.swift index 35284aed..d2a03c10 100644 --- a/Sources/Domain/Interfaces/Services/LocalNotificationService.swift +++ b/Sources/Domain/Interfaces/Services/LocalNotificationService.swift @@ -29,6 +29,6 @@ public protocol LocalNotificationService { /// 매일 알림을 설정한 마지막 시각을 반환합니다. /// - /// /// - throws: 저장된 시각이 없을 때 Error 를 던집니다. + /// - throws: 저장된 시각이 없을 때 Error 를 던집니다. func getLatestDailyReminderTime() throws -> DateComponents } From 322f03397b76a0e316d939aa1b276efa2a1f4430 Mon Sep 17 00:00:00 2001 From: Jaewon Yun Date: Tue, 13 Feb 2024 01:11:13 +0900 Subject: [PATCH 20/24] Add feature that prevent to add duplecated word --- Changelog/next.md | 3 +- .../Localization/en.lproj/Localizable.strings | 3 ++ .../Localization/ko.lproj/Localizable.strings | 2 ++ .../UseCases/Word/WordUseCaseError.swift | 3 ++ Sources/Domain/UseCases/WordUseCase.swift | 6 ++++ .../WordChecking/WordCheckingReactor.swift | 35 +++++++++++++++++-- .../WordCheckingViewController.swift | 25 +++++++++++-- .../iOSSupport/Localization/WCString.swift | 5 +++ Tests/DomainTests/WordUseCaseTests.swift | 19 ++++++++++ .../WordCheckingReactorTests.swift | 11 ++++++ 10 files changed, 105 insertions(+), 7 deletions(-) diff --git a/Changelog/next.md b/Changelog/next.md index 1a1f33c4..ca3c6c01 100644 --- a/Changelog/next.md +++ b/Changelog/next.md @@ -1,4 +1,5 @@ -## Added +## New features - Added to change theme feature. +- Added feature that prevent to add duplecated word. ## Fixed diff --git a/Resources/iOSSupport/Localization/en.lproj/Localizable.strings b/Resources/iOSSupport/Localization/en.lproj/Localizable.strings index 00de3e20..160922cf 100644 --- a/Resources/iOSSupport/Localization/en.lproj/Localizable.strings +++ b/Resources/iOSSupport/Localization/en.lproj/Localizable.strings @@ -38,6 +38,9 @@ all = "All"; there_are_no_words = "There are no words."; quick_add_word = "Quick add word"; "%@_added_successfully" = "[ %@ ] added successfully"; +already_added_word = "Already added word."; +"%@_added_failed" = "[ %@ ] added failed."; + settings = "Settings"; translation_language = "Translation language"; diff --git a/Resources/iOSSupport/Localization/ko.lproj/Localizable.strings b/Resources/iOSSupport/Localization/ko.lproj/Localizable.strings index 4c8fc113..a807bee8 100644 --- a/Resources/iOSSupport/Localization/ko.lproj/Localizable.strings +++ b/Resources/iOSSupport/Localization/ko.lproj/Localizable.strings @@ -38,6 +38,8 @@ all = "전체"; there_are_no_words = "단어가 없습니다."; quick_add_word = "빠른 단어 추가"; "%@_added_successfully" = "[ %@ ] 추가 성공"; +already_added_word = "이미 추가된 단어입니다."; +"%@_added_failed" = "[ %@ ] 단어 추가 실패"; settings = "설정"; languages = "언어"; diff --git a/Sources/Domain/Interfaces/UseCases/Word/WordUseCaseError.swift b/Sources/Domain/Interfaces/UseCases/Word/WordUseCaseError.swift index c4c034e4..be3e3781 100644 --- a/Sources/Domain/Interfaces/UseCases/Word/WordUseCaseError.swift +++ b/Sources/Domain/Interfaces/UseCases/Word/WordUseCaseError.swift @@ -16,6 +16,9 @@ public enum WordUseCaseError: Error { /// 저장하려는 단어가 이미 암기 완료 상태입니다. case wordStateInvalid + /// 저장하려는 단어가 중복 단어입니다. + case duplecatedWord(word: String) + } /// `retrieveFailed` 에러가 발생한 이유입니다. diff --git a/Sources/Domain/UseCases/WordUseCase.swift b/Sources/Domain/UseCases/WordUseCase.swift index 840269fb..5fabf0d9 100644 --- a/Sources/Domain/UseCases/WordUseCase.swift +++ b/Sources/Domain/UseCases/WordUseCase.swift @@ -27,6 +27,12 @@ public final class WordUseCase: WordUseCaseProtocol { return Disposables.create() } + let allWords = self.wordRepository.getAllWords() + if allWords.contains(where: { $0.word.lowercased() == word.word.lowercased() }) { + single(.failure(WordUseCaseError.saveFailed(reason: .duplecatedWord(word: word.word)))) + return Disposables.create() + } + self.unmemorizedWordListRepository.addWord(word) self.wordRepository.save(word) diff --git a/Sources/iOSScenes/WordChecking/WordCheckingReactor.swift b/Sources/iOSScenes/WordChecking/WordCheckingReactor.swift index dfab6e3d..79c9ceb4 100644 --- a/Sources/iOSScenes/WordChecking/WordCheckingReactor.swift +++ b/Sources/iOSScenes/WordChecking/WordCheckingReactor.swift @@ -11,6 +11,17 @@ import Foundation import IOSSupport import ReactorKit +enum WordCheckingReactorError: Error { + + enum AddWordFailureReason { + case duplecatedWord(word: String) + case unknown(word: String) + } + + case addWordFailed(reason: AddWordFailureReason) + +} + final class WordCheckingReactor: Reactor { enum Action { @@ -27,12 +38,14 @@ final class WordCheckingReactor: Reactor { case setCurrentWord(Word?) case setSourceLanguage(TranslationLanguage) case setTargetLanguage(TranslationLanguage) + case showAddCompleteToast(Result) } struct State { var currentWord: Word? var translationSourceLanguage: TranslationLanguage var translationTargetLanguage: TranslationLanguage + @Pulse var showAddCompleteToast: Result? } let initialState: State = State( @@ -82,9 +95,23 @@ final class WordCheckingReactor: Reactor { let newWord: Word = .init(word: newWord) return wordUseCase.addNewWord(newWord) .asObservable() - .flatMap { self.wordUseCase.getCurrentUnmemorizedWord() } - .map(Mutation.setCurrentWord) - .catchAndReturn(.setCurrentWord(nil)) + .flatMap { _ in self.wordUseCase.getCurrentUnmemorizedWord() } + .flatMap { currentWord -> Observable in + return .merge([ + .just(.setCurrentWord(currentWord)), + .just(.showAddCompleteToast(.success(newWord.word))), + ]) + } + .catch { error in + switch error { + case WordUseCaseError.saveFailed(reason: .duplecatedWord): + return .just(.showAddCompleteToast(.failure(.addWordFailed(reason: .duplecatedWord(word: newWord.word))))) + case WordUseCaseError.noMemorizingWords: + return .just(.setCurrentWord(nil)) + default: + return .just(.showAddCompleteToast(.failure(.addWordFailed(reason: .unknown(word: newWord.word))))) + } + } case .updateToNextWord: return wordUseCase.updateToNextWord() @@ -164,6 +191,8 @@ final class WordCheckingReactor: Reactor { state.translationSourceLanguage = translationSourceLanguage case .setTargetLanguage(let translationTargetLanguage): state.translationTargetLanguage = translationTargetLanguage + case .showAddCompleteToast(let result): + state.showAddCompleteToast = result } return state diff --git a/Sources/iOSScenes/WordChecking/WordCheckingViewController.swift b/Sources/iOSScenes/WordChecking/WordCheckingViewController.swift index a5bfb60e..635713da 100644 --- a/Sources/iOSScenes/WordChecking/WordCheckingViewController.swift +++ b/Sources/iOSScenes/WordChecking/WordCheckingViewController.swift @@ -169,6 +169,27 @@ final class WordCheckingViewController: RxBaseViewController, View, WordChecking owner.setAccessibilityLanguage() } .disposed(by: self.disposeBag) + + reactor.pulse(\.$showAddCompleteToast) + .asSignalOnErrorJustComplete() + .emit(with: self) { owner, showAddCompleteToast in + guard let showAddCompleteToast = showAddCompleteToast else { return } + switch showAddCompleteToast { + case .success(let word): + owner.view.makeToast(WCString.word_added_successfully(word: word), duration: 2.0, position: .top) + case .failure(let error): + switch error { + case .addWordFailed(let reason): + switch reason { + case .duplecatedWord: + owner.view.makeToast(WCString.already_added_word, duration: 2.0, position: .top) + case .unknown(let word): + owner.view.makeToast(WCString.word_added_failed(word: word), duration: 2.0, position: .top) + } + } + } + } + .disposed(by: self.disposeBag) } func setAccessibilityLanguage() { @@ -188,15 +209,13 @@ final class WordCheckingViewController: RxBaseViewController, View, WordChecking } alertController.addAction(cancelAction) - let addAction: UIAlertAction = .init(title: WCString.add, style: .default) { [weak self] _ in + let addAction: UIAlertAction = .init(title: WCString.add, style: .default) { _ in guard let word = alertController.textFields?.first?.text else { assertionFailure("Failed to get word.") return } observer(.success(word)) - - self?.view.makeToast(WCString.word_added_successfully(word: word), duration: 1.2, position: .top) } alertController.addAction(addAction) diff --git a/Sources/iOSSupport/Localization/WCString.swift b/Sources/iOSSupport/Localization/WCString.swift index 668a6aeb..ffdcb0ef 100644 --- a/Sources/iOSSupport/Localization/WCString.swift +++ b/Sources/iOSSupport/Localization/WCString.swift @@ -63,6 +63,11 @@ public struct WCString { let localizedString = NSLocalizedString("%@_added_successfully", bundle: Bundle.module, comment: "단어 추가 완료 후 표시되는 메세지") return .init(format: localizedString, arguments: [word]) } + public static let already_added_word = NSLocalizedString("already_added_word", bundle: Bundle.module, comment: "") + public static func word_added_failed(word: String) -> String { + let localizedString = NSLocalizedString("%@_added_failed", bundle: Bundle.module, comment: "알 수 없는 이유로 단어 추가 실패 후 표시되는 메세지") + return .init(format: localizedString, arguments: [word]) + } public static let please_check_your_network_connection = NSLocalizedString("please_check_your_network_connection", bundle: Bundle.module, comment: "") diff --git a/Tests/DomainTests/WordUseCaseTests.swift b/Tests/DomainTests/WordUseCaseTests.swift index 13cf1dbe..2ae4fc27 100644 --- a/Tests/DomainTests/WordUseCaseTests.swift +++ b/Tests/DomainTests/WordUseCaseTests.swift @@ -198,6 +198,25 @@ final class WordUseCaseTests: XCTestCase { XCTAssertNotEqual(try sut.getCurrentUnmemorizedWord().toBlocking().single(), oldCurrentWord) } + func test_addDuplecatedWord() throws { + // Given + let duplecatedWord = unmemorizedWordList[0] + + // When + let addNewWord = sut.addNewWord(duplecatedWord) + .toBlocking() + + // Then + XCTAssertThrowsError(try addNewWord.single()) { error in + switch error { + case WordUseCaseError.saveFailed(reason: .duplecatedWord): + break + default: + XCTFail() + } + } + } + } // MARK: - Helpers diff --git a/Tests/iOSScenesTests/WordCheckingTests/WordCheckingReactorTests.swift b/Tests/iOSScenesTests/WordCheckingTests/WordCheckingReactorTests.swift index 0f0d0615..957de40a 100644 --- a/Tests/iOSScenesTests/WordCheckingTests/WordCheckingReactorTests.swift +++ b/Tests/iOSScenesTests/WordCheckingTests/WordCheckingReactorTests.swift @@ -64,4 +64,15 @@ final class WordCheckingReactorTests: XCTestCase { XCTAssertNil(sut.currentState.currentWord) } + func test_addDuplecatedWord() { + // Given + sut.action.onNext(.addWord("testWord")) + + // When + sut.action.onNext(.addWord("TESTWORD")) + + // Then + XCTAssertNotNil(sut.currentState.showAddCompleteToast) + } + } From 7d7fdffe351f44d7d0988b77f1c0e20a0e60e9f8 Mon Sep 17 00:00:00 2001 From: Jaewon Yun Date: Tue, 13 Feb 2024 13:58:55 +0900 Subject: [PATCH 21/24] Fix typo --- .../Interfaces/UseCases/Word/WordUseCaseError.swift | 2 +- Sources/Domain/UseCases/WordUseCase.swift | 2 +- Sources/iOSScenes/WordChecking/WordCheckingReactor.swift | 6 +++--- .../WordChecking/WordCheckingViewController.swift | 2 +- Tests/DomainTests/WordUseCaseTests.swift | 8 ++++---- .../WordCheckingTests/WordCheckingReactorTests.swift | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Sources/Domain/Interfaces/UseCases/Word/WordUseCaseError.swift b/Sources/Domain/Interfaces/UseCases/Word/WordUseCaseError.swift index be3e3781..37a1d8ef 100644 --- a/Sources/Domain/Interfaces/UseCases/Word/WordUseCaseError.swift +++ b/Sources/Domain/Interfaces/UseCases/Word/WordUseCaseError.swift @@ -17,7 +17,7 @@ public enum WordUseCaseError: Error { case wordStateInvalid /// 저장하려는 단어가 중복 단어입니다. - case duplecatedWord(word: String) + case duplicatedWord(word: String) } diff --git a/Sources/Domain/UseCases/WordUseCase.swift b/Sources/Domain/UseCases/WordUseCase.swift index 5fabf0d9..7e5abb3f 100644 --- a/Sources/Domain/UseCases/WordUseCase.swift +++ b/Sources/Domain/UseCases/WordUseCase.swift @@ -29,7 +29,7 @@ public final class WordUseCase: WordUseCaseProtocol { let allWords = self.wordRepository.getAllWords() if allWords.contains(where: { $0.word.lowercased() == word.word.lowercased() }) { - single(.failure(WordUseCaseError.saveFailed(reason: .duplecatedWord(word: word.word)))) + single(.failure(WordUseCaseError.saveFailed(reason: .duplicatedWord(word: word.word)))) return Disposables.create() } diff --git a/Sources/iOSScenes/WordChecking/WordCheckingReactor.swift b/Sources/iOSScenes/WordChecking/WordCheckingReactor.swift index 79c9ceb4..afa66106 100644 --- a/Sources/iOSScenes/WordChecking/WordCheckingReactor.swift +++ b/Sources/iOSScenes/WordChecking/WordCheckingReactor.swift @@ -14,7 +14,7 @@ import ReactorKit enum WordCheckingReactorError: Error { enum AddWordFailureReason { - case duplecatedWord(word: String) + case duplicatedWord(word: String) case unknown(word: String) } @@ -104,8 +104,8 @@ final class WordCheckingReactor: Reactor { } .catch { error in switch error { - case WordUseCaseError.saveFailed(reason: .duplecatedWord): - return .just(.showAddCompleteToast(.failure(.addWordFailed(reason: .duplecatedWord(word: newWord.word))))) + case WordUseCaseError.saveFailed(reason: .duplicatedWord): + return .just(.showAddCompleteToast(.failure(.addWordFailed(reason: .duplicatedWord(word: newWord.word))))) case WordUseCaseError.noMemorizingWords: return .just(.setCurrentWord(nil)) default: diff --git a/Sources/iOSScenes/WordChecking/WordCheckingViewController.swift b/Sources/iOSScenes/WordChecking/WordCheckingViewController.swift index 635713da..b1052c75 100644 --- a/Sources/iOSScenes/WordChecking/WordCheckingViewController.swift +++ b/Sources/iOSScenes/WordChecking/WordCheckingViewController.swift @@ -181,7 +181,7 @@ final class WordCheckingViewController: RxBaseViewController, View, WordChecking switch error { case .addWordFailed(let reason): switch reason { - case .duplecatedWord: + case .duplicatedWord: owner.view.makeToast(WCString.already_added_word, duration: 2.0, position: .top) case .unknown(let word): owner.view.makeToast(WCString.word_added_failed(word: word), duration: 2.0, position: .top) diff --git a/Tests/DomainTests/WordUseCaseTests.swift b/Tests/DomainTests/WordUseCaseTests.swift index 2ae4fc27..3d0dde27 100644 --- a/Tests/DomainTests/WordUseCaseTests.swift +++ b/Tests/DomainTests/WordUseCaseTests.swift @@ -198,18 +198,18 @@ final class WordUseCaseTests: XCTestCase { XCTAssertNotEqual(try sut.getCurrentUnmemorizedWord().toBlocking().single(), oldCurrentWord) } - func test_addDuplecatedWord() throws { + func test_addDuplicatedWord() throws { // Given - let duplecatedWord = unmemorizedWordList[0] + let duplicatedWord = unmemorizedWordList[0] // When - let addNewWord = sut.addNewWord(duplecatedWord) + let addNewWord = sut.addNewWord(duplicatedWord) .toBlocking() // Then XCTAssertThrowsError(try addNewWord.single()) { error in switch error { - case WordUseCaseError.saveFailed(reason: .duplecatedWord): + case WordUseCaseError.saveFailed(reason: .duplicatedWord): break default: XCTFail() diff --git a/Tests/iOSScenesTests/WordCheckingTests/WordCheckingReactorTests.swift b/Tests/iOSScenesTests/WordCheckingTests/WordCheckingReactorTests.swift index 957de40a..8ceaa337 100644 --- a/Tests/iOSScenesTests/WordCheckingTests/WordCheckingReactorTests.swift +++ b/Tests/iOSScenesTests/WordCheckingTests/WordCheckingReactorTests.swift @@ -64,7 +64,7 @@ final class WordCheckingReactorTests: XCTestCase { XCTAssertNil(sut.currentState.currentWord) } - func test_addDuplecatedWord() { + func test_addDuplicatedWord() { // Given sut.action.onNext(.addWord("testWord")) From 505c02cc4a0641da153aa796b3065ed1b7a3716d Mon Sep 17 00:00:00 2001 From: Jaewon Yun Date: Wed, 14 Feb 2024 19:12:54 +0900 Subject: [PATCH 22/24] Fix memory leak --- .../iOSScenes/WordAddition/WordAdditionViewController.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/iOSScenes/WordAddition/WordAdditionViewController.swift b/Sources/iOSScenes/WordAddition/WordAdditionViewController.swift index e1f8085e..b098795b 100644 --- a/Sources/iOSScenes/WordAddition/WordAdditionViewController.swift +++ b/Sources/iOSScenes/WordAddition/WordAdditionViewController.swift @@ -87,19 +87,19 @@ final class WordAdditionViewController: RxBaseViewController, WordAdditionViewCo [ output.saveComplete .emit(with: self, onNext: { owner, _ in - owner.delegate?.viewControllerMustBeDismissed(self) + owner.delegate?.viewControllerMustBeDismissed(owner) }), output.wordTextIsNotEmpty .drive(doneBarButton.rx.isEnabled), output.reconfirmDismiss .emit(with: self, onNext: { owner, _ in owner.presentDismissActionSheet { - owner.delegate?.viewControllerMustBeDismissed(self) + owner.delegate?.viewControllerMustBeDismissed(owner) } }), output.dismissComplete .emit(with: self, onNext: { owner, _ in - owner.delegate?.viewControllerMustBeDismissed(self) + owner.delegate?.viewControllerMustBeDismissed(owner) }), ] .forEach { $0.disposed(by: disposeBag) } From 0afa4651d75916b379d7fdc76243c0b4bd3ad602 Mon Sep 17 00:00:00 2001 From: Jaewon Yun Date: Thu, 15 Feb 2024 00:06:39 +0900 Subject: [PATCH 23/24] Prevent adding duplicate word anywhere --- .../Localization/en.lproj/Localizable.strings | 2 + .../Localization/ko.lproj/Localizable.strings | 2 + .../UseCases/Word/WordUseCaseProtocol.swift | 5 ++ Sources/Domain/UseCases/WordUseCase.swift | 18 +++++ Sources/DomainTesting/WordUseCaseFake.swift | 42 +++++++++- .../WordAdditionViewController.swift | 26 +++++- .../WordAddition/WordAdditionViewModel.swift | 14 +++- .../WordDetail/WordDetailReactor.swift | 52 ++++++++++-- .../WordDetail/WordDetailViewController.swift | 48 ++++++++++- .../iOSSupport/Localization/WCString.swift | 1 + Tests/DomainTests/WordUseCaseTests.swift | 25 ++++++ .../WordDetailReactorTests.swift | 81 +++++++++++++++++++ 12 files changed, 299 insertions(+), 17 deletions(-) create mode 100644 Tests/iOSScenesTests/WordDetailTests/WordDetailReactorTests.swift diff --git a/Resources/iOSSupport/Localization/en.lproj/Localizable.strings b/Resources/iOSSupport/Localization/en.lproj/Localizable.strings index 160922cf..a4273419 100644 --- a/Resources/iOSSupport/Localization/en.lproj/Localizable.strings +++ b/Resources/iOSSupport/Localization/en.lproj/Localizable.strings @@ -80,3 +80,5 @@ more_menu = "More menu"; memorize_words = "Memorize words"; next_word = "Next word"; previous_word = "Previous word"; + +duplicate_word = "Duplicate word."; diff --git a/Resources/iOSSupport/Localization/ko.lproj/Localizable.strings b/Resources/iOSSupport/Localization/ko.lproj/Localizable.strings index a807bee8..b5966b46 100644 --- a/Resources/iOSSupport/Localization/ko.lproj/Localizable.strings +++ b/Resources/iOSSupport/Localization/ko.lproj/Localizable.strings @@ -79,3 +79,5 @@ more_menu = "메뉴 더보기"; memorize_words = "단어 암기"; next_word = "다음 단어"; previous_word = "이전 단어"; + +duplicate_word = "중복 단어입니다."; diff --git a/Sources/Domain/Interfaces/UseCases/Word/WordUseCaseProtocol.swift b/Sources/Domain/Interfaces/UseCases/Word/WordUseCaseProtocol.swift index 8787b9fa..c04d121c 100644 --- a/Sources/Domain/Interfaces/UseCases/Word/WordUseCaseProtocol.swift +++ b/Sources/Domain/Interfaces/UseCases/Word/WordUseCaseProtocol.swift @@ -44,4 +44,9 @@ public protocol WordUseCaseProtocol { func getCurrentUnmemorizedWord() -> Single + /// `word` 파라미터로 전달된 단어가 이미 저장되어 있는 단어인지 검사합니다. + /// + /// - Returns: 반환된 Sequence 는 `ture` or `false` 값을 가진 next 이벤트만 방출됩니다. error 이벤트는 방출되지 않습니다. + func isWordDuplicated(_ word: String) -> Single + } diff --git a/Sources/Domain/UseCases/WordUseCase.swift b/Sources/Domain/UseCases/WordUseCase.swift index 7e5abb3f..9e27a02e 100644 --- a/Sources/Domain/UseCases/WordUseCase.swift +++ b/Sources/Domain/UseCases/WordUseCase.swift @@ -99,6 +99,15 @@ public final class WordUseCase: WordUseCaseProtocol { } public func updateWord(by uuid: UUID, to newWord: Word) -> RxSwift.Single { + guard let originWord = wordRepository.getWord(by: uuid) else { + return .error(WordUseCaseError.retrieveFailed(reason: .uuidInvaild(uuid: uuid))) + } + + let allWords = self.wordRepository.getAllWords() + if (originWord.word != newWord.word) && allWords.contains(where: { $0.word.lowercased() == newWord.word.lowercased() }) { + return .error(WordUseCaseError.saveFailed(reason: .duplicatedWord(word: newWord.word))) + } + return .create { single in let updateTarget: Word = .init( uuid: uuid, @@ -180,4 +189,13 @@ public final class WordUseCase: WordUseCaseProtocol { return .just(currentWord) } + public func isWordDuplicated(_ word: String) -> Single { + let allWords = self.wordRepository.getAllWords() + if allWords.contains(where: { $0.word.lowercased() == word.lowercased() }) { + return .just(true) + } else { + return .just(false) + } + } + } diff --git a/Sources/DomainTesting/WordUseCaseFake.swift b/Sources/DomainTesting/WordUseCaseFake.swift index 8ff0e6f9..858de266 100644 --- a/Sources/DomainTesting/WordUseCaseFake.swift +++ b/Sources/DomainTesting/WordUseCaseFake.swift @@ -15,6 +15,7 @@ import Utility public final class WordUseCaseFake: WordUseCaseProtocol { + /// Fake 객체 구현을 위해 사용한 인메모리 단어 저장소 public var _wordList: [Domain.Word] = [] public var _unmemorizedWordList: UnmemorizedWordListRepositorySpy = .init() @@ -22,6 +23,10 @@ public final class WordUseCaseFake: WordUseCaseProtocol { public init() {} public func addNewWord(_ word: Domain.Word) -> Single { + if _wordList.contains(where: { $0.word.lowercased() == word.word.lowercased() }) { + return .error(WordUseCaseError.saveFailed(reason: .duplicatedWord(word: word.word))) + } + _wordList.append(word) _unmemorizedWordList.addWord(word) return .just(()) @@ -57,10 +62,33 @@ public final class WordUseCaseFake: WordUseCaseProtocol { } public func updateWord(by uuid: UUID, to newWord: Domain.Word) -> Single { - if let index = _wordList.firstIndex(where: { $0.uuid == uuid }) { - _wordList[index] = newWord + guard let index = _wordList.firstIndex(where: { $0.uuid == uuid }) else { + return .error(WordUseCaseError.retrieveFailed(reason: .uuidInvaild(uuid: uuid))) + } + + if (newWord.word != _wordList[index].word) && _wordList.contains(where: { $0.word.lowercased() == newWord.word.lowercased() }) { + return .error(WordUseCaseError.saveFailed(reason: .duplicatedWord(word: newWord.word))) + } + + let updateTarget: Word = .init( + uuid: uuid, + word: newWord.word, + memorizedState: newWord.memorizedState + ) + + if _unmemorizedWordList.contains(where: { $0.uuid == updateTarget.uuid }) { + switch updateTarget.memorizedState { + case .memorized: + _unmemorizedWordList.deleteWord(by: uuid) + case .memorizing: + _unmemorizedWordList.replaceWord(where: uuid, with: updateTarget) + } + } else if updateTarget.memorizedState == .memorizing { + _unmemorizedWordList.addWord(updateTarget) } - _unmemorizedWordList.replaceWord(where: uuid, with: newWord) + + _wordList[index] = updateTarget + return .just(()) } @@ -97,4 +125,12 @@ public final class WordUseCaseFake: WordUseCaseProtocol { return .just(currentWord) } + public func isWordDuplicated(_ word: String) -> Single { + if _wordList.contains(where: { $0.word.lowercased() == word.lowercased() }) { + return .just(true) + } else { + return .just(false) + } + } + } diff --git a/Sources/iOSScenes/WordAddition/WordAdditionViewController.swift b/Sources/iOSScenes/WordAddition/WordAdditionViewController.swift index b098795b..35a665ac 100644 --- a/Sources/iOSScenes/WordAddition/WordAdditionViewController.swift +++ b/Sources/iOSScenes/WordAddition/WordAdditionViewController.swift @@ -31,6 +31,13 @@ final class WordAdditionViewController: RxBaseViewController, WordAdditionViewCo $0.borderStyle = .roundedRect } + let duplicatedWordAlertLabel: UILabel = .init().then { + $0.text = WCString.duplicate_word + $0.textColor = .systemRed + $0.adjustsFontForContentSizeCategory = true + $0.font = .preferredFont(forTextStyle: .footnote) + } + lazy var cancelBarButton: UIBarButtonItem = .init(systemItem: .cancel) lazy var doneBarButton: UIBarButtonItem = .init(systemItem: .done).then { @@ -55,11 +62,17 @@ final class WordAdditionViewController: RxBaseViewController, WordAdditionViewCo func setupSubviews() { self.view.addSubview(wordTextField) + self.view.addSubview(duplicatedWordAlertLabel) wordTextField.snp.makeConstraints { make in make.top.equalTo(self.view.safeAreaLayoutGuide).offset(10) make.leading.trailing.equalTo(self.view.safeAreaLayoutGuide).inset(20) } + + duplicatedWordAlertLabel.snp.makeConstraints { make in + make.leading.trailing.equalTo(self.view.safeAreaLayoutGuide).inset(22) + make.top.equalTo(wordTextField.snp.bottom).offset(10) + } } func setupNavigationBar() { @@ -75,7 +88,7 @@ final class WordAdditionViewController: RxBaseViewController, WordAdditionViewCo return } let input: WordAdditionViewModel.Input = .init( - wordText: wordTextField.rx.text.orEmpty.asDriver(), + wordText: wordTextField.rx.text.orEmpty.asDriver().distinctUntilChanged(), saveAttempt: doneBarButton.rx.tap.asSignal(), dismissAttempt: Signal.merge( cancelBarButton.rx.tap.asSignal(), @@ -89,8 +102,6 @@ final class WordAdditionViewController: RxBaseViewController, WordAdditionViewCo .emit(with: self, onNext: { owner, _ in owner.delegate?.viewControllerMustBeDismissed(owner) }), - output.wordTextIsNotEmpty - .drive(doneBarButton.rx.isEnabled), output.reconfirmDismiss .emit(with: self, onNext: { owner, _ in owner.presentDismissActionSheet { @@ -101,6 +112,15 @@ final class WordAdditionViewController: RxBaseViewController, WordAdditionViewCo .emit(with: self, onNext: { owner, _ in owner.delegate?.viewControllerMustBeDismissed(owner) }), + output.enteredWordIsDuplicated + .distinctUntilChanged() + .map { !$0 } + .drive(duplicatedWordAlertLabel.rx.isHidden), + Driver.zip([ + output.wordTextIsNotEmpty, + output.enteredWordIsDuplicated, + ]).map { $0[0] && !$0[1] } + .drive(doneBarButton.rx.isEnabled), ] .forEach { $0.disposed(by: disposeBag) } } diff --git a/Sources/iOSScenes/WordAddition/WordAdditionViewModel.swift b/Sources/iOSScenes/WordAddition/WordAdditionViewModel.swift index 7ea95686..24c6ce2f 100644 --- a/Sources/iOSScenes/WordAddition/WordAdditionViewModel.swift +++ b/Sources/iOSScenes/WordAddition/WordAdditionViewModel.swift @@ -22,7 +22,7 @@ final class WordAdditionViewModel: ViewModelType { } func transform(input: Input) -> Output { - let initialWordText = "" + let initialWordText = "" // 새 단어를 추가할때는 초기 단어가 없으므로 let hasChanges = input.wordText.map { $0 != initialWordText } @@ -53,11 +53,18 @@ final class WordAdditionViewModel: ViewModelType { .mapToVoid() .asSignalOnErrorJustComplete() + let enteredWordIsDuplicated = input.wordText + .flatMapLatest { word in + return self.wordUseCase.isWordDuplicated(word) + .asDriverOnErrorJustComplete() + } + return .init( saveComplete: saveComplete, wordTextIsNotEmpty: wordTextIsNotEmpty, reconfirmDismiss: reconfirmDismiss, - dismissComplete: dismissComplete + dismissComplete: dismissComplete, + enteredWordIsDuplicated: enteredWordIsDuplicated ) } @@ -81,10 +88,13 @@ extension WordAdditionViewModel { let wordTextIsNotEmpty: Driver + /// Dismiss 해야하는지 재확인이 필요할때 next 이벤트가 방출됩니다. let reconfirmDismiss: Signal let dismissComplete: Signal + /// 입력되어 있는 단어가 중복된 단어인지 여부 + var enteredWordIsDuplicated: Driver } } diff --git a/Sources/iOSScenes/WordDetail/WordDetailReactor.swift b/Sources/iOSScenes/WordDetail/WordDetailReactor.swift index 67db11cb..f387f2ef 100644 --- a/Sources/iOSScenes/WordDetail/WordDetailReactor.swift +++ b/Sources/iOSScenes/WordDetail/WordDetailReactor.swift @@ -17,21 +17,45 @@ final class WordDetailReactor: Reactor { case viewDidLoad case beginEditing case doneEditing - case editWord(String) + + /// 현재 입력된 단어 + case enteredWord(String) + case changeMemorizedState(MemorizedState) } enum Mutation { case updateWord(Word) case markAsEditing + + /// 현재 입력된 단어를 중복된 단어로 표시할지 결정하는 Mutation + case setDuplicated(Bool) + + /// 현재 입력된 단어의 비어있음 상태를 결정하는 Mutation + case setEmpty(Bool) } struct State { + + /// 현재 입력된 단어 var word: Word + + /// 현재 화면에서 변경사항이 발생했는지 여부를 나타내는 값 var hasChanges: Bool + + /// 입력되어 있는 단어가 중복된 단어인지 여부를 나타내는 값 + var enteredWordIsDuplicated: Bool + + /// 현재 입력된 단어가 비어있는지 여부를 나타내는 값 + var enteredWordIsEmpty: Bool } - var initialState: State = State(word: .empty, hasChanges: false) + var initialState: State = State( + word: .empty, + hasChanges: false, + enteredWordIsDuplicated: false, + enteredWordIsEmpty: false // Detail 화면에서는 항상 초기 단어가 있으므로 + ) /// 현재 보여지고 있는 단어의 UUID 입니다. let uuid: UUID @@ -73,10 +97,24 @@ final class WordDetailReactor: Reactor { .asObservable() .flatMap { _ -> Observable in return .empty() } - case .editWord(let word): - self.currentState.word.word = word + case .enteredWord(let enteredWord): + let setDuplicatedMutation = wordUseCase.isWordDuplicated(enteredWord) + .asObservable() + .map { [weak self] isWordDuplicated in + if isWordDuplicated && (enteredWord != self?.originWord) { // 원래 단어와 달라야 중복이므로 + return Mutation.setDuplicated(true) + } else { + return Mutation.setDuplicated(false) + } + } + + self.currentState.word.word = enteredWord - return .just(.updateWord(self.currentState.word)) + return .merge([ + .just(.updateWord(self.currentState.word)), + setDuplicatedMutation, + .just(.setEmpty(enteredWord.isEmpty)), + ]) case .changeMemorizedState(let state): self.currentState.word.memorizedState = state @@ -96,6 +134,10 @@ final class WordDetailReactor: Reactor { state.word = word case .markAsEditing: state.hasChanges = true + case .setDuplicated(let enteredWordIsDuplicated): + state.enteredWordIsDuplicated = enteredWordIsDuplicated + case .setEmpty(let enteredWordIsEmpty): + state.enteredWordIsEmpty = enteredWordIsEmpty } return state diff --git a/Sources/iOSScenes/WordDetail/WordDetailViewController.swift b/Sources/iOSScenes/WordDetail/WordDetailViewController.swift index 89bf7d43..eedc68e9 100644 --- a/Sources/iOSScenes/WordDetail/WordDetailViewController.swift +++ b/Sources/iOSScenes/WordDetail/WordDetailViewController.swift @@ -8,6 +8,7 @@ import Domain import IOSSupport import ReactorKit +import RxCocoa import SnapKit import Then import UIKit @@ -35,6 +36,13 @@ final class WordDetailViewController: RxBaseViewController, WordDetailViewContro $0.accessibilityIdentifier = AccessibilityIdentifier.WordDetail.wordTextField } + let duplicatedWordAlertLabel: UILabel = .init().then { + $0.text = WCString.duplicate_word + $0.textColor = .systemRed + $0.adjustsFontForContentSizeCategory = true + $0.font = .preferredFont(forTextStyle: .footnote) + } + lazy var memorizationStatePopupButton: UIButton = { var config: UIButton.Configuration = .bordered() config.baseBackgroundColor = .systemGray5 @@ -77,6 +85,7 @@ final class WordDetailViewController: RxBaseViewController, WordDetailViewContro private func setupSubviews() { self.view.addSubview(wordTextField) + self.view.addSubview(duplicatedWordAlertLabel) self.view.addSubview(memorizationStatePopupButton) wordTextField.snp.makeConstraints { make in @@ -84,8 +93,13 @@ final class WordDetailViewController: RxBaseViewController, WordDetailViewContro make.leading.trailing.equalTo(self.view.safeAreaLayoutGuide).inset(20) } + duplicatedWordAlertLabel.snp.makeConstraints { make in + make.leading.trailing.equalTo(self.view.safeAreaLayoutGuide).inset(22) + make.top.equalTo(wordTextField.snp.bottom).offset(10) + } + memorizationStatePopupButton.snp.makeConstraints { make in - make.top.equalTo(wordTextField.snp.bottom).offset(20) + make.top.equalTo(duplicatedWordAlertLabel.snp.bottom).offset(20) make.leading.trailing.equalTo(self.view.safeAreaLayoutGuide).inset(20) } } @@ -119,7 +133,8 @@ final class WordDetailViewController: RxBaseViewController, WordDetailViewContro extension WordDetailViewController: View { func bind(reactor: WordDetailReactor) { - // Action + // MARK: Action + self.rx.sentMessage(#selector(self.viewDidLoad)) .map { _ in Reactor.Action.viewDidLoad } .bind(to: reactor.action) @@ -135,7 +150,9 @@ extension WordDetailViewController: View { .disposed(by: self.disposeBag) wordTextField.rx.text.orEmpty - .map(Reactor.Action.editWord) + .skip(1) // 초깃값("") 무시 + .distinctUntilChanged() + .map(Reactor.Action.enteredWord) .bind(to: reactor.action) .disposed(by: self.disposeBag) @@ -146,7 +163,8 @@ extension WordDetailViewController: View { .bind(to: reactor.action) .disposed(by: self.disposeBag) - // State + // MARK: State + reactor.state .map(\.hasChanges) .distinctUntilChanged() @@ -175,6 +193,28 @@ extension WordDetailViewController: View { } } .disposed(by: self.disposeBag) + + // 완료 버튼 활성화/비활성화 + Driver.zip([ + reactor.state + .map(\.enteredWordIsEmpty) + .asDriverOnErrorJustComplete(), + reactor.state + .map(\.enteredWordIsDuplicated) + .asDriverOnErrorJustComplete(), + ]).map { !$0[0] && !$0[1] } + .drive(doneBarButton.rx.isEnabled) + .disposed(by: self.disposeBag) + + // 중복 단어 경고 레이블 표시/비표시 + reactor.state + .map(\.enteredWordIsDuplicated) + .distinctUntilChanged() + .asDriverOnErrorJustComplete() + .drive(with: self) { owner, enteredWordIsDuplicated in + owner.duplicatedWordAlertLabel.isHidden = !enteredWordIsDuplicated + } + .disposed(by: self.disposeBag) } } diff --git a/Sources/iOSSupport/Localization/WCString.swift b/Sources/iOSSupport/Localization/WCString.swift index ffdcb0ef..6c6efab1 100644 --- a/Sources/iOSSupport/Localization/WCString.swift +++ b/Sources/iOSSupport/Localization/WCString.swift @@ -92,4 +92,5 @@ public struct WCString { public static let previous_word = NSLocalizedString("previous_word", bundle: Bundle.module, comment: "") public static let next_word = NSLocalizedString("next_word", bundle: Bundle.module, comment: "") + public static let duplicate_word = NSLocalizedString("duplicate_word", bundle: Bundle.module, comment: "") } diff --git a/Tests/DomainTests/WordUseCaseTests.swift b/Tests/DomainTests/WordUseCaseTests.swift index 3d0dde27..809be3bd 100644 --- a/Tests/DomainTests/WordUseCaseTests.swift +++ b/Tests/DomainTests/WordUseCaseTests.swift @@ -217,6 +217,31 @@ final class WordUseCaseTests: XCTestCase { } } + func test_isWordDuplicated() throws { + // Given + let duplicatedWord = unmemorizedWordList[0] + + // When + let isWordDuplicated = try sut.isWordDuplicated(duplicatedWord.word) + .toBlocking() + .single() + + // Then + XCTAssertTrue(isWordDuplicated) + } + + func test_throwError_whenUpdateToDuplicatedWord() { + // Given + let duplicatedWord: Word = .init(uuid: unmemorizedWordList[0].uuid, word: "J") // 단어 A 를 J(중복) 로 업데이트 + + // When + let updateWord = sut.updateWord(by: duplicatedWord.uuid, to: duplicatedWord) + .toBlocking() + + // Then + XCTAssertThrowsError(try updateWord.single()) + } + } // MARK: - Helpers diff --git a/Tests/iOSScenesTests/WordDetailTests/WordDetailReactorTests.swift b/Tests/iOSScenesTests/WordDetailTests/WordDetailReactorTests.swift new file mode 100644 index 00000000..25e77149 --- /dev/null +++ b/Tests/iOSScenesTests/WordDetailTests/WordDetailReactorTests.swift @@ -0,0 +1,81 @@ +// +// WordDetailReactorTests.swift +// WordDetailTests +// +// Created by Jaewon Yun on 2/13/24. +// Copyright © 2024 woin2ee. All rights reserved. +// + +@testable import WordDetail + +import Domain +import DomainTesting +import XCTest + +final class WordDetailReactorTests: XCTestCase { + + var sut: WordDetailReactor! + + override func tearDownWithError() throws { + try super.tearDownWithError() + sut = nil + } + + func test_enteredWordIsDuplicated() { + // Given + let uuid1: UUID = .init() + let word1: Word = .init(uuid: uuid1, word: "Word1") + + let uuid2: UUID = .init() + let word2: Word = .init(uuid: uuid2, word: "Word2") + + let wordUseCase = WordUseCaseFake() + wordUseCase._wordList = [word1, word2] + + sut = .init(uuid: word1.uuid, globalAction: .shared, wordUseCase: wordUseCase) + sut.action.onNext(.viewDidLoad) + + // When + sut.action.onNext(.enteredWord("Word2")) + + // Then + XCTAssertTrue(sut.currentState.enteredWordIsDuplicated) + } + + func test_enteredWordIsDuplicated_whenSameOriginWord() { + // Given + let uuid1: UUID = .init() + let word1: Word = .init(uuid: uuid1, word: "Word1") + + let wordUseCase = WordUseCaseFake() + wordUseCase._wordList = [word1] + + sut = .init(uuid: word1.uuid, globalAction: .shared, wordUseCase: wordUseCase) + sut.action.onNext(.viewDidLoad) + + // When + sut.action.onNext(.enteredWord("Word1")) + + // Then + XCTAssertFalse(sut.currentState.enteredWordIsDuplicated) + } + + func test_enteredWordIsEmpty() { + // Given + let uuid1: UUID = .init() + let word1: Word = .init(uuid: uuid1, word: "Word1") + + let wordUseCase = WordUseCaseFake() + wordUseCase._wordList = [word1] + + sut = .init(uuid: word1.uuid, globalAction: .shared, wordUseCase: wordUseCase) + sut.action.onNext(.viewDidLoad) + + // When + sut.action.onNext(.enteredWord("")) + + // Then + XCTAssertTrue(sut.currentState.enteredWordIsEmpty) + } + +} From 6e141065a81c9b9595e6f2af2ca169f8c19339ad Mon Sep 17 00:00:00 2001 From: Jaewon Yun Date: Thu, 15 Feb 2024 00:29:36 +0900 Subject: [PATCH 24/24] Update changelog, QA --- .gitignore | 3 +++ Changelog/1.7.0.md | 8 ++++++++ Changelog/next.md | 5 ----- QA.md | 21 ++++++++++++++++----- 4 files changed, 27 insertions(+), 10 deletions(-) create mode 100644 Changelog/1.7.0.md delete mode 100644 Changelog/next.md diff --git a/.gitignore b/.gitignore index cda1d4da..677f1625 100644 --- a/.gitignore +++ b/.gitignore @@ -78,3 +78,6 @@ Resources/InfoPlist/Info.plist ### Using tests Tests/**/*.plist + +### Changelog +Changelog/next.md diff --git a/Changelog/1.7.0.md b/Changelog/1.7.0.md new file mode 100644 index 00000000..6962b9f3 --- /dev/null +++ b/Changelog/1.7.0.md @@ -0,0 +1,8 @@ +## New features +- Added to change theme feature. + +## Enhancements +- Prevent adding duplecate word. + +## Fixed +- Fixed memory leak for view controller. diff --git a/Changelog/next.md b/Changelog/next.md deleted file mode 100644 index ca3c6c01..00000000 --- a/Changelog/next.md +++ /dev/null @@ -1,5 +0,0 @@ -## New features -- Added to change theme feature. -- Added feature that prevent to add duplecated word. - -## Fixed diff --git a/QA.md b/QA.md index 4e0de536..fe8a9b67 100644 --- a/QA.md +++ b/QA.md @@ -1,10 +1,10 @@ #Version -1.5.0 +1.7.0 ##Common - Localization 적용 확인 -##WordCheking +## WordCheking - 단어 추가 시 리스트에 반영 - 단어 암기 완료 시 리스트에 반영 @@ -12,20 +12,31 @@ - 단어 앞, 뒤 이동 정상 작동 - 마지막 단어 암기 완료 표시 시 '단어 없음' 문구 출력 - 마지막 단어 삭제 시 '단어 없음' 문구 출력 +- 중복 단어 추가 시 중복 단어 Toast 메세지 표시 확인 -##WordList +## WordList - 현재 단어 수정 시 암기 화면에 반영 - 현재 단어 삭제 시 암기 화면에 반영 - 현재 단어 암기 완료 표시 시 암기 화면에 반영 - 현재 단어 없을 때 단어 암기중 표시 시 화면에 반영 -##WordSearch +## WordAddition + +- 단어 추가 시 중복 단어일 때 중복 경고 메세지 표시 확인 +- 단어 추가 시 입력된 단어가 없을 때 완료 버튼 비활성화 확인 + +## WordDetail + +- 단어 편집 시 중복 단어일 때 중복 경고 메세지 표시 확인 +- 수정 사항이 존재할 때 취소 확인 ActionSheet 표시 확인 + +## WordSearch - 단어 검색 이상 여부 확인 - 단어 검색 후 수정 시 리스트&암기 화면에 바로 적용 여부 확인 -##Settings +## Settings - Source language / Translation language 변경시 번역 사이트에 정상 적용 확인 - 구글 드라이브 로그인 여부에 따라 로그아웃 버튼 표시