SwiftUI Webview with a Progress Bar

If you’re not familiar, SwiftUI is the new UI framework for building user interfaces in Swift apps. If you squint, the declarative syntax is vaguely reminiscent of contemporary JavaScript frameworks like React.

With a declarative Swift syntax that’s easy to read and natural to write, SwiftUI works seamlessly with new Xcode design tools to keep your code and design perfectly in sync. Automatic support for Dynamic Type, Dark Mode, localization, and accessibility means your first line of SwiftUI code is already the most powerful UI code you’ve ever written.

developer.apple.com/xcode/swiftui/

The API is still fairly immature. When you get beyond the sample apps, it’s hard to build anything real with the components that come out of the box. Components that you would expect to see if you’re familiar with UIKit, like WKWebView and UIActivityIndicatorView don’t exist in SwiftUI yet.

Luckily, it’s not that hard to create them yourself.

To get started with a basic view, you need an object that implements UIViewRepresentable. A simple Webview could look like this:

struct Webview: UIViewRepresentable { let url: URL func makeUIView(context: UIViewRepresentableContext<Webview>) -> WKWebView { let webview = WKWebView() let request = URLRequest(url: self.url, cachePolicy: .returnCacheDataElseLoad) webview.load(request) return webview } func updateUIView(_ webview: WKWebView, context: UIViewRepresentableContext<Webview>) { let request = URLRequest(url: self.url, cachePolicy: .returnCacheDataElseLoad) webview.load(request) } }

Progress Bar Example

It’s also possible to model a UIViewController by implementing UIViewControllerRepresentable.

For example, a view controller that renders a web view with a progress bar:

class WebviewController: UIViewController { lazy var webview: WKWebView = WKWebView() lazy var progressbar: UIProgressView = UIProgressView() override func viewDidLoad() { super.viewDidLoad() self.webview.frame = self.view.frame self.view.addSubview(self.webview) self.view.addSubview(self.progressbar) self.progressbar.translatesAutoresizingMaskIntoConstraints = false self.view.addConstraints([ self.progressbar.topAnchor.constraint(equalTo: self.view.topAnchor), self.progressbar.leadingAnchor.constraint(equalTo: self.view.leadingAnchor), self.progressbar.trailingAnchor.constraint(equalTo: self.view.trailingAnchor), ]) self.progressbar.progress = 0.1 webview.addObserver(self, forKeyPath: "estimatedProgress", options: .new, context: nil) } // MARK: - Web view progress override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { switch keyPath { case "estimatedProgress": if self.webview.estimatedProgress >= 1.0 { UIView.animate(withDuration: 0.3, animations: { () in self.progressbar.alpha = 0.0 }, completion: { finished in self.progressbar.setProgress(0.0, animated: false) }) } else { self.progressbar.isHidden = false self.progressbar.alpha = 1.0 progressbar.setProgress(Float(self.webview.estimatedProgress), animated: true) } default: super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) } } }

Then you can implement that as a UIViewControllerRepresentable like so:

struct Webview: UIViewControllerRepresentable { let url: URL func makeUIViewController(context: Context) -> WebviewController { let webviewController = WebviewController() let request = URLRequest(url: self.url, cachePolicy: .returnCacheDataElseLoad) webviewController.webview.load(request) return webviewController } func updateUIViewController(_ webviewController: WebviewController, context: Context) { let request = URLRequest(url: self.url, cachePolicy: .returnCacheDataElseLoad) webviewController.webview.load(request) } }

You can see a working example of this view controller over on GitHub.

Wire

Wire is an RSS reader for iOS that displays articles with their native formatting — or an optimized mobile version for sites that support AMP — and doesn’t require yet another account to sign up for.

As mentioned previously, it had been quite some time since I wrote any iOS apps, so I decided to use some of my free time this summer to build an app that I wanted to use.

There are other RSS readers. It’s not exactly new territory. But like most apps, the good ones all seem to require you to sign up for another account. I don’t know about you, but I find the number of accounts I already have to worry about somewhat overwhelming. I don’t need anymore. Relying on an iCloud account won’t solve this problem for everyone, but in this case it solves it for me.

The other main feature I wanted in an RSS reader was the ability to disable the monotonous E-reader-like view that has become so common. So many of the websites I read look great on mobile.

As you can see, it’s a pretty standard design. There’s a simple list of articles that you can group however you want. The article view loads a selected article in a web view. I really prefer this real view of the site to the reader-ized version other apps use.

I have found Wire to be quite good for what I want. It doesn’t do much more than aggregate articles from the websites you want to keep up with, but I think it does that well.

Core Data & Concurrency

It’s been a while since I last worked on any mobile apps, so I thought the last couple weeks of sabbatical would be a good time to get caught up on iOS and specifically to learn Swift, which didn’t exist last time ?. For the most part, it was pretty easy to pick up and I was able to move fairly quickly on some apps I had been thinking about.

One of the only problems I ran into was related to Core Data concurrency. Specifically, if two different threads are reading and writing data, you get errors like 'NSGenericException', reason: Collection <__NSArrayM: 0x7fabb400> was mutated while being enumerated.

The solution is private queue contexts. I won’t do a full explanation of queue contexts here, but the Apple Developer Documentation has some good information. The idea is to create a private context to operate on while we’re doing background work.

For this to work as expected there are a few things that need to happen:

  1. Set context.parent. In order for the changes to eventually be written to disk, we have to associate the new, private context with the main context by setting newContext.parent = oldContext.
  2. Use context.object(with: objectID) to make core data relationships. We need to get a reference in the current context to any objects that were created outside the context.
  3. After saving, we also need to save the parent context to commit the changes to disk. To make it thread safe, we use oldContext.perform or oldContext.performAndWait depending on whether is should be asynchronous or not.

I put together a gist to demonstrate:

The following articles were especially helpful to understand how to fix this problem in my case:

  1. Apple’s Core Data Programming Guide > Concurrency
  2. Core Data Concurrency & Maintaining a Silky Smooth UI