I am using a WKWebView to display a webpage in my iOS app. However, when a text field within the webpage becomes active, I am receiving constraints warnings in the console.
Implementation:
struct WKWebViewRepresentable: UIViewRepresentable {
typealias UIViewType = WKWebView
var url: URL
func makeUIView(context: Context) -> WKWebView {
let webView = WKWebView()
webView.navigationDelegate = context.coordinator
return webView
}
func updateUIView(_ webView: WKWebView, context: Context) {
let request = URLRequest(url: url)
webView.load(request)
}
func makeCoordinator() -> WKWebViewRepresentableCoordinator {
WKWebViewRepresentableCoordinator()
}
}
Use:
.fullScreenCover(item: $viewModel.activeSheet) { activeSheet in
switch activeSheet {
case .addPaymentMethodLocation(let url):
WKWebViewRepresentable(url: url)
}
}
Output:
debug logs
I'm looking for the best approach to implement "pull to refresh" mechanism to SwiftUI WebView.
This is the closest solution I could wrote, however the refreshing process is not ended (activity indicator is present and nothing else is happening)
Those two extensions I've taken from this post https://stackoverflow.com/a/36256504
import SwiftUI
import WebKit
struct WebView: UIViewRepresentable {
let webView: WKWebView
init() {
webView = WKWebView(frame: .zero)
webView.load(URLRequest(url: URL(string: "https://cnn.com")!))
webView.setPullToRefresh(type: .embed)
}
func makeUIView(context: Context) -> WKWebView {
webView
}
func updateUIView(_ uiView: WKWebView, context: Context) {}
}
extension WKWebView {
var refreshControl: UIRefreshControl? { (scrollView.getAllSubviews() as [UIRefreshControl]).first }
enum PullToRefreshType {
case none
case embed
}
func setPullToRefresh(type: PullToRefreshType) {
(scrollView.getAllSubviews() as [UIRefreshControl]).forEach { $0.removeFromSuperview() }
switch type {
case .none: break
case .embed: _setPullToRefresh(target: self, selector: #selector(webViewPullToRefreshHandler(source:)))
}
}
private func _setPullToRefresh(target: Any, selector: Selector) {
let refreshControl = UIRefreshControl()
refreshControl.addTarget(target, action: selector, for: .valueChanged)
scrollView.addSubview(refreshControl)
}
#objc func webViewPullToRefreshHandler(source: UIRefreshControl) {
guard let url = self.url else { source.endRefreshing(); return }
load(URLRequest(url: url))
}
}
extension UIView {
class func getAllSubviews<T: UIView>(from parenView: UIView) -> [T] {
return parenView.subviews.flatMap { subView -> [T] in
var result = getAllSubviews(from: subView) as [T]
if let view = subView as? T { result.append(view) }
return result
}
}
func getAllSubviews<T: UIView>() -> [T] { return UIView.getAllSubviews(from: self) as [T] }
}
OK, here how I solved this problem.
Checked on iPhone 14 Pro, iOS 15 & 16,
Swift 5
import SwiftUI
import WebKit
struct HomeView: UIViewRepresentable {
var webView = WebView()
func makeUIView(context: Context) -> WKWebView {
webView.webView
}
func updateUIView(_ uiView: WKWebView, context: Context) {}
}
class WebView: NSObject {
var webView: WKWebView!
override init() {
super.init()
webView = WKWebView(frame: .zero)
webView.load(URLRequest(url: URL(string: "https://cnn.com")!))
webView.setPullToRefresh()
webView.navigationDelegate = self
}
}
extension WebView: WKNavigationDelegate {
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
webView.refreshControl?.endRefreshing()
}
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
webView.refreshControl?.endRefreshing()
}
func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
webView.refreshControl?.endRefreshing()
}
}
extension WKWebView {
var refreshControl: UIRefreshControl? {
(scrollView.getAllSubviews() as [UIRefreshControl]).first
}
func setPullToRefresh() {
(scrollView.getAllSubviews() as [UIRefreshControl]).forEach {
$0.removeFromSuperview()
}
let refreshControl = UIRefreshControl()
refreshControl.addTarget(self, action: #selector(webViewPullToRefreshHandler(source:)), for: .valueChanged)
scrollView.addSubview(refreshControl)
}
#objc func webViewPullToRefreshHandler(source: UIRefreshControl) {
guard let url = self.url else {
source.endRefreshing()
return
}
load(URLRequest(url: url))
}
}
extension UIView {
class func getAllSubviews<T: UIView>(from parentView: UIView) -> [T] {
return parentView.subviews.flatMap { subView -> [T] in
var result = getAllSubviews(from: subView) as [T]
if let view = subView as? T {
result.append(view)
}
return result
}
}
func getAllSubviews<T: UIView>() -> [T] {
return UIView.getAllSubviews(from: self) as [T]
}
}
Aim
To display a web view inside a SwiftUI view
Reload the contents of the web view when a button (SwiftUI) is pressed.
My Attempt
I have made an attempt to do the above.
Pass a Publisher (CurrentValueSubject) to the UIViewRepresentable
Observe for any new values from the publisher
If the value is true refresh the web view
Question:
Is there a better way to do this? or is my approach reasonable?
Code:
WebView
import SwiftUI
import WebKit
import Combine
struct WebView: UIViewRepresentable {
let url: URL
let shouldRefresh: CurrentValueSubject<Bool, Never>
func makeUIView(context: Context) -> some UIView {
let configuration = WKWebViewConfiguration()
let webView = WKWebView(frame: .zero, configuration: configuration)
let request = URLRequest(url: url)
context.coordinator.navigation = webView.load(request)
context.coordinator.webView = webView
context.coordinator.observeChanges(for: shouldRefresh)
return webView
}
func updateUIView(_ uiView: UIViewType, context: Context) {
print("update view called")
}
func makeCoordinator() -> Coordinator {
Coordinator()
}
}
extension WebView {
class Coordinator {
var navigation: WKNavigation?
var webView: WKWebView?
private var cancellable: AnyCancellable?
#MainActor
func observeChanges(for shouldRefresh: CurrentValueSubject<Bool, Never>) {
cancellable = shouldRefresh
.filter { $0 == true }
.sink { [weak self] newValue in
self?.navigation = self?.webView?.reload()
}
}
}
}
ContentView
import SwiftUI
import Combine
struct ContentView: View {
#StateObject private var model = Model()
var body: some View {
VStack {
Button("Reload") {
print("reload pressed")
reload()
}
WebView(url: URL(string: "https://www.apple.com")!,
shouldRefresh: model.shouldRefresh)
}
}
func reload() {
model.shouldRefresh.send(true)
}
}
extension ContentView {
class Model: ObservableObject {
var shouldRefresh = CurrentValueSubject<Bool, Never>(false)
}
}
I'm trying to make a very barebones mobile browser to practice swiftui and wkwebview (WKwebview is wrapped in a UIViewRepresentable). However, when trying to implement multiple tabs for the browser I hit an odd error. The default webview works but when pressing the button to add new tabs, I notice that the new webview never actually has it's makeuiview() function run. The new tab just displays the last page that was loaded by the first webview but remains static and can't receive navigation. I'm not sure what would cause a uiviewrepresentable to not run it's makeuiview method. There's more code to this but I posted what I thought mattered most.
Content View
(Where the logic for adding the new tab resides) Refer to Tablist.newTab() function
import SwiftUI
import CoreData
class TabList: ObservableObject {
#Published var tabs: [BrowserPage] = [BrowserPage()]
func newTab(){
tabs.append(BrowserPage())
}
}
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#StateObject var tablist: TabList = TabList()
#State var selection = 0
#State var showTabs: Bool = false
var body: some View {
VStack {
tablist.tabs[selection]
Button("next") {
withAnimation {
selection = (selection + 1) % tablist.tabs.count
}
}
Divider()
Button("add") {
withAnimation {
tablist.newTab()
selection = tablist.tabs.count - 1
}
}
Divider()
Text("\(selection)")
Text("\(tablist.tabs.count)")
}
.frame( maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.sheet(isPresented: $showTabs, content: {
})
}
}
WebviewModel Class
import Foundation
import Combine
class WebViewModel: ObservableObject{
var webViewNavigationPublisher = PassthroughSubject<WebViewNavigation, Never>()
var showWebTitle = PassthroughSubject<String, Never>()
var showLoader = PassthroughSubject<Bool, Never>()
var valuePublisher = PassthroughSubject<String, Never>()
var url: String = "https://www.google.com"
#Published var urltoEdit: String = "https://www.google.com"
}
enum WebViewNavigation {
case backward, forward, reload, load
}
WebsiteView (UIViewRepresentable for WKWebview)
import Foundation
import UIKit
import SwiftUI
import Combine
import WebKit
protocol WebViewHandlerDelegate {
func receivedJsonValueFromWebView(value: [String: Any?])
func receivedStringValueFromWebView(value: String)
}
struct WebView: UIViewRepresentable, WebViewHandlerDelegate {
func receivedJsonValueFromWebView(value: [String : Any?]) {
print("JSON value received from web is: \(value)")
}
func receivedStringValueFromWebView(value: String) {
print("String value received from web is: \(value)")
}
#ObservedObject var viewModel: WebViewModel
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> WKWebView {
let preferences = WKPreferences()
let configuration = WKWebViewConfiguration()
configuration.userContentController.add(self.makeCoordinator(), name: "iOSNative")
configuration.preferences = preferences
let webView = WKWebView(frame: CGRect.zero, configuration: configuration)
webView.navigationDelegate = context.coordinator
//remove after debugging...
webView.allowsBackForwardNavigationGestures = true
webView.scrollView.isScrollEnabled = true
webView.load(URLRequest(url: URL(string: viewModel.url)!))
print("UUUUUUUUUUUUUUUUUUUUU " + viewModel.url)
return webView
}
func updateUIView(_ webView: WKWebView, context: Context) {
}
class Coordinator : NSObject, WKNavigationDelegate {
var parent: WebView
var valueSubscriber: AnyCancellable? = nil
var webViewNavigationSubscriber: AnyCancellable? = nil
var delegate: WebViewHandlerDelegate?
init(_ uiWebView: WebView) {
self.parent = uiWebView
}
deinit {
valueSubscriber?.cancel()
webViewNavigationSubscriber?.cancel()
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
// Get the title of loaded webcontent
webView.evaluateJavaScript("document.title") { (response, error) in
if let error = error {
print("Error getting title")
print(error.localizedDescription)
}
guard let title = response as? String else {
return
}
self.parent.viewModel.showWebTitle.send(title)
}
valueSubscriber = parent.viewModel.valuePublisher.receive(on: RunLoop.main).sink(receiveValue: { value in
let javascriptFunction = "valueGotFromIOS(\(value));"
webView.evaluateJavaScript(javascriptFunction) { (response, error) in
if let error = error {
print("Error calling javascript:valueGotFromIOS()")
print(error.localizedDescription)
} else {
print("Called javascript:valueGotFromIOS()")
}
}
})
// Page loaded so no need to show loader anymore
self.parent.viewModel.showLoader.send(false)
}
/* Here I implemented most of the WKWebView's delegate functions so that you can know them and
can use them in different necessary purposes */
func webViewWebContentProcessDidTerminate(_ webView: WKWebView) {
// Hides loader
parent.viewModel.showLoader.send(false)
}
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
// Hides loader
parent.viewModel.showLoader.send(false)
}
func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
self.parent.viewModel.urltoEdit = webView.url!.absoluteString
// Shows loader
parent.viewModel.showLoader.send(true)
}
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
// Shows loader
parent.viewModel.showLoader.send(true)
self.webViewNavigationSubscriber = self.parent.viewModel.webViewNavigationPublisher.receive(on: RunLoop.main).sink(receiveValue: { navigation in
switch navigation {
case .backward:
if webView.canGoBack {
webView.goBack()
}
case .forward:
if webView.canGoForward {
webView.goForward()
}
case .reload:
webView.reload()
case .load:
webView.load(URLRequest(url: URL(string: self.parent.viewModel.url)!))
print("IIIIIIIIIIIIIIIIIIIII " + self.parent.viewModel.url)
}
})
}
// This function is essential for intercepting every navigation in the webview
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
// Suppose you don't want your user to go a restricted site
// Here you can get many information about new url from 'navigationAction.request.description'
decisionHandler(.allow)
}
}
}
// MARK: - Extensions
extension WebView.Coordinator: WKScriptMessageHandler {
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
// Make sure that your passed delegate is called
if message.name == "iOSNative" {
if let body = message.body as? [String: Any?] {
delegate?.receivedJsonValueFromWebView(value: body)
} else if let body = message.body as? String {
delegate?.receivedStringValueFromWebView(value: body)
}
}
}
}
BrowserPage View
import SwiftUI
struct BrowserPage: View, Identifiable{
let id: UUID = UUID()
#ObservedObject var viewModel = WebViewModel()
var body: some View {
VStack{
URLBarView(viewModel: viewModel)
HStack{
WebView(viewModel: viewModel)
}
BottomNavBarView(viewModel: viewModel)
.padding(.bottom, 25)
}
}
}
I'm a swift learner. I work with SwiftUI which is a struct, I have to implement a WKWebView and in that, a url is changing dynamically. I have to catch these changing urls, but solutions I have tried are not working.
For example: https://stackoverflow.com/a/48273950/10088243
I tried this code block but it is not working and it gives me some compiler errors:
import SwiftUI
import WebKit
struct ContentView: UIViewRepresentable, WKNavigationDelegate {
let request = URLRequest(url: URL(string: "https://apple.com")!)
func makeUIView(context: Context) -> WKWebView {
let preferences = WKPreferences()
preferences.javaScriptEnabled = true
preferences.javaScriptCanOpenWindowsAutomatically = true
let configuration = WKWebViewConfiguration()
configuration.preferences = preferences
let webView = WKWebView(frame: .zero, configuration: configuration)
webView.allowsBackForwardNavigationGestures = true
return webView
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
// 'override' can only be specified on class membe
if keyPath == #keyPath(WKWebView.url) {
print("### URL:", self.webView.url!)
}
if keyPath == #keyPath(WKWebView.estimatedProgress) {
// When page load finishes. Should work on each page reload.
if (self.webView.estimatedProgress == 1) {
print("### EP:", self.webView.estimatedProgress)
}
}
}
func updateUIView(_ uiView: WKWebView, context: Context) {
uiView.load(request)
}
func webViewDidFinishLoad(webView : WKWebView) {
print("Loaded: \(String(describing: webView.url))")
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
print("Loaded: \(String(describing: webView.url))")
//progressView.isHidden = true
}
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
//progressView.isHidden = false
print("Loaded: \(String(describing: webView.url))")
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I have a Non-class type 'ContentView' cannot conform to class protocol 'NSObjectProtocol' error at line struct ContentView...
You can simply create a ObservableObject model class of webview with the name "WebViewModel" like
class WebViewModel: ObservableObject {
#Published var link: String
#Published var didFinishLoading: Bool = false
init (link: String) {
self.link = link
}
}
and also import
import WebKit
import Combine
and then copy this code snippets
struct SwiftUIWebView: UIViewRepresentable {
#ObservedObject var viewModel: WebViewModel
let webView = WKWebView()
func makeUIView(context: UIViewRepresentableContext<SwiftUIWebView>) -> WKWebView {
self.webView.navigationDelegate = context.coordinator
if let url = URL(string: viewModel.link) {
self.webView.load(URLRequest(url: url))
}
return self.webView
}
func updateUIView(_ uiView: WKWebView, context: UIViewRepresentableContext<SwiftUIWebView>) {
return
}
class Coordinator: NSObject, WKNavigationDelegate {
private var viewModel: WebViewModel
init(_ viewModel: WebViewModel) {
self.viewModel = viewModel
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
//print("WebView: navigation finished")
self.viewModel.didFinishLoading = true
}
}
func makeCoordinator() -> SwiftUIWebView.Coordinator {
Coordinator(viewModel)
}
}
struct SwiftUIWebView_Previews: PreviewProvider {
static var previews: some View {
SwiftUIWebView(viewModel: WebViewModel(link: "https://google.com"))
//WebView(request: URLRequest(url: URL(string: "https://www.apple.com")!))
}
}
and in you view
struct AnyView: View {
#ObservedObject var model = WebViewModel(link: "https://www.wikipedia.org/")
var body: some View {
NavigationView {
SwiftUIWebView(viewModel: model)
if model.didFinishLoading {
//do your stuff
}
}
}}
so in this way you can get the others delegates response.
you use this to delegates of WKNavigationProtocol to perform(e.g to allow or cancel URL Loading) your action
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
if let host = navigationAction.request.url?.host {
if host.contains("facebook.com") {
decisionHandler(.cancel)
return
}
}
decisionHandler(.allow)
}
Use the following delegate function of WKWebView:
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
// Suppose you don't want your user to go a restricted site
if let host = navigationAction.request.url?.host {
if host == "restricted.com" {
decisionHandler(.cancel)
return
}
}
decisionHandler(.allow)
}
You can read this article from Medium which shows a better way of intercepting every network call or Url changing and obtaining upcoming Url related data. It also shows how to implement WebView in SwiftUI, interfacing with JavaScript functions and loading a local .html file from iOS project
Simple, Just use this delegate method
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
print(webView.url?.absoluteString)
}
You can use key/value observation to detect changes to the url property of the WKWebView.
Here is a simple example of wrapping a WKWebView in a UIViewRepresentable.
Note that because we are modifying a property, the UIViewRepresentable is a final class rather than a struct.
import Combine
import SwiftUI
import WebKit
final class WebView: UIViewRepresentable {
#Published var url: URL? = nil {
didSet {
if url != nil {
willChange.send(url)
}
}
}
private let view = WKWebView()
private var urlChangedObservation: NSKeyValueObservation?
private let willChange = PassthroughSubject<URL?, Never>()
func makeUIView(context: Context) -> WKWebView {
return makeWebView()
}
func updateUIView(_ uiView: WKWebView, context: Context) {
}
func display(_ html: String) {
self.view.loadHTMLString(html, baseURL: nil)
}
public func load(_ url: String) -> WebView {
let link = URL(string: url)!
let request = URLRequest(url: link)
self.view.load(request)
return self
}
func makeWebView() -> WKWebView {
self.urlChangedObservation = self.view.observe(\WKWebView.url, options: .new) { view, change in
if let url = view.url {
self.url = url
}
}
return self.view
}
}
You can then listen to the url modified notification in the onReceive() of the container holding the WebView:
.onReceive(self.webview.$url) { url in
if let url = url {
}
}
I came here trying a fast way to get a working sample in SwiftUI to get an HTML response from an web auth service. (in the specific the new DropBox awful auth schema using an URI... we do no see this details, but call-backs and code should be explanatory enough. (JSOn comes from my web server specified in URI) )
in our Swift UI part:
struct ContentView: View {
#State private var showingSheet = false
private var webCallBack: WebCallBack = nil
let webView = WKWebView(frame: .zero)
#State private var auth_code = ""
var body: some View {
VStack{
Text("\(auth_code)")
.font(.system(size: 50))
Button("Show Auth web form") {
self.showingSheet = true
}
.sheet(isPresented: $showingSheet) {
WebView( webView: webView, webCallBack: { (d: Dict?) in
print("\n", d)
auth_code = (d?["auth_code"] as? String) ?? "!!"
showingSheet = false
} )
}
}
}
}
Our implementation:
typealias WebCallBack = ( (Dict?)->() )?
class MyWKDelegate: NSObject, WKNavigationDelegate{
private var webCallBack : WebCallBack = nil
init(webCallBack: WebCallBack) {
self.webCallBack = webCallBack
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
print("End loading")
webView.evaluateJavaScript("document.body.innerHTML", completionHandler: { result, error in
if let html = result as? String {
//print(html)
// we are here also at first call, i.e. web view with user / password. Custiomize as needed.
if let d = dictFromJSONWith(string: html){
//print(d)
self.webCallBack?(d)
}
}
})
}
}
struct WebView: UIViewRepresentable {
let webView: WKWebView
let delegate: MyWKDelegate
internal init(webView: WKWebView, webCallBack: WebCallBack) {
self.webView = webView
self.delegate = MyWKDelegate(webCallBack: webCallBack)
webView.navigationDelegate = delegate
let urlStr = DB_URL.replacingOccurrences(of: "APP_KEY", with: APP_KEY).replacingOccurrences(of: "REDIRECT_URI", with: REDIRECT_URI)
print(urlStr)
if let url = URL(string: urlStr){
webView.load(URLRequest(url: url))
}
}
func makeUIView(context: Context) -> WKWebView {
return webView
}
func updateUIView(_ uiView: WKWebView, context: Context) { }
}
Some accessory code to make life easier:
typealias Dict = [String : Any]
typealias Dicts = [Dict]
func dictFromJSONWith(data: Data?)->Dict? {
guard let data = data else {
return nil
}
if let dict = try? JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions() ){
return dict as? Dict
}
return nil
}
func dictFromJSONWith(string: String?)->Dict?{
guard let data = string?.data(using: .utf8) else{
return nil
}
return dictFromJSONWith(data: data)
}
I have found a very good solution to my question. I will post it here. Maybe someone wants to see it and might be useful to them.
observe.observation = uiView.observe(\WKWebView.url, options: .new) { view, change in
if let url = view.url {
// do something with your url
}
}