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:
parent
2e8e458b9c
commit
9d347f49f3
|
@ -41,6 +41,8 @@
|
||||||
B5C76E4B24C73ED5003EABB2 /* AppStorageDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C76E4924C73ED4003EABB2 /* AppStorageDemo.swift */; };
|
B5C76E4B24C73ED5003EABB2 /* AppStorageDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C76E4924C73ED4003EABB2 /* AppStorageDemo.swift */; };
|
||||||
B5DBA22B24D509B4003D3347 /* RedactDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5DBA22A24D509B4003D3347 /* RedactDemo.swift */; };
|
B5DBA22B24D509B4003D3347 /* RedactDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5DBA22A24D509B4003D3347 /* RedactDemo.swift */; };
|
||||||
B5DBA22C24D509B4003D3347 /* 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 */; };
|
D1B4229024B3B9BB00682F74 /* ListDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1B4228E24B3B9BB00682F74 /* ListDemo.swift */; };
|
||||||
D1B4229124B3B9BB00682F74 /* 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 */; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
D1C726F224CB63C6003B576D /* ButtonStyleDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ButtonStyleDemo.swift; sourceTree = "<group>"; };
|
||||||
|
@ -188,6 +191,7 @@
|
||||||
D1B4228F24B3B9BB00682F74 /* OutlineGroupDemo.swift */,
|
D1B4228F24B3B9BB00682F74 /* OutlineGroupDemo.swift */,
|
||||||
B51F214F24B920B400CF2583 /* PathDemo.swift */,
|
B51F214F24B920B400CF2583 /* PathDemo.swift */,
|
||||||
D1EE7EA624C0DD2100C0D127 /* PickerDemo.swift */,
|
D1EE7EA624C0DD2100C0D127 /* PickerDemo.swift */,
|
||||||
|
B5F2BE022571443D00FB3653 /* PreferenceKeyDemo.swift */,
|
||||||
B5DBA22A24D509B4003D3347 /* RedactDemo.swift */,
|
B5DBA22A24D509B4003D3347 /* RedactDemo.swift */,
|
||||||
3DCDE44324CA6AD400910F17 /* SidebarDemo.swift */,
|
3DCDE44324CA6AD400910F17 /* SidebarDemo.swift */,
|
||||||
8500293E24D2FF3E001A2E84 /* SliderDemo.swift */,
|
8500293E24D2FF3E001A2E84 /* SliderDemo.swift */,
|
||||||
|
@ -350,6 +354,7 @@
|
||||||
85ED18A324AD425E0085DFA0 /* SpacerDemo.swift in Sources */,
|
85ED18A324AD425E0085DFA0 /* SpacerDemo.swift in Sources */,
|
||||||
D1B4229024B3B9BB00682F74 /* ListDemo.swift in Sources */,
|
D1B4229024B3B9BB00682F74 /* ListDemo.swift in Sources */,
|
||||||
D1EE7EA724C0DD2100C0D127 /* PickerDemo.swift in Sources */,
|
D1EE7EA724C0DD2100C0D127 /* PickerDemo.swift in Sources */,
|
||||||
|
B5F2BE032571443D00FB3653 /* PreferenceKeyDemo.swift in Sources */,
|
||||||
8500293F24D2FF3E001A2E84 /* SliderDemo.swift in Sources */,
|
8500293F24D2FF3E001A2E84 /* SliderDemo.swift in Sources */,
|
||||||
85ED18A924AD425E0085DFA0 /* TokamakDemo.swift in Sources */,
|
85ED18A924AD425E0085DFA0 /* TokamakDemo.swift in Sources */,
|
||||||
B5C76E4A24C73ED5003EABB2 /* AppStorageDemo.swift in Sources */,
|
B5C76E4A24C73ED5003EABB2 /* AppStorageDemo.swift in Sources */,
|
||||||
|
@ -378,6 +383,7 @@
|
||||||
85ED18B024AD425E0085DFA0 /* EnvironmentDemo.swift in Sources */,
|
85ED18B024AD425E0085DFA0 /* EnvironmentDemo.swift in Sources */,
|
||||||
D1B4229124B3B9BB00682F74 /* ListDemo.swift in Sources */,
|
D1B4229124B3B9BB00682F74 /* ListDemo.swift in Sources */,
|
||||||
D1EE7EA824C0DD2100C0D127 /* PickerDemo.swift in Sources */,
|
D1EE7EA824C0DD2100C0D127 /* PickerDemo.swift in Sources */,
|
||||||
|
B5F2BE042571443D00FB3653 /* PreferenceKeyDemo.swift in Sources */,
|
||||||
8500294024D2FF3E001A2E84 /* SliderDemo.swift in Sources */,
|
8500294024D2FF3E001A2E84 /* SliderDemo.swift in Sources */,
|
||||||
85ED18B624AD42D70085DFA0 /* NSAppDelegate.swift in Sources */,
|
85ED18B624AD42D70085DFA0 /* NSAppDelegate.swift in Sources */,
|
||||||
B5C76E4B24C73ED5003EABB2 /* AppStorageDemo.swift in Sources */,
|
B5C76E4B24C73ED5003EABB2 /* AppStorageDemo.swift in Sources */,
|
||||||
|
|
|
@ -7,7 +7,7 @@ import PackageDescription
|
||||||
let package = Package(
|
let package = Package(
|
||||||
name: "Tokamak",
|
name: "Tokamak",
|
||||||
platforms: [
|
platforms: [
|
||||||
.macOS(.v10_15),
|
.macOS(.v11),
|
||||||
.iOS(.v13),
|
.iOS(.v13),
|
||||||
],
|
],
|
||||||
products: [
|
products: [
|
||||||
|
|
|
@ -30,9 +30,7 @@ public struct _EnvironmentKeyWritingModifier<Value>: ViewModifier, EnvironmentMo
|
||||||
self.value = value
|
self.value = value
|
||||||
}
|
}
|
||||||
|
|
||||||
public func body(content: Content) -> some View {
|
public typealias Body = Never
|
||||||
content
|
|
||||||
}
|
|
||||||
|
|
||||||
func modifyEnvironment(_ values: inout EnvironmentValues) {
|
func modifyEnvironment(_ values: inout EnvironmentValues) {
|
||||||
values[keyPath: keyPath] = value
|
values[keyPath: keyPath] = value
|
||||||
|
|
|
@ -13,13 +13,27 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
public extension View {
|
public extension View {
|
||||||
// FIXME: Implement
|
@available(*, deprecated, renamed: "navigationTitle(_:)")
|
||||||
func navigationBarTitle<S>(_ title: S) -> some View where S: StringProtocol {
|
func navigationBarTitle(_ title: Text) -> some View {
|
||||||
self
|
navigationTitle(title)
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: Implement
|
@available(*, deprecated, renamed: "navigationTitle(_:)")
|
||||||
func navigationTitle<S>(_ title: S) -> some View where S: StringProtocol {
|
func navigationBarTitle<S: StringProtocol>(_ title: S) -> some View {
|
||||||
self
|
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()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,11 @@ public struct _BackgroundModifier<Background>: ViewModifier, EnvironmentReader
|
||||||
}
|
}
|
||||||
|
|
||||||
public func body(content: Content) -> some View {
|
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) {
|
mutating func setContent(from values: EnvironmentValues) {
|
||||||
|
@ -67,6 +71,7 @@ public struct _OverlayModifier<Overlay>: ViewModifier, EnvironmentReader
|
||||||
}
|
}
|
||||||
|
|
||||||
public func body(content: Content) -> some View {
|
public func body(content: Content) -> some View {
|
||||||
|
// FIXME: Clip to content shape.
|
||||||
ZStack(alignment: alignment) {
|
ZStack(alignment: alignment) {
|
||||||
content
|
content
|
||||||
overlay
|
overlay
|
||||||
|
|
|
@ -22,8 +22,13 @@ public struct _ViewModifier_Content<Modifier>: View where Modifier: ViewModifier
|
||||||
public let modifier: Modifier
|
public let modifier: Modifier
|
||||||
public let view: AnyView
|
public let view: AnyView
|
||||||
|
|
||||||
public var body: Never {
|
public init(modifier: Modifier, view: AnyView) {
|
||||||
neverBody("_ViewModifier_Content")
|
self.modifier = modifier
|
||||||
|
self.view = view
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: AnyView {
|
||||||
|
view
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,6 +40,8 @@ public extension View {
|
||||||
|
|
||||||
public extension ViewModifier where Body == Never {
|
public extension ViewModifier where Body == Never {
|
||||||
func body(content: Content) -> Body {
|
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:)`"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,13 +22,18 @@ import Runtime
|
||||||
// is the computed content of the specified `Scene`, instead of having child
|
// is the computed content of the specified `Scene`, instead of having child
|
||||||
// `View`s
|
// `View`s
|
||||||
final class MountedApp<R: Renderer>: MountedCompositeElement<R> {
|
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.
|
// `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 childBody = reconciler.render(mountedApp: self)
|
||||||
|
|
||||||
let child: MountedElement<R> = mountChild(childBody)
|
let child: MountedElement<R> = mountChild(childBody)
|
||||||
mountedChildren = [child]
|
mountedChildren = [child]
|
||||||
child.mount(before: nil, with: reconciler)
|
child.mount(before: nil, on: self, with: reconciler)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func unmount(with reconciler: StackReconciler<R>) {
|
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> {
|
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 {
|
if let title = mountedScene.title {
|
||||||
// swiftlint:disable force_cast
|
// swiftlint:disable force_cast
|
||||||
(app.type as! _TitledApp.Type)._setTitle(title)
|
(app.type as! _TitledApp.Type)._setTitle(title)
|
||||||
|
|
|
@ -37,19 +37,34 @@ class MountedCompositeElement<R: Renderer>: MountedElement<R> {
|
||||||
*/
|
*/
|
||||||
var persistentSubscriptions = [AnyCancellable]()
|
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
|
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
|
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
|
self.parentTarget = parentTarget
|
||||||
super.init(view, environmentValues)
|
super.init(view, environmentValues, parent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,12 +19,20 @@ import CombineShim
|
||||||
import Runtime
|
import Runtime
|
||||||
|
|
||||||
final class MountedCompositeView<R: Renderer>: MountedCompositeElement<R> {
|
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 childBody = reconciler.render(compositeView: self)
|
||||||
|
|
||||||
let child: MountedElement<R> = childBody.makeMountedView(parentTarget, environmentValues)
|
let child: MountedElement<R> = childBody.makeMountedView(
|
||||||
|
parentTarget,
|
||||||
|
environmentValues,
|
||||||
|
self
|
||||||
|
)
|
||||||
mountedChildren = [child]
|
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
|
// `_TargetRef` is a composite view, so it's enough to check for it only here
|
||||||
if var targetRef = view.view as? TargetRefType {
|
if var targetRef = view.view as? TargetRefType {
|
||||||
|
@ -41,12 +49,27 @@ final class MountedCompositeView<R: Renderer>: MountedCompositeElement<R> {
|
||||||
view.view = targetRef
|
view.view = targetRef
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: this has to be implemented in a render-specific way, otherwise it's equivalent to
|
reconciler.afterCurrentRender(perform: { [weak self] in
|
||||||
// `_onMount` and `_onUnmount` at the moment,
|
guard let self = self else { return }
|
||||||
// see https://github.com/swiftwasm/Tokamak/issues/175 for more details
|
|
||||||
if let appearanceAction = view.view as? AppearanceActionType {
|
// FIXME: this has to be implemented in a render-specific way, otherwise it's equivalent to
|
||||||
appearanceAction.appear?()
|
// `_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>) {
|
override func unmount(with reconciler: StackReconciler<R>) {
|
||||||
|
@ -67,7 +90,7 @@ final class MountedCompositeView<R: Renderer>: MountedCompositeElement<R> {
|
||||||
$0.environmentValues = environmentValues
|
$0.environmentValues = environmentValues
|
||||||
$0.view = AnyView(element)
|
$0.view = AnyView(element)
|
||||||
},
|
},
|
||||||
mountChild: { $0.makeMountedView(parentTarget, environmentValues) }
|
mountChild: { $0.makeMountedView(parentTarget, environmentValues, self) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -88,20 +88,31 @@ public class MountedElement<R: Renderer> {
|
||||||
var mountedChildren = [MountedElement<R>]()
|
var mountedChildren = [MountedElement<R>]()
|
||||||
var environmentValues: EnvironmentValues
|
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)
|
element = .app(app)
|
||||||
|
self.parent = parent
|
||||||
self.environmentValues = environmentValues
|
self.environmentValues = environmentValues
|
||||||
updateEnvironment()
|
updateEnvironment()
|
||||||
}
|
}
|
||||||
|
|
||||||
init(_ scene: _AnyScene, _ environmentValues: EnvironmentValues) {
|
init(_ scene: _AnyScene, _ environmentValues: EnvironmentValues, _ parent: MountedElement<R>?) {
|
||||||
element = .scene(scene)
|
element = .scene(scene)
|
||||||
|
self.parent = parent
|
||||||
self.environmentValues = environmentValues
|
self.environmentValues = environmentValues
|
||||||
updateEnvironment()
|
updateEnvironment()
|
||||||
}
|
}
|
||||||
|
|
||||||
init(_ view: AnyView, _ environmentValues: EnvironmentValues) {
|
init(_ view: AnyView, _ environmentValues: EnvironmentValues, _ parent: MountedElement<R>?) {
|
||||||
element = .view(view)
|
element = .view(view)
|
||||||
|
self.parent = parent
|
||||||
self.environmentValues = environmentValues
|
self.environmentValues = environmentValues
|
||||||
updateEnvironment()
|
updateEnvironment()
|
||||||
}
|
}
|
||||||
|
@ -122,7 +133,11 @@ public class MountedElement<R: Renderer> {
|
||||||
return info
|
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")
|
fatalError("implement \(#function) in subclass")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -217,14 +232,15 @@ extension TypeInfo {
|
||||||
extension AnyView {
|
extension AnyView {
|
||||||
func makeMountedView<R: Renderer>(
|
func makeMountedView<R: Renderer>(
|
||||||
_ parentTarget: R.TargetType,
|
_ parentTarget: R.TargetType,
|
||||||
_ environmentValues: EnvironmentValues
|
_ environmentValues: EnvironmentValues,
|
||||||
|
_ parent: MountedElement<R>?
|
||||||
) -> MountedElement<R> {
|
) -> MountedElement<R> {
|
||||||
if type == EmptyView.self {
|
if type == EmptyView.self {
|
||||||
return MountedEmptyView(self, environmentValues)
|
return MountedEmptyView(self, environmentValues, parent)
|
||||||
} else if bodyType == Never.self && !(type is ViewDeferredToRenderer.Type) {
|
} else if bodyType == Never.self && !(type is ViewDeferredToRenderer.Type) {
|
||||||
return MountedHostView(self, parentTarget, environmentValues)
|
return MountedHostView(self, parentTarget, environmentValues, parent)
|
||||||
} else {
|
} else {
|
||||||
return MountedCompositeView(self, parentTarget, environmentValues)
|
return MountedCompositeView(self, parentTarget, environmentValues, parent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,11 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
final class MountedEmptyView<R: Renderer>: MountedElement<R> {
|
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>) {}
|
override func unmount(with reconciler: StackReconciler<R>) {}
|
||||||
|
|
||||||
|
|
|
@ -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.
|
/// Target of this host view supplied by a renderer after mounting has completed.
|
||||||
private(set) var target: R.TargetType?
|
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
|
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(
|
guard let target = reconciler.renderer?.mountTarget(
|
||||||
before: sibling,
|
before: sibling,
|
||||||
to: parentTarget,
|
to: parentTarget,
|
||||||
|
@ -50,7 +59,7 @@ public final class MountedHostView<R: Renderer>: MountedElement<R> {
|
||||||
guard !view.children.isEmpty else { return }
|
guard !view.children.isEmpty else { return }
|
||||||
|
|
||||||
mountedChildren = view.children.map {
|
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
|
/* 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`.
|
`GroupView`.
|
||||||
*/
|
*/
|
||||||
let isGroupView = view.type is GroupView.Type
|
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>) {
|
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
|
// if no existing children then mount all new children
|
||||||
case (true, false):
|
case (true, false):
|
||||||
mountedChildren = childrenViews.map { $0.makeMountedView(target, environmentValues) }
|
mountedChildren = childrenViews.map { $0.makeMountedView(target, environmentValues, self) }
|
||||||
mountedChildren.forEach { $0.mount(with: reconciler) }
|
mountedChildren.forEach { $0.mount(on: self, with: reconciler) }
|
||||||
|
|
||||||
// if both arrays have items then reconcile by types and keys
|
// if both arrays have items then reconcile by types and keys
|
||||||
case (false, false):
|
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
|
as a "cursor" sibling when mounting. Only then we can dispose of the old mounted child
|
||||||
by unmounting it.
|
by unmounting it.
|
||||||
*/
|
*/
|
||||||
newChild = childView.makeMountedView(target, environmentValues)
|
newChild = childView.makeMountedView(target, environmentValues, self)
|
||||||
newChild.mount(before: mountedChild.firstDescendantTarget, with: reconciler)
|
newChild.mount(before: mountedChild.firstDescendantTarget, on: self, with: reconciler)
|
||||||
mountedChild.unmount(with: reconciler)
|
mountedChild.unmount(with: reconciler)
|
||||||
}
|
}
|
||||||
newChildren.append(newChild)
|
newChildren.append(newChild)
|
||||||
|
@ -135,8 +146,8 @@ public final class MountedHostView<R: Renderer>: MountedElement<R> {
|
||||||
// mount remaining views
|
// mount remaining views
|
||||||
for firstChild in childrenViews {
|
for firstChild in childrenViews {
|
||||||
let newChild: MountedElement<R> =
|
let newChild: MountedElement<R> =
|
||||||
firstChild.makeMountedView(target, environmentValues)
|
firstChild.makeMountedView(target, environmentValues, self)
|
||||||
newChild.mount(with: reconciler)
|
newChild.mount(on: self, with: reconciler)
|
||||||
newChildren.append(newChild)
|
newChildren.append(newChild)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,19 +22,25 @@ final class MountedScene<R: Renderer>: MountedCompositeElement<R> {
|
||||||
_ title: String?,
|
_ title: String?,
|
||||||
_ children: [MountedElement<R>],
|
_ children: [MountedElement<R>],
|
||||||
_ parentTarget: R.TargetType,
|
_ parentTarget: R.TargetType,
|
||||||
_ environmentValues: EnvironmentValues
|
_ environmentValues: EnvironmentValues,
|
||||||
|
_ parent: MountedElement<R>?
|
||||||
) {
|
) {
|
||||||
self.title = title
|
self.title = title
|
||||||
super.init(scene, parentTarget, environmentValues)
|
super.init(scene, parentTarget, environmentValues, parent)
|
||||||
mountedChildren = children
|
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 childBody = reconciler.render(mountedScene: self)
|
||||||
|
|
||||||
let child: MountedElement<R> = childBody.makeMountedElement(parentTarget, environmentValues)
|
let child: MountedElement<R> = childBody
|
||||||
|
.makeMountedElement(parentTarget, environmentValues, self)
|
||||||
mountedChildren = [child]
|
mountedChildren = [child]
|
||||||
child.mount(before: sibling, with: reconciler)
|
child.mount(before: sibling, on: self, with: reconciler)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func unmount(with reconciler: StackReconciler<R>) {
|
override func unmount(with reconciler: StackReconciler<R>) {
|
||||||
|
@ -56,7 +62,7 @@ final class MountedScene<R: Renderer>: MountedCompositeElement<R> {
|
||||||
$0.view = AnyView(view)
|
$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>(
|
func makeMountedElement<R: Renderer>(
|
||||||
_ parentTarget: R.TargetType,
|
_ parentTarget: R.TargetType,
|
||||||
_ environmentValues: EnvironmentValues
|
_ environmentValues: EnvironmentValues,
|
||||||
|
_ parent: MountedElement<R>?
|
||||||
) -> MountedElement<R> {
|
) -> MountedElement<R> {
|
||||||
switch self {
|
switch self {
|
||||||
case let .scene(scene):
|
case let .scene(scene):
|
||||||
return scene.makeMountedScene(parentTarget, environmentValues)
|
return scene.makeMountedScene(parentTarget, environmentValues, parent)
|
||||||
case let .view(view):
|
case let .view(view):
|
||||||
return view.makeMountedView(parentTarget, environmentValues)
|
return view.makeMountedView(parentTarget, environmentValues, parent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -87,7 +94,8 @@ extension _AnyScene.BodyResult {
|
||||||
extension _AnyScene {
|
extension _AnyScene {
|
||||||
func makeMountedScene<R: Renderer>(
|
func makeMountedScene<R: Renderer>(
|
||||||
_ parentTarget: R.TargetType,
|
_ parentTarget: R.TargetType,
|
||||||
_ environmentValues: EnvironmentValues
|
_ environmentValues: EnvironmentValues,
|
||||||
|
_ parent: MountedElement<R>?
|
||||||
) -> MountedScene<R> {
|
) -> MountedScene<R> {
|
||||||
var title: String?
|
var title: String?
|
||||||
if let titledSelf = scene as? TitledScene,
|
if let titledSelf = scene as? TitledScene,
|
||||||
|
@ -97,12 +105,16 @@ extension _AnyScene {
|
||||||
}
|
}
|
||||||
let children: [MountedElement<R>]
|
let children: [MountedElement<R>]
|
||||||
if let deferredScene = scene as? SceneDeferredToRenderer {
|
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 {
|
} 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 {
|
} else {
|
||||||
children = []
|
children = []
|
||||||
}
|
}
|
||||||
return .init(self, title, children, parentTarget, environmentValues)
|
return .init(self, title, children, parentTarget, environmentValues, parent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)) }
|
||||||
|
}
|
||||||
|
}
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
|
@ -74,9 +74,9 @@ public final class StackReconciler<R: Renderer> {
|
||||||
self.scheduler = scheduler
|
self.scheduler = scheduler
|
||||||
rootTarget = target
|
rootTarget = target
|
||||||
|
|
||||||
rootElement = AnyView(view).makeMountedView(target, environment)
|
rootElement = AnyView(view).makeMountedView(target, environment, nil)
|
||||||
|
|
||||||
rootElement.mount(with: self)
|
performInitialMount()
|
||||||
}
|
}
|
||||||
|
|
||||||
public init<A: App>(
|
public init<A: App>(
|
||||||
|
@ -90,15 +90,20 @@ public final class StackReconciler<R: Renderer> {
|
||||||
self.scheduler = scheduler
|
self.scheduler = scheduler
|
||||||
rootTarget = target
|
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> {
|
if let mountedApp = rootElement as? MountedApp<R> {
|
||||||
setupPersistentSubscription(for: app._phasePublisher, to: \.scenePhase, of: mountedApp)
|
setupPersistentSubscription(for: app._phasePublisher, to: \.scenePhase, of: mountedApp)
|
||||||
setupPersistentSubscription(for: app._colorSchemePublisher, to: \.colorScheme, of: mountedApp)
|
setupPersistentSubscription(for: app._colorSchemePublisher, to: \.colorScheme, of: mountedApp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func performInitialMount() {
|
||||||
|
rootElement.mount(with: self)
|
||||||
|
performPostrenderCallbacks()
|
||||||
|
}
|
||||||
|
|
||||||
private func queueStorageUpdate(
|
private func queueStorageUpdate(
|
||||||
for mountedElement: MountedCompositeElement<R>,
|
for mountedElement: MountedCompositeElement<R>,
|
||||||
id: Int,
|
id: Int,
|
||||||
|
@ -108,7 +113,7 @@ public final class StackReconciler<R: Renderer> {
|
||||||
queueUpdate(for: mountedElement)
|
queueUpdate(for: mountedElement)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func queueUpdate(for mountedElement: MountedCompositeElement<R>) {
|
internal func queueUpdate(for mountedElement: MountedCompositeElement<R>) {
|
||||||
let shouldSchedule = queuedRerenders.isEmpty
|
let shouldSchedule = queuedRerenders.isEmpty
|
||||||
queuedRerenders.insert(mountedElement)
|
queuedRerenders.insert(mountedElement)
|
||||||
|
|
||||||
|
@ -123,6 +128,7 @@ public final class StackReconciler<R: Renderer> {
|
||||||
}
|
}
|
||||||
|
|
||||||
queuedRerenders.removeAll()
|
queuedRerenders.removeAll()
|
||||||
|
performPostrenderCallbacks()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setupStorage(
|
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,7 +39,7 @@ public protocol AnyColorBoxDeferredToRenderer: AnyColorBox {
|
||||||
func deferredResolve(in environment: EnvironmentValues) -> AnyColorBox.ResolvedValue
|
func deferredResolve(in environment: EnvironmentValues) -> AnyColorBox.ResolvedValue
|
||||||
}
|
}
|
||||||
|
|
||||||
public class AnyColorBox: AnyTokenBox {
|
public class AnyColorBox: AnyTokenBox, Equatable {
|
||||||
public struct _RGBA: Hashable, Equatable {
|
public struct _RGBA: Hashable, Equatable {
|
||||||
public let red: Double
|
public let red: Double
|
||||||
public let green: 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) {
|
public func hash(into hasher: inout Hasher) {
|
||||||
fatalError("implement \(#function) in subclass")
|
fatalError("implement \(#function) in subclass")
|
||||||
}
|
}
|
||||||
|
@ -74,8 +82,10 @@ public class AnyColorBox: AnyTokenBox {
|
||||||
public class _ConcreteColorBox: AnyColorBox {
|
public class _ConcreteColorBox: AnyColorBox {
|
||||||
public let rgba: AnyColorBox._RGBA
|
public let rgba: AnyColorBox._RGBA
|
||||||
|
|
||||||
public static func == (lhs: _ConcreteColorBox, rhs: _ConcreteColorBox) -> Bool {
|
override public func equals(_ other: AnyColorBox) -> Bool {
|
||||||
lhs.rgba == rhs.rgba
|
guard let other = other as? _ConcreteColorBox
|
||||||
|
else { return false }
|
||||||
|
return rgba == other.rgba
|
||||||
}
|
}
|
||||||
|
|
||||||
override public func hash(into hasher: inout Hasher) {
|
override public func hash(into hasher: inout Hasher) {
|
||||||
|
@ -94,10 +104,10 @@ public class _ConcreteColorBox: AnyColorBox {
|
||||||
public class _EnvironmentDependentColorBox: AnyColorBox {
|
public class _EnvironmentDependentColorBox: AnyColorBox {
|
||||||
public let resolver: (EnvironmentValues) -> Color
|
public let resolver: (EnvironmentValues) -> Color
|
||||||
|
|
||||||
public static func == (lhs: _EnvironmentDependentColorBox,
|
override public func equals(_ other: AnyColorBox) -> Bool {
|
||||||
rhs: _EnvironmentDependentColorBox) -> Bool
|
guard let other = other as? _EnvironmentDependentColorBox
|
||||||
{
|
else { return false }
|
||||||
lhs.resolver(EnvironmentValues()) == rhs.resolver(EnvironmentValues())
|
return resolver(EnvironmentValues()) == other.resolver(EnvironmentValues())
|
||||||
}
|
}
|
||||||
|
|
||||||
override public func hash(into hasher: inout Hasher) {
|
override public func hash(into hasher: inout Hasher) {
|
||||||
|
@ -113,8 +123,8 @@ public class _EnvironmentDependentColorBox: AnyColorBox {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class _SystemColorBox: AnyColorBox {
|
public class _SystemColorBox: AnyColorBox, CustomStringConvertible {
|
||||||
public enum SystemColor: Equatable, Hashable {
|
public enum SystemColor: String, Equatable, Hashable {
|
||||||
case clear
|
case clear
|
||||||
case black
|
case black
|
||||||
case white
|
case white
|
||||||
|
@ -130,10 +140,16 @@ public class _SystemColorBox: AnyColorBox {
|
||||||
case secondary
|
case secondary
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public var description: String {
|
||||||
|
value.rawValue
|
||||||
|
}
|
||||||
|
|
||||||
public let value: SystemColor
|
public let value: SystemColor
|
||||||
|
|
||||||
public static func == (lhs: _SystemColorBox, rhs: _SystemColorBox) -> Bool {
|
override public func equals(_ other: AnyColorBox) -> Bool {
|
||||||
lhs.value == rhs.value
|
guard let other = other as? _SystemColorBox
|
||||||
|
else { return false }
|
||||||
|
return value == other.value
|
||||||
}
|
}
|
||||||
|
|
||||||
override public func hash(into hasher: inout Hasher) {
|
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 {
|
public extension Color {
|
||||||
private init(systemColor: _SystemColorBox.SystemColor) {
|
private init(systemColor: _SystemColorBox.SystemColor) {
|
||||||
self.init(_SystemColorBox(systemColor))
|
self.init(_SystemColorBox(systemColor))
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
public enum ColorScheme: CaseIterable {
|
public enum ColorScheme: CaseIterable, Equatable {
|
||||||
case dark
|
case dark
|
||||||
case light
|
case light
|
||||||
}
|
}
|
||||||
|
@ -35,3 +35,16 @@ public extension View {
|
||||||
environment(\.colorScheme, colorScheme)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -89,7 +89,7 @@ public struct _NavigationLinkProxy<Label, Destination> where Label: View, Destin
|
||||||
|
|
||||||
public var style: _AnyNavigationLinkStyle { subject.style }
|
public var style: _AnyNavigationLinkStyle { subject.style }
|
||||||
public var isSelected: Bool {
|
public var isSelected: Bool {
|
||||||
ObjectIdentifier(subject.destination) == ObjectIdentifier(subject.navigationContext.destination)
|
subject.destination === subject.navigationContext.destination
|
||||||
}
|
}
|
||||||
|
|
||||||
public func activate() {
|
public func activate() {
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -116,6 +116,10 @@ public extension Text {
|
||||||
.init(storage: storage, modifiers: modifiers + [.font(font)])
|
.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 {
|
func fontWeight(_ weight: Font.Weight?) -> Text {
|
||||||
.init(storage: storage, modifiers: modifiers + [.weight(weight)])
|
.init(storage: storage, modifiers: modifiers + [.weight(weight)])
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,8 @@ import TokamakCore
|
||||||
public typealias Environment = TokamakCore.Environment
|
public typealias Environment = TokamakCore.Environment
|
||||||
public typealias EnvironmentObject = TokamakCore.EnvironmentObject
|
public typealias EnvironmentObject = TokamakCore.EnvironmentObject
|
||||||
|
|
||||||
|
public typealias PreferenceKey = TokamakCore.PreferenceKey
|
||||||
|
|
||||||
public typealias Binding = TokamakCore.Binding
|
public typealias Binding = TokamakCore.Binding
|
||||||
public typealias ObservableObject = TokamakCore.ObservableObject
|
public typealias ObservableObject = TokamakCore.ObservableObject
|
||||||
public typealias ObservedObject = TokamakCore.ObservedObject
|
public typealias ObservedObject = TokamakCore.ObservedObject
|
||||||
|
|
|
@ -157,7 +157,7 @@ final class DOMRenderer: Renderer {
|
||||||
) {
|
) {
|
||||||
defer { completion() }
|
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 }
|
else { return }
|
||||||
|
|
||||||
_ = parent.ref.removeChild!(target.ref)
|
_ = parent.ref.removeChild!(target.ref)
|
||||||
|
|
|
@ -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() }
|
||||||
|
}
|
||||||
|
}
|
|
@ -129,6 +129,9 @@ struct TokamakDemoView: View {
|
||||||
Section(header: Text("Misc")) {
|
Section(header: Text("Misc")) {
|
||||||
NavItem("Path", destination: PathDemo())
|
NavItem("Path", destination: PathDemo())
|
||||||
NavItem("Environment", destination: EnvironmentDemo().font(.system(size: 8)))
|
NavItem("Environment", destination: EnvironmentDemo().font(.system(size: 8)))
|
||||||
|
if #available(macOS 11.0, iOS 14.0, *) {
|
||||||
|
NavItem("Preferences", destination: PreferenceKeyDemo())
|
||||||
|
}
|
||||||
NavItem("Color", destination: ColorDemo())
|
NavItem("Color", destination: ColorDemo())
|
||||||
if #available(OSX 11.0, iOS 14.0, *) {
|
if #available(OSX 11.0, iOS 14.0, *) {
|
||||||
NavItem("AppStorage", destination: AppStorageDemo())
|
NavItem("AppStorage", destination: AppStorageDemo())
|
||||||
|
|
|
@ -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 {
|
public var deferredBody: AnyView {
|
||||||
if let domModifier = modifier as? DOMViewModifier {
|
if let domModifier = modifier as? DOMViewModifier {
|
||||||
if let adjacentModifier = content as? AnyModifiedContent,
|
if let adjacentModifier = content as? AnyModifiedContent,
|
||||||
|
@ -51,8 +51,10 @@ extension ModifiedContent: ViewDeferredToRenderer where Content: View {
|
||||||
content
|
content
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else {
|
} else if Modifier.Body.self == Never.self {
|
||||||
return AnyView(content)
|
return AnyView(content)
|
||||||
|
} else {
|
||||||
|
return AnyView(modifier.body(content: .init(modifier: modifier, view: AnyView(content))))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,14 +16,15 @@ import TokamakCore
|
||||||
|
|
||||||
extension NavigationView: ViewDeferredToRenderer {
|
extension NavigationView: ViewDeferredToRenderer {
|
||||||
public var deferredBody: AnyView {
|
public var deferredBody: AnyView {
|
||||||
AnyView(HTML("div", [
|
let proxy = _NavigationViewProxy(self)
|
||||||
|
return AnyView(HTML("div", [
|
||||||
"class": "_tokamak-navigationview",
|
"class": "_tokamak-navigationview",
|
||||||
]) {
|
]) {
|
||||||
_NavigationViewProxy(self).content
|
proxy.content
|
||||||
HTML("div", [
|
HTML("div", [
|
||||||
"class": "_tokamak-navigationview-content",
|
"class": "_tokamak-navigationview-content",
|
||||||
]) {
|
]) {
|
||||||
_NavigationViewProxy(self).destination
|
proxy.destination
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue