How do I inject CSS/JS in a WKWebView using SwiftUI? - swift

I'm new to SwiftUI and trying to inject some custom CSS/JS into a page loaded with WKWebView:
import SwiftUI
import WebKit
struct WebView: UIViewRepresentable {
func makeUIView(context: Context) -> WKWebView {
return WKWebView()
}
func updateUIView(_ webView: WKWebView, context: Context) {
let request = URLRequest(url: URL(string: "https://example.com")!)
WKWebsiteDataStore.default().removeData(ofTypes: [WKWebsiteDataTypeDiskCache, WKWebsiteDataTypeMemoryCache], modifiedSince: Date(timeIntervalSince1970: 0), completionHandler:{ })
webView.load(request)
webView.configuration.userContentController.addUserScript(WKUserScript( source: "alert('debug')", injectionTime: .atDocumentEnd, forMainFrameOnly: true))
}
}
Which is load like this:
struct ContentView: View {
var body: some View {
WebView()
}
}
Sadly, the code doesn't seem to actually inject anything. I've tried running it before webView.load as well. Having been googling quite a bit, I only see examples done in UIKit and unfortunately, I'm too inexperienced to wrap UIKit in a way that I can use with SwiftUI.
Any guidance would be greatly appreciated.

First of all try to avoid including business code in your views whenever you can. You may use two functions in the Webkit API if you want to include/inject JS to the webview content: EvaluateJS and AddUserScript. You may use "AddUserScript" before the "load" starts. Also please not that "alert" function in JS, would not work in current Mobile Safari. You should have see the text colors to appear in blue with the script below.
Result:
import SwiftUI
import WebKit
struct ContentView: View {
var body: some View {
VStack {
CustomWebview()
}
.padding()
}
}
struct SwiftUIWebView: UIViewRepresentable {
typealias UIViewType = WKWebView
let webView: WKWebView
func makeUIView(context: Context) -> WKWebView {
webView
}
func updateUIView(_ uiView: WKWebView, context: Context) {
}
}
final class SwiftUIWebViewModel: ObservableObject {
#Published var addressStr = "https://www.stackoverflow.com"
let webView: WKWebView
init() {
webView = WKWebView(frame: .zero)
loadUrl()
}
func loadUrl() {
guard let url = URL(string: addressStr) else {
return
}
webView.configuration.userContentController.addUserScript(WKUserScript( source: """
window.userscr ="hey this is prior injection";
""", injectionTime: .atDocumentStart, forMainFrameOnly: false))
webView.load(URLRequest(url: url))
// You will have the chance in 8 seconds to open Safari debugger if needed. PS: Also put a breakpoint to injectJS function.
DispatchQueue.main.asyncAfter(deadline: .now() + 8.0) {
self.injectJS()
}
}
func injectJS () {
webView.evaluateJavaScript("""
window.temp = "hey here!";
document.getElementById("content").style.color = "blue";
""")
}
}
struct CustomWebview: View {
#StateObject private var model = SwiftUIWebViewModel()
var body: some View {
VStack {
SwiftUIWebView(webView: model.webView)
.padding()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

Related

Can't see buttons at top of webView

I can't see the buttons that should be on top of the web view that make it go back and forward. I don't know if this is a web view taking up the whole screen problem, or something else. I've tried making a new view with the buttons and putting the view in there. I don't know.
Here is my code:
import WebKit
import SwiftUI
struct Webview : UIViewRepresentable {
let request: URLRequest
var webview: WKWebView?
init(web: WKWebView?, req: URLRequest) {
self.webview = WKWebView()
self.request = req
}
func makeUIView(context: Context) -> WKWebView {
return webview!
}
func updateUIView(_ uiView: WKWebView, context: Context) {
uiView.load(request)
}
func goBack(){
webview?.goBack()
}
func goForward(){
webview?.goForward()
}
}
struct ContentView: View {
#State var searchURL = "https://google.com"
var body: some View {
VStack {
HStack {
//MARK: BACK BUTTON
Button(action: {
Webview(web: nil, req: URLRequest(url: URL(string: "\(searchURL.prefix(4) == "http" ? searchURL : "https://\(searchURL)")")!)).goBack()
}) {
Image("arrowshape.turn.up.backward")
}
//MARK: FOREWARD BUTTON
Button(action: {
Webview(web: nil, req: URLRequest(url: URL(string: "\(searchURL.prefix(4) == "http" ? searchURL : "https://\(searchURL)")")!)).goForward()
}) {
Image("arrowshape.turn.up.foreward")
}
}
Webview(web: nil, req: URLRequest(url: URL(string: "\(searchURL.prefix(4) == "http" ? searchURL : "https://\(searchURL)")")!))
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
The button action should not be a view in itself but should just update a variable that stores the URL to be displayed by the webview. However that is not required.
Take a look at this. Should cover all the scenarios you need without the need to manually manager URLs

How to disable zoom/pinch in webView of ios14?

I am currently doing a project to convert website to ios application and successfully created it. The only problem is that the application can be zoom in and out. One more thing, whenever any form is click, the form will be zoomed. I am new to xcode (1 week of knowledge) pls guide me. Thanks!
Below is my current code.
WebView.swift
import SwiftUI
import WebKit
import UIKit
class ViewController: UIViewController{
override func viewDidLoad() {
super.viewDidLoad()
}
}
struct SwiftUIWebView: UIViewRepresentable {
let url: URL?
func makeUIView(context: Context) -> WKWebView {
let prefs = WKWebpagePreferences()
prefs.allowsContentJavaScript = true
let config = WKWebViewConfiguration()
config.defaultWebpagePreferences = prefs
return WKWebView(
frame: .zero,
configuration: config
)
}
func updateUIView(_ uiView: WKWebView, context: Context) {
guard let myURL = url else {
return
}
let request = URLRequest (url: myURL)
uiView.load(request)
}
}
ContentView.swift
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView{
SwiftUIWebView(url: URL(string: "THE URL"))
.navigationTitle("NAVIGATION NAME")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

Sheet and alert keeps refreshing my page for WKWebView SwiftUI

I am currently using WKWebView and I added a sheet and an alert inside my wkwebview. However, every time I close the sheet or close the alarm, My WkWebView keeps navigating(loading) back to the origin domain that I specified (for this case, google.com). I an wondering how I can fix this issue.
WebView
import SwiftUI
import WebKit
struct WebView : UIViewRepresentable {
let request: URLRequest
func makeUIView(context: Context) -> WKWebView {
return WKWebView()
}
func updateUIView(_ uiView: WKWebView, context: Context) {
uiView.load(request)
}
}
Main View
struct MainView: View {
var webView: WebView = WebView(request: URLRequest(url: URL(string: "https://www.google.com")!))
var body: some View {
VStack(){
AlertView()
webView
SheetScreenView(webView: $webView)
}
}
AlertView()
...
Button(action: {
}, label: {}..alert(isPresented: $isBookmarked) {
Alert(title: Text(" \(webView.getURL())"), dismissButton: .default(Text("Ok")))
}
...
SheetScreenView
....
#State private var showingSheet = false
var body: some View {
Button("Show Sheet") {
showingSheet.toggle()
}
.sheet(isPresented: $showingSheet) {
SheetView()
}
}
...
SheetView
struct SheetView: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
Button("Press to dismiss") {
presentationMode.wrappedValue.dismiss()
}
.font(.title)
.padding()
.background(Color.black)
}
}
Your issue can be reproduced even without the sheet and alert by just rotating the device. The problem is that in updateView (which is going to get called often), you call load on the URLRequest. This is going to get compounded by the fact that you're storing a view in a var which is going to get recreated on every new render of MainView, since Views in SwiftUI are transient.
The simplest way to avoid this if your URL isn't going to change is to just call load in makeUIView:
struct WebView : UIViewRepresentable {
var request: URLRequest
func makeUIView(context: Context) -> WKWebView {
let webView = WKWebView()
webView.load(request)
return webView
}
func updateUIView(_ uiView: WKWebView, context: Context) {
}
}
struct ContentView: View {
var body: some View {
WebView(request: URLRequest(url:URL(string: "https://www.google.com")!))
}
}
If your URL may change, you need a way to compare the previous state with the new one. This seems like the shortest (but certainly not only) way to do this:
struct WebView : UIViewRepresentable {
var url: URL
#State private var prevURL : URL?
func makeUIView(context: Context) -> WKWebView {
return WKWebView()
}
func updateUIView(_ uiView: WKWebView, context: Context) {
if (prevURL != url) {
let request = URLRequest(url: url)
uiView.load(request)
DispatchQueue.main.async { prevURL = url }
}
}
}
struct ContentView: View {
var body: some View {
WebView(url: URL(string: "https://www.google.com")!)
}
}

Implement webkit with swiftUI on macOS (And create a preview of a webpage)

I'm trying to create a menubar application which shows the inbox of this site. I would like to make an easy function that opens a small popup with the url of the item (without opening safari). An inbox item would look something like this
struct InboxItem: View {
#State var MesgSite: String = "https://duckduckgo.com"
#State private var showSafari = false
var body: some View {
VStack(alignment: .leading) {
Text("some text")
.background(SelectColor.opacity(0.5))
.onLongPressGesture {
//show preview of the MesgSite here
self.SelectColor = .blue
self.showSafari.toggle()
}.popover(isPresented: self.$showSafari) {
SafariPreview()
}
}
}
struct SafariPreview: View {
var body: some View {
VStack {
Text("Display the webpage here")
.padding()
}.frame(maxWidth: 533, maxHeight: 300)
}
}
I would like to, when one longpresses on the item, it should make a preview of the associated webpage just like in the default mail app on macOS like so:
Got the popup working now
I have tried adding a wkwebview as well as a SafariView in a NSViewRepresentable, however I got (among similar) the following error message using code from this SO post
Use of undeclared type 'UIViewRepresentable'
TIA!
Edit:
The most basic version of the project can be found here on github
Edit 2:
Gave more focus to the question
I have also tried to come up with a solution. Since I couldn't find any documentation on this online whatsoever I'll give the solution that I've found by trial and error.
First, as it turns out UI... has its counterpart on macOS called NS.... Thus UIViewRepresentable would be NSViewRepresentable on macOS. Next I found this SO question which had an example of a WKWebview on macOS. By combining that code with this answer on another SO question I could also detect the url change as well know when the view was done loading.
The SwiftUI WebView on macOS
This resulted in the following code. For clarity, I suggest putting it in a different file like WebView.swift:
First, import the needed packages:
import SwiftUI
import WebKit
import Combine
Then create a model that holds the data that you want to be able to access in your SwiftUI views:
class WebViewModel: ObservableObject {
#Published var link: String
#Published var didFinishLoading: Bool = false
#Published var pageTitle: String
init (link: String) {
self.link = link
self.pageTitle = ""
}
}
Lastly, create the struct with NSViewRepresentable that will be the HostingViewController of the WebView() like so:
struct SwiftUIWebView: NSViewRepresentable {
public typealias NSViewType = WKWebView
#ObservedObject var viewModel: WebViewModel
private let webView: WKWebView = WKWebView()
public func makeNSView(context: NSViewRepresentableContext<SwiftUIWebView>) -> WKWebView {
webView.navigationDelegate = context.coordinator
webView.uiDelegate = context.coordinator as? WKUIDelegate
webView.load(URLRequest(url: URL(string: viewModel.link)!))
return webView
}
public func updateNSView(_ nsView: WKWebView, context: NSViewRepresentableContext<SwiftUIWebView>) { }
public func makeCoordinator() -> Coordinator {
return Coordinator(viewModel)
}
class Coordinator: NSObject, WKNavigationDelegate {
private var viewModel: WebViewModel
init(_ viewModel: WebViewModel) {
//Initialise the WebViewModel
self.viewModel = viewModel
}
public func webView(_: WKWebView, didFail: WKNavigation!, withError: Error) { }
public func webView(_: WKWebView, didFailProvisionalNavigation: WKNavigation!, withError: Error) { }
//After the webpage is loaded, assign the data in WebViewModel class
public func webView(_ web: WKWebView, didFinish: WKNavigation!) {
self.viewModel.pageTitle = web.title!
self.viewModel.link = web.url?.absoluteString as! String
self.viewModel.didFinishLoading = true
}
public func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) { }
public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
decisionHandler(.allow)
}
}
}
This code can be used as follows:
struct ContentView: View {
var body: some View {
//Pass the url to the SafariWebView struct.
SafariWebView(mesgURL: "https://stackoverflow.com/")
}
}
struct SafariWebView: View {
#ObservedObject var model: WebViewModel
init(mesgURL: String) {
//Assign the url to the model and initialise the model
self.model = WebViewModel(link: mesgURL)
}
var body: some View {
//Create the WebView with the model
SwiftUIWebView(viewModel: model)
}
}
Create a Safari Preview
So now we have this knowledge it is relatively easy to recreate the handy safari preview.
To have that, make sure to add #State private var showSafari = false (which will be toggled when you want to show the preview) to the view that will call the preview.
Also add the .popover(isPresented: self.$showSafari) { ... to show the preview
struct ContentView: View {
#State private var showSafari = false
var body: some View {
VStack(alignment: .leading) {
Text("Press me to get a preview")
.padding()
}
.onLongPressGesture {
//Toggle to showSafari preview
self.showSafari.toggle()
}//if showSafari is true, create a popover
.popover(isPresented: self.$showSafari) {
//The view inside the popover is made of the SafariPreview
SafariPreview(mesgURL: "https://duckduckgo.com/")
}
}
}
Now the SafariPreview struct will look like this:
struct SafariPreview: View {
#ObservedObject var model: WebViewModel
init(mesgURL: String) {
self.model = WebViewModel(link: mesgURL)
}
var body: some View {
//Create a VStack that contains the buttons in a preview as well a the webpage itself
VStack {
HStack(alignment: .center) {
Spacer()
Spacer()
//The title of the webpage
Text(self.model.didFinishLoading ? self.model.pageTitle : "")
Spacer()
//The "Open with Safari" button on the top right side of the preview
Button(action: {
if let url = URL(string: self.model.link) {
NSWorkspace.shared.open(url)
}
}) {
Text("Open with Safari")
}
}
//The webpage itself
SwiftUIWebView(viewModel: model)
}.frame(width: 800, height: 450, alignment: .bottom)
.padding(5.0)
}
}
The result looks like this:
Answer of Chiel is great!
But, possibly, someone search for html renderer view without full browser features
WORKS FOR MACOS
import SwiftUI
import WebKit
struct WebView: View {
#Binding var html: String
var body: some View {
WebViewWrapper(html: html)
}
}
struct WebViewWrapper: NSViewRepresentable {
let html: String
func makeNSView(context: Context) -> WKWebView {
return WKWebView()
}
func updateNSView(_ nsView: WKWebView, context: Context) {
nsView.loadHTMLString(html, baseURL: nil)
}
}
usage:
#State var text = "<html><body><h1>Hello World</h1><p><strong>hell</strong> yeah!</p></body></html>"
//......
TextField("", text: $text)
Divider()
WebView(html: $text)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)

A link with swift webview application target _blank is not working

I'm a beginner, sorry I did webview with swift, but a link with target _blank doesn't work
I've created a UIViewRepresentable class for the WKWebView from UIKit. The UIViewRepresentable class can be used to create and manage views(UIView) from UIKit in SwiftUI. There are two views in the project,
ContentView lists the urls from string array. Selecting an url navigates to the Detail view.
The Detail view shows the web page for the selected url using WKWebView.
Hope this helps. Here's the code.
import SwiftUI
import UIKit
import WebKit
struct ContentView: View {
var urls: [String] = ["https://www.stackoverflow.com", "https://www.yahoo.com"]
#State private var hideStatusBar = false
var body: some View {
NavigationView {
List {
ForEach(urls, id: \.self) { url in
VStack {
NavigationLink(destination: DetailView(url: url)) {
Text(url)
}
}
}
}
.navigationBarTitle("Main")
}
}
}
struct DetailView: View {
var url: String = ""
var body: some View {
VStack {
Webview(url: url)
Spacer()
}
.navigationBarHidden(true)
}
}
struct Webview: UIViewRepresentable {
var url: String
typealias UIViewType = WKWebView
func makeUIView(context: UIViewRepresentableContext<Webview>) -> WKWebView {
let wkWebView = WKWebView()
guard let url = URL(string: self.url) else {
return wkWebView
}
let request = URLRequest(url: url)
wkWebView.load(request)
return wkWebView
}
func updateUIView(_ uiView: WKWebView, context: UIViewRepresentableContext<Webview>) {
}
}