Initial `NavigationView` implementation (#130)

* Initial NavigationView implementation

* Make the _ButtonProxy type more generic

* Split Navigation.swift files

* Move this too

* Implement Navigation controls

* Update progress.md

* Make NavigationLinks links

* Break line

(wishing for Prettier for Swift)

* Update Path.swift

* n-th time’s the charm

* Update Path.swift

* Update project.pbxproj

* Fixes

* Hopefully fix build issues

* Update Navigation.swift

* Improve ColorDemo

* Fixes & reverts

* Fix crash

* Revert "Fix crash"

This reverts commit ae6f13dcc9.

* Tweak rendering of demos

* add todo for accessibility

* Apply suggestions from @MaxDesiatov

Co-authored-by: Max Desiatov <max@desiatov.com>

* Update TokamakDemo.swift

* Move things to Core.swift

* Switch default destination to EmptyView

* Fix build for macOS

* Revert "Apply suggestions from @MaxDesiatov"

This reverts commit 73c9c3f6ac.

Co-authored-by: Max Desiatov <max@desiatov.com>
Co-authored-by: Carson Katri <Carson.katri@gmail.com>
This commit is contained in:
Jed Fox 2020-07-22 17:12:15 -04:00 committed by GitHub
parent 2b93f37d64
commit ac50208447
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 403 additions and 86 deletions

View File

@ -434,6 +434,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.15;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
@ -488,6 +489,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.15;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.jedfox.Tokamak-Native";
@ -556,9 +558,9 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.15;
PRODUCT_NAME = "TokamakDemo Native";
SDKROOT = macosx;
SUPPORTED_PLATFORMS = macosx;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
};
@ -580,9 +582,9 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.15;
PRODUCT_NAME = "TokamakDemo Native";
SDKROOT = macosx;
SUPPORTED_PLATFORMS = macosx;
SWIFT_VERSION = 5.0;
};
name = Release;
@ -596,6 +598,7 @@
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
SKIP_INSTALL = YES;
SUPPORTED_PLATFORMS = "iphonesimulator iphoneos macosx";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
@ -610,6 +613,7 @@
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
SKIP_INSTALL = YES;
SUPPORTED_PLATFORMS = "iphonesimulator iphoneos macosx";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;

View File

@ -23,13 +23,14 @@ public class NSApplication: UIApplication {}
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
let window = UIWindow()
var window: UIWindow?
func application(
_: UIApplication,
didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
window.rootViewController = UIHostingController(rootView: TokamakDemoView())
window.makeKeyAndVisible()
window = UIWindow()
window?.rootViewController = UIHostingController(rootView: TokamakDemoView())
window?.makeKeyAndVisible()
return true
}
}

View File

@ -0,0 +1,25 @@
// 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.
extension View {
// FIXME: Implement
public func navigationBarTitle<S>(_ title: S) -> some View where S: StringProtocol {
self
}
// FIXME: Implement
public func navigationTitle<S>(_ title: S) -> some View where S: StringProtocol {
self
}
}

View File

