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 */; };
|
||||
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 */,
|
||||
|
|
|
@ -7,7 +7,7 @@ import PackageDescription
|
|||
let package = Package(
|
||||
name: "Tokamak",
|
||||
platforms: [
|
||||
.macOS(.v10_15),
|
||||
.macOS(.v11),
|
||||
.iOS(.v13),
|
||||
],
|
||||
products: [
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:)`"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>) {}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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)])
|
||||
}
|
||||
|
||||
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)])
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")) {
|
||||
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())
|
||||
|
|
|
@ -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))))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue