Book Image

Learning iOS UI Development

Book Image

Learning iOS UI Development

Overview of this book

Through this comprehensive one-stop guide, you’ll get to grips with the entire UIKit framework and in a flash, you’ll be creating modern user interfaces for your iOS devices using Swift. Starting with an overview of the iOS drawing system and the available tools, you will then learn how to use these technologies to create adaptable layouts and custom elements for your applications. Next, you’ll be introduced to other topics such as animation and code-drawing with Core Graphics, which will give you all the knowledge you need to create astonishing user interfaces. By the end of this book, you will have a solid foundation in iOS user interface development and will have gained valuable insights on the process of building firm and complex UIs.
Table of Contents (16 chapters)
Learning iOS UI Development
Credits
About the Author
About the Reviewers
www.PacktPub.com
Preface
Index

UI hierarchy and views inheritance


In this chapter, we already talked about UI hierarchies. Let's dive into the topic by introducing the concepts of subview and superview and learning how to build and manage complex hierarchies.

Every instance of UIView (or its subclass) can be connected with other views in a parent-child relationship. The parent view is called superview, while the children are called subviews. A view can only have one superview, but it can contain more than one subview:

Thanks to the dedicated UIView properties and functions, you can easily inspect a view hierarchy and navigate from a root view down through to the last view of the hierarchy.

A view can access its parent from the superview property, as follows:

let parentView = view.superview 

The property returns a single UIView reference or nil if the view is not added to the hierarchy yet.

In a similar way, the subviews property returns an array of UIView instances that are the children of the view. Take a look at this code:

let children = view.subviews 

Managing with the hierarchy

The UIView class offers helpful functions to add, move, and delete elements from the hierarchy; you can call these functions directly from the view instances.

The addSubview: function pushes a view as the child of the caller, as follows:

containerView.addSubview(childView)

Views that are added as subviews of the same view are sibling. Sibling subviews are assigned to an index based on the order of insertion, which in turn corresponds to the drawing order—the highest index is drawn at the front, while the lowest is drawn behind the other sibling views. The addSubview: function assigns the first free index to the view, determining its position, which is in front of all the other views with lower indexes.

This order can be manipulated using functions that specify the desired index in an absolute or relative way:

containerView.insertSubview(childView, atIndex: 2) 

With the insertSubview:atIndex function, you can specify an index in which to place the view. If the index is not free, the view at this index gets shifted up by one index, consequently shifting all the other sibling views with indexes greater than the requested one.

With the same logic, a view can be placed into the hierarchy using another view as a reference and specifying that the new view should be inserted above (insertSubview:aboveSubview) or below (insertSubview:belowSubview) the referenced view, as follows:

containerView.insertSubview(childView, aboveSubview: anotherView)

containerView.insertSubview(childView, belowSubview: anotherView) 

Removing a view from the hierarchy is extremely simple. If you have a reference of the view that you want to remove, just call the removeFromSuperview function for this view. Alternatively, if you want to remove all the subviews from the parent view, just loop through the subviews and remove the views one by one. You can use the following code for this:

for subview in container.subviews {
    subview.removeFromSuperview()
}

Note

When you remove a view from the hierarchy, you are removing its subviews as well. If you haven't defined any reference to the views, they are no longer retained in memory; therefore, all chances to get access to the views are lost.

When you need to access a subview on the fly without saving a reference to it, you can use the tag property. A UIView instance can be easily marked using an integer, and you can obtain the view that corresponds to the same tag using the viewWithTag; function. This function returns the view itself or the first subview whose tag coincides with the requested tag.

View and subview visibility

A parent view defines its subviews' visibility outside its boundaries through the Boolean property clipToBounds. If this property is true, the view behaves as a mask for its subviews, preventing them from being drawn outside the boundaries. The following image presents the result of different clipToBounds values on the same hierarchy:

Another important note about subview visibility is related to the alpha property, which, as we mentioned previously, defines the opacity of the view. Subviews indirectly inherit the alpha value from their superviews, so the resulting opacity of the subview depends both on their parent and their own alpha.

Note

If your view has a nontransparent background, it is good practice to leave the opaque property set to true as a hint for the drawing system. Opaque views are optimized for this. The opposite is suggested if the view is fully or partially transparent; set this property to false.

Hierarchy events

Changes on the hierarchy can be intercepted and managed through callbacks called on the parent and child views.

When a view is attached to a superview, the didMoveToSuperview function is called. If we want to perform a task after this event is triggered, we have to override this function with a UIView subclass by executing the following code:

class CustomView: UIView {

    override func didMoveToSuperview() {
        println("I have a superview!")
    }
}

In the same way, the event can be intercepted by the superview overriding the didAddSubview:subview function, as follows:

override func didAddSubview(subview: UIView) {
    println("The subview \(subview) has been added")
}

The view will be added to a hierarchy that has a window as root. At this point, the didMoveToWindow event is called. After the event is triggered, the window property of the view instance becomes a reference to the root window; this turns out to be a handy way to check whether a view has reached the screen.

Remember that the only way to show a view on screen is through a window; so, if the window property is nil, you can be sure that the view is not added to a window hierarchy yet and that, for this reason, it won't be visible. Take a look at this code:

override func didMoveToWindow() {
   println("I've been attached to this window hierarchy: \(window)")
}

Another important method that responds to hierarchy changes is layoutSubviews. This function is called as soon as a subview is added to or removed from the hierarchy and every time the bounds of the view change. Within this function, Auto Layout constraints (more on this will be discussed in Chapter 5, Adaptive User Interfaces) are read, and they act to organize the subview's layout. You can override this function to perform your customizations to the layout, adding or tweaking the current constraints.