@ -65,11 +65,13 @@ extension Button: ParentView {
}
/// This is a helper class that works around absence of "package private" access control in Swift
public struct _ButtonProxy {
public let subject: Button<Text>
public struct _ButtonProxy<Label> where Label: View {
let subject: Button<Label>
public init(_ subject: Button<Text>) { self.subject = subject }
public var label: _TextProxy { _TextProxy(subject.label) }
public init(_ subject: Button<Label>) { self.subject = subject }
public var action: () -> () { subject.action }
}
extension _ButtonProxy where Label == Text {
public var label: _TextProxy { _TextProxy(subject.label) }
}

View File

@ -0,0 +1,79 @@
// 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 Jed Fox on 06/30/2020.
//
public struct NavigationLink<Label, Destination>: View where Label: View, Destination: View {
let destination: Destination
let label: Label
@Environment(_navigationDestinationKey) var navigationContext
public init(destination: Destination, @ViewBuilder label: () -> Label) {
self.destination = destination
self.label = label()
}
/// Creates an instance that presents `destination` when active.
// public init(destination: Destination, isActive: Binding<Bool>, @ViewBuilder label: () -> Label)
/// Creates an instance that presents `destination` when `selection` is set
/// to `tag`.
// public init<V>(
// destination: Destination,
// tag: V, selection: Binding<V?>,
// @ViewBuilder label: () -> Label
// ) where V : Hashable
public var body: Never {
neverBody("NavigationLink")
}
}
extension NavigationLink where Label == Text {
/// Creates an instance that presents `destination`, with a `Text` label
/// generated from a title string.
public init<S>(_ title: S, destination: Destination) where S: StringProtocol {
self.destination = destination
label = Text(title)
}
/// Creates an instance that presents `destination` when active, with a
/// `Text` label generated from a title string.
// public init<S>(
// _ title: S, destination: Destination,
// isActive: Binding<Bool>
// ) where S : StringProtocol
/// Creates an instance that presents `destination` when `selection` is set
/// to `tag`, with a `Text` label generated from a title string.
// public init<S, V>(
// _ title: S, destination: Destination,
// tag: V, selection: Binding<V?>
// ) where S : StringProtocol, V : Hashable
}
/// This is a helper class that works around absence of "package private" access control in Swift
public struct _NavigationLinkProxy<Label, Destination> where Label: View, Destination: View {
public let subject: NavigationLink<Label, Destination>
public init(_ subject: NavigationLink<Label, Destination>) { self.subject = subject }
public var label: Label { subject.label }
public func activate() {
subject.navigationContext!.wrappedValue = AnyView(subject.destination)
}
}

View File

@ -0,0 +1,62 @@
// 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 Jed Fox on 06/30/2020.
//
public struct NavigationView<Content>: View where Content: View {
let content: Content
@State var destination = AnyView(EmptyView())
public init(@ViewBuilder content: () -> Content) {
self.content = content()
}
public var body: Never {
neverBody("NavigationView")
}
}
/// This is a helper class that works around absence of "package private" access control in Swift
public struct _NavigationViewProxy<Content: View> {
public let subject: NavigationView<Content>
public init(_ subject: NavigationView<Content>) { self.subject = subject }
public var content: Content { subject.content }
public var body: some View {
HStack {
content
subject.destination
}.environment(\.navigationDestination, subject.$destination)
}
}
struct NavigationDestinationKey: EnvironmentKey {
public static let defaultValue: Binding<AnyView>? = nil
}
extension EnvironmentValues {
var navigationDestination: Binding<AnyView>? {
get {
self[NavigationDestinationKey.self]
}
set {
self[NavigationDestinationKey.self] = newValue
}
}
}
public let _navigationDestinationKey = \EnvironmentValues.navigationDestination

View File

@ -83,6 +83,8 @@ public typealias HStack = TokamakCore.HStack
public typealias LazyHGrid = TokamakCore.LazyHGrid
public typealias LazyVGrid = TokamakCore.LazyVGrid
public typealias List = TokamakCore.List
public typealias NavigationLink = TokamakCore.NavigationLink
public typealias NavigationView = TokamakCore.NavigationView
public typealias OutlineGroup = TokamakCore.OutlineGroup
public typealias Picker = TokamakCore.Picker
public typealias ScrollView = TokamakCore.ScrollView
@ -112,6 +114,8 @@ public typealias SceneStorage = TokamakCore.SceneStorage
// MARK: Misc
public typealias ViewBuilder = TokamakCore.ViewBuilder
// FIXME: I would put this inside TokamakCore, but for
// some reason it doesn't get exported with the typealias
extension Text {

View File

@ -14,7 +14,6 @@
//
// Created by Carson Katri on 6/29/20.
//
import TokamakCore
extension Path: ViewDeferredToRenderer {

View File

@ -0,0 +1,30 @@
// 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 TokamakCore
extension NavigationLink: ViewDeferredToRenderer {
public var deferredBody: AnyView {
let proxy = _NavigationLinkProxy(self)
return AnyView(
HTML("a", [
"href": "javascript:void%200",
], listeners: [
// FIXME: Focus destination or something so assistive
// technology knows where to look when clicking the link.
"click": { _ in proxy.activate() },
]) { proxy.label }
)
}
}

View File

@ -0,0 +1,29 @@
// 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
extension NavigationView: ViewDeferredToRenderer {
public var deferredBody: AnyView {
AnyView(HTML("div", [
"style": """
display: flex; flex-direction: row; align-items: stretch;
width: 100%; height: 100%;
""",
]) {
_NavigationViewProxy(self).body
})
}
}

View File

@ -59,28 +59,30 @@ public struct ColorDemo: View {
@State private var v2: String = "0.5"
public var body: some View {
VStack {
Button("Input \(colorForm.rawValue.uppercased())") {
colorForm = colorForm == .rgb ? .hsb : .rgb
}
TextField(colorForm == .rgb ? "Red" : "Hue", text: $v0)
TextField(colorForm == .rgb ? "Green" : "Saturation", text: $v1)
TextField(colorForm == .rgb ? "Blue" : "Brightness", text: $v2)
Text("\(v0) \(v1) \(v2)")
.bold()
.padding()
.background(color)
Text("Accent Color: \(Color.accentColor.description)")
.bold()
.padding()
.background(Color.accentColor)
ForEach(colors, id: \.self) {
Text($0.description)
.font(.caption)
ScrollView {
VStack {
Button("Input \(colorForm.rawValue.uppercased())") {
colorForm = colorForm == .rgb ? .hsb : .rgb
}
TextField(colorForm == .rgb ? "Red" : "Hue", text: $v0)
TextField(colorForm == .rgb ? "Green" : "Saturation", text: $v1)
TextField(colorForm == .rgb ? "Blue" : "Brightness", text: $v2)
Text("\(v0) \(v1) \(v2)")
.bold()
.padding()
.background($0)
}
.background(color)
Text("Accent Color: \(Color.accentColor.description)")
.bold()
.padding()
.background(Color.accentColor)
ForEach(colors, id: \.self) {
Text($0.description)
.font(.caption)
.bold()
.padding()
.background($0)
}
}.padding(.horizontal)
}
}
}

View File

@ -28,7 +28,7 @@ struct Counter: View {
let limit: Int
public var body: some View {
@ViewBuilder public var body: some View {
if count.value < limit {
VStack {
Button("Increment") { count.value += 1 }

View File

@ -23,7 +23,7 @@ public struct ForEachDemo: View {
Text("Add item")
}
ForEach(0..<maxItem) {
ForEach(0..<maxItem, id: \.self) {
Text("Item: \($0)")
}
}

View File

@ -23,7 +23,7 @@ struct File: Identifiable {
let children: [File]?
}
@available(OSX 10.16, iOS 14, *)
@available(OSX 10.16, iOS 14.0, *)
struct OutlineGroupDemo: View {
let fs: [File] = [
.init(id: 0, name: "Users", children: [

View File

@ -19,10 +19,14 @@ import TokamakShim
struct SpacerDemo: View {
var body: some View {
HStack {
Text("Left side.")
VStack {
HStack {
Text("Left side.")
Spacer()
Text("Right side.")
}
Spacer()
Text("Right side.")
Text("Forced to bottom.")
}
}
}

View File

@ -17,56 +17,132 @@
import TokamakShim
func title<V>(_ view: V, title: String) -> AnyView where V: View {
if #available(OSX 10.16, iOS 14.0, *) {
return AnyView(view.navigationTitle(title))
} else {
#if !os(macOS)
return AnyView(view.navigationBarTitle(title))
#else
return AnyView(view)
#endif
}
}
struct NavItem: Identifiable {
var id: String
var destination: AnyView?
init<V>(_ id: String, destination: V) where V: View {
self.id = id
self.destination = title(destination.frame(minWidth: 300), title: id)
}
init(unavailiable id: String) {
self.id = id
}
}
var outlineGroupDemo: NavItem {
if #available(OSX 10.16, iOS 14.0, *) {
return NavItem("OutlineGroup", destination: OutlineGroupDemo())
} else {
return NavItem(unavailiable: "OutlineGroup")
}
}
#if !os(macOS)
var listDemo: AnyView {
if #available(iOS 14.0, *) {
return AnyView(ListDemo().listStyle(InsetGroupedListStyle()))
} else {
return AnyView(ListDemo())
}
}
#else
var listDemo = ListDemo()
#endif
var gridDemo: NavItem {
if #available(OSX 10.16, iOS 14.0, *) {
return NavItem("Grid", destination: GridDemo())
} else {
return NavItem(unavailiable: "Grid")
}
}
var appStorageDemo: NavItem {
if #available(OSX 11.0, iOS 14.0, *) {
return NavItem("AppStorage", destination: AppStorageDemo())
} else {
return NavItem(unavailiable: "AppStorage")
}
}
var links: [NavItem] {
[
NavItem("Counter", destination:
Counter(count: Count(value: 5), limit: 15)
.padding()
.background(Color(red: 0.9, green: 0.9, blue: 0.9, opacity: 1.0))
.border(Color.red, width: 3)),
NavItem("ZStack", destination: ZStack {
Text("I'm on bottom")
Text("I'm forced to the top")
.zIndex(1)
Text("I'm on top")
}.padding(20)),
NavItem("ForEach", destination: ForEachDemo()),
NavItem("Text", destination: TextDemo()),
NavItem("Toggle", destination: ToggleDemo()),
NavItem("Path", destination: PathDemo()),
NavItem("TextField", destination: TextFieldDemo()),
NavItem("Spacer", destination: SpacerDemo()),
NavItem("Environment", destination: EnvironmentDemo().font(.system(size: 8))),
NavItem("Picker", destination: PickerDemo()),
NavItem("List", destination: listDemo),
outlineGroupDemo,
NavItem("Color", destination: ColorDemo()),
appStorageDemo,
gridDemo,
]
}
struct TokamakDemoView: View {
var body: some View {
ScrollView(showsIndicators: true) {
HStack {
Spacer()
}
VStack {
Group {
Counter(count: Count(value: 5), limit: 15)
.padding()
.background(Color(red: 0.9, green: 0.9, blue: 0.9, opacity: 1.0))
.border(Color.red, width: 3)
ZStack {
Text("I'm on bottom")
Text("I'm forced to the top")
.zIndex(1)
Text("I'm on top")
}
.padding(20)
}
Group {
if #available(OSX 11, iOS 14, *) {
AppStorageDemo()
}
ForEachDemo()
TextDemo()
ToggleDemo()
PathDemo()
TextFieldDemo()
SpacerDemo()
EnvironmentDemo()
.font(.system(size: 8))
PickerDemo()
}
Group {
#if canImport(TokamakDOM)
ListDemo().listStyle(InsetGroupedListStyle())
#else
ListDemo()
#endif
if #available(OSX 10.16, iOS 14.0, *) {
OutlineGroupDemo()
}
ColorDemo()
.padding()
if #available(OSX 10.16, iOS 14.0, *) {
GridDemo()
}
}
NavigationView { () -> AnyView in
let list = title(
List(links) { link in
if let dest = link.destination {
NavigationLink(link.id, destination: HStack {
Spacer()
dest
Spacer()
})
} else {
#if os(WASI)
Text(link.id)
#else
HStack {
Text(link.id)
Spacer()
Text("unavailable").opacity(0.5)
}
#endif
}
}
.frame(minHeight: 300),
title: "Demos"
)
#if os(WASI)
return AnyView(list)
#else
if #available(iOS 14.0, *) {
return AnyView(list.listStyle(SidebarListStyle()))
} else {
return AnyView(list)
}
#endif
}
.environmentObject(TestEnvironment())
}

View File

@ -33,7 +33,7 @@ Table columns:
| | | |
| --- | ------------------------------------------------------------------------------------------------ | :-: |
| 🚧 | [Button](https://developer.apple.com/documentation/swiftui/button) | |
| | [NavigationLink](https://developer.apple.com/documentation/swiftui/navigationlink) | |
| 🚧 | [NavigationLink](https://developer.apple.com/documentation/swiftui/navigationlink) | |
| | [EditButton](https://developer.apple.com/documentation/swiftui/editbutton) | |
| | [PasteButton](https://developer.apple.com/documentation/swiftui/pastebutton) | |
| | [SignInWithAppleButton](https://developer.apple.com/documentation/swiftui/signinwithapplebutton) | β |
@ -117,7 +117,7 @@ Table columns:
| | | |
| --- | ---------------------------------------------------------------------------------- | :-: |
| | [NavigationView](https://developer.apple.com/documentation/swiftui/navigationview) | |
| 🚧 | [NavigationView](https://developer.apple.com/documentation/swiftui/navigationview) | |
| | [TabView](https://developer.apple.com/documentation/swiftui/tabview) | |
| | [HSplitView](https://developer.apple.com/documentation/swiftui/hsplitview) | |
| | [VSplitView](https://developer.apple.com/documentation/swiftui/vsplitview) | |