Update: (Nov 16, 2021) – This now includes better constraint management from feedback on GitHub.

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, WKNavigationDelegate {
	lazy var webview: WKWebView = WKWebView()
	lazy var progressbar: UIProgressView = UIProgressView()

	deinit {
		self.webview.removeObserver(self, forKeyPath: "estimatedProgress")
		self.webview.scrollView.removeObserver(self, forKeyPath: "contentOffset")
	}

	override func viewDidLoad() {
		super.viewDidLoad()

		self.webview.navigationDelegate = self
		self.view.addSubview(self.webview)

		self.webview.frame = self.view.frame
		self.webview.translatesAutoresizingMaskIntoConstraints = false
		self.view.addConstraints([
			self.webview.topAnchor.constraint(equalTo: self.view.topAnchor),
			self.webview.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
			self.webview.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
			self.webview.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
		])

		self.webview.addSubview(self.progressbar)
		self.setProgressBarPosition()

		webview.scrollView.addObserver(self, forKeyPath: "contentOffset", options: .new, context: nil)

		self.progressbar.progress = 0.1
		webview.addObserver(self, forKeyPath: "estimatedProgress", options: .new, context: nil)
	}

	func setProgressBarPosition() {
		self.progressbar.translatesAutoresizingMaskIntoConstraints = false
		self.webview.removeConstraints(self.webview.constraints)
		self.webview.addConstraints([
			self.progressbar.topAnchor.constraint(equalTo: self.webview.topAnchor, constant: self.webview.scrollView.contentOffset.y * -1),
			self.progressbar.leadingAnchor.constraint(equalTo: self.webview.leadingAnchor),
			self.progressbar.trailingAnchor.constraint(equalTo: self.webview.trailingAnchor),
		])
	}

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

		case "contentOffset":
			self.setProgressBarPosition()

		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.