diff --git a/Core/.swiftpm/xcode/xcshareddata/xcschemes/AboutPresentationTests.xcscheme b/Core/.swiftpm/xcode/xcshareddata/xcschemes/AboutPresentationTests.xcscheme new file mode 100644 index 0000000..984f632 --- /dev/null +++ b/Core/.swiftpm/xcode/xcshareddata/xcschemes/AboutPresentationTests.xcscheme @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Core/Package.swift b/Core/Package.swift index 028e69a..a6b7dc7 100644 --- a/Core/Package.swift +++ b/Core/Package.swift @@ -16,6 +16,9 @@ let package = Package( .library( name: "Core", targets: [ + // MARK: - About declaration + "AboutDomain", + "AboutPresentation", // MARK: - App Storage declaration "AppStorage", "RealAppStorage", @@ -48,6 +51,43 @@ let package = Package( .package(url: "https://github.com/kishikawakatsumi/swift-power-assert", from: Version(0, 12, 0)) ], targets: [ + + // MARK: - About defintion + // MARK: About Domain + .target( + name: "AboutDomain", + dependencies: [ + "Provider" + ], + path: "Sources/About/Domain" + ), + .testTarget( + name: "AboutDomainTests", + dependencies: [ + "AboutDomain" + ], + path: "Tests/About/DomainTests" + ), + + // MARK: About Presentation + .target( + name: "AboutPresentation", + dependencies: [ + "AboutDomain", + "Provider", + "SwiftlyUtils" + ], + path: "Sources/About/Presentation" + ), + .testTarget( + name: "AboutPresentationTests", + dependencies: [ + "AboutPresentation", + "SwiftlyTest", + .product(name: "PowerAssert", package: "swift-power-assert") + ], + path: "Tests/About/PresentationTests" + ), // MARK: - App Storage definition // MARK: App Storage diff --git a/Core/Sources/About/Domain/AboutDomainModule.swift b/Core/Sources/About/Domain/AboutDomainModule.swift new file mode 100644 index 0000000..01420be --- /dev/null +++ b/Core/Sources/About/Domain/AboutDomainModule.swift @@ -0,0 +1,9 @@ +import Provider + +public final class AboutDomainModule: Module { + public init() {} + + public func register(on provider: Provider) { + provider.register { RealGetAppVersion() as GetAppVersion } + } +} diff --git a/Core/Sources/About/Domain/Models/AppVersion.swift b/Core/Sources/About/Domain/Models/AppVersion.swift new file mode 100644 index 0000000..3ca0d07 --- /dev/null +++ b/Core/Sources/About/Domain/Models/AppVersion.swift @@ -0,0 +1,13 @@ +public struct AppVersion { + public let major: Int + public let minor: Int + + public init(major: Int, minor: Int) { + self.major = major + self.minor = minor + } +} + +public enum AppVersionError: Error { + case unknown +} diff --git a/Core/Sources/About/Domain/UseCases/GetAppVersion.swift b/Core/Sources/About/Domain/UseCases/GetAppVersion.swift new file mode 100644 index 0000000..87b7375 --- /dev/null +++ b/Core/Sources/About/Domain/UseCases/GetAppVersion.swift @@ -0,0 +1,43 @@ +import Foundation + +public protocol GetAppVersion { + + func run() -> Result +} + +class RealGetAppVersion: GetAppVersion { + + func run() -> Result { + guard let appVersionString = getBundleVersion() else { + return .failure(.unknown) + } + let parts = appVersionString.split(separator: ".") + return .success( + AppVersion( + major: Int(parts[0])!, + minor: Int(parts[1])! + ) + ) + } + + private func getBundleVersion() -> String? { + Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String + } +} + +public class FakeGetAppVersion: GetAppVersion { + + let appVersionResult: Result + + public init(appVersionResult: Result = .failure(.unknown)) { + self.appVersionResult = appVersionResult + } + + public convenience init(appVersion: AppVersion) { + self.init(appVersionResult: .success(appVersion)) + } + + public func run() -> Result { + appVersionResult + } +} diff --git a/Core/Sources/About/Presentation/AboutPresentationModule.swift b/Core/Sources/About/Presentation/AboutPresentationModule.swift new file mode 100644 index 0000000..d7f0d34 --- /dev/null +++ b/Core/Sources/About/Presentation/AboutPresentationModule.swift @@ -0,0 +1,16 @@ +import AboutDomain +import Provider + +public final class AboutPresentationModule: Module { + public init() {} + + public var dependencies: [Module.Type] = [ + AboutDomainModule.self + ] + + public func register(on provider: Provider) { + provider.register { + AboutViewModel(getAppVersion: provider.get()) + } + } +} diff --git a/Core/Sources/About/Presentation/AboutViewModel.swift b/Core/Sources/About/Presentation/AboutViewModel.swift new file mode 100644 index 0000000..ac0dfc0 --- /dev/null +++ b/Core/Sources/About/Presentation/AboutViewModel.swift @@ -0,0 +1,36 @@ +import AboutDomain +import Foundation +import SwiftlyUtils + +public final class AboutViewModel: ViewModel { + public typealias Action = AboutAction + public typealias State = AboutState + + private let getAppVersion: GetAppVersion + @Published public var state: AboutState + + init( + getAppVersion: GetAppVersion, + initialState: AboutState = AboutState.initial + ) { + self.getAppVersion = getAppVersion + state = initialState + Task { load() } + } + + public func send(_ action: AboutAction) { + switch action { + case .none: break + } + } + + private func load() { + let appVersion: GenericLce = getAppVersion.run() + .map { "\($0.major).\($0.minor)" } + .toLce() + + emit { + self.state = AboutState(appVersion: appVersion) + } + } +} diff --git a/Core/Sources/About/Presentation/Models/AboutAction.swift b/Core/Sources/About/Presentation/Models/AboutAction.swift new file mode 100644 index 0000000..6653984 --- /dev/null +++ b/Core/Sources/About/Presentation/Models/AboutAction.swift @@ -0,0 +1,3 @@ +public enum AboutAction { + case none +} diff --git a/Core/Sources/About/Presentation/Models/AboutState.swift b/Core/Sources/About/Presentation/Models/AboutState.swift new file mode 100644 index 0000000..b07aaab --- /dev/null +++ b/Core/Sources/About/Presentation/Models/AboutState.swift @@ -0,0 +1,14 @@ +import Foundation +import SwiftlyUtils + +public struct AboutState { + let appVersion: GenericLce +} + +extension AboutState { + static var initial: AboutState { + AboutState( + appVersion: .loading + ) + } +} diff --git a/Core/Sources/About/Presentation/UI/AboutView.swift b/Core/Sources/About/Presentation/UI/AboutView.swift new file mode 100644 index 0000000..1fa094e --- /dev/null +++ b/Core/Sources/About/Presentation/UI/AboutView.swift @@ -0,0 +1,18 @@ +import Provider +import SwiftUI + +public struct AboutView: View { + @StateObject var viewModel: AboutViewModel = getProvider().get() + + public init() {} + + public var body: some View { + VStack { + + } + } +} + +#Preview { + AboutView() +} diff --git a/Core/Sources/Common/Utils/GenericError.swift b/Core/Sources/Common/Utils/GenericError.swift new file mode 100644 index 0000000..490aee3 --- /dev/null +++ b/Core/Sources/Common/Utils/GenericError.swift @@ -0,0 +1 @@ +public struct GenericError: Error, Equatable {} diff --git a/Core/Sources/Common/Utils/Lce.swift b/Core/Sources/Common/Utils/Lce.swift new file mode 100644 index 0000000..75fdbc3 --- /dev/null +++ b/Core/Sources/Common/Utils/Lce.swift @@ -0,0 +1,32 @@ +/// Loading, Content, Error construct +public enum Lce: Equatable where C: Equatable, E: Equatable { + case content(C) + case error(E) + case loading +} + +/// Lce with GenericError +public typealias GenericLce = Lce + +public extension Result where Success: Equatable { + + /// Map Result to Lce + /// - Parameter error: closure that maps a Result's Failure to Lce's Error + func toLce(error: (Failure) -> E) -> Lce where E: Error, E: Equatable { + switch self { + case let .failure(e): Lce.error(error(e)) + case let .success(content): Lce.content(content) + } + } + + /// Maps Result to GenericLce + func toLce() -> GenericLce { + toLce(error: { _ in GenericError() }) + } +} + +public extension Lce where E == GenericError { + static var error: Lce { + .error(GenericError()) + } +} diff --git a/Core/Tests/About/DomainTests/AboutDomainTests.swift b/Core/Tests/About/DomainTests/AboutDomainTests.swift new file mode 100644 index 0000000..53e42b0 --- /dev/null +++ b/Core/Tests/About/DomainTests/AboutDomainTests.swift @@ -0,0 +1,8 @@ +// +// File.swift +// +// +// Created by Davide Giuseppe Farella on 02/01/24. +// + +import Foundation diff --git a/Core/Tests/About/PresentationTests/AboutViewModelTests.swift b/Core/Tests/About/PresentationTests/AboutViewModelTests.swift new file mode 100644 index 0000000..15c2e5a --- /dev/null +++ b/Core/Tests/About/PresentationTests/AboutViewModelTests.swift @@ -0,0 +1,50 @@ +import XCTest + +import AboutDomain +import PowerAssert +import SwiftlyTest +@testable import AboutPresentation + +final class AboutViewModelTests: XCTestCase { + + func test_initialAppVersionIsLoading() { + // given + let scenario = Scenario() + + // then + #assert(scenario.sut.state.appVersion == .loading) + } + + func test_appVersionIsLoadedCorrectly() async { + // given + let scenario = Scenario(appVersion: AppVersion(major: 1, minor: 2)) + + // when + await test(scenario.sut.$state.map(\.appVersion)) { turbine in + await turbine.expectInitial(value: .loading) + + // then + let result = await turbine.value() + #assert(result == .content("1.2")) + } + } +} + +private class Scenario { + + let sut: AboutViewModel + + init( + getAppVersion: GetAppVersion = FakeGetAppVersion() + ) { + sut = AboutViewModel(getAppVersion: getAppVersion) + } + + convenience init( + appVersion: AppVersion + ) { + self.init( + getAppVersion: FakeGetAppVersion(appVersion: appVersion) + ) + } +} diff --git a/Swiftly/SwiftlyModule.swift b/Swiftly/SwiftlyModule.swift index f3e810b..7a7237f 100644 --- a/Swiftly/SwiftlyModule.swift +++ b/Swiftly/SwiftlyModule.swift @@ -1,3 +1,4 @@ +import AboutPresentation import DateUtils import Provider import ConverterData @@ -8,6 +9,7 @@ import RealAppStorage final class SwiftlyModule: Module { var dependencies: [Module.Type] = [ + AboutPresentationModule.self, AppStorageModule.self, ConverterDataModule.self, ConverterPresentionModule.self,