Skip to content

Instantly share code, notes, and snippets.

@swiftui-lab
Last active February 10, 2023 03:12
Show Gist options
  • Save swiftui-lab/a873bf413770db6fd1a525fa424ce8cd to your computer and use it in GitHub Desktop.
Save swiftui-lab/a873bf413770db6fd1a525fa424ce8cd to your computer and use it in GitHub Desktop.
import SwiftUI
import WebKit
import Combine
class WebViewData: ObservableObject {
@Published var loading: Bool = false
@Published var scrollPercent: Float = 0
@Published var url: URL? = nil
@Published var urlBar: String = "https://nasa.gov"
var scrollOnLoad: Float? = nil
}
#if os(macOS)
struct WebView: NSViewRepresentable {
@ObservedObject var data: WebViewData
func makeNSView(context: Context) -> WKWebView {
return context.coordinator.webView
}
func updateNSView(_ nsView: WKWebView, context: Context) {
guard context.coordinator.loadedUrl != data.url else { return }
context.coordinator.loadedUrl = data.url
if let url = data.url {
DispatchQueue.main.async {
let request = URLRequest(url: url)
nsView.load(request)
}
}
context.coordinator.data.url = data.url
}
func makeCoordinator() -> WebViewCoordinator {
return WebViewCoordinator(data: data)
}
}
#else
struct WebView: UIViewRepresentable {
@ObservedObject var data: WebViewData
func makeUIView(context: Context) -> WKWebView {
return context.coordinator.webView
}
func updateUIView(_ uiView: WKWebView, context: Context) {
guard context.coordinator.loadedUrl != data.url else { return }
context.coordinator.loadedUrl = data.url
if let url = data.url {
DispatchQueue.main.async {
let request = URLRequest(url: url)
uiView.load(request)
}
}
context.coordinator.data.url = data.url
}
func makeCoordinator() -> WebViewCoordinator {
return WebViewCoordinator(data: data)
}
}
#endif
class WebViewCoordinator: NSObject, WKNavigationDelegate {
@ObservedObject var data: WebViewData
var webView: WKWebView = WKWebView()
var loadedUrl: URL? = nil
init(data: WebViewData) {
self.data = data
super.init()
self.setupScripts()
webView.navigationDelegate = self
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
DispatchQueue.main.async {
if let scrollOnLoad = self.data.scrollOnLoad {
self.scrollTo(scrollOnLoad)
self.data.scrollOnLoad = nil
}
self.data.loading = false
if let urlstr = webView.url?.absoluteString {
self.data.urlBar = urlstr
}
}
}
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
DispatchQueue.main.async { self.data.loading = true }
}
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
showError(title: "Navigation Error", message: error.localizedDescription)
DispatchQueue.main.async { self.data.loading = false }
}
func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
showError(title: "Loading Error", message: error.localizedDescription)
DispatchQueue.main.async { self.data.loading = false }
}
func scrollTo(_ percent: Float) {
let js = "scrollToPercent(\(percent))"
webView.evaluateJavaScript(js)
}
func setupScripts() {
let monitor = WKUserScript(source: ScrollMonitorScript.monitorScript,
injectionTime: .atDocumentEnd,
forMainFrameOnly: true)
let scrollTo = WKUserScript(source: ScrollMonitorScript.scrollTo,
injectionTime: .atDocumentEnd,
forMainFrameOnly: true)
webView.configuration.userContentController.addUserScript(monitor)
webView.configuration.userContentController.addUserScript(scrollTo)
let msgHandler = ScrollMonitorScript { percent in
DispatchQueue.main.async {
self.data.scrollPercent = percent
}
}
webView.configuration.userContentController.add(msgHandler, contentWorld: .page, name: "notifyScroll")
}
func showError(title: String, message: String) {
#if os(macOS)
let alert: NSAlert = NSAlert()
alert.messageText = title
alert.informativeText = message
alert.alertStyle = .warning
alert.runModal()
#else
print("\(title): \(message)")
#endif
}
}
class ScrollMonitorScript: NSObject, WKScriptMessageHandler {
let callback: (Float) -> ()
static var monitorScript: String {
return """
let last_known_scroll_position = 0;
let ticking = false;
function getScrollPercent() {
var docu = document.documentElement;
let t = docu.scrollTop;
let h = docu.scrollHeight;
let ch = docu.clientHeight
return (t / (h - ch)) * 100;
}
window.addEventListener('scroll', function(e) {
window.webkit.messageHandlers.notifyScroll.postMessage(getScrollPercent());
});
"""
}
static var scrollTo: String {
return """
function scrollToPercent(pct) {
var docu = document.documentElement;
let h = docu.scrollHeight;
let ch = docu.clientHeight
let t = (pct * (h - ch)) / 100;
window.scrollTo(0, t);
}
"""
}
init(callback: @escaping (Float) -> ()) {
self.callback = callback
}
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
if let percent = message.body as? NSNumber {
self.callback(percent.floatValue)
}
}
}
@alelordelo
Copy link

thanks @getflourish

But did you get it working with some HTML/JS, or just the Hello World?

I tried, but got this error:
Hello World
File not found
SwiftUIWebP5js/ContentView.swift:18: Fatal error: Unexpectedly found nil while unwrapping an Optional value
2022-02-07 11:51:58.661144+0100 SwiftUIWebP5js[17460:720071] SwiftUIWebP5js/ContentView.swift:18: Fatal error: Unexpectedly found nil while unwrapping an Optional value
(lldb)

Which is weird, because my HTML opens fine when clicked:
https://gyazo.com/d64b5771aa89d5e03bafe068545a0cd3

I pushed updated changes:
https://github.com/alelordelo/SwiftUIWebP5js

And here is the www folder with the HTML/JS
https://drive.google.com/drive/folders/1jkgIW5mSy-Xlxsb2bsfQ2UL1xO8NfddL?usp=sharing

Could totally be the case that I am doing something stupid, as I am zero with anything web/HTML/JS. ☺️

@getflourish
Copy link

getflourish commented Feb 7, 2022

@alelordelo Good that you simplified your project. When I open your project, it opens with "iOS" as the build target which I changed to "macOS".

image

I then moved the p5.js into www and changed the paths in the index.html to load the scripts relative to the HTML:

<script src="./p5.js"></script>
<script src="./sketch.js"></script>

After doing that, Xcode complained that it didn’t find the index.html.

I then removed the www folder and once again did Right Click → Add files to … and added the www folder. Then it worked. TBH I don’t understand how these folders are supposed to work but managing them through Finder alone doesn’t seem to be working correctly?

image

Here’s the result:

image

@alelordelo
Copy link

Thanks again @getflourish!

I tried about 30 times (not kidding), with all possible configs: adding folders again like you mentioned, clean derived data, clean build folder, restart macOS, etc... Nothing works! 🥵

Would you mind sharing your project?

@getflourish
Copy link

@alelordelo
Copy link

now worked @getflourish !
awesome, thanks man!

@getflourish
Copy link

@alelordelo Great! Mind sharing what you’re working on where you want to run p5.js inside a Swift app? Access to native features?

@alelordelo
Copy link

sure, happy to share @getflourish . Do you use slack, or other message app?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment