Learning Swift — Days 134 to 135

Hacking with Swift — Learning Project 31

After a good pause where I feel I have almost forgotten everything, it is time to go on and finish this book, before going back, reviewing it all and creating my coding snippets library so that, when my memory will fail, and it will fail, I will know where to look!

Setting up

This new project is called Multi-browser and its aim is to get us started with UIStackView and see how easy iPad multitasking is.

As usual in this series we create a new Xcode project based on the Single View App template, we save it somewhere sensible and, then, for this very one, we change the Deployment Info in the Project Editor so that it is iPad only!

UIStackView by example

We learn here that there are two kinds of multitasking: Slide Over and Split View. In both of them our app will have much less space available and here is where UIStackView comes to our aid.

Inside Main.storyboard, embed the view controller in a navigation controller and then drag a Text Field and a Horizontal Stack View to the canvas from the Object Library. All that matters by now is that the text field be above the stack view.

Select the text field and click on the Pin menu in the bottom right corner; deselect Constrain to Margins and add constraints for each side so that they are 5 points from each neighbour.

Repeat the same operation for the horizontal stack view just this time using 5 for the top constraint and 0 for the other three. With the stack view still selected, open the Attributes Inspector and change the Distribution attribute to Fill Equally and the Spacing attribute to 5.

Open the Assistant Editor and create outlets for our views, called addressBar for the text field and stackView for the stack view. Last but not least for Interface Builder, ctrl-drag from the text field to the white and yellow symbol just above to make the view controller be the delegate of our text field. Now close the Assistant Editor and open ViewController.swift.

Adding view to UIStackView with addArrangedSubview()

Inside viewDidLoad() add a call to the yet unwritten setDefaultTitle() method and then create two bar button items, called add and delete. The first will have a barButtonSystemItem parameter of .add a target of self and an action of #selector(addWebView) (not written yet), while the second will have respectively .trash, self and #selector(deleteWebView).

Finally, group them into the navigationItem.rightBarButtonItems array with the delete button before the add.

Below viewDidLoad() let’s add the setDefaultTitle() method which simply sets the title property of our view controller to “Multibrowser”.

Import WebKit then, just below setDefaultTitle() add the @objc method called addWebView(). Inside it, create a new web view and set its .navigationDelegate property to self (our view controller). Xcode will complain so add conformance to the WKNavigationDelegate protocol. This protocol is thus described:

The methods of the WKNavigationDelegate protocol help you implement custom behaviors that are triggered during a web view’s process of accepting, loading, and completing a navigation request.

After this we should add our web view as an arranged subview to our stack view. We do this instead of using the addSubview() method because the stack view manages its own subviews directly. This is the method we are calling instead:

Summary

Adds a view to the end of the arrangedSubviews array.

Declaration

Discussion

The stack view ensures that the arrangedSubviews array is always a subset of its subviews array. This method automatically adds the provided view as a subview of the stack view, if it is not already. If the view is already a subview, this operation does not alter the subview ordering.

Parameters

view

The view to be added to the array of views arranged by the stack.

We then create a new URL from a string of a website and call the load(request:) method of our web view with such URL.

Before moving on, add conformance to two extra protocols which we will use later: UITextFieldDelegate and UIGestureRecognizerDelegate.

Here is the Documentation page for the first one:


UITextFieldDelegate

A set of optional methods that you use to manage the editing and validation of text in a text field object.

Declaration

protocol UITextFieldDelegate

Overview

A text field calls the methods of its delegate in response to important changes. You use these methods to validate text that was typed by the user, to respond to specific interactions with the keyboard, and to control the overall editing process. Editing begins shortly before the text field becomes the first responder and displays the keyboard (or its assigned input view). The flow of the editing process is as follows:

  1. Before becoming the first responder, the text field calls its delegate’s textFieldShouldBeginEditing(_:) method. Use that method to allow or prevent the editing of the text field’s contents.
  2. The text field becomes the first responder.
    In response, the system displays the keyboard (or the text field’s input view) and posts the keyboardWillShowNotification and keyboardDidShowNotification notifications as needed. If the keyboard or another input view was already visible, the system posts the keyboardWillChangeFrameNotification and keyboardDidChangeFrameNotification notifications instead.
  3. The text field calls its delegate’s textFieldDidBeginEditing(_:) method and posts a textDidBeginEditingNotification notification.
  4. The text field calls various delegate methods during editing:
    • Whenever the current text changes, it calls the textField(_:shouldChangeCharactersIn:replacementString:) method and posts the textDidChangeNotification notification.
    • It calls the textFieldShouldClear(_:) method when the user taps the built-in button to clear the text.
    • It calls the textFieldShouldReturn(_:) method when the user taps the keyboard’s return button.
  5. Before resigning as first responder, the text field calls its delegate’s textFieldShouldEndEditing(_:) method. Use that method to validate the current text.
  6. The text field resigns as first responder.
    In response, the system hides or adjusts the keyboard as needed. When hiding the keyboard, the system posts the keyboardWillHideNotification and keyboardDidHideNotification notifications.
  7. The text field calls its delegate’s textFieldDidEndEditing(_:) method and posts a textDidEndEditingNotification notification.

The second one is described as follows:


Summary

A set of methods implemented by the delegate of a gesture recognizer to fine-tune an app’s gesture-recognition behavior.

Declaration

protocol UIGestureRecognizerDelegate

Discussion

The delegates receive messages from a gesture recognizer, and their responses to these messages enable them to affect the operation of the gesture recognizer or to specify a relationship between it and another gesture recognizer, such as allowing simultaneous recognition or setting up a dynamic failure requirement.

An example of a situation where dynamic failure requirements are useful is in an app that attaches a screen-edge pan gesture recognizer to a view. In this case, you might want all other relevant gesture recognizers associated with that view’s subtree to require the screen-edge gesture recognizer to fail so you can prevent any graphical glitches that might occur when the other recognizers get canceled after starting the recognition process. To do this, you could use code similar to the following:

let myScreenEdgePanGestureRecognizer = UIScreenEdgePanGestureRecognizer(target: self, action:#selector(handleScreenEdgePan))
myScreenEdgePanGestureRecognizer.delegate = self
    // Configure the gesture recognizer and attach it to the view.
 
...
 
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
    guard let myView = myScreenEdgePanGestureRecognizer.view,
          let otherView = otherGestureRecognizer.view else { return false }
    
    return gestureRecognizer == myScreenEdgePanGestureRecognizer &&
           otherView.isDescendant(of: myView)
}

At the end of the addWebView method we are going to set the border color of the layer of web view equal to the cgColor version of the blue UIColor. We then call the yet unwritten selectWebView method, passing it our webView as an argument. Finally we declare a new UITapGestureRecognizer with a target of self and an action of #selector(webViewTapped) (also not written yet), we set its delegate to be our view controller and we call the .addGestureRecognizer() method on our web view passing the recogniser in as an argument. Here is the Documentation for the UITapGestureRecognizer:


Summary

A concrete subclass of UIGestureRecognizer that looks for single or multiple taps.

Declaration

Discussion

For the gesture to be recognized, the specified number of fingers must tap the view a specified number of times. Although taps are discrete gestures, they are discrete for each state of the gesture recognizer; thus the associated action message is sent when the gesture begins and is sent for each intermediate state until (and including) the ending state of the gesture. Code that handles tap gestures should therefore test for the state of the gesture, for example:

Action methods handling this gesture may get the location of the gesture as a whole by calling the UIGestureRecognizer method location(in:); if there are multiple taps, this location is the first tap; if there are multiple touches, this location is the centroid of all fingers tapping the view. Clients may get the location of particular touches in the tap by calling location(ofTouch:in:); if multiple taps are allowed, this location is that of the first tap.


Let’s now create a weak variable property to track the active web view, of type WKWebView? (optional and weak because it might not exist AND it may go away at any time).

Now write the selectWebView method, which takes a single parameter of type WKWebView, which loops over the arranged subviews of the stack view and changes their layer’s border width to 0 and the newly selected view’s one to have a border width of 3 points.

Write the webViewTapped method which takes a single parameter of type UITapGestureRecognizer. This method checks if there is a web view as the recogniser’s view and, if so, stores it in a constant through optional binding and set it as the argument for the selectWebView method called inside.

As we want our gesture to be recognised together with the web view’s ones we need to simply add this method:

func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
    return true
}

Now, before trying to run this project, implement the textFieldShouldReturn(_:) method of UITextFieldDelegate. Inside check that we have a web view which is the active web view and that there is text in the address bar. If this succeeds check that it is possible to convert that text into an URL and, if this succeeds, load that page. Then, resign the first responder status of the text field and return true.


The project now compiles and works, but I already see many things I would like change but I will be a good student and keep calm! 🙂

Removing views from a UIStackView with removeArrangedSubview()

Fill in the deleteWebView() method: first let’s check that there is an active web view and store it in a constant, second let’s check that such web view can be found in the array of the stack view’s arranged subviews and get its first index. If all this succeeds, lets call .removeArrangedSubview on the stack view passing our web view as the only argument. This method is pretty interesting and worth taking a look at in the Documentation:

Summary

Removes the provided view from the stack’s array of arranged subviews.

Declaration

Discussion

This method removes the provided view from the stack’s arrangedSubviews array. The view’s position and size will no longer be managed by the stack view. However, this method does not remove the provided view from the stack’s subviews array; therefore, the view is still displayed as part of the view hierarchy.

To prevent the view from appearing on screen after calling the stack’s removeArrangedSubview: method, explicitly remove the view from the subviews array by calling the view’s removeFromSuperview() method, or set the view’s isHidden property to true.

After reading this it is clear that our next step is to call the removeFromSuperview() method on our web view. Once again, let’s look at it in the Documentation:

Summary

Unlinks the view from its superview and its window, and removes it from the responder chain.

Declaration

Discussion

If the view’s superview is not nil, the superview releases the view.

Calling this method removes any constraints that refer to the view you are removing, or that refer to any view in the subtree of the view you are removing.

Important

Never call this method from inside your view’s draw(_:) method.

Now, if this was the last arranged subview we call the setDefaultTitle() method but, if that was not the case, we convert the index extracted above into an integer and, if this is the only one remaining, we decrease that index by 1. If there is an arranged subview in the stack view at that index and if it is possible to downcast it as a WKWebView, we call the selectWebView() method passing this new object as its only argument.

The only thing that remains unclear to me, and I have read the chapter again and again, is that the title of the view controller should change according to the page’s title, but I found no code for that.

iPad multitasking

To support multitasking in a good way we need to override the traitCollectionDidChange method by having an if-else statement that sets the stack view’s axis to vertical is the horizontal size class is compact and to horizontal otherwise. Let’s delve a bit deeper into this, by looking at the method’s description (which is partly in Swift and partly in Objective-C still):

Summary

Called when the iOS interface environment changes.

Declaration

Discussion

The system calls this method when the iOS interface environment changes. Implement this method in view controllers and views, according to your app’s needs, to respond to such changes. For example, you might adjust the layout of the subviews of a view controller when an iPhone is rotated from portrait to landscape orientation. The default implementation of this method is empty.

At this point the documentation recommends that we call super.traitCollectionDidChange() but we didn’t do this here, most probably for a very good reason.

The UITraitCollection class is a very interesting bit of code to look at, and here it is:

Summary

The iOS interface environment for your app, defined by traits such as horizontal and vertical size class, display scale, and user interface idiom.

Declaration

Discussion

The iOS trait environment is exposed though the traitCollection property of the UITraitEnvironment protocol. This protocol is adopted by the following classes: UIScreen, UIWindow, UIViewController, UIPresentationController, and UIView. To create an adaptive interface, write code to adjust your app’s layout according to changes in these traits. You access specific trait values using the UITraitCollection horizontalSizeClass, verticalSizeClass, displayScale, and userInterfaceIdiom properties. The values that express idiom and size traits are defined in the UIUserInterfaceIdiom and UIUserInterfaceSizeClass enumerations; the value for the display scale trait is expressed as a floating point number.

To make your view controllers and views responsive to changes in the iOS interface environment, override the traitCollectionDidChange(_:) method from the trait environment protocol. To customize view controller animations in response to interface environment changes, override the willTransition(to:with:) method of the UIContentContainer protocol.

Figure 1 shows the horizontal (width) and vertical (height) size classes your app can encounter when running on various devices fullscreen.

For information about size classes your app encounters in Slide Over and Split View on iPad, read Slide Over and Split View Quick Start in Adopting Multitasking Enhancements on iPad.

