Customising UIWebView

      4 Comments on Customising UIWebView

If you want to display complex layout you could easily spend a week working on one view controller. Fortunately, there’s an easier way. You can use a web view to do this. In this article, we’ll take a web page, strip some elements from it, and display it in our app.

We’ll be using one of the articles from this blog as an example web page. We’ll strip HTML elements from it, like header, footer, and some others. We’ll also add a list of internal hosts. If you tap on a link that’s not on the list of internal hosts, the link will open in Safari, otherwise, the link will open within the web view.

Stripping HTML Elements

I won’t go into details of how HTML works, what’s important for you to remember is that elements of a web page can be identified using an element id (or class). We’ll use this to find our elements and remove them.

If you open this blog in Safari on your iPhone you’ll see something like this:

And the footer will look something like this:

We’ll strip this page down and just keep the original text. So, the header and everything below the ‘More resources’ section will be gone.

Let’s quickly set up the UI, create a new project and add a couple of elements, like so:

The big white rectangle in the middle is the web view 🙂 Hook all the buttons and actions to your view controller. Before we start coding, don’t forget to allow arbitrary loads, otherwise, you won’t be able to load the page. So add ‘NSAppTransportSecurity’ and set ‘NSAllowsArbitraryLoads’ to ‘YES’ in your Info.plist:

Let’s code 🙂

Let’s make sure we hooked everything up. We’ll load a web page:

    override func viewDidLoad() {
        super.viewDidLoad()
        getPost()
    }

    func getPost() {
        loadURL(urlString: "https://agostini.tech/2017/05/22/using-sirikit/")
    }
    
    func loadURL(urlString: String) {
        
        if let url = URL(string: urlString) {
            self.webView.loadRequest(URLRequest(url: url))
        }
    }

If we build and run, the page should load. And it should look the same as if we loaded it in Safari.

We’ll have an array of HTML element IDs and classes that we’ll remove, and we’ll have a method that will iterate over this array and remove the elements one by one. Let’s take a look at the method:

    private func removeElements(fromWebView webView: UIWebView) {
        self.elementsToRemove.forEach { self.removeElement(elementID: $0, fromWebView: webView) }
    }
    
    private func removeElement(elementID: String, fromWebView webView: UIWebView) {
        let removeElementIdScript = "var element = document.getElementById('\(elementID)'); element.parentElement.removeChild(element);"
        webView.stringByEvaluatingJavaScript(from: removeElementIdScript)
        
        let removeElementClassScript = "document.getElementsByClassName('\(elementID)')[0].style.display=\"none\";"
        webView.stringByEvaluatingJavaScript(from: removeElementClassScript)
    }

In the ‘removeElement’ method we’re using a bit of JavaScript to fetch the element and then we remove the element from its parent. UIWebView is able to execute JavaScript on the fly, which works great for us. So we can just execute this small piece of JavaScript by calling ‘stringByEvaluatingJavaScript’. We’ll do something similar to find the element by its class name, and remove the first element we find with that class name.

Set your view controller as the delegate of your webView and in the ‘webViewDidFinishLoad’ just call ‘removeElements’ method.

If you inspect your web page in Chrome browser you can easily find the IDs of the elements you want to remove, for example, this is the header element that I want to get rid of:

If you’re using this blog as an example, just add the ‘masthead’ into the ‘elementsToRemove’ array, build and run and hopefully, you won’t see the header anymore:

Bye, bye header 🙂 You can use Chrome to find the IDs of the remaining elements you want to remove, just add all of them in that array. This is what I ended up with:

    public var elementsToRemove: [String] = [
        "masthead",
        "secondary",
        "sharedaddy sd-sharing-enabled",
        "jp-relatedposts",
        "sharedaddy sd-block sd-like jetpack-likes-widget-wrapper jetpack-likes-widget-loaded",
        "entry-meta",
        "nav-single",
        "comments",
        "colophon"
    ]

We managed to strip all but the main content of the web page, the footer looks pretty clean:

Internal Hosts

One other thing I wanted to do was to keep the navigation within the agostini.tech host in the web view, and to open all the other links in Safari. So, for example, if you tap on the ‘Offline Voice Command and Text-ToSpeech’, that page would load in the web view, but if you tapped on ‘DADependencyInjection’ (a GitHub repo), that link would open in Safari. This might sound complicated, but it’s quite simple to implement.

We’ll add all the hosts in an array, and in one of the web view delegates, we’ll check if the destination is in the list of internal hosts, if it’s not, we’ll open the link in Safari. Easy, right? Let’s see some code… The array is obvious, but let’s mention it anyway:

public var internalHosts: [String] = ["agostini.tech"]

Next, we need one utility method to check if a URL’s host is in the array of hosts:

    private func isExternalHost(forURL url: URL) -> Bool {
        
        if let host = url.host, internalHosts.contains(host) {
            return false
        }
        
        return true
    }

In the delegate callback we’ll check if the next URL request’s host is in the list, and open the page in Safari if it isn’t:

    func webView(_ webView: UIWebView, shouldStartLoadWith request: URLRequest, navigationType: UIWebViewNavigationType) -> Bool {
        
        if let url = request.url, navigationType == .linkClicked, isExternalHost(forURL: url), UIApplication.shared.canOpenURL(url) {
            UIApplication.shared.open(url)
            return false
        }
        
        return true
    }

That’s pretty much it. WebView will call the delegate when it’s about to load the next request, which will give us the option of proceeding with the load, or not. We’ll intercept it here and load it in Safari if the host is not in the array.

Favorites

Just as a little extra, I added a favourite button. You can add/remove favourites. Maybe you’ll find it useful, so I’ll just mention it here quickly. The code is simple:

    fileprivate func addFavourite(fav: String) {
        if self.favourites.contains(fav) == false {
            self.favourites.append(fav)
        }
    }
    
    fileprivate func removeFavourite(fav: String) {
        if let index = self.favourites.index(of: fav) {
            self.favourites.remove(at: index)
        }
    }

And the IBAction:

    @IBAction func favourite(button: UIBarButtonItem) {
        guard let url = self.webView.request?.url?.absoluteString else {
            return
        }
        
        if self.favourites.contains(url) {
            self.removeFavourite(fav: url)
        } else {
            self.addFavourite(fav: url)
        }
        
        self.setFavouriteButton()
    }

Conclusion

I was thinking of creating an app for my blog, just for fun. I didn’t want to spend days on the layout for the post details, so I came up with the idea of using a simple web view. Obviously, I didn’t want the full page, but the stripped down version, hence, the post 🙂 I was surprised to see how well this actually works, and how much time I saved up. It takes just a couple of hours to set this up, and it looks great.

This was one of the shorter posts I wrote, I hope you’ll find it short and useful 🙂 All the code is on GitHub, so check it out.

Have a nice day 🙂

Dejan.

More resources

4 thoughts on “Customising UIWebView

  1. ramakrishna chunduri

    The article is a lot helpful. Few examples i can think of are

    In some cases where we are building an app for websites but want to clean clutter around the login screen we can do that.

    If we wish to have separate controllers for one article we can have example one for content and another for comments and ratings.

    Thanks a lot for such a detailed article.

    Reply
    1. Dejan Agostini Post author

      I’m very happy you found the article useful. Thank you very much for the kind words, and you’re welcome. I just hope you’ll find some of my other articles useful as well.

      Those are great examples. The reason I did this was because I wanted to display my blog articles in a web view, without all the clutter. If I had that problem, I’m sure other people had the same one. So I decided to share what I’ve learned.

      Have a nice day 🙂
      Dejan.

      Reply
  2. Kelvin Tan

    Lets say you dont have an ID, but you have only class name. Like <header class="header" ….
    What do you do with that? I tried it with your code and it didnt work with me.

    Reply
    1. Dejan Agostini Post author

      Getting an element by class name is a bit unstable. You see in the code where I’m accessing the first element in the array and removing it, you’ll have to iterate through the array, or figure out the index of the element you want to remove.

      Reply

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.