diff --git a/DependencyGraph/mohanyang_dev_graph.png b/DependencyGraph/mohanyang_dev_graph.png index 0e69108..4998a2b 100644 Binary files a/DependencyGraph/mohanyang_dev_graph.png and b/DependencyGraph/mohanyang_dev_graph.png differ diff --git a/DependencyGraph/mohanyang_prod_graph.png b/DependencyGraph/mohanyang_prod_graph.png index bc264ae..b7838f1 100644 Binary files a/DependencyGraph/mohanyang_prod_graph.png and b/DependencyGraph/mohanyang_prod_graph.png differ diff --git a/Plugins/DependencyPlugin/ProjectDescriptionHelpers/Module/Core.swift b/Plugins/DependencyPlugin/ProjectDescriptionHelpers/Module/Core.swift index d204e42..cc3f95d 100644 --- a/Plugins/DependencyPlugin/ProjectDescriptionHelpers/Module/Core.swift +++ b/Plugins/DependencyPlugin/ProjectDescriptionHelpers/Module/Core.swift @@ -13,4 +13,5 @@ public enum Core: String, Modulable { case KeychainClient case UserNotificationClient case DatabaseClient + case UserDefaultsClient } diff --git a/Projects/Core/Core/Sources/Exports.swift b/Projects/Core/Core/Sources/Exports.swift index 1e69818..9d4d244 100644 --- a/Projects/Core/Core/Sources/Exports.swift +++ b/Projects/Core/Core/Sources/Exports.swift @@ -10,3 +10,9 @@ @_exported import APIClientInterface @_exported import KeychainClient @_exported import KeychainClientInterface +@_exported import UserDefaultsClient +@_exported import UserDefaultsClientInterface +@_exported import UserNotificationClient +@_exported import UserNotificationClientInterface +@_exported import DatabaseClient +@_exported import DatabaseClientInterface diff --git a/Projects/Core/DatabaseClient/Interface/DatabaseClientInterface.swift b/Projects/Core/DatabaseClient/Interface/DatabaseClientInterface.swift index 429de45..e8b0f09 100644 --- a/Projects/Core/DatabaseClient/Interface/DatabaseClientInterface.swift +++ b/Projects/Core/DatabaseClient/Interface/DatabaseClientInterface.swift @@ -17,7 +17,7 @@ public struct DatabaseClient { /// Use create(object:). public var create: @Sendable (any Persistable) async throws -> Void /// Use read(_ type:). - public var read: @Sendable (any Persistable.Type) async throws -> [Object] + public var read: @Sendable (Object.Type) async throws -> [Object] /// Use read(_ type:, predicateFormat:, args:). public var readWithFilter: @Sendable (Object.Type, String, Any) async throws -> [Object] /// Use update(_ object:) @@ -36,7 +36,7 @@ public struct DatabaseClient { } public func read(_ type: T.Type) async throws -> [T] { - let results = try await self.read(type) + let results = try await self.read(type.ManagedObject) return await Self.realmActor?.convertToPersistable(type: type, objects: results) ?? [] } diff --git a/Projects/Core/DatabaseClient/Sources/DatabaseClient.swift b/Projects/Core/DatabaseClient/Sources/DatabaseClient.swift index 7e15c93..679e556 100644 --- a/Projects/Core/DatabaseClient/Sources/DatabaseClient.swift +++ b/Projects/Core/DatabaseClient/Sources/DatabaseClient.swift @@ -31,7 +31,7 @@ extension DatabaseClient: DependencyKey { }, read: { type in if let realmActor { - let results = await realmActor.read(type.ManagedObject as! Object.Type) + let results = await realmActor.read(type as! Object.Type) return results } else { throw(NSError(domain: "Realm is not initialized", code: 0)) diff --git a/Projects/Core/KeychainClient/Project.swift b/Projects/Core/KeychainClient/Project.swift index d95f83f..7ae4da7 100644 --- a/Projects/Core/KeychainClient/Project.swift +++ b/Projects/Core/KeychainClient/Project.swift @@ -11,8 +11,7 @@ let project: Project = .makeTMABasedProject( targets: [ .sources, .interface, - .tests, - .testing + .tests ], dependencies: [ .interface: [ diff --git a/Projects/Core/KeychainClient/Testing/KeychainClientTesting.swift b/Projects/Core/KeychainClient/Testing/KeychainClientTesting.swift deleted file mode 100644 index e09020e..0000000 --- a/Projects/Core/KeychainClient/Testing/KeychainClientTesting.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// KeychainClientTesting.swift -// KeychainClient -// -// Created by devMinseok on 7/20/24. -// Copyright © 2024 PomoNyang. All rights reserved. -// - -import KeychainClientInterface - -import Dependencies - -extension KeychainClient { - public static let mock = Self( - create: { _, _ in - return true - }, - read: { value in - return value - }, - update: { _, _ in - return true - }, - delete: { _ in - return true - }, - deleteAll: { - } - ) -} diff --git a/Projects/Core/UserDefaultsClient/Interface/UserDefaultsClientInterface.swift b/Projects/Core/UserDefaultsClient/Interface/UserDefaultsClientInterface.swift new file mode 100644 index 0000000..fbbd110 --- /dev/null +++ b/Projects/Core/UserDefaultsClient/Interface/UserDefaultsClientInterface.swift @@ -0,0 +1,44 @@ +// +// UserDefaultsClientInterface.swift +// UserDefaultsClient +// +// Created by devMinseok on 8/4/24. +// + +import Foundation + +import Dependencies +import DependenciesMacros + +@DependencyClient +public struct UserDefaultsClient { + // MARK: - Create, Update + + public var setString: @Sendable (String?, _ key: String) async -> Void + public var setBool: @Sendable (Bool, _ key: String) async -> Void + public var setData: @Sendable (Data?, _ key: String) async -> Void + public var setDouble: @Sendable (Double, _ key: String) async -> Void + public var setInteger: @Sendable (Int, _ key: String) async -> Void + + // MARK: - Read + + public var stringForKey: @Sendable (String) -> String? + public var boolForKey: @Sendable (String) -> Bool = { _ in false } + public var dataForKey: @Sendable (String) -> Data? + public var doubleForKey: @Sendable (String) -> Double = { _ in 0.0 } + public var integerForKey: @Sendable (String) -> Int = { _ in 0 } + + + // MARK: - Delete + + public var remove: @Sendable (String) async -> Void + + // MARK: - Reset + + public var removePersistentDomain: @Sendable (_ bundleId: String) -> Void +} + +extension UserDefaultsClient: TestDependencyKey { + public static var testValue = Self() + public static var previewValue = Self() +} diff --git a/Projects/Core/UserDefaultsClient/Project.swift b/Projects/Core/UserDefaultsClient/Project.swift new file mode 100644 index 0000000..845d9e7 --- /dev/null +++ b/Projects/Core/UserDefaultsClient/Project.swift @@ -0,0 +1,28 @@ +// +// UserDefaultsClientTesting.swift +// UserDefaultsClientManifests +// +// Created by devMinseok on 8/4/24. +// + +import ProjectDescription +import ProjectDescriptionHelpers + +@_spi(Core) +@_spi(Shared) +import DependencyPlugin + +let project: Project = .makeTMABasedProject( + module: Core.UserDefaultsClient, + scripts: [], + targets: [ + .sources, + .interface, + .tests + ], + dependencies: [ + .interface: [ + .dependency(rootModule: Shared.self) + ] + ] +) diff --git a/Projects/Core/UserDefaultsClient/Sources/UserDefaultsClient.swift b/Projects/Core/UserDefaultsClient/Sources/UserDefaultsClient.swift new file mode 100644 index 0000000..1bcf777 --- /dev/null +++ b/Projects/Core/UserDefaultsClient/Sources/UserDefaultsClient.swift @@ -0,0 +1,34 @@ +// +// UserDefaultsClient.swift +// UserDefaultsClient +// +// Created by devMinseok on 8/4/24. +// + +import Foundation + +import Dependencies + +import UserDefaultsClientInterface + +extension UserDefaultsClient: DependencyKey { + public static let liveValue: UserDefaultsClient = .live() + + public static func live() -> UserDefaultsClient { + let userDefaults = { UserDefaults.standard } + return .init( + setString: { userDefaults().set($0, forKey: $1) }, + setBool: { userDefaults().set($0, forKey: $1) }, + setData: { userDefaults().set($0, forKey: $1) }, + setDouble: { userDefaults().set($0, forKey: $1) }, + setInteger: { userDefaults().set($0, forKey: $1) }, + stringForKey: { userDefaults().string(forKey: $0) }, + boolForKey: { userDefaults().bool(forKey: $0) }, + dataForKey: { userDefaults().data(forKey: $0) }, + doubleForKey: { userDefaults().double(forKey: $0) }, + integerForKey: { userDefaults().integer(forKey: $0) }, + remove: { userDefaults().removeObject(forKey: $0) }, + removePersistentDomain: { userDefaults().removePersistentDomain(forName: $0) } + ) + } +} diff --git a/Projects/Core/UserDefaultsClient/Tests/UserDefaultsClientTests.swift b/Projects/Core/UserDefaultsClient/Tests/UserDefaultsClientTests.swift new file mode 100644 index 0000000..219b9f0 --- /dev/null +++ b/Projects/Core/UserDefaultsClient/Tests/UserDefaultsClientTests.swift @@ -0,0 +1,129 @@ +// +// UserDefaultsClientTests.swift +// UserDefaultsClient +// +// Created by devMinseok on 8/4/24. +// + +import XCTest + +import UserDefaultsClient +import UserDefaultsClientInterface + +import Dependencies + +final class UserDefaultsClientTests: XCTestCase { + @Dependency(UserDefaultsClient.self) var userDefaultsClient + + override func tearDown() { + withDependencies { + $0[UserDefaultsClient.self] = UserDefaultsClient.live() + } operation: { + userDefaultsClient.removePersistentDomain(bundleId: Bundle.main.bundleIdentifier ?? "") + } + super.tearDown() + } + + func testSetString() async { + await withDependencies { + $0[UserDefaultsClient.self] = UserDefaultsClient.live() + } operation: { + // given + let testKey: String = "TestKey" + let testValue: String = "TestString" + + // when + await userDefaultsClient.setString(testValue, key: testKey) + + // then + let result = userDefaultsClient.stringForKey(testKey) + XCTAssertEqual(result, testValue) + } + } + + func testSetBool() async { + await withDependencies { + $0[UserDefaultsClient.self] = UserDefaultsClient.live() + } operation: { + // given + let testKey: String = "TestKeyBool" + let testValue: Bool = true + + // when + await userDefaultsClient.setBool(testValue, testKey) + + // then + let result = userDefaultsClient.boolForKey(testKey) + XCTAssertEqual(result, testValue) + } + } + + func testSetData() async { + await withDependencies { + $0[UserDefaultsClient.self] = UserDefaultsClient.live() + } operation: { + // given + let testKey: String = "TestKeyData" + let testValue: Data = "TestData".data(using: .utf8)! + + // when + await userDefaultsClient.setData(testValue, testKey) + + // then + let result = userDefaultsClient.dataForKey(testKey) + XCTAssertEqual(result, testValue) + } + } + + func testSetDouble() async { + await withDependencies { + $0[UserDefaultsClient.self] = UserDefaultsClient.live() + } operation: { + // given + let testKey: String = "TestKeyDouble" + let testValue: Double = 123.456 + + // when + await userDefaultsClient.setDouble(testValue, testKey) + + // then + let result = userDefaultsClient.doubleForKey(testKey) + XCTAssertEqual(result, testValue) + } + } + + func testSetInteger() async { + await withDependencies { + $0[UserDefaultsClient.self] = UserDefaultsClient.live() + } operation: { + // given + let testKey: String = "TestKeyInteger" + let testValue: Int = 123 + + // when + await userDefaultsClient.setInteger(testValue, testKey) + + // then + let result = userDefaultsClient.integerForKey(testKey) + XCTAssertEqual(result, testValue) + } + } + + func testRemove() async { + await withDependencies { + $0[UserDefaultsClient.self] = UserDefaultsClient.live() + } operation: { + // given + let testKey: String = "TestKeyRemove" + let testValue: String = "TestString" + await userDefaultsClient.setString(testValue, testKey) + + // when + await userDefaultsClient.remove(testKey) + + // then + let result = userDefaultsClient.stringForKey(testKey) + XCTAssertNil(result) + } + } +} diff --git a/Projects/Core/UserNotificationClient/Sources/Implementation.swift b/Projects/Core/UserNotificationClient/Sources/Implementation.swift index 4c83f66..43382d6 100644 --- a/Projects/Core/UserNotificationClient/Sources/Implementation.swift +++ b/Projects/Core/UserNotificationClient/Sources/Implementation.swift @@ -76,7 +76,11 @@ extension UserNotificationClient { withCompletionHandler completionHandler: @escaping () -> Void ) { self.continuation.yield( - .didReceiveResponse(.init(rawValue: response)) { completionHandler() } + .didReceiveResponse(.init(rawValue: response)) { + Task { @MainActor in + completionHandler() + } + } ) } @@ -96,7 +100,11 @@ extension UserNotificationClient { @escaping (UNNotificationPresentationOptions) -> Void ) { self.continuation.yield( - .willPresentNotification(.init(rawValue: notification)) { completionHandler($0) } + .willPresentNotification(.init(rawValue: notification)) { options in + Task { @MainActor in + completionHandler(options) + } + } ) } } diff --git a/Projects/Domain/Domain/Sources/Exports.swift b/Projects/Domain/Domain/Sources/Exports.swift index 882a131..1640776 100644 --- a/Projects/Domain/Domain/Sources/Exports.swift +++ b/Projects/Domain/Domain/Sources/Exports.swift @@ -7,4 +7,4 @@ // @_exported import AppService -@_exported import AppServiceInterface +@_exported import PushService diff --git a/Projects/Feature/Feature/Sources/AppCore.swift b/Projects/Feature/Feature/Sources/AppCore.swift index 93cf503..87b8ea4 100644 --- a/Projects/Feature/Feature/Sources/AppCore.swift +++ b/Projects/Feature/Feature/Sources/AppCore.swift @@ -1,6 +1,6 @@ // // AppCore.swift -// AppServiceInterface +// Feature // // Created by devMinseok on 7/22/24. // Copyright © 2024 PomoNyang. All rights reserved. diff --git a/Projects/Feature/Feature/Sources/AppDelegateCore.swift b/Projects/Feature/Feature/Sources/AppDelegateCore.swift index cd802e8..fea3a74 100644 --- a/Projects/Feature/Feature/Sources/AppDelegateCore.swift +++ b/Projects/Feature/Feature/Sources/AppDelegateCore.swift @@ -1,6 +1,6 @@ // // AppDelegateCore.swift -// AppServiceInterface +// Feature // // Created by devMinseok on 7/22/24. // Copyright © 2024 PomoNyang. All rights reserved. diff --git a/Projects/Feature/Feature/Sources/AppView.swift b/Projects/Feature/Feature/Sources/AppView.swift index b0e5e2d..04f3032 100644 --- a/Projects/Feature/Feature/Sources/AppView.swift +++ b/Projects/Feature/Feature/Sources/AppView.swift @@ -1,6 +1,6 @@ // // AppServcieView.swift -// AppServiceInterface +// Feature // // Created by devMinseok on 7/22/24. // Copyright © 2024 PomoNyang. All rights reserved.