diff --git a/.swiftlint.yml b/.swiftlint.yml index d35f42c..a3f5f41 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -152,7 +152,7 @@ only_rules: # Ensure definitions have a lower access control level than their enclosing parent - mark # MARK comment should be in valid format. e.g. '// MARK: ...' or '// MARK: - ...' - - missing_docs +# - missing_docs # Declarations should be documented. - modifier_order # Modifier order should be consistent. diff --git a/Example/Shared/Actions/LoadAction.swift b/Example/Shared/Actions/LoadAction.swift new file mode 100644 index 0000000..2002887 --- /dev/null +++ b/Example/Shared/Actions/LoadAction.swift @@ -0,0 +1,14 @@ +// +// LoadAction.swift +// Example +// +// Created by Paul Kraft on 20.07.21. +// + +import Presenter + +struct LoadAction: Action { + func perform(on model: Model) { + // TODO + } +} diff --git a/Example/Shared/Actions/LoginAction.swift b/Example/Shared/Actions/LoginAction.swift new file mode 100644 index 0000000..1c3c306 --- /dev/null +++ b/Example/Shared/Actions/LoginAction.swift @@ -0,0 +1,30 @@ +// +// LoginAction.swift +// Example +// +// Created by Paul Kraft on 20.07.21. +// + +import Presenter + +struct LoginAction: Action { + func perform(on model: Model) { + guard let username = model[.username], + let password = model[.password] else { + model[.loginErrorMessage] = "Could not find username or password." + model[.showLoginError] = true + return + } + + guard !username.isEmpty && !password.isEmpty else { + model[.loginErrorMessage] = "Please enter username and password." + model[.showLoginError] = true + return + } + + let prefix = Data(username.utf8).base64EncodedString() + let suffix = Data(password.utf8).base64EncodedString() + model[.authenticationToken] = prefix + ":" + suffix + model[.isAuthenticated] = true + } +} diff --git a/Example/Shared/Actions/LogoutAction.swift b/Example/Shared/Actions/LogoutAction.swift new file mode 100644 index 0000000..35a63af --- /dev/null +++ b/Example/Shared/Actions/LogoutAction.swift @@ -0,0 +1,16 @@ +// +// Logout.swift +// Example +// +// Created by Paul Kraft on 20.07.21. +// +import Presenter + +struct LogoutAction: Action { + func perform(on model: Model) { + model[.username] = nil + model[.password] = nil + model[.authenticationToken] = nil + model[.isAuthenticated] = false + } +} diff --git a/Example/Shared/App/AppPlugin.swift b/Example/Shared/App/AppPlugin.swift new file mode 100644 index 0000000..58c344b --- /dev/null +++ b/Example/Shared/App/AppPlugin.swift @@ -0,0 +1,27 @@ +// +// AppPlugin.swift +// Example +// +// Created by Paul Kraft on 20.07.21. +// + +import Presenter +import MetricPresenter +import TracePresenter + +struct AppPlugin: Plugin { + var plugins: [Plugin] { + [ + MetricPresenter(), + TracePresenter() + ] + } + + var actions: [Action.Type] { + [ + LoadAction.self, + LoginAction.self, + LogoutAction.self + ] + } +} diff --git a/Example/Shared/App/ExampleApp.swift b/Example/Shared/App/ExampleApp.swift new file mode 100644 index 0000000..eecdf27 --- /dev/null +++ b/Example/Shared/App/ExampleApp.swift @@ -0,0 +1,54 @@ +// +// ExampleApp.swift +// Shared +// +// Created by Paul Kraft on 20.07.21. +// + +import Presenter +import SwiftUI + +@main +struct ExampleApp: App { + // MARK: Stored Properties + + @SwiftUI.StateObject private var model = Model() + @SwiftUI.State private var view: AnyView? + + // MARK: Initialization + + init() { + Presenter.use(plugin: AppPlugin()) + } + + // MARK: Views + + var body: some Scene { + WindowGroup { + load(ContentView()) + .environmentObject(model) + } + } + + // MARK: Helpers + + private func load(_ presenterView: V) -> AnyView? { + if let view = view { + return view + } + DispatchQueue.global(qos: .userInitiated).async { + let view: AnyView + do { + let data = try Presenter.encode(presenterView) + view = try Presenter.decode(from: data) + .eraseToAnyView() + } catch { + view = AnyView(SwiftUI.Text(error.localizedDescription)) + } + DispatchQueue.main.async { + self.view = view + } + } + return nil + } +} diff --git a/Example/Shared/App/Model+Key.swift b/Example/Shared/App/Model+Key.swift new file mode 100644 index 0000000..1a2643c --- /dev/null +++ b/Example/Shared/App/Model+Key.swift @@ -0,0 +1,59 @@ +// +// Model+Key.swift +// Example +// +// Created by Paul Kraft on 20.07.21. +// + +import Presenter + +extension Model.Key { + static var username: Model.Key { + .init(rawValue: "login-username") + } + + static var password: Model.Key { + .init(rawValue: "login-password") + } + + static var authenticationToken: Model.Key { + .init(rawValue: "login-token") + } + + static var isAuthenticated: Model.Key { + .init(rawValue: "login-active") + } + + static var showLoginError: Model.Key { + .init(rawValue: "login-show-error") + } + + static var loginErrorMessage: Model.Key { + .init(rawValue: "login-error-message") + } + + static var isLoadingAllRecipes: Model.Key { + .init(rawValue: "recipe-all-loading") + } + + static var allRecipeIdentifiers: Model.Key<[String]> { + .init(rawValue: "recipe-all-ids") + } +} + +extension State { + init(_ key: Model.Key, default defaultValue: Content) { + self.init(key.rawValue, default: defaultValue) + } +} + +extension Model { + struct Key { + let rawValue: String + } + + subscript(key: Key) -> Value? { + get { get(key.rawValue) as? Value } + set { set(key.rawValue, to: newValue) } + } +} diff --git a/Example/Shared/Assets.xcassets/AccentColor.colorset/Contents.json b/Example/Shared/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/Example/Shared/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Example/Shared/Assets.xcassets/AppIcon.appiconset/Contents.json b/Example/Shared/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..c136eaf --- /dev/null +++ b/Example/Shared/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,148 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Example/Shared/Assets.xcassets/Contents.json b/Example/Shared/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Example/Shared/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Example/Shared/Views/ContentView.swift b/Example/Shared/Views/ContentView.swift new file mode 100644 index 0000000..c928695 --- /dev/null +++ b/Example/Shared/Views/ContentView.swift @@ -0,0 +1,21 @@ +// +// ContentView.swift +// Shared +// +// Created by Paul Kraft on 20.07.21. +// + +import Presenter + +typealias PresenterModel = Model +typealias PresenterView = View + +struct ContentView: UserView { + @State(.isAuthenticated, default: false) var isAuthenticated + + var body: View { + If(isAuthenticated, + then: HomeView(), + else: LoginView()) + } +} diff --git a/Example/Shared/Views/HomeView.swift b/Example/Shared/Views/HomeView.swift new file mode 100644 index 0000000..2cb2b35 --- /dev/null +++ b/Example/Shared/Views/HomeView.swift @@ -0,0 +1,18 @@ +// +// HomeView.swift +// Example +// +// Created by Paul Kraft on 20.07.21. +// + +import Presenter + +struct HomeView: UserView { + var body: View { + VStack(spacing: 8) { + Text("You are logged in!") + Button(Text("Logout"), action: LogoutAction()) + } + .onAppear(perform: LoadAction()) + } +} diff --git a/Example/Shared/Views/LoginView.swift b/Example/Shared/Views/LoginView.swift new file mode 100644 index 0000000..87b961f --- /dev/null +++ b/Example/Shared/Views/LoginView.swift @@ -0,0 +1,29 @@ +// +// LoginView.swift +// Example +// +// Created by Paul Kraft on 20.07.21. +// + +import Presenter + +struct LoginView: UserView { + @State(.username, default: "") var username + @State(.password, default: "") var password + @State(.showLoginError, default: false) var showLoginError + @State(.loginErrorMessage, default: "Unknown Error") var loginErrorMessage + + var body: View { + VStack { + TextField("Username", text: $username) + SecureField("Password", text: $password) + Button(Text("Login"), action: LoginAction()) + } + .padding(16) + .background(Color(red: 0.6, green: 0.6, blue: 0.6)) + .cornerRadius(8) + .shadow(radius: 8) + .padding(16) + .sheet(isPresented: $showLoginError, content: Text(loginErrorMessage)) + } +} diff --git a/Example/iOS/Info.plist b/Example/iOS/Info.plist new file mode 100644 index 0000000..efc211a --- /dev/null +++ b/Example/iOS/Info.plist @@ -0,0 +1,50 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + + UIApplicationSupportsIndirectInputEvents + + UILaunchScreen + + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/Example/macOS/Info.plist b/Example/macOS/Info.plist new file mode 100644 index 0000000..bacbc56 --- /dev/null +++ b/Example/macOS/Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + + diff --git a/Example/macOS/macOS.entitlements b/Example/macOS/macOS.entitlements new file mode 100644 index 0000000..f2ef3ae --- /dev/null +++ b/Example/macOS/macOS.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-only + + + diff --git a/Package.swift b/Package.swift index 241940a..fbbd625 100644 --- a/Package.swift +++ b/Package.swift @@ -1,68 +1,38 @@ -// swift-tools-version:5.4 +// swift-tools-version:5.5 import PackageDescription - -#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) -let exampleTarget: [Target] = [ - .executableTarget( - name: "Example", - dependencies: ["Presenter"] - ) -] -let exampleProduct: [Product] = [ - .executable( - name: "Example", - targets: ["Example", "Presenter"] - ) -] -#else -let exampleTarget: [Target] = [] -let exampleProduct: [Product] = [] -#endif - - let package = Package( name: "Presenter", - platforms: [.macOS(.v10_15), .watchOS(.v6), .tvOS(.v13), .iOS(.v13)], + platforms: [.iOS(.v13), .macOS(.v10_15), .tvOS(.v13), .watchOS(.v6)], products: [ .library( name: "Presenter", - targets: ["Presenter"] - ), + targets: ["Presenter"]), .library( name: "ChartPresenter", - targets: ["ChartPresenter"] - ), - .library( - name: "MetricPresenter", - targets: ["MetricPresenter"] - ), - .library( - name: "TracePresenter", - targets: ["TracePresenter"] - ) - ] + exampleProduct, + targets: ["ChartPresenter"]), + .executable( + name: "Example", + targets: ["Example"]), + ], + dependencies: [ + .package(url: "https://github.com/spacenation/swiftui-charts", from: "1.0.0"), + ], targets: [ .target( name: "Presenter", - dependencies: [] - ), + dependencies: []), .target( name: "ChartPresenter", - dependencies: ["Presenter"] - ), - .target( - name: "MetricPresenter", - dependencies: ["ChartPresenter"] - ), - .target( - name: "TracePresenter", - dependencies: ["Presenter"] - ), + dependencies: [ + .product(name: "Charts", package: "swiftui-charts", condition: .when(platforms: [.macOS, .macCatalyst, .iOS, .watchOS, .tvOS])), + .target(name: "Presenter"), + ]), + .executableTarget( + name: "Example", + dependencies: ["Presenter"]), .testTarget( name: "PresenterTests", - dependencies: ["Presenter", "ChartPresenter", "MetricPresenter", "TracePresenter"] - ) - ] + exampleTarget -) + dependencies: ["Presenter"]), + ]) diff --git a/README.md b/README.md index 4017b3b..3fdbfe0 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,112 @@ # Presenter -## Requirements +The `Presenter` framework to encode, decode and use distributed user interfaces. While `Presenter` can be used on a server to encode user interfaces in a similar style to SwiftUI, it can be used on the client-side (iOS, watchOS, tvOS and macOS) to decode these user interfaces and display to the user using the SwiftUI framework. -## Installation/Setup/Integration +## Features + +- Create user interfaces on the server using a declarative DSL similar to SwiftUI +- Encode the user interface using the Codable API (e.g. to JSON) +- Decode the user interface using the Codable API (e.g. from JSON) +- Display the user interface using SwiftUI Components + +- Use local memory to store temporary state +- Interact with system functionality using local actions +- Create custom views, view modifiers and actions +- Add plugins with custom views, view modifiers and actions + +## Installation + +Presenter uses the Swift Package Manager. + +```swift +dependencies: [ + .package(url: "https://github.com/Apodini/Presenter.git", from: "0.1.0") +] +``` + +After specifying the package as a dependency, you can use it in your targets. + +```swift +targets: [ + .target( + name: "YourTarget", + dependencies: [ + .product(name: "Presenter", package: "Presenter") + ] + ) +] +``` ## Usage +### Server + +You can specify `Presenter` views similar to SwiftUI. + +```swift +struct LoginView: UserView { + + @State("login-username", default: "") var username + @State("login-password", default: "") var password + + var body: View { + VStack { + TextField("Username", text: $username) + SecureField("Password", text: $password) + Button(Text("Login"), action: LoginAction()) + } + .padding(16) + .background(Color(red: 1, green: 1, blue: 1)) + .shadow(radius: 8) + .padding(16) + } + +} +``` + +This example above already showcases some of the more complicated features of SwiftUI, including state management and local actions. + +### Client + +On the client side, `Presenter` uses `ServedView` to provide a SwiftUI interface from a url or a custom publisher. + +```swift +import Presenter +import SwiftUI + +struct ContentView: SwiftUI.View { + + @StateObject var model = Model() + + var body: some SwiftUI.View { + ServedView(url: URL(string: "..."), model: model) { state in + switch state { + case .empty: + Text("Empty") + case .loading: + Text("Loading") + case let .failure(error): + Text("Error: " + error.localizedDescription) + } + } + } + +} +``` + +When using plugins, custom views, custom view modifiers or actions, please make sure to register them during app start using `Presenter.use(plugin:)`, `Presenter.use(view:)`, `Presenter.use(modifier:)` or `Presenter.use(action:)` respectively. Tip: You can also create an app-specific plugin to register multiple views, view modifiers and actions at once. + ## Example -We provide a small example proejct to showcase Presenter. -Clone the repo, navigate in the root folder and start the example application using `swift run Example`. +We provide a small example project to showcase Presenter. + +Clone the repo, navigate in the root folder and start the example server application using `swift run Example`. +To check out how to use Presenter on the client side, see the [example client application](Example). ## Contributing + Contributions to this projects are welcome. Please make sure to read the [contribution guidelines](https://github.com/Apodini/.github/blob/release/CONTRIBUTING.md) first. ## License + This project is licensed under the MIT License. See [License](https://github.com/Apodini/Presenter/blob/release/LICENSE) for more information. diff --git a/Sources/ChartPresenter/Chart.swift b/Sources/ChartPresenter/Chart.swift index 3d1840b..6164d54 100644 --- a/Sources/ChartPresenter/Chart.swift +++ b/Sources/ChartPresenter/Chart.swift @@ -1,5 +1,5 @@ -public struct Chart: SwiftUIView { +public struct Chart: View { // MARK: Stored Properties @@ -42,9 +42,9 @@ extension Chart: SwiftUI.View { #elseif canImport(SwiftUI) -extension Chart { +extension Chart: SwiftUI.View { - public var view: some SwiftUI.View { + public var body: some SwiftUI.View { EmptyView() } diff --git a/Sources/ChartPresenter/UIservCharts.swift b/Sources/ChartPresenter/ChartPresenter.swift similarity index 72% rename from Sources/ChartPresenter/UIservCharts.swift rename to Sources/ChartPresenter/ChartPresenter.swift index ed9f9e1..1441419 100644 --- a/Sources/ChartPresenter/UIservCharts.swift +++ b/Sources/ChartPresenter/ChartPresenter.swift @@ -3,7 +3,7 @@ public struct ChartPresenter: Plugin { public init() {} - public var views: [_CodableView.Type] { + public var views: [View.Type] { [ Chart.self, ] diff --git a/Sources/Example/App.swift b/Sources/Example/App.swift index 8041195..9234beb 100644 --- a/Sources/Example/App.swift +++ b/Sources/Example/App.swift @@ -1,4 +1,3 @@ - import SwiftUI @available(macOS 11.0, iOS 14.0, *) @@ -17,13 +16,4 @@ struct ExampleApp: App { .environmentObject(model) } } - } - - -struct ContentView_Provider: PreviewProvider { - static var previews: some SwiftUI.View { - ContentView().eraseToAnyView() - } -} - diff --git a/Sources/Example/ContentView.swift b/Sources/Example/ContentView.swift index d55e26d..4e0f274 100644 --- a/Sources/Example/ContentView.swift +++ b/Sources/Example/ContentView.swift @@ -1,13 +1,11 @@ - import Presenter typealias PresenterModel = Model -struct ContentView: View { +struct ContentView: UserView { @State("text", default: "") var text - - var body: some View { + var body: View { TextField("hallo", text: $text) } } diff --git a/Sources/Example/main.swift b/Sources/Example/main.swift index 0b8cbab..f9a3a29 100644 --- a/Sources/Example/main.swift +++ b/Sources/Example/main.swift @@ -1,4 +1,3 @@ - import Foundation if #available(OSX 11.0, iOS 14.0, *) { diff --git a/Sources/MetricPresenter/Card.swift b/Sources/MetricPresenter/Card.swift deleted file mode 100644 index d564521..0000000 --- a/Sources/MetricPresenter/Card.swift +++ /dev/null @@ -1,60 +0,0 @@ - -struct Card: AnyViewModifying { - - // MARK: Stored Properties - - var radius: CGFloat? - -} - -extension Card: CustomStringConvertible { - - var description: String { - "card(cornerRadius: \(radius?.description ?? "nil"))" - } - -} - -#if canImport(SwiftUI) - -extension Card: ViewModifier { - - #if os(macOS) - - func body(content: Content) -> some SwiftUI.View { - content - .background(SwiftUI.Color(.textBackgroundColor)) - .cornerRadius(radius ?? 8) - .shadow(color: SwiftUI.Color.primary.opacity(0.2), radius: 4) - } - - #else - - func body(content: Content) -> some SwiftUI.View { - content - .background(SwiftUI.Color(.systemBackground)) - .cornerRadius(radius ?? 8) - .shadow(color: SwiftUI.Color.primary.opacity(0.2), radius: 4) - } - - #endif - -} - -extension SwiftUI.View { - - public func card(cornerRadius: CGFloat? = nil) -> some SwiftUI.View { - modifier(Card(radius: cornerRadius)) - } - -} - -#endif - -extension View { - - public func card(cornerRadius: CGFloat? = nil) -> some View { - modified(using: Card(radius: cornerRadius)) - } - -} diff --git a/Sources/MetricPresenter/Exports.swift b/Sources/MetricPresenter/Exports.swift deleted file mode 100644 index 01c342d..0000000 --- a/Sources/MetricPresenter/Exports.swift +++ /dev/null @@ -1,2 +0,0 @@ - -@_exported import ChartPresenter diff --git a/Sources/MetricPresenter/Gauge.swift b/Sources/MetricPresenter/Gauge.swift deleted file mode 100644 index f92e592..0000000 --- a/Sources/MetricPresenter/Gauge.swift +++ /dev/null @@ -1,123 +0,0 @@ - -public struct Gauge: View { - - // MARK: Stored Properties - - private let value: CGFloat - private let thickness: CGFloat - private let scale: CGFloat - private let colors: [ColorCode] - private let content: CoderView - - // MARK: Initialization - - public init( - value: CGFloat, - thickness: CGFloat = 6, - scale: CGFloat = 1.777, - colors: [ColorCode], - @ViewBuilder content: () -> Content - ) { - self.value = value - self.thickness = thickness - self.scale = scale - self.colors = colors - self.content = CoderView(content()) - } - - // MARK: Views - - public var body: some View { - content - .modified(using: - GaugeModifier( - value: value, - thickness: thickness, - scale: scale, - colors: colors - ) - ) - } - -} - -struct GaugeModifier: AnyViewModifying { - - var value: CGFloat - var thickness: CGFloat - var scale: CGFloat - var colors: [ColorCode] - -} - -#if canImport(SwiftUI) - -private struct GaugeView: SwiftUI.View { - - // MARK: Stored Properties - - let gauge: GaugeModifier - let content: Content - - // MARK: Computed Properties - - var gradient: SwiftUI.AngularGradient { - .init( - gradient: .init(colors: gauge.colors.map { $0.color.view }), - center: .center, - startAngle: .degrees(0), - endAngle: .degrees(270) - ) - } - - var circleOffset: CGSize { - let ratio = -(gauge.value * 0.75) * 2 * .pi + CGFloat.pi / 2 - let radius = diameter / 2 - gauge.thickness / 2 - return CGSize(width: sin(ratio) * radius, - height: cos(ratio) * radius) - } - - // MARK: State - - @SwiftUI.State private var diameter = CGFloat.zero - - // MARK: Views - - var body: some SwiftUI.View { - SwiftUI.ZStack { - content - .size(in: GaugeSizePreferenceKey.self) - - SwiftUI.ZStack { - SwiftUI.Circle() - .trim(from: 0, to: 0.75) - .stroke(gradient, style: .init(lineWidth: gauge.thickness, lineCap: .round)) - - SwiftUI.Circle() - .stroke(SwiftUI.Color.white, style: .init(lineWidth: gauge.thickness / 2)) - .frame(width: gauge.thickness * 1.5, height: gauge.thickness * 1.5) - .offset(circleOffset) - } - .padding(gauge.thickness / 2) - .rotationEffect(.degrees(135)) - .frame(width: diameter, height: diameter) - } - .onPreferenceChange(GaugeSizePreferenceKey.self) { size in - self.diameter = size.width * self.gauge.scale - } - .foregroundColor(SwiftUI.Color.black) - } - -} - -extension GaugeModifier: ViewModifier { - - func body(content: Content) -> some SwiftUI.View { - GaugeView(gauge: self, content: content) - } - -} - -private struct GaugeSizePreferenceKey: SizePreferenceKey {} - -#endif diff --git a/Sources/MetricPresenter/Graph+Grid.swift b/Sources/MetricPresenter/Graph+Grid.swift deleted file mode 100644 index bfb2bae..0000000 --- a/Sources/MetricPresenter/Graph+Grid.swift +++ /dev/null @@ -1,77 +0,0 @@ - -#if canImport(SwiftUI) - -extension Graph { - - struct Grid { - let axis: Axis - let values: [CGFloat] - let lineWidth: CGFloat - } - -} - -extension Graph.Grid: SwiftUI.View { - - var body: some SwiftUI.View { - GeometryReader { geometry in - SwiftUI.ZStack(alignment: .topLeading) { - ForEach(self.values, id: \.self) { value in - self.line - .offset(self.offset(at: value, size: geometry.size)) - } - } - } - } - - // MARK: Helpers - - private var line: some SwiftUI.View { - SwiftUI.Color.gray - .frame(width: requiredWidth, height: requiredHeight) - .frame(maxWidth: maxWidth, maxHeight: maxHeight) - } - - private func offset(at percentage: CGFloat, size: CGSize) -> CGSize { - switch axis { - case .vertical: - return CGSize(width: percentage * size.width, height: 0) - case .horizontal: - return CGSize(width: 0, height: (1 - percentage) * size.height) - } - } - - private var requiredWidth: CGFloat? { - axis == .vertical ? lineWidth : nil - } - - private var requiredHeight: CGFloat? { - axis == .horizontal ? lineWidth : nil - } - - private var maxWidth: CGFloat? { - axis == .horizontal ? .infinity : nil - } - - private var maxHeight: CGFloat? { - axis == .vertical ? .infinity : nil - } - -} - -extension SwiftUI.View { - - public func graphGrid(vertical: [CGFloat] = [], horizontal: [CGFloat] = [], width: CGFloat = 8) -> some SwiftUI.View { - SwiftUI.ZStack { - Group { - Graph.Grid(axis: .vertical, values: vertical, lineWidth: width) - Graph.Grid(axis: .horizontal, values: horizontal, lineWidth: width) - } - - self - } - } - -} - -#endif diff --git a/Sources/MetricPresenter/Graph+Labeling.swift b/Sources/MetricPresenter/Graph+Labeling.swift deleted file mode 100644 index ac05f05..0000000 --- a/Sources/MetricPresenter/Graph+Labeling.swift +++ /dev/null @@ -1,93 +0,0 @@ - -#if canImport(SwiftUI) - -extension Graph { - - struct Labeling { - - // MARK: Stored Properties - - var top: [Graph.Label] - var trailing: [Graph.Label] - var bottom: [Graph.Label] - var leading: [Graph.Label] - - var spacing: CGFloat - var borderWidth: CGFloat - - } - -} - -extension Graph.Labeling: ViewModifier { - - func body(content: Content) -> some SwiftUI.View { - GeometryReader { geometry in - SwiftUI.HStack(spacing: 0) { - self.horizontalLabels(isLeading: true, size: geometry.size) - SwiftUI.VStack(spacing: 0) { - self.verticalLabels(isTop: true, size: geometry.size) - content - self.verticalLabels(isTop: false, size: geometry.size) - } - self.horizontalLabels(isLeading: false, size: geometry.size) - } - } - } - - // MARK: Helpers - - private func horizontalLabels(isLeading: Bool, size: CGSize) -> some SwiftUI.View { - labeling(isLeading ? leading : trailing, - at: isLeading ? .leading : .trailing) - .frame(width: borderWidth, - height: size.height - (spacing + borderWidth) * 2) - .padding(isLeading ? .trailing : .leading, spacing) - } - - private func verticalLabels(isTop: Bool, size: CGSize) -> some SwiftUI.View { - labeling(isTop ? top : bottom, - at: isTop ? .top : .bottom) - .frame(width: size.width - (spacing + borderWidth) * 2, - height: borderWidth) - .padding(isTop ? .bottom : .top, spacing) - } - - private func labeling(_ labels: [Graph.Label], at edge: Graph.Edge) -> some SwiftUI.View { - Group { - if labels.isEmpty { - SwiftUI.Color.clear - } else { - Graph.LabelingEdge(labels: labels, edge: edge) - } - } - } - -} - -extension SwiftUI.View { - - public func graphLabeling( - top: [Graph.Label] = [], - trailing: [Graph.Label] = [], - bottom: [Graph.Label] = [], - leading: [Graph.Label] = [], - spacing: CGFloat = 6, - borderWidth: CGFloat = 36 - ) -> some SwiftUI.View { - - modifier( - Graph.Labeling( - top: top, - trailing: trailing, - bottom: bottom, - leading: leading, - spacing: spacing, - borderWidth: borderWidth - ) - ) - } - -} - -#endif diff --git a/Sources/MetricPresenter/Graph+LabelingEdge.swift b/Sources/MetricPresenter/Graph+LabelingEdge.swift deleted file mode 100644 index d1c89ec..0000000 --- a/Sources/MetricPresenter/Graph+LabelingEdge.swift +++ /dev/null @@ -1,64 +0,0 @@ - -#if canImport(SwiftUI) - -extension Graph.Edge { - - var axis: Axis { - switch self { - case .top, .bottom: - return .horizontal - case .leading, .trailing: - return .vertical - } - } - - var alignment: SwiftUI.Alignment { - switch self { - case .top, .bottom: - return .center - case .leading: - return .trailing - case .trailing: - return .leading - } - } - -} - -extension Graph { - - struct LabelingEdge { - - var labels: [Graph.Label] - var edge: Graph.Edge - - } - -} - -extension Graph.LabelingEdge: SwiftUI.View { - - var body: some SwiftUI.View { - GeometryReader { geometry in - SwiftUI.ZStack(alignment: self.edge.alignment) { - ForEach(self.labels) { label in - SwiftUI.Text(label.text) - .offset(self.offset(value: label.value, size: geometry.size)) - } - } - } - .font(.caption) - } - - private func offset(value: CGFloat, size: CGSize) -> CGSize { - switch edge.axis { - case .vertical: - return CGSize(width: 0, height: (1 - value) * size.height) - case .horizontal: - return CGSize(width: value * size.width, height: 0) - } - } - -} - -#endif diff --git a/Sources/MetricPresenter/Graph.swift b/Sources/MetricPresenter/Graph.swift deleted file mode 100644 index bc6bab3..0000000 --- a/Sources/MetricPresenter/Graph.swift +++ /dev/null @@ -1,141 +0,0 @@ - -public struct Graph: SwiftUIView { - - // MARK: Nested Types - - public struct Label: Codable { - - // MARK: Stored Properties - - public var text: String - public var value: CGFloat - - // MARK: Initialization - - public init(text: String, value: CGFloat) { - self.text = text - self.value = value - } - - } - - public enum Edge: String, Codable { - case top = "t" - case trailing = "r" - case bottom = "b" - case leading = "l" - } - - public struct DataSet: Codable { - - // MARK: Stored Properties - - public var title: String - public var color: ColorCode? - public var data: [Double] - public var style: Chart.Style - - // MARK: Initialization - - public init(title: String, color: ColorCode? = nil, data: [Double], style: Chart.Style) { - self.title = title - self.color = color - self.data = data - self.style = style - } - - } - - // MARK: Stored Properties - - public var labels: [Edge: [Label]] - public var data: [DataSet] - public var gridWidth: CGFloat - - // MARK: Initialization - - public init( - topLabels: [Graph.Label]? = nil, - trailingLabels: [Graph.Label]? = nil, - bottomLabels: [Graph.Label]? = nil, - leadingLabels: [Graph.Label]? = nil, - data: [Graph.DataSet], - gridWidth: CGFloat - ) { - - self.labels = [ - .top: topLabels, - .trailing: trailingLabels, - .bottom: bottomLabels, - .leading: leadingLabels - ] - .compactMapValues { ($0 ?? []).isEmpty ? nil : $0 } - - self.data = data - self.gridWidth = gridWidth - } - -} - -#if canImport(SwiftUI) - -extension Graph.Label: Identifiable { - - public var id: CGFloat { - value - } - -} - -extension Graph.DataSet: Identifiable { - - public var id: String { - title - } - -} - -extension Graph.DataSet { - - public var chart: Chart { - Chart(data: data, style: style) - } - -} - -extension Graph { - - public var view: some SwiftUI.View { - content - .graphGrid( - vertical: verticalGridValues, - horizontal: horizontalGridValues, - width: gridWidth - ) - .graphLabeling( - top: labels[.top] ?? [], - trailing: labels[.trailing] ?? [], - bottom: labels[.bottom] ?? [], - leading: labels[.leading] ?? [] - ) - } - - private var horizontalGridValues: [CGFloat] { - let horizontalLabels = (labels[.leading] ?? []) + (labels[.trailing] ?? []) - return Set(horizontalLabels.map { $0.value }).sorted() - } - - private var verticalGridValues: [CGFloat] { - let verticalLabels = (labels[.top] ?? []) + (labels[.bottom] ?? []) - return Set(verticalLabels.map { $0.value }).sorted() - } - - private var content: some SwiftUI.View { - SwiftUI.ZStack { - ForEach(data) { $0.chart.body } - } - } - -} - -#endif diff --git a/Sources/MetricPresenter/MetricCard.swift b/Sources/MetricPresenter/MetricCard.swift deleted file mode 100644 index 65976dd..0000000 --- a/Sources/MetricPresenter/MetricCard.swift +++ /dev/null @@ -1,50 +0,0 @@ - -struct MetricCard: AnyViewModifying { - - // MARK: Stored Properties - - var title: String - var subtitle: String - -} - -#if canImport(SwiftUI) - -extension MetricCard: ViewModifier { - - func body(content: Content) -> some SwiftUI.View { - SwiftUI.VStack(alignment: .leading) { - SwiftUI.Text(title) - .font(.headline) - SwiftUI.Text(subtitle) - .font(.caption) - .opacity(0.5) - - SwiftUI.HStack { - SwiftUI.Spacer() - SwiftUI.VStack { - SwiftUI.Spacer() - content - SwiftUI.Spacer() - } - SwiftUI.Spacer() - } - .aspectRatio(2, contentMode: .fit) - .frame(maxWidth: .infinity) - } - .padding(16) - .card() - .padding(8) - } - -} - -#endif - -extension View { - - public func metricCard(title: String, subtitle: String) -> some View { - modified(using: MetricCard(title: title, subtitle: subtitle)) - } - -} diff --git a/Sources/MetricPresenter/MetricPresenter.swift b/Sources/MetricPresenter/MetricPresenter.swift deleted file mode 100644 index 79728a0..0000000 --- a/Sources/MetricPresenter/MetricPresenter.swift +++ /dev/null @@ -1,26 +0,0 @@ - -public struct MetricPresenter: Plugin { - - public init() {} - - public var views: [_CodableView.Type] { - [ - Graph.self, - ] - } - - public var viewModifiers: [AnyViewModifying.Type] { - [ - GaugeModifier.self, - MetricCard.self, - Card.self, - ] - } - - public var plugins: [Plugin.Type] { - [ - ChartPresenter.self, - ] - } - -} diff --git a/Sources/MetricPresenter/SizePreferenceKey.swift b/Sources/MetricPresenter/SizePreferenceKey.swift deleted file mode 100644 index 8dcab09..0000000 --- a/Sources/MetricPresenter/SizePreferenceKey.swift +++ /dev/null @@ -1,28 +0,0 @@ - -#if canImport(SwiftUI) - -protocol SizePreferenceKey: PreferenceKey where Value == CGSize {} - -extension SizePreferenceKey { - - static var defaultValue: CGSize { .zero } - - static func reduce(value: inout CGSize, nextValue: () -> CGSize) { - let next = nextValue() - guard next != .zero && next != value else { return } - value = next - } - -} - -extension SwiftUI.View { - func size(in key: Key.Type) -> some SwiftUI.View { - background( - GeometryReader { geometry in - SwiftUI.Color.clear.preference(key: Key.self, value: geometry.size) - } - ) - } -} - -#endif diff --git a/Sources/Presenter/Configuration/Configuration+Plugin.swift b/Sources/Presenter/Configuration/Configuration+Plugin.swift index 33bc94b..a61c709 100644 --- a/Sources/Presenter/Configuration/Configuration+Plugin.swift +++ b/Sources/Presenter/Configuration/Configuration+Plugin.swift @@ -1,14 +1,14 @@ - import Foundation extension Presenter { - private static var didImportPlugins = false // MARK: Plugins - static func usePlugins() { - guard !didImportPlugins else { return } + internal static func usePlugins() { + guard !didImportPlugins else { + return + } didImportPlugins = true use(plugin: DefaultPlugin()) } @@ -30,53 +30,44 @@ extension Presenter { plugin.plugins.forEach { $0.remove() } plugin.didRemove() } - } -extension _View where Self: Codable { - - static func use() { +extension View where Self: Decodable { + fileprivate static func use() { Presenter.use(view: Self.self) } - static func remove() { + fileprivate static func remove() { Presenter.remove(view: Self.self) } - } -extension AnyViewModifying { - - static func use() { +extension ViewModifier where Self: Codable { + fileprivate static func use() { Presenter.use(modifier: Self.self) } - static func remove() { + fileprivate static func remove() { Presenter.remove(modifier: Self.self) } - } extension Action { - - static func use() { + fileprivate static func use() { Presenter.use(action: Self.self) } - static func remove() { + fileprivate static func remove() { Presenter.remove(action: Self.self) } - } extension Plugin { - - func use() { + fileprivate func use() { Presenter.use(plugin: self) } - func remove() { + fileprivate func remove() { Presenter.remove(plugin: self) } - } diff --git a/Sources/Presenter/Configuration/Configuration+Use.swift b/Sources/Presenter/Configuration/Configuration+Use.swift index c46d22a..72e8839 100644 --- a/Sources/Presenter/Configuration/Configuration+Use.swift +++ b/Sources/Presenter/Configuration/Configuration+Use.swift @@ -1,23 +1,21 @@ - extension Presenter { - // MARK: View - public static func use(view: View.Type) { - CoderView.register(View.self) + public static func use(view: V.Type) { + CoderView.register(V.self) } - public static func remove(view: View.Type) { - CoderView.unregister(View.self) + public static func remove(view: V.Type) { + CoderView.unregister(V.self) } // MARK: View Modifier - public static func use(modifier: Modifier.Type) { + public static func use(modifier: Modifier.Type) { CoderViewModifier.register(Modifier.self) } - public static func remove(modifier: Modifier.Type) { + public static func remove(modifier: Modifier.Type) { CoderViewModifier.unregister(Modifier.self) } @@ -30,5 +28,4 @@ extension Presenter { public static func remove(action: A.Type) { CoderAction.unregister(A.self) } - } diff --git a/Sources/Presenter/Configuration/Configuration.swift b/Sources/Presenter/Configuration/Configuration.swift index f059666..f100f2f 100644 --- a/Sources/Presenter/Configuration/Configuration.swift +++ b/Sources/Presenter/Configuration/Configuration.swift @@ -1,13 +1,10 @@ - public enum Presenter { - // MARK: Decoding public static func decode( from data: Data, decoder: JSONDecoder = .init() - ) throws -> some View { - + ) throws -> View { try decoder.decode(CoderView.self, from: data) } @@ -16,8 +13,7 @@ public enum Presenter { public static func decode( from data: Decoder.Input, decoder: Decoder - ) throws -> some View { - + ) throws -> View { try decoder.decode(CoderView.self, from: data) } @@ -25,24 +21,21 @@ public enum Presenter { // MARK: Encoding - public static func encode( - _ view: V, + public static func encode( + _ view: View, encoder: JSONEncoder = .init() ) throws -> Data { - try encoder.encode(CoderView(view)) } #if canImport(Combine) - public static func encode( - _ view: V, + public static func encode( + _ view: View, encoder: Encoder ) throws -> Encoder.Output { - try encoder.encode(CoderView(view)) } #endif - } diff --git a/Sources/Presenter/Configuration/DefaultPlugin.swift b/Sources/Presenter/Configuration/DefaultPlugin.swift index c8a6bee..9b64ec5 100644 --- a/Sources/Presenter/Configuration/DefaultPlugin.swift +++ b/Sources/Presenter/Configuration/DefaultPlugin.swift @@ -1,9 +1,7 @@ - import Foundation struct DefaultPlugin: Plugin { - - var views: [_CodableView.Type] { + var views: [CodableView.Type] { [ AngularGradient.self, Button.self, @@ -38,11 +36,11 @@ struct DefaultPlugin: Plugin { Toggle.self, VGrid.self, VStack.self, - ZStack.self, + ZStack.self ] } - var viewModifiers: [AnyViewModifying.Type] { + var viewModifiers: [CodableViewModifier.Type] { [ AccentColor.self, AnimationModifier.self, @@ -63,7 +61,7 @@ struct DefaultPlugin: Plugin { Opacity.self, Padding.self, Shadow.self, - Sheet.self, + Sheet.self ] } @@ -86,8 +84,7 @@ struct DefaultPlugin: Plugin { SetAction.self, SetAction.self, SetAction.self, - SetAction.self, + SetAction.self ] } - } diff --git a/Sources/Presenter/Configuration/Exports.swift b/Sources/Presenter/Configuration/Exports.swift index 4094bd1..b95e2df 100644 --- a/Sources/Presenter/Configuration/Exports.swift +++ b/Sources/Presenter/Configuration/Exports.swift @@ -1,4 +1,3 @@ - @_exported import Foundation #if canImport(Combine) diff --git a/Sources/Presenter/Configuration/NamedType.swift b/Sources/Presenter/Configuration/NamedType.swift index 5524c17..6b887f1 100644 --- a/Sources/Presenter/Configuration/NamedType.swift +++ b/Sources/Presenter/Configuration/NamedType.swift @@ -1,14 +1,9 @@ - -import Foundation - public protocol NamedType { static var type: String { get } } extension NamedType { - public static var type: String { String(describing: self) } - } diff --git a/Sources/Presenter/Configuration/Plugin.swift b/Sources/Presenter/Configuration/Plugin.swift index e841802..d5fb227 100644 --- a/Sources/Presenter/Configuration/Plugin.swift +++ b/Sources/Presenter/Configuration/Plugin.swift @@ -1,30 +1,25 @@ - public protocol Plugin { - func willAdd() func didAdd() func willRemove() func didRemove() - var views: [_CodableView.Type] { get } - var viewModifiers: [AnyViewModifying.Type] { get } + var views: [CodableView.Type] { get } + var viewModifiers: [CodableViewModifier.Type] { get } var actions: [Action.Type] { get } var plugins: [Plugin] { get } - } extension Plugin { - public func willAdd() {} public func didAdd() {} public func willRemove() {} public func didRemove() {} - public var views: [_CodableView.Type] { [] } - public var viewModifiers: [AnyViewModifying.Type] { [] } + public var views: [CodableView.Type] { [] } + public var viewModifiers: [CodableViewModifier.Type] { [] } public var actions: [Action.Type] { [] } public var plugins: [Plugin] { [] } - } diff --git a/Sources/Presenter/Model/Action.swift b/Sources/Presenter/Model/Action.swift index 95e9caa..d79dfe3 100644 --- a/Sources/Presenter/Model/Action.swift +++ b/Sources/Presenter/Model/Action.swift @@ -1,4 +1,3 @@ - public protocol Action: Codable { static var type: String { get } @@ -8,9 +7,7 @@ public protocol Action: Codable { } extension Action { - public static var type: String { String(describing: Self.self) } - } diff --git a/Sources/Presenter/Model/Binding.swift b/Sources/Presenter/Model/Binding.swift index 978a28a..33c4e4f 100644 --- a/Sources/Presenter/Model/Binding.swift +++ b/Sources/Presenter/Model/Binding.swift @@ -1,7 +1,5 @@ - @propertyWrapper public struct Binding: Codable { - // MARK: Stored Properties let key: String @@ -18,5 +16,4 @@ public struct Binding: Codable { public static func at(_ key: String, default value: Content) -> Self { .init(key: key, default: value) } - } diff --git a/Sources/Presenter/Model/CoderAction.swift b/Sources/Presenter/Model/CoderAction.swift index afe0f2a..2907332 100644 --- a/Sources/Presenter/Model/CoderAction.swift +++ b/Sources/Presenter/Model/CoderAction.swift @@ -1,6 +1,4 @@ - public struct CoderAction { - // MARK: Nested Types private enum CodingKeys: String, CodingKey { @@ -46,13 +44,11 @@ public struct CoderAction { try container.encode(typeDescription, forKey: .type) try coder.encode(element, encoder) } - } // MARK: - Registration extension CoderAction { - // MARK: Nested Types private struct Coder { @@ -62,6 +58,7 @@ extension CoderAction { // MARK: Static Properties + // swiftlint:disable:next discouraged_optional_collection private static var _registeredTypes: [String: Coder]? private static var registeredTypes: [String: Coder] { @@ -96,13 +93,11 @@ extension CoderAction { internal static func unregister(_: A.Type) { registeredTypes.removeValue(forKey: A.type) } - } // MARK: - ModelAction extension CoderAction: Action { - #if canImport(SwiftUI) public func perform(on model: Model) { @@ -110,5 +105,4 @@ extension CoderAction: Action { } #endif - } diff --git a/Sources/Presenter/Model/ComposedAction.swift b/Sources/Presenter/Model/ComposedAction.swift index 232e990..fef06f4 100644 --- a/Sources/Presenter/Model/ComposedAction.swift +++ b/Sources/Presenter/Model/ComposedAction.swift @@ -1,6 +1,4 @@ - public struct ComposedAction: Action { - // MARK: Stored Properties fileprivate var actions: [CoderAction] @@ -20,5 +18,4 @@ public struct ComposedAction: Action { } #endif - } diff --git a/Sources/Presenter/Model/CopyAction.swift b/Sources/Presenter/Model/CopyAction.swift index 28acbcb..57ecfd1 100644 --- a/Sources/Presenter/Model/CopyAction.swift +++ b/Sources/Presenter/Model/CopyAction.swift @@ -1,16 +1,14 @@ - public struct CopyAction: Action { - // MARK: Stored Properties - private var from: String - private var to: String + private let fromKey: String + private let toKey: String // MARK: Initialization - public init(from: String, to: String) { - self.from = from - self.to = to + public init(from fromKey: String, to toKey: String) { + self.fromKey = fromKey + self.toKey = toKey } // MARK: Methods @@ -18,9 +16,8 @@ public struct CopyAction: Action { #if canImport(SwiftUI) public func perform(on model: Model) { - model.state[to] = model.state[from] + model.set(toKey, to: model.get(fromKey)) } #endif - } diff --git a/Sources/Presenter/Model/JavaScriptAction.swift b/Sources/Presenter/Model/JavaScriptAction.swift index 2747a65..dd365fa 100644 --- a/Sources/Presenter/Model/JavaScriptAction.swift +++ b/Sources/Presenter/Model/JavaScriptAction.swift @@ -1,10 +1,8 @@ - #if canImport(JavaScriptCore) import JavaScriptCore #endif public struct JavaScriptAction: Action { - // MARK: Stored Properties private var inputKeys: [String] @@ -31,44 +29,40 @@ public struct JavaScriptAction: Action { #if canImport(SwiftUI) public func perform(on model: Model) { - #if canImport(JavaScriptCore) guard let context = JSContext() else { return assertionFailure() } - context.exceptionHandler = { context, value in - model.state[errorKey] = value?.toObject() + context.exceptionHandler = { _, value in + model.set(errorKey, to: value?.toObject()) } for (key, name) in zip(inputKeys, inputNames) { - context.setObject(model.state[key], forKeyedSubscript: name as NSString) + context.setObject(model.get(key), forKeyedSubscript: name as NSString) } let functionName = "$_xyz_abc_javascript_action_main_function_cba_xyz_$_$" context.evaluateScript("function " + functionName + "() {\n" + script + "\n}") - model.state[resultKey] = context + let result = context .objectForKeyedSubscript(functionName) .call(withArguments: []) .toObject() + model.set(resultKey, to: result) #else // Not possible as of now - but is only relevant for watchOS #endif - } #endif - } extension JavaScriptAction { - public struct ScriptInput: ExpressibleByStringLiteral { - // MARK: Stored Properties public var stateKey: String @@ -85,7 +79,5 @@ extension JavaScriptAction { self.stateKey = value self.variableName = value } - } - } diff --git a/Sources/Presenter/Model/Model.swift b/Sources/Presenter/Model/Model.swift index fc8c842..e9506b8 100644 --- a/Sources/Presenter/Model/Model.swift +++ b/Sources/Presenter/Model/Model.swift @@ -1,11 +1,9 @@ - #if canImport(SwiftUI) -public final class Model: ObservableObject { - +public class Model: ObservableObject { // MARK: Stored Properties - @Published public var state: [String: Any] = [:] + @Published private var state = [String: Any]() // MARK: Initialization @@ -36,14 +34,13 @@ public final class Model: ObservableObject { { _ in action?.perform(on: self) } } - func get(key: String, for object: AnyObject) -> Any? { - state["\(ObjectIdentifier(object))_\(key)"] + public func get(_ key: String) -> Any? { + state[key] } - func set(_ any: Any?, key: String, for object: AnyObject) { - state["\(ObjectIdentifier(object))_\(key)"] = any + public func set(_ key: String, to value: Any?) { + state[key] = value } - } #endif diff --git a/Sources/Presenter/Model/ModelView.swift b/Sources/Presenter/Model/ModelView.swift index e149aaa..77a38e6 100644 --- a/Sources/Presenter/Model/ModelView.swift +++ b/Sources/Presenter/Model/ModelView.swift @@ -1,26 +1,23 @@ - #if canImport(SwiftUI) -public struct ModelView: SwiftUI.View { - +public struct ModelView: SwiftUI.View { // MARK: Stored Properties @EnvironmentObject var model: Model - let create: (Model) -> V + let create: (Model) -> Body // MARK: Initialization - public init(create: @escaping (Model) -> V) { + public init(@SwiftUI.ViewBuilder create: @escaping (Model) -> Body) { self.create = create } // MARK: Views - public var body: V { + public var body: Body { create(model) } - } #endif diff --git a/Sources/Presenter/Model/SetAction.swift b/Sources/Presenter/Model/SetAction.swift index 36b27e3..ee5acc8 100644 --- a/Sources/Presenter/Model/SetAction.swift +++ b/Sources/Presenter/Model/SetAction.swift @@ -1,6 +1,4 @@ - public struct SetAction: Action { - // MARK: Stored Properties private var key: String @@ -18,9 +16,8 @@ public struct SetAction: Action { #if canImport(SwiftUI) public func perform(on model: Model) { - model.state[key] = value + model.set(key, to: value) } #endif - } diff --git a/Sources/Presenter/Model/State.swift b/Sources/Presenter/Model/State.swift index 2da069d..80f5a44 100644 --- a/Sources/Presenter/Model/State.swift +++ b/Sources/Presenter/Model/State.swift @@ -1,7 +1,5 @@ - @propertyWrapper public struct State: Codable { - // MARK: Stored Properties private let `default`: Content @@ -23,5 +21,4 @@ public struct State: Codable { self.key = key self.default = value } - } diff --git a/Sources/Presenter/Model/Value.swift b/Sources/Presenter/Model/Value.swift index cf9a228..63f946c 100644 --- a/Sources/Presenter/Model/Value.swift +++ b/Sources/Presenter/Model/Value.swift @@ -1,6 +1,4 @@ - public struct Value: Codable { - // MARK: Stored Properties let key: String? @@ -11,7 +9,7 @@ public struct Value: Codable { #if canImport(SwiftUI) public func get(from model: Model) -> Content { - key.flatMap { model.state[$0] as? Content } ?? self.default + key.flatMap { model.get($0) as? Content } ?? self.default } #endif @@ -25,5 +23,4 @@ public struct Value: Codable { public static func at(_ key: String, default value: Content) -> Self { .init(key: key, default: value) } - } diff --git a/Sources/Presenter/Types/Alignment.swift b/Sources/Presenter/Types/Alignment.swift index a43ee55..cc46e45 100644 --- a/Sources/Presenter/Types/Alignment.swift +++ b/Sources/Presenter/Types/Alignment.swift @@ -1,4 +1,3 @@ - public enum HorizontalAlignment: String, Codable { case center, leading, trailing } @@ -16,7 +15,6 @@ public enum Alignment: String, Codable { #if canImport(SwiftUI) extension HorizontalAlignment { - var swiftUIValue: SwiftUI.HorizontalAlignment { switch self { case .center: @@ -27,11 +25,9 @@ extension HorizontalAlignment { return .trailing } } - } extension VerticalAlignment { - var swiftUIValue: SwiftUI.VerticalAlignment { switch self { case .center: @@ -42,11 +38,9 @@ extension VerticalAlignment { return .bottom } } - } extension Alignment { - var swiftUIValue: SwiftUI.Alignment { switch self { case .center: @@ -69,7 +63,6 @@ extension Alignment { return .bottomTrailing } } - } #endif diff --git a/Sources/Presenter/Types/Angle.swift b/Sources/Presenter/Types/Angle.swift index 05dff50..62bc30e 100644 --- a/Sources/Presenter/Types/Angle.swift +++ b/Sources/Presenter/Types/Angle.swift @@ -1,6 +1,4 @@ - public struct Angle { - // MARK: Stored Properties public var radians: Double @@ -31,11 +29,9 @@ public struct Angle { public static var zero: Angle { .init(radians: 0) } - } extension Angle: Codable { - public init(from decoder: Decoder) throws { self.init(radians: try .init(from: decoder)) } @@ -43,17 +39,14 @@ extension Angle: Codable { public func encode(to encoder: Encoder) throws { try radians.encode(to: encoder) } - } #if canImport(SwiftUI) extension Angle { - var swiftUIValue: SwiftUI.Angle { .init(radians: radians) } - } #endif diff --git a/Sources/Presenter/Types/Animation.swift b/Sources/Presenter/Types/Animation.swift index efb2b87..c2afa85 100644 --- a/Sources/Presenter/Types/Animation.swift +++ b/Sources/Presenter/Types/Animation.swift @@ -1,13 +1,11 @@ - public struct Animation: Codable { - // MARK: Stored Properties private var kind: Kind private var delay: Double? private var speed: Double? private var repeatCount: Int? - private var autoreverses: Bool? + private var autoreverses: Bool? // swiftlint:disable:this discouraged_optional_boolean // MARK: Static Functions @@ -17,11 +15,15 @@ public struct Animation: Codable { ) } - public static func interpolatingSpring(mass: Double? = nil, stiffness: Double, - damping: Double, initialVelocity: Double? = nil) -> Animation { + public static func interpolatingSpring(mass: Double? = nil, + stiffness: Double, + damping: Double, + initialVelocity: Double? = nil) -> Animation { Animation(kind: - .interpolatingSpring(mass: mass, stiffness: stiffness, - damping: damping, initialVelocity: initialVelocity) + .interpolatingSpring(mass: mass, + stiffness: stiffness, + damping: damping, + initialVelocity: initialVelocity) ) } @@ -61,9 +63,11 @@ public struct Animation: Codable { Animation(kind: .linear(duration: nil)) } - public static func timingCurve(c0x: Double, c0y: Double, - c1x: Double, c1y: Double, duration: Double? = nil) -> Animation { - + public static func timingCurve(c0x: Double, + c0y: Double, + c1x: Double, + c1y: Double, + duration: Double? = nil) -> Animation { Animation(kind: .timingCurve(c0x: c0x, c0y: c0y, c1x: c1x, c1y: c1y, duration: duration)) } @@ -74,8 +78,11 @@ public struct Animation: Codable { // MARK: Methods public func delay(_ interval: Double) -> Animation { - Animation(kind: kind, delay: (delay ?? 0) + interval, speed: speed, - repeatCount: repeatCount, autoreverses: autoreverses) + Animation(kind: kind, + delay: (delay ?? 0) + interval, + speed: speed, + repeatCount: repeatCount, + autoreverses: autoreverses) } public func repeatCount(_ count: Int, autoreverses: Bool = true) -> Animation { @@ -89,23 +96,21 @@ public struct Animation: Codable { public func speed(_ speed: Double) -> Animation { Animation(kind: kind, delay: delay, speed: speed, repeatCount: repeatCount, autoreverses: autoreverses) } - } extension Animation: CustomStringConvertible { - public var description: String { if case .none = kind { return "nil" } - let repeating: String = repeatCount.flatMap { count -> String? in - if count < 0 { + let repeating: String = repeatCount.flatMap { repeatCount -> String? in + if repeatCount < 0 { return ".repeatForever(autoreverses: \(autoreverses ?? true))" - } else if count == 0 { + } else if repeatCount == 0 { return nil } else { - return ".repeatCount(\(count), autoreverses: \(autoreverses ?? true))" + return ".repeatCount(\(repeatCount), autoreverses: \(autoreverses ?? true))" } } ?? "" @@ -119,13 +124,10 @@ extension Animation: CustomStringConvertible { return "Animation.\(kind)" + operators } } - } extension Animation { - enum Kind: Codable { - // MARK: Cases case spring(response: Double?, damping: Double?, blendDuration: Double?) @@ -135,8 +137,9 @@ extension Animation { case easeOut(duration: Double?) case easeInOut(duration: Double?) case linear(duration: Double?) - case timingCurve(c0x: Double, c0y: Double, c1x: Double, c1y: Double, duration: Double?) case none + case timingCurve(c0x: Double, c0y: Double, c1x: Double, c1y: Double, duration: Double?) + // swiftlint:disable:previous enum_case_associated_values_count // MARK: Nested Types @@ -199,8 +202,10 @@ extension Animation { let c0y = try container.decode(Double.self, forKey: .c0y) let c1x = try container.decode(Double.self, forKey: .c1x) let c1y = try container.decode(Double.self, forKey: .c1y) - self = .timingCurve(c0x: c0x, c0y: c0y, - c1x: c1x, c1y: c1y, + self = .timingCurve(c0x: c0x, + c0y: c0y, + c1x: c1x, + c1y: c1y, duration: duration) case .none: self = .none @@ -249,13 +254,10 @@ extension Animation { try container.encode(Name.none, forKey: .name) } } - } - } extension Animation.Kind: CustomStringConvertible { - var description: String { switch self { case let .spring(response, damping, blendDuration): @@ -288,13 +290,11 @@ extension Animation.Kind: CustomStringConvertible { return "none" } } - } #if canImport(SwiftUI) extension Animation { - internal var animation: SwiftUI.Animation? { guard var animation = kind.animation else { return nil @@ -316,11 +316,9 @@ extension Animation { } return animation } - } extension Animation.Kind { - var animation: SwiftUI.Animation? { switch self { case .none: @@ -344,11 +342,10 @@ extension Animation.Kind { return duration.map { .easeInOut(duration: $0) } ?? .easeInOut case .linear(duration: let duration): return duration.map { .linear(duration: $0) } ?? .linear - case .timingCurve(c0x: let c0x, c0y: let c0y, c1x: let c1x, c1y: let c1y, duration: let duration): - return .timingCurve(c0x, c0y, c1x, c1y, duration: duration ?? 0.35) + case let .timingCurve(c0x: c0x, c0y: c0y, c1x: c1x, c1y: c1y, duration: duration): + return .timingCurve(c0x, c0y, c1x, c1y, duration: duration ?? 0.35) } } - } #endif diff --git a/Sources/Presenter/Types/AxisSet.swift b/Sources/Presenter/Types/AxisSet.swift index 331fa5f..78ee4af 100644 --- a/Sources/Presenter/Types/AxisSet.swift +++ b/Sources/Presenter/Types/AxisSet.swift @@ -1,6 +1,4 @@ - public enum AxisSet: String, Codable, ExpressibleByArrayLiteral { - // MARK: Cases case horizontal = "h" @@ -19,13 +17,11 @@ public enum AxisSet: String, Codable, ExpressibleByArrayLiteral { self = value.contains(.horizontal) ? .horizontal : .none } } - } #if canImport(SwiftUI) extension AxisSet { - var swiftUIValue: Axis.Set { switch self { case .horizontal: diff --git a/Sources/Presenter/Types/ColorCode.swift b/Sources/Presenter/Types/ColorCode.swift index 3028a3b..ad21c4a 100644 --- a/Sources/Presenter/Types/ColorCode.swift +++ b/Sources/Presenter/Types/ColorCode.swift @@ -1,6 +1,4 @@ - public struct ColorCode: Codable { - // MARK: Nested Types private enum Error: Swift.Error { @@ -54,13 +52,11 @@ public struct ColorCode: Codable { var container = encoder.singleValueContainer() try container.encode(description) } - } // MARK: - CustomStringConvertible extension ColorCode: CustomStringConvertible { - public var description: String { fullHex() } @@ -76,5 +72,4 @@ extension ColorCode: CustomStringConvertible { let value = Int(color * Double(UInt8.max)) return String(format: "%02x", value) } - } diff --git a/Sources/Presenter/Types/Font.swift b/Sources/Presenter/Types/Font.swift index 56c200d..6c7690d 100644 --- a/Sources/Presenter/Types/Font.swift +++ b/Sources/Presenter/Types/Font.swift @@ -1,6 +1,4 @@ - public struct Font: Codable { - // MARK: Nested Types public enum Style: String, Codable { @@ -44,7 +42,7 @@ public struct Font: Codable { private var size: CGFloat? private var weight: Weight? private var design: Design? - private var styling: [Styling]? + private var styling: [Styling]? // swiftlint:disable:this discouraged_optional_collection // MARK: Factory Functions - Style @@ -137,11 +135,9 @@ public struct Font: Codable { font.weight = weight return font } - } extension Font: CustomStringConvertible { - public var description: String { let styling = [weight.map { ".weight(\($0))" }].compactMap { $0 } + (self.styling ?? []).map { "." + $0.rawValue + "()" } @@ -170,13 +166,11 @@ extension Font: CustomStringConvertible { } } } - } #if canImport(SwiftUI) extension Font { - public var swiftUIValue: SwiftUI.Font { (styling ?? []) .reduce(swiftUIValueWithoutStyling.weight(weight?.swiftUIValue ?? .regular)) { $1.apply(to: $0) } @@ -194,11 +188,9 @@ extension Font { design: design?.swiftUIValue ?? .default) } } - } extension Font.Style { - fileprivate var swiftUIValue: SwiftUI.Font.TextStyle { switch self { case .body: @@ -219,11 +211,9 @@ extension Font.Style { return .title } } - } extension Font.Weight { - fileprivate var swiftUIValue: SwiftUI.Font.Weight { switch self { case .black: @@ -246,11 +236,9 @@ extension Font.Weight { return .ultraLight } } - } extension Font.Design { - fileprivate var swiftUIValue: SwiftUI.Font.Design { switch self { case .default: @@ -271,11 +259,9 @@ extension Font.Design { #endif } } - } extension Font.Styling { - fileprivate func apply(to font: SwiftUI.Font) -> SwiftUI.Font { switch self { case .italic: @@ -292,7 +278,6 @@ extension Font.Styling { return font.uppercaseSmallCaps() } } - } #endif diff --git a/Sources/Presenter/Types/Gradient.swift b/Sources/Presenter/Types/Gradient.swift index 0c0ca3b..28d12f6 100644 --- a/Sources/Presenter/Types/Gradient.swift +++ b/Sources/Presenter/Types/Gradient.swift @@ -1,10 +1,7 @@ - public struct Gradient: Codable { - // MARK: Nested Types public struct Stop: Codable { - // MARK: Stored Properties public var colorCode: ColorCode @@ -27,7 +24,6 @@ public struct Gradient: Codable { self.colorCode = ColorCode(color) self.location = location } - } // MARK: Stored Properties @@ -45,25 +41,20 @@ public struct Gradient: Codable { public init(stops: [Stop]) { self.stops = stops } - } #if canImport(SwiftUI) extension Gradient { - var swiftUIValue: SwiftUI.Gradient { .init(stops: stops.map(\.swiftUIValue)) } - } extension Gradient.Stop { - var swiftUIValue: SwiftUI.Gradient.Stop { - .init(color: color.view, location: location) + .init(color: color.body, location: location) } - } #endif diff --git a/Sources/Presenter/Types/GridItem.swift b/Sources/Presenter/Types/GridItem.swift index 1f50489..420e5c7 100644 --- a/Sources/Presenter/Types/GridItem.swift +++ b/Sources/Presenter/Types/GridItem.swift @@ -1,16 +1,12 @@ - public struct GridItem: Codable { - // MARK: Nested Types public enum Size { - // MARK: Cases case adaptive(minimum: CGFloat, maximum: CGFloat) case fixed(CGFloat) case flexible(minimum: CGFloat, maximum: CGFloat) - } // MARK: Stored Properties @@ -26,13 +22,11 @@ public struct GridItem: Codable { self.spacing = spacing self.alignment = alignment } - } #if canImport(SwiftUI) extension GridItem { - #if !os(macOS) && !targetEnvironment(macCatalyst) @available(iOS 14.0, tvOS 14.0, watchOS 7.0, *) @@ -41,11 +35,9 @@ extension GridItem { } #endif - } extension GridItem.Size { - #if !os(macOS) && !targetEnvironment(macCatalyst) @available(iOS 14.0, tvOS 14.0, watchOS 7.0, *) @@ -61,13 +53,11 @@ extension GridItem.Size { } #endif - } #endif extension GridItem.Size: Codable { - // MARK: Nested Types enum CodingKeys: String, CodingKey { @@ -126,5 +116,4 @@ extension GridItem.Size: Codable { try container.encode(value, forKey: .value) } } - } diff --git a/Sources/Presenter/Types/PinnedScrollableViews.swift b/Sources/Presenter/Types/PinnedScrollableViews.swift index 60f61b3..cdb60db 100644 --- a/Sources/Presenter/Types/PinnedScrollableViews.swift +++ b/Sources/Presenter/Types/PinnedScrollableViews.swift @@ -1,6 +1,4 @@ - public struct PinnedScrollableViews: Codable, OptionSet { - // MARK: Stored Properties public var rawValue: UInt32 @@ -15,13 +13,11 @@ public struct PinnedScrollableViews: Codable, OptionSet { public static let sectionHeaders: PinnedScrollableViews = .init(rawValue: 1 << 1) public static let sectionFooters: PinnedScrollableViews = .init(rawValue: 1 << 0) - } #if canImport(SwiftUI) extension PinnedScrollableViews { - #if !os(macOS) && !targetEnvironment(macCatalyst) @available(iOS 14.0, tvOS 14.0, watchOS 7.0, *) @@ -37,7 +33,6 @@ extension PinnedScrollableViews { } #endif - } #endif diff --git a/Sources/Presenter/Types/RoundedCornerStyle.swift b/Sources/Presenter/Types/RoundedCornerStyle.swift index 4d9881f..954a2ae 100644 --- a/Sources/Presenter/Types/RoundedCornerStyle.swift +++ b/Sources/Presenter/Types/RoundedCornerStyle.swift @@ -1,4 +1,3 @@ - public enum RoundedCornerStyle: String, Codable { case circular case continuous @@ -7,7 +6,6 @@ public enum RoundedCornerStyle: String, Codable { #if canImport(SwiftUI) extension RoundedCornerStyle { - var swiftUIValue: SwiftUI.RoundedCornerStyle { switch self { case .circular: @@ -16,7 +14,6 @@ extension RoundedCornerStyle { return .continuous } } - } #endif diff --git a/Sources/Presenter/Types/UnitPoint.swift b/Sources/Presenter/Types/UnitPoint.swift index 08769fc..ceb5a96 100644 --- a/Sources/Presenter/Types/UnitPoint.swift +++ b/Sources/Presenter/Types/UnitPoint.swift @@ -1,6 +1,6 @@ +// swiftlint:disable identifier_name public struct UnitPoint: Codable { - // MARK: Stored Properties public var x: CGFloat @@ -25,17 +25,14 @@ public struct UnitPoint: Codable { public static let topTrailing = UnitPoint(x: 1, y: 0) public static let trailing = UnitPoint(x: 1, y: 0.5) public static let zero = UnitPoint(x: 0, y: 0) - } #if canImport(SwiftUI) extension UnitPoint { - var unitPoint: SwiftUI.UnitPoint { .init(x: x, y: y) } - } #endif diff --git a/Sources/Presenter/View - General/CoderView.swift b/Sources/Presenter/View - General/CoderView.swift index 1fa8ac6..352bfb2 100644 --- a/Sources/Presenter/View - General/CoderView.swift +++ b/Sources/Presenter/View - General/CoderView.swift @@ -1,6 +1,4 @@ - -public struct CoderView: Codable { - +public struct CoderView: CodableView { // MARK: Nested Types private enum CodingKeys: String, CodingKey { @@ -10,32 +8,34 @@ public struct CoderView: Codable { private enum Error: Swift.Error { case unregisteredType(String) - case unexpectedType(_View.Type, expected: _View.Type) + case unexpectedType(View.Type, expected: View.Type) } // MARK: Stored Properties - public let element: _View + public let body: View // MARK: Initialization - public init(_ element: _CodableView) { - if let body = element.erasedCodableBody, - Self.registeredTypes[Swift.type(of: element).type] == nil { - self.init(body) + public init(_ element: View) { + if let coderView = element as? CoderView { + self.init(coderView.body) + } else if Self.registeredTypes[Swift.type(of: element).type] == nil, + let userView = element as? UserView { + self.init(userView.body) } else { self.init(last: element) } } - private init(last element: _CodableView) { - self.element = element + private init(last element: View) { + self.body = element } public init(from decoder: Decoder) throws { if let singleValueContainer = try? decoder.singleValueContainer() { guard !singleValueContainer.decodeNil() else { - self.element = Nil() + self.body = Nil() return } } @@ -45,7 +45,7 @@ public struct CoderView: Codable { while !unkeyedContainer.isAtEnd { content.append(try unkeyedContainer.decode(CoderView.self)) } - self.element = ArrayView(content: content) + self.body = ArrayView(content: content) return } @@ -60,18 +60,18 @@ public struct CoderView: Codable { if container.contains(.modifiers) { let modifiers = try container.decode([CoderViewModifier].self, forKey: .modifiers) - self.element = ComposedView(content: CoderView(element as! _CodableView), modifiers: modifiers) + self.body = ComposedView(content: CoderView(element), modifiers: modifiers) } else { - self.element = element + self.body = element } } // MARK: Methods public func encode(to encoder: Encoder) throws { - let typeDescription = Swift.type(of: element).type + let typeDescription = Swift.type(of: body).type - if let arrayView = element as? ArrayView { + if let arrayView = body as? ArrayView { var container = encoder.unkeyedContainer() for content in arrayView.content { try container.encode(content) @@ -88,31 +88,30 @@ public struct CoderView: Codable { try container.encode(typeDescription, forKey: .type) } - if let composedView = element as? ComposedView { + if let composedView = body as? ComposedView { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(composedView.modifiers, forKey: .modifiers) try composedView.content.encode(to: encoder) } else { - try coder.encode(element, encoder) + try coder.encode(body, encoder) } } - } // MARK: - View Registration extension CoderView { - // MARK: Nested Types private struct Coder { var isOptional: Bool - var decode: (Decoder) throws -> _View - var encode: (_View, Encoder) throws -> Void + var decode: (Decoder) throws -> View + var encode: (View, Encoder) throws -> Void } // MARK: Static Properties + // swiftlint:disable:next discouraged_optional_collection private static var _registeredTypes: [String: Coder]? private static var registeredTypes: [String: Coder] { @@ -131,50 +130,47 @@ extension CoderView { // MARK: Static Functions - internal static func register(_: View.Type) { - let isOptional = View.type.hasPrefix("Optional<") + internal static func register(_: V.Type) { + let isOptional = V.type.hasPrefix("Optional<") if !isOptional { - register(Optional.self) + register(Optional.self) } let coder = Coder( isOptional: isOptional, - decode: { decoder in try View(from: decoder) }, + decode: { decoder in try V(from: decoder) }, encode: { view, encoder in - guard let viewView = view as? View else { - throw Error.unexpectedType(Swift.type(of: view), expected: View.self) + guard let viewView = view as? V else { + throw Error.unexpectedType(Swift.type(of: view), expected: V.self) } try viewView.encode(to: encoder) } ) - registeredTypes.updateValue(coder, forKey: View.type) + registeredTypes.updateValue(coder, forKey: V.type) } - internal static func unregister(_: View.Type) { - registeredTypes.removeValue(forKey: View.type) + internal static func unregister(_: V.Type) { + registeredTypes.removeValue(forKey: V.type) } - } // MARK: - CustomStringConvertible extension CoderView: CustomStringConvertible { - public var description: String { - "\(element)" + "\(body)" } - } -// MARK: - InternalView +#if canImport(SwiftUI) -extension CoderView: InternalView { - - #if canImport(SwiftUI) - - public var view: _View { - element +extension CoderView { + public func eraseToAnyView() -> AnyView { + body.eraseToAnyView() } - #endif - + public func apply(_ modifier: Modifier) -> View { + body.apply(modifier) + } } + +#endif diff --git a/Sources/Presenter/View - General/InternalView.swift b/Sources/Presenter/View - General/InternalView.swift deleted file mode 100644 index 103b350..0000000 --- a/Sources/Presenter/View - General/InternalView.swift +++ /dev/null @@ -1,32 +0,0 @@ - -public protocol InternalView: View where Body == Never { - - #if canImport(SwiftUI) - var view: _View { get } - #endif - -} - -extension InternalView { - - public var body: Never { - fatalError() - } - - #if canImport(SwiftUI) - - public func eraseToAnyView() -> AnyView { - view.eraseToAnyView() - } - - public func apply(_ modifier: M) -> _View { - view.apply(modifier) - } - - public func apply(_ modifier: AnyViewModifying) -> _View { - view.apply(modifier) - } - - #endif - -} diff --git a/Sources/Presenter/View - General/ServedView.swift b/Sources/Presenter/View - General/ServedView.swift index 19ba91b..de4c9a2 100644 --- a/Sources/Presenter/View - General/ServedView.swift +++ b/Sources/Presenter/View - General/ServedView.swift @@ -1,57 +1,142 @@ - #if canImport(SwiftUI) && canImport(Combine) -public struct ServedView: SwiftUI.View { +public struct ServedView: SwiftUI.View { + // MARK: Nested Types + + public enum PlaceholderState { + case empty + case loading + case failure(Error) + } + + private enum State { + case empty + case loading + case success(AnyView) + case failure(Error) + } // MARK: Stored Properties - public let url: URL? - @ObservedObject var model: Model + private let publisher: AnyPublisher? + private let placeholder: (PlaceholderState) -> Placeholder + @ObservedObject private var model: Model // MARK: State - @SwiftUI.State private var didStartLoading = false - @SwiftUI.State private var view: AnyView? + @SwiftUI.State private var state = State.empty @SwiftUI.State private var cancellables = Set() // MARK: Initialization - public init(url: URL?, model: Model) { - self.url = url + public init( + url: URL?, + session: URLSession = .shared, + model: Model, + @SwiftUI.ViewBuilder placeholder: @escaping (PlaceholderState) -> Placeholder + ) { + guard let url = url else { + self.init( + dataPublisher: nil as AnyPublisher?, + model: model, + placeholder: placeholder + ) + return + } + + if url.isFileURL { + self.init( + dataPublisher: Just(url).tryMap { try Data(contentsOf: $0) }, + model: model, + placeholder: placeholder + ) + } else { + self.init( + dataPublisher: session.dataTaskPublisher(for: url) + .tryMap { data, response -> Data in + guard let httpResponse = response as? HTTPURLResponse, + httpResponse.statusCode == 200 else { + throw URLError(.badServerResponse) + } + return data + }, + model: model, + placeholder: placeholder + ) + } + } + + public init( + dataPublisher: P?, + model: Model, + @SwiftUI.ViewBuilder placeholder: @escaping (PlaceholderState) -> Placeholder + ) where P.Output == Data { + let viewPublisher = dataPublisher? + .tryMap { try Presenter.decode(from: $0).eraseToAnyView() } + + self.init( + viewPublisher: viewPublisher, + model: model, + placeholder: placeholder + ) + } + + public init( + viewPublisher: P?, + model: Model, + @SwiftUI.ViewBuilder placeholder: @escaping (PlaceholderState) -> Placeholder + ) where P.Output == AnyView { + self.publisher = viewPublisher? + .mapError { $0 as Error } + .eraseToAnyPublisher() + self.model = model + self.placeholder = placeholder } // MARK: Views public var body: some SwiftUI.View { - view - .environmentObject(model) - .onAppear(perform: load) + content + .environmentObject(model) + .onAppear(perform: load) + } + + @SwiftUI.ViewBuilder + private var content: some SwiftUI.View { + switch state { + case .empty: + placeholder(.empty) + case .loading: + placeholder(.loading) + case let .success(view): + view + case let .failure(error): + placeholder(.failure(error)) + } } // MARK: Helpers private func load() { - guard let url = url, !didStartLoading else { return } - didStartLoading = true - guard !url.isFileURL else { - if let data = try? Data(contentsOf: url), - let view = try? Presenter.decode(from: data) { - self.view = view.eraseToAnyView() - } + switch state { + case .success, .failure, .loading: return + case .empty: + guard let publisher = publisher else { + return + } + + state = .loading + + publisher + .map { State.success($0) } + .catch { Just(.failure($0)) } + .receive(on: DispatchQueue.main) + .assign(to: \.state, on: self) + .store(in: &cancellables) } - URLSession.shared.dataTaskPublisher(for: url) - .map(\.data) - .decode(type: CoderView.self, decoder: JSONDecoder()) - .map(Optional.some) - .replaceError(with: nil) - .map { $0?.eraseToAnyView() } - .receive(on: DispatchQueue.main) - .assign(to: \.view, on: self) - .store(in: &cancellables) } - } #endif diff --git a/Sources/Presenter/View - General/SwiftUIView.swift b/Sources/Presenter/View - General/SwiftUIView.swift deleted file mode 100644 index 4942666..0000000 --- a/Sources/Presenter/View - General/SwiftUIView.swift +++ /dev/null @@ -1,33 +0,0 @@ - -public protocol SwiftUIView: View where Body == Never { - - #if canImport(SwiftUI) - associatedtype SwiftUIBody: SwiftUI.View - var view: SwiftUIBody { get } - #endif - -} - -extension SwiftUIView { - - public var body: Never { - fatalError() - } - - #if canImport(SwiftUI) - - public func eraseToAnyView() -> AnyView { - AnyView(view) - } - - public func apply(_ modifier: M) -> _View { - view.modifier(modifier) - } - - public func apply(_ modifier: AnyViewModifying) -> _View { - modifier.apply(to: view) - } - - #endif - -} diff --git a/Sources/Presenter/View - General/View.swift b/Sources/Presenter/View - General/View.swift index de0d965..0d132a4 100644 --- a/Sources/Presenter/View - General/View.swift +++ b/Sources/Presenter/View - General/View.swift @@ -1,88 +1,54 @@ +public typealias CodableView = View & Decodable -public typealias _CodableView = _View & Codable - -public protocol _View: NamedType { - - var erasedCodableBody: _CodableView? { get } - +public protocol View: NamedType, Encodable { #if canImport(SwiftUI) - func eraseToAnyView() -> AnyView - func apply(_ m: M) -> _View - func apply(_ modifier: AnyViewModifying) -> _View - + func apply(_ modifier: Modifier) -> View #endif +} + +public typealias CodableWrapperView = WrapperView & Decodable +public protocol WrapperView: View { + #if canImport(SwiftUI) + var body: View { get } + #endif } #if canImport(SwiftUI) -extension _View where Self: SwiftUI.View { - +extension WrapperView { public func eraseToAnyView() -> AnyView { - AnyView(self) - } - - public func apply(_ m: M) -> _View { - modifier(m) + body.eraseToAnyView() } - public func apply(_ modifier: AnyViewModifying) -> _View { - modifier.apply(to: self) + public func apply(_ modifier: Modifier) -> View { + body.apply(modifier) } - } #endif - -public protocol View: _View, Codable { - associatedtype Body: View - var body: Body { get } -} - -extension Never: View { - - public var body: Never { - get { - fatalError("This can't happen.") - } - } - - public init(from decoder: Decoder) throws { - fatalError() - } - - public func encode(to encoder: Encoder) throws { - switch self {} - } - +public protocol UserView: WrapperView { + var body: View { get } } -extension View { - - public var erasedCodableBody: _CodableView? { - Body.self == Never.self ? nil : body +extension UserView { + func encode(to encoder: Encoder) throws { + try body.encode(to: encoder) } - } #if canImport(SwiftUI) -extension View { - +extension View where Self: SwiftUI.View { public func eraseToAnyView() -> AnyView { - body.eraseToAnyView() - } - - public func apply(_ modifier: M) -> _View { - body.apply(modifier) + AnyView(self) } - public func apply(_ modifier: AnyViewModifying) -> _View { - body.apply(modifier) + public func apply(_ modifier: Modifier) -> View { + modifier.body(for: self) } - } #endif diff --git a/Sources/Presenter/View - General/ViewBuilder.swift b/Sources/Presenter/View - General/ViewBuilder.swift index f63b545..2d86541 100644 --- a/Sources/Presenter/View - General/ViewBuilder.swift +++ b/Sources/Presenter/View - General/ViewBuilder.swift @@ -1,31 +1,28 @@ - @resultBuilder -public struct ViewBuilder { - +public enum ViewBuilder { // MARK: Static Functions - public static func buildBlock(_ item: V) -> some View { + public static func buildBlock(_ item: V) -> View { item } - public static func buildBlock(_ items: _CodableView...) -> some View { + public static func buildBlock(_ items: View...) -> View { ArrayView(content: items) } - public static func buildBlock(_ items: [_CodableView]) -> some View { + public static func buildBlock(_ items: [View]) -> View { ArrayView(content: items) } - public static func buildEither(first: V) -> V { + public static func buildEither(first: View) -> View { first } - public static func buildEither(second: V) -> V { + public static func buildEither(second: View) -> View { second } - static func buildIf(_ content: V?) -> V? { + static func buildIf(_ content: View?) -> View? { content } - } diff --git a/Sources/Presenter/ViewModifier - General/AnyViewModifying.swift b/Sources/Presenter/ViewModifier - General/AnyViewModifying.swift deleted file mode 100644 index 6eecb8e..0000000 --- a/Sources/Presenter/ViewModifier - General/AnyViewModifying.swift +++ /dev/null @@ -1,30 +0,0 @@ - -public protocol AnyViewModifying: Codable, NamedType { - - #if canImport(SwiftUI) - - func apply(to view: V) -> _View - - #endif - -} - -#if canImport(SwiftUI) - -extension AnyViewModifying where Self: ViewModifier { - - public func apply(to view: V) -> _View { - view.modifier(self) - } - -} - -extension ModifiedContent: _View, NamedType where Content: SwiftUI.View, Modifier: ViewModifier { - - public var erasedCodableBody: _CodableView? { - nil - } - -} - -#endif diff --git a/Sources/Presenter/ViewModifier - General/CoderViewModifier.swift b/Sources/Presenter/ViewModifier - General/CoderViewModifier.swift index bc394f3..994440c 100644 --- a/Sources/Presenter/ViewModifier - General/CoderViewModifier.swift +++ b/Sources/Presenter/ViewModifier - General/CoderViewModifier.swift @@ -1,6 +1,4 @@ - -public struct CoderViewModifier: Codable, AnyViewModifying { - +public struct CoderViewModifier: CodableViewModifier { // MARK: Nested Types private enum CodingKeys: String, CodingKey { @@ -10,16 +8,16 @@ public struct CoderViewModifier: Codable, AnyViewModifying { private enum Error: Swift.Error { case unregisteredType(String) - case unexpectedType(AnyViewModifying.Type, expected: AnyViewModifying.Type) + case unexpectedType(ViewModifier.Type, expected: ViewModifier.Type) } // MARK: Stored Properties - let element: AnyViewModifying + let element: ViewModifier // MARK: Initialization - internal init(_ element: AnyViewModifying) { + internal init(_ element: ViewModifier) { self.element = element } @@ -45,32 +43,29 @@ public struct CoderViewModifier: Codable, AnyViewModifying { } try coder.encode(element, encoder) } - } // MARK: - CustomStringConvertible extension CoderViewModifier: CustomStringConvertible { - public var description: String { "\(element)" } - } // MARK: - Registration extension CoderViewModifier { - // MARK: Nested Types private struct Coder { - var decode: (Decoder) throws -> AnyViewModifying - var encode: (AnyViewModifying, Encoder) throws -> Void + var decode: (Decoder) throws -> ViewModifier + var encode: (ViewModifier, Encoder) throws -> Void } // MARK: Static Properties + // swiftlint:disable:next discouraged_optional_collection private static var _registeredTypes: [String: Coder]? private static var registeredTypes: [String: Coder] { @@ -89,7 +84,7 @@ extension CoderViewModifier { // MARK: Static Functions - public static func register(_: Modifier.Type) { + public static func register(_: Modifier.Type) { let coder = Coder( decode: { decoder in try Modifier(from: decoder) }, encode: { modifier, encoder in @@ -102,10 +97,9 @@ extension CoderViewModifier { registeredTypes.updateValue(coder, forKey: Modifier.type) } - public static func unregister(_: Modifier.Type) { + public static func unregister(_: Modifier.Type) { registeredTypes.removeValue(forKey: Modifier.type) } - } // MARK: - AnyViewModifying @@ -113,12 +107,9 @@ extension CoderViewModifier { #if canImport(SwiftUI) extension CoderViewModifier { - - public func apply(to view: V) -> _View { - element.apply(to: view) + public func body(for content: Content) -> View { + element.body(for: content) } - } #endif - diff --git a/Sources/Presenter/ViewModifier - General/ViewModifier.swift b/Sources/Presenter/ViewModifier - General/ViewModifier.swift new file mode 100644 index 0000000..b8e22ec --- /dev/null +++ b/Sources/Presenter/ViewModifier - General/ViewModifier.swift @@ -0,0 +1,39 @@ +public typealias CodableViewModifier = Codable & ViewModifier + +public protocol ViewModifier: NamedType { + #if canImport(SwiftUI) + + func body(for content: Content) -> View + + #endif +} + +#if canImport(SwiftUI) + +extension ViewModifier where Self: SwiftUI.ViewModifier { + public func body(for content: Content) -> View { + content.modifier(self) + } +} + +extension ModifiedContent: View, Encodable, NamedType + where Content: SwiftUI.View, Modifier: SwiftUI.ViewModifier { + public func encode(to encoder: Encoder) throws { + guard let content = content as? View, + let modifier = modifier as? ViewModifier else { + assertionFailure("Why?") + return + } + try modifier.encode(with: content, to: encoder) + } +} + +extension ViewModifier { + fileprivate func encode(with view: View, to encoder: Encoder) throws { + try CoderView(view) + .modifier(self) + .encode(to: encoder) + } +} + +#endif diff --git a/Sources/Presenter/ViewModifiers/AccentColor.swift b/Sources/Presenter/ViewModifiers/AccentColor.swift index 993d4d2..2abbe3e 100644 --- a/Sources/Presenter/ViewModifiers/AccentColor.swift +++ b/Sources/Presenter/ViewModifiers/AccentColor.swift @@ -1,28 +1,22 @@ - -internal struct AccentColor: AnyViewModifying { - +internal struct AccentColor: CodableViewModifier { // MARK: Stored Properties let color: ColorCode - } // MARK: - CustomStringConvertible extension AccentColor: CustomStringConvertible { - var description: String { "accentColor(\(color))" } - } // MARK: - ViewModifier #if canImport(SwiftUI) -extension AccentColor: ViewModifier { - +extension AccentColor: SwiftUI.ViewModifier { #if os(macOS) func body(content: Content) -> some SwiftUI.View { @@ -32,7 +26,7 @@ extension AccentColor: ViewModifier { #else func body(content: Content) -> some SwiftUI.View { - content.accentColor(color.color.view) + content.accentColor(color.color.body) } #endif @@ -43,9 +37,7 @@ extension AccentColor: ViewModifier { // MARK: - View Extensions extension View { - - public func accentColor(_ color: Color) -> some View { - modified(using: AccentColor(color: ColorCode(color))) + public func accentColor(_ color: Color) -> View { + modifier(AccentColor(color: ColorCode(color))) } - } diff --git a/Sources/Presenter/ViewModifiers/AnimationModifier.swift b/Sources/Presenter/ViewModifiers/AnimationModifier.swift index 441591c..a1d0faf 100644 --- a/Sources/Presenter/ViewModifiers/AnimationModifier.swift +++ b/Sources/Presenter/ViewModifiers/AnimationModifier.swift @@ -1,32 +1,25 @@ - -internal struct AnimationModifier: AnyViewModifying { - +internal struct AnimationModifier: CodableViewModifier { // MARK: Stored Properties let animation: Animation - } // MARK: - CustomStringConvertible extension AnimationModifier: CustomStringConvertible { - var description: String { "animation(\(animation))" } - } // MARK: - ViewModifier #if canImport(SwiftUI) -extension AnimationModifier: ViewModifier { - +extension AnimationModifier: SwiftUI.ViewModifier { func body(content: Content) -> some SwiftUI.View { content.animation(animation.animation) } - } #endif @@ -34,9 +27,7 @@ extension AnimationModifier: ViewModifier { // MARK: - View Extensions extension View { - - public func animation(_ animation: Animation?) -> some View { - modified(using: AnimationModifier(animation: animation ?? .none)) + public func animation(_ animation: Animation?) -> View { + modifier(AnimationModifier(animation: animation ?? .none)) } - } diff --git a/Sources/Presenter/ViewModifiers/AspectRatio.swift b/Sources/Presenter/ViewModifiers/AspectRatio.swift index f3030a1..34313ef 100644 --- a/Sources/Presenter/ViewModifiers/AspectRatio.swift +++ b/Sources/Presenter/ViewModifiers/AspectRatio.swift @@ -1,22 +1,18 @@ - public enum ContentMode: String, Codable { case fit case fill } -internal struct AspectRatio: AnyViewModifying { - +internal struct AspectRatio: CodableViewModifier { // MARK: Stored Properties var ratio: CGFloat? var contentMode: ContentMode - } // MARK: - CustomStringConvertible extension AspectRatio: CustomStringConvertible { - var description: String { if let ratio = ratio { return "aspectRatio(\(ratio), contentMode: \(contentMode))" @@ -29,23 +25,19 @@ extension AspectRatio: CustomStringConvertible { return "scaledToFill()" } } - } // MARK: - ViewModifier #if canImport(SwiftUI) -extension AspectRatio: ViewModifier { - +extension AspectRatio: SwiftUI.ViewModifier { func body(content: Content) -> some SwiftUI.View { content.aspectRatio(ratio, contentMode: contentMode.swiftUIValue) } - } extension ContentMode { - fileprivate var swiftUIValue: SwiftUI.ContentMode { switch self { case .fill: @@ -54,7 +46,6 @@ extension ContentMode { return .fit } } - } #endif @@ -62,21 +53,19 @@ extension ContentMode { // MARK: - View Extensions extension View { - - public func aspectRatio(_ ratio: CGSize, contentMode: ContentMode) -> some View { - modified(using: AspectRatio(ratio: ratio.width / ratio.height, contentMode: contentMode)) + public func aspectRatio(_ ratio: CGSize, contentMode: ContentMode) -> View { + modifier(AspectRatio(ratio: ratio.width / ratio.height, contentMode: contentMode)) } - public func aspectRatio(_ ratio: CGFloat?, contentMode: ContentMode) -> some View { - modified(using: AspectRatio(ratio: ratio, contentMode: contentMode)) + public func aspectRatio(_ ratio: CGFloat?, contentMode: ContentMode) -> View { + modifier(AspectRatio(ratio: ratio, contentMode: contentMode)) } - public func scaledToFit() -> some View { - modified(using: AspectRatio(ratio: nil, contentMode: .fit)) + public func scaledToFit() -> View { + modifier(AspectRatio(ratio: nil, contentMode: .fit)) } - public func scaledToFill() -> some View { - modified(using: AspectRatio(ratio: nil, contentMode: .fill)) + public func scaledToFill() -> View { + modifier(AspectRatio(ratio: nil, contentMode: .fill)) } - } diff --git a/Sources/Presenter/ViewModifiers/Background.swift b/Sources/Presenter/ViewModifiers/Background.swift index f924227..b755d83 100644 --- a/Sources/Presenter/ViewModifiers/Background.swift +++ b/Sources/Presenter/ViewModifiers/Background.swift @@ -1,42 +1,41 @@ - -internal struct Background: AnyViewModifying { - +internal struct Background: CodableViewModifier { // MARK: Stored Properties - let view: CoderView - + let background: CoderView } // MARK: - CustomStringConvertible extension Background: CustomStringConvertible { - var description: String { - "background(\(view))" + "background(\(background))" } - } // MARK: - ViewModifier #if canImport(SwiftUI) -extension Background: ViewModifier { +extension Background { + public func body(for content: Content) -> View { + background.modifier(Modifier(foreground: content)) + } +} + +private struct Modifier: ViewModifier, SwiftUI.ViewModifier { + let foreground: Foreground func body(content: Content) -> some SwiftUI.View { - content.background(view.eraseToAnyView()) + foreground.background(content) } - } #endif // MARK: - View Extensions -extension View where Self: Codable { - - public func background(_ view: V) -> some View { - modified(using: Background(view: CoderView(view))) +extension View { + public func background(_ view: View) -> View { + modifier(Background(background: CoderView(view))) } - } diff --git a/Sources/Presenter/ViewModifiers/Blur.swift b/Sources/Presenter/ViewModifiers/Blur.swift index e951586..d463f54 100644 --- a/Sources/Presenter/ViewModifiers/Blur.swift +++ b/Sources/Presenter/ViewModifiers/Blur.swift @@ -1,31 +1,29 @@ - -internal struct Blur: AnyViewModifying { - +internal struct Blur: CodableViewModifier { // MARK: Stored Properties let radius: CGFloat - let opaque: Bool? - + let opaque: Bool? // swiftlint:disable:this discouraged_optional_boolean } // MARK: - CustomStringConvertible extension Blur: CustomStringConvertible { - var description: String { "blur(radius: \(radius), opaque: \(opaque ?? false))" } - } // MARK: - ViewModifier #if canImport(SwiftUI) -extension Blur: ViewModifier { - +extension Blur: SwiftUI.ViewModifier { func body(content: Content) -> some SwiftUI.View { - content.blur(radius: radius, opaque: opaque ?? false) + if let opaque = opaque { + content.blur(radius: radius, opaque: opaque) + } else { + content.blur(radius: radius) + } } } @@ -34,9 +32,8 @@ extension Blur: ViewModifier { // MARK: - View Extensions extension View { - - public func blur(radius: CGFloat, opaque: Bool? = nil) -> some View { - modified(using: Blur(radius: radius, opaque: opaque)) + public func blur(radius: CGFloat, + opaque: Bool? = nil) -> View { // swiftlint:disable:this discouraged_optional_boolean + modifier(Blur(radius: radius, opaque: opaque)) } - } diff --git a/Sources/Presenter/ViewModifiers/Clipped.swift b/Sources/Presenter/ViewModifiers/Clipped.swift index 12a7e42..0c11ef6 100644 --- a/Sources/Presenter/ViewModifiers/Clipped.swift +++ b/Sources/Presenter/ViewModifiers/Clipped.swift @@ -1,32 +1,25 @@ - -internal struct Clipped: AnyViewModifying { - +internal struct Clipped: CodableViewModifier { // MARK: Stored Properties - let antialiased: Bool? - + let antialiased: Bool? // swiftlint:disable:this discouraged_optional_boolean } // MARK: - CustomStringConvertible extension Clipped: CustomStringConvertible { - var description: String { "clipped(antialiased: \(antialiased ?? false))" } - } // MARK: - ViewModifier #if canImport(SwiftUI) -extension Clipped: ViewModifier { - +extension Clipped: SwiftUI.ViewModifier { func body(content: Content) -> some SwiftUI.View { content.clipped(antialiased: antialiased ?? false) } - } #endif @@ -34,9 +27,9 @@ extension Clipped: ViewModifier { // MARK: - View Extensions extension View { - - public func clipped(antialiased: Bool? = nil) -> some View { - modified(using: Clipped(antialiased: antialiased)) + public func clipped( + antialiased: Bool? = nil // swiftlint:disable:this discouraged_optional_boolean + ) -> View { + modifier(Clipped(antialiased: antialiased)) } - } diff --git a/Sources/Presenter/ViewModifiers/CornerRadius.swift b/Sources/Presenter/ViewModifiers/CornerRadius.swift index 7ccc8e8..f0f47dc 100644 --- a/Sources/Presenter/ViewModifiers/CornerRadius.swift +++ b/Sources/Presenter/ViewModifiers/CornerRadius.swift @@ -1,29 +1,23 @@ - -internal struct CornerRadius: AnyViewModifying { - +internal struct CornerRadius: CodableViewModifier { // MARK: Stored Properties let value: CGFloat - let antialiased: Bool? - + let antialiased: Bool? // swiftlint:disable:this discouraged_optional_boolean } // MARK: - CustomStringConvertible extension CornerRadius: CustomStringConvertible { - var description: String { "cornerRadius(\(value), antialiased: \(antialiased ?? true))" } - } // MARK: - ViewModifier #if canImport(SwiftUI) -extension CornerRadius: ViewModifier { - +extension CornerRadius: SwiftUI.ViewModifier { func body(content: Content) -> some SwiftUI.View { content.cornerRadius(value, antialiased: antialiased ?? true) } @@ -34,9 +28,10 @@ extension CornerRadius: ViewModifier { // MARK: - View Extensions extension View { - - public func cornerRadius(_ value: CGFloat, antialiased: Bool? = nil) -> some View { - modified(using: CornerRadius(value: value, antialiased: antialiased)) + public func cornerRadius( + _ value: CGFloat, + antialiased: Bool? = nil // swiftlint:disable:this discouraged_optional_boolean + ) -> View { + modifier(CornerRadius(value: value, antialiased: antialiased)) } - } diff --git a/Sources/Presenter/ViewModifiers/DrawingGroup.swift b/Sources/Presenter/ViewModifiers/DrawingGroup.swift index edc735a..92e52c4 100644 --- a/Sources/Presenter/ViewModifiers/DrawingGroup.swift +++ b/Sources/Presenter/ViewModifiers/DrawingGroup.swift @@ -1,44 +1,36 @@ - public enum ColorRenderingMode: String, Codable { case extendedLinear case linear case nonLinear } -internal struct DrawingGroup: AnyViewModifying { - +internal struct DrawingGroup: CodableViewModifier { // MARK: Stored Properties - let opaque: Bool? + let opaque: Bool? // swiftlint:disable:this discouraged_optional_boolean let colorMode: ColorRenderingMode? - } // MARK: - CustomStringConvertible extension DrawingGroup: CustomStringConvertible { - var description: String { "drawingGroup(opaque: \(opaque ?? true), colorMode: \(colorMode ?? .nonLinear))" } - } // MARK: - ViewModifier #if canImport(SwiftUI) -extension DrawingGroup: ViewModifier { - +extension DrawingGroup: SwiftUI.ViewModifier { func body(content: Content) -> some SwiftUI.View { content.drawingGroup(opaque: opaque ?? false, colorMode: colorMode?.swiftUIValue ?? .nonLinear) } - } extension ColorRenderingMode { - var swiftUIValue: SwiftUI.ColorRenderingMode { switch self { case .extendedLinear: @@ -49,7 +41,6 @@ extension ColorRenderingMode { return .nonLinear } } - } #endif @@ -57,9 +48,8 @@ extension ColorRenderingMode { // MARK: - View Extensions extension View { - - public func drawingGroup(opaque: Bool? = nil, colorMode: ColorRenderingMode? = nil) -> some View { - modified(using: DrawingGroup(opaque: opaque, colorMode: colorMode)) + public func drawingGroup(opaque: Bool? = nil, // swiftlint:disable:this discouraged_optional_boolean + colorMode: ColorRenderingMode? = nil) -> View { + modifier(DrawingGroup(opaque: opaque, colorMode: colorMode)) } - } diff --git a/Sources/Presenter/ViewModifiers/DynamicFrame.swift b/Sources/Presenter/ViewModifiers/DynamicFrame.swift index c6d915a..120d57a 100644 --- a/Sources/Presenter/ViewModifiers/DynamicFrame.swift +++ b/Sources/Presenter/ViewModifiers/DynamicFrame.swift @@ -1,6 +1,4 @@ - -internal struct DynamicFrame: AnyViewModifying { - +internal struct DynamicFrame: CodableViewModifier { // MARK: Stored Properties let minWidth: CGFloat? @@ -12,13 +10,11 @@ internal struct DynamicFrame: AnyViewModifying { let maxHeight: CGFloat? let alignment: Alignment? - } // MARK: - CustomStringConvertible extension DynamicFrame: CustomStringConvertible { - var description: String { let values: [(String, Any?)] = [ ("minWidth", minWidth), ("idealWidth", idealWidth), ("maxWidth", maxWidth), @@ -28,15 +24,13 @@ extension DynamicFrame: CustomStringConvertible { let nonOptionalValues = values.filter { $0.1 != nil } return "frame(\(nonOptionalValues.map { "\($0.0): \($0.1!)" }.joined(separator: ", ")))" } - } // MARK: - ViewModifier #if canImport(SwiftUI) -extension DynamicFrame: ViewModifier { - +extension DynamicFrame: SwiftUI.ViewModifier { func body(content: Content) -> some SwiftUI.View { content.frame(minWidth: minWidth, idealWidth: idealWidth, @@ -46,7 +40,6 @@ extension DynamicFrame: ViewModifier { maxHeight: maxHeight, alignment: alignment?.swiftUIValue ?? .center) } - } #endif @@ -54,21 +47,23 @@ extension DynamicFrame: ViewModifier { // MARK: - View Extensions extension View { - public func frame(minWidth: CGFloat? = nil, idealWidth: CGFloat? = nil, maxWidth: CGFloat? = nil, minHeight: CGFloat? = nil, idealHeight: CGFloat? = nil, maxHeight: CGFloat? = nil, - alignment: Alignment? = nil) -> some View { - modified(using: + alignment: Alignment = .center) -> View { + modifier( DynamicFrame( - minWidth: minWidth, idealWidth: idealWidth, maxWidth: maxWidth, - minHeight: minHeight, idealHeight: idealHeight, maxHeight: maxHeight, + minWidth: minWidth, + idealWidth: idealWidth, + maxWidth: maxWidth, + minHeight: minHeight, + idealHeight: idealHeight, + maxHeight: maxHeight, alignment: alignment ) ) } - } diff --git a/Sources/Presenter/ViewModifiers/FontModifier.swift b/Sources/Presenter/ViewModifiers/FontModifier.swift index fcd8994..e16c795 100644 --- a/Sources/Presenter/ViewModifiers/FontModifier.swift +++ b/Sources/Presenter/ViewModifiers/FontModifier.swift @@ -1,32 +1,25 @@ - -internal struct FontModifier: AnyViewModifying { - +internal struct FontModifier: CodableViewModifier { // MARK: Stored Properties let font: Font? - } // MARK: - CustomStringConvertible extension FontModifier: CustomStringConvertible { - var description: String { "font(\(font?.description ?? "nil"))" } - } // MARK: - ViewModifier #if canImport(SwiftUI) -extension FontModifier: ViewModifier { - +extension FontModifier: SwiftUI.ViewModifier { func body(content: Content) -> some SwiftUI.View { content.font(font?.swiftUIValue) } - } #endif @@ -34,9 +27,7 @@ extension FontModifier: ViewModifier { // MARK: - View Extensions extension View { - - public func font(_ font: Font?) -> some View { - modified(using: FontModifier(font: font)) + public func font(_ font: Font?) -> View { + modifier(FontModifier(font: font)) } - } diff --git a/Sources/Presenter/ViewModifiers/ForegroundColor.swift b/Sources/Presenter/ViewModifiers/ForegroundColor.swift index b6610e1..03df6e2 100644 --- a/Sources/Presenter/ViewModifiers/ForegroundColor.swift +++ b/Sources/Presenter/ViewModifiers/ForegroundColor.swift @@ -1,32 +1,25 @@ - -internal struct ForegroundColor: AnyViewModifying { - +internal struct ForegroundColor: CodableViewModifier { // MARK: Stored Properties let color: ColorCode - } // MARK: - CustomStringConvertible extension ForegroundColor: CustomStringConvertible { - var description: String { "foregroundColor(\(color))" } - } // MARK: - ViewModifier #if canImport(SwiftUI) -extension ForegroundColor: ViewModifier { - +extension ForegroundColor: SwiftUI.ViewModifier { func body(content: Content) -> some SwiftUI.View { - content.foregroundColor(color.color.view) + content.foregroundColor(color.color.body) } - } #endif @@ -34,9 +27,7 @@ extension ForegroundColor: ViewModifier { // MARK: - View Extensions extension View { - - public func foregroundColor(_ color: Color) -> some View { - modified(using: ForegroundColor(color: ColorCode(color))) + public func foregroundColor(_ color: Color) -> View { + modifier(ForegroundColor(color: ColorCode(color))) } - } diff --git a/Sources/Presenter/ViewModifiers/Frame.swift b/Sources/Presenter/ViewModifiers/Frame.swift index 87efc2e..c9a931a 100644 --- a/Sources/Presenter/ViewModifiers/Frame.swift +++ b/Sources/Presenter/ViewModifiers/Frame.swift @@ -1,37 +1,31 @@ - -internal struct Frame: AnyViewModifying { - +internal struct Frame: CodableViewModifier { // MARK: Stored Properties let height: CGFloat? let width: CGFloat? let alignment: Alignment? - } // MARK: - CustomStringConvertible extension Frame: CustomStringConvertible { - var description: String { let values: [(String, Any?)] = [("height", height), ("width", width), ("alignment", alignment)] let nonOptionalValues = values.filter { $0.1 != nil } return "frame(\(nonOptionalValues.map { "\($0.0): \($0.1!)" }.joined(separator: ", ")))" } - } // MARK: - ViewModifier #if canImport(SwiftUI) -extension Frame: ViewModifier { - +extension Frame: SwiftUI.ViewModifier { func body(content: Content) -> some SwiftUI.View { - content.frame(width: width, height: height, + content.frame(width: width, + height: height, alignment: alignment?.swiftUIValue ?? .center) } - } #endif @@ -39,9 +33,9 @@ extension Frame: ViewModifier { // MARK: - View Extensions extension View { - - public func frame(width: CGFloat? = nil, height: CGFloat? = nil, alignment: Alignment? = nil) -> some View { - modified(using: Frame(height: height, width: width, alignment: alignment)) + public func frame(width: CGFloat? = nil, + height: CGFloat? = nil, + alignment: Alignment? = nil) -> View { + modifier(Frame(height: height, width: width, alignment: alignment)) } - } diff --git a/Sources/Presenter/ViewModifiers/LayoutPriority.swift b/Sources/Presenter/ViewModifiers/LayoutPriority.swift index 7dd6d15..bc7771f 100644 --- a/Sources/Presenter/ViewModifiers/LayoutPriority.swift +++ b/Sources/Presenter/ViewModifiers/LayoutPriority.swift @@ -1,28 +1,22 @@ - -internal struct LayoutPriority: AnyViewModifying { - +internal struct LayoutPriority: CodableViewModifier { // MARK: Stored Properties let value: Double - } // MARK: - CustomStringConvertible extension LayoutPriority: CustomStringConvertible { - var description: String { "layoutPriority(\(value))" } - } // MARK: - ViewModifier #if canImport(SwiftUI) -extension LayoutPriority: ViewModifier { - +extension LayoutPriority: SwiftUI.ViewModifier { func body(content: Content) -> some SwiftUI.View { content.layoutPriority(value) } @@ -33,9 +27,7 @@ extension LayoutPriority: ViewModifier { // MARK: - View Extensions extension View { - - public func layoutPriority(_ value: Double) -> some View { - modified(using: LayoutPriority(value: value)) + public func layoutPriority(_ value: Double) -> View { + modifier(LayoutPriority(value: value)) } - } diff --git a/Sources/Presenter/ViewModifiers/LifecycleModifier.swift b/Sources/Presenter/ViewModifiers/LifecycleModifier.swift index 7e6b633..b277b8e 100644 --- a/Sources/Presenter/ViewModifiers/LifecycleModifier.swift +++ b/Sources/Presenter/ViewModifiers/LifecycleModifier.swift @@ -1,31 +1,25 @@ - -internal struct LifecycleModifier: AnyViewModifying { - +internal struct LifecycleModifier: CodableViewModifier { // MARK: Stored Properties let onAppear: CoderAction? let onDisappear: CoderAction? - } // MARK: - CustomStringConvertible extension LifecycleModifier: CustomStringConvertible { - var description: String { onAppear != nil ? "onAppear(...)" : "onDisappear(...)" } - } // MARK: - ViewModifier #if canImport(SwiftUI) -extension LifecycleModifier: ViewModifier { - +extension LifecycleModifier: SwiftUI.ViewModifier { func body(content: Content) -> some SwiftUI.View { ModelView { model in content @@ -33,7 +27,6 @@ extension LifecycleModifier: ViewModifier { .onDisappear(perform: model.action(for: self.onDisappear)) } } - } #endif @@ -41,13 +34,11 @@ extension LifecycleModifier: ViewModifier { // MARK: - View Extensions extension View { - - public func onAppear(perform action: Action) -> some View { - modified(using: LifecycleModifier(onAppear: CoderAction(action), onDisappear: nil)) + public func onAppear(perform action: Action) -> View { + modifier(LifecycleModifier(onAppear: CoderAction(action), onDisappear: nil)) } - public func onDisappear(perform action: Action) -> some View { - modified(using: LifecycleModifier(onAppear: nil, onDisappear: CoderAction(action))) + public func onDisappear(perform action: Action) -> View { + modifier(LifecycleModifier(onAppear: nil, onDisappear: CoderAction(action))) } - } diff --git a/Sources/Presenter/ViewModifiers/Mask.swift b/Sources/Presenter/ViewModifiers/Mask.swift index 32f6e9c..2a67ded 100644 --- a/Sources/Presenter/ViewModifiers/Mask.swift +++ b/Sources/Presenter/ViewModifiers/Mask.swift @@ -1,30 +1,32 @@ - -internal struct Mask: AnyViewModifying { - +internal struct Mask: CodableViewModifier { // MARK: Stored Properties let mask: CoderView - } // MARK: - CustomStringConvertible extension Mask: CustomStringConvertible { - var description: String { "mask(\(mask))" } - } // MARK: - ViewModifier #if canImport(SwiftUI) -extension Mask: ViewModifier { +extension Mask { + public func body(for content: Content) -> View { + mask.modifier(Modifier(foreground: content)) + } +} + +private struct Modifier: ViewModifier, SwiftUI.ViewModifier { + let foreground: Foreground func body(content: Content) -> some SwiftUI.View { - content.mask(mask.eraseToAnyView()) + foreground.mask(content) } } @@ -33,9 +35,7 @@ extension Mask: ViewModifier { // MARK: - View Extensions extension View { - - public func mask(_ mask: M) -> some View { - modified(using: Mask(mask: CoderView(mask))) + public func mask(_ mask: View) -> View { + modifier(Mask(mask: CoderView(mask))) } - } diff --git a/Sources/Presenter/ViewModifiers/NavigationBarTitle.swift b/Sources/Presenter/ViewModifiers/NavigationBarTitle.swift index c915637..f3afc0c 100644 --- a/Sources/Presenter/ViewModifiers/NavigationBarTitle.swift +++ b/Sources/Presenter/ViewModifiers/NavigationBarTitle.swift @@ -1,35 +1,29 @@ - public enum NavigationBarTitleDisplayMode: String, Codable { case inline case automatic case large } -internal struct NavigationBarTitle: AnyViewModifying { - +internal struct NavigationBarTitle: CodableViewModifier { // MARK: Stored Properties let title: Value let displayMode: NavigationBarTitleDisplayMode? - } // MARK: - CustomStringConvertible extension NavigationBarTitle: CustomStringConvertible { - var description: String { "navigationBarTitle(\(title), displayMode: \(displayMode ?? .automatic))" } - } // MARK: - ViewModifier #if canImport(SwiftUI) -extension NavigationBarTitle: ViewModifier { - +extension NavigationBarTitle: SwiftUI.ViewModifier { #if os(tvOS) || os(watchOS) || os(macOS) func body(content: Content) -> some SwiftUI.View { @@ -46,13 +40,11 @@ extension NavigationBarTitle: ViewModifier { } #endif - } #if canImport(UIKit) extension NavigationBarTitleDisplayMode { - fileprivate var swiftUIValue: NavigationBarItem.TitleDisplayMode { switch self { case .automatic: @@ -76,10 +68,8 @@ extension NavigationBarTitleDisplayMode { // MARK: - View Extensions extension View { - public func navigationBarTitle(_ title: Value, - displayMode: NavigationBarTitleDisplayMode? = nil) -> some View { - modified(using: NavigationBarTitle(title: title, displayMode: displayMode)) + displayMode: NavigationBarTitleDisplayMode? = nil) -> View { + modifier(NavigationBarTitle(title: title, displayMode: displayMode)) } - } diff --git a/Sources/Presenter/ViewModifiers/Opacity.swift b/Sources/Presenter/ViewModifiers/Opacity.swift index c4d3d27..17d0068 100644 --- a/Sources/Presenter/ViewModifiers/Opacity.swift +++ b/Sources/Presenter/ViewModifiers/Opacity.swift @@ -1,28 +1,22 @@ - -internal struct Opacity: AnyViewModifying { - +internal struct Opacity: CodableViewModifier { // MARK: Stored Properties let value: Double - } // MARK: - CustomStringConvertible extension Opacity: CustomStringConvertible { - var description: String { "opacity(\(value))" } - } // MARK: - ViewModifier #if canImport(SwiftUI) -extension Opacity: ViewModifier { - +extension Opacity: SwiftUI.ViewModifier { func body(content: Content) -> some SwiftUI.View { content.opacity(value) } @@ -33,9 +27,7 @@ extension Opacity: ViewModifier { // MARK: - View Extensions extension View { - - public func opacity(_ value: Double, antialiased: Bool? = nil) -> some View { - modified(using: Opacity(value: value)) + public func opacity(_ value: Double) -> View { + modifier(Opacity(value: value)) } - } diff --git a/Sources/Presenter/ViewModifiers/Overlay.swift b/Sources/Presenter/ViewModifiers/Overlay.swift index fb0378f..ac84b38 100644 --- a/Sources/Presenter/ViewModifiers/Overlay.swift +++ b/Sources/Presenter/ViewModifiers/Overlay.swift @@ -1,32 +1,37 @@ - -internal struct Overlay: AnyViewModifying { - +internal struct Overlay: CodableViewModifier { // MARK: Stored Properties let overlay: CoderView let alignment: Alignment? - } // MARK: - CustomStringConvertible extension Overlay: CustomStringConvertible { - var description: String { "overlay(\(overlay), alignment: \(alignment ?? .center))" } - } // MARK: - ViewModifier #if canImport(SwiftUI) -extension Overlay: ViewModifier { +extension Overlay { + public func body(for content: Content) -> View { + overlay.modifier( + Modifier(foreground: content, + alignment: alignment?.swiftUIValue ?? .center) + ) + } +} + +private struct Modifier: ViewModifier, SwiftUI.ViewModifier { + let foreground: Foreground + let alignment: SwiftUI.Alignment func body(content: Content) -> some SwiftUI.View { - content.overlay(overlay.eraseToAnyView(), - alignment: alignment?.swiftUIValue ?? .center) + foreground.overlay(content, alignment: alignment) } } @@ -35,9 +40,7 @@ extension Overlay: ViewModifier { // MARK: - View Extensions extension View { - - public func overlay(_ overlay: O, alignment: Alignment? = nil) -> some View { - modified(using: Overlay(overlay: CoderView(overlay), alignment: alignment)) + public func overlay(_ overlay: View, alignment: Alignment? = nil) -> View { + modifier(Overlay(overlay: CoderView(overlay), alignment: alignment)) } - } diff --git a/Sources/Presenter/ViewModifiers/Padding.swift b/Sources/Presenter/ViewModifiers/Padding.swift index b41a73f..5da53ae 100644 --- a/Sources/Presenter/ViewModifiers/Padding.swift +++ b/Sources/Presenter/ViewModifiers/Padding.swift @@ -1,38 +1,31 @@ - -internal struct Padding: AnyViewModifying { - +internal struct Padding: CodableViewModifier { // MARK: Stored Properties let top: CGFloat? let leading: CGFloat? let bottom: CGFloat? let trailing: CGFloat? - } // MARK: - CustomStringConvertible extension Padding: CustomStringConvertible { - var description: String { let values = [("top", top), ("leading", leading), ("bottom", bottom), ("trailing", trailing)] let nonOptionalValues = values.filter { $0.1 != nil } return "padding(\(nonOptionalValues.map { "\($0.0): \($0.1 ?? 0)" }.joined(separator: ", ")))" } - } // MARK: - ViewModifier #if canImport(SwiftUI) -extension Padding: ViewModifier { - +extension Padding: SwiftUI.ViewModifier { func body(content: Content) -> some SwiftUI.View { let insets = EdgeInsets(top: top ?? 0, leading: leading ?? 0, bottom: bottom ?? 0, trailing: trailing ?? 0) return content.padding(insets) } - } #endif @@ -40,14 +33,14 @@ extension Padding: ViewModifier { // MARK: - View Extensions extension View { - - public func padding(top: CGFloat? = nil, leading: CGFloat? = nil, - bottom: CGFloat? = nil, trailing: CGFloat? = nil) -> some View { - modified(using: Padding(top: top, leading: leading, bottom: bottom, trailing: trailing)) + public func padding(top: CGFloat? = nil, + leading: CGFloat? = nil, + bottom: CGFloat? = nil, + trailing: CGFloat? = nil) -> View { + modifier(Padding(top: top, leading: leading, bottom: bottom, trailing: trailing)) } - public func padding(_ value: CGFloat) -> some View { - modified(using: Padding(top: value, leading: value, bottom: value, trailing: value)) + public func padding(_ value: CGFloat) -> View { + modifier(Padding(top: value, leading: value, bottom: value, trailing: value)) } - } diff --git a/Sources/Presenter/ViewModifiers/Shadow.swift b/Sources/Presenter/ViewModifiers/Shadow.swift index 390f953..4cc9634 100644 --- a/Sources/Presenter/ViewModifiers/Shadow.swift +++ b/Sources/Presenter/ViewModifiers/Shadow.swift @@ -1,19 +1,15 @@ - -internal struct Shadow: AnyViewModifying { - +internal struct Shadow: CodableViewModifier { // MARK: Stored Properties let color: ColorCode? let radius: CGFloat - let x: CGFloat? - let y: CGFloat? - + let x: CGFloat? // swiftlint:disable:this identifier_name + let y: CGFloat? // swiftlint:disable:this identifier_name } // MARK: - CustomStringConvertible extension Shadow: CustomStringConvertible { - var description: String { if let color = color { return "shadow(color: \(color), radius: \(radius), x: \(x ?? 0), y: \(y ?? 0)" @@ -21,18 +17,16 @@ extension Shadow: CustomStringConvertible { return "shadow(radius: \(radius), x: \(x ?? 0), y: \(y ?? 0)" } } - } // MARK: - ViewModifier #if canImport(SwiftUI) -extension Shadow: ViewModifier { - +extension Shadow: SwiftUI.ViewModifier { func body(content: Content) -> some SwiftUI.View { if let color = color { - return content.shadow(color: color.color.view, radius: radius, x: x ?? 0, y: y ?? 0) + return content.shadow(color: color.color.body, radius: radius, x: x ?? 0, y: y ?? 0) } else { return content.shadow(radius: radius, x: x ?? 0, y: y ?? 0) } @@ -44,9 +38,12 @@ extension Shadow: ViewModifier { // MARK: - View Extensions extension View { - - public func shadow(color: Color? = nil, radius: CGFloat, x: CGFloat? = nil, y: CGFloat? = nil) -> some View { - modified(using: Shadow(color: color.map(ColorCode.init), radius: radius, x: x, y: y)) + public func shadow( + color: Color? = nil, + radius: CGFloat, + x xValue: CGFloat? = nil, + y yValue: CGFloat? = nil + ) -> View { + modifier(Shadow(color: color.map(ColorCode.init), radius: radius, x: xValue, y: yValue)) } - } diff --git a/Sources/Presenter/ViewModifiers/Sheet.swift b/Sources/Presenter/ViewModifiers/Sheet.swift index a899c50..4b18e08 100644 --- a/Sources/Presenter/ViewModifiers/Sheet.swift +++ b/Sources/Presenter/ViewModifiers/Sheet.swift @@ -1,39 +1,41 @@ - -internal struct Sheet: AnyViewModifying { - +internal struct Sheet: CodableViewModifier { // MARK: Stored Properties let isPresented: Binding let content: CoderView - } // MARK: - CustomStringConvertible extension Sheet: CustomStringConvertible { - var description: String { "sheet(isPresented: \(isPresented), content: \(content))" } - } // MARK: - ViewModifier #if canImport(SwiftUI) -extension Sheet: ViewModifier { +extension Sheet { + func body(for caller: Caller) -> View { + content.modifier(Modifier(caller: caller, isPresented: isPresented)) + } +} + +private struct Modifier: ViewModifier, SwiftUI.ViewModifier { + let caller: Caller + let isPresented: Binding func body(content: Content) -> some SwiftUI.View { ModelView { model in - content - .sheet(isPresented: model.binding(for: self.isPresented)) { - self.content.eraseToAnyView() - .environmentObject(model) - } + caller + .sheet(isPresented: model.binding(for: self.isPresented)) { + content + .environmentObject(model) + } } } - } #endif @@ -41,9 +43,7 @@ extension Sheet: ViewModifier { // MARK: - View Extensions extension View { - - public func sheet(isPresented: Binding, content: Content) -> some View { - modified(using: Sheet(isPresented: isPresented, content: CoderView(content))) + public func sheet(isPresented: Binding, content: View) -> View { + modifier(Sheet(isPresented: isPresented, content: CoderView(content))) } - } diff --git a/Sources/Presenter/Views/AngularGradient.swift b/Sources/Presenter/Views/AngularGradient.swift index ec49a66..2458d4a 100644 --- a/Sources/Presenter/Views/AngularGradient.swift +++ b/Sources/Presenter/Views/AngularGradient.swift @@ -1,6 +1,4 @@ - -public struct AngularGradient: SwiftUIView { - +public struct AngularGradient: CodableView { // MARK: Stored Properties private let gradient: Gradient @@ -14,7 +12,6 @@ public struct AngularGradient: SwiftUIView { center: UnitPoint, startAngle: Angle = .zero, endAngle: Angle = .zero) { - self.gradient = gradient self.center = center self.startAngle = startAngle @@ -24,32 +21,27 @@ public struct AngularGradient: SwiftUIView { public init(gradient: Gradient, center: UnitPoint, angle: Angle) { - self.gradient = gradient self.center = center self.startAngle = angle self.endAngle = angle } - } // MARK: - CustomStringConvertible extension AngularGradient: CustomStringConvertible { - public var description: String { "AngularGradient(gradient: \(gradient), center: \(center), startAngle: \(startAngle), endAngle: \(endAngle))" } - } // MARK: - View #if canImport(SwiftUI) -extension AngularGradient { - - public var view: some SwiftUI.View { +extension AngularGradient: SwiftUI.View { + public var body: some SwiftUI.View { SwiftUI.AngularGradient( gradient: gradient.swiftUIValue, center: center.unitPoint, @@ -57,8 +49,6 @@ extension AngularGradient { endAngle: endAngle.swiftUIValue ) } - } #endif - diff --git a/Sources/Presenter/Views/ArrayView.swift b/Sources/Presenter/Views/ArrayView.swift index 7b7b031..9b2cf99 100644 --- a/Sources/Presenter/Views/ArrayView.swift +++ b/Sources/Presenter/Views/ArrayView.swift @@ -1,13 +1,11 @@ - -struct ArrayView: SwiftUIView { - +struct ArrayView: CodableView { // MARK: Stored Properties let content: [CoderView] // MARK: Initialization - public init(content: [_CodableView]) { + init(content: [View]) { self.content = content.flatMap { contentView -> [CoderView] in if let arrayView = contentView as? ArrayView { return arrayView.content @@ -16,31 +14,26 @@ struct ArrayView: SwiftUIView { } } } - } // MARK: - CustomStringConvertible extension ArrayView: CustomStringConvertible { - public var description: String { content.description } - } // MARK: - View #if canImport(SwiftUI) -extension ArrayView { - - public var view: some SwiftUI.View { +extension ArrayView: SwiftUI.View { + public var body: some SwiftUI.View { ForEach(content.indices) { index in self.content[index].eraseToAnyView() } } - } #endif diff --git a/Sources/Presenter/Views/Button.swift b/Sources/Presenter/Views/Button.swift index d385fd6..c20c998 100644 --- a/Sources/Presenter/Views/Button.swift +++ b/Sources/Presenter/Views/Button.swift @@ -1,6 +1,4 @@ - -public struct Button: SwiftUIView { - +public struct Button: CodableWrapperView { // MARK: Stored Properties let label: CoderView @@ -8,21 +6,18 @@ public struct Button: SwiftUIView { // MARK: Initialization - public init(_ label: Label, action: Action) { + public init(_ label: View, action: Action) { self.label = CoderView(label) self.action = CoderAction(action) } - } // MARK: - CustomStringConvertible extension Button: CustomStringConvertible { - public var description: String { "Button(\(label))" } - } // MARK: - View @@ -30,15 +25,12 @@ extension Button: CustomStringConvertible { #if canImport(SwiftUI) extension Button { - - public var view: some SwiftUI.View { - label.apply(Modifier1(action: action)).eraseToAnyView() + public var body: View { + label.modifier(Modifier(action: action)) } - } -private struct Modifier1: ViewModifier { - +private struct Modifier: ViewModifier, SwiftUI.ViewModifier { let action: CoderAction func body(content: Content) -> some SwiftUI.View { @@ -49,7 +41,6 @@ private struct Modifier1: ViewModifier { ) } } - } #endif diff --git a/Sources/Presenter/Views/Capsule.swift b/Sources/Presenter/Views/Capsule.swift index 5e2fd62..441dcca 100644 --- a/Sources/Presenter/Views/Capsule.swift +++ b/Sources/Presenter/Views/Capsule.swift @@ -1,6 +1,4 @@ - -public struct Capsule: SwiftUIView { - +public struct Capsule: CodableView { // MARK: Stored Properties private let style: RoundedCornerStyle @@ -10,29 +8,24 @@ public struct Capsule: SwiftUIView { public init(style: RoundedCornerStyle) { self.style = style } - } // MARK: - CustomStringConvertible extension Capsule: CustomStringConvertible { - public var description: String { "Capsule(style: \(style))" } - } // MARK: - View #if canImport(SwiftUI) -extension Capsule { - - public var view: some SwiftUI.View { +extension Capsule: SwiftUI.View { + public var body: some SwiftUI.View { SwiftUI.Capsule(style: style.swiftUIValue) } - } #endif diff --git a/Sources/Presenter/Views/Circle.swift b/Sources/Presenter/Views/Circle.swift index cefcc36..add26a9 100644 --- a/Sources/Presenter/Views/Circle.swift +++ b/Sources/Presenter/Views/Circle.swift @@ -1,32 +1,25 @@ - -public struct Circle: SwiftUIView { - +public struct Circle: CodableView { // MARK: Initialization public init() {} - } // MARK: - CustomStringConvertible extension Circle: CustomStringConvertible { - public var description: String { "Circle()" } - } // MARK: - View #if canImport(SwiftUI) -extension Circle { - - public var view: some SwiftUI.View { +extension Circle: SwiftUI.View { + public var body: some SwiftUI.View { SwiftUI.Circle() } - } #endif diff --git a/Sources/Presenter/Views/Color.swift b/Sources/Presenter/Views/Color.swift index 663381a..145380a 100644 --- a/Sources/Presenter/Views/Color.swift +++ b/Sources/Presenter/Views/Color.swift @@ -1,6 +1,4 @@ - -public struct Color: SwiftUIView { - +public struct Color: CodableView { // MARK: Nested Types private enum CodingKeys: String, CodingKey { @@ -41,29 +39,24 @@ public struct Color: SwiftUIView { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(ColorCode(self).description, forKey: .code) } - } // MARK: - CustomStringConvertible extension Color: CustomStringConvertible { - public var description: String { - "Color(\(ColorCode(self))" + "Color(\(ColorCode(self)))" } - } // MARK: - View #if canImport(SwiftUI) -extension Color { - - public var view: SwiftUI.Color { +extension Color: SwiftUI.View { + public var body: SwiftUI.Color { SwiftUI.Color(red: red, green: green, blue: blue, opacity: opacity) } - } #endif diff --git a/Sources/Presenter/Views/ColorPicker.swift b/Sources/Presenter/Views/ColorPicker.swift index 0be84dc..cea2c01 100644 --- a/Sources/Presenter/Views/ColorPicker.swift +++ b/Sources/Presenter/Views/ColorPicker.swift @@ -1,34 +1,29 @@ - -public struct ColorPicker: SwiftUIView { - +public struct ColorPicker: CodableWrapperView { // MARK: Stored Properties - private let color: Binding + private let color: Binding private let supportsOpacity: Bool private let label: CoderView // MARK: Initialization - public init( + public init( color: Binding, supportsOpacity: Bool, - @ViewBuilder label: () -> Label + @ViewBuilder label: () -> View ) { self.color = color self.supportsOpacity = supportsOpacity self.label = CoderView(label()) } - } // MARK: - CustomStringConvertible extension ColorPicker: CustomStringConvertible { - public var description: String { "ColorPicker(color: \(color), supportsOpacity: \(supportsOpacity), label: \(label))" } - } // MARK: - View @@ -36,32 +31,36 @@ extension ColorPicker: CustomStringConvertible { #if canImport(SwiftUI) extension ColorPicker { - #if !os(macOS) && !os(tvOS) && !os(watchOS) && !targetEnvironment(macCatalyst) - @SwiftUI.ViewBuilder - public var view: some SwiftUI.View { - if #available(iOS 14.0, *) { - ModelView { model in - SwiftUI.ColorPicker( - selection: model.binding(for: color) { $0.view }, - supportsOpacity: supportsOpacity) { - label.eraseToAnyView() - } - } - } else { - SwiftUI.EmptyView() - } + public var body: View { + label.modifier(Modifier(color: color, supportsOpacity: supportsOpacity)) } #else - public var view: some SwiftUI.View { - SwiftUI.EmptyView() + public var body: View { + Nil() } #endif +} + +private struct Modifier: ViewModifier, SwiftUI.ViewModifier { + let color: Binding + let supportsOpacity: Bool + func body(content: Content) -> some SwiftUI.View { + if #available(iOS 14.0, macOS 11.0, *) { + ModelView { model in + SwiftUI.ColorPicker( + selection: model.binding(for: color) { $0.body }, + supportsOpacity: supportsOpacity) { + content + } + } + } + } } #endif diff --git a/Sources/Presenter/Views/ComposedView.swift b/Sources/Presenter/Views/ComposedView.swift index 3bf3a97..2583803 100644 --- a/Sources/Presenter/Views/ComposedView.swift +++ b/Sources/Presenter/Views/ComposedView.swift @@ -1,21 +1,16 @@ - -struct ComposedView: InternalView, Codable { - +struct ComposedView: CodableWrapperView { // MARK: Stored Properties let content: CoderView let modifiers: [CoderViewModifier] - } // MARK: - CustomStringConvertible extension ComposedView: CustomStringConvertible { - public var description: String { - "\(content)" + modifiers.reduce("") { $0 + ".\($1)" } + "\(content)" + modifiers.reduce(into: "") { $0 += ".\($1)" } } - } // MARK: - View @@ -23,11 +18,9 @@ extension ComposedView: CustomStringConvertible { #if canImport(SwiftUI) extension ComposedView { - - var view: _View { - modifiers.reduce(content as _View) { $0.apply($1) } + public var body: View { + modifiers.reduce(content as View) { $0.apply($1) } } - } #endif @@ -35,8 +28,7 @@ extension ComposedView { // MARK: - View Extensions extension View { - - public func modified(using modifier: AnyViewModifying) -> some View { + public func modifier(_ modifier: Modifier) -> View { if let composition = self as? ComposedView { return ComposedView(content: composition.content, modifiers: composition.modifiers + [CoderViewModifier(modifier)]) @@ -44,5 +36,4 @@ extension View { return ComposedView(content: CoderView(self), modifiers: [CoderViewModifier(modifier)]) } - } diff --git a/Sources/Presenter/Views/DataView.swift b/Sources/Presenter/Views/DataView.swift index fe752d9..e732b69 100644 --- a/Sources/Presenter/Views/DataView.swift +++ b/Sources/Presenter/Views/DataView.swift @@ -1,6 +1,4 @@ - -public struct DataView: SwiftUIView { - +public struct DataView: CodableView { // MARK: Stored Properties let data: Data @@ -10,17 +8,14 @@ public struct DataView: SwiftUIView { public init(_ data: Data) { self.data = data } - } // MARK: - CustomStringConvertible extension DataView: CustomStringConvertible { - public var description: String { "DataView(\(String(data: data, encoding: .utf8) ?? "nil"))" } - } // MARK: - View @@ -28,7 +23,6 @@ extension DataView: CustomStringConvertible { #if canImport(SwiftUI) private struct _DataView: SwiftUI.View { - let data: Data @SwiftUI.State private var view: AnyView? @@ -43,19 +37,23 @@ private struct _DataView: SwiftUI.View { } private func decode() -> some SwiftUI.View { - didTryDecoding = true - view = try? Presenter.decode(from: data).eraseToAnyView() + DispatchQueue.main.async { + self.didTryDecoding = true + do { + let decodedView = try Presenter.decode(from: data) + self.view = decodedView.eraseToAnyView() + } catch { + self.view = AnyView(SwiftUI.Text(error.localizedDescription)) + } + } return view } - } -extension DataView { - - public var view: some SwiftUI.View { +extension DataView: SwiftUI.View { + public var body: some SwiftUI.View { _DataView(data: data) } - } #endif diff --git a/Sources/Presenter/Views/Divider.swift b/Sources/Presenter/Views/Divider.swift index f3f7dd5..84aaa20 100644 --- a/Sources/Presenter/Views/Divider.swift +++ b/Sources/Presenter/Views/Divider.swift @@ -1,32 +1,25 @@ - -public struct Divider: SwiftUIView { - +public struct Divider: CodableView { // MARK: Initialization public init() {} - } // MARK: - CustomStringConvertible extension Divider: CustomStringConvertible { - public var description: String { "Divider()" } - } // MARK: - View #if canImport(SwiftUI) -extension Divider { - - public var view: some SwiftUI.View { +extension Divider: SwiftUI.View { + public var body: some SwiftUI.View { SwiftUI.Divider() } - } #endif diff --git a/Sources/Presenter/Views/HGrid.swift b/Sources/Presenter/Views/HGrid.swift index a503355..7bf2ea3 100644 --- a/Sources/Presenter/Views/HGrid.swift +++ b/Sources/Presenter/Views/HGrid.swift @@ -1,6 +1,4 @@ - -public struct HGrid: SwiftUIView { - +public struct HGrid: CodableWrapperView { // MARK: Stored Properties private let rows: [GridItem] @@ -11,12 +9,12 @@ public struct HGrid: SwiftUIView { // MARK: Initialization - public init( + public init( rows: [GridItem], alignment: VerticalAlignment? = nil, spacing: CGFloat? = nil, pinnedViews: PinnedScrollableViews, - @ViewBuilder content: () -> Content + @ViewBuilder content: () -> View ) { self.rows = rows self.alignment = alignment @@ -24,17 +22,14 @@ public struct HGrid: SwiftUIView { self.pinnedViews = pinnedViews self.content = CoderView(content()) } - } // MARK: - CustomStringConvertible extension HGrid: CustomStringConvertible { - public var description: String { "HGrid(rows: \(rows), alignment: \(alignment.map { "\($0)" } ?? "nil"), spacing: \(spacing.map(\.description) ?? "nil"), pinnedViews: \(pinnedViews), content: \(content))" } - } // MARK: - View @@ -42,38 +37,34 @@ extension HGrid: CustomStringConvertible { #if canImport(SwiftUI) extension HGrid { - #if !os(macOS) && !targetEnvironment(macCatalyst) - @SwiftUI.ViewBuilder - public var view: some SwiftUI.View { + public var body: View { if #available(iOS 14.0, tvOS 14.0, watchOS 7.0, *) { - content.apply( + return content.modifier( Modifier( rows: rows.map(\.swiftUIValue), alignment: alignment?.swiftUIValue ?? .center, spacing: spacing, pinnedViews: pinnedViews.swiftUIValue ) - ).eraseToAnyView() + ) } else { - SwiftUI.EmptyView() + return Nil() } } #else - public var view: some SwiftUI.View { - SwiftUI.EmptyView() + public var body: View { + Nil() } #endif - } @available(iOS 14.0, macOS 11.0, *) -private struct Modifier: ViewModifier { - +private struct Modifier: ViewModifier, SwiftUI.ViewModifier { let rows: [SwiftUI.GridItem] let alignment: SwiftUI.VerticalAlignment let spacing: CGFloat? @@ -88,8 +79,6 @@ private struct Modifier: ViewModifier { content: { content } ) } - } #endif - diff --git a/Sources/Presenter/Views/HStack.swift b/Sources/Presenter/Views/HStack.swift index 5701c12..cc7c6fd 100644 --- a/Sources/Presenter/Views/HStack.swift +++ b/Sources/Presenter/Views/HStack.swift @@ -1,6 +1,4 @@ - -public struct HStack: InternalView, Codable { - +public struct HStack: CodableWrapperView { // MARK: Stored Properties private let alignment: VerticalAlignment? @@ -9,26 +7,23 @@ public struct HStack: InternalView, Codable { // MARK: Initialization - public init( + public init( alignment: VerticalAlignment? = nil, spacing: CGFloat? = nil, - @ViewBuilder content: () -> Content + @ViewBuilder content: () -> View ) { self.alignment = alignment self.spacing = spacing self.content = CoderView(content()) } - } // MARK: - CustomStringConvertible extension HStack: CustomStringConvertible { - public var description: String { "HStack(alignment: .\((alignment ?? .center).rawValue), spacing: \(spacing.map { $0.description } ?? "nil"), content: \(content))" } - } // MARK: - View @@ -36,16 +31,13 @@ extension HStack: CustomStringConvertible { #if canImport(SwiftUI) extension HStack { - - public var view: _View { + public var body: View { content - .apply(Modifier(alignment: alignment?.swiftUIValue ?? .center, spacing: spacing)) + .modifier(Modifier(alignment: alignment?.swiftUIValue ?? .center, spacing: spacing)) } - } -private struct Modifier: ViewModifier { - +private struct Modifier: ViewModifier, SwiftUI.ViewModifier { let alignment: SwiftUI.VerticalAlignment let spacing: CGFloat? @@ -54,8 +46,6 @@ private struct Modifier: ViewModifier { content } } - } #endif - diff --git a/Sources/Presenter/Views/If.swift b/Sources/Presenter/Views/If.swift index ae7e658..990bd0c 100644 --- a/Sources/Presenter/Views/If.swift +++ b/Sources/Presenter/Views/If.swift @@ -1,6 +1,4 @@ - -public struct If: SwiftUIView { - +public struct If: CodableWrapperView { // swiftlint:disable:this type_name // MARK: Nested Types private enum CodingKeys: String, CodingKey { @@ -17,28 +15,28 @@ public struct If: SwiftUIView { // MARK: Initialization - public init( + public init( _ condition: Value, - then trueView: TrueView, - else falseView: FalseView + then trueView: View, + else falseView: View ) { self.condition = condition self.trueView = CoderView(trueView) self.falseView = CoderView(falseView) } - public init( + public init( _ condition: Value, - then trueView: TrueView + then trueView: View ) { self.condition = condition self.trueView = CoderView(trueView) self.falseView = CoderView(Nil()) } - public init( + public init( _ condition: Value, - else falseView: FalseView + else falseView: View ) { self.condition = condition self.trueView = CoderView(Nil()) @@ -61,17 +59,14 @@ public struct If: SwiftUIView { try container.encode(falseView, forKey: .falseView) } } - } // MARK: - CustomStringConvertible extension If: CustomStringConvertible { - public var description: String { "If(\(condition), then: \(trueView), else: \(falseView))" } - } // MARK: - View @@ -79,17 +74,33 @@ extension If: CustomStringConvertible { #if canImport(SwiftUI) extension If { + public var body: View { + trueView.modifier(Modifier1(falseView: falseView, condition: condition)) + } +} - public var view: some SwiftUI.View { - ModelView { model in +private struct Modifier1: ViewModifier { + let falseView: CoderView + let condition: Value + + func body(for content: Content) -> View { + falseView.modifier(Modifier2(trueView: content, condition: condition)) + } +} + +private struct Modifier2: ViewModifier, SwiftUI.ViewModifier { + let trueView: TrueView + let condition: Value + + func body(content: Content) -> some SwiftUI.View { + ModelView { model in if self.condition.get(from: model) { - return self.trueView.eraseToAnyView() + trueView } else { - return self.falseView.eraseToAnyView() + content } } } - } #endif diff --git a/Sources/Presenter/Views/Image.swift b/Sources/Presenter/Views/Image.swift index 2844143..819d928 100644 --- a/Sources/Presenter/Views/Image.swift +++ b/Sources/Presenter/Views/Image.swift @@ -1,6 +1,4 @@ - -public struct Image: SwiftUIView { - +public struct Image: CodableView { // MARK: Nested Types fileprivate enum Kind: String, Codable { @@ -46,18 +44,15 @@ public struct Image: SwiftUIView { public func resizable() -> Image { Image(kind: kind, identifier: identifier, isResizable: true) } - } // MARK: - CustomStringConvertible extension Image: CustomStringConvertible { - public var description: String { "Image(\(kind): \(identifier))" + (isResizable ? ".resizable()" : "") } - } // MARK: - View @@ -65,7 +60,6 @@ extension Image: CustomStringConvertible { #if canImport(SwiftUI) && canImport(Combine) private struct ImageView: SwiftUI.View { - // MARK: Stored Properties var image: Image @@ -86,7 +80,10 @@ private struct ImageView: SwiftUI.View { // MARK: Helpers private func load() { - guard !didStartLoading else { return } + guard !didStartLoading else { + return + } + didStartLoading = true switch image.kind { @@ -109,13 +106,11 @@ private struct ImageView: SwiftUI.View { private func set(_ img: SwiftUI.Image) { loadedImage = image.isResizable ? img.resizable() : img } - } #if canImport(UIKit) extension ImageView { - private func loadExternalImage(at url: URL) -> AnyPublisher { URLSession.shared.dataTaskPublisher(for: url) .map { response -> SwiftUI.Image? in @@ -127,13 +122,11 @@ extension ImageView { .replaceError(with: nil) .eraseToAnyPublisher() } - } #elseif canImport(AppKit) extension ImageView { - private func loadExternalImage(at url: URL) -> AnyPublisher { URLSession.shared.dataTaskPublisher(for: url) .map { response -> SwiftUI.Image? in @@ -145,17 +138,14 @@ extension ImageView { .replaceError(with: nil) .eraseToAnyPublisher() } - } #endif -extension Image { - - public var view: some SwiftUI.View { +extension Image: SwiftUI.View { + public var body: some SwiftUI.View { ImageView(image: self) } - } #endif diff --git a/Sources/Presenter/Views/LinearGradient.swift b/Sources/Presenter/Views/LinearGradient.swift index d98047e..975fbcf 100644 --- a/Sources/Presenter/Views/LinearGradient.swift +++ b/Sources/Presenter/Views/LinearGradient.swift @@ -1,6 +1,4 @@ - -public struct LinearGradient: SwiftUIView { - +public struct LinearGradient: CodableView { // MARK: Stored Properties private let gradient: Gradient @@ -14,34 +12,28 @@ public struct LinearGradient: SwiftUIView { self.startPoint = startPoint self.endPoint = endPoint } - } // MARK: - CustomStringConvertible extension LinearGradient: CustomStringConvertible { - public var description: String { "LinearGradient(gradient: \(gradient), startPoint: \(startPoint), endPoint: \(endPoint))" } - } // MARK: - View #if canImport(SwiftUI) -extension LinearGradient { - - public var view: some SwiftUI.View { +extension LinearGradient: SwiftUI.View { + public var body: some SwiftUI.View { SwiftUI.LinearGradient( gradient: gradient.swiftUIValue, startPoint: startPoint.unitPoint, endPoint: endPoint.unitPoint ) } - } #endif - diff --git a/Sources/Presenter/Views/Link.swift b/Sources/Presenter/Views/Link.swift index 5ec50d4..d46cd12 100644 --- a/Sources/Presenter/Views/Link.swift +++ b/Sources/Presenter/Views/Link.swift @@ -1,6 +1,4 @@ - -public struct Link: SwiftUIView { - +public struct Link: CodableWrapperView { // MARK: Stored Properties private let label: CoderView @@ -8,21 +6,18 @@ public struct Link: SwiftUIView { // MARK: Initialization - public init(label: Label, destination: String) { + public init(label: View, destination: String) { self.label = CoderView(label) self.destination = destination } - } // MARK: - CustomStringConvertible extension Link: CustomStringConvertible { - public var description: String { "Link(\(label), destination: \(destination))" } - } // MARK: - View @@ -30,26 +25,21 @@ extension Link: CustomStringConvertible { #if canImport(SwiftUI) extension Link { + public var body: View { + label.modifier(Modifier(destination: destination)) + } +} - #if !os(macOS) && !targetEnvironment(macCatalyst) +private struct Modifier: ViewModifier, SwiftUI.ViewModifier { + let destination: String - @SwiftUI.ViewBuilder - public var view: some SwiftUI.View { - if #available(iOS 14.0, tvOS 14.0, watchOS 7.0, *), let url = URL(string: destination) { - SwiftUI.Link(destination: url, label: label.eraseToAnyView) + func body(content: Content) -> some SwiftUI.View { + if #available(iOS 14.0, macOS 11.0, *), let url = URL(string: destination) { + SwiftUI.Link(destination: url) { content } } else { - label.eraseToAnyView() + content } } - - #else - - public var view: some SwiftUI.View { - label.eraseToAnyView() - } - - #endif - } #endif diff --git a/Sources/Presenter/Views/Local.swift b/Sources/Presenter/Views/Local.swift index 3c72cba..b4d868d 100644 --- a/Sources/Presenter/Views/Local.swift +++ b/Sources/Presenter/Views/Local.swift @@ -1,6 +1,4 @@ - -public struct Local: SwiftUIView { - +public struct Local: CodableView { // MARK: Stored Properties private let key: String @@ -10,40 +8,35 @@ public struct Local: SwiftUIView { public init(key: String) { self.key = key } - } // MARK: - CustomStringConvertible extension Local: CustomStringConvertible { - public var description: String { "Local(key: \(key))" } - } // MARK: - View #if canImport(SwiftUI) -extension Local { - - public var view: some SwiftUI.View { +extension Local: SwiftUI.View { + public var body: some SwiftUI.View { ModelView { model in - view(for: model.state[self.key]) + view(for: model.get(key)) } } @SwiftUI.ViewBuilder private func view(for value: Any?) -> some SwiftUI.View { - if let view = value as? _View { + if let view = value as? View { view.eraseToAnyView() } else if let view = value as? AnyView { view } } - } #endif diff --git a/Sources/Presenter/Views/NavigationLink.swift b/Sources/Presenter/Views/NavigationLink.swift index 53b838e..947db52 100644 --- a/Sources/Presenter/Views/NavigationLink.swift +++ b/Sources/Presenter/Views/NavigationLink.swift @@ -1,6 +1,4 @@ - -public struct NavigationLink: InternalView, Codable { - +public struct NavigationLink: CodableWrapperView { // MARK: Stored Properties private let destination: CoderView @@ -9,26 +7,23 @@ public struct NavigationLink: InternalView, Codable { // MARK: Initialization - public init( - destination: Destination, + public init( + destination: View, isActive: Binding, - label: Label + label: View ) { self.destination = CoderView(destination) self.isActive = isActive self.label = CoderView(label) } - } // MARK: - CustomStringConvertible extension NavigationLink: CustomStringConvertible { - public var description: String { "NavigationLink(destination: \(destination), isActive: \(isActive), label: \(label))" } - } // MARK: - View @@ -36,28 +31,21 @@ extension NavigationLink: CustomStringConvertible { #if canImport(SwiftUI) extension NavigationLink { - - public var view: _View { - destination.apply(Modifier1(label: label, isActive: isActive)) + public var body: View { + destination.modifier(Modifier1(label: label, isActive: isActive)) } - } private struct Modifier1: ViewModifier { - let label: CoderView let isActive: Binding - func body(content: Content) -> some SwiftUI.View { - label - .apply(Modifier2(destination: content, isActive: isActive)) - .eraseToAnyView() + func body(for content: Content) -> View { + label.modifier(Modifier2(destination: content, isActive: isActive)) } - } -private struct Modifier2: ViewModifier { - +private struct Modifier2: ViewModifier, SwiftUI.ViewModifier { var destination: Destination let isActive: Binding @@ -68,14 +56,20 @@ private struct Modifier2: ViewModifier { func body(content: Content) -> some SwiftUI.View { ModelView { model in - SwiftUI.NavigationLink( - destination: destination, - isActive: model.binding(for: self.isActive), - label: { content } - ) + SwiftUI.ZStack { + SwiftUI.NavigationLink( + destination: destination, + isActive: model.binding(for: self.isActive), + label: { content } + ) + + SwiftUI.NavigationLink( + destination: EmptyView(), + label: { EmptyView() } + ) + } } } - } #endif diff --git a/Sources/Presenter/Views/NavigationView.swift b/Sources/Presenter/Views/NavigationView.swift index 537cc46..d0d3017 100644 --- a/Sources/Presenter/Views/NavigationView.swift +++ b/Sources/Presenter/Views/NavigationView.swift @@ -1,28 +1,23 @@ - -public struct NavigationView: InternalView, Codable { - +public struct NavigationView: CodableWrapperView { // MARK: Stored Properties private let content: CoderView // MARK: Initialization - public init( - @ViewBuilder content: () -> Content + public init( + @ViewBuilder content: () -> View ) { self.content = CoderView(content()) } - } // MARK: - CustomStringConvertible extension NavigationView: CustomStringConvertible { - public var description: String { "NavigationView(content: \(content))" } - } // MARK: - View @@ -30,21 +25,17 @@ extension NavigationView: CustomStringConvertible { #if canImport(SwiftUI) extension NavigationView { - - public var view: _View { - content.apply(Modifier()) + public var body: View { + content.modifier(Modifier()) } - } -private struct Modifier: ViewModifier, AnyViewModifying { - +private struct Modifier: ViewModifier, SwiftUI.ViewModifier { func body(content: Content) -> some SwiftUI.View { SwiftUI.NavigationView { content } } - } #endif diff --git a/Sources/Presenter/Views/Nil.swift b/Sources/Presenter/Views/Nil.swift index f5ef186..d79f4f2 100644 --- a/Sources/Presenter/Views/Nil.swift +++ b/Sources/Presenter/Views/Nil.swift @@ -1,18 +1,19 @@ - -extension Optional: InternalView, View, _View, NamedType where Wrapped: _CodableView { - +extension Optional: View, NamedType, WrapperView where Wrapped: CodableView { #if canImport(SwiftUI) - public var view: _View { - self ?? Nil() + public var body: View { + switch self { + case let .some(view): + return view + case .none: + return Nil() + } } #endif - } -struct Nil: InternalView, Codable { - +struct Nil: CodableView { // MARK: Initialization init() {} @@ -35,37 +36,14 @@ struct Nil: InternalView, Codable { var container = encoder.singleValueContainer() try container.encodeNil() } - - #if canImport(SwiftUI) - - public var view: _View { - EmptyView() - } - - #endif - } #if canImport(SwiftUI) -extension SwiftUI.EmptyView: _View { - - public var erasedCodableBody: _CodableView? { - nil - } - - public func eraseToAnyView() -> AnyView { - AnyView(self) - } - - public func apply(_ m: M) -> _View { - modifier(m) - } - - public func apply(_ modifier: AnyViewModifying) -> _View { - modifier.apply(to: self) +extension Nil: SwiftUI.View { + public var body: some SwiftUI.View { + EmptyView() } - } #endif @@ -73,9 +51,7 @@ extension SwiftUI.EmptyView: _View { // MARK: - CustomStringConvertible extension Nil: CustomStringConvertible { - var description: String { "nil" } - } diff --git a/Sources/Presenter/Views/Path.swift b/Sources/Presenter/Views/Path.swift index 5bacfc5..91776b4 100644 --- a/Sources/Presenter/Views/Path.swift +++ b/Sources/Presenter/Views/Path.swift @@ -1,6 +1,4 @@ - -public struct Path: SwiftUIView { - +public struct Path: CodableView { // MARK: Nested Types private enum ElementKind: String, Codable { @@ -53,26 +51,22 @@ public struct Path: SwiftUIView { public mutating func closeSubpath() { elements.append(.init(kind: .closeSubpath, points: [])) } - } // MARK: - CustomStringConvertible extension Path: CustomStringConvertible { - public var description: String { "Path(elements: \(elements))" } - } // MARK: - View #if canImport(SwiftUI) -extension Path { - - public var view: some SwiftUI.View { +extension Path: SwiftUI.View { + public var body: some SwiftUI.View { SwiftUI.Path { path in for element in elements { switch element.kind { @@ -93,7 +87,6 @@ extension Path { } } } - } #endif diff --git a/Sources/Presenter/Views/ScrollView.swift b/Sources/Presenter/Views/ScrollView.swift index c569f80..713c09d 100644 --- a/Sources/Presenter/Views/ScrollView.swift +++ b/Sources/Presenter/Views/ScrollView.swift @@ -1,34 +1,29 @@ - -public struct ScrollView: InternalView, Codable { - +public struct ScrollView: CodableWrapperView { // MARK: Stored Properties private let axis: AxisSet? - private let showsIndicators: Bool? + private let showsIndicators: Bool? // swiftlint:disable:this discouraged_optional_boolean private let content: CoderView // MARK: Initialization - public init( + public init( _ axis: AxisSet? = nil, - showsIndicators: Bool? = nil, - @ViewBuilder content: () -> Content + showsIndicators: Bool? = nil, // swiftlint:disable:this discouraged_optional_boolean + @ViewBuilder content: () -> View ) { self.axis = axis self.showsIndicators = showsIndicators self.content = CoderView(content()) } - } // MARK: - CustomStringConvertible extension ScrollView: CustomStringConvertible { - public var description: String { "ScrollView(content: \(content))" } - } // MARK: - View @@ -36,18 +31,15 @@ extension ScrollView: CustomStringConvertible { #if canImport(SwiftUI) extension ScrollView { - - public var view: _View { - content.apply( + public var body: View { + content.modifier( Modifier(axis: axis?.swiftUIValue ?? .vertical, showsIndicators: showsIndicators ?? true) ) } - } -private struct Modifier: ViewModifier { - +private struct Modifier: ViewModifier, SwiftUI.ViewModifier { let axis: SwiftUI.Axis.Set let showsIndicators: Bool @@ -56,7 +48,6 @@ private struct Modifier: ViewModifier { content } } - } #endif diff --git a/Sources/Presenter/Views/Section.swift b/Sources/Presenter/Views/Section.swift index 5443bbc..d9c319c 100644 --- a/Sources/Presenter/Views/Section.swift +++ b/Sources/Presenter/Views/Section.swift @@ -1,6 +1,4 @@ - -public struct Section: SwiftUIView { - +public struct Section: CodableWrapperView { // MARK: Stored Properties private let header: CoderView? @@ -9,52 +7,23 @@ public struct Section: SwiftUIView { // MARK: Initialization - public init( - @ViewBuilder content: () -> Content - ) { - self.header = nil - self.footer = nil - self.content = CoderView(content()) - } - - public init( - header: Header, - @ViewBuilder content: () -> Content - ) { - self.header = CoderView(header) - self.footer = nil - self.content = CoderView(content()) - } - - public init( - footer: Footer, - @ViewBuilder content: () -> Content - ) { - self.header = nil - self.footer = CoderView(footer) - self.content = CoderView(content()) - } - - public init( - header: Header, - footer: Footer, - @ViewBuilder content: () -> Content + public init( + header: View? = nil, + footer: View? = nil, + @ViewBuilder content: () -> View ) { - self.header = CoderView(header) - self.footer = CoderView(footer) + self.header = header.map(CoderView.init) + self.footer = footer.map(CoderView.init) self.content = CoderView(content()) } - } // MARK: - CustomStringConvertible extension Section: CustomStringConvertible { - public var description: String { "Section(header: \(header.map { "\($0)" } ?? "nil")), footer: \(footer.map { "\($0)" } ?? "nil"), content: \(content))" } - } // MARK: - View @@ -62,25 +31,36 @@ extension Section: CustomStringConvertible { #if canImport(SwiftUI) extension Section { + public var body: View { + content.modifier(Modifier1(header: header, footer: footer)) + } +} + +private struct Modifier1: ViewModifier { + let header: CoderView? + let footer: CoderView? - @SwiftUI.ViewBuilder - public var view: some SwiftUI.View { - switch (header, footer) { - case let (.some(header), .some(footer)): - SwiftUI.Section(header: header.eraseToAnyView(), - footer: footer.eraseToAnyView(), - content: content.eraseToAnyView) - case let (.none, .some(footer)): - SwiftUI.Section(footer: footer.eraseToAnyView(), - content: content.eraseToAnyView) - case let (.some(header), .none): - SwiftUI.Section(header: header.eraseToAnyView(), - content: content.eraseToAnyView) - case (.none, .none): - SwiftUI.Section(content: content.eraseToAnyView) - } + func body(for content: Content) -> View { + (header?.body ?? Nil()).modifier(Modifier2(footer: footer, section: content)) } +} +private struct Modifier2: ViewModifier { + let footer: CoderView? + let section: Section + + func body(for header: Header) -> View { + (footer?.body ?? Nil()).modifier(Modifier3(header: header, section: section)) + } +} + +private struct Modifier3: ViewModifier, SwiftUI.ViewModifier { + let header: Header + let section: Section + + func body(content footer: Content) -> some SwiftUI.View { + SwiftUI.Section(header: header, footer: footer) { section } + } } #endif diff --git a/Sources/Presenter/Views/SecureField.swift b/Sources/Presenter/Views/SecureField.swift index 4a4fa04..a0cfd64 100644 --- a/Sources/Presenter/Views/SecureField.swift +++ b/Sources/Presenter/Views/SecureField.swift @@ -1,6 +1,4 @@ - -public struct SecureField: SwiftUIView { - +public struct SecureField: CodableView { // MARK: Stored Properties private let title: Value @@ -9,37 +7,35 @@ public struct SecureField: SwiftUIView { // MARK: Initialization - public init(_ title: String, text: Binding, + public init(_ title: String, + text: Binding, onCommit: Action? = nil) { self.init(.static(title), text: text, onCommit: onCommit) } - public init(_ title: Value, text: Binding, + public init(_ title: Value, + text: Binding, onCommit: Action? = nil) { self.title = title self.text = text self.onCommit = onCommit.map(CoderAction.init) } - } // MARK: - CustomStringConvertible extension SecureField: CustomStringConvertible { - public var description: String { "SecureField(\"\(title)\", text: \(text))" } - } // MARK: - View #if canImport(SwiftUI) -extension SecureField { - - public var view: some SwiftUI.View { +extension SecureField: SwiftUI.View { + public var body: some SwiftUI.View { ModelView { model in SwiftUI.SecureField( self.title.get(from: model), @@ -48,7 +44,6 @@ extension SecureField { ) } } - } #endif diff --git a/Sources/Presenter/Views/Slider.swift b/Sources/Presenter/Views/Slider.swift index 03f3de2..0d606d0 100644 --- a/Sources/Presenter/Views/Slider.swift +++ b/Sources/Presenter/Views/Slider.swift @@ -1,6 +1,4 @@ - -public struct Slider: SwiftUIView { - +public struct Slider: CodableView { // MARK: Stored Properties private let value: Binding @@ -18,34 +16,30 @@ public struct Slider: SwiftUIView { self.maxValue = range.upperBound self.onEditingChanged = onEditingChanged.map(CoderAction.init) } - } // MARK: - CustomStringConvertible extension Slider: CustomStringConvertible { - public var description: String { "Slider(value: \(value), in: \(minValue...maxValue))" } - } // MARK: - View #if canImport(SwiftUI) -extension Slider { - +extension Slider: SwiftUI.View { #if os(tvOS) - public var view: AnyView { - Nil().eraseToAnyView() + public var body: some SwiftUI.View { + SwiftUI.EmptyView() } #else - public var view: some SwiftUI.View { + public var body: some SwiftUI.View { ModelView { model in SwiftUI.Slider( value: model.binding(for: self.value), @@ -56,7 +50,6 @@ extension Slider { } #endif - } #endif diff --git a/Sources/Presenter/Views/Spacer.swift b/Sources/Presenter/Views/Spacer.swift index 65b0bce..ba6502c 100644 --- a/Sources/Presenter/Views/Spacer.swift +++ b/Sources/Presenter/Views/Spacer.swift @@ -1,6 +1,4 @@ - -public struct Spacer: SwiftUIView { - +public struct Spacer: CodableView { // MARK: Stored Properties private let minLength: Value @@ -14,33 +12,28 @@ public struct Spacer: SwiftUIView { public init(minLength: Value) { self.minLength = minLength } - } // MARK: - CustomStringConvertible extension Spacer: CustomStringConvertible { - public var description: String { "Spacer(minLength: \(minLength))" } - } // MARK: - View #if canImport(SwiftUI) -extension Spacer { - - public var view: some SwiftUI.View { +extension Spacer: SwiftUI.View { + public var body: some SwiftUI.View { ModelView { model in SwiftUI.Spacer( minLength: self.minLength.get(from: model) ) } } - } #endif diff --git a/Sources/Presenter/Views/TabView.swift b/Sources/Presenter/Views/TabView.swift index cbebcba..7b2bc79 100644 --- a/Sources/Presenter/Views/TabView.swift +++ b/Sources/Presenter/Views/TabView.swift @@ -1,6 +1,4 @@ - -public struct TabView: SwiftUIView { - +public struct TabView: CodableView { // MARK: Stored Properties private let selection: Binding? @@ -8,35 +6,31 @@ public struct TabView: SwiftUIView { // MARK: Initialization - public init( + public init( selection: Binding? = nil, - content: Content + content: View ) { self.selection = selection self.content = CoderView(content) } - } // MARK: - CustomStringConvertible extension TabView: CustomStringConvertible { - public var description: String { "TabView(selection: \(selection.map { "\($0)" } ?? "nil"), content: \(content))" } - } // MARK: - View #if canImport(SwiftUI) -extension TabView { - +extension TabView: SwiftUI.View { #if !os(macOS) && !targetEnvironment(macCatalyst) && !os(watchOS) - public var view: some SwiftUI.View { + public var body: some SwiftUI.View { ModelView { model in SwiftUI.TabView( selection: selection.map(model.binding), @@ -47,7 +41,7 @@ extension TabView { #elseif os(watchOS) - public var view: some SwiftUI.View { + public var body: some SwiftUI.View { if #available(watchOS 7.0, *) { ModelView { model in SwiftUI.TabView( @@ -62,12 +56,11 @@ extension TabView { #else - public var view: some SwiftUI.View { + public var body: some SwiftUI.View { SwiftUI.EmptyView() } #endif - } #endif diff --git a/Sources/Presenter/Views/Text.swift b/Sources/Presenter/Views/Text.swift index 54c2a97..28225de 100644 --- a/Sources/Presenter/Views/Text.swift +++ b/Sources/Presenter/Views/Text.swift @@ -1,6 +1,4 @@ - -public struct Text: SwiftUIView { - +public struct Text: CodableView { // MARK: Stored Properties private let text: Value @@ -14,31 +12,26 @@ public struct Text: SwiftUIView { public init(_ text: Value) { self.text = text } - } // MARK: - CustomStringConvertible extension Text: CustomStringConvertible { - public var description: String { "Text(\(text))" } - } // MARK: - View #if canImport(SwiftUI) -extension Text { - - public var view: some SwiftUI.View { +extension Text: SwiftUI.View { + public var body: some SwiftUI.View { ModelView { model in SwiftUI.Text(self.text.get(from: model)) } } - } #endif diff --git a/Sources/Presenter/Views/TextEditor.swift b/Sources/Presenter/Views/TextEditor.swift index c16c87a..b462cad 100644 --- a/Sources/Presenter/Views/TextEditor.swift +++ b/Sources/Presenter/Views/TextEditor.swift @@ -1,6 +1,4 @@ - -public struct TextEditor: SwiftUIView { - +public struct TextEditor: CodableView { // MARK: Stored Properties private let text: Binding @@ -10,29 +8,25 @@ public struct TextEditor: SwiftUIView { public init(text: Binding) { self.text = text } - } // MARK: - CustomStringConvertible extension TextEditor: CustomStringConvertible { - public var description: String { "TextEditor(text: \(text))" } - } // MARK: - View #if canImport(SwiftUI) -extension TextEditor { - +extension TextEditor: SwiftUI.View { #if !os(watchOS) && !os(macOS) && !os(tvOS) && !targetEnvironment(macCatalyst) @SwiftUI.ViewBuilder - public var view: some SwiftUI.View { + public var body: some SwiftUI.View { if #available(iOS 14.0, *) { ModelView { model in SwiftUI.TextEditor(text: model.binding(for: text)) @@ -40,17 +34,15 @@ extension TextEditor { } else { SwiftUI.EmptyView() } - } #else - public var view: some SwiftUI.View { + public var body: some SwiftUI.View { SwiftUI.EmptyView() } #endif - } #endif diff --git a/Sources/Presenter/Views/TextField.swift b/Sources/Presenter/Views/TextField.swift index 5720b13..069fdac 100644 --- a/Sources/Presenter/Views/TextField.swift +++ b/Sources/Presenter/Views/TextField.swift @@ -1,6 +1,4 @@ - -public struct TextField: SwiftUIView { - +public struct TextField: CodableView { // MARK: Stored Properties private let title: Value @@ -10,8 +8,10 @@ public struct TextField: SwiftUIView { // MARK: Initialization - public init(_ title: String, text: Binding, - onCommit: Action? = nil, onEditingChanged: Action? = nil) { + public init(_ title: String, + text: Binding, + onCommit: Action? = nil, + onEditingChanged: Action? = nil) { self.init( .static(title), text: text, @@ -20,33 +20,31 @@ public struct TextField: SwiftUIView { ) } - public init(_ title: Value, text: Binding, - onCommit: Action? = nil, onEditingChanged: Action? = nil) { + public init(_ title: Value, + text: Binding, + onCommit: Action? = nil, + onEditingChanged: Action? = nil) { self.title = title self.text = text self.onCommit = onCommit.map(CoderAction.init) self.onEditingChanged = onEditingChanged.map(CoderAction.init) } - } // MARK: - CustomStringConvertible extension TextField: CustomStringConvertible { - public var description: String { "TextField(\"\(title)\", text: \(text))" } - } // MARK: - View #if canImport(SwiftUI) -extension TextField { - - public var view: some SwiftUI.View { +extension TextField: SwiftUI.View { + public var body: some SwiftUI.View { ModelView { model in SwiftUI.TextField( self.title.get(from: model), @@ -56,7 +54,6 @@ extension TextField { ) } } - } #endif diff --git a/Sources/Presenter/Views/Toggle.swift b/Sources/Presenter/Views/Toggle.swift index c5b1bb5..f6a98e2 100644 --- a/Sources/Presenter/Views/Toggle.swift +++ b/Sources/Presenter/Views/Toggle.swift @@ -1,6 +1,4 @@ - -public struct Toggle: SwiftUIView { - +public struct Toggle: CodableView { // MARK: Stored Properties private let isOn: Binding @@ -8,33 +6,29 @@ public struct Toggle: SwiftUIView { // MARK: Initialization - public init( + public init( isOn: Binding, - label: Label + label: View ) { self.isOn = isOn self.label = CoderView(label) } - } // MARK: - CustomStringConvertible extension Toggle: CustomStringConvertible { - public var description: String { "Toggle(isOn: \(isOn), label: \(label))" } - } // MARK: - View #if canImport(SwiftUI) -extension Toggle { - - public var view: some SwiftUI.View { +extension Toggle: SwiftUI.View { + public var body: some SwiftUI.View { ModelView { model in SwiftUI.Toggle( isOn: model.binding(for: self.isOn), @@ -42,7 +36,6 @@ extension Toggle { ) } } - } #endif diff --git a/Sources/Presenter/Views/VGrid.swift b/Sources/Presenter/Views/VGrid.swift index 1080d31..67e371f 100644 --- a/Sources/Presenter/Views/VGrid.swift +++ b/Sources/Presenter/Views/VGrid.swift @@ -1,6 +1,4 @@ - -public struct VGrid: InternalView, Codable { - +public struct VGrid: CodableWrapperView { // MARK: Stored Properties private let columns: [GridItem] @@ -11,31 +9,27 @@ public struct VGrid: InternalView, Codable { // MARK: Initialization - public init( + public init( columns: [GridItem], alignment: HorizontalAlignment? = nil, spacing: CGFloat? = nil, pinnedViews: PinnedScrollableViews, - @ViewBuilder content: () -> Content + @ViewBuilder content: () -> View ) { - self.columns = columns self.alignment = alignment self.spacing = spacing self.pinnedViews = pinnedViews self.content = CoderView(content()) } - } // MARK: - CustomStringConvertible extension VGrid: CustomStringConvertible { - public var description: String { "VGrid(columns: \(columns), alignment: \(alignment.map { "\($0)" } ?? "nil"), spacing: \(spacing.map(\.description) ?? "nil"), pinnedViews: \(pinnedViews), content: \(content))" } - } // MARK: - View @@ -43,35 +37,32 @@ extension VGrid: CustomStringConvertible { #if canImport(SwiftUI) extension VGrid { - #if !os(macOS) && !targetEnvironment(macCatalyst) - public var view: _View { + public var body: View { if #available(iOS 14.0, tvOS 14.0, watchOS 7.0, *) { - return content.apply( + return content.modifier( Modifier(columns: columns.map(\.swiftUIValue), alignment: alignment?.swiftUIValue ?? .center, spacing: spacing, pinnedViews: pinnedViews.swiftUIValue) ) } else { - return SwiftUI.EmptyView() + return Nil() } } #else - public var view: _View { - SwiftUI.EmptyView() + public var body: View { + Nil() } #endif - } @available(iOS 14.0, macOS 11.0, *) -private struct Modifier: ViewModifier { - +private struct Modifier: ViewModifier, SwiftUI.ViewModifier { let columns: [SwiftUI.GridItem] let alignment: SwiftUI.HorizontalAlignment let spacing: CGFloat? @@ -86,8 +77,6 @@ private struct Modifier: ViewModifier { content: { content } ) } - } #endif - diff --git a/Sources/Presenter/Views/VStack.swift b/Sources/Presenter/Views/VStack.swift index 81d85df..449bf98 100644 --- a/Sources/Presenter/Views/VStack.swift +++ b/Sources/Presenter/Views/VStack.swift @@ -1,6 +1,4 @@ - -public struct VStack: InternalView, Codable { - +public struct VStack: CodableWrapperView { // MARK: Stored Properties private let alignment: HorizontalAlignment? @@ -9,27 +7,23 @@ public struct VStack: InternalView, Codable { // MARK: Initialization - public init( + public init( alignment: HorizontalAlignment? = nil, spacing: CGFloat? = nil, - @ViewBuilder content: () -> Content + @ViewBuilder content: () -> View ) { - self.alignment = alignment self.spacing = spacing self.content = CoderView(content()) } - } // MARK: - CustomStringConvertible extension VStack: CustomStringConvertible { - public var description: String { "VStack(alignment: .\((alignment ?? .center).rawValue), spacing: \(spacing.map { $0.description } ?? "nil"), content: \(content))" } - } // MARK: - View @@ -37,26 +31,26 @@ extension VStack: CustomStringConvertible { #if canImport(SwiftUI) extension VStack { - - public var view: _View { + public var body: View { content - .apply(Modifier(alignment: alignment?.swiftUIValue ?? .center, spacing: spacing)) + .modifier(Modifier(alignment: alignment?.swiftUIValue ?? .center, spacing: spacing)) } - } -private struct Modifier: ViewModifier { - +private struct Modifier: ViewModifier, SwiftUI.ViewModifier { let alignment: SwiftUI.HorizontalAlignment let spacing: CGFloat? + init(alignment: SwiftUI.HorizontalAlignment, spacing: CGFloat?) { + self.alignment = alignment + self.spacing = spacing + } + func body(content: Content) -> some SwiftUI.View { SwiftUI.VStack(alignment: alignment, spacing: spacing) { content } } - } #endif - diff --git a/Sources/Presenter/Views/ZStack.swift b/Sources/Presenter/Views/ZStack.swift index 60e78a5..775cf80 100644 --- a/Sources/Presenter/Views/ZStack.swift +++ b/Sources/Presenter/Views/ZStack.swift @@ -1,6 +1,4 @@ - -public struct ZStack: InternalView, Codable { - +public struct ZStack: CodableWrapperView { // MARK: Stored Properties private let alignment: Alignment? @@ -8,25 +6,21 @@ public struct ZStack: InternalView, Codable { // MARK: Initialization - public init( + public init( alignment: Alignment? = nil, - @ViewBuilder content: () -> Content + @ViewBuilder content: () -> View ) { - self.alignment = alignment self.content = CoderView(content()) } - } // MARK: - CustomStringConvertible extension ZStack: CustomStringConvertible { - public var description: String { "ZStack(alignment: .\((alignment ?? .center).rawValue), content: \(content))" } - } // MARK: - View @@ -34,15 +28,12 @@ extension ZStack: CustomStringConvertible { #if canImport(SwiftUI) extension ZStack { - - public var view: _View { - content.apply(Modifier(alignment: alignment?.swiftUIValue ?? .center)) + public var body: View { + content.modifier(Modifier(alignment: alignment?.swiftUIValue ?? .center)) } - } -private struct Modifier: ViewModifier { - +private struct Modifier: ViewModifier, SwiftUI.ViewModifier { let alignment: SwiftUI.Alignment func body(content: Content) -> some SwiftUI.View { @@ -50,8 +41,6 @@ private struct Modifier: ViewModifier { content } } - } #endif - diff --git a/Sources/TracePresenter/Exports.swift b/Sources/TracePresenter/Exports.swift deleted file mode 100644 index 3456567..0000000 --- a/Sources/TracePresenter/Exports.swift +++ /dev/null @@ -1,2 +0,0 @@ - -@_exported import Presenter diff --git a/Sources/TracePresenter/TraceGraph.swift b/Sources/TracePresenter/TraceGraph.swift deleted file mode 100644 index 6b4fbf0..0000000 --- a/Sources/TracePresenter/TraceGraph.swift +++ /dev/null @@ -1,94 +0,0 @@ - -public struct TraceGraph: SwiftUIView { - - // MARK: Stored Properties - - public var spans: [Span] - - // MARK: Initialization - - public init?(spans: [Span]) { - let children = Dictionary(grouping: spans) { $0.parentService ?? "_" } - .mapValues { Dictionary(grouping: $0) { $0.parentOperation ?? "_" } } - - guard let rootSpan = spans.first(where: { $0.parentService == nil }) else { - return nil - } - - self.spans = [rootSpan] - var currentIndex = 0 - - while currentIndex < self.spans.count { - let currentSpan = spans[currentIndex] - if let spanChildren = children[currentSpan.service]?[currentSpan.operation] { - self.spans.append(contentsOf: spanChildren.sorted { $0.start < $1.start }) - } - currentIndex += 1 - } - } - -} - -extension TraceGraph { - - public struct Span: Codable { - - public let service: String - public let operation: String - - public let start: CGFloat - public let end: CGFloat - - public let parentService: String? - public let parentOperation: String? - - public init(service: String, - operation: String, - start: CGFloat, - end: CGFloat, - parentService: String?, - parentOperation: String? ) { - self.service = service - self.operation = operation - self.start = start - self.end = end - self.parentService = parentService - self.parentOperation = parentOperation - } - - } - -} - -#if canImport(SwiftUI) - -extension TraceGraph { - - public var view: some SwiftUI.View { - SwiftUI.VStack { - SwiftUI.ForEach(spans.indices) { index in - SpanRow(span: spans[index]) - .frame(height: 8) - } - } - } - - private struct SpanRow: SwiftUI.View { - - let span: Span - - var body: some SwiftUI.View { - GeometryReader { geometry in - SwiftUI.Capsule() - .frame(width: (span.end - span.start) * geometry.size.width, - height: geometry.size.width, - alignment: .leading) - .offset(x: span.start) - } - } - - } - -} - -#endif diff --git a/Sources/TracePresenter/TracePresenter.swift b/Sources/TracePresenter/TracePresenter.swift deleted file mode 100644 index c1307d1..0000000 --- a/Sources/TracePresenter/TracePresenter.swift +++ /dev/null @@ -1,12 +0,0 @@ - -public struct TracePresenter: Plugin { - - public init() {} - - public var views: [_CodableView.Type] { - [ - TraceGraph.self, - ] - } - -} diff --git a/Tests/PresenterTests/PresenterTests.swift b/Tests/PresenterTests/PresenterTests.swift index 8d99cf6..963b2c5 100644 --- a/Tests/PresenterTests/PresenterTests.swift +++ b/Tests/PresenterTests/PresenterTests.swift @@ -1,97 +1,70 @@ - #if !os(watchOS) && canImport(XCTest) && canImport(SwiftUI) import XCTest import Presenter -typealias _Model = Model - struct TestAction: Action { func perform(on model: Model) { - model.state["login-title"] = "Success" + model.set("login-title", to: "Success") } } extension Color { - static var gray: Color { Color(red: 0.5, green: 0.5, blue: 0.5) } - } -struct MyCustomView: View { - +struct MyCustomView: UserView { @State("text", default: "") var text - @Binding + @Binding var text2: Value - var body: some View { + var body: View { VStack { TextField(text2, text: $text) MyCustomView2() } } - } -struct MyCustomView2: View { - - var body: some View { +struct MyCustomView2: UserView { + var body: View { Text("gallo") } - } final class PresenterTests: XCTestCase { - func testCustom() throws { let view = MyCustomView(text2: .at("test", default: "")) let data = try Presenter.encode(view) print(String(data: data, encoding: .utf8) ?? "nil") } - func testInverseSquareRoot() { - func inverseSquareRoot(of value: Float) -> Float { - let x2 = value * 0.5 - var y = value - var i = withUnsafeBytes(of: value) { $0.load(as: UInt32.self) } - i = 0x5f3759df &- (i &>> 1) - y = withUnsafeBytes(of: i) { $0.load(as: Float.self) } - y = y * (1.5 - (x2 * y * y)) - y = y * (1.5 - (x2 * y * y)) - y = y * (1.5 - (x2 * y * y)) - return y - } - - for _ in 0..<1_000_000 { - let value = Float.random(in: 0...1_000) + .ulpOfOne - let expected = 1 / sqrt(value) - let actual = inverseSquareRoot(of: value) - XCTAssertEqual(expected, actual, accuracy: expected * 0.000_001) - } - } - func testExample() { let optional: Text? = nil let serverView = HStack(spacing: 8) { Text("Hallo") - .padding(8) - .frame(minWidth: 10, idealWidth: 30, maxWidth: 50, - minHeight: 100, idealHeight: 110, maxHeight: 120, - alignment: Alignment.center) + .padding(8) + .frame(minWidth: 10, + idealWidth: 30, + maxWidth: 50, + minHeight: 100, + idealHeight: 110, + maxHeight: 120, + alignment: .center) Text("Check") - .frame(width: 100) + .frame(width: 100) optional Text("What") - .padding(8) - .background(Color.gray) + .padding(8) + .background(Color.gray) } check(serverView) @@ -131,8 +104,8 @@ final class PresenterTests: XCTestCase { func testJSAction() { let model = Model() - model.state["two"] = Int(2) - model.state["three"] = Int(3) + model.set("two", to: Int(2)) + model.set("three", to: Int(3)) let action = JavaScriptAction( script: """ var five = two + three @@ -143,8 +116,9 @@ final class PresenterTests: XCTestCase { errorKey: "error" ) action.perform(on: model) - XCTAssert((model.state["error"] as AnyObject) is NSNull, "\(String(describing: model.state["error"]))") - XCTAssertEqual(model.state["five"] as? Int, 5) + XCTAssert((model.get("error") as AnyObject) is NSNull, + "\(String(describing: model.get("error")))") + XCTAssertEqual(model.get("five") as? Int, 5) } private func check(_ serverView: V) { @@ -158,11 +132,6 @@ final class PresenterTests: XCTestCase { XCTFail("\(error)") } } - - static var allTests = [ - ("testExample", testExample), - ] - } #endif diff --git a/Tests/PresenterTests/TracePresenterTests.swift b/Tests/PresenterTests/TracePresenterTests.swift index ce6495a..ced7387 100644 --- a/Tests/PresenterTests/TracePresenterTests.swift +++ b/Tests/PresenterTests/TracePresenterTests.swift @@ -1,5 +1,4 @@ - -#if !os(watchOS) && canImport(XCTest) && canImport(UIKit) +#if !os(watchOS) && canImport(XCTest) && canImport(UIKit) && canImport(TracePresenter) import XCTest import Presenter @@ -7,7 +6,6 @@ import TracePresenter import UIKit final class TracePresenterTests: XCTestCase { - func testTracePresenter() { let view = TraceGraph(spans: [ .init( @@ -49,25 +47,22 @@ final class TracePresenterTests: XCTestCase { end: 0.5, parentService: "processing", parentOperation: "locations" - ), + ) ])! let image = view.eraseToAnyView().image(in: CGSize(width: 200, height: 200)) print(image) } - } extension SwiftUI.View { - func image(in size: CGSize) -> UIImage { let view = UIHostingController(rootView: self).view! view.frame = CGRect(origin: .zero, size: size) - return UIGraphicsImageRenderer(size: size).image { context in + return UIGraphicsImageRenderer(size: size).image { _ in view.drawHierarchy(in: view.frame, afterScreenUpdates: true) } } - } #endif