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

2
.vscode/tasks.json vendored
View File

@ -11,7 +11,7 @@
{
"label": "swift test",
"type": "shell",
"command": "swift test"
"command": "swift build --product TokamakPackageTests && `xcrun --find xctest` .build/debug/TokamakPackageTests.xctest"
},
{
"label": "carton dev",

View File

@ -154,5 +154,14 @@ let package = Package(
name: "TokamakTests",
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

@ -59,8 +59,16 @@ public struct AnyView: View {
bodyType = V.Body.self
self.view = view
if view is ViewDeferredToRenderer {
// swiftlint:disable:next force_cast
bodyClosure = { ($0 as! ViewDeferredToRenderer).deferredBody }
bodyClosure = {
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 {
// swiftlint:disable:next force_cast
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 {
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 {
public var deferredBody: AnyView {
let proxy = _SecureFieldProxy(self)
return AnyView(WidgetView(build: { _ in
build(textBinding: proxy.textBinding, label: proxy.label, visible: false)
},
update: { w in
guard case let .widget(entry) = w.storage else { return }
update(entry: entry, textBinding: proxy.textBinding, label: proxy.label)
}) {})
return AnyView(WidgetView(
build: { _ in
build(textBinding: proxy.textBinding, label: proxy.label, visible: false)
},
update: { w in
guard case let .widget(entry) = w.storage else { return }
update(entry: entry, textBinding: proxy.textBinding, label: proxy.label)
}
) {})
}
}
extension TextField: ViewDeferredToRenderer where Label == Text {
public var deferredBody: AnyView {
let proxy = _TextFieldProxy(self)
return AnyView(WidgetView(build: { _ in
build(textBinding: proxy.textBinding, label: proxy.label)
},
update: { a in
guard case let .widget(widget) = a.storage else { return }
update(entry: widget, textBinding: proxy.textBinding, label: proxy.label)
}) {})
return AnyView(WidgetView(
build: { _ in
build(textBinding: proxy.textBinding, label: proxy.label)
},
update: { a in
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)
}
}