Pull to refresh in SwiftUI WebView - ios

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]
}
}

Related

Trying to implement the ability to launch multiple wkwebview instances to emulate tabbing on a browser

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)
}
}
}

Load an Activity Indicator while website is loading in SwiftUI

I have a basic list that displays a webview. I want to add an activity indicator that shows while the webpage is loading. this is the code that I've created.
import SwiftUI
import WebKit
struct WebView: UIViewRepresentable {
var url: String
// makeUIView func
func makeUIView(context: Context) -> WKWebView {
guard let url = URL(string: self.url) else {
return WKWebView()
}
let request = URLRequest(url: url)
let wkWebView = WKWebView()
wkWebView.load(request)
return wkWebView
}
// updateUIView func
func updateUIView(_ uiView: WKWebView, context: Context) {
}
}
struct WebView_Preview: PreviewProvider {
static var previews: some View {
WebView(url: "https://www.google.com")
}
}
Thank you!
Here's something that should do what you want. The WebView has a delegate in the Coordinator class. It changes a binding, to which the ContentView can react appropriately. Currently it's just a Text displaying the raw value of the state, but it can be replaced with an activity indicator of some sorts.
struct ContentView: View {
#State var urlString = ""
#State var workState = WebView.WorkState.initial
var body: some View {
VStack(spacing: 20) {
WebView(urlString: self.$urlString, workState: self.$workState)
Button("Play") {
self.urlString = "https://www.example.com/"
}
Text("Current work = " + self.workState.rawValue)
}
}
}
struct WebView: UIViewRepresentable {
enum WorkState: String {
case initial
case done
case working
case errorOccurred
}
#Binding var urlString: String
#Binding var workState: WorkState
func makeUIView(context: Context) -> WKWebView {
let webView = WKWebView()
webView.navigationDelegate = context.coordinator
return webView
}
func updateUIView(_ uiView: WKWebView, context: Context) {
switch self.workState {
case .initial:
if let url = URL(string: self.urlString) {
uiView.load(URLRequest(url: url))
}
default:
break
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, WKNavigationDelegate {
var parent: WebView
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
self.parent.workState = .working
}
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
self.parent.workState = .errorOccurred
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
self.parent.workState = .done
}
init(_ parent: WebView) {
self.parent = parent
}
}
}
You will need to add an Coordinator to your UIViewRepresentable.
I think the answers for this question give you the right ideas.
SwiftUI WKWebView detect url changing

SwiftUI WKWebView detect url changing

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
}
}

How do I add Pull to Refresh in WebView?

