Add `GeometryReader` implementation (#239)

This is just an empty API at the moment. I hope it can be implemented purely in the `deferredBody` of `GeometryReader` with [the ResizeObserver API](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver) without requiring any tweaks in the `Renderer` protocol or the reconciler.

Seems like I need the `domRef` modifier that writes `JSObjectRef` to a given binding working first, as discussed in #231.
This commit is contained in:
Max Desiatov 2020-08-11 16:47:12 +01:00 committed by GitHub
parent c43d2db1b3
commit b7434a2e54
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 228 additions and 11 deletions

View File

@ -45,6 +45,8 @@
D1B4229324B3B9BB00682F74 /* OutlineGroupDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1B4228F24B3B9BB00682F74 /* OutlineGroupDemo.swift */; };
D1C726F324CB63C6003B576D /* ButtonStyleDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1C726F224CB63C6003B576D /* ButtonStyleDemo.swift */; };
D1C726F424CB63C6003B576D /* ButtonStyleDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1C726F224CB63C6003B576D /* ButtonStyleDemo.swift */; };
D1D6B62324D817350041E1D9 /* GeometryReaderDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1D6B62224D817350041E1D9 /* GeometryReaderDemo.swift */; };
D1D6B62424D817350041E1D9 /* GeometryReaderDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1D6B62224D817350041E1D9 /* GeometryReaderDemo.swift */; };
D1E5FDAD24C1D57000E7485E /* TokamakShim.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1E5FDAC24C1D57000E7485E /* TokamakShim.swift */; };
D1E5FDAF24C1D58E00E7485E /* libTokamakShim.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D1E5FDA424C1D54B00E7485E /* libTokamakShim.a */; };
D1E5FDB224C1D59400E7485E /* libTokamakShim.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D1E5FDA424C1D54B00E7485E /* libTokamakShim.a */; };
@ -107,6 +109,7 @@
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>"; };
D1D6B62224D817350041E1D9 /* GeometryReaderDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeometryReaderDemo.swift; sourceTree = "<group>"; };
D1E5FDA424C1D54B00E7485E /* libTokamakShim.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libTokamakShim.a; sourceTree = BUILT_PRODUCTS_DIR; };
D1E5FDAC24C1D57000E7485E /* TokamakShim.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokamakShim.swift; sourceTree = "<group>"; };
D1EE7EA624C0DD2100C0D127 /* PickerDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PickerDemo.swift; sourceTree = "<group>"; };
@ -168,6 +171,7 @@
85ED189924AD425E0085DFA0 /* TokamakDemo */ = {
isa = PBXGroup;
children = (
D1D6B62224D817350041E1D9 /* GeometryReaderDemo.swift */,
D1C726F224CB63C6003B576D /* ButtonStyleDemo.swift */,
B5C76E4924C73ED4003EABB2 /* AppStorageDemo.swift */,
B56F22DF24BC89FD001738DF /* ColorDemo.swift */,
@ -332,6 +336,7 @@
85ED186A24AD38F20085DFA0 /* UIAppDelegate.swift in Sources */,
B56F22E324BD1C26001738DF /* GridDemo.swift in Sources */,
D1B4229224B3B9BB00682F74 /* OutlineGroupDemo.swift in Sources */,
D1D6B62324D817350041E1D9 /* GeometryReaderDemo.swift in Sources */,
B5DBA22B24D509B4003D3347 /* RedactDemo.swift in Sources */,
B56F22E024BC89FD001738DF /* ColorDemo.swift in Sources */,
B51F215024B920B400CF2583 /* PathDemo.swift in Sources */,
@ -358,6 +363,7 @@
85ED18AA24AD425E0085DFA0 /* TokamakDemo.swift in Sources */,
B56F22E424BD1C26001738DF /* GridDemo.swift in Sources */,
D1B4229324B3B9BB00682F74 /* OutlineGroupDemo.swift in Sources */,
D1D6B62424D817350041E1D9 /* GeometryReaderDemo.swift in Sources */,
B5DBA22C24D509B4003D3347 /* RedactDemo.swift in Sources */,
B56F22E124BC89FD001738DF /* ColorDemo.swift in Sources */,
B51F215124B920B400CF2583 /* PathDemo.swift in Sources */,

View File

@ -21,10 +21,6 @@ protocol AppearanceActionType {
struct _AppearanceActionModifier: ViewModifier {
var appear: (() -> ())?
var disappear: (() -> ())?
init(appear: (() -> ())? = nil, disappear: (() -> ())? = nil) {
self.appear = appear
self.disappear = disappear
}
typealias Body = Never
}

View File

@ -0,0 +1,24 @@
// 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.
// FIXME: these should have standalone implementations
extension View {
public func _onMount(perform action: (() -> ())? = nil) -> some View {
modifier(_AppearanceActionModifier(appear: action))
}
public func _onUnmount(perform action: (() -> ())? = nil) -> some View {
modifier(_AppearanceActionModifier(disappear: action))
}
}

View File

@ -22,10 +22,6 @@ final class MountedCompositeView<R: Renderer>: MountedCompositeElement<R> {
override func mount(with reconciler: StackReconciler<R>) {
let childBody = reconciler.render(compositeView: self)
if let appearanceAction = view.view as? AppearanceActionType {
appearanceAction.appear?()
}
let child: MountedElement<R> = childBody.makeMountedView(parentTarget, environmentValues)
mountedChildren = [child]
child.mount(with: reconciler)
@ -44,6 +40,13 @@ final class MountedCompositeView<R: Renderer>: MountedCompositeElement<R> {
targetRef.target = hostDescendant.target
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?()
}
}
override func unmount(with reconciler: StackReconciler<R>) {

View File

@ -0,0 +1,15 @@
// 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.
public typealias StateObject = ObservedObject

View File

@ -1,6 +1,17 @@
// Copyright 2020 Tokamak contributors
//
// File.swift
// 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 6/28/20.
//

View File

@ -0,0 +1,52 @@
// 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.
public struct GeometryProxy {
public let size: CGSize
}
public func makeProxy(from size: CGSize) -> GeometryProxy {
.init(size: size)
}
// FIXME: to be implemented
// public enum CoordinateSpace {
// case global
// case local
// case named(AnyHashable)
// }
// public struct Anchor<Value> {
// let box: AnchorValueBoxBase<Value>
// public struct Source {
// private var box: AnchorBoxBase<Value>
// }
// }
// extension GeometryProxy {
// public let safeAreaInsets: EdgeInsets
// public func frame(in coordinateSpace: CoordinateSpace) -> CGRect
// public subscript<T>(anchor: Anchor<T>) -> T {}
// }
public struct GeometryReader<Content>: View where Content: View {
public let content: (GeometryProxy) -> Content
public init(@ViewBuilder content: @escaping (GeometryProxy) -> Content) {
self.content = content
}
public var body: Never {
neverBody("GeometryReader")
}
}

View File

@ -87,6 +87,7 @@ public typealias Button = TokamakCore.Button
public typealias DisclosureGroup = TokamakCore.DisclosureGroup
public typealias Divider = TokamakCore.Divider
public typealias ForEach = TokamakCore.ForEach
public typealias GeometryReader = TokamakCore.GeometryReader
public typealias GridItem = TokamakCore.GridItem
public typealias Group = TokamakCore.Group
public typealias HStack = TokamakCore.HStack

View File

@ -0,0 +1,80 @@
// 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.
import JavaScriptKit
import TokamakCore
import TokamakStaticHTML
private let ResizeObserver = JSObjectRef.global.ResizeObserver.function!
extension GeometryReader: ViewDeferredToRenderer {
public var deferredBody: AnyView {
AnyView(_GeometryReader(content: content))
}
}
struct _GeometryReader<Content: View>: View {
final class State: ObservableObject {
/** Holds a strong reference to a `JSClosure` instance that has to stay alive as long as
the `_GeometryReader` owner is alive.
*/
var closure: JSClosure?
/// A reference to a DOM node being observed for size updates.
var observedNodeRef: JSObjectRef?
/// A reference to a `ResizeObserver` instance.
var observerRef: JSObjectRef?
/// The last known size of the `observedNodeRef` DOM node.
@Published var size: CGSize?
}
let content: (GeometryProxy) -> Content
@StateObject private var state = State()
var body: some View {
HTML("div", ["class": "_tokamak-geometryreader"]) {
if let size = state.size {
content(makeProxy(from: size))
} else {
EmptyView()
}
}
._domRef($state.observedNodeRef)
._onMount {
let closure = JSClosure { [weak state] args in
// FIXME: `JSArrayRef` is not a `RandomAccessCollection` for some reason, which forces
// us to use a string subscript
guard
let rect = args[0].object?[dynamicMember: "0"].object?.contentRect.object,
let width = rect.width.number,
let height = rect.height.number
else { return .undefined }
state?.size = .init(width: width, height: height)
return .undefined
}
state.closure = closure
let observerRef = ResizeObserver.new(closure)
_ = observerRef.observe!(state.observedNodeRef!)
state.observerRef = observerRef
}
}
}

View File

@ -0,0 +1,23 @@
// 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.
import TokamakShim
struct GeometryReaderDemo: View {
var body: some View {
GeometryReader {
Text("\(String(describing: $0.size))")
}
}
}

View File

@ -115,6 +115,7 @@ struct TokamakDemoView: View {
.zIndex(1)
Text("I'm on top")
}.padding(20))
NavItem("GeometryReader", destination: GeometryReaderDemo())
}
Section(header: Text("Selectors")) {
NavItem("Picker", destination: PickerDemo())

View File

@ -31,7 +31,6 @@ public let tokamakStyles = """
height: 100%;
padding: 0;
}
._tokamak-disclosuregroup-label {
cursor: pointer;
}
@ -76,7 +75,13 @@ public let tokamakStyles = """
height: 1.2em;
border-radius: .1em;
}
._tokamak-geometryreader {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
._tokamak-navigationview {
display: flex;
flex-direction: row;