Add Preferences (#307)

This adds the `PreferenceKey` protocol and related modifiers.

* Initial PreferenceKey implementation

* Don't send default value to match SwiftUI behavior

* Add CustomDebugStringConvertible conformance to Color

* PR fixes

* Fix onAppear and preference modification calls

* Attempt macOS build fix

* Fix <background/overlay>PreferenceValue

* Implement/revise transformPreference

* Fix linter warnings, apply SwiftFormat

Co-authored-by: Max Desiatov <max@desiatov.com>
This commit is contained in:
Carson Katri 2020-12-04 06:19:14 -05:00 committed by GitHub
parent 2e8e458b9c
commit 9d347f49f3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 759 additions and 92 deletions

View File

@ -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 = "<group>"; };
B5C76E4924C73ED4003EABB2 /* AppStorageDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppStorageDemo.swift; sourceTree = "<group>"; };
B5DBA22A24D509B4003D3347 /* RedactDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RedactDemo.swift; sourceTree = "<group>"; };
B5F2BE022571443D00FB3653 /* PreferenceKeyDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferenceKeyDemo.swift; sourceTree = "<group>"; };
D1B4228E24B3B9BB00682F74 /* ListDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListDemo.swift; sourceTree = "<group>"; };
D1B4228F24B3B9BB00682F74 /* OutlineGroupDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutlineGroupDemo.swift; sourceTree = "<group>"; };
D1C726F224CB63C6003B576D /* ButtonStyleDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ButtonStyleDemo.swift; sourceTree = "<group>"; };
@ -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 */,

View File

@ -7,7 +7,7 @@ import PackageDescription
let package = Package(
name: "Tokamak",
platforms: [
.macOS(.v10_15),
.macOS(.v11),
.iOS(.v13),
],
products: [

View File

@ -30,9 +30,7 @@ public struct _EnvironmentKeyWritingModifier<Value>: 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

View File

@ -13,13 +13,27 @@
// limitations under the License.
public extension View {
// FIXME: Implement
func navigationBarTitle<S>(_ title: S) -> some View where S: StringProtocol {
self
@available(*, deprecated, renamed: "navigationTitle(_:)")
func navigationBarTitle(_ title: Text) -> some View {
navigationTitle(title)
}
// FIXME: Implement
func navigationTitle<S>(_ title: S) -> some View where S: StringProtocol {
self
@available(*, deprecated, renamed: "navigationTitle(_:)")
func navigationBarTitle<S: StringProtocol>(_ title: S) -> some View {
navigationTitle(title)
}
func navigationTitle(_ title: Text) -> some View {
navigationTitle { title }
}
func navigationTitle<S: StringProtocol>(_ titleKey: S) -> some View {
navigationTitle(Text(titleKey))
}
func navigationTitle<V>(@ViewBuilder _ title: () -> V) -> some View
where V: View
{
preference(key: NavigationTitleKey.self, value: AnyView(title()))
}
}

View File

@ -28,7 +28,11 @@ public struct _BackgroundModifier<Background>: 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<Overlay>: ViewModifier, EnvironmentReader
}
public func body(content: Content) -> some View {
// FIXME: Clip to content shape.
ZStack(alignment: alignment) {
content
overlay

View File

@ -22,8 +22,13 @@ public struct _ViewModifier_Content<Modifier>: 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:)`"
)
}
}

View File

@ -22,13 +22,18 @@ import Runtime
// is the computed content of the specified `Scene`, instead of having child
// `View`s
final class MountedApp<R: Renderer>: MountedCompositeElement<R> {
override func mount(before _: R.TargetType? = nil, with reconciler: StackReconciler<R>) {
override func mount(
before _: R.TargetType? = nil,
on _: MountedElement<R>? = nil,
with reconciler: StackReconciler<R>
) {
// `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<R> = mountChild(childBody)
mountedChildren = [child]
child.mount(before: nil, with: reconciler)
child.mount(before: nil, on: self, with: reconciler)
}
override func unmount(with reconciler: StackReconciler<R>) {
@ -36,7 +41,8 @@ final class MountedApp<R: Renderer>: MountedCompositeElement<R> {
}
private func mountChild(_ childBody: _AnyScene) -> MountedElement<R> {
let mountedScene: MountedScene<R> = childBody.makeMountedScene(parentTarget, environmentValues)
let mountedScene: MountedScene<R> = childBody
.makeMountedScene(parentTarget, environmentValues, self)
if let title = mountedScene.title {
// swiftlint:disable force_cast
(app.type as! _TitledApp.Type)._setTitle(title)

View File

@ -37,19 +37,34 @@ class MountedCompositeElement<R: Renderer>: MountedElement<R> {
*/
var persistentSubscriptions = [AnyCancellable]()
init<A: App>(_ app: A, _ parentTarget: R.TargetType, _ environmentValues: EnvironmentValues) {
init<A: App>(
_ app: A,
_ parentTarget: R.TargetType,
_ environmentValues: EnvironmentValues,
_ parent: MountedElement<R>?
) {
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<R>?
) {
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<R>?
) {
self.parentTarget = parentTarget
super.init(view, environmentValues)
super.init(view, environmentValues, parent)
}
}

View File

@ -19,12 +19,20 @@ import CombineShim
import Runtime
final class MountedCompositeView<R: Renderer>: MountedCompositeElement<R> {
override func mount(before sibling: R.TargetType? = nil, with reconciler: StackReconciler<R>) {
override func mount(
before sibling: R.TargetType? = nil,
on parent: MountedElement<R>? = nil,
with reconciler: StackReconciler<R>
) {
let childBody = reconciler.render(compositeView: self)
let child: MountedElement<R> = childBody.makeMountedView(parentTarget, environmentValues)
let child: MountedElement<R> = 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<R: Renderer>: MountedCompositeElement<R> {
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<R>) {
@ -67,7 +90,7 @@ final class MountedCompositeView<R: Renderer>: MountedCompositeElement<R> {
$0.environmentValues = environmentValues
$0.view = AnyView(element)
},
mountChild: { $0.makeMountedView(parentTarget, environmentValues) }
mountChild: { $0.makeMountedView(parentTarget, environmentValues, self) }
)
}
}

View File

@ -88,20 +88,31 @@ public class MountedElement<R: Renderer> {
var mountedChildren = [MountedElement<R>]()
var environmentValues: EnvironmentValues
init(_ app: _AnyApp, _ environmentValues: EnvironmentValues) {
unowned var parent: MountedElement<R>?
/// `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<R>?) {
element = .app(app)
self.parent = parent
self.environmentValues = environmentValues
updateEnvironment()
}
init(_ scene: _AnyScene, _ environmentValues: EnvironmentValues) {
init(_ scene: _AnyScene, _ environmentValues: EnvironmentValues, _ parent: MountedElement<R>?) {
element = .scene(scene)
self.parent = parent
self.environmentValues = environmentValues
updateEnvironment()
}
init(_ view: AnyView, _ environmentValues: EnvironmentValues) {
init(_ view: AnyView, _ environmentValues: EnvironmentValues, _ parent: MountedElement<R>?) {
element = .view(view)
self.parent = parent
self.environmentValues = environmentValues
updateEnvironment()
}
@ -122,7 +133,11 @@ public class MountedElement<R: Renderer> {
return info
}
func mount(before sibling: R.TargetType? = nil, with reconciler: StackReconciler<R>) {
func mount(
before sibling: R.TargetType? = nil,
on parent: MountedElement<R>? = nil,
with reconciler: StackReconciler<R>
) {
fatalError("implement \(#function) in subclass")
}
@ -217,14 +232,15 @@ extension TypeInfo {
extension AnyView {
func makeMountedView<R: Renderer>(
_ parentTarget: R.TargetType,
_ environmentValues: EnvironmentValues
_ environmentValues: EnvironmentValues,
_ parent: MountedElement<R>?
) -> MountedElement<R> {
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)
}
}
}

View File

@ -16,7 +16,11 @@
//
final class MountedEmptyView<R: Renderer>: MountedElement<R> {
override func mount(before sibling: R.TargetType? = nil, with reconciler: StackReconciler<R>) {}
override func mount(
before sibling: R.TargetType? = nil,
on parent: MountedElement<R>? = nil,
with reconciler: StackReconciler<R>
) {}
override func unmount(with reconciler: StackReconciler<R>) {}

View File

@ -31,13 +31,22 @@ public final class MountedHostView<R: Renderer>: MountedElement<R> {
/// 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<R>?
) {
self.parentTarget = parentTarget
super.init(view, environmentValues)
super.init(view, environmentValues, parent)
}
override func mount(before sibling: R.TargetType? = nil, with reconciler: StackReconciler<R>) {
override func mount(
before sibling: R.TargetType? = nil,
on parent: MountedElement<R>? = nil,
with reconciler: StackReconciler<R>
) {
guard let target = reconciler.renderer?.mountTarget(
before: sibling,
to: parentTarget,
@ -50,7 +59,7 @@ public final class MountedHostView<R: Renderer>: MountedElement<R> {
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<R: Renderer>: MountedElement<R> {
`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<R>) {
@ -92,8 +103,8 @@ public final class MountedHostView<R: Renderer>: MountedElement<R> {
// 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<R: Renderer>: MountedElement<R> {
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<R: Renderer>: MountedElement<R> {
// mount remaining views
for firstChild in childrenViews {
let newChild: MountedElement<R> =
firstChild.makeMountedView(target, environmentValues)
newChild.mount(with: reconciler)
firstChild.makeMountedView(target, environmentValues, self)
newChild.mount(on: self, with: reconciler)
newChildren.append(newChild)
}
}

View File

@ -22,19 +22,25 @@ final class MountedScene<R: Renderer>: MountedCompositeElement<R> {
_ title: String?,
_ children: [MountedElement<R>],
_ parentTarget: R.TargetType,
_ environmentValues: EnvironmentValues
_ environmentValues: EnvironmentValues,
_ parent: MountedElement<R>?
) {
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<R>) {
override func mount(
before sibling: R.TargetType? = nil,
on parent: MountedElement<R>? = nil,
with reconciler: StackReconciler<R>
) {
let childBody = reconciler.render(mountedScene: self)
let child: MountedElement<R> = childBody.makeMountedElement(parentTarget, environmentValues)
let child: MountedElement<R> = 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<R>) {
@ -56,7 +62,7 @@ final class MountedScene<R: Renderer>: MountedCompositeElement<R> {
$0.view = AnyView(view)
}
},
mountChild: { $0.makeMountedElement(parentTarget, environmentValues) }
mountChild: { $0.makeMountedElement(parentTarget, environmentValues, self) }
)
}
}
@ -73,13 +79,14 @@ extension _AnyScene.BodyResult {
func makeMountedElement<R: Renderer>(
_ parentTarget: R.TargetType,
_ environmentValues: EnvironmentValues
_ environmentValues: EnvironmentValues,
_ parent: MountedElement<R>?
) -> MountedElement<R> {
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<R: Renderer>(
_ parentTarget: R.TargetType,
_ environmentValues: EnvironmentValues
_ environmentValues: EnvironmentValues,
_ parent: MountedElement<R>?
) -> MountedScene<R> {
var title: String?
if let titledSelf = scene as? TitledScene,
@ -97,12 +105,16 @@ extension _AnyScene {
}
let children: [MountedElement<R>]
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)
}
}

View File

@ -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<Key> 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<Key>(forKey key: Key.Type = Key.self) -> _PreferenceValue<Key>
where Key: PreferenceKey
{
values[String(describing: key)] as? _PreferenceValue<Key>
?? _PreferenceValue(valueList: [Key.defaultValue])
}
public mutating func insert<Key>(_ value: Key.Value, forKey key: Key.Type = Key.self)
where Key: PreferenceKey
{
let previousValues = self.value(forKey: key).valueList
values[String(describing: key)] = _PreferenceValue<Key>(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)
)
}
}

View File

@ -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<Key>: _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<K>(
_ key: K.Type = K.self,
perform action: @escaping (K.Value) -> ()
) -> some View
where K: PreferenceKey, K.Value: Equatable
{
modifier(_PreferenceActionModifier<K>(action: action))
}
}

View File

@ -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<Key, Content>: View, _PreferenceReadingViewProtocol
where Key: PreferenceKey, Content: View
{
@State private var resolvedValue: _PreferenceValue<Key> = _PreferenceValue(
valueList: [Key.defaultValue]
)
public let transform: (_PreferenceValue<Key>) -> Content
public init(transform: @escaping (_PreferenceValue<Key>) -> 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<T>(
_ transform: @escaping (_PreferenceValue<Self>) -> T
) -> some View
where T: View
{
_DelayedPreferenceView(transform: transform)
}
}
public extension View {
func overlayPreferenceValue<Key, T>(
_ 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, T>(
_ 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)) }
}
}

View File

@ -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<Key>: _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<K>(
_ key: K.Type = K.self,
_ callback: @escaping (inout K.Value) -> ()
) -> some View
where K: PreferenceKey
{
modifier(_PreferenceTransformModifier<K>(transform: callback))
}
}

View File

@ -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<Key>: _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<K>(key: K.Type = K.self, value: K.Value) -> some View
where K: PreferenceKey
{
modifier(_PreferenceWritingModifier<K>(value: value))
}
}

View File

@ -74,9 +74,9 @@ public final class StackReconciler<R: Renderer> {
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<A: App>(
@ -90,15 +90,20 @@ public final class StackReconciler<R: Renderer> {
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<R> {
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<R>,
id: Int,
@ -108,7 +113,7 @@ public final class StackReconciler<R: Renderer> {
queueUpdate(for: mountedElement)
}
private func queueUpdate(for mountedElement: MountedCompositeElement<R>) {
internal func queueUpdate(for mountedElement: MountedCompositeElement<R>) {
let shouldSchedule = queuedRerenders.isEmpty
queuedRerenders.insert(mountedElement)
@ -123,6 +128,7 @@ public final class StackReconciler<R: Renderer> {
}
queuedRerenders.removeAll()
performPostrenderCallbacks()
}
private func setupStorage(
@ -278,4 +284,14 @@ public final class StackReconciler<R: Renderer> {
}
}
}
private var queuedPostrenderCallbacks = [() -> ()]()
func afterCurrentRender(perform callback: @escaping () -> ()) {
queuedPostrenderCallbacks.append(callback)
}
private func performPostrenderCallbacks() {
queuedPostrenderCallbacks.forEach { $0() }
queuedPostrenderCallbacks.removeAll()
}
}

View File

@ -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))

View File

@ -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)
}
}

View File

@ -89,7 +89,7 @@ public struct _NavigationLinkProxy<Label, Destination> 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() {

View File

@ -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()
}
}

View File

@ -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)])
}

View File

@ -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

View File

@ -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)

View File

@ -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<Content: View>: 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() }
}
}

View File

@ -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())

View File

@ -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))))
}
}
}

View File

@ -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
}
})
}