diff --git a/NativeDemo/TokamakDemo.xcodeproj/project.pbxproj b/NativeDemo/TokamakDemo.xcodeproj/project.pbxproj index 80eab715..7243c670 100644 --- a/NativeDemo/TokamakDemo.xcodeproj/project.pbxproj +++ b/NativeDemo/TokamakDemo.xcodeproj/project.pbxproj @@ -41,6 +41,8 @@ B5C76E4B24C73ED5003EABB2 /* AppStorageDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C76E4924C73ED4003EABB2 /* AppStorageDemo.swift */; }; B5DBA22B24D509B4003D3347 /* RedactDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5DBA22A24D509B4003D3347 /* RedactDemo.swift */; }; B5DBA22C24D509B4003D3347 /* RedactDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5DBA22A24D509B4003D3347 /* RedactDemo.swift */; }; + B5F2BE032571443D00FB3653 /* PreferenceKeyDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5F2BE022571443D00FB3653 /* PreferenceKeyDemo.swift */; }; + B5F2BE042571443D00FB3653 /* PreferenceKeyDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5F2BE022571443D00FB3653 /* PreferenceKeyDemo.swift */; }; D1B4229024B3B9BB00682F74 /* ListDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1B4228E24B3B9BB00682F74 /* ListDemo.swift */; }; D1B4229124B3B9BB00682F74 /* ListDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1B4228E24B3B9BB00682F74 /* ListDemo.swift */; }; D1B4229224B3B9BB00682F74 /* OutlineGroupDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1B4228F24B3B9BB00682F74 /* OutlineGroupDemo.swift */; }; @@ -110,6 +112,7 @@ B56F22E224BD1C26001738DF /* GridDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GridDemo.swift; sourceTree = ""; }; B5C76E4924C73ED4003EABB2 /* AppStorageDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppStorageDemo.swift; sourceTree = ""; }; B5DBA22A24D509B4003D3347 /* RedactDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RedactDemo.swift; sourceTree = ""; }; + B5F2BE022571443D00FB3653 /* PreferenceKeyDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferenceKeyDemo.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 = ""; }; @@ -188,6 +191,7 @@ D1B4228F24B3B9BB00682F74 /* OutlineGroupDemo.swift */, B51F214F24B920B400CF2583 /* PathDemo.swift */, D1EE7EA624C0DD2100C0D127 /* PickerDemo.swift */, + B5F2BE022571443D00FB3653 /* PreferenceKeyDemo.swift */, B5DBA22A24D509B4003D3347 /* RedactDemo.swift */, 3DCDE44324CA6AD400910F17 /* SidebarDemo.swift */, 8500293E24D2FF3E001A2E84 /* SliderDemo.swift */, @@ -350,6 +354,7 @@ 85ED18A324AD425E0085DFA0 /* SpacerDemo.swift in Sources */, D1B4229024B3B9BB00682F74 /* ListDemo.swift in Sources */, D1EE7EA724C0DD2100C0D127 /* PickerDemo.swift in Sources */, + B5F2BE032571443D00FB3653 /* PreferenceKeyDemo.swift in Sources */, 8500293F24D2FF3E001A2E84 /* SliderDemo.swift in Sources */, 85ED18A924AD425E0085DFA0 /* TokamakDemo.swift in Sources */, B5C76E4A24C73ED5003EABB2 /* AppStorageDemo.swift in Sources */, @@ -378,6 +383,7 @@ 85ED18B024AD425E0085DFA0 /* EnvironmentDemo.swift in Sources */, D1B4229124B3B9BB00682F74 /* ListDemo.swift in Sources */, D1EE7EA824C0DD2100C0D127 /* PickerDemo.swift in Sources */, + B5F2BE042571443D00FB3653 /* PreferenceKeyDemo.swift in Sources */, 8500294024D2FF3E001A2E84 /* SliderDemo.swift in Sources */, 85ED18B624AD42D70085DFA0 /* NSAppDelegate.swift in Sources */, B5C76E4B24C73ED5003EABB2 /* AppStorageDemo.swift in Sources */, diff --git a/Package.swift b/Package.swift index 68ffd913..be7f653d 100644 --- a/Package.swift +++ b/Package.swift @@ -7,7 +7,7 @@ import PackageDescription let package = Package( name: "Tokamak", platforms: [ - .macOS(.v10_15), + .macOS(.v11), .iOS(.v13), ], products: [ diff --git a/Sources/TokamakCore/Environment/EnvironmentKey.swift b/Sources/TokamakCore/Environment/EnvironmentKey.swift index 05bef4a7..6f482a89 100644 --- a/Sources/TokamakCore/Environment/EnvironmentKey.swift +++ b/Sources/TokamakCore/Environment/EnvironmentKey.swift @@ -30,9 +30,7 @@ public struct _EnvironmentKeyWritingModifier: ViewModifier, EnvironmentMo self.value = value } - public func body(content: Content) -> some View { - content - } + public typealias Body = Never func modifyEnvironment(_ values: inout EnvironmentValues) { values[keyPath: keyPath] = value diff --git a/Sources/TokamakCore/Modifiers/Navigation.swift b/Sources/TokamakCore/Modifiers/Navigation.swift index 133421e2..41898a35 100644 --- a/Sources/TokamakCore/Modifiers/Navigation.swift +++ b/Sources/TokamakCore/Modifiers/Navigation.swift @@ -13,13 +13,27 @@ // limitations under the License. public extension View { - // FIXME: Implement - func navigationBarTitle(_ title: S) -> some View where S: StringProtocol { - self + @available(*, deprecated, renamed: "navigationTitle(_:)") + func navigationBarTitle(_ title: Text) -> some View { + navigationTitle(title) } - // FIXME: Implement - func navigationTitle(_ title: S) -> some View where S: StringProtocol { - self + @available(*, deprecated, renamed: "navigationTitle(_:)") + func navigationBarTitle(_ title: S) -> some View { + navigationTitle(title) + } + + func navigationTitle(_ title: Text) -> some View { + navigationTitle { title } + } + + func navigationTitle(_ titleKey: S) -> some View { + navigationTitle(Text(titleKey)) + } + + func navigationTitle(@ViewBuilder _ title: () -> V) -> some View + where V: View + { + preference(key: NavigationTitleKey.self, value: AnyView(title())) } } diff --git a/Sources/TokamakCore/Modifiers/StyleModifiers.swift b/Sources/TokamakCore/Modifiers/StyleModifiers.swift index bcfbb49c..a7f73411 100644 --- a/Sources/TokamakCore/Modifiers/StyleModifiers.swift +++ b/Sources/TokamakCore/Modifiers/StyleModifiers.swift @@ -28,7 +28,11 @@ public struct _BackgroundModifier: ViewModifier, EnvironmentReader } public func body(content: Content) -> some View { - content + // FIXME: Clip to bounds of foreground. + ZStack(alignment: alignment) { + background + content + } } mutating func setContent(from values: EnvironmentValues) { @@ -67,6 +71,7 @@ public struct _OverlayModifier: ViewModifier, EnvironmentReader } public func body(content: Content) -> some View { + // FIXME: Clip to content shape. ZStack(alignment: alignment) { content overlay diff --git a/Sources/TokamakCore/Modifiers/ViewModifier.swift b/Sources/TokamakCore/Modifiers/ViewModifier.swift index 3f5ba805..6b1f8724 100644 --- a/Sources/TokamakCore/Modifiers/ViewModifier.swift +++ b/Sources/TokamakCore/Modifiers/ViewModifier.swift @@ -22,8 +22,13 @@ public struct _ViewModifier_Content: View where Modifier: ViewModifier public let modifier: Modifier public let view: AnyView - public var body: Never { - neverBody("_ViewModifier_Content") + public init(modifier: Modifier, view: AnyView) { + self.modifier = modifier + self.view = view + } + + public var body: AnyView { + view } } @@ -35,6 +40,8 @@ public extension View { public extension ViewModifier where Body == Never { func body(content: Content) -> Body { - fatalError("\(self) is a primitive `ViewModifier`, you're not supposed to run `body(content:)`") + fatalError( + "\(Self.self) is a primitive `ViewModifier`, you're not supposed to run `body(content:)`" + ) } } diff --git a/Sources/TokamakCore/MountedViews/MountedApp.swift b/Sources/TokamakCore/MountedViews/MountedApp.swift index b74f3441..e491cd45 100644 --- a/Sources/TokamakCore/MountedViews/MountedApp.swift +++ b/Sources/TokamakCore/MountedViews/MountedApp.swift @@ -22,13 +22,18 @@ import Runtime // is the computed content of the specified `Scene`, instead of having child // `View`s final class MountedApp: MountedCompositeElement { - override func mount(before _: R.TargetType? = nil, with reconciler: StackReconciler) { + override func mount( + before _: R.TargetType? = nil, + on _: MountedElement? = nil, + with reconciler: StackReconciler + ) { // `App` elements have no siblings, hence the `before` argument is discarded. + // They also have no parents, so the `parent` argument is discarded as well. let childBody = reconciler.render(mountedApp: self) let child: MountedElement = mountChild(childBody) mountedChildren = [child] - child.mount(before: nil, with: reconciler) + child.mount(before: nil, on: self, with: reconciler) } override func unmount(with reconciler: StackReconciler) { @@ -36,7 +41,8 @@ final class MountedApp: MountedCompositeElement { } private func mountChild(_ childBody: _AnyScene) -> MountedElement { - let mountedScene: MountedScene = childBody.makeMountedScene(parentTarget, environmentValues) + let mountedScene: MountedScene = childBody + .makeMountedScene(parentTarget, environmentValues, self) if let title = mountedScene.title { // swiftlint:disable force_cast (app.type as! _TitledApp.Type)._setTitle(title) diff --git a/Sources/TokamakCore/MountedViews/MountedCompositeElement.swift b/Sources/TokamakCore/MountedViews/MountedCompositeElement.swift index 907258e6..85f81fa6 100644 --- a/Sources/TokamakCore/MountedViews/MountedCompositeElement.swift +++ b/Sources/TokamakCore/MountedViews/MountedCompositeElement.swift @@ -37,19 +37,34 @@ class MountedCompositeElement: MountedElement { */ var persistentSubscriptions = [AnyCancellable]() - init(_ app: A, _ parentTarget: R.TargetType, _ environmentValues: EnvironmentValues) { + init( + _ app: A, + _ parentTarget: R.TargetType, + _ environmentValues: EnvironmentValues, + _ parent: MountedElement? + ) { self.parentTarget = parentTarget - super.init(_AnyApp(app), environmentValues) + super.init(_AnyApp(app), environmentValues, parent) } - init(_ scene: _AnyScene, _ parentTarget: R.TargetType, _ environmentValues: EnvironmentValues) { + init( + _ scene: _AnyScene, + _ parentTarget: R.TargetType, + _ environmentValues: EnvironmentValues, + _ parent: MountedElement? + ) { self.parentTarget = parentTarget - super.init(scene, environmentValues) + super.init(scene, environmentValues, parent) } - init(_ view: AnyView, _ parentTarget: R.TargetType, _ environmentValues: EnvironmentValues) { + init( + _ view: AnyView, + _ parentTarget: R.TargetType, + _ environmentValues: EnvironmentValues, + _ parent: MountedElement? + ) { self.parentTarget = parentTarget - super.init(view, environmentValues) + super.init(view, environmentValues, parent) } } diff --git a/Sources/TokamakCore/MountedViews/MountedCompositeView.swift b/Sources/TokamakCore/MountedViews/MountedCompositeView.swift index 41e785e2..c0edfae2 100644 --- a/Sources/TokamakCore/MountedViews/MountedCompositeView.swift +++ b/Sources/TokamakCore/MountedViews/MountedCompositeView.swift @@ -19,12 +19,20 @@ import CombineShim import Runtime final class MountedCompositeView: MountedCompositeElement { - override func mount(before sibling: R.TargetType? = nil, with reconciler: StackReconciler) { + override func mount( + before sibling: R.TargetType? = nil, + on parent: MountedElement? = nil, + with reconciler: StackReconciler + ) { let childBody = reconciler.render(compositeView: self) - let child: MountedElement = childBody.makeMountedView(parentTarget, environmentValues) + let child: MountedElement = childBody.makeMountedView( + parentTarget, + environmentValues, + self + ) mountedChildren = [child] - child.mount(before: sibling, with: reconciler) + child.mount(before: sibling, on: self, with: reconciler) // `_TargetRef` is a composite view, so it's enough to check for it only here if var targetRef = view.view as? TargetRefType { @@ -41,12 +49,27 @@ final class MountedCompositeView: MountedCompositeElement { view.view = targetRef } - // FIXME: this has to be implemented in a render-specific way, otherwise it's equivalent to - // `_onMount` and `_onUnmount` at the moment, - // see https://github.com/swiftwasm/Tokamak/issues/175 for more details - if let appearanceAction = view.view as? AppearanceActionType { - appearanceAction.appear?() - } + reconciler.afterCurrentRender(perform: { [weak self] in + guard let self = self else { return } + + // FIXME: this has to be implemented in a render-specific way, otherwise it's equivalent to + // `_onMount` and `_onUnmount` at the moment, + // see https://github.com/swiftwasm/Tokamak/issues/175 for more details + if let appearanceAction = self.view.view as? AppearanceActionType { + appearanceAction.appear?() + } + + if let preferenceModifier = self.view.view as? _PreferenceWritingViewProtocol { + self.view = preferenceModifier.modifyPreferenceStore(&self.preferenceStore) + if let parent = parent { + parent.preferenceStore.merge(with: self.preferenceStore) + } + } + + if let preferenceReader = self.view.view as? _PreferenceReadingViewProtocol { + preferenceReader.preferenceStore(self.preferenceStore) + } + }) } override func unmount(with reconciler: StackReconciler) { @@ -67,7 +90,7 @@ final class MountedCompositeView: MountedCompositeElement { $0.environmentValues = environmentValues $0.view = AnyView(element) }, - mountChild: { $0.makeMountedView(parentTarget, environmentValues) } + mountChild: { $0.makeMountedView(parentTarget, environmentValues, self) } ) } } diff --git a/Sources/TokamakCore/MountedViews/MountedElement.swift b/Sources/TokamakCore/MountedViews/MountedElement.swift index 1e06a4fe..0696991f 100644 --- a/Sources/TokamakCore/MountedViews/MountedElement.swift +++ b/Sources/TokamakCore/MountedViews/MountedElement.swift @@ -88,20 +88,31 @@ public class MountedElement { var mountedChildren = [MountedElement]() var environmentValues: EnvironmentValues - init(_ app: _AnyApp, _ environmentValues: EnvironmentValues) { + unowned var parent: MountedElement? + /// `didSet` on this field propagates the preference changes up the view tree. + var preferenceStore: _PreferenceStore = .init() { + didSet { + parent?.preferenceStore.merge(with: preferenceStore) + } + } + + init(_ app: _AnyApp, _ environmentValues: EnvironmentValues, _ parent: MountedElement?) { element = .app(app) + self.parent = parent self.environmentValues = environmentValues updateEnvironment() } - init(_ scene: _AnyScene, _ environmentValues: EnvironmentValues) { + init(_ scene: _AnyScene, _ environmentValues: EnvironmentValues, _ parent: MountedElement?) { element = .scene(scene) + self.parent = parent self.environmentValues = environmentValues updateEnvironment() } - init(_ view: AnyView, _ environmentValues: EnvironmentValues) { + init(_ view: AnyView, _ environmentValues: EnvironmentValues, _ parent: MountedElement?) { element = .view(view) + self.parent = parent self.environmentValues = environmentValues updateEnvironment() } @@ -122,7 +133,11 @@ public class MountedElement { return info } - func mount(before sibling: R.TargetType? = nil, with reconciler: StackReconciler) { + func mount( + before sibling: R.TargetType? = nil, + on parent: MountedElement? = nil, + with reconciler: StackReconciler + ) { fatalError("implement \(#function) in subclass") } @@ -217,14 +232,15 @@ extension TypeInfo { extension AnyView { func makeMountedView( _ parentTarget: R.TargetType, - _ environmentValues: EnvironmentValues + _ environmentValues: EnvironmentValues, + _ parent: MountedElement? ) -> MountedElement { if type == EmptyView.self { - return MountedEmptyView(self, environmentValues) + return MountedEmptyView(self, environmentValues, parent) } else if bodyType == Never.self && !(type is ViewDeferredToRenderer.Type) { - return MountedHostView(self, parentTarget, environmentValues) + return MountedHostView(self, parentTarget, environmentValues, parent) } else { - return MountedCompositeView(self, parentTarget, environmentValues) + return MountedCompositeView(self, parentTarget, environmentValues, parent) } } } diff --git a/Sources/TokamakCore/MountedViews/MountedEmptyView.swift b/Sources/TokamakCore/MountedViews/MountedEmptyView.swift index aef6940a..f070e798 100644 --- a/Sources/TokamakCore/MountedViews/MountedEmptyView.swift +++ b/Sources/TokamakCore/MountedViews/MountedEmptyView.swift @@ -16,7 +16,11 @@ // final class MountedEmptyView: MountedElement { - override func mount(before sibling: R.TargetType? = nil, with reconciler: StackReconciler) {} + override func mount( + before sibling: R.TargetType? = nil, + on parent: MountedElement? = nil, + with reconciler: StackReconciler + ) {} override func unmount(with reconciler: StackReconciler) {} diff --git a/Sources/TokamakCore/MountedViews/MountedHostView.swift b/Sources/TokamakCore/MountedViews/MountedHostView.swift index df9c5e32..7dfa41d6 100644 --- a/Sources/TokamakCore/MountedViews/MountedHostView.swift +++ b/Sources/TokamakCore/MountedViews/MountedHostView.swift @@ -31,13 +31,22 @@ public final class MountedHostView: MountedElement { /// Target of this host view supplied by a renderer after mounting has completed. private(set) var target: R.TargetType? - init(_ view: AnyView, _ parentTarget: R.TargetType, _ environmentValues: EnvironmentValues) { + init( + _ view: AnyView, + _ parentTarget: R.TargetType, + _ environmentValues: EnvironmentValues, + _ parent: MountedElement? + ) { self.parentTarget = parentTarget - super.init(view, environmentValues) + super.init(view, environmentValues, parent) } - override func mount(before sibling: R.TargetType? = nil, with reconciler: StackReconciler) { + override func mount( + before sibling: R.TargetType? = nil, + on parent: MountedElement? = nil, + with reconciler: StackReconciler + ) { guard let target = reconciler.renderer?.mountTarget( before: sibling, to: parentTarget, @@ -50,7 +59,7 @@ public final class MountedHostView: MountedElement { guard !view.children.isEmpty else { return } mountedChildren = view.children.map { - $0.makeMountedView(target, environmentValues) + $0.makeMountedView(target, environmentValues, self) } /* Remember that `GroupView`s are always "flattened", their `target` instances are targets of @@ -59,7 +68,9 @@ public final class MountedHostView: MountedElement { `GroupView`. */ let isGroupView = view.type is GroupView.Type - mountedChildren.forEach { $0.mount(before: isGroupView ? sibling : nil, with: reconciler) } + mountedChildren.forEach { + $0.mount(before: isGroupView ? sibling : nil, on: self, with: reconciler) + } } override func unmount(with reconciler: StackReconciler) { @@ -92,8 +103,8 @@ public final class MountedHostView: MountedElement { // if no existing children then mount all new children case (true, false): - mountedChildren = childrenViews.map { $0.makeMountedView(target, environmentValues) } - mountedChildren.forEach { $0.mount(with: reconciler) } + mountedChildren = childrenViews.map { $0.makeMountedView(target, environmentValues, self) } + mountedChildren.forEach { $0.mount(on: self, with: reconciler) } // if both arrays have items then reconcile by types and keys case (false, false): @@ -115,8 +126,8 @@ public final class MountedHostView: MountedElement { as a "cursor" sibling when mounting. Only then we can dispose of the old mounted child by unmounting it. */ - newChild = childView.makeMountedView(target, environmentValues) - newChild.mount(before: mountedChild.firstDescendantTarget, with: reconciler) + newChild = childView.makeMountedView(target, environmentValues, self) + newChild.mount(before: mountedChild.firstDescendantTarget, on: self, with: reconciler) mountedChild.unmount(with: reconciler) } newChildren.append(newChild) @@ -135,8 +146,8 @@ public final class MountedHostView: MountedElement { // mount remaining views for firstChild in childrenViews { let newChild: MountedElement = - firstChild.makeMountedView(target, environmentValues) - newChild.mount(with: reconciler) + firstChild.makeMountedView(target, environmentValues, self) + newChild.mount(on: self, with: reconciler) newChildren.append(newChild) } } diff --git a/Sources/TokamakCore/MountedViews/MountedScene.swift b/Sources/TokamakCore/MountedViews/MountedScene.swift index caa85a35..b1327e3c 100644 --- a/Sources/TokamakCore/MountedViews/MountedScene.swift +++ b/Sources/TokamakCore/MountedViews/MountedScene.swift @@ -22,19 +22,25 @@ final class MountedScene: MountedCompositeElement { _ title: String?, _ children: [MountedElement], _ parentTarget: R.TargetType, - _ environmentValues: EnvironmentValues + _ environmentValues: EnvironmentValues, + _ parent: MountedElement? ) { self.title = title - super.init(scene, parentTarget, environmentValues) + super.init(scene, parentTarget, environmentValues, parent) mountedChildren = children } - override func mount(before sibling: R.TargetType? = nil, with reconciler: StackReconciler) { + override func mount( + before sibling: R.TargetType? = nil, + on parent: MountedElement? = nil, + with reconciler: StackReconciler + ) { let childBody = reconciler.render(mountedScene: self) - let child: MountedElement = childBody.makeMountedElement(parentTarget, environmentValues) + let child: MountedElement = childBody + .makeMountedElement(parentTarget, environmentValues, self) mountedChildren = [child] - child.mount(before: sibling, with: reconciler) + child.mount(before: sibling, on: self, with: reconciler) } override func unmount(with reconciler: StackReconciler) { @@ -56,7 +62,7 @@ final class MountedScene: MountedCompositeElement { $0.view = AnyView(view) } }, - mountChild: { $0.makeMountedElement(parentTarget, environmentValues) } + mountChild: { $0.makeMountedElement(parentTarget, environmentValues, self) } ) } } @@ -73,13 +79,14 @@ extension _AnyScene.BodyResult { func makeMountedElement( _ parentTarget: R.TargetType, - _ environmentValues: EnvironmentValues + _ environmentValues: EnvironmentValues, + _ parent: MountedElement? ) -> MountedElement { switch self { case let .scene(scene): - return scene.makeMountedScene(parentTarget, environmentValues) + return scene.makeMountedScene(parentTarget, environmentValues, parent) case let .view(view): - return view.makeMountedView(parentTarget, environmentValues) + return view.makeMountedView(parentTarget, environmentValues, parent) } } } @@ -87,7 +94,8 @@ extension _AnyScene.BodyResult { extension _AnyScene { func makeMountedScene( _ parentTarget: R.TargetType, - _ environmentValues: EnvironmentValues + _ environmentValues: EnvironmentValues, + _ parent: MountedElement? ) -> MountedScene { var title: String? if let titledSelf = scene as? TitledScene, @@ -97,12 +105,16 @@ extension _AnyScene { } let children: [MountedElement] if let deferredScene = scene as? SceneDeferredToRenderer { - children = [deferredScene.deferredBody.makeMountedView(parentTarget, environmentValues)] + children = [ + deferredScene.deferredBody.makeMountedView(parentTarget, environmentValues, parent), + ] } else if let groupScene = scene as? GroupScene { - children = groupScene.children.map { $0.makeMountedScene(parentTarget, environmentValues) } + children = groupScene.children.map { + $0.makeMountedScene(parentTarget, environmentValues, parent) + } } else { children = [] } - return .init(self, title, children, parentTarget, environmentValues) + return .init(self, title, children, parentTarget, environmentValues, parent) } } diff --git a/Sources/TokamakCore/Preferences/PreferenceKey.swift b/Sources/TokamakCore/Preferences/PreferenceKey.swift new file mode 100644 index 00000000..55962a2f --- /dev/null +++ b/Sources/TokamakCore/Preferences/PreferenceKey.swift @@ -0,0 +1,120 @@ +// 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 Carson Katri on 11/26/20. +// + +public protocol PreferenceKey { + associatedtype Value + static var defaultValue: Value { get } + static func reduce(value: inout Value, nextValue: () -> Value) +} + +public extension PreferenceKey where Self.Value: ExpressibleByNilLiteral { + static var defaultValue: Value { nil } +} + +public struct _PreferenceValue where Key: PreferenceKey { + /// Every value the `Key` has had. + var valueList: [Key.Value] + /// The latest value. + public var value: Key.Value { + reduce(valueList) + } + + func reduce(_ values: [Key.Value]) -> Key.Value { + values.reduce(into: Key.defaultValue) { prev, next in + Key.reduce(value: &prev) { next } + } + } +} + +public struct _PreferenceStore { + /// The backing values of the `_PreferenceStore`. + private var values: [String: Any] + + public init(values: [String: Any] = [:]) { + self.values = values + } + + public func value(forKey key: Key.Type = Key.self) -> _PreferenceValue + where Key: PreferenceKey + { + values[String(describing: key)] as? _PreferenceValue + ?? _PreferenceValue(valueList: [Key.defaultValue]) + } + + public mutating func insert(_ value: Key.Value, forKey key: Key.Type = Key.self) + where Key: PreferenceKey + { + let previousValues = self.value(forKey: key).valueList + values[String(describing: key)] = _PreferenceValue(valueList: previousValues + [value]) + } + + public mutating func merge(with other: Self) { + self = merging(with: other) + } + + public func merging(with other: Self) -> Self { + var result = values + for (key, value) in other.values { + result[key] = value + } + return .init(values: result) + } +} + +/// A protocol that allows a `View` to read values from the current `_PreferenceStore`. +/// The key difference between `_PreferenceReadingViewProtocol` and +/// `_PreferenceWritingViewProtocol` is that `_PreferenceReadingViewProtocol` +/// calls `preferenceStore` during the current render, and `_PreferenceWritingViewProtocol` +/// waits until the current render finishes. +public protocol _PreferenceReadingViewProtocol { + func preferenceStore(_ preferenceStore: _PreferenceStore) +} + +/// A protocol that allows a `View` to modify values from the current `_PreferenceStore`. +public protocol _PreferenceWritingViewProtocol { + func modifyPreferenceStore(_ preferenceStore: inout _PreferenceStore) -> AnyView +} + +/// A protocol that allows a `ViewModifier` to modify values from the current `_PreferenceStore`. +public protocol _PreferenceWritingModifierProtocol: ViewModifier + where Body == AnyView +{ + func body(_ content: Self.Content, with preferenceStore: inout _PreferenceStore) -> AnyView +} + +public extension _PreferenceWritingViewProtocol where Self: View { + var body: Never { + neverBody(String(describing: Self.self)) + } +} + +public extension _PreferenceWritingModifierProtocol { + func body(content: Content) -> AnyView { + content.view + } +} + +extension ModifiedContent: _PreferenceWritingViewProtocol + where Content: View, Modifier: _PreferenceWritingModifierProtocol +{ + public func modifyPreferenceStore(_ preferenceStore: inout _PreferenceStore) -> AnyView { + AnyView( + modifier + .body(.init(modifier: modifier, view: AnyView(content)), with: &preferenceStore) + ) + } +} diff --git a/Sources/TokamakCore/Preferences/_PreferenceActionModifier.swift b/Sources/TokamakCore/Preferences/_PreferenceActionModifier.swift new file mode 100644 index 00000000..0356ee41 --- /dev/null +++ b/Sources/TokamakCore/Preferences/_PreferenceActionModifier.swift @@ -0,0 +1,45 @@ +// 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 Carson Katri on 11/26/20. +// + +public struct _PreferenceActionModifier: _PreferenceWritingModifierProtocol + where Key: PreferenceKey, Key.Value: Equatable +{ + public let action: (Key.Value) -> () + public init(action: @escaping (Key.Value) -> Swift.Void) { + self.action = action + } + + public func body(_ content: Content, with preferenceStore: inout _PreferenceStore) -> AnyView { + let value = preferenceStore.value(forKey: Key.self) + let previousValue = value.reduce(value.valueList.dropLast()) + if previousValue != value.value { + action(value.value) + } + return content.view + } +} + +public extension View { + func onPreferenceChange( + _ key: K.Type = K.self, + perform action: @escaping (K.Value) -> () + ) -> some View + where K: PreferenceKey, K.Value: Equatable + { + modifier(_PreferenceActionModifier(action: action)) + } +} diff --git a/Sources/TokamakCore/Preferences/_PreferenceReadingView.swift b/Sources/TokamakCore/Preferences/_PreferenceReadingView.swift new file mode 100644 index 00000000..227b2e9f --- /dev/null +++ b/Sources/TokamakCore/Preferences/_PreferenceReadingView.swift @@ -0,0 +1,69 @@ +// 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 Carson Katri on 11/26/20. +// + +/// Delays the retrieval of a `PreferenceKey.Value` by passing the `_PreferenceValue` to a build +/// function. +public struct _DelayedPreferenceView: View, _PreferenceReadingViewProtocol + where Key: PreferenceKey, Content: View +{ + @State private var resolvedValue: _PreferenceValue = _PreferenceValue( + valueList: [Key.defaultValue] + ) + public let transform: (_PreferenceValue) -> Content + + public init(transform: @escaping (_PreferenceValue) -> Content) { + self.transform = transform + } + + public func preferenceStore(_ preferenceStore: _PreferenceStore) { + resolvedValue = preferenceStore.value(forKey: Key.self) + } + + public var body: some View { + transform(resolvedValue) + } +} + +public extension PreferenceKey { + static func _delay( + _ transform: @escaping (_PreferenceValue) -> T + ) -> some View + where T: View + { + _DelayedPreferenceView(transform: transform) + } +} + +public extension View { + func overlayPreferenceValue( + _ key: Key.Type = Key.self, + @ViewBuilder _ transform: @escaping (Key.Value) -> T + ) -> some View + where Key: PreferenceKey, T: View + { + Key._delay { self.overlay(transform($0.value)) } + } + + func backgroundPreferenceValue( + _ key: Key.Type = Key.self, + @ViewBuilder _ transform: @escaping (Key.Value) -> T + ) -> some View + where Key: PreferenceKey, T: View + { + Key._delay { self.background(transform($0.value)) } + } +} diff --git a/Sources/TokamakCore/Preferences/_PreferenceTransformModifier.swift b/Sources/TokamakCore/Preferences/_PreferenceTransformModifier.swift new file mode 100644 index 00000000..9fbc0ea3 --- /dev/null +++ b/Sources/TokamakCore/Preferences/_PreferenceTransformModifier.swift @@ -0,0 +1,48 @@ +// 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 Carson Katri on 11/26/20. +// + +/// Transforms a `PreferenceKey.Value`. +public struct _PreferenceTransformModifier: _PreferenceWritingModifierProtocol + where Key: PreferenceKey +{ + public let transform: (inout Key.Value) -> () + + public init( + key _: Key.Type = Key.self, + transform: @escaping (inout Key.Value) -> () + ) { + self.transform = transform + } + + public func body(_ content: Content, with preferenceStore: inout _PreferenceStore) -> AnyView { + var newValue = preferenceStore.value(forKey: Key.self).value + transform(&newValue) + preferenceStore.insert(newValue, forKey: Key.self) + return content.view + } +} + +public extension View { + func transformPreference( + _ key: K.Type = K.self, + _ callback: @escaping (inout K.Value) -> () + ) -> some View + where K: PreferenceKey + { + modifier(_PreferenceTransformModifier(transform: callback)) + } +} diff --git a/Sources/TokamakCore/Preferences/_PreferenceWritingModifier.swift b/Sources/TokamakCore/Preferences/_PreferenceWritingModifier.swift new file mode 100644 index 00000000..0eec368c --- /dev/null +++ b/Sources/TokamakCore/Preferences/_PreferenceWritingModifier.swift @@ -0,0 +1,44 @@ +// 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 Carson Katri on 11/26/20. +// + +public struct _PreferenceWritingModifier: _PreferenceWritingModifierProtocol + where Key: PreferenceKey +{ + public let value: Key.Value + public init(key: Key.Type = Key.self, value: Key.Value) { + self.value = value + } + + public func body(_ content: Content, with preferenceStore: inout _PreferenceStore) -> AnyView { + preferenceStore.insert(value, forKey: Key.self) + return content.view + } +} + +extension _PreferenceWritingModifier: Equatable where Key.Value: Equatable { + public static func == (a: Self, b: Self) -> Bool { + a.value == b.value + } +} + +public extension View { + func preference(key: K.Type = K.self, value: K.Value) -> some View + where K: PreferenceKey + { + modifier(_PreferenceWritingModifier(value: value)) + } +} diff --git a/Sources/TokamakCore/StackReconciler.swift b/Sources/TokamakCore/StackReconciler.swift index 69089240..fb6950ec 100644 --- a/Sources/TokamakCore/StackReconciler.swift +++ b/Sources/TokamakCore/StackReconciler.swift @@ -74,9 +74,9 @@ public final class StackReconciler { self.scheduler = scheduler rootTarget = target - rootElement = AnyView(view).makeMountedView(target, environment) + rootElement = AnyView(view).makeMountedView(target, environment, nil) - rootElement.mount(with: self) + performInitialMount() } public init( @@ -90,15 +90,20 @@ public final class StackReconciler { self.scheduler = scheduler rootTarget = target - rootElement = MountedApp(app, target, environment) + rootElement = MountedApp(app, target, environment, nil) - rootElement.mount(with: self) + performInitialMount() if let mountedApp = rootElement as? MountedApp { setupPersistentSubscription(for: app._phasePublisher, to: \.scenePhase, of: mountedApp) setupPersistentSubscription(for: app._colorSchemePublisher, to: \.colorScheme, of: mountedApp) } } + private func performInitialMount() { + rootElement.mount(with: self) + performPostrenderCallbacks() + } + private func queueStorageUpdate( for mountedElement: MountedCompositeElement, id: Int, @@ -108,7 +113,7 @@ public final class StackReconciler { queueUpdate(for: mountedElement) } - private func queueUpdate(for mountedElement: MountedCompositeElement) { + internal func queueUpdate(for mountedElement: MountedCompositeElement) { let shouldSchedule = queuedRerenders.isEmpty queuedRerenders.insert(mountedElement) @@ -123,6 +128,7 @@ public final class StackReconciler { } queuedRerenders.removeAll() + performPostrenderCallbacks() } private func setupStorage( @@ -278,4 +284,14 @@ public final class StackReconciler { } } } + + private var queuedPostrenderCallbacks = [() -> ()]() + func afterCurrentRender(perform callback: @escaping () -> ()) { + queuedPostrenderCallbacks.append(callback) + } + + private func performPostrenderCallbacks() { + queuedPostrenderCallbacks.forEach { $0() } + queuedPostrenderCallbacks.removeAll() + } } diff --git a/Sources/TokamakCore/Tokens/Color.swift b/Sources/TokamakCore/Tokens/Color.swift index 07d8b53a..ac35b10b 100644 --- a/Sources/TokamakCore/Tokens/Color.swift +++ b/Sources/TokamakCore/Tokens/Color.swift @@ -39,7 +39,7 @@ public protocol AnyColorBoxDeferredToRenderer: AnyColorBox { func deferredResolve(in environment: EnvironmentValues) -> AnyColorBox.ResolvedValue } -public class AnyColorBox: AnyTokenBox { +public class AnyColorBox: AnyTokenBox, Equatable { public struct _RGBA: Hashable, Equatable { public let red: Double public let green: Double @@ -61,7 +61,15 @@ public class AnyColorBox: AnyTokenBox { } } - public static func == (lhs: AnyColorBox, rhs: AnyColorBox) -> Bool { false } + public static func == (lhs: AnyColorBox, rhs: AnyColorBox) -> Bool { + lhs.equals(rhs) + } + + /// We use a function separate from `==` so that subclasses can override the equality checks. + public func equals(_ other: AnyColorBox) -> Bool { + fatalError("implement \(#function) in subclass") + } + public func hash(into hasher: inout Hasher) { fatalError("implement \(#function) in subclass") } @@ -74,8 +82,10 @@ public class AnyColorBox: AnyTokenBox { public class _ConcreteColorBox: AnyColorBox { public let rgba: AnyColorBox._RGBA - public static func == (lhs: _ConcreteColorBox, rhs: _ConcreteColorBox) -> Bool { - lhs.rgba == rhs.rgba + override public func equals(_ other: AnyColorBox) -> Bool { + guard let other = other as? _ConcreteColorBox + else { return false } + return rgba == other.rgba } override public func hash(into hasher: inout Hasher) { @@ -94,10 +104,10 @@ public class _ConcreteColorBox: AnyColorBox { public class _EnvironmentDependentColorBox: AnyColorBox { public let resolver: (EnvironmentValues) -> Color - public static func == (lhs: _EnvironmentDependentColorBox, - rhs: _EnvironmentDependentColorBox) -> Bool - { - lhs.resolver(EnvironmentValues()) == rhs.resolver(EnvironmentValues()) + override public func equals(_ other: AnyColorBox) -> Bool { + guard let other = other as? _EnvironmentDependentColorBox + else { return false } + return resolver(EnvironmentValues()) == other.resolver(EnvironmentValues()) } override public func hash(into hasher: inout Hasher) { @@ -113,8 +123,8 @@ public class _EnvironmentDependentColorBox: AnyColorBox { } } -public class _SystemColorBox: AnyColorBox { - public enum SystemColor: Equatable, Hashable { +public class _SystemColorBox: AnyColorBox, CustomStringConvertible { + public enum SystemColor: String, Equatable, Hashable { case clear case black case white @@ -130,10 +140,16 @@ public class _SystemColorBox: AnyColorBox { case secondary } + public var description: String { + value.rawValue + } + public let value: SystemColor - public static func == (lhs: _SystemColorBox, rhs: _SystemColorBox) -> Bool { - lhs.value == rhs.value + override public func equals(_ other: AnyColorBox) -> Bool { + guard let other = other as? _SystemColorBox + else { return false } + return value == other.value } override public func hash(into hasher: inout Hasher) { @@ -252,6 +268,16 @@ public extension Color { } } +extension Color: CustomStringConvertible { + public var description: String { + if let providerDescription = provider as? CustomStringConvertible { + return providerDescription.description + } else { + return String(describing: self) + } + } +} + public extension Color { private init(systemColor: _SystemColorBox.SystemColor) { self.init(_SystemColorBox(systemColor)) diff --git a/Sources/TokamakCore/Tokens/ColorScheme.swift b/Sources/TokamakCore/Tokens/ColorScheme.swift index cad7615a..3914fac7 100644 --- a/Sources/TokamakCore/Tokens/ColorScheme.swift +++ b/Sources/TokamakCore/Tokens/ColorScheme.swift @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -public enum ColorScheme: CaseIterable { +public enum ColorScheme: CaseIterable, Equatable { case dark case light } @@ -35,3 +35,16 @@ public extension View { environment(\.colorScheme, colorScheme) } } + +public struct PreferredColorSchemeKey: PreferenceKey { + public typealias Value = ColorScheme? + public static func reduce(value: inout Value, nextValue: () -> Value) { + value = nextValue() + } +} + +public extension View { + func preferredColorScheme(_ colorScheme: ColorScheme?) -> some View { + preference(key: PreferredColorSchemeKey.self, value: colorScheme) + } +} diff --git a/Sources/TokamakCore/Views/Navigation/NavigationLink.swift b/Sources/TokamakCore/Views/Navigation/NavigationLink.swift index 6f8cd8a5..219a11b5 100644 --- a/Sources/TokamakCore/Views/Navigation/NavigationLink.swift +++ b/Sources/TokamakCore/Views/Navigation/NavigationLink.swift @@ -89,7 +89,7 @@ public struct _NavigationLinkProxy where Label: View, Destin public var style: _AnyNavigationLinkStyle { subject.style } public var isSelected: Bool { - ObjectIdentifier(subject.destination) == ObjectIdentifier(subject.navigationContext.destination) + subject.destination === subject.navigationContext.destination } public func activate() { diff --git a/Sources/TokamakCore/Views/Navigation/NavigationView.swift b/Sources/TokamakCore/Views/Navigation/NavigationView.swift index 43a2188b..c33b9044 100644 --- a/Sources/TokamakCore/Views/Navigation/NavigationView.swift +++ b/Sources/TokamakCore/Views/Navigation/NavigationView.swift @@ -65,4 +65,9 @@ extension EnvironmentValues { } } -public let _navigationDestinationKey = \EnvironmentValues.navigationDestination +struct NavigationTitleKey: PreferenceKey { + typealias Value = AnyView? + static func reduce(value: inout AnyView?, nextValue: () -> AnyView?) { + value = nextValue() + } +} diff --git a/Sources/TokamakCore/Views/Text/Text.swift b/Sources/TokamakCore/Views/Text/Text.swift index b4881e0a..433dc78b 100644 --- a/Sources/TokamakCore/Views/Text/Text.swift +++ b/Sources/TokamakCore/Views/Text/Text.swift @@ -116,6 +116,10 @@ public extension Text { .init(storage: storage, modifiers: modifiers + [.font(font)]) } + func foregroundColor(_ color: Color?) -> Text { + .init(storage: storage, modifiers: modifiers + [.color(color)]) + } + func fontWeight(_ weight: Font.Weight?) -> Text { .init(storage: storage, modifiers: modifiers + [.weight(weight)]) } diff --git a/Sources/TokamakDOM/Core.swift b/Sources/TokamakDOM/Core.swift index 6b21cadf..cecdf4c1 100644 --- a/Sources/TokamakDOM/Core.swift +++ b/Sources/TokamakDOM/Core.swift @@ -22,6 +22,8 @@ import TokamakCore public typealias Environment = TokamakCore.Environment public typealias EnvironmentObject = TokamakCore.EnvironmentObject +public typealias PreferenceKey = TokamakCore.PreferenceKey + public typealias Binding = TokamakCore.Binding public typealias ObservableObject = TokamakCore.ObservableObject public typealias ObservedObject = TokamakCore.ObservedObject diff --git a/Sources/TokamakDOM/DOMRenderer.swift b/Sources/TokamakDOM/DOMRenderer.swift index aeec9151..9b39eb7a 100644 --- a/Sources/TokamakDOM/DOMRenderer.swift +++ b/Sources/TokamakDOM/DOMRenderer.swift @@ -157,7 +157,7 @@ final class DOMRenderer: Renderer { ) { defer { completion() } - guard let html = mapAnyView(host.view, transform: { (html: AnyHTML) in html }) + guard mapAnyView(host.view, transform: { (html: AnyHTML) in html }) != nil else { return } _ = parent.ref.removeChild!(target.ref) diff --git a/Sources/TokamakDemo/PreferenceKeyDemo.swift b/Sources/TokamakDemo/PreferenceKeyDemo.swift new file mode 100644 index 00000000..b8536e1a --- /dev/null +++ b/Sources/TokamakDemo/PreferenceKeyDemo.swift @@ -0,0 +1,152 @@ +// 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 Carson Katri on 11/26/20. +// + +import TokamakShim + +struct TestPreferenceKey: PreferenceKey { + static let defaultValue = Color.red + static func reduce(value: inout Color, nextValue: () -> Color) { + value = nextValue() + } +} + +@available(macOS 11, iOS 14, *) +struct PreferenceKeyDemo: View { + @State private var testKeyValue: Color = .yellow + @Environment(\.colorScheme) var colorScheme + + var body: some View { + VStack { + Group { + Text("Preferences are like reverse-environment values.") + Text( + """ + In this demo, the background color of each item \ + is set to the value of the PreferenceKey. + """ + ) + Text("Default color: red (this won't show on the innermost because it never 'changed').") + Text("Innermost child sets the color to blue.") + Text("One level up sets the color to green, and so on.") + + VStack { + Text("Root") + .padding(.bottom, 8) + SetColor(3, .purple) { + SetColor(2, .green) { + SetColor(1, .blue) + } + } + } + .padding() + .background(testKeyValue) + .onPreferenceChange(TestPreferenceKey.self) { + print("Value changed to \($0)") + testKeyValue = $0 + } + } + + Group { + Text("Preferences can also be accessed and used immediately via a background or overlay:") + Circle() + .frame(width: 25, height: 25) + .foregroundColor(testKeyValue) + .backgroundPreferenceValue(TestPreferenceKey.self) { + Circle() + .frame(width: 50, height: 50) + .foregroundColor($0) + } + Circle() + .frame(width: 50, height: 50) + .foregroundColor(testKeyValue) + .overlayPreferenceValue(TestPreferenceKey.self) { + Circle() + .frame(width: 25, height: 25) + .foregroundColor($0) + } + } + + Group { + Text( + """ + We can also transform the key. Here we perform several transformations and use an \ + `overlayPreferenceValue` + """ + ) + Text("1. Set the color to yellow") + Text("2. Transform if the color is yellow -> green") + Text("3. Transform if the color is green -> blue") + Text("4. Use the final color in an overlay.") + Circle() + .frame(width: 50, height: 50) + .foregroundColor(testKeyValue) + .preference(key: TestPreferenceKey.self, value: .yellow) + .transformPreference(TestPreferenceKey.self) { + print("Transforming \($0) ->? green") + if $0 == .yellow { + $0 = .green + } + } + .transformPreference(TestPreferenceKey.self) { + print("Transforming \($0) ->? blue") + if $0 == .green { + $0 = .blue + } + } + .overlayPreferenceValue(TestPreferenceKey.self) { newColor in + Circle() + .frame(width: 25, height: 25) + .foregroundColor(newColor) + } + } + } + } + + struct SetColor: View { + let level: Int + let color: Color + let content: Content + @State private var testKeyValue: Color = .yellow + + init(_ level: Int, _ color: Color, @ViewBuilder _ content: () -> Content) { + self.level = level + self.color = color + self.content = content() + } + + var body: some View { + VStack { + Text("Level \(level)") + .padding(.bottom, level == 1 ? 0 : 8) + content + } + .padding() + .background(testKeyValue) + .onPreferenceChange(TestPreferenceKey.self) { + testKeyValue = $0 + } + .preference(key: TestPreferenceKey.self, value: color) + } + } +} + +@available(macOS 11, iOS 14, *) +extension PreferenceKeyDemo.SetColor where Content == EmptyView { + init(_ level: Int, _ color: Color) { + self.init(level, color) { EmptyView() } + } +} diff --git a/Sources/TokamakDemo/TokamakDemo.swift b/Sources/TokamakDemo/TokamakDemo.swift index 23a3756f..976f41f7 100644 --- a/Sources/TokamakDemo/TokamakDemo.swift +++ b/Sources/TokamakDemo/TokamakDemo.swift @@ -129,6 +129,9 @@ struct TokamakDemoView: View { Section(header: Text("Misc")) { NavItem("Path", destination: PathDemo()) NavItem("Environment", destination: EnvironmentDemo().font(.system(size: 8))) + if #available(macOS 11.0, iOS 14.0, *) { + NavItem("Preferences", destination: PreferenceKeyDemo()) + } NavItem("Color", destination: ColorDemo()) if #available(OSX 11.0, iOS 14.0, *) { NavItem("AppStorage", destination: AppStorageDemo()) diff --git a/Sources/TokamakStaticHTML/Modifiers/ModifiedContent.swift b/Sources/TokamakStaticHTML/Modifiers/ModifiedContent.swift index 14a58884..558a6a77 100644 --- a/Sources/TokamakStaticHTML/Modifiers/ModifiedContent.swift +++ b/Sources/TokamakStaticHTML/Modifiers/ModifiedContent.swift @@ -30,7 +30,7 @@ extension ModifiedContent: AnyModifiedContent where Modifier: DOMViewModifier, C } } -extension ModifiedContent: ViewDeferredToRenderer where Content: View { +extension ModifiedContent: ViewDeferredToRenderer where Content: View, Modifier: ViewModifier { public var deferredBody: AnyView { if let domModifier = modifier as? DOMViewModifier { if let adjacentModifier = content as? AnyModifiedContent, @@ -51,8 +51,10 @@ extension ModifiedContent: ViewDeferredToRenderer where Content: View { content }) } - } else { + } else if Modifier.Body.self == Never.self { return AnyView(content) + } else { + return AnyView(modifier.body(content: .init(modifier: modifier, view: AnyView(content)))) } } } diff --git a/Sources/TokamakStaticHTML/Views/Navigation/NavigationView.swift b/Sources/TokamakStaticHTML/Views/Navigation/NavigationView.swift index 5262c33e..afc32943 100644 --- a/Sources/TokamakStaticHTML/Views/Navigation/NavigationView.swift +++ b/Sources/TokamakStaticHTML/Views/Navigation/NavigationView.swift @@ -16,14 +16,15 @@ import TokamakCore extension NavigationView: ViewDeferredToRenderer { public var deferredBody: AnyView { - AnyView(HTML("div", [ + let proxy = _NavigationViewProxy(self) + return AnyView(HTML("div", [ "class": "_tokamak-navigationview", ]) { - _NavigationViewProxy(self).content + proxy.content HTML("div", [ "class": "_tokamak-navigationview-content", ]) { - _NavigationViewProxy(self).destination + proxy.destination } }) }