Skip to content

Commit c513e50

Browse files
committed
Implement WebView (only AppKitBackend and UIKitBackend for now)
1 parent f58b757 commit c513e50

File tree

9 files changed

+212
-3
lines changed

9 files changed

+212
-3
lines changed

Examples/Bundler.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,8 @@ version = '0.1.0'
5454
identifier = 'dev.swiftcrossui.PathsExample'
5555
product = 'PathsExample'
5656
version = '0.1.0'
57+
58+
[apps.WebViewExample]
59+
identifier = 'dev.swiftcrossui.WebViewExample'
60+
product = 'WebViewExample'
61+
version = '0.1.0'

Examples/Package.resolved

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Examples/Package.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,10 @@ let package = Package(
6868
.executableTarget(
6969
name: "PathsExample",
7070
dependencies: exampleDependencies
71+
),
72+
.executableTarget(
73+
name: "WebViewExample",
74+
dependencies: exampleDependencies
7175
)
7276
]
7377
)
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import Foundation
2+
import SwiftCrossUI
3+
import DefaultBackend
4+
5+
#if canImport(SwiftBundlerRuntime)
6+
import SwiftBundlerRuntime
7+
#endif
8+
9+
@main
10+
@HotReloadable
11+
struct WebViewApp: App {
12+
@State var urlInput = "https://stackotter.dev"
13+
14+
@State var url = URL(string: "https://stackotter.dev")!
15+
16+
var body: some Scene {
17+
WindowGroup("WebViewExample") {
18+
#hotReloadable {
19+
VStack {
20+
HStack {
21+
TextField("URL", text: $urlInput)
22+
Button("Go") {
23+
guard let url = URL(string: urlInput) else {
24+
return // disabled
25+
}
26+
27+
self.url = url
28+
}.disabled(URL(string: urlInput) == nil)
29+
}
30+
.padding()
31+
32+
WebView($url)
33+
.onChange(of: url) {
34+
urlInput = url.absoluteString
35+
}
36+
}
37+
}
38+
}
39+
}
40+
}

Package.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,15 +100,15 @@ let package = Package(
100100
),
101101
.package(
102102
url: "https://github.com/stackotter/swift-windowsappsdk",
103-
branch: "5caed8b4f1b4abc6fc89b8f0a8fa20f3edfab14a"
103+
branch: "ed938db0b9790b36391dc91b20cee81f2410309f"
104104
),
105105
.package(
106106
url: "https://github.com/thebrowsercompany/swift-windowsfoundation",
107107
branch: "main"
108108
),
109109
.package(
110110
url: "https://github.com/stackotter/swift-winui",
111-
branch: "fad446caf8f40370d82a043ec293646023e07e61"
111+
branch: "927e2c46430cfb1b6c195590b9e65a30a8fd98a2"
112112
),
113113
// .package(
114114
// url: "https://github.com/stackotter/TermKit",

Sources/AppKitBackend/AppKitBackend.swift

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import AppKit
22
import SwiftCrossUI
3+
import WebKit
34

45
extension App {
56
public typealias Backend = AppKitBackend
@@ -1481,6 +1482,27 @@ public final class AppKitBackend: AppBackend {
14811482

14821483
widget.needsDisplay = true
14831484
}
1485+
1486+
public func createWebView() -> Widget {
1487+
let webView = CustomWKWebView()
1488+
webView.navigationDelegate = webView.strongNavigationDelegate
1489+
return webView
1490+
}
1491+
1492+
public func updateWebView(
1493+
_ webView: Widget,
1494+
environment: EnvironmentValues,
1495+
onNavigate: @escaping (URL) -> Void
1496+
) {
1497+
let webView = webView as! CustomWKWebView
1498+
webView.strongNavigationDelegate.onNavigate = onNavigate
1499+
}
1500+
1501+
public func navigateWebView(_ webView: Widget, to url: URL) {
1502+
let webView = webView as! CustomWKWebView
1503+
let request = URLRequest(url: url)
1504+
webView.load(request)
1505+
}
14841506
}
14851507

14861508
final class NSCustomTapGestureTarget: NSView {
@@ -1908,3 +1930,20 @@ final class NSDisabledScrollView: NSScrollView {
19081930
self.nextResponder?.scrollWheel(with: event)
19091931
}
19101932
}
1933+
1934+
final class CustomWKWebView: WKWebView {
1935+
var strongNavigationDelegate = CustomWKNavigationDelegate()
1936+
}
1937+
1938+
final class CustomWKNavigationDelegate: NSObject, WKNavigationDelegate {
1939+
var onNavigate: ((URL) -> Void)?
1940+
1941+
func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
1942+
guard let url = webView.url else {
1943+
print("warning: Web view has no URL")
1944+
return
1945+
}
1946+
1947+
onNavigate?(url)
1948+
}
1949+
}

Sources/SwiftCrossUI/Backend/AppBackend.swift

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,7 @@ public protocol AppBackend {
587587
)
588588

589589
// MARK: Paths
590+
590591
/// Create a widget that can contain a path.
591592
func createPathWidget() -> Widget
592593
/// Create a path. It will not be shown until ``renderPath(_:container:)`` is called.
@@ -624,6 +625,20 @@ public protocol AppBackend {
624625
fillColor: Color,
625626
overrideStrokeStyle: StrokeStyle?
626627
)
628+
629+
// MARK: Web view
630+
631+
/// Create a web view.
632+
func createWebView() -> Widget
633+
/// Update a web view to reflect the given environment and use the given
634+
/// navigation handler.
635+
func updateWebView(
636+
_ webView: Widget,
637+
environment: EnvironmentValues,
638+
onNavigate: @escaping (URL) -> Void
639+
)
640+
/// Navigates a web view to a given URL.
641+
func navigateWebView(_ webView: Widget, to url: URL)
627642
}
628643

629644
extension AppBackend {
@@ -1025,4 +1040,21 @@ extension AppBackend {
10251040
) {
10261041
todo()
10271042
}
1043+
1044+
public func createWebView() -> Widget {
1045+
todo()
1046+
}
1047+
public func updateWebView(
1048+
_ webView: Widget,
1049+
environment: EnvironmentValues,
1050+
onNavigate: @escaping (URL) -> Void
1051+
) {
1052+
todo()
1053+
}
1054+
public func navigateWebView(
1055+
_ webView: Widget,
1056+
to url: URL
1057+
) {
1058+
todo()
1059+
}
10281060
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import Foundation
2+
3+
public struct WebView: ElementaryView {
4+
@State var currentURL: URL?
5+
@Binding var url: URL
6+
7+
public init(_ url: Binding<URL>) {
8+
_url = url
9+
}
10+
11+
func asWidget<Backend: AppBackend>(backend: Backend) -> Backend.Widget {
12+
backend.createWebView()
13+
}
14+
15+
func update<Backend: AppBackend>(
16+
_ widget: Backend.Widget,
17+
proposedSize: SIMD2<Int>,
18+
environment: EnvironmentValues,
19+
backend: Backend,
20+
dryRun: Bool
21+
) -> ViewUpdateResult {
22+
if !dryRun {
23+
if url != currentURL {
24+
backend.navigateWebView(widget, to: url)
25+
currentURL = url
26+
}
27+
backend.updateWebView(widget, environment: environment) { destination in
28+
currentURL = destination
29+
url = destination
30+
}
31+
backend.setSize(of: widget, to: proposedSize)
32+
}
33+
34+
return ViewUpdateResult(
35+
size: ViewSize(
36+
size: proposedSize,
37+
idealSize: SIMD2(10, 10),
38+
minimumWidth: 0,
39+
minimumHeight: 0,
40+
maximumWidth: nil,
41+
maximumHeight: nil
42+
),
43+
childResults: []
44+
)
45+
}
46+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import WebKit
2+
import SwiftCrossUI
3+
4+
extension UIKitBackend {
5+
public func createWebView() -> Widget {
6+
WebViewWidget()
7+
}
8+
9+
public func updateWebView(
10+
_ webView: Widget,
11+
environment: EnvironmentValues,
12+
onNavigate: @escaping (URL) -> Void
13+
) {
14+
let webView = webView as! WebViewWidget
15+
webView.onNavigate = onNavigate
16+
}
17+
18+
public func navigateWebView(_ webView: Widget, to url: URL) {
19+
let webView = webView as! WebViewWidget
20+
let request = URLRequest(url: url)
21+
webView.child.load(request)
22+
}
23+
}
24+
25+
/// A wrapper for WKWebView. Acts as the web view's delegate as well.
26+
final class WebViewWidget: WrapperWidget<WKWebView>, WKNavigationDelegate {
27+
var onNavigate: ((URL) -> Void)?
28+
29+
init() {
30+
super.init(child: WKWebView())
31+
32+
child.navigationDelegate = self
33+
}
34+
35+
func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
36+
guard let url = webView.url else {
37+
print("warning: Web view has no URL")
38+
return
39+
}
40+
41+
onNavigate?(url)
42+
}
43+
}

0 commit comments

Comments
 (0)