You can create standalone trait collections to assist in matching against specific environments. The UITraitCollection class includes four specialized constructors as well as a constructor that lets you combine an array of trait collections, init(traitsFrom:).

One important use of standalone trait collections is to enable conditional use of images based on the current iOS interface environment. You can associate a trait collection with a UIImage instance by way of a UIImageAsset instance, as described in the overview section of UIImageAsset. For information on configuring asset catalogs graphically from within the Xcode IDE, see Asset Catalog Help.

You can employ a standalone trait collection to enable a two-column split view in landscape orientation on iPhone. See the setOverrideTraitCollection(_:forChild:) method of the UIViewController class.

A standalone trait collection is also useful in customizing view appearance, by way of the appearance(for:) protocol method, as described in UIAppearance.


…an awful lot to learn and only so much time…

As I was wondering before, here is the method that will modify the title of the view controller according to the selected web view. It is called updateUI(for webView: WKWebView) and it sets the view controller’s title to be equal to the web view’s title and the address bar’s text to be the absolute string or the web view’s url or, if this is not available, an empty string. What an absolute string is remains obscure to me and it is not explained in the Documentation.

Whatever, call this method at the end of the selectWebView() method, passing the webView parameter as its argument.

Last thing, implement the webView(_:didFinish:) method with a check that the webView parameter is equal to the activeWebView property and, if so, call the updateUI(for:) method on the web view.

Done!


A few things remain unpolished:

  1. We only support https:// website, which we need to correct
  2. If the user doesn’t type https:// (or http:// after our previous edit) the app doesn’t work so we need to extend this.
  3. Having the Multibrowser title with a big white space below isn’t that helpful, so we should add at least a placeholder text in there.
  4. When you delete the last open web view the “https://hackingwithswift.com” string remains in the text field. This should be corrected.

Let’s see how we fare with these challenges.

The first challenge is pretty straightforward: open the Info.plist file as Source Code and, before the last two lines, paste in this code

<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsArbitraryLoadsInWebContent</key>
    <true/>
</dict>

The second challenge required a bit more of thinking, as it should!, but it was more because I needed to wrap my head around what each method was doing. The solution (hopefully a good one), was to edit the following line of the textFieldShouldReturn method:

if let url = URL(string: "https://" + address) {

…in this way every address one types in is preceded by “https://“. Sure, this is not yet a full fledged browser that will just open any domain I throw at it but, it is for sure a good start.

For the third challenge I added this to the setDefaultTitle() method:

if stackView.arrangedSubviews.count == 0 {
    addressBar.placeholder = "Hit +, enter a web address and hit return to start browsing!"
}

But this is not enough because, if you hit +, then select the URL, erase it and … oh … the same placeholder is there so… in the addWebView() method, somewhere toward the top, add this line:

addressBar.placeholder = ""

For the fourth and final small detail, add this line to the end of the setDefaultTitle() method:

addressBar.text = ""

Now the project compiles as I want it to!

I feel proud!

Please don’t forget to drop a hello and a thank you to Paul for all his great work (you can find him on Twitter) and be sure to visit the 100 Days Of Swift initiative page. We are learning so much thanks to him and he deserves to know of our gratitude.

He has written about 20 great books on Swift, all of which you can check about here.

The 100 Days of Swift initiative is based on the Hacking with Swift book, which you should definitely check out.


If you like what I’m doing here please consider liking this article and sharing it with some of your peers. If you are feeling like being really awesome, please consider making a small donation to support my studies and my writing (please appreciate that I am not using advertisement on my articles).

If you are interested in my music engraving and my publications don’t forget visit my Facebook page and the pages where I publish my scores (Gumroad, SheetMusicPlus, ScoreExchange and on Apple Books).

You can also support me by buying Paul Hudson’s books from this Affiliate Link.

Anyways, thank you so much for reading!

Till the next one!

Published by Michele Galvagno

Professional Musical Scores Designer and Engraver Graduated Classical Musician (cello) and Teacher Tech Enthusiast and Apprentice iOS / macOS Developer Grafico di Partiture Musicali Professionista Musicista classico diplomato (violoncello) ed insegnante Appassionato di tecnologia ed apprendista Sviluppatore iOS / macOS

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: