From c0bdf61dda32cce9821887fabf46e05b1cc37eeb Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 26 Aug 2024 08:56:46 -0700 Subject: [PATCH] wip --- 0292-cross-platform-pt3/Counter/.gitignore | 8 + .../Counter/.swift-version | 1 + 0292-cross-platform-pt3/Counter/Package.swift | 44 ++ .../Sources/Counter/CounterModel.swift | 53 ++ .../Counter/Sources/Counter/FactClient.swift | 36 ++ .../Counter/Sources/WasmApp/App.swift | 68 +++ .../ModernUIKit.xcodeproj/project.pbxproj | 454 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../ModernUIKit/.swift-version | 1 + 0292-cross-platform-pt3/ModernUIKit/App.swift | 15 + .../ModernUIKit/AppFeature.swift | 89 ++++ .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 13 + .../ModernUIKit/Assets.xcassets/Contents.json | 6 + .../ModernUIKit/CounterFeatureEpoxy.swift | 232 +++++++++ .../ModernUIKit/CounterFeatureSwiftUI.swift | 36 ++ .../ModernUIKit/CounterFeatureUIKit.swift | 125 +++++ .../ModernUIKit/Info.plist | 11 + .../Preview Assets.xcassets/Contents.json | 6 + .../ModernUIKit/SettingsFeature.swift | 71 +++ 0292-cross-platform-pt3/README.md | 5 + README.md | 1 + 23 files changed, 1301 insertions(+) create mode 100644 0292-cross-platform-pt3/Counter/.gitignore create mode 100644 0292-cross-platform-pt3/Counter/.swift-version create mode 100644 0292-cross-platform-pt3/Counter/Package.swift create mode 100644 0292-cross-platform-pt3/Counter/Sources/Counter/CounterModel.swift create mode 100644 0292-cross-platform-pt3/Counter/Sources/Counter/FactClient.swift create mode 100644 0292-cross-platform-pt3/Counter/Sources/WasmApp/App.swift create mode 100644 0292-cross-platform-pt3/ModernUIKit.xcodeproj/project.pbxproj create mode 100644 0292-cross-platform-pt3/ModernUIKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 0292-cross-platform-pt3/ModernUIKit.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 0292-cross-platform-pt3/ModernUIKit/.swift-version create mode 100644 0292-cross-platform-pt3/ModernUIKit/App.swift create mode 100644 0292-cross-platform-pt3/ModernUIKit/AppFeature.swift create mode 100644 0292-cross-platform-pt3/ModernUIKit/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 0292-cross-platform-pt3/ModernUIKit/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 0292-cross-platform-pt3/ModernUIKit/Assets.xcassets/Contents.json create mode 100644 0292-cross-platform-pt3/ModernUIKit/CounterFeatureEpoxy.swift create mode 100644 0292-cross-platform-pt3/ModernUIKit/CounterFeatureSwiftUI.swift create mode 100644 0292-cross-platform-pt3/ModernUIKit/CounterFeatureUIKit.swift create mode 100644 0292-cross-platform-pt3/ModernUIKit/Info.plist create mode 100644 0292-cross-platform-pt3/ModernUIKit/Preview Content/Preview Assets.xcassets/Contents.json create mode 100644 0292-cross-platform-pt3/ModernUIKit/SettingsFeature.swift create mode 100644 0292-cross-platform-pt3/README.md diff --git a/0292-cross-platform-pt3/Counter/.gitignore b/0292-cross-platform-pt3/Counter/.gitignore new file mode 100644 index 00000000..0023a534 --- /dev/null +++ b/0292-cross-platform-pt3/Counter/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/0292-cross-platform-pt3/Counter/.swift-version b/0292-cross-platform-pt3/Counter/.swift-version new file mode 100644 index 00000000..2304abaf --- /dev/null +++ b/0292-cross-platform-pt3/Counter/.swift-version @@ -0,0 +1 @@ +wasm-DEVELOPMENT-SNAPSHOT-2024-07-16-a diff --git a/0292-cross-platform-pt3/Counter/Package.swift b/0292-cross-platform-pt3/Counter/Package.swift new file mode 100644 index 00000000..3be6380e --- /dev/null +++ b/0292-cross-platform-pt3/Counter/Package.swift @@ -0,0 +1,44 @@ +// swift-tools-version: 6.0 + +import PackageDescription + +let package = Package( + name: "Counter", + platforms: [.iOS(.v16), .macOS(.v14)], + products: [ + .library( + name: "Counter", + targets: ["Counter"] + ), + ], + dependencies: [ + .package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.0.0"), + .package(url: "https://github.com/pointfreeco/swift-navigation", from: "2.0.0"), + .package(url: "https://github.com/pointfreeco/swift-perception", from: "1.0.0"), + .package(url: "https://github.com/swiftwasm/carton", from: "1.0.0"), + .package(url: "https://github.com/swiftwasm/JavaScriptKit", exact: "0.19.2"), + ], + targets: [ + .executableTarget( + name: "WasmApp", + dependencies: [ + "Counter", + .product(name: "SwiftNavigation", package: "swift-navigation"), + .product(name: "JavaScriptEventLoop", package: "JavaScriptKit"), + .product(name: "JavaScriptKit", package: "JavaScriptKit"), + ] + ), + .target( + name: "Counter", + dependencies: [ + .product(name: "JavaScriptKit", package: "JavaScriptKit", condition: .when(platforms: [.wasi])), + .product(name: "JavaScriptEventLoop", package: "JavaScriptKit", condition: .when(platforms: [.wasi])), + .product(name: "Dependencies", package: "swift-dependencies"), + .product(name: "DependenciesMacros", package: "swift-dependencies"), + .product(name: "SwiftNavigation", package: "swift-navigation"), + .product(name: "Perception", package: "swift-perception") + ] + ), + ], + swiftLanguageVersions: [.v6] +) diff --git a/0292-cross-platform-pt3/Counter/Sources/Counter/CounterModel.swift b/0292-cross-platform-pt3/Counter/Sources/Counter/CounterModel.swift new file mode 100644 index 00000000..13fb25c7 --- /dev/null +++ b/0292-cross-platform-pt3/Counter/Sources/Counter/CounterModel.swift @@ -0,0 +1,53 @@ +import Dependencies +import Foundation +import Perception +import SwiftNavigation + +@MainActor +@Perceptible +public class CounterModel: HashableObject { + @PerceptionIgnored + @Dependency(FactClient.self) var factClient + + public var count = 0 { + didSet { + isTextFocused = !count.isMultiple(of: 3) + } + } + public var fact: Fact? + public var factIsLoading = false + public var isTextFocused = false + public var text = "" + + public struct Fact: Identifiable { + public var value: String + public var id: String { value } + } + + public init() {} + + public func incrementButtonTapped() { + count += 1 + fact = nil + } + + public func decrementButtonTapped() { + count -= 1 + fact = nil + } + + public func factButtonTapped() async { + fact = nil + factIsLoading = true + defer { factIsLoading = false } + + do { + try await Task.sleep(for: .seconds(1)) + + var count = count + fact = Fact(value: try await factClient.fetch(count)) + } catch { + // TODO: error handling + } + } +} diff --git a/0292-cross-platform-pt3/Counter/Sources/Counter/FactClient.swift b/0292-cross-platform-pt3/Counter/Sources/Counter/FactClient.swift new file mode 100644 index 00000000..6157694a --- /dev/null +++ b/0292-cross-platform-pt3/Counter/Sources/Counter/FactClient.swift @@ -0,0 +1,36 @@ +import Dependencies +import DependenciesMacros +#if canImport(JavaScriptKit) + @preconcurrency import JavaScriptKit +#endif +#if canImport(JavaScriptEventLoop) + import JavaScriptEventLoop +#endif + +@DependencyClient +struct FactClient: Sendable { + var fetch: @Sendable (Int) async throws -> String +} + +extension FactClient: TestDependencyKey { + static let testValue = FactClient() +} + +extension FactClient: DependencyKey { + static let liveValue = FactClient { number in +#if canImport(JavaScriptKit) && canImport(JavaScriptEventLoop) + let response = try await JSPromise( + JSObject.global.fetch!("http://www.numberapi.com/\(number)").object! + )!.value + return try await JSPromise(response.text().object!)!.value.string! +#else + return try await String( + decoding: URLSession.shared + .data( + from: URL(string: "http://www.numberapi.com/\(number)")! + ).0, + as: UTF8.self + ) +#endif + } +} diff --git a/0292-cross-platform-pt3/Counter/Sources/WasmApp/App.swift b/0292-cross-platform-pt3/Counter/Sources/WasmApp/App.swift new file mode 100644 index 00000000..2a77592c --- /dev/null +++ b/0292-cross-platform-pt3/Counter/Sources/WasmApp/App.swift @@ -0,0 +1,68 @@ +import Counter +import JavaScriptEventLoop +import JavaScriptKit +import SwiftNavigation + +@main +@MainActor +struct App { + static var tokens: Set = [] + + static func main() { + JavaScriptEventLoop.installGlobalExecutor() + + let model = CounterModel() + + let document = JSObject.global.document + + var countLabel = document.createElement("span") + _ = document.body.appendChild(countLabel) + + var decrementButton = document.createElement("button") + decrementButton.innerText = "-" + decrementButton.onclick = .object( + JSClosure { _ in + model.decrementButtonTapped() + return .undefined + } + ) + _ = document.body.appendChild(decrementButton) + + var incrementButton = document.createElement("button") + incrementButton.innerText = "+" + incrementButton.onclick = .object( + JSClosure { _ in + model.incrementButtonTapped() + return .undefined + } + ) + _ = document.body.appendChild(incrementButton) + + var factButton = document.createElement("button") + factButton.innerText = "Get fact" + factButton.onclick = .object( + JSClosure { _ in + Task { await model.factButtonTapped() } + return .undefined + } + ) + _ = document.body.appendChild(factButton) + + + var factLabel = document.createElement("div") + _ = document.body.appendChild(factLabel) + + observe { + countLabel.innerText = .string("Count: \(model.count)") + + if let fact = model.fact?.value { + factLabel.innerText = .string(fact) + } else if model.factIsLoading { + factLabel.innerText = "Fact is loading..." + } else { + factLabel.innerText = "" + } + } + .store(in: &tokens) + } +} diff --git a/0292-cross-platform-pt3/ModernUIKit.xcodeproj/project.pbxproj b/0292-cross-platform-pt3/ModernUIKit.xcodeproj/project.pbxproj new file mode 100644 index 00000000..750b1e19 --- /dev/null +++ b/0292-cross-platform-pt3/ModernUIKit.xcodeproj/project.pbxproj @@ -0,0 +1,454 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + 2A154CF02C5D56C7009FC7D9 /* Epoxy in Frameworks */ = {isa = PBXBuildFile; productRef = 2A154CEF2C5D56C7009FC7D9 /* Epoxy */; }; + 2A154CF22C5D56E1009FC7D9 /* CounterFeatureEpoxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A154CF12C5D56E1009FC7D9 /* CounterFeatureEpoxy.swift */; }; + 2A154CF42C5D607D009FC7D9 /* Counter in Resources */ = {isa = PBXBuildFile; fileRef = 2A154CF32C5D607D009FC7D9 /* Counter */; }; + 2A154CF82C5D620A009FC7D9 /* .swift-version in Resources */ = {isa = PBXBuildFile; fileRef = 2A154CF72C5D6206009FC7D9 /* .swift-version */; }; + 2A1CEB272BFD271600753A66 /* Perception in Frameworks */ = {isa = PBXBuildFile; productRef = 2A1CEB262BFD271600753A66 /* Perception */; }; + 2A6230292BFEA5C600930179 /* AppFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A6230282BFEA5C600930179 /* AppFeature.swift */; }; + 4B54C2252BFD0D1900E95174 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B54C2242BFD0D1900E95174 /* App.swift */; }; + 4B54C2292BFD0D1A00E95174 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4B54C2282BFD0D1A00E95174 /* Assets.xcassets */; }; + 4B54C22C2BFD0D1A00E95174 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4B54C22B2BFD0D1A00E95174 /* Preview Assets.xcassets */; }; + 4B54C24F2BFD0D9B00E95174 /* CounterFeatureSwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B54C24E2BFD0D9B00E95174 /* CounterFeatureSwiftUI.swift */; }; + 4BB60BE42BFE8098002516B4 /* SettingsFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB60BE32BFE8098002516B4 /* SettingsFeature.swift */; }; + CA5C1D632C5932B500F8882D /* SwiftNavigation in Frameworks */ = {isa = PBXBuildFile; productRef = CA5C1D622C5932B500F8882D /* SwiftNavigation */; }; + CA5C1D652C5932B500F8882D /* SwiftUINavigation in Frameworks */ = {isa = PBXBuildFile; productRef = CA5C1D642C5932B500F8882D /* SwiftUINavigation */; }; + CA5C1D672C5932B500F8882D /* UIKitNavigation in Frameworks */ = {isa = PBXBuildFile; productRef = CA5C1D662C5932B500F8882D /* UIKitNavigation */; }; + CA5C1D6B2C5939D400F8882D /* CounterFeatureUIKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA5C1D6A2C5939D400F8882D /* CounterFeatureUIKit.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 2A154CF12C5D56E1009FC7D9 /* CounterFeatureEpoxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CounterFeatureEpoxy.swift; sourceTree = ""; }; + 2A154CF32C5D607D009FC7D9 /* Counter */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Counter; sourceTree = SOURCE_ROOT; }; + 2A154CF72C5D6206009FC7D9 /* .swift-version */ = {isa = PBXFileReference; lastKnownFileType = text; path = ".swift-version"; sourceTree = ""; }; + 2A1CEB242BFD1A3800753A66 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + 2A6230282BFEA5C600930179 /* AppFeature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppFeature.swift; sourceTree = ""; }; + 4B54C2212BFD0D1900E95174 /* ModernUIKit.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ModernUIKit.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 4B54C2242BFD0D1900E95174 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = ""; }; + 4B54C2282BFD0D1A00E95174 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 4B54C22B2BFD0D1A00E95174 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + 4B54C24E2BFD0D9B00E95174 /* CounterFeatureSwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CounterFeatureSwiftUI.swift; sourceTree = ""; }; + 4BB60BE32BFE8098002516B4 /* SettingsFeature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsFeature.swift; sourceTree = ""; }; + CA5C1D6A2C5939D400F8882D /* CounterFeatureUIKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CounterFeatureUIKit.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 4B54C21E2BFD0D1900E95174 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 2A154CF02C5D56C7009FC7D9 /* Epoxy in Frameworks */, + CA5C1D632C5932B500F8882D /* SwiftNavigation in Frameworks */, + CA5C1D672C5932B500F8882D /* UIKitNavigation in Frameworks */, + 2A1CEB272BFD271600753A66 /* Perception in Frameworks */, + CA5C1D652C5932B500F8882D /* SwiftUINavigation in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 4B54C2182BFD0D1900E95174 = { + isa = PBXGroup; + children = ( + 4B54C2232BFD0D1900E95174 /* ModernUIKit */, + 4B54C2222BFD0D1900E95174 /* Products */, + ); + sourceTree = ""; + }; + 4B54C2222BFD0D1900E95174 /* Products */ = { + isa = PBXGroup; + children = ( + 4B54C2212BFD0D1900E95174 /* ModernUIKit.app */, + ); + name = Products; + sourceTree = ""; + }; + 4B54C2232BFD0D1900E95174 /* ModernUIKit */ = { + isa = PBXGroup; + children = ( + 2A154CF72C5D6206009FC7D9 /* .swift-version */, + 2A154CF32C5D607D009FC7D9 /* Counter */, + 2A1CEB242BFD1A3800753A66 /* Info.plist */, + 4B54C2242BFD0D1900E95174 /* App.swift */, + 2A6230282BFEA5C600930179 /* AppFeature.swift */, + 2A154CF12C5D56E1009FC7D9 /* CounterFeatureEpoxy.swift */, + 4B54C24E2BFD0D9B00E95174 /* CounterFeatureSwiftUI.swift */, + CA5C1D6A2C5939D400F8882D /* CounterFeatureUIKit.swift */, + 4BB60BE32BFE8098002516B4 /* SettingsFeature.swift */, + 4B54C2282BFD0D1A00E95174 /* Assets.xcassets */, + 4B54C22A2BFD0D1A00E95174 /* Preview Content */, + ); + path = ModernUIKit; + sourceTree = ""; + }; + 4B54C22A2BFD0D1A00E95174 /* Preview Content */ = { + isa = PBXGroup; + children = ( + 4B54C22B2BFD0D1A00E95174 /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 4B54C2202BFD0D1900E95174 /* ModernUIKit */ = { + isa = PBXNativeTarget; + buildConfigurationList = 4B54C2452BFD0D1A00E95174 /* Build configuration list for PBXNativeTarget "ModernUIKit" */; + buildPhases = ( + 4B54C21D2BFD0D1900E95174 /* Sources */, + 4B54C21E2BFD0D1900E95174 /* Frameworks */, + 4B54C21F2BFD0D1900E95174 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = ModernUIKit; + packageProductDependencies = ( + 2A1CEB262BFD271600753A66 /* Perception */, + CA5C1D622C5932B500F8882D /* SwiftNavigation */, + CA5C1D642C5932B500F8882D /* SwiftUINavigation */, + CA5C1D662C5932B500F8882D /* UIKitNavigation */, + 2A154CEF2C5D56C7009FC7D9 /* Epoxy */, + ); + productName = ModernUIKit; + productReference = 4B54C2212BFD0D1900E95174 /* ModernUIKit.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 4B54C2192BFD0D1900E95174 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1540; + LastUpgradeCheck = 1540; + TargetAttributes = { + 4B54C2202BFD0D1900E95174 = { + CreatedOnToolsVersion = 15.4; + }; + }; + }; + buildConfigurationList = 4B54C21C2BFD0D1900E95174 /* Build configuration list for PBXProject "ModernUIKit" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 4B54C2182BFD0D1900E95174; + packageReferences = ( + 2A1CEB252BFD271600753A66 /* XCRemoteSwiftPackageReference "swift-perception" */, + CA5C1D612C5932B500F8882D /* XCRemoteSwiftPackageReference "swift-navigation" */, + 2A154CEE2C5D56C7009FC7D9 /* XCRemoteSwiftPackageReference "epoxy-ios" */, + ); + productRefGroup = 4B54C2222BFD0D1900E95174 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 4B54C2202BFD0D1900E95174 /* ModernUIKit */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 4B54C21F2BFD0D1900E95174 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2A154CF82C5D620A009FC7D9 /* .swift-version in Resources */, + 2A154CF42C5D607D009FC7D9 /* Counter in Resources */, + 4B54C22C2BFD0D1A00E95174 /* Preview Assets.xcassets in Resources */, + 4B54C2292BFD0D1A00E95174 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 4B54C21D2BFD0D1900E95174 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + CA5C1D6B2C5939D400F8882D /* CounterFeatureUIKit.swift in Sources */, + 4B54C2252BFD0D1900E95174 /* App.swift in Sources */, + 4B54C24F2BFD0D9B00E95174 /* CounterFeatureSwiftUI.swift in Sources */, + 4BB60BE42BFE8098002516B4 /* SettingsFeature.swift in Sources */, + 2A154CF22C5D56E1009FC7D9 /* CounterFeatureEpoxy.swift in Sources */, + 2A6230292BFEA5C600930179 /* AppFeature.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 4B54C2432BFD0D1A00E95174 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_STRICT_CONCURRENCY = complete; + }; + name = Debug; + }; + 4B54C2442BFD0D1A00E95174 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_STRICT_CONCURRENCY = complete; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 4B54C2462BFD0D1A00E95174 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"ModernUIKit/Preview Content\""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = ModernUIKit/Info.plist; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.ModernUIKit; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 4B54C2472BFD0D1A00E95174 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"ModernUIKit/Preview Content\""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = ModernUIKit/Info.plist; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.ModernUIKit; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 4B54C21C2BFD0D1900E95174 /* Build configuration list for PBXProject "ModernUIKit" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4B54C2432BFD0D1A00E95174 /* Debug */, + 4B54C2442BFD0D1A00E95174 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 4B54C2452BFD0D1A00E95174 /* Build configuration list for PBXNativeTarget "ModernUIKit" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4B54C2462BFD0D1A00E95174 /* Debug */, + 4B54C2472BFD0D1A00E95174 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 2A154CEE2C5D56C7009FC7D9 /* XCRemoteSwiftPackageReference "epoxy-ios" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/airbnb/epoxy-ios"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.10.0; + }; + }; + 2A1CEB252BFD271600753A66 /* XCRemoteSwiftPackageReference "swift-perception" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/pointfreeco/swift-perception"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.1.7; + }; + }; + CA5C1D612C5932B500F8882D /* XCRemoteSwiftPackageReference "swift-navigation" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/pointfreeco/swift-navigation.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 2.0.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 2A154CEF2C5D56C7009FC7D9 /* Epoxy */ = { + isa = XCSwiftPackageProductDependency; + package = 2A154CEE2C5D56C7009FC7D9 /* XCRemoteSwiftPackageReference "epoxy-ios" */; + productName = Epoxy; + }; + 2A1CEB262BFD271600753A66 /* Perception */ = { + isa = XCSwiftPackageProductDependency; + package = 2A1CEB252BFD271600753A66 /* XCRemoteSwiftPackageReference "swift-perception" */; + productName = Perception; + }; + CA5C1D622C5932B500F8882D /* SwiftNavigation */ = { + isa = XCSwiftPackageProductDependency; + package = CA5C1D612C5932B500F8882D /* XCRemoteSwiftPackageReference "swift-navigation" */; + productName = SwiftNavigation; + }; + CA5C1D642C5932B500F8882D /* SwiftUINavigation */ = { + isa = XCSwiftPackageProductDependency; + package = CA5C1D612C5932B500F8882D /* XCRemoteSwiftPackageReference "swift-navigation" */; + productName = SwiftUINavigation; + }; + CA5C1D662C5932B500F8882D /* UIKitNavigation */ = { + isa = XCSwiftPackageProductDependency; + package = CA5C1D612C5932B500F8882D /* XCRemoteSwiftPackageReference "swift-navigation" */; + productName = UIKitNavigation; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 4B54C2192BFD0D1900E95174 /* Project object */; +} diff --git a/0292-cross-platform-pt3/ModernUIKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/0292-cross-platform-pt3/ModernUIKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/0292-cross-platform-pt3/ModernUIKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/0292-cross-platform-pt3/ModernUIKit.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/0292-cross-platform-pt3/ModernUIKit.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/0292-cross-platform-pt3/ModernUIKit.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/0292-cross-platform-pt3/ModernUIKit/.swift-version b/0292-cross-platform-pt3/ModernUIKit/.swift-version new file mode 100644 index 00000000..2304abaf --- /dev/null +++ b/0292-cross-platform-pt3/ModernUIKit/.swift-version @@ -0,0 +1 @@ +wasm-DEVELOPMENT-SNAPSHOT-2024-07-16-a diff --git a/0292-cross-platform-pt3/ModernUIKit/App.swift b/0292-cross-platform-pt3/ModernUIKit/App.swift new file mode 100644 index 00000000..200192cb --- /dev/null +++ b/0292-cross-platform-pt3/ModernUIKit/App.swift @@ -0,0 +1,15 @@ +import SwiftUI +import UIKitNavigation + +@main +struct ModernUIKitApp: App { + var body: some Scene { + WindowGroup { + UIViewControllerRepresenting { + NavigationStackController( + model: AppModel() + ) + } + } + } +} diff --git a/0292-cross-platform-pt3/ModernUIKit/AppFeature.swift b/0292-cross-platform-pt3/ModernUIKit/AppFeature.swift new file mode 100644 index 00000000..685ccb93 --- /dev/null +++ b/0292-cross-platform-pt3/ModernUIKit/AppFeature.swift @@ -0,0 +1,89 @@ +import Perception +import SwiftUI +import UIKitNavigation + +@Perceptible +class AppModel { + var path: [Path] + init(path: [Path] = []) { + self.path = path + } + + enum Path: Hashable { + case counter(CounterModel) + case settings(SettingsModel) + } +} + +struct AppView: View { + @Perception.Bindable var model: AppModel + + var body: some View { + WithPerceptionTracking { + NavigationStack(path: $model.path) { + Form { + Button("Counter") { + model.path.append(.counter(CounterModel())) + } + Button("Settings") { + model.path.append(.settings(SettingsModel())) + } + + NavigationLink("Counter w/ value", value: AppModel.Path.counter(CounterModel())) + } + .navigationDestination(for: AppModel.Path.self) { path in + switch path { + case let .counter(model): + CounterView(model: model) + case let .settings(model): + SettingsView(model: model) + } + } + } + } + } +} + +extension NavigationStackController { + convenience init(model: AppModel) { + @UIBindable var model = model + self.init(path: $model.path) { + RootViewController() + } + self.navigationDestination(for: AppModel.Path.self) { path in + switch path { + case let .counter(model): + CounterViewController(model: model) + case let .settings(model): + SettingsViewController(model: model) + } + } + } +} + +final class RootViewController: UIViewController { + override func viewDidLoad() { + super.viewDidLoad() + + let counterButton = UIButton(type: .system, primaryAction: UIAction { [weak self] _ in + self?.navigationController?.push(value: AppModel.Path.counter(CounterModel())) + }) + counterButton.setTitle("Counter", for: .normal) + let settingsButton = UIButton(type: .system, primaryAction: UIAction { [weak self] _ in + self?.navigationController?.push(value: AppModel.Path.settings(SettingsModel())) + }) + settingsButton.setTitle("Settings", for: .normal) + let stack = UIStackView(arrangedSubviews: [ + counterButton, + settingsButton, + ]) + stack.axis = .vertical + stack.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(stack) + + NSLayoutConstraint.activate([ + stack.centerXAnchor.constraint(equalTo: view.centerXAnchor), + stack.centerYAnchor.constraint(equalTo: view.centerYAnchor), + ]) + } +} diff --git a/0292-cross-platform-pt3/ModernUIKit/Assets.xcassets/AccentColor.colorset/Contents.json b/0292-cross-platform-pt3/ModernUIKit/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/0292-cross-platform-pt3/ModernUIKit/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0292-cross-platform-pt3/ModernUIKit/Assets.xcassets/AppIcon.appiconset/Contents.json b/0292-cross-platform-pt3/ModernUIKit/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..13613e3e --- /dev/null +++ b/0292-cross-platform-pt3/ModernUIKit/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0292-cross-platform-pt3/ModernUIKit/Assets.xcassets/Contents.json b/0292-cross-platform-pt3/ModernUIKit/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/0292-cross-platform-pt3/ModernUIKit/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0292-cross-platform-pt3/ModernUIKit/CounterFeatureEpoxy.swift b/0292-cross-platform-pt3/ModernUIKit/CounterFeatureEpoxy.swift new file mode 100644 index 00000000..02108c4f --- /dev/null +++ b/0292-cross-platform-pt3/ModernUIKit/CounterFeatureEpoxy.swift @@ -0,0 +1,232 @@ +import Epoxy +import UIKit +import UIKitNavigation + +class EpoxyCounterViewController: CollectionViewController { + @UIBindable var model: CounterModel + + init(model: CounterModel) { + self.model = model + super.init( + layout: UICollectionViewCompositionalLayout.list( + using: UICollectionLayoutListConfiguration( + appearance: .grouped + ) + ) + ) + } + + private enum DataID { + case activity + case count + case decrementButton + case fact + case factButton + case incrementButton + } + + override func viewDidLoad() { + super.viewDidLoad() + + observe { [weak self] in + guard let self else { return } + + setItems(items, animated: false) + } + + present(item: $model.fact) { fact in + let alert = UIAlertController( + title: "Fact", + message: fact.value, + preferredStyle: .alert + ) + alert.addAction(UIAlertAction(title: "OK", style: .default)) + return alert + } + } + + @ItemModelBuilder + private var items: [ItemModeling] { + Label.itemModel( + dataID: DataID.count, + content: "Count: \(model.count)", + style: .style(with: .title1) + ) + ButtonRow.itemModel( + dataID: DataID.decrementButton, + content: ButtonRow.Content(text: "Decrement"), + behaviors: ButtonRow.Behaviors( + didTap: { [weak self] in + self?.model.decrementButtonTapped() + } + ) + ) + ButtonRow.itemModel( + dataID: DataID.incrementButton, + content: ButtonRow.Content(text: "Increment"), + behaviors: ButtonRow.Behaviors( + didTap: { [weak self] in + self?.model.incrementButtonTapped() + } + ) + ) + ButtonRow.itemModel( + dataID: DataID.factButton, + content: ButtonRow.Content(text: "Get fact"), + behaviors: ButtonRow.Behaviors( + didTap: { [weak self] in + Task { + await self?.model.factButtonTapped() + } + } + ) + ) +// if let fact = model.fact { +// Label.itemModel( +// dataID: DataID.fact, +// content: fact.value, +// style: .style(with: .body) +// ) +// } else { + ActivityIndicatorView.itemModel( + dataID: DataID.activity, + content: model.factIsLoading, + style: .large + ) +// } + } +} + +final class ActivityIndicatorView: UIActivityIndicatorView, EpoxyableView { + func setContent(_ content: Bool, animated: Bool) { + if content { + startAnimating() + } else { + stopAnimating() + } + } +} + +final class Label: UILabel, @preconcurrency EpoxyableView { + + // MARK: Lifecycle + + init(style: Style) { + super.init(frame: .zero) + translatesAutoresizingMaskIntoConstraints = false + font = style.font + numberOfLines = style.numberOfLines + if style.showLabelBackground { + backgroundColor = .secondarySystemBackground + } + } + + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + struct Style: Hashable { + let font: UIFont + let showLabelBackground: Bool + var numberOfLines = 0 + } + + typealias Content = String + + func setContent(_ content: String, animated _: Bool) { + text = content + } +} + +extension Label.Style { + static func style( + with textStyle: UIFont.TextStyle, + showBackground: Bool = false + ) -> Label.Style { + .init( + font: UIFont.preferredFont(forTextStyle: textStyle), + showLabelBackground: showBackground + ) + } +} + +final class ButtonRow: UIView, @preconcurrency EpoxyableView { + + // MARK: Lifecycle + + init() { + super.init(frame: .zero) + setUp() + } + + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: Internal + + struct Behaviors { + var didTap: (() -> Void)? + } + + struct Content: Equatable { + var text: String? + } + + func setContent(_ content: Content, animated _: Bool) { + text = content.text + } + + func setBehaviors(_ behaviors: Behaviors?) { + didTap = behaviors?.didTap + } + + // MARK: Private + + private let button = UIButton(type: .system) + private var didTap: (() -> Void)? + + private var text: String? { + get { button.title(for: .normal) } + set { button.setTitle(newValue, for: .normal) } + } + + private func setUp() { + translatesAutoresizingMaskIntoConstraints = false + layoutMargins = UIEdgeInsets(top: 20, left: 24, bottom: 20, right: 24) + backgroundColor = .quaternarySystemFill + + button.tintColor = .systemBlue + button.titleLabel?.font = UIFont.preferredFont(forTextStyle: .title3) + button.translatesAutoresizingMaskIntoConstraints = false + + addSubview(button) + NSLayoutConstraint.activate([ + button.leadingAnchor + .constraint(equalTo: layoutMarginsGuide.leadingAnchor), + button.topAnchor + .constraint(equalTo: layoutMarginsGuide.topAnchor), + button.trailingAnchor + .constraint(equalTo: layoutMarginsGuide.trailingAnchor), + button.bottomAnchor + .constraint(equalTo: layoutMarginsGuide.bottomAnchor), + ]) + + button.addTarget(self, action: #selector(handleTap), for: .touchUpInside) + } + + @objc + private func handleTap() { + didTap?() + } +} + +import SwiftUI +// #Preview +struct EpoxyCounterViewControllerPreview: PreviewProvider { + static var previews: some View { + UIViewControllerRepresenting { + EpoxyCounterViewController(model: CounterModel()) + } + } +} diff --git a/0292-cross-platform-pt3/ModernUIKit/CounterFeatureSwiftUI.swift b/0292-cross-platform-pt3/ModernUIKit/CounterFeatureSwiftUI.swift new file mode 100644 index 00000000..350d663d --- /dev/null +++ b/0292-cross-platform-pt3/ModernUIKit/CounterFeatureSwiftUI.swift @@ -0,0 +1,36 @@ +import Perception +import SwiftUI +import SwiftUINavigation + +struct CounterView: View { + @Perception.Bindable var model: CounterModel + var body: some View { + WithPerceptionTracking { + Form { + Stepper( + "\(model.count)", + value: $model.count + ) + if model.factIsLoading { + ProgressView().id(UUID()) + } + + Button("Get fact") { + Task { + await model.factButtonTapped() + } + } + } + .disabled(model.factIsLoading) + .sheet(item: $model.fact) { fact in + Text(fact.value) + } + } + } +} + +#Preview("SwiftUI") { + NavigationStack { + CounterView(model: CounterModel()) + } +} diff --git a/0292-cross-platform-pt3/ModernUIKit/CounterFeatureUIKit.swift b/0292-cross-platform-pt3/ModernUIKit/CounterFeatureUIKit.swift new file mode 100644 index 00000000..c6816f96 --- /dev/null +++ b/0292-cross-platform-pt3/ModernUIKit/CounterFeatureUIKit.swift @@ -0,0 +1,125 @@ +import UIKit +import UIKitNavigation +import SwiftUI + +final class CounterViewController: UIViewController { + @UIBindable var model: CounterModel + + init(model: CounterModel) { + self.model = model + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .systemBackground + + let countLabel = UILabel() + countLabel.textAlignment = .center + + let counter = UIStepper(frame: .zero, value: $model.count.toDouble) + + let textField = UITextField(frame: .zero, text: $model.text) + textField.bind(focus: $model.isTextFocused) + textField.placeholder = "Some text" + textField.borderStyle = .bezel + + let factLabel = UILabel() + factLabel.numberOfLines = 0 + let activityIndicator = UIActivityIndicatorView() + activityIndicator.startAnimating() + let factButton = UIButton(type: .system, primaryAction: UIAction { [weak self] _ in + guard let self else { return } + Task { await self.model.factButtonTapped() } + }) + factButton.setTitle("Get fact", for: .normal) + let resetButton = UIButton(type: .system, primaryAction: UIAction { [weak self] _ in + guard let self else { return } + model.count = 0 + }) + resetButton.setTitle("Reset", for: .normal) + + let counterStack = UIStackView(arrangedSubviews: [ + countLabel, + counter, + resetButton, + textField, + factLabel, + activityIndicator, + factButton, + ]) + counterStack.axis = .vertical + counterStack.spacing = 12 + counterStack.translatesAutoresizingMaskIntoConstraints = false + + view.addSubview(counterStack) + NSLayoutConstraint.activate([ + counterStack.centerXAnchor.constraint(equalTo: view.centerXAnchor), + counterStack.centerYAnchor.constraint(equalTo: view.centerYAnchor), + counterStack.leadingAnchor.constraint(equalTo: view.leadingAnchor), + counterStack.trailingAnchor.constraint(equalTo: view.trailingAnchor), + ]) + + observe { [weak self] in + guard let self else { return } + countLabel.text = "\(model.count)" + activityIndicator.isHidden = !model.factIsLoading + counter.isEnabled = !model.factIsLoading + factButton.isEnabled = !model.factIsLoading + } + + present(item: $model.fact) { fact in + FactViewController(fact: fact.value) + } + } +} + +extension Int { + fileprivate var toDouble: Double { + get { + Double(self) + } + set { + self = Int(newValue) + } + } +} + +class FactViewController: UIViewController { + let fact: String + init(fact: String) { + self.fact = fact + super.init(nibName: nil, bundle: nil) + } + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = .white + let factLabel = UILabel() + factLabel.text = fact + factLabel.numberOfLines = 0 + factLabel.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(factLabel) + NSLayoutConstraint.activate([ + factLabel.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + factLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + factLabel.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), + factLabel.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), + ]) + } +} + +#Preview("UIKit") { + UIViewControllerRepresenting { + UINavigationController( + rootViewController: CounterViewController(model: CounterModel()) + ) + } +} diff --git a/0292-cross-platform-pt3/ModernUIKit/Info.plist b/0292-cross-platform-pt3/ModernUIKit/Info.plist new file mode 100644 index 00000000..6a6654d9 --- /dev/null +++ b/0292-cross-platform-pt3/ModernUIKit/Info.plist @@ -0,0 +1,11 @@ + + + + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + + diff --git a/0292-cross-platform-pt3/ModernUIKit/Preview Content/Preview Assets.xcassets/Contents.json b/0292-cross-platform-pt3/ModernUIKit/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/0292-cross-platform-pt3/ModernUIKit/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0292-cross-platform-pt3/ModernUIKit/SettingsFeature.swift b/0292-cross-platform-pt3/ModernUIKit/SettingsFeature.swift new file mode 100644 index 00000000..05898d4d --- /dev/null +++ b/0292-cross-platform-pt3/ModernUIKit/SettingsFeature.swift @@ -0,0 +1,71 @@ +import Perception +import SwiftUI +import SwiftUINavigation + +@MainActor +@Perceptible +class SettingsModel: HashableObject { + var isOn = false +} + +struct SettingsView: View { + @Perception.Bindable var model: SettingsModel + + var body: some View { + WithPerceptionTracking { + Form { + Toggle(isOn: $model.isOn) { + Text("Is on?") + } + } + } + } +} + +class SettingsViewController: UIViewController { + let model: SettingsModel + + init(model: SettingsModel) { + self.model = model + super.init(nibName: nil, bundle: nil) + } + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .systemBackground + + let isOnSwitch = UISwitch() + isOnSwitch.addAction( + UIAction { [weak model = self.model, weak isOnSwitch] _ in + guard let model, let isOnSwitch + else { return } + model.isOn = isOnSwitch.isOn + }, + for: .valueChanged + ) + isOnSwitch.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(isOnSwitch) + + NSLayoutConstraint.activate([ + isOnSwitch.centerXAnchor.constraint(equalTo: view.centerXAnchor), + isOnSwitch.centerYAnchor.constraint(equalTo: view.centerYAnchor), + ]) + + observe { [weak self] in + guard let self else { return } + isOnSwitch.setOn(model.isOn, animated: true) + } + } +} + +#Preview("SwiftUI") { + SettingsView(model: SettingsModel()) +} +#Preview("UIKit") { + UIViewControllerRepresenting { + SettingsViewController(model: SettingsModel()) + } +} diff --git a/0292-cross-platform-pt3/README.md b/0292-cross-platform-pt3/README.md new file mode 100644 index 00000000..f8b7baed --- /dev/null +++ b/0292-cross-platform-pt3/README.md @@ -0,0 +1,5 @@ +## [Point-Free](https://www.pointfree.co) + +> #### This directory contains code from Point-Free Episode: [Cross-Platform Swift: Networkgin](https://www.pointfree.co/episodes/ep292-cross-platform-networking) +> +> Let’s dial up the complexity of our Wasm application! We’ll introduce some async logic in the form of a network request. We’ll take steps to not only control this dependency, but we’ll do so across both Apple and Wasm platforms, and we’ll isolate its interface from its live implementation to speed up our builds and reduce our app’s size. diff --git a/README.md b/README.md index e4802ca4..cc106430 100644 --- a/README.md +++ b/README.md @@ -293,3 +293,4 @@ This repository is the home of code written on episodes of [Point-Free](https:// 1. [Modern UIKit: UIControl Bindings](0289-modern-uikit-pt9) 1. [Cross-Platform Swift: View Paradigms](0290-cross-platform-pt1) 1. [Cross-Platform Swift: WebAssembly](0291-cross-platform-pt2) +1. [Cross-Platform Swift: Networking](0292-cross-platform-pt3)