Fix crashes in views with optional content (#364)
* Add TokamakStaticHTMLTests target * Add AnyOptional, clarify conformances issues
This commit is contained in:
parent
163005dfe0
commit
67aea3cc3b
|
@ -11,7 +11,7 @@
|
||||||
{
|
{
|
||||||
"label": "swift test",
|
"label": "swift test",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "swift test"
|
"command": "swift build --product TokamakPackageTests && `xcrun --find xctest` .build/debug/TokamakPackageTests.xctest"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "carton dev",
|
"label": "carton dev",
|
||||||
|
@ -39,4 +39,4 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -154,5 +154,14 @@ let package = Package(
|
||||||
name: "TokamakTests",
|
name: "TokamakTests",
|
||||||
dependencies: ["TokamakTestRenderer"]
|
dependencies: ["TokamakTestRenderer"]
|
||||||
),
|
),
|
||||||
|
// FIXME: re-enable when `ViewDeferredToRenderer` conformance conflicts issue is resolved
|
||||||
|
// Currently, when multiple modules that have conflicting `ViewDeferredToRenderer`
|
||||||
|
// implementations are linked in the same binary, only a single one is used with no defined
|
||||||
|
// behavior for that. We need to replace `ViewDeferredToRenderer` with a different solution
|
||||||
|
// that isn't prone to these hard to debug errors.
|
||||||
|
// .testTarget(
|
||||||
|
// name: "TokamakStaticHTMLTests",
|
||||||
|
// dependencies: ["TokamakStaticHTML"]
|
||||||
|
// ),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
|
@ -125,7 +125,7 @@ public final class StackReconciler<R: Renderer> {
|
||||||
private func updateStateAndReconcile() {
|
private func updateStateAndReconcile() {
|
||||||
let queued = queuedRerenders
|
let queued = queuedRerenders
|
||||||
queuedRerenders.removeAll()
|
queuedRerenders.removeAll()
|
||||||
|
|
||||||
for mountedView in queued {
|
for mountedView in queued {
|
||||||
mountedView.update(with: self)
|
mountedView.update(with: self)
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,8 +59,16 @@ public struct AnyView: View {
|
||||||
bodyType = V.Body.self
|
bodyType = V.Body.self
|
||||||
self.view = view
|
self.view = view
|
||||||
if view is ViewDeferredToRenderer {
|
if view is ViewDeferredToRenderer {
|
||||||
// swiftlint:disable:next force_cast
|
bodyClosure = {
|
||||||
bodyClosure = { ($0 as! ViewDeferredToRenderer).deferredBody }
|
let deferredView: Any
|
||||||
|
if let opt = $0 as? AnyOptional, let value = opt.value {
|
||||||
|
deferredView = value
|
||||||
|
} else {
|
||||||
|
deferredView = $0
|
||||||
|
}
|
||||||
|
// swiftlint:disable:next force_cast
|
||||||
|
return (deferredView as! ViewDeferredToRenderer).deferredBody
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// swiftlint:disable:next force_cast
|
// swiftlint:disable:next force_cast
|
||||||
bodyClosure = { AnyView(($0 as! V).body) }
|
bodyClosure = { AnyView(($0 as! V).body) }
|
||||||
|
|
|
@ -62,6 +62,19 @@ extension Optional: View where Wrapped: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protocol AnyOptional {
|
||||||
|
var value: Any? { get }
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Optional: AnyOptional {
|
||||||
|
var value: Any? {
|
||||||
|
switch self {
|
||||||
|
case let .some(value): return value
|
||||||
|
case .none: return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@_functionBuilder public enum ViewBuilder {
|
@_functionBuilder public enum ViewBuilder {
|
||||||
public static func buildBlock() -> EmptyView { EmptyView() }
|
public static func buildBlock() -> EmptyView { EmptyView() }
|
||||||
|
|
||||||
|
|
|
@ -59,25 +59,29 @@ private func bindAction(to entry: UnsafeMutablePointer<GtkWidget>, textBinding:
|
||||||
extension SecureField: ViewDeferredToRenderer where Label == Text {
|
extension SecureField: ViewDeferredToRenderer where Label == Text {
|
||||||
public var deferredBody: AnyView {
|
public var deferredBody: AnyView {
|
||||||
let proxy = _SecureFieldProxy(self)
|
let proxy = _SecureFieldProxy(self)
|
||||||
return AnyView(WidgetView(build: { _ in
|
return AnyView(WidgetView(
|
||||||
build(textBinding: proxy.textBinding, label: proxy.label, visible: false)
|
build: { _ in
|
||||||
},
|
build(textBinding: proxy.textBinding, label: proxy.label, visible: false)
|
||||||
update: { w in
|
},
|
||||||
guard case let .widget(entry) = w.storage else { return }
|
update: { w in
|
||||||
update(entry: entry, textBinding: proxy.textBinding, label: proxy.label)
|
guard case let .widget(entry) = w.storage else { return }
|
||||||
}) {})
|
update(entry: entry, textBinding: proxy.textBinding, label: proxy.label)
|
||||||
|
}
|
||||||
|
) {})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TextField: ViewDeferredToRenderer where Label == Text {
|
extension TextField: ViewDeferredToRenderer where Label == Text {
|
||||||
public var deferredBody: AnyView {
|
public var deferredBody: AnyView {
|
||||||
let proxy = _TextFieldProxy(self)
|
let proxy = _TextFieldProxy(self)
|
||||||
return AnyView(WidgetView(build: { _ in
|
return AnyView(WidgetView(
|
||||||
build(textBinding: proxy.textBinding, label: proxy.label)
|
build: { _ in
|
||||||
},
|
build(textBinding: proxy.textBinding, label: proxy.label)
|
||||||
update: { a in
|
},
|
||||||
guard case let .widget(widget) = a.storage else { return }
|
update: { a in
|
||||||
update(entry: widget, textBinding: proxy.textBinding, label: proxy.label)
|
guard case let .widget(widget) = a.storage else { return }
|
||||||
}) {})
|
update(entry: widget, textBinding: proxy.textBinding, label: proxy.label)
|
||||||
|
}
|
||||||
|
) {})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 Max Desiatov on 07/12/2018.
|
||||||
|
//
|
||||||
|
|
||||||
|
import TokamakStaticHTML
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
final class ReconcilerTests: XCTestCase {
|
||||||
|
struct Model {
|
||||||
|
let text: Text
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct OptionalBody: View {
|
||||||
|
var model: Model?
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
if let text = model?.text {
|
||||||
|
VStack {
|
||||||
|
text
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testOptional() {
|
||||||
|
let renderer = StaticHTMLRenderer(OptionalBody(model: Model(text: Text("text"))))
|
||||||
|
|
||||||
|
XCTAssertEqual(renderer.html.count, 2777)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue