Jamie Birch
Jamie Birch

Reputation: 6112

Swift 3, iOS 10 - programmatically declared UIStackView not fitting screen width

I'm a new Swift developer implementing the main parts of Safari with a WKWebView (I need to interface between Javascript & Swift, so SFSafariViewController is not an option) and am trying to declare all elements programmatically.

To imitate Safari's search bar and progress bar, I want to set a UISearchBar, stacked on top of a UIProgressView, as the titleView for UIViewController's UINavigationItem. I can manage it with just one element, but not with a stack of two elements.

Here's what my project looks like right now. The UISearchBar and UIProgressView are either too wide or too thin to fill the UINavigationBar properly, depending on the rotation:

Portrait view Landscape view

Here is my code for ViewController.swift:

import WebKit
import UIKit

class ViewController: UIViewController, WKNavigationDelegate, UISearchBarDelegate {
    var searchBar: UISearchBar = UISearchBar()
    var progressView: UIProgressView = UIProgressView(progressViewStyle: .bar)
    var stackView: UIStackView = UIStackView()
    var webView: WKWebView!

    override func loadView() {
        webView = WKWebView()
        webView.navigationDelegate = self
        view = webView
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        /** Watches for changes in the WKWebView.estimatedProgress variable, and  */
        webView.addObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress), options: .new, context: nil)

        /** Initialise toolbar elements */
        let spacer = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
        let refresh = UIBarButtonItem(barButtonSystemItem: .refresh, target: webView, action: #selector(webView.reload))
        toolbarItems = [spacer, refresh]
        navigationController?.isToolbarHidden = false

        /** Initialise the UISearchBar */
        searchBar.delegate = self // not clear yet whether setting this is necessary.
        searchBar.searchBarStyle = UISearchBarStyle.minimal
        searchBar.showsCancelButton = true
        searchBar.widthAnchor.constraint(equalToConstant: (navigationController?.navigationBar.bounds.width)!).isActive = true
        // searchBar.heightAnchor.constraint(equalToConstant: 44.0).isActive = true
        // searchBar.sizeToFit()

        /** Initialise the UIProgressView */
        progressView.widthAnchor.constraint(equalToConstant: (navigationController?.navigationBar.bounds.width)!).isActive = true
        // progressView.heightAnchor.constraint(equalToConstant: 4.0).isActive = true
        // progressView.sizeToFit()

        /** Add the UISearchBar & UIProgressView to the UIStackView, then initialise it and finally set it as the UINavigationItem's titleView.*/
        stackView.axis  = UILayoutConstraintAxis.vertical
        stackView.alignment = UIStackViewAlignment.center
        stackView.distribution = UIStackViewDistribution.fillProportionally
        stackView.addArrangedSubview(searchBar)
        stackView.addArrangedSubview(progressView)
        stackView.translatesAutoresizingMaskIntoConstraints = false;
        /* These two constraints are causing a crash, so disabling them for now. */
        // stackView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
        // stackView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true
        navigationItem.titleView = stackView

        navigationController?.hidesBarsOnSwipe = true

        let url = URL(string: "https://en.wikipedia.org")!
        webView.load(URLRequest(url: url))
        webView.allowsBackForwardNavigationGestures = true
    }

    /** Updates the UIProgressView. */
    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        // keyPath "estimatedProgress" is equivalent to #keyPath(WKWebView.estimatedProgress)
        if keyPath == "estimatedProgress" {
            // progressView.isHidden = webView.estimatedProgress == 1 // if we want to hide upon 100%
            progressView.progress = Float(webView.estimatedProgress)
        }
    }

    /** Sets the webView's title upon navigation finishing. */
    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        title = webView.title
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

}

Note: any accepted solution must continue to display the stack of elements properly following the callback to hide the UINavigationBar initiated by navigationController?.hidesBarsOnSwipe, ie. when a user performs a swipe gesture on the WKWebView.

Upvotes: 1

Views: 1792

Answers (1)

Jamie Birch
Jamie Birch

Reputation: 6112

Reporting back with my final code, gratefully guided by the solution given on Reddit by the author of Hacking With Swift:

class ViewController: UIViewController, UISearchBarDelegate {
    var webView: WKWebView?
    var searchBar: UISearchBar?
    var progressView: UIProgressView?

    override func loadView() {
        super.loadView()
        setUpWebView()
        view = self.webView! // TODO: fix constraints error when video is run.

        setUpSearchbar()
        setUpProgressView()

        let url = URL(string: "http://www.bbc.com")!
        webView!.load(URLRequest(url: url))
    }

    func setUpWebView(){
        webView = WKWebView()
    }

    func setUpSearchbar(){
        searchBar = UISearchBar()
        searchBar!.delegate = self
    }

    func setUpProgressView() {
        webView!.addObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress), options: .new, context: nil)
        guard let bar = navigationController?.navigationBar else { return; }

        progressView = UIProgressView(progressViewStyle: .bar)
        progressView!.translatesAutoresizingMaskIntoConstraints = false
        bar.addSubview(progressView!)

        progressView!.leadingAnchor.constraint(equalTo: bar.leadingAnchor).isActive = true
        progressView!.trailingAnchor.constraint(equalTo: bar.trailingAnchor).isActive = true
        progressView!.bottomAnchor.constraint(equalTo: bar.bottomAnchor).isActive = true
    }

    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        if keyPath == "estimatedProgress" {
            // progressView.isHidden = webView.estimatedProgress == 1 /* Optional. This hides progressView on 100% */
            progressView!.progress = Float((webView?.estimatedProgress)!)
        }
    }
}

Upvotes: 1

Related Questions