I'm very new to developing. I'm building a WebView and I want it to have pull to refresh capabilities. How do I go about doing that in swift?
My code in the View Controller is
#IBOutlet var webView: UIWebView!
override func viewDidLoad() {
super.viewDidLoad()
func webViewDidFinishLoad(webView: UIWebView) {
UIApplication.sharedApplication().networkActivityIndicatorVisible = false
}
func webViewDidStartLoad(webView: UIWebView) {
UIApplication.sharedApplication().networkActivityIndicatorVisible = true
}
let url = NSURL (string: "https://www.foo.com");
let requestObj = NSURLRequest(URL: url!);
webView.loadRequest(requestObj);
NSUserDefaults.standardUserDefaults().registerDefaults(["UserAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/601.5.17 (KHTML, like Gecko) Version/9.1 Safari/601.5.17"])
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
Your code is buggy you added two Web view Delegate in side the ViewDidLoad method that must be following and check the following viewDidLoad code for adding Pull to Refresh in web view:
#IBOutlet var webView: UIWebView!
var refController:UIRefreshControl = UIRefreshControl()
override func viewDidLoad() {
super.viewDidLoad()
let url = NSURL (string: "https://www.foo.com");
let requestObj = NSURLRequest(URL: url!);
webView.loadRequest(requestObj);
NSUserDefaults.standardUserDefaults().registerDefaults(["UserAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/601.5.17 (KHTML, like Gecko) Version/9.1 Safari/601.5.17"])
refController.bounds = CGRectMake(0, 50, refController.bounds.size.width, refController.bounds.size.height)
refController.addTarget(self, action: Selector("mymethodforref:"), forControlEvents: UIControlEvents.ValueChanged)
refController.attributedTitle = NSAttributedString(string: "Pull to refresh")
webView.scrollView.addSubview(refController)
}
func mymethodforref(refresh:UIRefreshControl){
webView.reload()
refController.endRefreshing()
}
func webViewDidFinishLoad(webView: UIWebView) {
UIApplication.sharedApplication().networkActivityIndicatorVisible = false
}
func webViewDidStartLoad(webView: UIWebView) {
UIApplication.sharedApplication().networkActivityIndicatorVisible = true
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}
Details
Xcode 11.3.1, Swift 5.1
Solution
extension WKWebView {
var refreshControl: UIRefreshControl? { (scrollView.getAllSubviews() as [UIRefreshControl]).first }
enum PullToRefreshType {
case none
case embed
case custom(target: Any, selector: Selector)
}
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:)))
case .custom(let params): _setPullToRefresh(target: params.target, selector: params.selector)
}
}
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))
}
}
// https://stackoverflow.com/a/47282118/4488252
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] }
}
Usage
Setup
import UIKit
import WebKit
class ViewController2: UIViewController {
private weak var webView: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
// ... create webView here
webView.navigationDelegate = self
}
}
// MARK: WKNavigationDelegate
extension ViewController2: 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()
}
}
Activate pull to refresh (option 1)
webView.setPullToRefresh(type: .embed)
Activate pull to refresh (option 2)
#objc func customPullToRefreshHandler(source: UIRefreshControl) {
guard let url = webView.url else { source.endRefreshing(); return }
webView.load(URLRequest(url: url))
}
// ....
webView.setPullToRefresh(type: .custom(target: self, selector: #selector(customPullToRefreshHandler(source:))))
Deactivate pull to refresh
webView.setPullToRefresh(type: .none)
Full sample
Do not forget to paste here the solution code
import UIKit
import WebKit
class ViewController: UIViewController {
private weak var webView: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
createWebView()
webView.load("https://google.com")
// Way 1
webView.setPullToRefresh(type: .embed)
// Way 2
// webView.setPullToRefresh(type: .custom(target: self, selector: #selector(customPullToRefreshHandler(source:))))
}
private func createWebView() {
let webView = WKWebView(frame: .zero, configuration: .init())
view.addSubview(webView)
webView.navigationDelegate = self
self.webView = webView
webView.translatesAutoresizingMaskIntoConstraints = false
webView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
webView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
view.bottomAnchor.constraint(equalTo: webView.bottomAnchor).isActive = true
view.rightAnchor.constraint(equalTo: webView.rightAnchor).isActive = true
}
// #objc func customPullToRefreshHandler(source: UIRefreshControl) {
// guard let url = webView.url else { source.endRefreshing(); return }
// webView.load(URLRequest(url: url))
// print("WebView started loading")
// }
}
// MARK: WKNavigationDelegate
extension ViewController: 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 {
func load(_ urlString: String) {
guard let url = URL(string: urlString) else { return }
load(URLRequest(url: url))
}
}
Following code will put the refresh control in scrollview of webview.Initially it will load google.com. To see pull to refresh clearly I have set white color to the background of scroll view so it is clearly visible and on pull to refresh webview opens facebook page.
class ViewController: UIViewController,UIWebViewDelegate {
#IBOutlet weak var webView: UIWebView!
var refreshControl:UIRefreshControl?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
self.refreshControl = UIRefreshControl.init()
refreshControl!.addTarget(self, action:#selector(refreshControlClicked), for: UIControlEvents.valueChanged)
self.webView.scrollView.addSubview(self.refreshControl!)
self.webView.scrollView.backgroundColor = UIColor.white
self.webView.delegate = self
let url:URL = URL.init(string:"https://www.google.com")!
self.loadRequestWithUrl(URLRequest.init(url: url))
}
func webViewDidStartLoad(_ webView: UIWebView) {
NSLog("website loaded")
}
func loadRequestWithUrl(_ urlRequest : URLRequest?){
if(urlRequest != nil){
self.webView.loadRequest(urlRequest!)
}
}
func refreshControlClicked(){
let url:URL = URL.init(string:"https://www.facebook.com")!
self.loadRequestWithUrl(URLRequest.init(url: url))
}
}
You can use this one
Write this in DidLoad method
webViewHome.scrollView.delegate = self;
And then write this method
#pragma mark Scrollview delegate
(void)scrollViewWillEndDragging:(UIScrollView *)scrollView
withVelocity:(CGPoint)velocity
targetContentOffset:(inout CGPoint *)targetContentOffset{
if (scrollView.contentOffset.y < 0){
DLog(#" webview on top ");
[self AgainCheckInternet:nil];
}
}
Note: Please make sure you didn't set scrollview bounces property to No otherwise this method will not call

Displaying activity indicator on WKWebView using swift

I am working on the following code and trying to show an activity indicator in the view whilst the page is loading..
I tried to implement the WKNavigationDelegate methods but I am failing as nothing shows.
Any suggestions on how to fix this?
I am not setting the SupportWebView view delegate anywhere but I wouldn't know how to do it in swift..
import UIKit
import WebKit
class SupportWebView: UIViewController, WKNavigationDelegate {
#IBOutlet var containerView : UIView? = nil
var webView: WKWebView?
override func loadView() {
super.loadView()
self.webView = WKWebView()
self.view = self.webView
}
override func viewDidLoad() {
super.viewDidLoad()
var dataManager = DataManager.sharedDataManager()
var url = dataManager.myValidURL
var req = NSURLRequest(URL:url!)
self.webView!.loadRequest(req)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func webView(webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
UIApplication.sharedApplication().networkActivityIndicatorVisible = true
}
func webView(webView: WKWebView, didFinishNavigation navigation: WKNavigation!) {
UIApplication.sharedApplication().networkActivityIndicatorVisible = false
}
}
As commented, you forgot to set the webView delegate:
override func loadView() {
super.loadView()
self.webView = WKWebView()
self.webView.navigationDelegate = self
self.view = self.webView
}
You should use the delegate methods for all other purposes, but key path monitoring works fine for this one purpose.
Here is a Swift 4 implementation that works fine.
// Somewhere in your view controller
private var loadingObservation: NSKeyValueObservation?
private lazy var loadingIndicator: UIActivityIndicatorView = {
let spinner = UIActivityIndicatorView()
spinner.translatesAutoresizingMaskIntoConstraints = false
spinner.color = .black
return spinner
}()
override func viewDidLoad() {
super.viewDidLoad()
// Setup...
loadingObservation = webView.observe(\.isLoading, options: [.new, .old]) { [weak self] (_, change) in
guard let strongSelf = self else { return }
// this is fine
let new = change.newValue!
let old = change.oldValue!
if new && !old {
strongSelf.view.addSubview(strongSelf.loadingIndicator)
strongSelf.loadingIndicator.startAnimating()
NSLayoutConstraint.activate([strongSelf.loadingIndicator.centerXAnchor.constraint(equalTo: strongSelf.view.centerXAnchor),
strongSelf.loadingIndicator.centerYAnchor.constraint(equalTo: strongSelf.view.centerYAnchor)])
strongSelf.view.bringSubview(toFront: strongSelf.loadingIndicator)
}
else if !new && old {
strongSelf.loadingIndicator.stopAnimating()
strongSelf.loadingIndicator.removeFromSuperview()
}
}
}
Please, below code which is working fine[Swift 4.2].
#IBOutlet weak var wv: WKWebView!
#IBOutlet weak var activityIndicator: UIActivityIndicatorView!
override func viewDidLoad() {
super.viewDidLoad()
loadYoutube(videoID: "KqNS7uAvOxk")
}
Now load Youtube Video
func loadYoutube(videoID:String) {
guard let youtubeURL = URL(string: "https://www.youtube.com/embed/\(videoID)")
else { return }
wv.load( URLRequest(url: youtubeURL) )
wv.navigationDelegate = self
}
Implement below this function:
func showActivityIndicator(show: Bool) {
if show {
activityIndicator.startAnimating()
} else {
activityIndicator.stopAnimating()
}
}
Implement below these three delegate method:
func webView(_ webView: WKWebView, didFinish navigation:
WKNavigation!) {
showActivityIndicator(show: false)
}
func webView(_ webView: WKWebView, didStartProvisionalNavigation
navigation: WKNavigation!) {
showActivityIndicator(show: true)
}
func webView(_ webView: WKWebView, didFail navigation:
WKNavigation!, withError error: Error) {
showActivityIndicator(show: false)
}
Let me know if it is not working.

Resources