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,