8

How can I add an activity indicator in WKWebView which will display the indicator while the webpage is loading and disappears when loaded ?

I've looked at some of the old posts but could not figure out how to do it in SwiftUI see link to one of the old solutions below How to add Activity Indicator to WKWebView (Swift 3)

Ashley Vaz
  • 93
  • 1
  • 5
  • This should help: https://stackoverflow.com/questions/56496638/activity-indicator-in-swiftui. UIActivityIndicator is not yet a native view in SwiftUI so you may want to convert it to one and then add it to your view. – palme Feb 04 '20 at 05:43
  • @palme How do I turn off the activity indicator once the WKWebview has finished loading? – Ashley Vaz Feb 04 '20 at 08:10
  • 1
    The WKWebView has a a navigationDelegate which calls `func webView(WKWebView, didFinish: WKNavigation!)` once it is done loading. In there you can use `stopAnimating()` of the `UIActivityIndicator `. – palme Feb 04 '20 at 09:19

2 Answers2

11

Use UIViewRepresentable to create a UIActivityIndicatorView:

You control when an activity indicator animates by calling the startAnimating() and stopAnimating() methods. To automatically hide the activity indicator when animation stops, set the hidesWhenStopped property to true.

You can set the color of the activity indicator by using the color property.

struct ActivityIndicatorView: UIViewRepresentable {
    @Binding var isAnimating: Bool
    let style: UIActivityIndicatorView.Style
    
    func makeUIView(context: UIViewRepresentableContext<ActivityIndicatorView>) -> UIActivityIndicatorView {
        return UIActivityIndicatorView(style: style)
    }
    
    func updateUIView(_ uiView: UIActivityIndicatorView, context: UIViewRepresentableContext<ActivityIndicatorView>) {
        isAnimating ? uiView.startAnimating() : uiView.stopAnimating()
    }
}

Create a LoadingView to allow you to wrap around your views:

This allows you to style the activity views content.

struct LoadingView<Content>: View where Content: View {
    @Binding var isShowing: Bool
    var content: () -> Content
    
    var body: some View {
        GeometryReader { geometry in
            ZStack(alignment: .center) {
                self.content()
                    .disabled(self.isShowing)
                    .blur(radius: self.isShowing ? 3 : 0)
                
                VStack {
                    Text("Loading...")
                    ActivityIndicatorView(isAnimating: .constant(true), style: .large)
                }
                .frame(width: geometry.size.width / 2, height: geometry.size.height / 5)
                .background(Color.secondary.colorInvert())
                .foregroundColor(Color.red)
                .cornerRadius(20)
                .opacity(self.isShowing ? 1 : 0)
                
            }
        }
    }
}

If you want to be able to update the LoadingView(...) status you'll need to introduce a view model that inherits from ObservableObject:

Based on this answer: https://stackoverflow.com/a/58825642/264802

class WebViewModel: ObservableObject {
    @Published var url: String
    @Published var isLoading: Bool = true
    
    init (url: String) {
        self.url = url
    }
}

struct WebView: UIViewRepresentable {
    @ObservedObject var viewModel: WebViewModel
    let webView = WKWebView()

    func makeCoordinator() -> Coordinator {
        Coordinator(self.viewModel)
    }
    
    class Coordinator: NSObject, WKNavigationDelegate {
        private var viewModel: WebViewModel
        
        init(_ viewModel: WebViewModel) {
            self.viewModel = viewModel
        }

        func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
            self.viewModel.isLoading = false
        }
    }
    
    func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<WebView>) { }
    
    func makeUIView(context: Context) -> UIView {
        self.webView.navigationDelegate = context.coordinator

        if let url = URL(string: self.viewModel.url) {
            self.webView.load(URLRequest(url: url))
        }

        return self.webView
    }
}

Then to use it inside your views you would do the following:

struct ContentView: View {
    @StateObject var model = WebViewModel(url: "http://www.google.com")

    var body: some View {
        LoadingView(isShowing: self.$model.isLoading) {
            WebView(viewModel: self.model)
        }
    }
}
nickreps
  • 903
  • 8
  • 20
gotnull
  • 26,454
  • 22
  • 137
  • 203
-1

Using 3 Steps I do it in my project.

Step 1: Create a Loading View

import SwiftUI
import UIKit

struct ActivityIndicatorView: UIViewRepresentable {
    @Binding var isAnimating: Bool
    let style: UIActivityIndicatorView.Style

    func makeUIView(context: Context) -> UIActivityIndicatorView {
        return UIActivityIndicatorView(style: style)
    }

    func updateUIView(_ uiView: UIActivityIndicatorView, context: Context) {
        isAnimating ? uiView.startAnimating() : uiView.stopAnimating()
    }
}

// Main View
struct LoadingView<Content>: View where Content: View {
    @Binding var isShowing: Bool
    let message: String
    var content: () -> Content

    var body: some View {
        GeometryReader { geometry in
            ZStack(alignment: .center) {
                self.content()
                    .disabled(self.isShowing)
                    .blur(radius: self.isShowing ? 3 : 0)

                VStack {
                    Text(self.message)
                        .bold()
                    ActivityIndicatorView(isAnimating: .constant(true), style: .large)
                }
                .frame(width: geometry.size.width / 2,
                       height: geometry.size.height / 5)
                .background(Color.secondary.colorInvert())
                .foregroundColor(Color.primary)
                .cornerRadius(20)
                .opacity(self.isShowing ? 1 : 0)
            }
        }
    }

}

// Mark: Testing

struct LoadingIndicator: View {
    var body: some View {
        LoadingView(isShowing: .constant(true), message: "Loading...") {
            NavigationView {
                List(["1", "2", "3", "4", "5"], id: \.self) { row in
                    Text(row)
                }.navigationBarTitle(Text("A List"), displayMode: .large)
            }
        }
    }
}

struct ActivityIndicatorView_Previews: PreviewProvider {
    static var previews: some View {
        LoadingIndicator()
    }
}

Step 2: Create a WebView and WebViewModel

import SwiftUI
import WebKit

class WebViewModel: ObservableObject {
    @Published var isLoading: Bool = false
}

struct WebView: UIViewRepresentable {
    @ObservedObject var webViewModel: WebViewModel
    let urlString: String

    func makeUIView(context: Context) -> WKWebView {
        let wkWebView = WKWebView()
        if let url = URL(string: urlString) {
            let urlRequest = URLRequest(url: url)
            wkWebView.load(urlRequest)
        }
        return wkWebView
    }

    func updateUIView(_ wkWebView: WKWebView, context: Context) {
        // do nothing
    }

    class Coordinator: NSObject, WKNavigationDelegate {
        let webViewModel: WebViewModel

        init(_ webViewModel: WebViewModel) {
            self.webViewModel = webViewModel
        }

        func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
            webViewModel.isLoading = true
        }

        func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
            webViewModel.isLoading = false
        }
    }

    func makeCoordinator() -> WebView.Coordinator {
        Coordinator(webViewModel)
    }
}

struct WebView_Previews: PreviewProvider {
    static var previews: some View {
        WebView(webViewModel: WebViewModel(),
                urlString: "https://instagram.com/mahmudahsan/")
    }
}

Step 3: In your main view use the following code to show indicator and webview

ZStack {
        WebView(webViewModel: webViewModel, urlString: "http://ithinkdiff.net")
            .frame(height: 1000)

        if webViewModel.isLoading {
              LoadingView(isShowing: .constant(true), message: "Loading...") {
                            EmptyView()
              }
        }
}
Mahmud Ahsan
  • 1,755
  • 19
  • 18
  • Thanks @Mahmud Ahsan, the view loads the webpage but continues to display the activity indicator after the page is fully loaded, any ideas ? – Ashley Vaz Feb 06 '20 at 02:17
  • 2
    I added a break point on the coordinator class, it seems the flow of control never reaches the `func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { self.webViewModel.isLoading = false }` in the coordinator. – Ashley Vaz Feb 06 '20 at 03:03