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 itssubviews
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:
- 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. - The text field becomes the first responder.
In response, the system displays the keyboard (or the text field’s input view) and posts thekeyboardWillShowNotification
andkeyboardDidShowNotification
notifications as needed. If the keyboard or another input view was already visible, the system posts thekeyboardWillChangeFrameNotification
andkeyboardDidChangeFrameNotification
notifications instead. - The text field calls its delegate’s
textFieldDidBeginEditing(_:)
method and posts atextDidBeginEditingNotification
notification. - The text field calls various delegate methods during editing:
- Whenever the current text changes, it calls the
textField(_:shouldChangeCharactersIn:replacementString:)
method and posts thetextDidChangeNotification
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.
- Whenever the current text changes, it calls the
- Before resigning as first responder, the text field calls its delegate’s
textFieldShouldEndEditing(_:)
method. Use that method to validate the current text. - 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 thekeyboardWillHideNotification
andkeyboardDidHideNotification
notifications. - The text field calls its delegate’s
textFieldDidEndEditing(_:)
method and posts atextDidEndEditingNotification
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
methodlocation(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 callinglocation(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’sremoveFromSuperview()
method, or set the view’sisHidden
property totrue
.
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 theUITraitEnvironment
protocol. This protocol is adopted by the following classes:UIScreen
,UIWindow
,UIViewController
,UIPresentationController
, andUIView
. 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 theUITraitCollection
horizontalSizeClass
,verticalSizeClass
,displayScale
, anduserInterfaceIdiom
properties. The values that express idiom and size traits are defined in theUIUserInterfaceIdiom
andUIUserInterfaceSizeClass
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 thewillTransition(to:with:)
method of theUIContentContainer 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 aUIImageAsset
instance, as described in the overview section ofUIImageAsset
. 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 theUIViewController
class.A standalone trait collection is also useful in customizing view appearance, by way of the
appearance(for:)
protocol method, as described inUIAppearance
.
…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:
- We only support https:// website, which we need to correct
- If the user doesn’t type https:// (or http:// after our previous edit) the app doesn’t work so we need to extend this.
- 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.
- 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!