Skip to content

Instantly share code, notes, and snippets.

@swiftui-lab
Last active February 10, 2023 03:12
Show Gist options
  • Star 28 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • 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)
}
}
}
@getflourish
Copy link

getflourish commented Apr 20, 2021

How can I use this in a ContentView.swift?

I’ve tried:

import SwiftUI

@available(OSX 11.0, *)
struct ContentView: View {
  
  var body: some View {
    WebView(data: WebViewData())
  }
}

But I get the following error:

[Process] 0x7fd4e0044820 - [pageProxyID=5, webPageID=6, PID=71360] WebPageProxy::tryReloadAfterProcessTermination: process crashed and the client did not handle it, not reloading the page because we reached the maximum number of attempts

@getflourish
Copy link

Ah, okay, I need to enable these in "Signing & Capabilities"
image

@getflourish
Copy link

Thank you, this works!

@BrunoCerberus
Copy link

Soooo, how do i use this? any examples?

@getflourish
Copy link

getflourish commented Jul 7, 2021

@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

@alelordelo
Copy link

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?

@getflourish
Copy link

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!

@alelordelo
Copy link

alelordelo commented Feb 4, 2022

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

@getflourish
Copy link

getflourish commented Feb 4, 2022

@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:

  1. Add files to …
  2. Choose your folder with your HTML, etc.
  3. 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")))
  }
}

@getflourish
Copy link

getflourish commented Feb 4, 2022

Here’s a screenshot of the WebView that loads index.html which then loads the JavaScript file that prints "Hello World". :)

Bildschirmfoto 2022-02-04 um 15 28 19

@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