I have an app that required some alerts. I have implemented the alert trigger with observableObject so that it can be triggered from the my coordinator class. The problem I am having is that the first trigger work and the alert shows. But after the first time no matter how many times I trigger the button the alert does not show.
The view has a webview (UIRepresentable) where when a button is pressed on the webpage the JS triggers the alert in the coordinator class
Relevant parts of code below
MainView with showAlerts class:
import SwiftUI
import WebKit
struct MainContentView: View {
#ObservedObject public var viewAlertsInstance = ShowAlerts()
var body: some View {
ZStack {
SubscriptionViewController(showAlert: viewAlertsInstance)
}
.alert(isPresented: $viewAlertsInstance.showAlert) {
Alert(
title: Text("Important message"),
message: Text("Something"),
dismissButton: .default(Text("Got it!"))
)
}
}
}
class ShowAlerts: ObservableObject {
#Published var showAlert:Bool = false
func displayAlert() {
self.showAlert.toggle()
print(showAlert)
}
}
SubscriptionView including the coordinator:
import SwiftUI
import WebKit
struct SubscriptionViewController: UIViewRepresentable {
#StateObject var showAlert: ShowAlerts
func makeUIView(context: UIViewRepresentableContext<SubscriptionViewController>) -> WKWebView {
let view = WKWebView()
view.navigationDelegate = context.coordinator
DispatchQueue.main.async {
let url = URL(string:"https")!
let request = URLRequest(url: url)
view.load(request)
}
context.coordinator.webView = view
return view
}
func updateUIView(_ uiView: WKWebView, context: UIViewRepresentableContext<SubscriptionViewController>) {
}
func makeCoordinator() -> CoordinatorSubscription {
CoordinatorSubscription(self, showAlert: self.showAlert)
}
typealias UIViewType = WKWebView
}
class CoordinatorSubscription: NSObject, WKNavigationDelegate, WKScriptMessageHandler{
var control: SubscriptionViewController
var showAlert: ShowAlerts
var webView : WKWebView?
init(_ control: SubscriptionViewController, showAlert: ShowAlerts) {
self.control = control
self.showAlert = showAlert
}
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
processReturnedJS(body: message.body as! String)
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
}
func processReturnedJS(body: String) {
//Here should toggle the alert
showAlert.displayAlert()
}
}
In the processReturnedJS function the showAlert.displayAlert() should show the alert every-time the JS is received
Would appreciate any help
Thanks
Swap your
#StateObject and #ObservedObject wrappers
StateObject is for initializing and ObservedObject takes its initial value as a parameter in the initializer per apple documentation.
Then remove
var showAlert: ShowAlerts
In your coordinator and access the variable like this
control.showAlert.displayAlert()
I think you flipped the use of #ObservedObject and #StateObject.
When you find yourself creating a default or a starting value for your object, like you're doing in MainContentView, use #StateObject.
When you are creating your object elsewhere up the view hierarchy and just want your view to observe the object, use #ObservedObject.
Related
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()
}
}
I'm trying to have a list of NavigationLinks that show a website in full screen. But when I tap on the NavigationLink I get the warning: "This method should not be called on the main thread as it may lead to UI unresponsiveness."
Here is a small example to reproduce the error:
struct ContentView: View {
var body: some View {
NavigationView {
List {
NavigationLink("Apple") {
WebViewScreen(url: URL(string: "https://www.apple.com")!)
}
}
}
}
}
struct WebViewScreen: View {
let url: URL
#State private var isWebViewLoading = true
var body: some View {
ZStack {
WebViewRepresentable(
isLoading: $isWebViewLoading,
url: url
)
if isWebViewLoading {
ProgressView()
.scaleEffect(1.5)
.tint(.accentColor)
}
}
}
}
struct WebViewRepresentable: UIViewRepresentable {
#Binding var isLoading: Bool
let url: URL
func makeUIView(context: Context) -> WKWebView {
let webView = WKWebView(frame: .zero)
webView.navigationDelegate = context.coordinator
let request = URLRequest(url: url)
webView.load(request)
return webView
}
func updateUIView(_ webView: WKWebView, context: Context) {}
func makeCoordinator() -> WebViewCoordinator {
WebViewCoordinator(isLoading: $isLoading)
}
}
class WebViewCoordinator: NSObject, WKNavigationDelegate {
#Binding var isLoading: Bool
init(isLoading: Binding<Bool>) {
_isLoading = isLoading
}
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
isLoading = true
}
func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
isLoading = false
}
}
The warning does not appear without if I only show the WebViewScreen.
I'm using Pulley a maps drawer library which is written in UIKit in a SwiftUI project. I have a SwiftUI ListView that I'm using in the project via a UIHostingController but I want to disable scrolling when the drawers position is not open and to do that I'm pretty sure I need to use one of the delegate functions Pulley provides (drawerPositionDidChange) but I'm not sure how to use the delegate in the Coordinator or if I should even try to use the delegate, maybe I just need to use some type of state variable?
Delegate in the view controller
#objc public protocol PulleyDelegate: AnyObject {
/** This is called after size changes, so if you care about the bottomSafeArea property for custom UI layout, you can use this value.
* NOTE: It's not called *during* the transition between sizes (such as in an animation coordinator), but rather after the resize is complete.
*/
#objc optional func drawerPositionDidChange(drawer: PulleyViewController, bottomSafeArea: CGFloat)
}
This is the UIViewRepresentable where I'm trying to use the delegate.
import SwiftUI
struct DrawerPosition: UIViewControllerRepresentable {
#Binding var bottomSafeArea: CGFloat?
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> some UIViewController {
let vc = PulleyViewController()
vc.delegate = context.coordinator
return vc
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
// Updates the state of the specified view controller with new information from SwiftUI.
}
class Coordinator: NSObject, PulleyDrawerViewControllerDelegate {
var parent: DrawerPosition
init (_ parent: DrawerPosition) {
self.parent = parent
}
func drawerPositionDidChange(drawer: PulleyViewController, bottomSafeArea: CGFloat){
self.parent.bottomSafeArea = bottomSafeArea
}
}
}
the ListView where I want to disable the scroll.
import SwiftUI
struct ListView: View {
#State private var bottomSafeArea: CGFloat?
var body: some View {
ScrollViewReader { proxy in
VStack {
Button("Jump to #50") {
proxy.scrollTo(50)
}
List(0..<100, id: \.self) { i in
Text("Example")
.id(i)
}.scrollDisabled(bottomSafeArea == 0 ? true : false)
}
}
}
}
class ListViewVHC: UIHostingController<ListView> {
required init?(coder: NSCoder) {
super.init (coder: coder, rootView: ListView())
}
}
struct ListView_Previews: PreviewProvider {
static var previews: some View {
ListView()
}
}
Here is the correct way to set up a Coordinator:
func makeCoordinator() -> Coordinator {
Coordinator()
}
func makeUIViewController(context: Context) -> PullyViewController {
context.coordinator.pullyViewController
}
func updateUIViewController(_ uiViewController: PullyViewController, context: Context) {
// Updates the state of the specified view controller with new information from SwiftUI.
context.coordinator.bottomSafeAreaChanged = { bottomSafeArea in
self.bottomSafeArea = bottomSafeArea
}
}
class Coordinator: NSObject, PulleyDrawerViewControllerDelegate {
lazy var pullyViewController: PulleyViewController = {
let vc = PulleyViewController()
vc.delegate = self
return vc
}()
var bottomSafeAreaChanged: ((CGFloat) -> Void)?
func drawerPositionDidChange(drawer: PulleyViewController, bottomSafeArea: CGFloat){
bottomSafeAreaChanged?(bottomSafeArea)
}
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")!)
}
}
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)