-
-
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) | |
} | |
} | |
} | |
Thank you, this works!
Soooo, how do i use this? any examples?
@BrunoCerberus: Here’s a tutorial with screenshots on how to use the code found here. I simplified the code as much as I understood to only get a website show up in the WebView.
https://florianschulz.info/portfolio/writing/wrapping-websites-in-webviews-using-swiftui
Thanks for sharing tjis @swiftui-lab!
@getflourish, tiur tutorial worked perfectly for remote urls.
I am trying to do the same with a local html/JavaScript that is a folder inside Xcode.
The goal is to get a P5.js Sketch like the example bellow inside a SwiftUI view, with the files packages into the app.
Any idea how to do this?
Hi @alelordelo
Thanks for trying my tutorial. I wanted to try it with local files for a while and will give it a try. Will report back!
thanks @getflourish!
This guy did a great tutorial on how t get a SwiftUI WebView working with local files:
https://medium.com/@mdyamin/swiftui-mastering-webview-5790e686833e
And also this
https://medium.com/@mdyamin/swiftui-mastering-webview-5790e686833e
TLRD...
Instead of:
private var url: URL? = URL(string: "https:www.google.com")
We use:
private var url: URL? = Bundle.main.url(forResource: "index", withExtension: "html", subdirectory: "www")
However, I cannot get the html +JS to work. Bellow is my complete repo:
https://github.com/alelordelo/SwiftUIWebP5js
@alelordelo What’s your issue? I downloaded your project and had some issues with the www
folder. I don’t know exactly what was wrong, but I managed to make it work. I first tried to just copy some files into the project using Finder. But it somehow didn’t work. Also, your folder was inside Shared
which might be yet another subdirectory. Anyway here’s what worked for me:
- Add files to …
- Choose your folder with your HTML, etc.
- Make sure to check "Copy items if needed" and "Add to targets: … (macOS)"
I then modified your ContentView.swift
to use a function that will print if the file wasn’t found. That helped me to figure out that the file didn’t exist or wasn’t in the right place. I think this isn’t necessary and you can keep your original code if you know that the file will exist. My code doesn’t actually prevent a crash 😂
import SwiftUI
@available(OSX 11.0, *)
struct ContentView: View {
func bundleURL(fileName: String, fileExtension: String) -> URL {
if let fileURL = Bundle.main.url(forResource: fileName, withExtension: fileExtension, subdirectory: "www") {
return fileURL
} else {
print("File not found")
return URL(string: "")!
}
}
init() {
print("Hello World")
}
var body: some View {
WebView(data: WebViewData(url: self.bundleURL(fileName: "index", fileExtension: "html")))
}
}
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.
@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".
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?
Here’s the result:
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?
now worked @getflourish !
awesome, thanks man!
@alelordelo Great! Mind sharing what you’re working on where you want to run p5.js inside a Swift app? Access to native features?
sure, happy to share @getflourish . Do you use slack, or other message app?
How can I use this in a
ContentView.swift
?I’ve tried:
But I get the following error: