diff --git a/NativeDemo/TokamakDemo.xcodeproj/project.pbxproj b/NativeDemo/TokamakDemo.xcodeproj/project.pbxproj index b2c122376..9e5a44b7e 100644 --- a/NativeDemo/TokamakDemo.xcodeproj/project.pbxproj +++ b/NativeDemo/TokamakDemo.xcodeproj/project.pbxproj @@ -41,6 +41,8 @@ D1B4229124B3B9BB00682F74 /* ListDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1B4228E24B3B9BB00682F74 /* ListDemo.swift */; }; D1B4229224B3B9BB00682F74 /* OutlineGroupDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1B4228F24B3B9BB00682F74 /* OutlineGroupDemo.swift */; }; D1B4229324B3B9BB00682F74 /* OutlineGroupDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1B4228F24B3B9BB00682F74 /* OutlineGroupDemo.swift */; }; + D1C726F324CB63C6003B576D /* ButtonStyleDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1C726F224CB63C6003B576D /* ButtonStyleDemo.swift */; }; + D1C726F424CB63C6003B576D /* ButtonStyleDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1C726F224CB63C6003B576D /* ButtonStyleDemo.swift */; }; D1E5FDAD24C1D57000E7485E /* TokamakShim.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1E5FDAC24C1D57000E7485E /* TokamakShim.swift */; }; D1E5FDAF24C1D58E00E7485E /* libTokamakShim.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D1E5FDA424C1D54B00E7485E /* libTokamakShim.a */; }; D1E5FDB224C1D59400E7485E /* libTokamakShim.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D1E5FDA424C1D54B00E7485E /* libTokamakShim.a */; }; @@ -101,6 +103,7 @@ B5C76E4924C73ED4003EABB2 /* AppStorageDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppStorageDemo.swift; sourceTree = ""; }; D1B4228E24B3B9BB00682F74 /* ListDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListDemo.swift; sourceTree = ""; }; D1B4228F24B3B9BB00682F74 /* OutlineGroupDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutlineGroupDemo.swift; sourceTree = ""; }; + D1C726F224CB63C6003B576D /* ButtonStyleDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ButtonStyleDemo.swift; sourceTree = ""; }; D1E5FDA424C1D54B00E7485E /* libTokamakShim.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libTokamakShim.a; sourceTree = BUILT_PRODUCTS_DIR; }; D1E5FDAC24C1D57000E7485E /* TokamakShim.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokamakShim.swift; sourceTree = ""; }; D1EE7EA624C0DD2100C0D127 /* PickerDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PickerDemo.swift; sourceTree = ""; }; @@ -162,6 +165,7 @@ 85ED189924AD425E0085DFA0 /* TokamakDemo */ = { isa = PBXGroup; children = ( + D1C726F224CB63C6003B576D /* ButtonStyleDemo.swift */, B5C76E4924C73ED4003EABB2 /* AppStorageDemo.swift */, B56F22DF24BC89FD001738DF /* ColorDemo.swift */, 85ED189E24AD425E0085DFA0 /* Counter.swift */, @@ -335,6 +339,7 @@ 3DCDE44424CA6AD400910F17 /* SidebarDemo.swift in Sources */, 85ED18AD24AD425E0085DFA0 /* TextFieldDemo.swift in Sources */, 85ED18A724AD425E0085DFA0 /* ForEachDemo.swift in Sources */, + D1C726F324CB63C6003B576D /* ButtonStyleDemo.swift in Sources */, 854A1A9124B3E3630027BC32 /* ToggleDemo.swift in Sources */, 85ED18A524AD425E0085DFA0 /* TextDemo.swift in Sources */, 85ED18AB24AD425E0085DFA0 /* Counter.swift in Sources */, @@ -359,6 +364,7 @@ 3DCDE44524CA6AD400910F17 /* SidebarDemo.swift in Sources */, 85ED18AC24AD425E0085DFA0 /* Counter.swift in Sources */, 85ED18A824AD425E0085DFA0 /* ForEachDemo.swift in Sources */, + D1C726F424CB63C6003B576D /* ButtonStyleDemo.swift in Sources */, 854A1A9324B3F28F0027BC32 /* ToggleDemo.swift in Sources */, 85ED18AE24AD425E0085DFA0 /* TextFieldDemo.swift in Sources */, 85ED18A624AD425E0085DFA0 /* TextDemo.swift in Sources */, diff --git a/Sources/TokamakCore/DynamicProperty.swift b/Sources/TokamakCore/DynamicProperty.swift index 8735c2d8c..7ed5e013a 100644 --- a/Sources/TokamakCore/DynamicProperty.swift +++ b/Sources/TokamakCore/DynamicProperty.swift @@ -24,34 +24,3 @@ public protocol DynamicProperty { extension DynamicProperty { public mutating func update() {} } - -extension TypeInfo { - /// Extract all `DynamicProperty` from a type, recursively. - /// This is necessary as a `DynamicProperty` can be nested. - /// `EnvironmentValues` can also be injected at this point. - func dynamicProperties(_ environment: EnvironmentValues, - source: inout Any, - shouldUpdate: Bool) -> [PropertyInfo] { - var dynamicProps = [PropertyInfo]() - for prop in properties where prop.type is DynamicProperty.Type { - dynamicProps.append(prop) - // swiftlint:disable force_try - let propInfo = try! typeInfo(of: prop.type) - propInfo.injectEnvironment(from: environment, into: &source) - var extracted = try! prop.get(from: source) - dynamicProps.append( - contentsOf: propInfo.dynamicProperties(environment, - source: &extracted, - shouldUpdate: shouldUpdate) - ) - // swiftlint:disable:next force_cast - var extractedDynamicProp = extracted as! DynamicProperty - if shouldUpdate { - extractedDynamicProp.update() - } - try! prop.set(value: extractedDynamicProp, on: &source) - // swiftlint:enable force_try - } - return dynamicProps - } -} diff --git a/Sources/TokamakCore/Environment/EnvironmentValues.swift b/Sources/TokamakCore/Environment/EnvironmentValues.swift index 258375d67..3b0b55266 100644 --- a/Sources/TokamakCore/Environment/EnvironmentValues.swift +++ b/Sources/TokamakCore/Environment/EnvironmentValues.swift @@ -45,6 +45,21 @@ public struct EnvironmentValues: CustomStringConvertible { } } +struct IsEnabledKey: EnvironmentKey { + static let defaultValue = true +} + +extension EnvironmentValues { + public var isEnabled: Bool { + get { + self[IsEnabledKey.self] + } + set { + self[IsEnabledKey.self] = newValue + } + } +} + struct _EnvironmentValuesWritingModifier: ViewModifier, EnvironmentModifier { let environmentValues: EnvironmentValues diff --git a/Sources/TokamakCore/MountedViews/MountedApp.swift b/Sources/TokamakCore/MountedViews/MountedApp.swift index 3c8b7f96b..5341d8994 100644 --- a/Sources/TokamakCore/MountedViews/MountedApp.swift +++ b/Sources/TokamakCore/MountedViews/MountedApp.swift @@ -57,20 +57,3 @@ final class MountedApp: MountedCompositeElement { ) } } - -extension _AnyApp { - func makeMountedApp( - _ parentTarget: R.TargetType, - _ environmentValues: EnvironmentValues - ) -> MountedApp where R: Renderer { - // swiftlint:disable:next force_try - let info = try! typeInfo(of: type) - - var modified = app - info.injectEnvironment(from: environmentValues, into: &modified) - - var result = self - result.app = modified - return MountedApp(result, parentTarget, environmentValues) - } -} diff --git a/Sources/TokamakCore/MountedViews/MountedCompositeElement.swift b/Sources/TokamakCore/MountedViews/MountedCompositeElement.swift index 05efc8958..adfd3d05e 100644 --- a/Sources/TokamakCore/MountedViews/MountedCompositeElement.swift +++ b/Sources/TokamakCore/MountedViews/MountedCompositeElement.swift @@ -17,24 +17,15 @@ import OpenCombine -class MountedCompositeElement: MountedElement, Hashable { - static func == (lhs: MountedCompositeElement, - rhs: MountedCompositeElement) -> Bool { - lhs === rhs - } - - func hash(into hasher: inout Hasher) { - hasher.combine(ObjectIdentifier(self)) - } - +class MountedCompositeElement: MountedElement { let parentTarget: R.TargetType var state = [Any]() var subscriptions = [AnyCancellable]() - init(_ app: _AnyApp, _ parentTarget: R.TargetType, _ environmentValues: EnvironmentValues) { + init(_ app: A, _ parentTarget: R.TargetType, _ environmentValues: EnvironmentValues) { self.parentTarget = parentTarget - super.init(app, environmentValues) + super.init(_AnyApp(app), environmentValues) } init(_ scene: _AnyScene, _ parentTarget: R.TargetType, _ environmentValues: EnvironmentValues) { @@ -47,3 +38,14 @@ class MountedCompositeElement: MountedElement, Hashable { super.init(view, environmentValues) } } + +extension MountedCompositeElement: Hashable { + static func == (lhs: MountedCompositeElement, + rhs: MountedCompositeElement) -> Bool { + lhs === rhs + } + + func hash(into hasher: inout Hasher) { + hasher.combine(ObjectIdentifier(self)) + } +} diff --git a/Sources/TokamakCore/MountedViews/MountedElement.swift b/Sources/TokamakCore/MountedViews/MountedElement.swift index ebf43f939..774d5d6f3 100644 --- a/Sources/TokamakCore/MountedViews/MountedElement.swift +++ b/Sources/TokamakCore/MountedViews/MountedElement.swift @@ -22,6 +22,14 @@ enum MountedElementKind { case app(_AnyApp) case scene(_AnyScene) case view(AnyView) + + var type: Any.Type { + switch self { + case let .app(app): return app.type + case let .scene(scene): return scene.type + case let .view(view): return view.type + } + } } public class MountedElement { @@ -65,14 +73,6 @@ public class MountedElement { } } - var elementType: Any.Type { - switch element { - case let .app(app): return app.type - case let .scene(scene): return scene.type - case let .view(view): return view.type - } - } - var typeConstructorName: String { switch element { case .app: fatalError(""" @@ -91,16 +91,34 @@ public class MountedElement { init(_ app: _AnyApp, _ environmentValues: EnvironmentValues) { element = .app(app) self.environmentValues = environmentValues + updateEnvironment() } init(_ scene: _AnyScene, _ environmentValues: EnvironmentValues) { element = .scene(scene) self.environmentValues = environmentValues + updateEnvironment() } init(_ view: AnyView, _ environmentValues: EnvironmentValues) { element = .view(view) self.environmentValues = environmentValues + updateEnvironment() + } + + @discardableResult func updateEnvironment() -> TypeInfo { + // swiftlint:disable:next force_try + let info = try! typeInfo(of: element.type) + switch element { + case .app: + environmentValues = info.injectEnvironment(from: environmentValues, into: &app.app) + case .scene: + environmentValues = info.injectEnvironment(from: environmentValues, into: &scene.scene) + case .view: + environmentValues = info.injectEnvironment(from: environmentValues, into: &view.view) + } + + return info } func mount(with reconciler: StackReconciler) { @@ -117,10 +135,20 @@ public class MountedElement { } extension TypeInfo { - func injectEnvironment(from environmentValues: EnvironmentValues, into element: inout Any) { + fileprivate func injectEnvironment( + from environmentValues: EnvironmentValues, + into element: inout Any + ) -> EnvironmentValues { + var modifiedEnv = environmentValues + // swiftlint:disable force_try + // Extract the view from the AnyView for modification, apply Environment changes: + if genericTypes.contains(where: { $0 is EnvironmentModifier.Type }), + let modifier = try! property(named: "modifier").get(from: element) as? EnvironmentModifier { + modifier.modifyEnvironment(&modifiedEnv) + } + // Inject @Environment values // swiftlint:disable force_cast - // swiftlint:disable force_try // `DynamicProperty`s can have `@Environment` properties contained in them, // so we have to inject into them as well. for dynamicProp in properties.filter({ $0.type is DynamicProperty.Type }) { @@ -128,18 +156,45 @@ extension TypeInfo { var propWrapper = try! dynamicProp.get(from: element) as! DynamicProperty for prop in propInfo.properties.filter({ $0.type is EnvironmentReader.Type }) { var wrapper = try! prop.get(from: propWrapper) as! EnvironmentReader - wrapper.setContent(from: environmentValues) + wrapper.setContent(from: modifiedEnv) try! prop.set(value: wrapper, on: &propWrapper) } try! dynamicProp.set(value: propWrapper, on: &element) } for prop in properties.filter({ $0.type is EnvironmentReader.Type }) { var wrapper = try! prop.get(from: element) as! EnvironmentReader - wrapper.setContent(from: environmentValues) + wrapper.setContent(from: modifiedEnv) try! prop.set(value: wrapper, on: &element) } // swiftlint:enable force_try // swiftlint:enable force_cast + + return modifiedEnv + } + + /// Extract all `DynamicProperty` from a type, recursively. + /// This is necessary as a `DynamicProperty` can be nested. + /// `EnvironmentValues` can also be injected at this point. + func dynamicProperties(_ environment: EnvironmentValues, + source: inout Any) -> [PropertyInfo] { + var dynamicProps = [PropertyInfo]() + for prop in properties where prop.type is DynamicProperty.Type { + dynamicProps.append(prop) + // swiftlint:disable force_try + let propInfo = try! typeInfo(of: prop.type) + _ = propInfo.injectEnvironment(from: environment, into: &source) + var extracted = try! prop.get(from: source) + dynamicProps.append( + contentsOf: propInfo.dynamicProperties(environment, + source: &extracted) + ) + // swiftlint:disable:next force_cast + var extractedDynamicProp = extracted as! DynamicProperty + extractedDynamicProp.update() + try! prop.set(value: extractedDynamicProp, on: &source) + // swiftlint:enable force_try + } + return dynamicProps } } @@ -148,30 +203,12 @@ extension AnyView { _ parentTarget: R.TargetType, _ environmentValues: EnvironmentValues ) -> MountedElement { - // Find Environment changes - var modifiedEnv = environmentValues - // swiftlint:disable force_try - // Extract the view from the AnyView for modification - let viewInfo = try! typeInfo(of: type) - if viewInfo.genericTypes.filter({ $0 is EnvironmentModifier.Type }).count > 0 { - // Apply Environment changes: - if let modifier = try! viewInfo - .property(named: "modifier") - .get(from: view) as? EnvironmentModifier { - modifier.modifyEnvironment(&modifiedEnv) - } - } - var modifiedView = view - viewInfo.injectEnvironment(from: environmentValues, into: &modifiedView) - - var anyView = self - anyView.view = modifiedView - if anyView.type == EmptyView.self { - return MountedEmptyView(anyView, modifiedEnv) - } else if anyView.bodyType == Never.self && !(anyView.type is ViewDeferredToRenderer.Type) { - return MountedHostView(anyView, parentTarget, modifiedEnv) + if type == EmptyView.self { + return MountedEmptyView(self, environmentValues) + } else if bodyType == Never.self && !(type is ViewDeferredToRenderer.Type) { + return MountedHostView(self, parentTarget, environmentValues) } else { - return MountedCompositeView(anyView, parentTarget, modifiedEnv) + return MountedCompositeView(self, parentTarget, environmentValues) } } } diff --git a/Sources/TokamakCore/MountedViews/MountedHostView.swift b/Sources/TokamakCore/MountedViews/MountedHostView.swift index 7c6e14e9b..071643835 100644 --- a/Sources/TokamakCore/MountedViews/MountedHostView.swift +++ b/Sources/TokamakCore/MountedViews/MountedHostView.swift @@ -38,9 +38,7 @@ public final class MountedHostView: MountedElement { } override func mount(with reconciler: StackReconciler) { - guard - let target = reconciler.renderer?.mountTarget(to: parentTarget, - with: self) + guard let target = reconciler.renderer?.mountTarget(to: parentTarget, with: self) else { return } self.target = target @@ -68,6 +66,7 @@ public final class MountedHostView: MountedElement { override func update(with reconciler: StackReconciler) { guard let target = target else { return } + updateEnvironment() target.view = view reconciler.renderer?.update(target: target, with: self) @@ -96,10 +95,7 @@ public final class MountedHostView: MountedElement { let newChild: MountedElement if firstChild.typeConstructorName == mountedChildren[0].view.typeConstructorName { child.view = firstChild - // Inject Environment - // swiftlint:disable:next force_try - let viewInfo = try! typeInfo(of: child.view.type) - viewInfo.injectEnvironment(from: environmentValues, into: &child.view.view) + child.updateEnvironment() child.update(with: reconciler) newChild = child } else { diff --git a/Sources/TokamakCore/MountedViews/MountedScene.swift b/Sources/TokamakCore/MountedViews/MountedScene.swift index 239a65a61..e2b75ab31 100644 --- a/Sources/TokamakCore/MountedViews/MountedScene.swift +++ b/Sources/TokamakCore/MountedViews/MountedScene.swift @@ -89,28 +89,19 @@ extension _AnyScene { _ parentTarget: R.TargetType, _ environmentValues: EnvironmentValues ) -> MountedScene { - // swiftlint:disable:next force_try - let info = try! typeInfo(of: type) - - var modified = scene - info.injectEnvironment(from: environmentValues, into: &modified) - var title: String? - if let titledSelf = modified as? TitledScene, + if let titledSelf = scene as? TitledScene, let text = titledSelf.title { title = _TextProxy(text).rawText } let children: [MountedElement] - if let deferredScene = modified as? SceneDeferredToRenderer { + if let deferredScene = scene as? SceneDeferredToRenderer { children = [deferredScene.deferredBody.makeMountedView(parentTarget, environmentValues)] - } else if let groupScene = modified as? GroupScene { + } else if let groupScene = scene as? GroupScene { children = groupScene.children.map { $0.makeMountedScene(parentTarget, environmentValues) } } else { children = [] } - - var result = self - result.scene = modified - return .init(result, title, children, parentTarget, environmentValues) + return .init(self, title, children, parentTarget, environmentValues) } } diff --git a/Sources/TokamakCore/StackReconciler.swift b/Sources/TokamakCore/StackReconciler.swift index 4eb227b30..d181c8c7b 100644 --- a/Sources/TokamakCore/StackReconciler.swift +++ b/Sources/TokamakCore/StackReconciler.swift @@ -90,7 +90,7 @@ public final class StackReconciler { self.scheduler = scheduler rootTarget = target - rootElement = _AnyApp(app).makeMountedApp(target, environment) + rootElement = MountedApp(app, target, environment) rootElement.mount(with: self) if let mountedApp = rootElement as? MountedApp { @@ -147,9 +147,8 @@ public final class StackReconciler { if state.getter == nil || state.setter == nil { state.getter = { compositeElement.state[id] } - // Avoiding an indirect reference cycle here: this closure can be - // owned by callbacks owned by view's target, which is strongly referenced - // by the reconciler. + // Avoiding an indirect reference cycle here: this closure can be owned by callbacks + // owned by view's target, which is strongly referenced by the reconciler. state.setter = { [weak self, weak compositeElement] newValue in guard let element = compositeElement else { return } self?.queueStateUpdate(for: element, id: id) { $0 = newValue } @@ -178,16 +177,15 @@ public final class StackReconciler { func render(compositeElement: MountedCompositeElement, body bodyKeypath: ReferenceWritableKeyPath, Any>, result: KeyPath, (Any) -> T>) -> T { - let info = try! typeInfo(of: compositeElement.elementType) - info.injectEnvironment(from: compositeElement.environmentValues, - into: &compositeElement[keyPath: bodyKeypath]) - - let needsSubscriptions = compositeElement.subscriptions.isEmpty + let info = compositeElement.updateEnvironment() var stateIdx = 0 - let dynamicProps = info.dynamicProperties(compositeElement.environmentValues, - source: &compositeElement[keyPath: bodyKeypath], - shouldUpdate: true) + let dynamicProps = info.dynamicProperties( + compositeElement.environmentValues, + source: &compositeElement[keyPath: bodyKeypath] + ) + + let needsSubscriptions = compositeElement.subscriptions.isEmpty for property in dynamicProps { // Setup state/subscriptions if property.type is ValueStorage.Type { diff --git a/Sources/TokamakCore/Styles/ButtonStyle.swift b/Sources/TokamakCore/Styles/ButtonStyle.swift new file mode 100644 index 000000000..6c68e6de8 --- /dev/null +++ b/Sources/TokamakCore/Styles/ButtonStyle.swift @@ -0,0 +1,97 @@ +// Copyright 2020 Tokamak contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Created by Gene Z. Ragan on 07/22/2020. + +public struct ButtonStyleConfiguration { + public struct Label: View { + let content: AnyView + public var body: Never { + neverBody("ButtonStyleConfiguration.Label") + } + } + + public let label: Label + public let isPressed: Bool +} + +/// This is a helper class that works around absence of "package private" access control in Swift +public struct _ButtonStyleConfigurationProxy { + public struct Label { + public typealias Subject = ButtonStyleConfiguration.Label + public let subject: Subject + + public init(_ subject: Subject) { self.subject = subject } + + public var content: AnyView { subject.content } + } + + public typealias Subject = ButtonStyleConfiguration + public let subject: Subject + + public init(label: AnyView, isPressed: Bool) { + subject = .init(label: .init(content: label), isPressed: isPressed) + } + + public var label: ButtonStyleConfiguration.Label { subject.label } +} + +public protocol ButtonStyle { + associatedtype Body: View + + func makeBody(configuration: Self.Configuration) -> Self.Body + + typealias Configuration = ButtonStyleConfiguration +} + +public struct _AnyButtonStyle: ButtonStyle { + public typealias Body = AnyView + + private let bodyClosure: (ButtonStyleConfiguration) -> AnyView + public let type: Any.Type + + public init(_ style: S) { + type = S.self + bodyClosure = { configuration in + AnyView(style.makeBody(configuration: configuration)) + } + } + + public func makeBody(configuration: ButtonStyleConfiguration) -> AnyView { + bodyClosure(configuration) + } +} + +public enum _ButtonStyleKey: EnvironmentKey { + public static var defaultValue: _AnyButtonStyle { + fatalError("\(self) must have a renderer-provided default value") + } +} + +extension EnvironmentValues { + var buttonStyle: _AnyButtonStyle { + get { + self[_ButtonStyleKey.self] + } + set { + self[_ButtonStyleKey.self] = newValue + } + } +} + +extension View { + public func buttonStyle(_ style: S) -> some View where S: ButtonStyle { + environment(\.buttonStyle, _AnyButtonStyle(style)) + } +} diff --git a/Sources/TokamakCore/Views/Buttons/Button.swift b/Sources/TokamakCore/Views/Buttons/Button.swift index 23cb1744b..f99872e71 100644 --- a/Sources/TokamakCore/Views/Buttons/Button.swift +++ b/Sources/TokamakCore/Views/Buttons/Button.swift @@ -36,17 +36,30 @@ /// Button("\(counter)", action: { counter += 1 }) /// } public struct Button