Fix crashes in views with optional content (#364)

* Add TokamakStaticHTMLTests target

* Add AnyOptional, clarify conformances issues
This commit is contained in:
Max Desiatov 2021-01-20 05:07:01 +00:00 committed by GitHub
parent 163005dfe0
commit 67aea3cc3b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 98 additions and 19 deletions

4
.vscode/tasks.json vendored
View File

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

View File

@ -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"]
// ),
] ]
) )

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,45 @@
// Copyright 2020 Tokamak contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Created by 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)
}
}