Notes about debug

When views are complex, the hierarchy is hardly straightforward. Xcode helps you, though, with an interesting debug instrument. When your application is running, select Debug -> View Debugging -> Capture View Hierarchy from the Xcode menu, and you'll see an interactive 3D representation of the current view hierarchy showing the depth and the position of all the views contained:

Hands-on code

In this section, you will practice some of the concepts that we just discussed by writing a real application.

Open the Chapter 1 folder and launch the ...\Start\Chapter1 project. You'll find a simple application structure that is the starting point for this exercise; the final result is the ...\Completed\Chapter1 project.

The base structure is really simple: an uppermost area with some controls (two buttons and one segmented control) and a view (with a light gray background) that we call a container:

Your goal is to implement the createView: function that receives a location (CGPoint) and adds a subview at this location inside the container. Depending on the value of the segmented control, you should use the received location as the center (red views) or upper-left corner of the new view (blue views). You need to also implement the clear function to remove all the added subviews. The project implements a tap gesture on the container, which invokes createView linked to the touch location.

Let's implement the createView function for the viewController.swift file first by executing the following:

    func createView(location:CGPoint){
        let viewSize = CGSize(width: 50, height: 50)
        let viewFrame = CGRect(origin: location, size: viewSize)
        let childView = UIView(frame: viewFrame)
        childView.backgroundColor = UIColor.redColor()
        viewContainer.addSubview(childView)

        if isCenterAligned {
            childView.center = location
            childView.backgroundColor = UIColor.blueColor()
        }
 }

The childView function is instantiated using a fixed size and the received location as its origin. Then, it is simply attached to viewContainer using addSubview. If the isCenterAligned property (which is handled by the segmented control) is true, the center property of the view is moved to the received location.

The implementation of the clear function is straightforward, as you can note in the following code:

@IBAction func clear(){
for subview in viewContainer.subviews{
            subview.removeFromSuperview()
        }
} 

It just performs a loop through the viewContainer subviews to call the removeFromSuperview function on these subviews.

A second functionality can be added to this exercise. Push the "Deep" button on the upper-left side of the screen, and debug the current hierarchy using the "capture view hierarchy" function to see this great feature in action!

View drawing and life cycle

iOS uses remarkable optimizations in the process of drawing contents on screen. Views are not continuously drawn; the system draws any view just once and creates snapshots for each displayed element. The current snapshot is shown until a view doesn't require an update. The view is then redrawn, and a new snapshot for the updated view is taken. This is a clever way to avoid a wastage of resources. In devices such as smartphones, optimization is mandatory.

The UIView content can be invalidated by calling the setNeedsDisplay: or setNeedsDisplayInRect: function.

This call basically tells the drawing system that the view content needs to be updated with a new version. Later, during the next run loop, the system asks the view to redraw. The main difference between the setNeedsDisplay: and setNeedsDisplayInRect: functions is that the latter performs an optimization using only a portion of the new view content.

In most cases, the redrawing process is managed for you by UIKit. If you need to create your really custom UIView subclass, though, you probably want to draw the contents of the view yourself. In this case, the drawRect: function is the place where you will add the drawing functions. You'll learn more about custom drawing in Chapters 6, Layers and Core Animation, and Chapter 9, Introduction to Core Graphics. For now, it suffices to say that you should never call this function directly! When the content of the view needs to be updated, just call setNeedDisplay: and the drawRect: function will be executed by the system following the right procedure.

Here is a quick example of custom drawing with which we can create a UIView subclass that draws a simple red ellipse at the center of the view:

import UIKit

class ellipseView: UIView {

    override func drawRect(rect: CGRect) {

        let path = UIBezierPath(ovalInRect: self.bounds)
        UIColor.redColor().setStroke()
        UIColor.orangeColor().setFill()
        path.fill()
        path.stroke()

    }
}

This function uses UIBezierPath to define an oval shape that has a stroke and fill of red and orange. This shape is finally drawn in the current Graphic Context, which can be seen as an empty dashboard where you can design using code. You'll learn more about Graphic Contexts in Chapter 9, Introduction to Core Graphics.

View controllers and views

The UIViewController property view is the root view of the hierarchy that defines the view controller's contents. As we already saw, a view controller is associated with the window's rootViewController property, and a connection is established between the window property of the view controller view and the window.

During its life cycle, a view controller deals with important events related to its view. Depending on your needs, you might find these events useful for the UI definition.

Here is a quick but complete description of the functions called in relation to these events:

Function

Description

loadView

This is called when the view is created and assigned to the view property. Depending on the setup of the view controller, a view can be created using a Storyboard or a .xib file. You will not override this method unless you decide not to implement the view using Interface Builder.

viewDidLoad

This is called after the view is loaded. You will override this to perform additional adjustments to the user interface. You can set the text value of labels with the data received from a previous controller in the navigation hierarchy, for instance.

viewWillAppear

This is called just before the view is added to a view hierarchy. It is obviously called after viewDidLoad, and it may be called more than once during the view controller's life cycle.

viewDidAppear

This is called after the view is added to a view hierarchy. When this method is called, the view is visible, and its window property is no longer nil.

viewWillLayoutSubviews

This method is called when the view bounds change or are initialized and the view is about to lay out the subviews.

viewDidLayoutSubview

When this method is called, the bounds are set and the view has just called the layoutSubviews method.

The appear-functions are called when the controller is presented for the first time when a back button is pressed or a modal is closed, showing the underlying view controller again. If you override these methods, you are required to call them on super before executing your code.