Intermediate Ios9 Swift
Intermediate Ios9 Swift
with Swift
Simon ng
APPCODA
Fully updated for Xcode 7 and Swift 2
Table of Contents
1. Preface
2. Building Adaptive User Interfaces
3. Adding Sections and Index list in UITableView
4. Animating Table View Cells
5. Working with JSON
6. How to Integrate Facebook and Twitter Sharing
7. Working with Email and Attachments
8. Sending SMS and MMS Using MessageUI Framework
9. How to Get Direction and Draw Route on Maps
10. Search Nearby Points of Interest Using Local Search
11. Audio Recording and Playback
12. Scan QR Code Using AVFoundation Framework
13. Working with URL Schemes
14. Building a Full Screen Camera
15. Video Capturing and Playback Using AVKit
16. Displaying Banner Ads using iAd
17. Working with Custom Fonts
18. Working with AirDrop and UIActivityViewController
19. Building Grid Layouts with Collection Views
20. Interacting with Collection Views
21. Adaptive Collection Views Using Size Classes and UITraitCollection
22. Building a Today Widget Using App Extensions
23. Building Slide Out Sidebar Menus
24. View Controller Transitions and Animations
25. Building a Slide Down Menu
26. Self Sizing Cells and Dynamic Type
27. XML Parsing and RSS
28. Applying a Blurred Background Using UIVisualEffect
29. Using Touch ID For Authentication
30. Building a Carousel-Like User Interface
2
Preface
At the time of this writing, the Swift programming language has been around for more than a
year. The new programming language has gained a lot of traction and continues to evolve, and
is clearly the future programming language of iOS. If you are planning to learn a programming
language this year, Swift should be on the top of your list.
I love to read cookbooks. Most of them are visually appealing, with pretty and delicious photos
involved. That's what gets me hooked and makes me want to try out the recipes. When I
started off writing this book, the very first question that popped into my mind was "Why are
most programming books poorly designed?" iOS and its apps are all beautifully crafted - so
why do the majority of technical books just look like ordinary textbooks?
I believe that a visually stunning book will make learning programming much more effective
and easy. With that in mind, I set out to make one that looks really great and is enjoyable to
read. But that isn't to say that I only focus on the visual elements. The tips and solutions
covered in this book will help you learn more about iOS 9 programming and empower you to
build fully functional apps more quickly.
The book uses a problem-solution approach to discuss the APIs and frameworks of iOS SDK,
and each chapter walks you through a feature (or two) with in-depth code samples. You will
learn how to build a universal app with adaptive UI, use Touch ID to authenticate your users,
create a widget in notification center and implement view controller animations, just to name a
few.
I recommend you to start reading from chapter 1 of the book - but you don't have to follow my
suggestion. Each chapter stands on its own, so you can also treat this book as a reference.
Simply pick the chapter that interests you and dive into it.
code using the download link included in each chapter. You can also join us on Facebook
(https://facebook.com/groups/appcoda) or Twitter (https://twitter.com/appcodamobile) for
any update announcement.
Got Questions?
If you have any questions about the book or find any error with the source code, post it on our
private community (https://facebook.com/groups/appcoda) or reach me at
[email protected].
Chapter 1
Building Adaptive User Interfaces
In the beginning, there was only one iPhone with a fixed 3.5-inch display. It was very easy to
design your apps.; you just needed to account for two different orientations (portrait and
orientation). Later on, Apple released the iPad with a 9.7-inch display. If you were an iOS
developer at that time, you had to create two different screen designs (i.e. storyboards / XIBs)
in Xcode for an app - one for the iPhone and the other for the iPad.
Gone are the good old days. Fast-forward to 2015: Apple's iPhone and iPad lineup has changed
a lot. With the recent launch of the iPhone 6s and iPhone 6s Plus, your apps are required to
7
support an array of devices with various screen sizes and resolutions including:
iPhone 4/4s (3.5-inch)
iPhone 5/5c/5s (4-inch)
iPhone 6/6s (4.7-inch)
iPhone 6/6s Plus (5.5-inch)
iPad / iPad 2 / iPad Air / iPad Air 2 (9.7-inch)
iPad Mini / iPad Mini 2 / iPad Mini 3 / iPad Mini 4 (7.9-inch)
iPad Pro (12.9-inch) (to be launched in November 2015)
It is a great challenge for iOS developers to create a universal app that adapts its UI for all of
the listed screen sizes and orientations. So what can you do to design pixel-perfect apps?
Starting from iOS 8, the mobile OS comes with a new concept called Adaptive User Interfaces,
which is Apple's answer to support any size display or orientation of an iOS device. Now apps
can adapt their UI to a particular device and device orientation.
This leads to a new UI design concept known as Adaptive Layout. Xcode 7 allows developers to
build an app UI that adapts to all different devices, screen sizes and orientation using a
universal storyboard. You can now lay out user interface components in a single storyboard.
To achieve adaptive layout, you will need to make use of a new concept called Size Classes,
which is available in iOS 8 and 9. This is probably the most important aspect which makes
adaptive layout possible. Size classes are an abstraction of how a device is categorized
depending on its screen size and orientation. You can use both size classes and auto layout
together to design adaptive user interfaces. In iOS 8/9, the process for creating adaptive
layouts is as follows:
You start by designing a generic layout. The base layout is good enough to support most of
the screen sizes and orientations.
You choose a particular size class and provide your layout specializations. For example,
you want to increase the spacing between two labels when a device is in landscape
orientation.
In this chapter, I will walk you through all the adaptive concepts such as size classes, by
building a universal app. The app supports all available screen sizes and orientations.
8
Adaptive UI demo
No coding is required for this project. You will primarily use storyboard to lay out the user
interface components and learn how to use auto layout and size classes to make the UI
adaptive. After going through the chapter, you will have an app with a single view controller
that adapts to multiple screen sizes and orientations.
Main.storyboard
square view controller with a default 600600 canvas. With size classes enabled, the view
controller represents a generic device instead of a concrete device (e.g. 4-inch iPhone). You can
lay out the user interface components (e.g. label), much like in older versions of Xcode, or in
project with size classes disabled.
9
Assets.xcassets
Next, go back to the storyboard. Drag an image view from the Object library to the view
controller. Set its width to
600
and height to
tshirt
390
Aspect Fill
Then, drag a view to the view controller and put it right below the image view. This view serves
as a container for holding other UI components like labels. By grouping related UI components
under the same view, it will be easier for you to work with auto layout in a later section. In Size
inspector, make sure you set the width to 600 and height to 210. Throughout the chapter, I will
refer to this view as Product Info View.
10
Avenir Next
and Y to
15
556
32
. Set
and height to
44
Drag another label and place it right below the previous label. In Attributes inspector, change
the text to
This is a free psd T-shirt mockup provided by pixeden.com. The PSD comes with a
plain simple tee-shirt mockup template. You can edit the t-shirt color and use the smart
layer to apply your designs. The high-resolution makes it easy to frame specific details
with close-ups.
number of lines to
Avenir Next
123
18
22
and Y to
58
556
and
Note that the two labels should be placed inside Product Info View. You can double-check by
opening Document Outline. The two labels are put under the view. If you've followed the
procedures correctly, your screen should look similar to this:
11
Even if your design does not match the reference design perfectly, it is absolutely fine, as we
will use auto layout constraints to lay out the view later. Now, let's conduct a quick test to
check out the look and feel of the design on different devices. In Xcode 7, you are not required
to run the app in simulators to see how the view appears. The latest version of Xcode comes
with a preview assistant which lets developers evaluate the resulting design on different size
displays.
In Interface Builder, open the Assistant pop-up menu > Preview (1). Then press and hold
option key, and click Main.storyboard (Preview).
Xcode will then display a preview of the app's UI in the assistant editor. By default, it shows
you the preview on an iPhone 4-inch device. You can click the + button at the lower-left corner
12
of the assistant editor to get a preview of an iPhone 3.5-inch and other devices. If you add all
the devices including the iPad in the assistant editor, your screen should look like the image
pictured below. As you can see, the current design doesn't look good on any of the devices. So
far we haven't defined any auto layout constraints. This is why the view doesn't fit properly on
any of the devices.
Constrain to
option is unchecked because we want to set the constraints relative to the super view's
Add 3 constraints
button.
Next, open Document Outline. Control-drag from the image view (tshirt) to the main view.
When prompted, select
Equal Heights
14
Once the Equal Heights constraint is added, it should appear in the Constraints section of
Document Outline. Select the constraint and go to Size inspector. Here you can edit the value
of the constraint to change its definition.
15
Superview.height
tshirt.Height
. If not, you can click the selection box of the first item and select
Reverse
100% of the main view (here, the main view is the superview). As mentioned earlier, the image
view should only take up around 65% of the main view. So change the multiplier from
0.65
to
Next, select Product Info View and click the Pin button. Select the left, right, and bottom sides,
and set the value to
the
Add 3 constraints
Constrain to margin
button. This adds three spacing constraints for the Product Info View.
Furthermore, we have to define a spacing constraint between the image view and the Product
Info View. In Document Outline, control-drag from the image view (tshirt) to Product Info
View. When prompted, select
Vertical Spacing
constraint such that there is no spacing between the bottom side of the image view and the top
of the Product Info View.
16
If you take a look at the views rendered in the preview assistant, the view should now look
much better on all devices; however, there is still a lot of work to do to perfect the design.
Now let's define the constraints for the two labels.
Select the title label, which is the one with the larger font size. Click the Pin button. Set the
value of the top side to
Constrain to margins
15
, left side to
22
22
Add 3 Constraints
Next, select the other label. Again, we'll add three spacing constraints for the left, right, and
bottom sides. Click the Pin button and add the constraints accordingly.
17
Lastly, define a spacing constraint between these two labels. Control-drag from the title label
to the description label. When prompted, select
Vertical Spacing
As soon as the constraint is added, you will see a few constraint lines in red, indicating some
layout issues. Auto layout issues can occur when some of the constraints are ambiguous. To fix
these issues, open Document Outline and click the red disclosure arrow to see a list of the
issues.
18
Xcode is smart enough to resolve these issues for us. Simply click the indicator icon and a popover shows you the possible solutions. When prompted, click
Change Priority
to resolve these
issues.
Cool! You've created all the auto layout constraints. Let's check out the preview and see the
result.
The view looks much better now, with the image view perfectly displayed and aligned.
19
252
251
. In other words,
the description label has a higher content hugging priority (vertical) than the title label. So
what's content hugging priority?
Do you remember that we defined a vertical spacing constraint between these two labels? The
constraint said that there is no space between the title and description. On iPad, in order to
satisfy this constraint, iOS has to expand either the title or the description label. So which one
should be made larger than its intrinsic size? iOS refers to the content hugging priority. The UI
element with a lower hugging priority will be chosen. Here, the title label has a lower hugging
priority. This is why it is enlarged, while the size of the description label is kept intact.
Now change the content hugging priority (vertical) of the description label from
and see what you get.
20
252
to
250
The hugging priority of the title label is now higher than that of the description label. iOS
chooses to enlarge the description label. And, if you look at the preview of iPhone 3.5-inch, it
now displays the title label.
Okay, it looks like the title label is displayed properly on the iPad. But the description label still
doesn't look good. We want it to be placed right below the title label. Let's see how we can fix it.
The issue is related to the constraints defined in the description label. You can always view the
constraints of a particular component under Size inspector. Select the description label and go
to Size inspector. You will find a list of constraints under the Constraints section. We have
defined four spacing constraints for the label. The issue is likely related to the top and bottom
spacing constraints. In auto layout, constraints have a priority level. Constraints with higher
priority levels are satisfied before constraints with lower priority levels. By default, all
constraints are assigned with the same level (i.e. 1000). This means that they are important
and must be satisfied. Here both the top and bottom spacing constraints have the same
priority, but it appears the bottom spacing constraint wins, contributing to a large spacing
between the title and description label.
To fix the issue, the bottom spacing constraint should be set to a lower priority level. Click the
21
Edit
250
Size Classes
Remember, designing adaptive UI is a two-part process. So far we have just finished the
generic layout. The base layout is good enough to support most screen sizes. The second part of
the process is to use size classes to fine-tune the design.
A size class identifies a relative amount of display space for both vertical (height) and
horizontal (width) dimensions. There are two types of size classes in iOS 8: regular and
compact. A regular size class denotes a large amount of screen space, while a compact size
class denotes a smaller amount of screen space.
By describing each display dimension using a size class, this will result in four abstract devices:
Regular width-Regular Height, Regular width-Compact Height, Compact width-Regular
Height and Compact width-Compact Height.
The table below shows the iOS devices and their corresponding size classes.
22
To characterize a display environment, you must specify both a horizontal size class and
vertical size class. For instance, an iPad has a regular horizontal (width) size class and a
regular vertical (height) size class.
With the base layout in place, you can use size classes to provide layout specializations which
override some of the design in the base layout. For example, you can change the font size of a
label for devices that adopt compact height-regular width size. Or you can change a position of
a button particularly for the regular-regular size.
Note that all iPhones in portrait orientation have a compact width and a regular height. In
other words, your UI will appear almost identically on an iPhone 4s as it does on an iPhone 6.
The iPhone 6 Plus, in landscape orientation, has a regular width and compact height size. This
allows you to create a UI design that is completely different from that of an iPhone 4/5/6.
With some basic understanding of size classes, let's see how to fix the two design issues listed
below:
The title cannot be displayed perfectly on all iPhone models.
The description is partially displayed on all iPhone models.
We want to make the title and description labels perfect for iPhones. The current font size is
ideal for the iPad but too large when it is used on an iPhone. With size classes, you can now
adjust the font for a particular screen size. Here, we want to change the font size for all iPhone
models in portrait orientation. In terms of size classes, the iPhone in portrait defaults to
compact size class for horizontal (width) and regular size class for vertical (height).
To set the font size for this particular size class, first select the title label. Under the Attributes
inspector, you should see a plus (+) button next to the font field. Click the + button and select
Compact Width > Regular Height. You will then see a new entry for the Font option, which is
dedicated to that particular size class. Keep the size intact for the original Font option but
change the size of
wC hR
font field to
20
points.
This will instruct iOS to use the second font with a smaller font size on iPhones (portrait). For
the iPad (and iPhone landscape), the original font will still be used to display the text. Now
select the description label. Again, under the Attributes inspector, click the + button and select
Compact Width > Regular Height. Change the size of
wC hR
24
font field to
13
points. Look at
25
A simple way to fix the issue is to use a smaller font for both title and description labels. You
should know how to do that. Instead of tweaking the existing design, I will show you how to
create another design for the view to take advantage of the wider screen size. This is the true
power of size classes.
With a wider but shorter screen size, it would be best to present the image view and Product
Info View side by side; each takes up 50% of the main view. This screen shows the final view
design for iPhone landscape.
26
So far, we haven't discussed the size class control in Interface Builder. We just designed the
view using the
wAny hAny
size classes.
wAny hAny
control presents a grid for specifying width and height combinations. As we are going to
customize the layout of the iPhone in landscape, we will provide layout specializations for AnyCompact size. This size class combination represents all iPhones in landscape orientation.
27
Move your pointer to set the width class to Any, and the height class to Compact.
Once you've clicked to confirm the selected size class, the dimension of the view controller
changes accordingly. Interface Builder indicates the current size class (
wAny hCompact
) in the
layout toolbar. Any layout change you are going to make will only apply to this size class
combination. This is how Xcode allows you to design different screen layouts in a single
storyboard.
Okay, let's start to design the view.
28
The current view derives the design and layout constraints from the generic layout (
hAny
wAny
). Since we are going to redesign the view, we will first remove the layout constraints,
This would clear all layout constraints for this particular size class combination. Note that the
layout constraints still apply to the generic layout; we just removed them from Any-Compact.
You can open Document Outline and expand the Constraints section. All the constraints are
still there but grayed out for this particular size class combination. If you select any of the
constraints and go to Size inspector, you will find that the Installed option of wAny hC is
deselected; however, the Installed option of the generic layout is still there. This indicates that
you only removed the constraints for the current size class selection.
29
Next, we will design the view such that the image view and Product Info View are displayed
side by side.
First, select the image view (tshirt). Under Size inspector, change the value of X to
width to
300
and height to
400
400
, Y to
and change its size using the Size inspector. Set the value of X to
and height to
30
300
, Y to
, width to
300
Now select the title label. Under Size inspector, set the value of X to
256
and height to
70
22
, Y to
15
, width to
The font is too large, so we will add a size class dependent font for the label. In the Attributes
inspector, click the + button next to the font option. Select Any Width | Compact Height
(current) option. This adds another font option for the current size combination. Change the
font size to
lines from
25
1
points. Presently, the title label can only display one line. Change the value of
to
Select the description label. Under Size inspector, change the value of X to
to
256
, and height to
250
31
22
, Y to
85
, width
Because we have removed all the layout constraints, we must define a new set of constraints for
elements in this particular size class combination. Select the image view and click the Pin
button of the layout menu in order to define a few spacing constraints. Click the top, bottom,
and left sides. Set each of the values to
option. Click the
Add 3 Constraints
32
Constrain to margins
Next, select Product Info View and click the Pin button. Set the value of the top, bottom and
right sides to
Constraints
Constrain to margins
Add 3
button to add the constraints. In Document Outline, control-drag from the image
view (t-shirt) to the main view, and select Equal Widths when prompted.
This constraint is used to ensure the image view takes up half of the main view. Like the Equal
Heights constraint that we worked with earlier, we have to change its multiplier so that the
image view will only take up 50% of the main view.
33
Expand the Constraints section in Document Outline, and select the Equal Widths constraint.
Under Size inspector, change the multiplier from
be set to
tshirt.width
to
0.5
superview.width
item and click the Reverse First and Second item option.
Next we will define a spacing constraint between the image view and Product Info View. In the
Document Outline, control-drag from the image view (tshirt) to Product Info View. Select
Horizontal Spacing when a pop-up appears.
Okay, now that we've defined the constraints for the image view and the Product Info View,
what's left are the constraints for the labels.
Select the title label and click the Pin button. Set the top constraint to
and right constraint to
22
. Deselect the
Constrain to margins
34
15
, left constraint to
Add 3
22
Constraints
button. Control-drag from the title label to the description label, and select
Select the description label and click the Pin button. For the left and right side, set the values to
22
Constraints
Constraint to margins
Add 2
Interface Builder should detect some layout issues. In Document Outline, click the yellow
disclosure button to reveal the list of issues. Click each of the indicator icons and select
frame
35
Update
Great! Now, go to the preview assistant to check out the design in landscape orientation. It
looks pretty awesome, right?
However, there are still some minor issues for 3.5-inch and 4-inch displays, as the description
is truncated. You will need to create a size class dependent font for the description label to fix
the issue. I will leave it to you as an exercise.
Furthermore, try to run the app using the iPhone simulators. As you rotate the device, iOS
renders a smooth transition when the view is changed from portrait to landscape.
36
As you can see, the title and description have been moved to the lower-right part of the view.
Obviously, we have to customize the top spacing constraints between the title label and its
superview.
Let's see how it can be done.
The iPhone 6 Plus in landscape defaults to regular size class for horizontal and compact size
class for vertical. Click the size class control in the Interface Builder. Select Regular width |
Compact Height and confirm.
37
In Document Outline, select the Vertical Space constraint between the title label and its
superview. Under Size Inspector, you should find a + button next to the Constant field. The
value is currently set to
15
Compact size, click the + button and select Regular Width | Compact Height (current) to add
a size class dependent constant. You should then see a new field named,
field value to
230
wR hC
. Change the
In case Interface Builder detects any layout issues, just click the issue indicator in Document
38
Outline and follow the suggestions to fix the issues. Look at the preview. The view of the 5.5
inch iPhone shows the new design, while the rest of the iPhone models use the original view
design.
Summary
With iOS 9 and Xcode 7, Apple has provided developers with the tools to build adaptive
layouts. In this chapter, I have covered the concept of size classes, showing you how to use
them to create adaptive user interfaces.
Adaptive layout is one of the most important concepts introduced since iOS 8. Gone are the
days where developers had only a single device and screen size for which to design. If you are
going to build your next app, make sure you grasp the concepts of size classes and auto layout,
and make your app adapts to multiple screen sizes and orientations. The future of app design is
more than likely going to be adaptive.
For reference, you can download the Xcode project from
https://www.dropbox.com/s/j2dja7mck9rgfvd/AdaptiveUIDemo.zip?dl=0.
39
Chapter 2
Adding Sections and Index list in
UITableView
If you'd like to show a large number of records in UITableView, you'd best rethink the
approach of how to display your data. As the number of rows grows, the table view becomes
unwieldy. One way to improve the user experience is to organize the data into sections. By
grouping related data together, you offer a better way for users to access it.
Furthermore, you can implement an index list in the table view. An indexed table view is more
or less the same as the plain-styled table view. The only difference is that it includes an index
on the right side of the table view. An indexed table is very common in iOS apps. The most
40
well-known example is the built-in Contacts app on the iPhone. By offering index scrolling,
users have the ability to access a particular section of the table instantly without scrolling
through each section.
Let's see how we can add sections and an index list to a simple table app. If you have a basic
understanding of the
UITableView
index list. Basically you need to deal with these methods as defined in the
UITableViewDataSource
protocol:
to
sectionForSectionIndexTitle method returns the section index that the table view should
jump to when a user taps a particular index.
There is no better way to explain the implementation than showing you an example. As usual,
we will build a simple app, which should give you a better idea of an index list implementation.
41
42
IndexTableDemo
array:
let animals = ["Bear", "Black Swan", "Buffalo", "Camel", "Cockatoo", "Dog",
"Donkey", "Emu", "Giraffe", "Greater Rhea", "Hippopotamus", "Horse", "Koala",
"Lion", "Llama", "Manatus", "Meerkat", "Panda", "Peacock", "Pig", "Platypus",
"Polar Bear", "Rhinoceros", "Seagull", "Tasmania Devil", "Whale", "Whale
Shark", "Wombat"]
Well, we're going to organize the data into sections based on the first letter of the animal name.
There are a lot of ways to do that. One way is to manually replace the animals array with a
dictionary like I've shown below:
let animals: [String: [String]] = ["B" : ["Bear", "Black Swan", "Buffalo"],
"C" : ["Camel", "Cockatoo"],
"D" : ["Dog", "Donkey"],
"E" : ["Emu"],
"G" : ["Giraffe", "Greater Rhea"],
"H" : ["Hippopotamus", "Horse"],
43
"K"
"L"
"M"
"P"
"R"
"S"
"T"
"W"
:
:
:
:
:
:
:
:
["Koala"],
["Lion", "Llama"],
["Manatus", "Meerkat"],
["Panda", "Peacock", "Pig", "Platypus", "Polar Bear"],
["Rhinoceros"],
["Seagull"],
["Tasmania Devil"],
["Whale", "Whale Shark", "Wombat"]]
In the above code, we've turned the animals array into a dictionary. The first letter of the
animal name is used as a key. The value that is associated with the corresponding key is an
array of animal names.
We could manually create the dictionary, but wouldn't it be great if we could create the indexes
from the
the
animals
array? Let's see how it can be done. First, declare two instance variables in
AnimalTableViewController
class:
We initialize an empty dictionary for storing the animals and an empty array for storing the
section titles of the table. The section title is the first letter of the animal name (e.g. B).
Because we want to generate a dictionary from the
animals
AnimalTableViewController
func createAnimalDict() {
for animal in animals {
// Get the first letter of the animal name and build the dictionary
let animalKey =
animal.substringToIndex(animal.startIndex.advancedBy(1))
if var animalValues = animalsDict[animalKey] {
animalValues.append(animal)
animalsDict[animalKey] = animalValues
} else {
animalsDict[animalKey] = [animal]
}
}
// Get the section titles from the dictionary's keys and sort them in
ascending order
animalSectionTitles = [String](animalsDict.keys)
animalSectionTitles = animalSectionTitles.sort({ $0 < $1 })
}
44
class:
animals
subStringToIndex
method of a string
can return a new string containing the characters up to a given index. The index should be of
the type
String.Index
. To obtain an index for a specific position, you have to ask the string
startIndex
advance()
characters between the beginning of the string and the target position. In this case, the target
position is
As mentioned before, the first letter of the animal's name is used as a key of the dictionary. The
value of the dictionary is an array of animals of that particular key. So once we got the key, we
either create a new array of animals or append the item to an existing array. Here we show the
values of
animalsDict
Iteration #1:
animalsDict["B"] = ["Bear"]
Iteration #2:
Iteration #3:
Iteration #4:
animalsDict["C"] = ["Camel"]
After
animalsDict
is completely generated, we can retrieve the section titles from the keys of
the dictionary.
To retrieve the keys of a dictionary, you can simply call the
keys
sort
, which
returns a sorted array of values of a known type, based on the output of a sorting closure you
provide.
The closure takes two arguments of the same type (in this example, it's the string) and returns
a
Bool
value to state whether the first value should appear before or after the second value
once the values are sorted. If the first value should appear before the second value, it should
return true.
One way to write the sort closure is like this:
animalSectionTitles = animalSectionTitles.sort( { (s1:String, s2:String) ->
Bool in
return s1 < s2
45
})
You should be very familiar with the closure expression syntax. In the body of the closure, we
compare the two string values. It returns
true
and that of
s1
is
is
closure returns true, indicating that B should appear before E. In this case, we can sort the
values in alphabetical order.
If you read the earlier code snippet carefully, you may wonder why I wrote the
sort
closure
like this:
animalSectionTitles = animalSectionTitles.sort({ $0 < $1 })
$0
and
$1
second String arguments. If you use shorthand argument names, you can omit nearly
everything of the closure including argument list and in keyword; you will just need to write
the body of the closure.
In Swift 2, Apple introduces another sort function called
very similar to the
sort
sortInPlace
sortInPlace
function applies the sorting on the original array. You can replace the line of code with the one
below:
animalSectionTitles.sortInPlace({ $0 < $1 })
viewDidLoad
numberOfSectionsInTableView
sections:
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
// Return the number of sections.
46
return animalSectionTitles.count
}
titleForHeaderInSection
method. This method is called every time a new section is displayed. Based on the given section
index, we simply return the corresponding section title.
override func tableView(tableView: UITableView, titleForHeaderInSection
section: Int) -> String? {
return animalSectionTitles[section]
}
It's very straightforward, right? Next, we have to tell the table view the number of rows in a
particular section. Update the
numberOfRowsInSection
AnimalTableViewController.swift
method in
like this:
When the app starts to render the data in the table view, the
numberOfRowsInSection
method is
called every time a new section is displayed. Based on the section index, we can get the section
title and use it as a key to retrieve the animal names of that section, followed by returning the
total number of animal names for that section.
Lastly, modify the
cellForRowAtIndexPath
method as follows:
47
return cell
}
The
indexPath
argument contains the current row number, as well as, the current section
index. So, based on the section index, we retrieve the section title (e.g. "B") and use it as the
key to retrieve the animal names for that section. The rest of the code is very straightforward.
We simply get the animal name and set it as the cell label. The
imageFilename
variable is
computed by converting the animal name to lowercase letters, followed by replacing all
occurrences of a space with an underscore.
Okay, you're ready to go! Hit the Run button and you should end up with an app with sections
but without the index list.
That's it! Compile and run the app again. You should find the index on the right side of the
table. Interestingly, you do not need any implementation and the indexing already works! Try
to tap any of the indexes and you'll be brought to a particular section of the table.
48
sectionForSectionIndexTitle
animals
animalIndexTitles
in
let animalIndexTitles = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K",
"L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"]
sectionIndexTitlesForTableView
animalSectionTitles
animalIndexTitles
array.
49
[AnyObject]! {
return animalIndexTitles
}
Now, compile and run the app again. Cool! The app displays the index from A to Z.
But wait a minute It doesn't work properly! If you try tapping the index "C," the app jumps to
the "D" section. And if you tap the index "G," it directs you to the "K" section. Below shows the
mapping between the old and new indexes.
Well, as you may notice, the number of indexes is greater than the number of sections, and the
UITableView
object doesn't know how to handle the indexing. It's your responsibility to
implement the
sectionForSectionIndexTitle
section number when a particular index is tapped. Add the following new method:
override func tableView(tableView: UITableView, sectionForSectionIndexTitle
title: String, atIndex index: Int) -> Int {
guard let index = animalSectionTitles.indexOf(title) else {
return -1
}
return index
}
Based on the selected index name (i.e. title), we locate the correct section index of
animalSectionTitles
-1
indexOf
the item. Compile and run the app again. The index list should now work!
Note: The old find function is not supported any more with Swift 2.0!
UITableView
UITableViewDelegate
changes:
Alter the height of the section header
Change the font of the section header
To alter the height of the section header, you can simply override the
heightForHeaderInSection
willDisplayHeaderView
The method includes an argument named view. This view object can be a custom header view
or a standard one. In our demo, we just use the standard header view, which is the
UITableViewHeaderFooterView
object. Once you have the header view, you can alter the text
Run the app again. The header view should be updated with your preferred font and color.
Summary
When you need to display a large number of records, it is simple and effective to organize the
data into sections and provide an index list for easy access. In this chapter, we've walked you
through the implementation of an indexed table. By now, I believe you should know how to
add sections and an index list to your table view.
For your reference, you can download the complete Xcode project from
https://www.dropbox.com/s/kpd0q1m5ccsaup7/IndexedTableDemo.zip?dl=0.
51
Chapter 3
Animating Table View Cells
When you read this chapter, I assume you already knew how to use
UITableView
to present
data. If not, go back and read the Beginning iOS 9 Programming with Swift book.
The
UITableView
one of the most commonly used components in iOS apps. Whether you're building a
productivity app, to-do app, or social app, you would make use of table views in one form or
another. The default implementation of
UITableView
apps. To differentiate one's app from the rest, you usually provide customizations for the table
views and table cells in order to make the app stand out. In this chapter, we'll show you a
powerful technique to liven up your app by adding subtle animation.
52
The Google+ app is a great example of table view animation. If you've used the app, every table
row (or card) is animated as you scroll through the table. The card seems to slide in from the
side when it first appears. This subtle animation would greatly enhance the user experience of
your app.
It is very easy to animate a table view cell. Again, to demonstrate how the animation is done,
we'll tweak an existing table-based app and add a subtle animation.
To start with, first download the project template from
https://www.dropbox.com/s/qnxqfwppra62mg8/TableCellAnimationTemplate.zip?dl=0.
After downloading, compile the app and make sure you can run it properly. It's just a very
simple app displaying a list of articles.
subtle animation when the table row appears? If you look into the documentation of the
UITableViewDelegate
tableView(_:willDisplayCell:forRowAtIndexPath:):
optional func tableView(_ tableView: UITableView, willDisplayCell cell:
UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath)
The method will be called right before a row is drawn. By implementating the method, you can
customize the cell object and add your own animation before the cell is displayed. Here is what
you need to create the fade-in effect. Insert the code snippet in
ArticleTableViewController.swift
Core Animation provides iOS developers with an easy way to create animation. All you need to
do is define the initial and final state of the visual element. Core Animation will then figure out
the required animation between these two states.
In the above code, we first set the initial alpha value of the cell to
transparency. Then we begin the animation; set the duration to 1 second and define the final
state of the cell, which is completely opaque. This will automatically create a fade-in effect
when the table cell appears.
You can now compile and run the app. Scroll through the table view and enjoy the fade-in
animation.
tableView(_:willDisplayCell:forRowAtIndexPath:)
cell animation. You can implement whichever type of animation in the method. The fade-in
54
animation is very simple. Now let's try to implement another animation using
CATransform3D
Same as before, we define the initial and final state of the transformation. The general idea is
that we first rotate the cell by 90 degrees clockwise and then bring it back to the normal
orientation which is the final state.
Okay, but how can we rotate a table cell by 90 degrees clockwise?
The key is to use the
CATransform3DMakeRotation
, while leaving
layer.
Next, we start the animation with the duration of 1 second. The final state of the cell is set to
CATransform3DIdentity
Quick Tip: You may wonder what CATransform3D is. It is actually a structure
representing a matrix. Performing transformation in 3D space such as rotation,
involves some matrices calculation. Ill not go into the details of matrices
calculation. If you want to learn more, you can check out
http://www.matrix44.net/cms/notes/opengl-3d-graphics/basic-3d-math-matrices.
tableView(_:willDisplayCell:forRowAtIndexPath:)
rotationTransform
The line of code simply translates or shifts the position of the cell. It indicates the cell is shifted
to the left (negative value) by
500
100
points. There is no
56
Your Exercise
For now, the cell animation is shown every time you scroll through the table, whether you're
scrolling down or up the table view. Though the animation is nice, your user will find it
annoying if the animation is displayed too frequently. You may want to display the animation
only when the cell first appears. Try to modify the existing project and add that restriction.
Summary
In this chapter, I just showed you the basics of table cell animation. Try to change the values of
the transform and see what effects you get.
For reference, you can download the complete Xcode project from
https://www.dropbox.com/s/98s9wiwe38l81sc/TableCellAnimation.zip?dl=0. The solution of
the exercise is included in the project.
57
Chapter 4
Working with JSON
First, what's JSON? JSON (short for JavaScript Object Notation) is a text-based, lightweight,
and easy way for storing and exchanging data. It's commonly used for representing structural
data and data interchange in client-server applications, serving as an alternative to XML. A lot
of the web services we use everyday have JSON-based APIs. Most of the iOS apps, including
Twitter, Facebook, and Flickr send data to their backend web services in JSON format. As an
example, here is a JSON representation of a sample Movie object:
{
"title": "The Amazing Spider-man",
"release_date": "03/07/2012",
"director": "Marc Webb",
58
"cast": [
{
"name": "Andrew Garfield",
"character": "Peter Parker"
},
{
"name": "Emma Stone",
"character": "Gwen Stacy"
},
{
"name": "Rhys Ifans",
"character": "Dr. Curt Connors"
}
]
}
As you can see, JSON formatted data is more human-readable and easier to parse than XML.
I'll not go into the details of JSON. This is not the purpose of this chapter. If you want to learn
more about the technology, I recommend you tocheck out the JSON Guide at
http://www.json.org/.
Since the release of iOS 5, the iOS SDK has already made it easy for developers to fetch and
parse JSON data. It comes with a handy class called
NSJSONSerialization
, which can
automatically convert JSON formatted data to objects. Later in this chapter, I will show you
how to use the API to parse some sample JSON formatted data, returned by a web service.
Once you understand how it works, it is fairly easy to build an app by integrating with other
free/paid web services.
Demo App
As usual, we'll create a demo app. Let's call it KivaLoan. The reason why we name the app
KivaLoan is that we will utilize a JSON-based API provided by Kiva.org. If you haven't heard
of Kiva, it is a non-profit organization with a mission to connect people through lending to
alleviate poverty. It lets individuals lend as little as $25 to help create opportunities around the
world. Kiva provides free web-based APIs for developers to access their data. For our demo
app, we'll call up the following Kiva API to retrieve the most recent fundraising loans and
display them in a table view:
https://api.kivaws.org/v1/loans/newest.json
59
Quick note: Starting from iOS 9, Apple introduced a new feature called App
Transport Security with the aim to improve the security of connections between
an app and web services. By default, all outgoing connections should ride on
HTTPS. Otherwise, your app will not be allowed to connect to the web service.
The returned data of the above API is in JSON format. Here is a sample result:
loans: (
{
activity = Retail;
"basket_amount" = 0;
"bonus_credit_eligibility" = 0;
"borrower_count" = 1;
description =
{
languages =
(
fr,
en
);
};
"funded_amount" = 0;
id = 734117;
image =
{
id = 1641389;
"template_id" = 1;
};
"lender_count" = 0;
"loan_amount" = 750;
location =
{
country = Senegal;
"country_code" = SN;
geo =
{
level = country;
pairs = "14 -14";
type = point;
};
};
name = "Mar\U00e8me";
"partner_id" = 108;
"planned_expiration_date" = "2014-08-05T09:20:02Z";
"posted_date" = "2014-07-06T09:20:02Z";
sector = Retail;
status = fundraising;
use = "to buy fabric to resell";
},
....
....
)
60
NSJSONSerialization
into objects. It's unbelievably simple. You'll see what I mean in a while.
To keep you focused on learning the JSON implementation, you can first download the project
template from https://www.dropbox.com/s/qud9s0zfcya3ji2/KivaLoanTemplate.zip?dl=0. I
have already created the skeleton of the app for you. It is a simple table-based app that displays
a list of loans provided by Kiva.org. The project template includes a pre-built storyboard and
custom classes for the table view controller and prototype cell. If you run the template, it
should result in an empty table app.
Loan
class
represents the loan information in the KivaLoan app and is used to store the loan information
returned by Kiva.org. To keep things simple, we won't use all the returned data of a loan.
Instead, the app will just display the following fields of a loan:
Name of the loan applicant
61
name = "Mar\U00e8me";
{
country = Senegal;
"country_code" = SN;
geo =
{
level = country;
pairs = "14 -14";
type = point;
};
};
Amount
"loan_amount" = 750;
These fields are good enough for filling up the labels in the table view. Now create a new class
file using the Swift File template. Name it
Loan.swift
Loan
class Loan {
var
var
var
var
name:String = ""
country:String = ""
use:String = ""
amount:Int = 0
JSON supports a few basic data types including number, String, Boolean, Array, and Objects
(an associated array with key and value pairs).
For the loan fields, the loan amount is stored as a numeric value in the JSON-formatted data.
This is why we declared the
are declared with the type
amount
String
62
Int
Okay, let's see how we can call up the Kiva API and parse the returned data. First, open
KivaLoanTableViewController.swift
We just defined the URL of the Kiva API, and declare the
loans
Loan objects. Next, insert the following methods in the same file:
func getLatestLoans() {
let request = NSURLRequest(URL: NSURL(string: kivaLoadURL)!)
let urlSession = NSURLSession.sharedSession()
let task = urlSession.dataTaskWithRequest(request, completionHandler: {
(data, response, error) -> Void in
if let error = error {
print(error)
return
}
// Parse JSON data
if let data = data {
self.loans = self.parseJsonData(data)
// Reload table view
NSOperationQueue.mainQueue().addOperationWithBlock({ () -> Void in
self.tableView.reloadData()
})
}
})
task.resume()
}
func parseJsonData(data: NSData) -> [Loan] {
63
These two methods form the core part of the app. Both methods work collaboratively to call the
Kiva API, retrieve the latest loans in JSON format and translate the JSON-formatted data into
an array of
In the
The
Loan
getLatestLoans
NSURLSession
NSURLSession
NSURLSession
NSURLConnection
, which has
NSURLSession
is session
tasks, which handle the loading of data, as well as uploading and downloading files and data
fetching from servers (e.g. JSON data fetching). With sessions, you can schedule three types of
tasks: data tasks (
NSURLSessionDataTask
NSURLSessionDownloadTask
NSURLSessionUploadTask
) for uploading a file from disk. Here we use the data task to retrieve
contents from Kiva.org. To add a data task to the session, we call the
dataTaskWithURL
method
with the specified URL of Kiva. To initiate the data task, you call the resume method (i.e.
task.resume()
NSURLSession
request completes, it returns the data by calling your completion handler closure.
In the completion handler, immediately after the data is returned, we check for an error and
invoke the
parseJsonData
method called
Loan
parseJsonData
NSJSONSerialization
class, which is
JSONObjectWithData
loans
. How do you
know what key to use? You can either refer to the API documentation or test the JSON data
using a JSON browser (e.g. http://jsonviewer.stack.hu). If you've loaded the Kiva API into the
JSON browser, here is an excerpt of the result:
{
"paging": {
"page": 1,
"total": 5297,
"page_size": 20,
"pages": 265
},
"loans": [
{
"id": 794429,
"name": "Joel",
"description": {
"languages": [
"es",
"en"
]
},
"status": "fundraising",
"funded_amount": 0,
"basket_amount": 0,
"image": {
"id": 1729143,
"template_id": 1
},
"activity": "Home Appliances",
"sector": "Personal Use",
65
66
},
"partner_id": 436,
"posted_date": "2014-11-20T08:50:02Z",
"planned_expiration_date": "2015-01-04T08:50:02Z",
"loan_amount": 800,
"borrower_count": 1,
"lender_count": 0,
"bonus_credit_eligibility": false,
"tags": [
]
},
...
NSJSONSerialization
paging
and
loans
jsonResult
as a Dictionary with the top-level items as keys. This is why we can use the key
) is returned
loans
to access
the array of loans. Here is the line of code for your reference:
let jsonLoans = jsonResult?["loans"] as! [AnyObject]
With the array of loans (i.e. jsonLoans) returned, we loop through the array. Each of the array
items (i.e. jsonLoan) is converted into a dictionary. In the loop, we extract the loan data from
each of the dictionaries and save them in a
Loan
(highlighted in yellow) by studying the JSON result. The value of a particular result is stored as
AnyObject
AnyObject
Array, Dictionary or null. This is why you have to downcast the value to a specific type such as
String
and
Int
loan
loans
After the JSON data is parsed and the array of loans is returned, we call the
67
reloadData
method to reload the table. You may wonder why we need to call
NSOperationQueue.mainQueue().addOperationWithBlock
thread. The block of code in the completion handler of the data task is executed in a
background thread. If you just call the
reloadData
reload will not happen immediately. To ensure a responsive GUI update, this operation should
be performed in the main thread. This is why we call the
NSOperationQueue.mainQueue().addOperationWithBlock
reloadData
KivaLoanTableViewController.swift
68
cell.amountLabel.text = "$\(loans[indexPath.row].amount)"
return cell
}
The above code is pretty straightforward if you are familiar with the implementation of
UITableView
. In the
cellForRowAtIndexPath:
the loans array and populate them in the custom table cell. One thing to take note of is the code
below:
"$\(loans[indexPath.row].amount)"
Sometimes you may want create a string by adding both string (e.g. $) and integer (e.g.
loans[indexPath.row].amount
strings, known as string interpolation. You can make use of it by using the above syntax.
Lastly, insert the following line of code in the
viewDidLoad
data:
getLatestLoans()
69
70
Chapter 5
How to Integrate Twitter and Facebook
Sharing
With the advent of social networks, you may want to provide social network sharing in your
apps. This is one of the many ways to increase user engagement. In the past, developers have
had to make use of the Facebook and Twitter API (or other social networks) in order to
implement the sharing feature.
Since iOS 6, Apple introduced a new framework known as Social Framework. The Social
framework lets you integrate your app with any supported social networking services.
Currently, it supports Facebook, Twitter, Sina Weibo, and Tencent Weibo. The framework
gives you a standard composer to create posts for different social networks, and shields you
71
from learning the APIs of the social networks. You don't even need to know how to initiate a
network request or handle single sign-on. The Social Framework simplifies everything. You
just need to write a few lines of code to bring up the composer for users to tweet / publish
Facebook posts within your app.
The framework comes with a very handy class called
MFMailComposeViewController
, the
SLComposeViewController
SLComposeViewController
. Similar to
controller for users to compose tweets or Facebook posts. It also allows developers to preset
the initial text, attach images, and add a URL to the post. If you just want to implement a
simple sharing feature, this is the only class you need to know.
If you're not familiar with
SLComposeViewController
72
The Facebook and Twitter sharing features are not yet implemented in the template. These are
what we're going to work on.
editActionsForRowAtIndexPath
method. You should find the code snippet shown below that instantiates the
instances of Twitter and Facebook actions. For both
set to
nil
UIAlertAction
twitterAction
73
UIAlertAction
Because the
SLComposeViewController
to do is import the
the
Social
RestaurantTableViewController
class:
import Social
twitterAction
74
Before testing the app, let's go through the above code line by line. First, we have a guard
statement to validate if the device is capable of sending tweets. We use the
isAvailableForServiceType
SLServiceTypeTwitter
) is available. One reason why users can't access the Twitter service is
because they haven't signed into their Twitter accounts in Settings. If the Twitter service is
unavailable, we simply prompt an error message and instruct the user to sign on the account in
iOS.
If the service is accessible, we then create an instance of the
SLComposeViewController
of the
Twitter service, followed by setting the initial text and image in the composer. Lastly, we
invoke the
presentViewController
That's the code we need to let users tweet within your app. It's much easier than you thought,
right? Cool! It's ready to go. Hit the Run button to compile and execute the app. Swipe a
restaurant record and tap the Share button. Once you select Twitter, the app shows you a
Tweet composer, populated with the restaurant name and image.
Quick tip: You must sign in with your Twitter account before you can test the
sharing feature. Go to Settings > Twitter and sign in.
75
method of
RestaurantTableViewController.swift
, replace
76
return
}
// Display Tweet Composer
let facebookComposer = SLComposeViewController(forServiceType:
SLServiceTypeFacebook)
facebookComposer.setInitialText(self.restaurantNames[indexPath.row])
facebookComposer.addImage(UIImage(named:
self.restaurantImages[indexPath.row]))
facebookComposer.addURL(NSURL(string: "http://www.appcoda.com")!)
self.presentViewController(facebookComposer, animated: true, completion:
nil)
})
That's it. The code is very similar to the code we've used in
the service type. Instead of using
use
SLServiceTypeFacebook
SLServiceTypeTwitter
twitterAction
, we tell
SLComposeViewController
to
addURL
method. Like the initial text and image, the URL is optional.
Let's run the app again and click the Facebook button. Your app should bring up the composer
for publishing a Facebook post.
77
Summary
As you can see from this chapter, it's pretty easy to add Twitter and Facebook features using
the Social Framework. If you're building your app, there is no reason why you shouldn't
incorporate these social features.
In this chapter, I introduced the basics of Facebook and Twitter integration. You can try to
tweak the sample app and upload multiple images to the social networks. However, if you want
to access more advanced features such as displaying a user's Facebook friends, you'll need to
make use of the Facebook API.
For reference, you can download the complete Xcode project from
https://www.dropbox.com/s/sky3t2b6kuajwyu/SocialDemo.zip?dl=0.
78
Chapter 6
Working with Email and Attachments
The
MessageUI
framework has made it really easy to send an email from your apps. You can
easily use the built-in APIs to integrate an email composer in your app. In this short chapter,
we'll show you how to send email and work with email attachments by creating a simple app.
Since the primary focus is to demonstrate the email feature of the
MessageUI
framework, we
will keep the demo app very simple. The app simply displays a list of files in a plain table view.
We'll populate the table with various types of files, including images in both PNG and JPEG
formats, a Microsoft Word document, a Powerpoint file, a PDF document, and an HTML file.
Whenever users tap on any of the files, the app automatically creates an email with the selected
file as an attachment.
79
80
MessageUI
MFMailComposeViewControllerDelegate
in the
AttachmentTableViewController.swift
file. Your
MIME stands for Multipurpose Internet Mail Extensions. In short, MIME is an Internet
standard that defines the way to send other kinds of information (e.g. graphic) through email.
The MIME type indicates the type of data to attach. For instance, the MIME type of a PNG
image is image/png. You can refer to the full list of MIME types at
http://www.iana.org/assignments/media-types/.
Enumerations are particularly useful for storing a group of related values. So we use an
enumeration to store the possible MIME types of the attachments. In Swift, you declare an
81
enum
case
enumeration cases. Optionally, you can assign a raw value for each case. In the above code, we
define the possible types of the files and assign each case with the corresponding MIME type.
In Swift, you define initializers in enumerations to provide an initial case value. In the above
initialization, we take in a file type/extension and look up for the corresponding case. Later we
will use this enumeration when creating the
MFMailComposeViewController
object.
Next, create the methods for displaying the mail composer. Insert the following code in the
same file:
func showEmail(attachmentFile: String) {
// Check if the device is capable to send email
guard MFMailComposeViewController.canSendMail() else {
return
}
let emailTitle = "Great Photo and Doc"
let messageBody = "Hey, check this out!"
let toRecipients = ["[email protected]"]
// Initialize the mail composer and populate the mail content
let mailComposer = MFMailComposeViewController()
mailComposer.mailComposeDelegate = self
mailComposer.setSubject(emailTitle)
mailComposer.setMessageBody(messageBody, isHTML: false)
mailComposer.setToRecipients(toRecipients)
// Determine the file name and extension
let fileparts = attachmentFile.componentsSeparatedByString(".")
let filename = fileparts[0]
let fileExtension = fileparts[1]
// Get the resource path and read the file using NSData
guard let filePath = NSBundle.mainBundle().pathForResource(filename,
ofType: fileExtension) else {
return
}
// Get the file data and MIME type
if let fileData = NSData(contentsOfFile: filePath),
mimeType = MIMEType(type: fileExtension) {
// Add attachment
82
The
showEmail
MFMailComposeViewController.canSendMail()
MFMailComposeViewController
object
and populate it with some initial values including the email subject, message content, and the
recipient email. The
MFMailComposeViewController
for managing the editing and sending of an email message. Later when it is presented, you will
see the predefined values in the mail message.
To add an attachment, all you need to do is call up the
MFMailComposeViewController
addAttachmentData
method of the
class.
NSData
the MIME type the MIME type of the attachment (e.g. image/png).
the file name that's the preferred file name to associate with the attachment.
The rest of the code in the
showEmail
parameters.
We first determine the path of the given file by using the
NSBundle
. In iOS, an
NSBundle
pathForResource
method of
group. In general, the main bundle corresponds to the directory where the current application
executable is located. Since our resource files are embedded in the app, we retrieve the main
bundle object. We then call the
pathForResource
attachment.
The last block of the code is the core part of the method.
// Get the file data and MIME type
if let fileData = NSData(contentsOfFile: filePath),
mimeType = MIMEType(type: fileExtension) {
// Add attachment
mailComposer.addAttachmentData(fileData, mimeType: mimeType.rawValue,
fileName: filename)
// Present mail view controller on screen
presentViewController(mailComposer, animated: true, completion: nil)
}
NSData
the MIME type of the given file in reference to its file extension. Both initializations return an
optional. In Swift 1.2 or later, you are allowed to combine multiple
one. Multiple optional bindings are separated by commas.
Before Swift 1.2:
84
if let
statements into
Once we successfully initialized the file data and MIME type, we create the
addAttachmentData
method to attach the file and then present the mail composer.
You may be familiar with the implementation of the
mailComposeController(_:didFinishWithResult:_)
MFMailComposeViewControllerDelegate
For demo purposes, we just log the mail result and dismiss the mail controller. In real world
apps, you can display an alert message if the mail fails to send.
We're almost ready to test the app. The app will bring up the mail interface when any of the
files are selected. So the last thing is to add the
didSelectRowAtIndexPath:
method:
You're good to go. Compile and run the app. Tap a file and the app should display the mail
interface with your selected attachment. Note that there is a bug in the iOS simulator. You may
not be able to bring up the mail composer. Try to run the app on a real iOS device, it should
work.
85
For your complete reference, you can download the full source from
https://www.dropbox.com/s/ymnpeqmk3wz73qu/EmailAttachment.zip?dl=0.
86
Chapter 7
Sending SMS and MMS Using
MessageUI Framework
MessageUI
controller for developers to present a standard interface for composing SMS text messages
within apps. While you can use the
MFMailComposeViewController
MFMessageComposeViewController
for handling
text messages.
Basically the usage of
MFMessageComposeViewController
class. If you've read the previous chapter about creating emails with attachments, you will find
it pretty easy to compose text messages. Anyway, I'll walk you through the usage of
87
MFMessageComposeViewController
the class.
Getting Started
To save you time from creating the Xcode project from scratch, you can download the project
template from https://www.dropbox.com/s/k48pgf9vmzf0x2o/SMSDemoTemplate.zip?dl=0
to begin with. I have pre-built the storyboard and already loaded the table data for you.
88
AttachmentTableViewController.swift
MFMessageComposeViewControllerDelegate
protocol:
import MessageUI
class AttachmentTableViewController: UITableViewController,
MFMessageComposeViewControllerDelegate
The
MFMessageComposeViewControllerDelegate
called when a user finishes composing the message. We have to provide the implementation of
the method to handle various situations:
A user cancels the editing of an SMS
A user taps the send button and the SMS is sent successfully
A user taps the send button, but the SMS has failed to send
Insert the following code in the
AttachmentTableViewController
class:
89
Here, we just display an alert message when the app fails to send a message. For other cases,
we log the error to the console and dismiss the message composer.
didSelectRowAtIndexPath
method in the
class:
The
sendSMS
method is the core method to initialize and populate the default content of the
SMS text message. Create the method using the following code:
func sendSMS(attachment:String) {
// Check if the device is capable of sending text message
guard MFMessageComposeViewController.canSendText() else {
let alertMessage = UIAlertController(title: "SMS Unavailable", message:
"Your device is not capable of sending SMS.", preferredStyle: .Alert)
alertMessage.addAction(UIAlertAction(title: "OK", style: .Default,
handler: nil))
presentViewController(alertMessage, animated: true, completion: nil)
return
}
// Prefill the SMS
let messageController = MFMessageComposeViewController()
messageController.messageComposeDelegate = self
messageController.recipients = ["12345678", "72345524"]
messageController.body = "Just sent the \(attachment) to your email. Please
check!"
// Present message view controller on screen
presentViewController(messageController, animated: true, completion: nil)
}
Though most of the iOS devices should be capable of sending a text message, you should be
prepared for the exception. What if your app is used on an iPod touch with iMessages
90
disabled? In this case, the device is not allowed to send a text message. So at the very
beginning of the code, we verify whether or not the device is allowed to send text messages by
using the
canSendText
method of
MFMessageComposeViewController
The rest of the code is very straightforward and similar to what we did in the previous chapter.
We pre-populate multiple recipients (i.e. phone number) in the text message and set the
message body.
With the content ready, simply invoke
presentViewController
composer. That's it! Simple and easy. You can now run the app and test it out. But please note
you have to test the app on a real iOS device. If you use the simulator to run the app, it shows
you an alert message.
Sending MMS
Wait! The app can only send a text message. How about a file attachment? The
91
MFMessageComposeViewController
sendSMS
selected file and retrieve the actual file path using the
Lastly, we add the file using the
addAttachmentURL
pathForResource
method of
method.
Quick note: I have explained the code snippet in details before. Please refer
to the previous chapter for details.
92
NSBundle
In iOS, you're allowed to communicate with other apps using a URL scheme. The mobile OS
already comes with built-in support of the http, mailto, tel, and sms URL schemes. When you
open an HTTP URL, iOS by default launches the URL via Safari. If you want to open the
Messages app, you can use the SMS URL scheme and specify the recipient. However, that URL
scheme doesn't allow you to insert default content.
Wrap Up
In this chapter, I showed you a simple way to send a text message within an app. For reference,
you can download the full source code from
https://www.dropbox.com/s/eprt7qjkpxy4dti/SMSDemo.zip?dl=0.
93
Chapter 8
How to Get Direction and Draw Route
on Maps
MapKit
MKDirections
API
which allows iOS developers to access the route-based directions data from Apple's server.
Typically you create an
MKDirections
instance then automatically contacts Apple's server and retrieves the route-based data.
You can use the
MKDirections
MKDirections
top of all that, the API lets you calculate the travel time of a route.
94
MKDirections
API.
MapKit
MapKit
pin a location on a map. To demonstrate the usage of the MKDirections API, we'll build a
sample map app. You can start with this project template
(https://www.dropbox.com/s/da8ag58xcjqdfep/MapKitDirectionDemoTemplate.zip?dl=0). If
you build the template, you should have an app that shows a list of restaurants. By tapping a
restaurant, the app brings you to the map view with the location of the restaurant annotated on
the map. That's pretty much the same as what you have implemented in the FoodPin app. We'll
enhance the demo app to get the user's current location, and display the directions to the
selected restaurant.
There is one thing I want to point out. If you look into the
95
MapViewController
The iOS 9 SDK provides a new API for customizing the pin color. The
class now lets you change the pin color using the
pinTintColor
MKPinAnnotationView
your app is going to support both iOS 8 and 9, you will need to check the OS version before
changing the pin color. Otherwise, this will cause errors when the app runs on older versions of
iOS.
Starting from Swift 2, it has built-in support for checking API availability. You can easily define
an availability condition so that the block of code will only be executed on certain iOS versions.
You use the
#available
keyword in a
if
the OS versions (e.g. iOS 9, OSX 10.10) you want to verify. The asterisk (*) is required and
indicates that the
if
versions of OS. In the above example, we will execute the code block only if the device is
running on iOS 9 (or up).
Main.storyboard
. Let's add a
Direction
button to the
navigation bar of the map view controller. Drag a Bar Button Item from the Object library and
add it to the navigation bar. Set the title of the button to Direction. When this button is tapped,
the app will get the user's current location and display the directions to the restaurant.
Quick note: Do you notice that you can now place two bar button items next to
each other using Interface Builder? It is a new feature in Xcode 7. Prior to
that, you cannot do that without writing your own code.
96
showDirection
in the
MapViewController
class.
Direction
Direction
97
class doesn't display the user's location on the map. You can set the
showsUserLocation
property of the
MKMapView
class to
true
set to true, the map view uses the built-in Core Location framework to search for the current
location and display it on the map.
In the
viewDidLoad
method of the
MapViewController
mapView.showsUserLocation = true
If you can't wait to test the app and see how it displays the user location, you can compile and
run the app. Select any of the restaurants to bring up the map. Unfortunately, it doesn't work
as expected. If you look into the console, you should find the following error:
MapKitDirectionDemo[2337:222562] Trying to start MapKit location updates
without prompting for location authorization. Must call -[CLLocationManager
requestWhenInUseAuthorization] or -[CLLocationManager
requestAlwaysAuthorization] first.
Starting from iOS 8, Core Location introduces a new feature known as Location Authorization.
You have to explicitly ask for a user's permission to grant your app location services. Basically,
you need to implement these two things to get the location working:
Request a user's authorization by calling the
requestAlwaysAuthorization
Add a key (
method of
requestWhenInUseAuthorization
CLLocationManager
NSLocationWhenInUseUsageDescription
or
NSLocationAlwaysUsageDescription
) to
your Info.plist.
There are two types of authorization:
requestAlwaysAuthorization
requestWhenInUseAuthorization
and
. You use the former if your app only needs location updates when
it's in use. The latter is designed for apps that use location services in the background
(suspended or terminated). For example, a social app that tracks a user's location requires
location updates even if it's not running in the foreground. Obviously,
requestWhenInUseAuthorization
Info.plist
NSLocationWhenInUseUsageDescription
NSLocationAlwaysUsageDescription
key to
Info.plist
or
user why your app needs location services. For example, you can enter a string like Location is
required to find out your current location.
Now we are ready to modify the code again. First, declare a location manager variable in the
MapViewController
class:
viewDidLoad
super.viewDidLoad()
requestWhenInUseAuthorization
the current authorization status. If the user has not yet been asked to authorize location
updates, it automatically prompts the user to authorize the use of location services.
Once the user makes a choice, we check the authorization status to see if the user granted
permission. If yes, we enable
showsUserLocation
99
in the app.
Now run the app again and have a quick test. When you launch the map view, you'll be
prompted to authorize location services. As you can see, the message shown is the one we
specified in the
NSLocationWhenInUseUsageDescription
up the scheme editor. Select the Options tab and set the default location.
Once you set the location, the simulator will display a blue dot in the map which indicates the
current user location. If you can't find the blue dot on the map, simply zoom out. In the
simulator, you can hold down the option key to simulate the pinch-in and pinch-out gestures.
For details, you can refer to Apple's official document.
placemark
variable in the
MapViewController
class:
var currentPlacemark:CLPlacemark?
This variable is used to save the current placemark. In other words, it is the
of the selected restaurant. In the
viewDidLoad
placemark
101
object
showDirection
MKDirections
route data. Update the method by using the following code snippet:
@IBAction func showDirection(sender: AnyObject) {
guard let currentPlacemark = currentPlacemark else {
return
}
let directionRequest = MKDirectionsRequest()
// Set the source and destination of the route
directionRequest.source = MKMapItem.mapItemForCurrentLocation()
let destinationPlacemark = MKPlacemark(placemark: currentPlacemark)
directionRequest.destination = MKMapItem(placemark: destinationPlacemark)
directionRequest.transportType = MKDirectionsTransportType.Automobile
// Calculate the direction
let directions = MKDirections(request: directionRequest)
directions.calculateDirectionsWithCompletionHandler { (routeResponse,
routeError) -> Void in
guard let routeResponse = routeResponse else {
if let routeError = routeError {
print("Error: \(routeError)")
}
return
}
let route = routeResponse.routes[0]
self.mapView.addOverlay(route.polyline, level:
MKOverlayLevel.AboveRoads)
}
}
currentPlacemark
contains a value.
. The class is used to store the source and destination of a route. There are
102
a few optional parameters you can configure such as transport type, alternate routes, etc. In
the above code, we just set the source, destination and transport type while using default
values for the rest of the options. The starting point is set to the user's current location. We use
MKMapItem.mapItemForCurrentLocation
route is set to the destination of the selected restaurant. The transport type is set to
automobile
With the
the
MKDirectionsRequest
calculateDirectionsWithCompletionHandler
MKDirections
request for directions and calls your completion handler when the request is completed. The
MKDirections
object simply passes your request to the Apple servers and asks for route-based
directions data. Once the request completes, the completion handler is called. The route
information returned by the Apple servers is returned as an
MKDirectionsResponse
MKDirectionsResponse
object.
provides a container for saving the route information so that the routes
routes
property.
In the completion handler block, we first check if the route response contains a value.
Otherwise, we just print the error. If we can successfully get the route response, we retrieve the
first
the
MKRoute
object. By default, only one route is returned. Apple may return multiple routes if
requestsAlternateRoutes
property of the
MKDirectionsRequest
we didn't enable the alternate route option, we just pick the first route.
With the route, we add it to the map by calling the
The detailed route geometry (i.e.
addOverlay
route.polyline
addOverlay
method of the
) is represented by an
MKPolyline
MKMapView
MKPolyline
class.
object. The
we configure the map view to overlay the route above roadways but below map labels or pointof-interest icons.
That's how you construct a direction request and overlay a route on map. If you run the app
now, you will not see a route when the Direction button is tapped. There is still one thing left.
We need to implement the
rendererForOverlay
103
return renderer
}
MKPolylineRenderer
representation for the specified MKPolyline overlay object. Here the overlay object is the one
we added earlier. The renderer object provides various properties to control the appearance of
the route path. We simply change the stroke color and line width.
Okay, let's run the app again and you should be able to see the route after pressing the
Direction
button. If you can't view the path, remember to check if you set the simulated
boundingMapRect
that completely encompasses the overlay and changes the visible region of the map view.
Insert the following lines of code in the
showDirection
method:
Compile and run the app again. The map should now scale automatically to display the route
within the screen real estate.
105
Next, go to
Car
Walking
button to the left of the navigation bar. The user interface will look better.
MapViewController.swift
Go back to the storyboard and connect the segmented control with the outlet variable. In the
viewDidLoad
method of
super.viewDidLoad()
MapViewController.swift
106
segmentedControl.hidden = true
Direction
MapViewController
class:
automobile
(i.e. car).
Due to the introduction of this variable, we have to change the following line of code in the
method:
showDirection
directionRequest.transportType = MKDirectionsTransportType.Automobile
Simply replace
MKDirectionsTransportType.Automobile
with
currentTransportType
Okay, you've got everything in place. But how can you detect the user's selection of a
segmented control? When a user presses one of the segments, the control sends a
ValueChanged
event. So all you need to do is register the event and perform the corresponding
addTarget
.ValueChanged
showDirection
).
Since we need to check the selected segment, insert the following code snippet at the very
beginning of the
showDirection
method:
switch segmentedControl.selectedSegmentIndex {
107
The
selectedSegmentedIndex
selected segment. If the first segment (i.e. Car) is selected, we set the current transport type to
automobile. Otherwise, it is set to walking. We also unhide the segmented control.
Lastly, insert the following line of code (highlighted in yellow) in the
calculateDirectionsWithCompletionHandler
closure:
self.mapView.removeOverlays(self.mapView.overlays)
addOverlay
like this:
directions.calculateDirectionsWithCompletionHandler { (routeResponse,
routeError) -> Void in
guard let routeResponse = routeResponse else {
if let routeError = routeError {
print("Error: \(routeError)")
}
return
}
let route = routeResponse.routes[0]
self.mapView.removeOverlays(self.mapView.overlays)
self.mapView.addOverlay(route.polyline, level: MKOverlayLevel.AboveRoads)
let rect = route.polyline.boundingMapRect
self.mapView.setRegion(MKCoordinateRegionForMapRect(rect), animated: true)
}
The line of code simply asks the map view to remove all the overlays. This is to avoid both Car
and Walk routes overlapping with each other.
You can now test the app. In the map view, tap the Direction button and the segmented control
should appear. You're free to select the Walking segment to display the walking directions. For
108
now, both types of routes are shown in blue. You can make a minor change in the
rendererForOverlay
method of the
MapViewController
We use blue color for the Car route and orange color for the Walking route. After the change,
run the app again. When walking is selected, the route is displayed in orange.
MKRouteStep
MKRoute
objects. An
MKRouteStep
object
represents one part of an overall route. Each step in a route corresponds to a single instruction
109
Cell
. Next, connect the map view controller with the new table view controller using a
segue. In the Document Outline of Interface Builder, control-drag the map view controller to
the table view controller. Select show for the segue type and set the segue's identifier to
showSteps
The UI design is ready. Now create a new class file using the Cocoa Touch class template.
Name it
RouteTableViewController
UITableViewController
. Once the
class is created, go back to the storyboard. Select the Steps table view controller. Under the
Identity inspector, set the custom class to
RouteTableViewController
objects. Each
steps
property of an
MKRouteStep
MKRoute
instructions
property that
stores the written instructions (e.g. Turn right onto Charles St) for following the path of a
particular step. So all we need to do is loop through all the
110
MKRouteStep
MKAnnotationView
right side of a standard callout bubble. Once you create the accessory view, the
calloutAccessoryControlTapped
RouteTableViewController.swift
import MapKit
MKRouteStep
The above code is very straightforward. We simply display the written instructions of the route
steps in the table view. Next, open
MapViewController.swift
111
In the
viewForAnnotation
annotationView
return
annotationView?.rightCalloutAccessoryView = UIButton(type:
UIButtonType.DetailDisclosure)
Here we add a detail disclosure button to the right side of an annotation. To handle a touch, we
implement the
mapView(_:calloutAccessoryControlTapped:_)
controller and the navigation controller and set the segue's identifier to
will navigate to the Steps table view controller when the above
showSteps
. The app
performSegueWithIdentifier
method is called.
Lastly, we have to pass the current route steps to the
In the body of the
RouteTableViewController
calculateDirectionsWithCompletionHandler
class.
removeOverlays
112
RouteTableViewController
, implement the
prepareForSegue
method
like this:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
// Get the new view controller using segue.destinationViewController.
// Pass the selected object to the new view controller.
if segue.identifier == "showSteps" {
let routeTableViewController = segue.destinationViewController as!
RouteTableViewController
if let steps = currentRoute?.steps {
routeTableViewController.routeSteps = steps
}
}
}
The above code snippet should be very familiar to you. We first get the destination controller,
which is the
RouteTableViewController
controller.
The app is now ready to run. When you tap the annotation on the map, the app shows you a list
of steps to follow.
113
For reference, you can download the complete Xcode project from
https://www.dropbox.com/s/kzkyokepakkzx0i/MapKitDirectionDemo.zip?dl=0.
114
Chapter 9
Search for Nearby Points of Interest
Using Local Search
MKLocalSearch
for points of interest and display them on maps. App developers can use this API to perform
searches for locations, which can be name, address, or type, such as coffee or pizza.
The use of
MKLocalSearch
MKDirections
MKLocalSearchRequest
query. You can also specify the map region to narrow down the search result. You then use the
configured object to initialize an
MKLocalSearch
115
The search is performed remotely in an asynchronous way. Once Apple returns the search
result (as an
MKLocalSearchResponse
executed. In general, you'll need to parse the response object and display the search results on
the map.
Main.storyboard
Name the button Nearby. Click the Issues button in the layout bar and select Add Missing
Constraints.
116
MapViewController.swift
for the Nearby button. Insert the following code snippet in the class:
117
To perform a local search, here are the two things you need to do:
Specify your search parameters in an
MKLocalSearchRequest
parameter. For example, if you want to search for a nearby cafe, you can specify cafe in the
search parameter. Since we want to search for similar types of restaurants, we specify
restaurant.category
in the query.
MKLocalSearch
MKLocalSearch
showNearby
method, we lookup the nearby restaurants that are of the same type (e.g.
Italian). Furthermore, we specify the current region of the map view as the search region.
We then initialize the search by creating the
startWithCompletionHandler:
MKLocalSearch
MKMapItem
through the items (i.e. nearby restaurants) and highlight them on the map using annotations.
Okay, you're almost ready to test the app. Just go to the storyboard and connect the Nearby
button with the
showNearby
showNearby:
118
action method.
Cool, right? With just a few lines of code, you took your Map app to the next level. If you're
going to embed a map within your app, try to explore the local search API.
119
mapView(_:viewForAnnotation:_)
120
}
}
}
annotationView?.rightCalloutAccessoryView = UIButton(type:
UIButtonType.DetailDisclosure)
return annotationView
}
We have modified the method a bit by adding a condition block. The code checks if the
annotation, which is about to display, is the same as the current placemark. In other words, we
check if the coordinate of the annotation is equal to the coordinate of the selected restaurant. If
the result is positive, we display the restaurant image in the callout bubble and keep the pin
color to orange. Otherwise, for those nearby restaurants, we set the pin color to red.
Now run the project and try out the Nearby feature again. The pin color of the nearby
restaurants is now in red.
121
For reference, you can download the complete Xcode project from
https://www.dropbox.com/s/la3872e8jffou9q/MapKitLocalSearch.zip?dl=0.
122
Chapter 10
Audio Recording and Playback
The iOS SDK provides various frameworks to let you work with sounds in your app. One of the
frameworks that you can use to play and record audio files is the AV Foundation
framework. In this chapter, I will walk you through the basics of the framework and show
you how to manage audio playback and recording.
The AV Foundation provides essential APIs for developers to deal with audio on iOS. In this
demo, we mainly use these two classes of the framework:
AVAudioPlayer think of it as an audio player for playing sound files. By using the
player, you can play sounds of any duration and in of the any audio formats available in
123
iOS.
AVAudioRecorder an audio recorder for recording audio.
ViewController
class.
Credit: The photo used in the demo app is courtesy of Eric May.
The
AVAudioRecorder
recording capability. In iOS, the audio being recorded comes from the built-in microphone or
headset microphone of the iOS device. These devices include the iPhone, iPad or iPod touch.
First, let's take a look at how we can use the
of the APIs in the SDK,
AVAudioRecorder
AVAudioRecorder
implement a delegate object for an audio recorder to respond to audio interruptions and to the
completion of a recording. The delegate of an
AVAudioRecorderDelegate
AVAudioRecorder
ViewController
AVAudioRecorderDelegate
class serves
protocol in the
class:
class ViewController: UIViewController, AVAudioRecorderDelegate
Because the delegate is defined in the AV Foundation framework, you have to import the
AVFoundation:
import AVFoundation
AVAudioPlayer
in
ViewController.swift
AVAudioRecorder
var audioRecorder:AVAudioRecorder?
var audioPlayer:AVAudioPlayer?
Let's focus on
AVAudioRecorder
AVAudioRecorder
audioPlayer
class provides an easy way to record sounds in your app. To use the recorder,
viewDidLoad
code:
override func viewDidLoad() {
super.viewDidLoad()
125
126
In the above code, we first disable both the Stop and Play buttons when the app is launched.
We then define the URL of the sound file for saving the recording - you can use
NSFileManager
to interact with the file system. We simply ask for the document directory in the user's home
directory (
NSSearchPathDomainMask.UserDomainMask
MyAudioMemo.m4a
AVAudioSessionCategoryPlayAndRecord
output, and uses the built-in speaker for recording and playback.
The
AVAudioRecorder
we use the option keys to configure the audio data format, sample rate, number of channels
and audio quality. After defining the audio settings, we initialize an
and set the delegate to itself. Lastly, we call the
prepareToRecord
AVAudioRecorder
object
file and get prepared for recording. Note that the recording has not yet started; the recording
will not begin until the record method is called.
As you may notice, weve put a try keyword in front of the
audioSession.setCategory
call. In
Swift 2, Apple has changed some of the APIs in favour of the do-try-catch error handling
model. Prior to Swift 2, it lacked a proper error handling model. When calling a method that
may cause a failure, you normally pass it with an NSError object (as a pointer) like this:
audioSession.setCategory(AVAudioSessionCategoryPlayAndRecord, withOptions:
AVAudioSessionCategoryOptions.DefaultToSpeaker, error: &error)
127
If there is an error, the object will be set to the corresponding error. You then check if the error
object is
nil
or not and respond to the error accordingly. This is how you handle errors in
Swift 1.2. Starting from Swift 2, it comes with an exception-like model. The same method call
no longer takes in an error parameter. Instead, it throws an error for any failures. To invoke a
method call that throws an error, you have to enclose it in a do-catch block like this:
do {
try audioSession.setCategory(AVAudioSessionCategoryPlayAndRecord,
withOptions: AVAudioSessionCategoryOptions.DefaultToSpeaker)
} catch {
print(error)
}
In the
do
error
catch
try
object.
128
if !recorder.recording {
let audioSession = AVAudioSession.sharedInstance()
do {
try audioSession.setActive(true)
// Start recording
recorder.record()
recordButton.setImage(UIImage(named: "recording"), forState:
UIControlState.Selected)
recordButton.selected = true
} catch {
print(error)
}
} else {
// Pause recording
recorder.pause()
recordButton.setImage(UIImage(named: "pause"), forState:
UIControlState.Normal)
recordButton.selected = false
}
}
stopButton.enabled = true
playButton.enabled = false
}
In the above code, we first check whether the audio player is playing. You probably don't want
to play an audio file while you're recording, so we simply stop any audio playback by using the
stop method.
If
audioRecorder
is not in recording mode, the app activates the audio sessions and starts the
record
active
audioSession.setActive(true)
Once the recording starts, we change the Record button to a recording button (a different
image). In case the user taps the Record button while the recorder is in recording mode, we
pause it by calling the
pause
method.
AVAudioRecorder
recording:
129
stop
action method is called when the user taps the Stop button. This method is pretty
simple. All we need to do is reset the Record and Play buttons to the normal state.
Furthermore, we will call the
stop
stop
following code:
@IBAction func stop(sender: AnyObject) {
recordButton.setImage(UIImage(named: "record"), forState:
UIControlState.Normal)
recordButton.selected = false
playButton.setImage(UIImage(named: "play"), forState:
UIControlState.Normal)
playButton.selected = false
stopButton.enabled = false
playButton.enabled = true
audioRecorder?.stop()
let audioSession = AVAudioSession.sharedInstance()
do {
try audioSession.setActive(false)
} catch {
print(error)
}
}
AVAudioRecorderDelegate
a phone call during audio recording) as well as to complete the recording. In the example,
ViewController
AVAudioRecorderDelegate
protocol
audioRecorderDidFinishRecording
method to handle the completion of recording. Add the following code to the
ViewController.swift
file:
After the recording completes, the app displays an alert dialog with a success message.
AVAudioPlayer
responsible for audio playback. Typically, there are a few things you have to implement in
order to use
AVAudioPlayer
Initialize the audio player and assign a sound file to it. In this case, it's the audio file of the
recording (i.e. MyAudioMemo.m4a). You can use the URL property of an
AVAudioRecorder
Designate an audio player delegate object, which handles interruptions as well as the
playback-completed event.
Call the
In the
play
ViewController
class, edit the play action method using the following code:
131
}
}
}
recorder.url
viewDidLoad
AVAudioPlayer
with
speaker. Thus, the player will use the speaker for audio playback.
AVAudioPlayer
ViewController
AVAudioPlayerDelegate
ViewController
protocol.
declaration to
The delegate allows you to handle interruptions, audio decoding errors, and update the user
interface when an audio file finishes playing. All methods in the
AVAudioplayerDelegate
protocol are optional, however. To demonstrate how it works, we'll implement the
audioPlayerDidFinishPlaying
playback. For usage of the other methods, you can refer to the official documentation of
AVAudioPlayerDelegate protocol.
Insert the following code in
ViewController.swift
132
For your reference, you can download the Xcode project from
https://www.dropbox.com/s/tz223o7whsemynd/AudioDemo.zip?dl=0.
133
Chapter 11
Scan QR Code Using AVFoundation
Framework
So, what's QR code? I believe most of you know what a QR code is. In case you haven't heard of
it, just take a look at the above image - that's a QR code.
QR (short for Quick Response) code is a kind of two-dimensional bar code developed by
Denso. Originally designed for tracking parts in manufacturing, QR code has gained popularity
in consumer space in recent years as a way to encode the URL of a landing page or marketing
information. Unlike the basic barcode that you're familiar with, a QR code contains
information in both the horizontal and vertical direction. Thus this contributes to its capability
of storing a larger amount of data in both numeric and letter form. I don't want to go into the
134
technical details of the QR code here. If you're interested in learning more, you can check out
the official website of QR code.
With the rising prevalence of iPhone and Android phones, the use of QR codes has been
increased dramatically. In some countries QR codes can be found nearly everywhere. They
appear in magazines, newspapers, advertisements, billboards and even name cards. As an iOS
developer, you may wonder how you can empower your app to read a QR code. Prior to iOS 7,
you had to rely on third party libraries to implement the scanning feature. Now, you can use
the built-in AVFoundation framework to discover and read barcodes in real-time.
Creating an app for scanning and translating QR codes has never been so easy.
Quick tip: You can generate your own QR code. Simply go to http://www.qrcodemonkey.com to create one.
135
ViewController.swift
136
import AVFoundation
AVCaptureMetadataOutputObjectsDelegate
about that in a while. For now, update the following line of code:
class ViewController: UIViewController, AVCaptureMetadataOutputObjectsDelegate
ViewController
viewDidLoad
AVCaptureDevice
method of the
AVCaptureSession
object with
ViewController
class:
137
An
AVCaptureDevice
configure the properties of the underlying hardware. Since we are going to capture video data,
we call the
defaultDeviceWithMediaType
method, passing it
AVMediaTypeVideo
AVCaptureSession
AVCaptureSession
object and
the flow of data from the video input device to our output.
In this case, the output of the session is set to an
AVCaptureMetaDataOutput
with the
AVCaptureMetaDataOutput
object. The
AVCaptureMetadataOutputObjectsDelegate
found in the input device (the QR code captured by the device's camera) and translate it to a
human-readable format. Don't worry if something sounds weird or if you don't totally
understand it right now - everything will become clear in a while. For now, continue to add the
following lines of code in the
do
block of the
viewDidLoad
method:
the
AVCaptureMetadataOutputObjectsDelegate
self
QRReaderViewController
class adopts
captured, they are forwarded to the delegate object for further processing. Here we also need to
specify the dispatch queue on which to execute the delegate's methods. According to Apple's
documentation, the queue must be a serial queue. So we simply use the
dispatch_get_main_queue()
// Set delegate and use the default dispatch queue to execute the call back
captureMetadataOutput.setMetadataObjectsDelegate(self, queue:
dispatch_get_main_queue())
captureMetadataOutput.metadataObjectTypes = [AVMetadataObjectTypeQRCode]
The
metadataObjectTypes
property is also quite important; as this is the point where we tell the
AVMetadataObjectTypeQRCode
clearly
AVCaptureMetadataOutput
138
the video captured by the device's camera on screen. This can be done using an
AVCaptureVideoPreviewLayer
, which actually is a
CALayer
conjunction with an AV capture session to display video. The preview layer is added as a
sublayer of the current view. Here is the code:
// Initialize the video preview layer and add it as a sublayer to the
viewPreview view's layer.
videoPreviewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
videoPreviewLayer?.videoGravity = AVLayerVideoGravityResizeAspectFill
videoPreviewLayer?.frame = view.layer.bounds
view.layer.addSublayer(videoPreviewLayer!)
startRunning
If you compile and run the app on a real iOS device, it should start capturing video when
launched. However, at this point the message label is still hidden. You can fix it by adding the
following line of code:
// Move the message label to the top view
view.bringSubviewToFront(messageLabel)
Re-run the app after making the changes. The message label No QR code is detected should
now appear on screen.
139
do
block of the
UIView
viewDidLoad
The
qrCodeFrameView
UIView
object is set
to zero by default. Later, when a QR code is detected, we will change its size and turn it into a
green box.
AVCaptureMetadataOutput
AVCaptureMetadataOutputObjectsDelegate
will be called:
So far we haven't implemented the method; this is why the app can't translate the QR code. In
order to capture the QR code and decode the information, we need to implement the method
to perform additional processing on metadata objects. Here is the code:
func captureOutput(captureOutput: AVCaptureOutput!, didOutputMetadataObjects
metadataObjects: [AnyObject]!, fromConnection connection: AVCaptureConnection!)
{
// Check if the metadataObjects array is not nil and it contains at least
one object.
if metadataObjects == nil || metadataObjects.count == 0 {
qrCodeFrameView?.frame = CGRectZero
messageLabel.text = "No QR code is detected"
140
return
}
// Get the metadata object.
let metadataObj = metadataObjects[0] as!
AVMetadataMachineReadableCodeObject
if metadataObj.type == AVMetadataObjectTypeQRCode {
// If the found metadata is equal to the QR code metadata then update
the status label's text and set the bounds
let barCodeObject =
videoPreviewLayer?.transformedMetadataObjectForMetadataObject(metadataObj)
qrCodeFrameView?.frame = barCodeObject!.bounds
if metadataObj.stringValue != nil {
messageLabel.text = metadataObj.stringValue
}
}
}
metadataObjects
all the metadata objects that have been read. The very first thing we need to do is make sure
that this array is not
qrCodeFrameView
nil
messageLabel
If a metadata object is found, we check to see if it is a QR code. If that's the case, we'll proceed
to find the bounds of the QR code. These couple lines of code are used to set up the green box
for highlighting the QR code. By calling the
method of
viewPreviewLayer
transformedMetadataObjectForMetadataObject
coordinates. From that, we can find the bounds of the QR code for constructing the green box.
Lastly, we decode the QR code into human-readable information. This step should be fairly
simple. The decoded information can be accessed by using the stringValue property of an
AVMetadataMachineReadableCode
object.
Now you're ready to go! Hit the Run button to compile and run the app on a real device. Once
launched, point it to a QR code like the one on your left. The app immediately detects the code
and decodes the information.
141
142
Your task is to tweak the existing Xcode project and enable the demo to scan other types of
barcodes. You'll need to instruct
captureMetadataOutput
I'll leave it for you to figure out the solution. While I include the solution in the Xcode project
below, I encourage you to try to sort out the problem on your own before moving on. It's gonna
be fun and this is the best way to really understand how the code operates.
If you've given it your best shot and are still stumped, you can download the solution from
https://www.dropbox.com/s/c1fo4cr4ksp147d/QRReaderDemo.zip?dl=0.
143
Chapter 12
Working with URL Schemes
The URL scheme is an interesting feature provided by the iOS SDK that allows developers to
launch system apps and third-party apps through URLs. For example, let's say your app
displays a phone number, and you want to make a call whenever a user taps that number. You
can use a specific URL scheme to launch the built-in phone app and dial the number
automatically. Similarly, you can use another URL scheme to launch the Message app for
sending an SMS. Additionally, you can create a custom URL scheme for your own app so that
other applications can launch your app via a URL. You'll see what I mean in a minute.
144
As usual, we will build an app to demonstrate the use of URL schemes. We will reuse the QR
code reader app that was built in the previous chapter. If you haven't read the previous
chapter, go back and read it before continuing on.
So far, the demo app is capable of decoding a QR code and displaying the decoded message on
screen. In this chapter, we'll make it even better. When the QR code is decoded, the app will
launch the corresponding app based on the type of the URL.
To start with, first download the QRCodeReader app from
https://www.dropbox.com/s/c1fo4cr4ksp147d/QRReaderDemo.zip?dl=0. If you compile and
run the app, you'll have a simple QR code reader app. Note that the app only works on a real
iOS device.
Quick tip: How do you know the custom URL of third-party iOS apps? You can
check out http://handleopenurl.com and
http://wiki.akosma.com/IPhone_URL_Schemes. Both websites provide an extensive
list of URLs of third-party iOS apps. Alternatively, you can refer to the
official documentation of the third-party apps (e.g. Facebook, Whatsapp)
Sample QR Codes
Here I include some sample QR codes that you can use to test the app. Alternatively, you can
create your QR code using online services like www.qrcode-monkey.com. Open the demo app
and point your device's camera at one of the codes. You should see the decoded message.
145
mailto
mailto:[email protected]
tel://743234028
openURL
) or the
tel
method of the
UIApplication
class. Here is
Now we will modify the demo to open the corresponding app when a QR code is decoded.
Open the Xcode project and select the
launchApp
ViewController.swift
in the class:
146
The
launchApp
method takes in a URL decoded from the QR code and creates an alert prompt.
If the user taps the Confirm button, the app then creates an
NSURL
accordingly. iOS will then open the corresponding app based on the given URL.
In the
captureOutput
to call the
launchApp
launchApp(metadataObj.stringValue)
Now compile and run the app. Point your device's camera at one of the sample QR codes (e.g.
tel://743234028
). The app will prompt you with an action sheet when the QR code is decoded.
Once you tap the Confirm button, it opens the Phone app and initiates the call.
147
But there is a minor issue with the current app. If you look into the console, you should find
the following warning:
2015-10-09 13:23:47.911 QRReaderDemo[4931:2581028] Warning: Attempt to present
<UIAlertController: 0x135e5a660> on <QRReaderDemo.ViewController: 0x135e3a230>
which is already presenting <UIAlertController: 0x135f0f740>
The
launchApp
method is called every time when a barcode or QR code is scanned. So the app
UIAlertController
presented.
UIAlertController
object
UIAlertController
presentViewController
method.
UIAlertController
presentedViewController
ViewController
UIAlertController
object, the
presentViewController
148
presentedViewController
UIAlertController
method,
property is set
presentedViewController
nil
With this property, it is quite easy for us to fix the warning issue. All you need to do is put the
following code at the beginning of the
launchApp
method:
if presentedViewController != nil {
return
}
We simply check if the property is set to a specific view controller, and present the
UIAlertController
object only if there is no presented view controller. Now run the app again.
fb://feed
whatsapp://send?text=Hello!
The first URL is used to open the news feed of the user's Facebook app. The other URL is for
sending a text message using Whatsapp. Interestingly, Apple allows developers to create their
own URLs for communicating between apps. Let's see how we can add a custom URL to our
QR Reader app.
We're going to create another app called TextReader. This app serves as a receiver app that
149
defines a custom URL and accepts a text message from other apps. The custom URL will look
like this:
textreader://Hello!
When an app (e.g. QR Code Reader) launches the URL, iOS will open the TextReader app and
pass it the Hello! message. In Xcode, create a new project using the Single View Application
template and name it
TextReader
Info.plist
You'll be prompted to select a key from a drop-down menu. Scroll to the bottom and select
types
URL
. This creates an array item so you can click the disclosure icon (i.e. triangle) to expand it.
You'll then select Item 0. Click the disclosure icon next to the item and expand it to show the
URL identifier line. Double-click the value field to fill in your identifier. Typically, you set the
value to be the same as the bundle ID (e.g. com.appcoda.TextReader).
Next, right click on
menu, select
Item 0
URL Schemes
and select
Add Row
150
Again, click the disclosure icon of URL Schemes to expand the item. Double click the value box
of Item 0 and key in
textreader
That's it. We have configured a custom URL scheme in the TextReader app. Now the app takes
the URL in the form of
textreader://<message>
that it knows what to do when another app launches the custom URL (e.g.
textreader://Hello!
).
AppDelegate
UIApplicationDelegate
protocol. The
method defined in the protocol gives you a chance to interact with important events during the
lifetime of your app. When an Open a URL event is sent to your app, the system calls the
application(_:openURL:sourceApplication:annotation:)
need to implement this method in respond to the launch of the custom URL.
Open
AppDelegate.swift
openURL
textreader://Hello!
URL object. The first line of code extracts the message by using the host property of the
NSURL object.
URLs can only contain ASCII characters, spaces are not allowed. For characters outside the
ASCII character set, they should be encoded using URL encoding. URL encoding replaces
unsafe ASCII characters with a % followed by two hexadecimal digits and a space with
%20
If you compile and run the app, you should see a blank screen. That's normal because the
TextReader app is triggered by another app using the custom URL. You have two ways to test
the app. You can open mobile Safari and enter
textreader://Great!%20It%20works!
in the
address bar - you'll be prompted to open the TextReader app. Once confirmed, the system
should redirect you to the TextReader app and displays the
Great! It works!
message.
Alternatively, you can use the QR Code Reader app for testing. If you open the app and point
the camera to the QR code shown below, the app should be able to decode the message but
fails to open the TextReader app.
152
canOpenURL
Info.plist
true
. To register a custom
LSApplicationQueriesSchemes
Array
textreader
fb
whatsapp
153
Once you've made the change, test the QR Reader app again. Point to a QR code with a custom
URL scheme (e.g. textreader). The app should be able to launch the corresponding app.
154
Chapter 13
Building a Full Screen Camera
iOS provides two ways for developers to access the built-in camera for taking photos. The
simple approach is to use
UIImagePickerViewController
Beginning iOS 9 Programming book. This class is very handy and comes with a standard
camera interface. Alternatively, you can control the built-in cameras and capture images using
AVFoundation framework. Compared to
UIImagePickerViewController
, AVFoundation
framework is more complicated, but also far more flexible and powerful for building a fully
custom camera interface.
In this chapter, we will see how to use the AVFoundation framework for capturing still images.
You will learn a lot of stuff including:
155
AVCaptureSession
flow between an input (e.g. back-facing camera) and an output (e.g. image file). In general, to
capture a still image using the AVFoundation framework, you'll need to:
Get an instance of
AVCaptureDevice
AVCaptureDeviceInput
Create an instance of
AVCaptureStillImageOutput
Use
AVCaptureSession
Create an instance of
to coordinate the data flow from the input and the output
AVCaptureVideoPreviewLayer
preview
If you still have questions at this point, no worries. The best way to learn any new concept is by
trying it out - following along with the demo creation should help to clear up any confusion
surrounding the AV Foundation framework.
Demo App
We're going to build a simple camera app that offers a full-screen experience and gesturebased controls. The app provides a minimalistic UI with a single capture button at the bottom
of the screen. Users can swipe up the screen to switch between the front-facing and back-facing
cameras. The camera offers up to 5x digital zoom. Users can swipe the screen from left to right
to zoom in or from right to left to zoom out.
When the user taps the capture button, it should capture the photo in full resolution.
Optionally, the user can save to the photo album.
156
PhotoViewController
is associated with the save action method, which is now without any implementation.
157
Configuring a Session
As mentioned, the heart of AVFoundation media capture is the
open
ViewController.swift
AVCaptureSession
AVCaptureSession
object. So
In the
viewDidLoad
sessionPreset
Here we preset it to
AVCaptureSessionPresetPhoto
ViewController
class:
158
var backFacingCamera:AVCaptureDevice?
var frontFacingCamera:AVCaptureDevice?
var currentDevice:AVCaptureDevice?
Since the camera app supports both front and back-facing cameras, we create two separate
variables for storing the
AVCaptureDevice
objects. The
currentDevice
viewDidLoad
method:
class provides a couple of methods for querying the available capture devices.
devicesWithMediaType
AVCaptureDevice
to return us an
array of capture devices that are capable of capturing video/still image (i.e.
AVMediaTypeVideo
).
With the cameras returned, we examine its position property to determine if it is a front-facing
or back-facing camera. By default, the camera app uses the back-facing camera when it's first
started. Thus, we set the
currentDevice
AVCaptureDeviceInput
ViewController
class
viewDidLoad
method:
// Configure the session with the output for capturing still images
stillImageOutput = AVCaptureStillImageOutput()
stillImageOutput?.outputSettings = [AVVideoCodecKey: AVVideoCodecJPEG]
AVCaptureStillImageOutput
AVCaptureStillImageOutput
do
block of the
viewDidLoad
method:
// Configure the session with the input and the output devices
captureSession.addInput(captureDeviceInput)
captureSession.addOutput(stillImageOutput)
AVCaptureSession
viewDidLoad
method:
160
cameraPreviewLayer?.videoGravity = AVLayerVideoGravityResizeAspectFill
cameraPreviewLayer?.frame = view.layer.frame
// Bring the camera button to front
view.bringSubviewToFront(cameraButton)
captureSession.startRunning()
AVCaptureVideoPreviewLayer
device. The layer is then added to the view's layer to display on the screen. The preview layer
object provides a property named
videoGravity
AVLayerVideoGravityResize
and
AVLayerVideoGravityResizeAspect
interface is presented.
As you add the preview layer to the view, it should cover the camera button. To unhide the
button, we simply bring it to the front. Lastly, we call the
startRunning
capture
method of the
161
When you add the input and the output to the session, the session forms connections between
them. To capture a still image, we first retrieve the corresponding connection (i.e. the one with
video media type). We then call the
captureStillImageAsynchronouslyFromConnection
method
to capture a still image asynchronously. When the capture completes, the system will call the
complete handler with the corresponding image data.
With the data captured in the form of a
jpegStillImageNSDataRepresentation
CMSampleBuffer
, we call the
NSData
object of a
still image. Lastly, we invoke the showPhoto segue to display the still image in the Photo View
Controller. Remember to add the
prepareForSegue
method in the
ViewController
class:
Now you're ready to test the app. Hit the Run button and test out the camera button. It should
now work and be able to capture a still image.
162
viewDidLoad
method:
The
UISwipeGestureRecognizer
directions. Since we look for swipe-up gestures, we configure the recognizer for the
direction only. You use the
addTarget
.Up
toggleCamera
implemented shortly. Once you've configured the recognizer object, you have to attach it to a
view; that is the view that receives touches. We simply call the
addGestureRecognizer
toggleCamera
in the
ViewController
class:
func toggleCamera() {
captureSession.beginConfiguration()
// Change the device based on the current camera
let newDevice = (currentDevice?.position == AVCaptureDevicePosition.Back) ?
frontFacingCamera : backFacingCamera
// Remove all inputs from the session
for input in captureSession.inputs {
captureSession.removeInput(input as! AVCaptureDeviceInput)
}
// Change to the new input
let cameraInput:AVCaptureDeviceInput
do {
cameraInput = try AVCaptureDeviceInput(device: newDevice)
} catch {
print(error)
return
}
if captureSession.canAddInput(cameraInput) {
captureSession.addInput(cameraInput)
}
currentDevice = newDevice
captureSession.commitConfiguration()
}
163
method
The method is used to toggle between front-facing and back-facing cameras. To switch the
input device of a session, we first call the
beginConfiguration
This indicates the start of the configuration change. Next, we determine the new device to use.
Before adding the new device input to the session, you have to remove all existing inputs from
the session. You can simply access the
inputs
inputs. We simply loop through them and remove them from the session by calling the
removeInput
method.
Once all the inputs are removed, we add the new device input (i.e. front / back facing camera)
to the session. Lastly, we call the
commitConfiguration
changes. Note that no changes are actually made until you invoke the method.
It's time to have a quick test. Run the app on a real iOS device. You should be able to switch
between cameras by swiping up the screen.
ViewController
UISwipeGestureRecognizer
viewDidLoad
method:
// Zoom In recognizer
zoomInGestureRecognizer.direction = .Right
zoomInGestureRecognizer.addTarget(self, action: "zoomIn")
view.addGestureRecognizer(zoomInGestureRecognizer)
// Zoom Out recognizer
zoomOutGestureRecognizer.direction = .Left
zoomOutGestureRecognizer.addTarget(self, action: "zoomOut")
view.addGestureRecognizer(zoomOutGestureRecognizer)
164
direction
gesture recognizers. I will not go into the details because the implementation is pretty much
the same as that covered in the previous section.
Now create two new methods for
zoomIn
and
zoomOut
func zoomIn() {
if let zoomFactor = currentDevice?.videoZoomFactor {
if zoomFactor < 5.0 {
let newZoomFactor = min(zoomFactor + 1.0, 5.0)
do {
try currentDevice?.lockForConfiguration()
currentDevice?.rampToVideoZoomFactor(newZoomFactor, withRate:
1.0)
currentDevice?.unlockForConfiguration()
} catch {
print(error)
}
}
}
}
func zoomOut() {
if let zoomFactor = currentDevice?.videoZoomFactor {
if zoomFactor > 1.0 {
let newZoomFactor = max(zoomFactor - 1.0, 1.0)
do {
try currentDevice?.lockForConfiguration()
currentDevice?.rampToVideoZoomFactor(newZoomFactor, withRate:
1.0)
currentDevice?.unlockForConfiguration()
} catch {
print(error)
}
}
}
}
To change the zoom level of a camera device, all you need to do is adjust the
videoZoomFactor
property. The property controls the enlargement of images captured by the device. For
example, a value of 2.0 doubles the size of an image. If it is set to
1.0
field of view. You can directly modify the value of the property to achieve a zoom effect.
However, to provide a smooth transition from one zoom factor to another, we use the
rampToVideoZoomFactor
zoomIn
method, we first check if the zoom factor is larger than 5.0 (the camera app only
lockForConfiguration
rampToVideoZoomFactor
method to acquire
to achieve the zooming effect. Once done, we release the lock by calling the
unlockForConfiguration
The
zoomOut
method.
zoomIn
method. Instead of
increasing the zoom factor, the method reduces the zoom factor when called. The minimum
value of the zoom factor is 1.0; this is why we have to ensure the zoom factor is not set to any
value less than 1.0.
Now hit the Run button to test the app on your iOS device. When the camera app is launched,
try out the zoom feature by swiping the screen from left to right.
PhotoViewController
class is used to display a still image captured by the device. For now
the image is stored in memory. You can't save the image to the built-in photo album because
we haven't implemented the Save button yet. If you open the
the
save
PhotoViewController.swift
file,
It is very simple to save a still image to the Camera Roll album. UIKit provides the following
function to let you add an image to the user's Camera Roll album:
func UIImageWriteToSavedPhotosAlbum(_ image: UIImage!, _ completionTarget:
AnyObject!, _ completionSelector: Selector , _ contextInfo:
UnsafeMutablePointer<Void>)
So in the
save
save
method of the
PhotoViewController
166
We first check if the image is ready to save. And then call the
UIImageWriteToSavedPhotosAlbum
function to save the still image to the camera roll, followed by dismissing the view controller.
Hit the Run button again to test the app. The Camera app should now be able to save photos to
your photo album. Just use Photos app to verify the result.
Congratulations! You've managed to use the AVFoundation framework and build a camera app
for capturing photos. To further explore the framework, I recommend you check out the
official documentation from Apple. For your reference, you can download the complete Xcode
project from https://www.dropbox.com/s/1zr5c9wqy9ys2u3/SimpleCamera.zip?dl=0.
167
Chapter 14
Video Capturing and Playback Using
AVKit
Previously, we built a simple camera app using the AVFoundation framework. You are not
limited to using the framework for capturing still images. By changing the input and the output
of
AVCaptureSession
, you can easily turn the simple camera app into a video-capturing app.
In this chapter, we will develop a simple video app that allows users to record videos. Not only
we will explore video capturing, but I will also show you a new framework known as AVKit.
The framework was first introduced in iOS 8 and can be used to play video content in your iOS
168
app. You will discover how easy it is to integrate AVKit into your app for video playback.
To get started, download the project template from
https://www.dropbox.com/s/af9fu0329r87r2q/SimpleVideoCameraTemplate.zip?dl=0. The
template is very similar to the one you worked on in the previous chapter. If you run the
template, you will see a blank screen with a red button (which is the record button) at the
bottom part of the screen.
Configuring a Session
Similar to image capturing, the first thing to do is import the
prepare the
AVCaptureSession
object. In the
AVFoundation
ViewController.swift
framework and
AVCaptureSession
In the
viewDidLoad
AVCaptureSessionPresetHigh
169
, which indicates a
AVCaptureSessionPresetMedium
which is suitable for capturing videos that can be shared over WiFi. If you need to share the
video over a 3G network, you may set the value to
AVCaptureSessionPresetLow
ViewController
class:
var currentDevice:AVCaptureDevice?
viewDidLoad
method:
For this demo app, we only support the back-facing camera for capturing videos. Once the
device is retrieved, we create an instance of
AVCaptureDeviceInput
170
ViewController
class
viewDidLoad
method:
AVCaptureMovieFileOutput
AVCaptureMovieFileOutput
controlling the length and size of the recording. For example, you can use the
maxRecordedDuration
property to specify the longest duration allowed for the recording. In this
viewDidLoad
method:
// Configure the session with the input and the output devices
captureSession.addInput(captureDeviceInput)
captureSession.addOutput(videoFileOutput)
viewDidLoad
method:
171
cameraPreviewLayer?.videoGravity = AVLayerVideoGravityResizeAspectFill
cameraPreviewLayer?.frame = view.layer.frame
// Bring the camera button to front
view.bringSubviewToFront(cameraButton)
captureSession.startRunning()
You use
AVCaptureVideoPreviewLayer
The layer is then added to the view's layer to display on the screen. This is pretty much the
same as what we implemented in the Camera app.
When you add the preview layer to the view, it will cover the record button. To unhide the
button, we simply bring it to the front. Lastly, we call the
startRunning
to start capturing data. If you compile and run the app on a real device, you should see the
camera preview.
Let's continue to implement the video capturing.
Now the output of the session is configured for capturing data to a movie file. However, the
saving process will not start until the
AVCaptureMovieFileOutput
startRecordingToOutputFileURL
capture
method of
172
videoFileOutput?.startRecordingToOutputFileURL(outputFileURL,
recordingDelegate: self)
} else {
isRecording = false
UIView.animateWithDuration(0.5, delay: 1.0, options: [], animations: {
() -> Void in
self.cameraButton.transform = CGAffineTransformMakeScale(1.0, 1.0)
}, completion: nil)
cameraButton.layer.removeAllAnimations()
videoFileOutput?.stopRecording()
}
}
We first check if the app is doing any recordings. If not, we initiate video capturing. Once
recording starts, we create a simple animation for the button to indicate recording is in
progress. If you've read over Chapter 13 of the Beginning iOS 9 Programming book, the
animateWithDuration
method shouldn't be new to you. What's new to you are the animation
options. Here I want to create a pulse animation for the button. In order to create such an
effect, here is what needs to be done:
First, reduce the size of the button by 50%
Then grow the button to the original size
Keep repeating step #1 and #2
If we write the above steps in code, this is the code snippet you need:
UIView.animateWithDuration(0.5, delay: 0.0, options: [.Repeat, .Autoreverse,
.AllowUserInteraction], animations: { () -> Void in
self.cameraButton.transform = CGAffineTransformMakeScale(0.5, 0.5)
}, completion: nil)
CGAffineTransformMakeScale
UIView animation, the button will reduce its size by half smoothly.
For step #2, we use the
.Autoreverse
.Repeat
indefinitely. While animating the button, users should still be able to interact with it. This is
why we also specify the
.AllowUserInteraction
option.
173
Now let's get back to the code for saving video data. The
provides a convenient method called
AVCaptureMovieFileOutput
startRecordingToOutputFileURL
class
to capture data to a
movie file. All you need to do is specify an output file path and the delegate object. Here, we
simply save the video to the temporary folder. Once the recording is completely written to the
movie file, it will notify the delegate object by calling the following method:
captureOutput:didFinishRecordingToOutputFileAtURL:fromConnections:error:
Conversely, if the recording is in progress when the button is tapped, we simply reset the
button to its original size, remove all animations, and then stop the recording.
AVPlayerViewController
UIViewController
AVPlayerViewController
is one of
AVPlayerViewController
class is the
player
property, which provides video content to the view controller. The player is of the type
AVPlayer
, which is a class from the AVFoundation framework for controlling playback. To use
AVPlayerViewController
AVPlayer
for video playback, you just need to set the player property to an
object.
AVPlayerViewController
the Interface Builder and open the Object library, you will find an
AVPlayerViewController
object. You can drag the object onto the storyboard and connect it with other view controllers.
Okay, let's continue to develop the video camera app.
First, open
Main.storyboard
and drag an
AVPlayerViewController
174
Next, connect the original View Controller to the AV Player View Controller using a segue. In
the Document Outline, control-drag from the View Controller to the AV Player View
Controller. When prompted, select Present Modally as the segue type. Select the segue and go
to Attributes inspector. Set the identifier of the segue to
playVideo
AVPlayerController
bring it up for video playback? For the demo app, we'll play the movie file right after the user
175
AVCaptureMovieFileOutput
captureOutput
method of the
delegate object once the video has been completely written to a movie file. Here, the
ViewController
AVCaptureFileOutputRecordingDelegate
So open the
ViewController.swift
protocol.
file, import
AVKit
import AVKit
class ViewController: UIViewController, AVCaptureFileOutputRecordingDelegate
Next, implement the delegate method and segue method like this:
func captureOutput(captureOutput: AVCaptureFileOutput!,
didFinishRecordingToOutputFileAtURL outputFileURL: NSURL!, fromConnections
connections: [AnyObject]!, error: NSError!) {
if error != nil {
print(error)
return
}
performSegueWithIdentifier("playVideo", sender: outputFileURL)
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "playVideo" {
let videoPlayerViewController = segue.destinationViewController as!
AVPlayerViewController
let videoFileURL = sender as! NSURL
videoPlayerViewController.player = AVPlayer(URL: videoFileURL)
}
}
When a video is captured and written to a file, the above method is invoked. We simply
determine if there are any errors and bring up the AV Player View Controller by calling the
performSegueWithIdentifier
In the
of
performSegueWithIdentifier
AVPlayer
player
176
AVPlayer
need to perform video playback. AVFoundation then takes care of opening the video URL,
buffering the content and playing it back.
Now you're ready to test the video camera app. Hit Run and capture a video. Once you stop the
video capturing, the app automatically plays the video in the AV Player View Controller.
177
Chapter 15
Displaying Banner Ads using iAd
Like most developers, you're probably looking for ways to make extra money from your app.
The most straightforward way is to put your app in the App Store and sell it for $0.99 or more.
This paid model works really well for some apps. But this is not the only monetization model.
In this chapter, we'll discuss how to monetize your app using iAd.
iAd is an advertising platform developed by Apple for its iOS devices including the iPhone,
iPod touch, and iPad. It allows developers to embed ads in their iOS apps without relying on
third-party advertisers. Apple sells the advertising space (e.g. banner) within your app to a
bunch of advertisers. You earn 70% of the ad revenue when a user views or clicks your ads.
178
What separates iAd from other banner ads is that it promises to deliver more interactive ads on
mobile. When a user taps on an ad banner, a full-screen interactive ad appears on the screen.
But from a developer's perspective, it provides the easiest way to deliver advertisements in an
iOS app. iAd is a part of the iOS SDK so you do not need to rely on other third party libraries.
All the required libraries are already bundled in Xcode. You'll discover how easy it is to
integrate an iAd banner into your app in a minute. It only takes a few lines (or even a single
line) of code to start making a profit from your app.
There is no better way to learn the iAd integration than by trying it out. As usual, we'll work on
a sample project and then add a banner ad. The sample app is similar to the one we built in
earlier chapter. You can download the Xcode project template from
https://www.dropbox.com/s/r504aapiy822jmr/iAdDemoTemplate.zip?dl=0.
iAdDemo
suggest you compile and run the project template so that you have a basic idea of the demo
app; it's a simple table-based app that displays a list of articles. We will tweak it to show an
advertisement to earn some extra revenue. You do not need to enroll in the Apple Developer
179
Program before testing iAd. The ad service can send test advertisements to help you verify that
your integration is correct.
To integrate iAd into your app, the first thing you need to do is add the iAd framework into the
Xcode project. Select
Build Phases
iAdDemo
. Expand
iAd framework, click the + button and search for iAd. Select iAd.framework and click the Add
button to add the framework into the project.
In the class that uses iAd, you should add an import statement in order to import the
framework. For our demo app, we will display a banner advertisement in the table view. Open
the
ArticleTableViewController.swift
beginning:
import iAd
UIViewController
allows you to access additional properties that are specifically designed for
iAd.
To display a banner ad at the default position, all you need to do is set the
canDisplayBannerAds
property of
UIViewController
to
true
resizing the content, grabbing the banner ad from Apple, and displaying a banner ad below the
180
content.
Now open
ArticleTableViewController.swift
viewDidLoad
method:
canDisplayBannerAds = true
Just one line of code? Yes, you read it correctly. That's all you need to add an ad banner.
Once you set the property to
true
the content is resized based on the size of the banner. For our demo app, you'll see that the
table view has been resized appropriately. Try to run the demo app and play around with it.
For testing purpose, the iAd framework will load a test ad.
181
Not only can you include banner ads in your apps, but
UIViewController
ADInterstitialPresentationPolicy
property. The property is set to none by default, which means no interstitial ads are allowed.
According to Apple's documentation, you can set the policy to automatic and the framework
automatically displays an interstitial ad on screen. The frequency of ad presentation is,
however, completely controlled by the iAd framework. You have no idea when the ad is
displayed.
Alternatively, you can set the policy to manual. You then call the
requestInterstitialAdPresentation
For example, let's say you want to display an interstitial ad 30 seconds after the app is
launched. We can implement it like this. First, insert the following lines of code in the
viewDidLoad
method of the
ArticleTableViewController
class:
interstitialPresentationPolicy = .Manual
UIViewController.prepareInterstitialAds()
.Manual
fetch and download an advertisement. The second line of the code tells the framework to
prepare for the ad presentation. To display an interstitial ad, we create a helper method:
func displayInterstitialAds() {
if displayingBannerAd {
canDisplayBannerAds = false
}
requestInterstitialAdPresentation()
canDisplayBannerAds = true
}
We first disable the current banner ad if it's active. Then we call the
requestInterstitialAdPresentation
the banner ad. To trigger an interstitial ad at a certain time, we will create and configure an
NSTimer
viewDidLoad
182
method:
The
NSTimer
class takes several parameters. Here we specify the fire date (i.e. the time at
which the time is triggered) to 30 seconds since now. When it's fired, the time will call the
displayInterstitialAds method to display the ad. We set the repeats option to false, since we
only want to display the interstitial ad once.
Now you're ready to test the app. After launching the app, wait for 30 seconds and you should
see a full-screen ad.
Quick note: If you can't make it work on the simulator (e.g. iPhone 6), try to
test the app on a real device or select an alternate simulator (e.g. iPhone
5s).
canDisplayBannerAds
property provides the easiest way to display a banner ad. That said,
it limits the position of banner ad to the bottom of the content. What if you want to display the
183
banner ad in other positions such as above the content or on the top part of the screen? In this
case you can't just rely on the
canDisplayBannerAds
on your own.
Indeed it's not difficult to render the banner ad with your own code. As a demonstration, we'll
modify the sample project and see how we can display the banner ad as a section header of the
table view. First, remove the following line code from the
iAdDemoTableViewController.swift
viewDidLoad:
method of
canDisplayBannerAds = true
Next, since we're going to display the banner ad using our own implementation, implement
ADBannerViewDelegate
ArticleTableViewController
class:
class ArticleTableViewController: UITableViewController, ADBannerViewDelegate {
var adBannerView:ADBannerView?
var isAdDisplayed = false
The
ADBannerView
class provides a view to display banner ads on the screen. Later we'll use the
iAdDisplayed
property is a Boolean
variable indicating whether the ad is loaded and displayed properly. We'll use this value to
determine whether we should hide/show the ad. Any events that occur during the display of a
banner ad are communicated via the
ArticleTableViewController
ADBannerViewDelegate
protocol. Here,
of the methods defined by the protocol to handle certain ad events (e.g the ad has loaded
successfully).
Open the
ArticleTableViewController.swift
viewDidLoad
adBannerView
delegate:
adBannerView = ADBannerView(adType: ADAdType.Banner)
adBannerView?.delegate = self
184
This tells the table view to use the ad banner view as the section header. Now you're ready to
go! Hit the Run button and test the app. Wait a few seconds and you should see the banner ad.
Show/Hide Banner Ad
Great! You've managed to change the ad position. But wouldn't it be even better if the app only
displayed the banner ad when it's available? Presently, the app displays a blank ad at the time
when it's first launched - it can take up to 10 seconds before the ad is shown properly.
To enhance the user experience, it would be great if the banner ad was displayed only when it
has been completely loaded. Until then, we just hide the banner ad. Let's tweak the demo
project and make the app even better.
185
ADBannerViewDelegate
protocol. Actually,
the banner view calls its delegate when a new ad is loaded or when an error occurs. Insert the
following code in the
ArticleTableViewController
class:
We use the
heightForHeaderInSection
iAdDisplayed=true
the banner view (e.g. 50 points for ads in portrait orientation). Otherwise, the height is set to
zero. By setting the height to zero, the app will not be displayed in the section header. In other
words, the banner ad is hidden. The
bannerViewDidLoadAd
isAdDisplayed
value to
true
heightForHeaderInSection
method will be
didFailToReceiveAdWithError:
186
method is called.
isAdDisplayed
value to
false
ad.
Alright, the app is ready to test. When the app is first launched, the banner is not shown. Once
the ad is ready, the section header is resized to display the banner. To simulate the error case,
you can disable your Wi-Fi or unplug your LAN cable. Wait 10-20 seconds till the ad reloads,
and you should see the message Failed to load banner ad shown in the console as the banner
ad is automatically hidden. Try to turn your iPhone sideways; the demo app can also handle
banner ads in the landscape orientation.
For your reference, you can download the complete Xcode project from
https://www.dropbox.com/s/4yyodyltdmpnvhi/iAdDemo.zip?dl=0.
187
Chapter 16
Working with Custom Fonts
When you add a label to a view, Xcode allows you to change the font type using the Attribute
inspector. From there, you can pick a system font or custom font from the pre-defined font
family.
What if you can't find any font from the default font family that fits into your app? Typography
is an important aspect of app design. Proper use of a typeface makes your app superior, so you
may want to use some custom fonts that are created by third parties but not bundled in Mac
OS. Just perform a simple search on Google and you'll find tons of free fonts for app
development. However, this still leaves you with the problem of bundling the font in your
Xcode project. You may think that we can just add the font file into the project, but it's a little
more difficult than that. In this chapter, I'll focus on how to bundle new fonts and go through
the procedures with you.
188
As always, I'll give you a demo and build the demo app together. The demo app is very simple;
it just displays a set of labels using different custom fonts. You can start by building the demo
from scratch or downloading the template from
https://www.dropbox.com/s/knfrdrwaxmyggtk/CustomFontTemplate.zip?dl=0.
189
When Xcode prompts you for confirmation, make sure to check the box of your targets (i.e.
CustomFont) and enable the Copy items if needed option. This instructs Xcode to copy the font
files to your app's folder. If you have this option unchecked, your Xcode project will only add a
reference to the font files.
190
The font files are usually in .ttf or .otf format. Once you add all the files, you should find them
in the project navigator under the font folder.
191
button next to Item 0 to add another font file. Repeat the same step
until all the font files are registered - you'll end up with a screenshot like the one shown below.
Make sure you key in the file names correctly. Otherwise, you won't be able to use the fonts.
UILabel
UIFont
object
. Here is an
example:
label1.font = UIFont(name: "Mohave-Italic", size: 25.0)
label2.font = UIFont(name: "Hallo sans", size: 30.0)
label3.font = UIFont(name: "CanterLight", size: 35.0)
viewDidLoad
method of the
ViewController
the app, all the labels should change to the specified custom fonts accordingly.
For starters, you may have a question in your mind: how can you find out the font name? It
seems that the font names differ from the file names.
That's a very good observation. When initializing a
193
UIFont
name instead of the filename of the font. To find the name of the font, you can right-click a font
file in Finder and select Get Info from the context menu. The value displayed in the Full Name
field is the font name used in UIFont. In the sample screenshot, the font name is CanterBold.
For your reference, you can download the complete project from
https://www.dropbox.com/s/aj9qf428wvx5sbo/CustomFont.zip?dl=0.
194
Chapter 17
Working with AirDrop and
UIActivityViewController
AirDrop is Apple's answer to file and data sharing. Prior to iOS 7, users had to rely on thirdparty apps like Bump to share files between iOS devices. Since the release of iOS 7, iOS users
are allowed to use a feature called AirDrop to share data with nearby iOS devices. In brief, the
feature allows you to share photos, videos, contacts, URLs, Passbook passes, app listings on the
App Store, media listings on iTunes Store, location in Maps, etc.
Wouldn't it be great if you could integrate AirDrop into your app? Your users could easily share
195
photos, text files, or any other type of document with nearby devices. The
UIActivityViewController
class bundled in the iOS SDK makes it easy for you to embed
AirDrop into your apps. The class shields you from the underlying details of file sharing. All
you need to do is tell the class the objects you want to share and the controller handles the rest.
In this chapter, we'll demonstrate the usage of
UIActivityViewController
196
AirDrop doesn't just work with the Photos app. You can also share items in your Contacts,
iTunes, App Store, and Safari browser, to name a few. If you're new to AirDrop, you should
now have a better idea of how it works.
Let's see how we can integrate AirDrop into an app to share various types of data.
UIActivityViewController Overview
You might think it would take a hundred lines of code to implement the AirDrop feature.
Conversely, you just need a few lines of code to embed AirDrop. The
UIActivityViewController
UIActivityViewController
standard services, such as copying items to the clipboard, sharing content to social media sites,
sending items via Messages, etc. Since iOS 7, the class adds the support of AirDrop sharing. In
iOS 8 or later, the activity view controller adds the support of app extensions. However, we will
197
UIActivityViewController
and then present the controller on screen. Here is the code snippet:
let objectsToShare = [fileURL]
let activityController = UIActivityViewController(activityItems:
objectsToShare, applicationActivities: nil)
presentViewController(activityController, animated: true, completion: nil)
As you can see, with just three lines of code you can bring up an activity view with the AirDrop
option. Whenever there is a nearby device detected, the activity controller automatically
displays the device and handles the data transfer if you choose to share the data. By default,
the activity controller includes sharing options such as Messages, Flickr and Sina Weibo.
Optionally, you can exclude these types of activities by setting the
excludedActivityTypes
UIActivityViewController
, and
NSURL
NSURL
String
UIImage
app. When you transfer a PDF file, the other device will open it in Safari. If you just share a
String
Demo App
To give you a better idea of
UIActivityViewController
usual. Once again, the app is very simple. When it is launched, you'll see a table view listing a
few files including image files, a PDF file, a document, and a Powerpoint. You can tap a file and
view its content in the detail view. In the content view, there is a toolbar at the bottom of the
198
screen. Tapping the Share action button in the toolbar will bring up the AirDrop option for
sharing the file with a nearby device.
To keep you focused on implementing the AirDrop feature, you can download the project
template from https://www.dropbox.com/s/74hrip2eefprkbq/AirdropDemoTemplate.zip?
dl=0. After downloading the template, open it and have a quick look.
The project template already includes the storyboard and the custom classes. The table view
controller is associated with
connected with
makes use of
FileTableViewController
FileDetailViewController
UIWebView
. The
FileDetailViewController
object simply
First, let's go to the storyboard. Drag a toolbar from the Object library to the detail view
controller and place it at the bottom part of the controller. Select the default bar button item
and change its identifier to
Action
this:
Next you'll need to add some layout constraints for the toolbar, otherwise, it will not be
properly displayed on some devices. In the layout bar, click the Issues button. Choose the Add
Missing Constraints option under the Selected Views section. Interface Builder will then
generate the layout constraints for the toolbar.
The constraints added ensure the toolbar is always displayed at the bottom part of the view
200
controller. And the leading and trailing edges are aligned with the view.
Now go back to
FileDetailViewController
Go back to
Main.storyboard
and connect the Share button with the action method. Control-
drag from the Share button to the view controller icon of the scene dock, and select
share:
method of the
FileDetailViewController
201
The above code should be very familiar to you; we discussed it at the very beginning of the
chapter. The code creates an instance of
UIActivityViewController
activities and presents the controller on the screen. The tricky part is how you define the
objects to share.
The
filename
property of
FileDetailViewController
to first find the full path of the file before passing it to the activity view controller. In the
project template, I already include a helper method for this purpose:
func fileToURL(file: String) -> NSURL? {
// Get the full path of the file
let fileComponents = file.componentsSeparatedByString(".")
if let filePath = NSBundle.mainBundle().pathForResource(fileComponents[0],
ofType: fileComponents[1]) {
return NSURL(fileURLWithPath: filePath)
}
return nil
}
glico.jpg
will be transformed to
file:///Users/simon/Library/Application%20Support/iPhone%20Simulator/8.1/Applications/A53
21493-318A-4A3B-8B37-E56B8B4405FC/AirdropDemo.app/glico.jpg
on the device you're running. But the URL should begin with the
file://
UIActivityViewController
AirDrop sharing.
That's all you need to implement AirDrop sharing. You're now ready to test the app. Compile
and run it on a real iPhone. Yes, you need a real device to test AirDrop sharing; the sharing
feature will not work in a simulator. Furthermore, you need to have at least two iOS devices or
a Mac running Mac OS Yosemite to test the sharing feature.
Once you launch the app, select a file, tap the Share action button, and enable AirDrop. Make
sure the receiving device has AirDrop enabled. The app should recognize the receiving device
for file transfer.
For reference, you can download the complete Xcode project from
https://www.dropbox.com/s/l1sv18bumf12frh/AirdropDemo.zip?dl=0.
may prompt you to pick an app for opening the file or open it directly in iBooks. How can iOS
know which app to use for a particular data type?
UTIs (short for Uniform Type Identifiers) is Apple's answer to identifying data within the
system. In brief, a uniform type identifier is a unique identifier for a particular type of data or
file. For instance, com.adobe.pdf represents a PDF document and public.png represents a PNG
image. You can find the full list of registered UTIs here. An application that is capable of
opening a specific type of file will be registered to handle that UTI with the iOS. So whenever
that type of file is opened, iOS hands off that file to the specific app.
The system allows multiple apps to register the same UTI. In this case, iOS will prompt user
with the list of capable apps for opening the file. For example, when you share a document, the
receiving device will prompt a menu for user's selection.
Summary
204
AirDrop is a very handy feature, which offers a great way to share data between devices. Best of
all, the built-in
UIActivityViewController
support in their apps. As you can see from the demo app, you just need a few lines of code to
implement the feature. I highly recommend you to integrate this sharing feature into your app.
205
Chapter 18
Building Grid Layouts Using Collection
Views
If you have no idea about what grid-like layout is, just take a look at the built-in Photos app.
The app presents photos in grid format. Before Apple introduced
UICollectionView
, you had
to write a lot of code or make use of third-party libraries to build a similar layout.
UICollectionView
, in my opinion, is one of the most spectacular APIs in the iOS SDK. Not only
can it simplify the way to arrange visual elements in a grid layout, it even lets developers
customize the layout (e.g. circular, cover flow style layout) without changing the data.
206
In this chapter, we will build a simple app to display a collection of recipe photos in grid layout.
Here is what you're going to learn:
An introduction to
How to use
UICollectionView
UICollectionView
UITableView
class. While
UITableView
manages a collection of data items and displays them on screen in a single-column layout, the
UICollectionView
layouts. You can present items in multi-column grids, tiled layout, circular layout, etc.
207
UICollectionViewFlowLayout
into a grid with optional header and footer views for each section. Later, we'll use the layout
class to build the demo app.
The UICollectionView is composed of several components:
Cells instances of
UICollectionViewCell
. Like
UITableViewCell
, a cell represents a
single item in the data collection. The cells are the main elements organized by the
associated layout. If
UICollectionViewFlowLayout
like format.
Supplementary views Optional. It's usually used for implementing the header or footer
views of sections.
Decoration views think of it as another type of supplementary view but for decoration
purpose only. The decoration view is unrelated to the data collection. We simply create
decoration views to enhance the visual appearance of the collection view.
Like the table view, you have two ways to implement a collection view. You can add a collection
view (UICollectionView) to your user interface and provide an object that conforms to the
UICollectionViewDataSource
UICollectionViewDataSource
protocol.
CollectionViewDemo
, set the device to iPhone and make sure you select Swift for the
Main.storyboard
in the project
navigator. Delete the default view controller and drag a Collection View Controller from the
Object library to the storyboard. The controller already has a collection view built-in. You
should see a collection view cell in the controller, which is similar to the prototype cell of a
208
table view.
Under the Attributes inspector, set the collection view controller as the initial view controller.
Next, open the Document Outline and select the collection view. Under the Size inspector,
change the width and height of the cell to
cells and for lines to
10
100
points.
The for cells value defines the minimum spacing between items in the same row, while the for
lines value defines the minimum spacing between successive rows.
Next, select the collection view cell and set the identifier to
209
Cell
Now drag an image view from the Object library to the cell. You then manually resizes the
image view and makes it fit the cell. As usual, you will need to add some layout constraints for
the image view. Selec the image view, click the Pin button in the layout bar and add 4 spacing
contraints (refer to the figure below for details).
Lastly, embed the collection view controller in a navigation controller. Go up to the Xcode
menu, select Editor > Embed In > Navigation Controller. Set the title of the navigation bar to
Recipes
. That's it. We have completed the user interface design. The next step is to create the
custom classes for the collection view controller and the collection view cell.
In the project navigator, delete ViewController.swift file that was generated by Xcode. We do
not need it because we will create our own classes. Right click the CollectionViewDemo folder
and select
class
New File...
. Create a new class using the Cocoa Touch Class template. Name the
RecipeCollectionViewCell
UICollectionViewCell
Repeat the process to create another class. Name the new class
RecipeCollectionViewController
open the
RecipeCollectionViewCell.swift
UICollectionViewController
. Now
an outlet variable for the image view. Your class should look like this:
class RecipeCollectionViewCell: UICollectionViewCell {
@IBOutlet var recipeImageView:UIImageView!
}
Go back to storyboard and select the collection view cell. Under the Identity inspector, change
the custom class to
RecipeCollectionViewCell
Next, select the collection view controller (Recipes). Under the Identity inspector, set the
custom class to
RecipeCollectionViewController
UICollectionView
UITableView
. To populate
data in a table view, all you have to do is implement two methods defined in the
UITableViewDataSource
protocol. Like
UITableView
, the
UICollectionViewDataSource
protocol
defines a number of data source methods to interact with the collection view. You have to
implement at least two methods:
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection
section: Int) -> Int
func collectionView(_ collectionView: UICollectionView, cellForItemAtIndexPath
indexPath: NSIndexPath) -> UICollectionViewCell
recipeImages
array in the
RecipeCollectionViewController
class:
212
viewDidLoad
view cell for reuse purpose. Since we already use a prototype cell in storyboard, this line of
code is no longer required. Remove it from the
viewDidLoad
method:
self.collectionView!.registerClass(UICollectionViewCell.self,
forCellWithReuseIdentifier: reuseIdentifier)
Similar to what you did when implementing a table view, update these data source methods of
the
UICollectionViewDataSource
The
numberOfSectionsInCollectionView
images.
The
collectionView(_:cellForItemAtIndexPath:_)
collectionView
to dequeue a
dequeueReusableCellWithReuseIdentifier
method
will either automatically create a cell or return a cell from the re-use queue. The cell returned is
213
RecipeCollectionViewCell
Quick note: If you run the app on iPhone 6/6 Plus simulator, the result will be
slightly difference. I will explain it in the later chapter.
UICollectionViewCell
lets
214
by
100
pixel. In order to frame the recipe photo, we'll first resize the image view of the
and Y to
Main.storyboard
90
and
72
pixels respectively.
As the size of the image view is changed, the existing layout constraints no longer apply. In the
Document Outline, you should notice a yellow arrow indicating that there are some issues with
the constraints. Click the arrow and then click the yellow indicator to bring up a menu. Select
Update constraints and click Fix Misplacement to update the constraints.
RecipeCollectionViewController.swift
collectionView(_:cellForItemAtIndexPath:_)
return cell
file. In the
We simply load the photo frame image and set it as the background view of the collection view
215
cell. Now compile and run the app again. Your app now displays a photo frame for each of the
cell item.
216
Chapter 19
Interacting with Collection Views
UICollectionView
how to display items in a collection view. However, you may not know how to interact with the
collection view cells. As mentioned before, a collection view works pretty much like a table
view. To give you a better idea, I will show you how to interact with the collection view cells,
specially about how to handle single and multiple item selections.
We'll continue to improve the collection view demo app. Here is what we're going to
implement:
217
When a user taps a recipe photo, the app will bring up a modal view and display the photo
in a larger size.
We'll also implement Facebook sharing in the app in order to demonstrate multiple item
selections. Users are allowed to select multiple photos and share them on Facebook.
Let's first see how to implement the first feature to handle single selection. When the user taps
any of the recipe photos, the app will bring up a modal view to display the photo in a higher
resolution. If you didn't go through the previous, you can start by downloading the project
template from https://www.dropbox.com/s/o1anhhgdxmoxz1p/CollectionViewDemo.zip?
dl=0.
First, let's design the view controller that is used to display the recipe photo. Go to
Main.storyboard
, drag a View Controller from the Object library to the storyboard. Then add
600
and
400
Aspect Fill
and enable
Clip
Lastly, embed it in a navigation controller and add a Bar Button Item to the navigation bar.
Change the title of the navigation bar to
identifier of the button item to
Done
Photo
image view. Simply select the image view object. Click the Issues button of the layout bar,
followed by choosing the Add Missing Constraints option. Your view controller should look
similar to the screenshot below.
218
Since we want to display the view controller when a user taps any of the recipe photos in the
collection view, we have to connect the collection view with the view controller using a segue.
Control-drag from the cell of the collection view in the Document Outline to the navigation
controller we just added. Select
showRecipePhoto
Present Modally
219
When the user taps the Done button in the Photo View Controller, the controller will be
dismissed. In order to do that, we will add an unwind segue. In
RecipeCollectionViewController.swift
Go back to the storyboard. Control-drag from the Done button to the Exit icon of the scene
dock. Select
unwindToHome:
current view controller is dismissed, the user will be brought back to the collection view
controller.
If you compile and run the app, you'll end up with an empty view when selecting any of the
220
recipe photos. Tapping the Done button will dismiss the view.
Since we haven't written any code, the modal view controller knows nothing about the selected
recipe photo. Create a new class and name it
UIViewController
PhotoViewController
. Make it a subclass of
PhotoViewController
class:
In Interface Builder, select the view controller we just created and set the custom class to
PhotoViewController
photoImageView
outlet variable.
Data Passing
In order to let other controllers pass the image name, we'll add an
PhotoViewController
imageName
property in the
To load the specified recipe image in the image view, change the
221
viewDidLoad
method of
PhotoViewController
to the following:
Now the
PhotoViewController
class should be able to load the recipe image in the image view -
but there's still one thing left. How can we identify the selected item of the collection view and
pass the image name to the PhotoViewController?
If you understand how data passing works via a segue, you know we must implement the
prepareForSegue
method in the
RecipeCollectionViewController
RecipeCollectionViewController.swift
class. Select
Just like
UITableView
, the
UICollectionView
indexPathsForSelectedItems
method that returns the index paths of the selected items. You may wonder why multiple index
paths are returned. The reason is that
UICollectionView
the index paths corresponds to one of the selected items. For this demo, we only have single
item selection. Therefore, we just pick the first index path, retrieve the selected image, and
pass it to the photo view controller.
When a user taps a collection cell in the collection view, the cell changes to the highlighted
state and then to the selected state. The last line of code is to deselect the selected item once
the image is displayed in the modal view controller.
222
Now, let's build and run the app. After the app is launched, tap any of the recipes. You should
see a modal view showing the selected recipe image.
supports both single and multiple selections. By default, the app only allows
allowsMultipleSelection
property of the
UICollectionView
class controls whether multiple items can be selected simultaneously. To enable multiple
selections, the trick is to set the property to
true
To give you a better idea of how multiple selections work, we'll continue to tweak the demo
app. Users are allowed to select multiple recipes and share them on Facebook in the following
ways:
A user taps the Share button in the navigation bar. Once the sharing starts, the button title
is automatically changed to Upload.
The user selects the recipe photos to share.
After selection, the user taps the Upload button. The app will bring up a dialog for sharing
the photos on Facebook.
We'll first add the Share button in the navigation bar of Recipe Collection View Controller. Go
to
Main.storyboard
, drag a Bar Button Item from the Object library, and put it in the
In
RecipeCollectionViewController.swift
223
As usual, go to Interface Builder. Establish a connection between the Share button and the
shareButtonTapped
shareButton
outlet.
The demo app now offers two modes: single selection and multiple selections. When a user
taps the Share button, the app goes into multiple selection mode. This allows users to select
multiple photos for sharing. To support multiple selection mode, we'll add two variables in the
RecipeCollectionViewController
shareEnabled
true
class:
, it indicates the Share button was tapped and multiple selection is enabled.
selectedRecipes
The
UICollectionViewDelegate
selection and highlight items in a collection view. When a user selects an item, the
collectionView(_:didSelectItemAtIndexPath:_)
selectedRecipes
RecipeCollectionViewController
class:
224
// Check if the sharing mode is enabled, otherwise, just leave this method
guard shareEnabled else {
return
}
// Determine the selected items by using the indexPath
let selectedRecipe = recipeImages[indexPath.row]
// Add the selected item into the array
selectedRecipes.append(selectedRecipe)
}
The
UICollectionViewCell
selectedBackgroundView
for
setting the background view of a selected item. To indicate a selected item, we'll change the
background image of a collection cell. I've included the photo-frame-selected.png file in the
project template. If you didn't use the template, you can download the image from
https://www.dropbox.com/s/owpr02wk24aq7n7/photo-frame-selected.png?dl=0, and add it
to the image asset. Edit the
collectionView(_:cellForItemAtIndexPath:_)
Now when a user selects a recipe item, the selected cell will be highlighted.
Not only do we have to handle item selection, we also need to account for deselection. A user
225
may deselect an item from the collection view. When an item is deselected, it should be
removed from the
selectedRecipes
array.
RecipeCollectionViewController
class:
shareButtonTouched
called when a user taps the Share button. Update the method to the following code:
@IBAction func shareButtonTapped(sender: AnyObject) {
if shareEnabled {
// Post selected photos to Facebook
if selectedRecipes.count > 0 {
if
SLComposeViewController.isAvailableForServiceType(SLServiceTypeFacebook) {
let facebookComposer = SLComposeViewController(forServiceType:
SLServiceTypeFacebook)
facebookComposer.setInitialText("Check out my recipes!")
for recipePhoto in selectedRecipes {
facebookComposer.addImage(UIImage(named: recipePhoto))
}
presentViewController(facebookComposer, animated: true,
completion: nil)
}
}
// Deselect all selected items
for indexPath in collectionView?.indexPathsForSelectedItems() as!
[NSIndexPath] {
226
guard
mode is enabled. If not, we'll put the app into sharing mode and enable multiple selections. To
enable multiple selections, all you need to do is set the
true
allowsMultipleSelection
guard
property to
statement check
if the user has selected at least one recipe. If no recipe is selected, we just leave the method and
do nothing.
When the app is in sharing mode (i.e.
shareEnabled
is set to
true
Upload button, we'll bring up the Facebook composer. The iOS SDK allows you to integrate the
social sharing feature in your app via the
SLComposeViewController
SLComposeViewController
class. The
various social networking services such as Facebook and Twitter. Here we use some of its builtin methods to upload multiple photos to Facebook. We first use the
isAvailableForServiceType
device. If it's configured, we create a Facebook composer and attach the selected images using
the
addImage
presentViewController
composer on screen.
227
After uploading the photos to Facebook, we deselect the selected items and remove them from
the
selectedRecipes
array. Lastly, we switch back to single selection mode and change the
button title.
Because
SLComposeViewController
RecipeCollectionViewController.swift
import Social
The app is almost ready. However, if you run the app now, you will end up with a bug. After
switching to sharing mode, the modal view still appears when you select any of the recipe
photos - the result is not what we expected. The segue is invoked every time a collection view
cell is tapped. Obviously, we don't want to trigger the segue when the app is in sharing mode.
We only want to trigger the segue when it's in single selection mode. The
shouldPerformSegueWithIdentifier
229
Chapter 20
Adaptive Collection Views Using Size
Classes and UITraitCollection
In the previous two chapters, you learned to build a demo app using a collection view. The app
works perfectly on iPhone 5/5s. But if you run the app on iPhone 6/6 Plus, your screen should
look like the screenshot shown below. The recipes are displayed in grid format but with a large
space between items. The screen of iPhone 6 and 6 Plus is wider than that of their
predecessors. As the size of the collection view cell was fixed, the app rendered extra space
between cells according to the screen width of the test device.
230
So how can we fix the issue? As mentioned in the first chapter of the book, iOS 8/9 comes with
a new concept called Adaptive User Interfaces. You will need to make use of Size Classes and
UITraitCollection to adapt the collection view to a particular device and device orientation. If
you haven't read Chapter 1, I would recommend you to take a pause here and go back to the
first chapter. Everything I will cover here is based on the material covered in the very
beginning of the book.
As usual, we will build a demo app to walk you through the concept. You are going to create an
app similar to the one before but with the following changes:
The cell is adaptive - The size of the collection view cell changes according to a particular
device and orientation. You will learn how to use size classes and UITraitCollection to
make the collection view adaptive.
The app is universal - It is a universal app that supports both iPhone and iPad.
We will use
UICollectionView
- Instead of using
UICollectionView
UICollectionViewController
, you will
Assuming you've downloaded the template, open the project in Xcode and go to
Main.storyboard
. Drag a Collection View object from the Object library to the View Controller.
232
10
128
by
128
yellow
points. The insets define the margins applied to the content of the
section. Because we are using a Collection View instead of Collection View Controller, we have
to deal with auto layout constraints on our own. The simplest way to do this is to select the
collection view and then click the Issues button of the auto layout menu, followed by selecting
the Add Missing Constraints option. Xcode automatically defines the constraints for you.
Next, select the collection view cell and set its identifier to
Cell
inspector. Drag an image view to the cell and resize it to make it fit the cell. Again, click the
Issues button of the auto layout menu and select the Add Missing Constraints option to define
the layout constraints.
233
CustomCollectionViewCell
. Once the file was created, declare an outlet variable for the image view:
Switch to the storyboard. Select the collection view cell and change its custom class (under the
Identity inspector) to
imageView
The
CustomCollectionViewCell
ViewController
want to present a set of images using the collection view, we have to implement both the
UICollectionViewDataSource
and
UICollectionViewDelegate
234
protocols.
Next, declare an array for the images and an outlet variable for the collection view:
var doodleImages = ["DoodleIcons-1", "DoodleIcons-2", "DoodleIcons-3",
"DoodleIcons-4", "DoodleIcons-5", "DoodleIcons-6", "DoodleIcons-7",
"DoodleIcons-8", "DoodleIcons-9", "DoodleIcons-10", "DoodleIcons-11",
"DoodleIcons-12", "DoodleIcons-13", "DoodleIcons-14", "DoodleIcons-15",
"DoodleIcons-16", "DoodleIcons-17", "DoodleIcons-18", "DoodleIcons-19",
"DoodleIcons-20"]
@IBOutlet var collectionView:UICollectionView!
Like what we did before, we will have to implement two required methods of the
UICollectionViewDataSource
protocol:
The above code is very straightforward. We return the total number of images in the first
method and set the image of the image view in the latter method.
Now switch over to the storyboard. Establish a connection between the collection view and the
235
collectionView outlet variable. Also, connect the dataSource and delegate with the view
controller.
That's it! We're ready to test the app. Compile and run the app on iPhone 5/5s simulator. The
app looks pretty good, right? Now try to test the app on other iOS devices including the iPad
and in landscape orientation. The app looks great on most devices but falls short on iPhone 6
and 6 Plus.
UICollectionView
fits its contents according to the cell size. As you can see below, the number of columns varies
depending on the screen size of a particular device. In portrait mode, the screen width of an
iPhone 6 and iPhone 6 Plus is 370 points and 414 points respectively. If you do a simple
calculation for the iPhone 6 Plus (e.g. [414 - 20 (margin) - 20 (cell spacing)] / 128 = 2.9), you
should understand why it can only display cells in two columns, leaving a large gap between
columns.
236
237
if isPhone {
if orientation.isPortrait {
// Change cell size
}
}
Starting from iOS 8, the above code is not ideal. You're discouraged from using
UIUserInterfaceIdiom
to verify the device type. Instead, you should use size classes to handle
issues related to idiom and orientation. I covered size classes in Chapter 1, so I won't go into
the details here. In short, it boils down to this two by two grid:
userInterfaceIdiom
UITraitEnvironment
UITraitEnvironment
). This is a new
UIViewController
conforms
protocol, you can access the current trait collection through the
238
traitCollection
viewDidLoad
method to
You should have something like this when running the app on an iPhone 6 Plus:
<UITraitCollection: 0x7fae9a435a60; _UITraitNameUserInterfaceIdiom = Phone,
_UITraitNameDisplayScale = 3.000000, _UITraitNameHorizontalSizeClass = Compact,
_UITraitNameVerticalSizeClass = Regular, _UITraitNameTouchLevel = 0,
_UITraitNameInteractionModel = 1, _UITraitNameForceTouchCapability = 1>
From the above information, you discover the device is an iPhone that is in the Compact
horizontal and Regular vertical size classes. The display scale of 3x indicates a Retina HD 5.5
display.
size of a cell:
collectionView(_:layout:sizeForItemAtIndexPath:)
All you need to do is override the method and return the cell size at runtime. Recall that we
only want to alter the cell size for iPhones in portrait mode, so we will implement the method
like this:
func collectionView(collectionView: UICollectionView, layout
collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath:
NSIndexPath) -> CGSize {
let sideSize = (traitCollection.horizontalSizeClass == .Compact &&
traitCollection.verticalSizeClass == .Regular) ? 80.0 : 128.0
return CGSize(width: sideSize, height: sideSize)
}
For devices with a Compact horizontal and a Regular vertical size class (i.e. iPhone Portrait),
239
we set the size of the cell to 80x80 points. Otherwise, we just keep the cell size the same. Run
the app again on an iPhone 6 / 6 Plus. It should look much better now.
When the size of the view is about to change (e.g. rotation), UIKit will call the method. Here we
simply update the collection view by reloading its data. Now test the app again. When your
iPhone is put in landscape mode, the cell size should be changed accordingly.
240
Your Exercise
In some scenarios, you may want all images to be visible in the collection view without
scrolling. In this case, you'll need to perform some calculations to adjust the cell size based on
the area of the collection view. To calculate the total area of the collection view, you can use the
code like this:
let collectionViewSize = collectionView.frame.size
let collectionViewArea = Double(collectionViewSize.width *
collectionViewSize.height)
With the total area and total number of images, you can calculate the new size of a cell. For the
rest of the implementation, I will leave it as an exercise for you. Take some time and try to
241
242
Chapter 21
Building a Today Widget
In iOS 8, Apple introduced app extensions, which let you extend functionality beyond your app
and make it available to users from other parts of the system (such as from other apps or the
Notification Center). For example, you can provide a widget for users to put in Notification
Center. This widget can display the latest information from your app (i.e. weather, sports
scores, stock quotes, etc.).
iOS defines different types of extensions, each of which is tied to an area of the system such as
the keyboard, Notification Center, etc. A system area that supports extensions is called an
243
244
When an extension is running, it doesn't run in the same process as the container app. Every
instance of your extension runs as its own process. It is also possible to have one extension run
in multiple processes at the same time. For example, let's say you have a sharing extension
which is invoked in Safari. An instance of the extension, a new process, is going to be created to
serve Safari. Now, if the user goes over to Mail and launches your share extension, a new
process of the extension is created again. These two processes don't share address space.
An extension cannot communicate directly with its container app, nor can it enable
communication between the host app and container app. However, indirect communication
with its container app is possible via either
of
NSUserDefaults
openURL()
to store data which both extension and container apps can read and write
to.
focused on building an extension instead of creating an app from scratch, I have provided a
starter project that you can download at
https://www.dropbox.com/s/s1eu2fsoobboypo/WeatherDemo.zip?dl=0. The project is a
simple weather app, showing the various weather information of a particular location. You will
need an internet connection for the data to be fetched. The app is very simple and doesn't
include any geoLocation functionality. The default location is assumed to be Paris, France. The
app, however, provides a setting screen for altering the default location. It relies on a free API
provided by openweather.org to aggregate weather information. The API returns weather data
of a particular location in JSON format. If you have no idea about JSON parsing in iOS, refer to
Chapter 4 for details.
When you open the app, you should see a visual that shows the weather information for the
default location. You can simply tap the menu button to change the location.
We are going to create a Today extension of the app that will show a brief summary of the
weather in the Today View. You'll also learn how to share data between the container app and
246
extension. We'll use this shared data to let a user choose the location they want weather
information about.
class to retrieve the latest weather information. You can replicate the files in
both targets. But this is not a good practice. When developing an app or an extension, you
should always consider code reuse.
To allow for code reuse, you create an embedded framework, which can be used across both
targets. You can place the common code that will need to be used by both the container app
and extension in the framework.
In the demo app, both the extension and container app make a call to a weather API and
retrieve the weather data. Without using a framework we would have to duplicate the code,
which would be inefficient and difficult to maintain.
247
WeatherInfoKit
Finish
248
Swift
You will see a new target appear in the list of targets as well as a new group folder in the
Project Navigator. When you expand the
WeatherInfoKit.h
WeatherInfoKit
. If you are using Objective-C, or if you have any Objective-C files in your
framework, you will have to include all public headers of your frameworks here. Because we're
now using Swift, we do not need to edit this file.
Next, on the General tab of the
check
WeatherInfoKit
249
8.1
because this
You should note that app extensions are somewhat limited in what they can do and therefore
not all Cocoa Touch APIs are available for use in extensions. For instance extensions cannot do
the following:
Access the camera or microphone on an iOS device
Receive data using AirDrop (however they can send data using AirDrop)
Perform long-running background tasks
Use any API marked in header files with the
NS_EXTENSION_UNAVAILABLE
macro, similar
WeatherData
WeatherService.swift
and
WeatherData.swift
WeatherService
class is a common
service class that is responsible for calling up the weather API and parsing the returned JSON
data.
250
WeatherInfoKit
WeatherInfoKit
Merely dragging a file from one target to another doesn't make the file part of that target - you
have to change the file's target membership yourself. To do this, select the
WeatherService.swift
file from the Project Navigator. Then open the File Inspector and
change the files target in the Target Membership section by unchecking the
and checking the
Because the
WeatherInfoKit
WeatherService
and
WeatherData
Weather
WeatherData.swift
ViewController.swift
target
file.
WeatherDemo
If you're new to Swift, it's important to know that it provides three access levels for entities in
your code: public, internal and private. By default, all entities (e.g. classes, variables) are
defined with the internal access level. That means the entities can only be used within any
source file from the same module/target. Now that the
WeatherService
and
WeatherData
ViewController
of the
WeatherDemo
target can no longer access both classes as the access level of the classes is set to
internal.
To resolve the error, we have to change the access level of these classes to public.
Public access allows entities to be used in source files from another module. When you're
developing a framework, typically, your classes should be accessible by source files of any
modules. In this case, you use public access to specify the public interface of a framework.
Therefore, open
WeatherData.swift
public
declaration:
public class WeatherData: NSObject {
public var temperature:Int = 0
public var weather:String = ""
}
Apply the same change to the class, method and typealias declarations of
WeatherService.swift
ViewController.swift
statement at the top of the file to import the framework we just created:
252
import WeatherInfoKit
Now compile the project again. You should be able to run the WeatherDemo without errors.
The app is still the same but the common files are now put into a framework.
Next
Weather Widget
253
At this point you should see a prompt asking if you want to activate the
scheme. Press
Activate
Weather Widget
. Another Xcode scheme has been created for you and you can switch
schemes by navigating to Product > Scheme and then selecting the scheme you want to switch
to. You can also switch schemes from the Xcode toolbar.
Next, select
WeatherDemo
Weather Widget
+
from the Project Navigator. From the list of available targets, select
8.1
press
Add
254
and
NSExtension
MainInterface. If you don't want to use the storyboard file provided by the template, you will
have to change this value to the name of your storyboard file. For this demo, we just keep it
intact.
Open
MainInterface.storyboard
Let's have a quick test before redesigning the widget. To run the extension, make sure the
Weather Widget
255
A window will pop up, allowing you to choose an app to run. This lets Xcode know which host
app to run. Choose
Today
. With this selection, iOS will know to open Notification Center in the
Today view, which in turn launches your widget. Notification Center is the Today Extension's
host app. Click Run and you should see the widget on your simulator's/device's Notification
Center.
To display the weather data in Today view, we first redesign the Today View Controller in
storyboard like this:
256
All you need to do is delete the Hello World label and add the City, Weather, and Temperature
labels. Remember to set the number of lines for the labels to zero and define auto layout
constraints so that it fits for multiple screen resolutions. If you're not familiar with auto layout,
the easiest way is to let Xcode add the rules for you. Once you place the labels in the view
controller, select City and click the Issues button in the layout bar. Select the Add Missing
Constraints option. Xcode then automatically adds the rules for you.
Now go to the
TodayViewController.swift
import WeatherInfoKit
Next, declare three outlet variables and an instance variable for the default location in
TodayViewController.swift
Go back to
MainInterface.storyboard
the Document Outline. Connect the outlet variables with the labels.
257
To present the weather information in the widget's view controller, insert the
method in
TodayViewController.swift
viewDidAppear
WeatherInfoKit
framework that we
created earlier to secure the weather information. To enable the widget to update its view when
it's off-screen, make the following changes to the
widgetPerformUpdateWithCompletionHandler
method:
func widgetPerformUpdateWithCompletionHandler(completionHandler:
((NCUpdateResult) -> Void)) {
// Perform any setup necessary in order to update the view.
// If an error is encountered, use NCUpdateResult.Failed
258
The method is called automatically to give you an opportunity to update the widget's content.
Here we retrieve the latest weather information for our location. If it updates successfully, the
function calls the system-provided completion block with the
NCUpdateResult.NewData
enumeration. If the update wasn't successful, then the existing snapshot is used, which is
indicated by
NCUpdateResult.NoData
Now compile and run the widget on iOS 9. You will end up with this exception in the console:
Optional(Error Domain=NSURLErrorDomain Code=-1022 "The resource could not be
loaded because the App Transport Security policy requires the use of a secure
connection." UserInfo={NSUnderlyingError=0x7fcab15f3860 {Error
Domain=kCFErrorDomainCFNetwork Code=-1022 "(null)"},
NSErrorFailingURLStringKey=http://api.openweathermap.org/data/2.5/weather?
appid=bd82977b86bf27fb59a04b61b657fb6f&units=metric&q=Paris,%20France,
NSErrorFailingURLKey=http://api.openweathermap.org/data/2.5/weather?
appid=bd82977b86bf27fb59a04b61b657fb6f&units=metric&q=Paris,%20France,
NSLocalizedDescription=The resource could not be loaded because the App
Transport Security policy requires the use of a secure connection.})
App Transport Security is first introduced in iOS 9. The purpose of the feature is to improve
the security of connections between an app and web services by enforcing some of the best
practices. One of them is the use of secure connections. With ATS, all network requests should
259
now be sent over HTTPS. If you make a network connection using HTTP, ATS will block the
request and display the error. For the API provided by openweathermap.org, it only comes
with the support of HTTP. To resolve the issue, one way is to opt out of App Transport
Security. To do so, you need to add a specific key in the widget's Info.plist to disable ATS.
Select
Info.plist
under the
Weather Widget
content in a property list editor. To add a new key, right click the editor and select Add Row.
For the key column, enter
NSAllowsArbitraryLoads
YES
NSAppTransportSecurity
Boolean
. By setting
Dictionary
NSAllowsArbitraryLoads
to
Now run the app again. It should be able to load the widget. The weather widget should look
like this:
260
. To enable data sharing you have to enable app groups for the containing app
App Groups
button to
create a new container and give it a unique name. Commonly, the name starts with
set the name to
group.com.appcoda.weatherdemo
. Select the
Weather Widget
group
.I
the above procedures to set the App Groups. Don't create a new container for it though - use
the one you had created for the WeatherDemo target.
After you enable app groups, an app extension and its containing app can both use the
NSUserDefaults
LocationTableViewController.swift
The LocationTableViewController class is the controller for handling the location selection. To
enable data sharing, we create a new
NSUserDefaults
262
selectedLocation = location
defaults.setValue(selectedLocation, forKey: "location")
}
tableView.reloadData()
}
if let
TodayViewController.swift
In the
widgetPerformUpdateWithCompletionHandler
the beginning:
// Get the location from defaults
if let defaultLocation = defaults.valueForKey("location") as? String {
location = defaultLocation
}
NSUserDefaults
user. Now we are ready to test the widget again. Run the app and change the default location.
Once the location is set, activate the Notification Center to review the weather widget, which
should be updated according to your preference.
263
For reference, you can download the complete Xcode project from
https://www.dropbox.com/s/do6x0dquqwzbknq/WeatherDemoFinal.zip?dl=0.
264
Chapter 22
Building Slide Out Sidebar Menus
In this chapter I will show you how create a slide-out navigation menu similar to the one you
find in the Gmail app. If you're unfamiliar with slide out navigation menus, take a look at the
figure above. Ken Yarmost (http://kenyarmosh.com/ios-pattern-slide-out-navigation/) gave a
good explanation and defined it as follows:
Slide-out navigation consists of a panel that slides out from underneath the left or the
right of the main content area, revealing a vertically independent scroll view that serves
as the primary navigation for the application.
265
The slide-out sidebar menu (also known as a hamburger menu) has been around for a few
years now. It was first introduced by Facebook in 2011. Since then it has become a standard
way to implement a navigation menu. The slide-out design pattern lets you build a navigation
menu in your apps but without wasting the screen real estate. Normally, the navigation menu
is hidden behind the front view. The menu can then be triggered by tapping a list button in the
navigation bar. Once the menu is expanded and becomes visible, users can close it by using the
list button or simply swiping left on the content area.
Lately, there are some debates (https://lmjabreu.com/post/why-and-how-to-avoidhamburger-menus/) about this kind of menu that it doesn't provide a good user experience
and less efficient. In most cases, you should prefer tab bars over sidebar menus for navigation.
Being that said, you can still easily find this design pattern in some popular content-related
apps, including Google Maps, Pocket, LinkedIn, etc. The purpose of this chapter is not to
discuss with you whether you should kill the hamburger menu. There are already a lot of
discussions out there:
Kill The Hamburger Button (http://techcrunch.com/2014/05/24/before-the-hamburgerbutton-kills-you/)
Why and How to Avoid Hamburger Menus by Luis Abreu
(https://lmjabreu.com/post/why-and-how-to-avoid-hamburger-menus/)
Hamburger vs Menu: The Final AB Test (http://exisweb.net/menu-eats-hamburger)
So our focus in this chapter is on how. I want to show you how to create a slide-out sidebar
menu using a free library.
You can build the sidebar menu from the ground up. But with so many free pre-built solutions
on GitHub, we're not going to build it from scratch. Instead, we'll make use of a library called
SWRevealViewController (https://github.com/John-Lluch/SWRevealViewController).
Developed by John Lluch, this excellent library provides a quick and easy way to put up a slideout navigation menu in your apps. Best of all, the library is available for free.
The library was written in Objective-C. By going through the tutorial, you will also learn how to
use Objective-C in a Swift project.
SWRevealViewController
. This app is
very simple but not fully functional. The primary purpose of this app is to walk you through the
implementation of slide-out navigation menu. The navigation menu will work like this:
The user triggers the menu by tapping the list button at the top-left of navigation bar.
The user can also bring up the menu by swiping right on the main content area.
Once the menu appears, the user can close it by tapping the list button again.
The user can also close the menu by dragging left on the content area.
SWRevealViewController
for building a sidebar menu, you create a container view controller, which is actually an empty
267
view controller, to hold both the menu view controller and a set of content view controllers.
I have already created the menu view controller for you. It is just a static table view with three
menu items. There are three content view controllers for displaying news, maps, and photos.
For demo purposes, there are three content view controllers, and they show only static data. If
you need to have a few more controllers, simply insert them into the storyboard.
All icons and images are included in the project template (credit: thanks to Pixeden for the free
icon).
SWRevealViewController
menu. To begin, download the library from GitHub (https://github.com/JohnLluch/SWRevealViewController/archive/master.zip) and extract the zipped file - you should
see the
SWRevealViewController
SWRevealViewController.h
and
SWRevealViewController.m
.swift
library was written in Objective-C; the file extension differs from that
New Group
SWRevealViewController
group. When
prompted, make sure theCopy items if needed option is checked. As soon as you confirm to
add the files, Xcode prompts you to configure an Objective-C bridging header.
By creating the header file, you'll be able to access the Objective-C code from Swift. Click Creat
Bridging Header to proceed. Xcode then generates a header file named
Header.h
under the
SWRevealViewController
SidebarMenu-Bridging-
SidebarMenu-Bridging-Header.h
By adding the header file of SWRevealViewController, our Swift project will be able to access
the Objective-C library. This is all you need to do when using an external library written in
269
Objective-C.
SWRevealViewController
SWRevealViewController
object with a front and a rear view controller using segues. The front view controller is the
main controller for displaying content. In our storyboard this is the navigation controller
which associates with a view controller for presenting news. The rear view controller is the
controller that shows the navigation menu. Here it is the Sidebar View Controller.
Go to the storyboard. First, select the empty view controller (i.e. container view controller) and
change its class to
SWRevealViewController
Next, control-drag from SWRevealViewController to the Menu view controller. After releasing
the button, you will see a context menu for segue selection. In this case, select
controller set segue
270
reveal view
sw_rear
SWRevealViewController
SWRevealViewControllerSetSegue
that the menu view controller is the rear view controller. This means
that the sidebar menu will be hidden behind a content view controller.
Next, repeat the same procedures to connect
SWRevealViewController
271
when
sw_front
SWRevealViewController
that the
272
If your app works properly, let's continue with the implementation. If it doesn't work properly,
go back to the beginning of the chapter and work through step-by-step to figure out where you
went wrong.
Open
NewsTableViewController.swift
viewDidLoad
if revealViewController() != nil {
menuButton.target = revealViewController()
menuButton.action = "revealToggle:"
view.addGestureRecognizer(self.revealViewController().panGestureRecognizer())
}
The
SWRevealViewController
parent
SWRevealViewController
revealViewController()
to get the
revealToggle:
method to handle the expansion and contraction of the sidebar menu. As you know, Cocoa uses
the target-action mechanism for communication between a control and another object. We set
273
the target of the menu button to the reveal view controller and action to the
method. So when the menu button is tapped, it will call the
revealToggle:
revealToggle:
method to display
Lastly, we add a gesture recognizer. Not only you can use the menu button to bring out the
sidebar menu, but the user can swipe the content area to activate the sidebar as well.
Cool! Let's compile and run the app in the simulator. Tap the menu button and the sidebar
menu should appear. You can hide the sidebar menu by tapping the menu button again. You
can also open the menu by using gesture. Try to swipe right on the content area and see what
you get.
274
Main.storyboard
. First, select the map cell. Control-drag from the map cell to
the navigation controller of the map view controller, and then select the
controller push controller
reveal view
segue under Selection Segue. Repeat the procedure for the News
and Photos items, but connect then with the navigation controllers of the news view controller
and photos view controller respectively.
The custom
SWRevealViewControllerSeguePushController
switching of the controllers. Similarly, insert the following lines of code in the
method of
MapViewController.swift
and
PhotoViewController.swift
menu:
if revealViewController() != nil {
menuButton.target = revealViewController()
menuButton.action = "revealToggle:"
275
viewDidLoad
view.addGestureRecognizer(self.revealViewController().panGestureRecognizer())
}
That's it! Hit the Run button and test out the app.
rearViewRevealWidth
method of
NewsTableViewController
viewDidLoad
revealViewController().rearViewRevealWidth = 62
When you run the app, you should have a sidebar menu like the one shown below. You can
look into the
SWRevealViewController.h
276
In the demo storyboard, it already comes with an Extra menu view controller. The procedures
to add a right sidebar is very similar to what we have already did. The trick is to change the
identifier of the segue from
In
Main.storyboard
sw_rear
to
, control-drag from
sw_right
SWRevealViewController
sw_right
. This tells
that the Extra menu view controller should be slided from right.
Now drag a bar button item to the News view controller, and place it at the right side of the
navigation bar. In the Identity inspector, set the System Item option to
277
Organize
NewsTableViewController.swift
In the
viewDidLoad
view.addGestureRecognizer
method call:
revealViewController().rightViewRevealWidth = 150
extraButton.target = revealViewController()
extraButton.action = "rightRevealToggle:"
rightRevealToggle:
150
revealToggle:
target
Organize
it! Run the project again. Now the app has a right sidebar.
278
For your reference, you can download the final project from
https://www.dropbox.com/s/ax0zhk655j204zu/SidebarMenu.zip?dl=0.
279
Chapter 23
View Controller Transitions and
Animations
Wouldn't it be great if you could define the transition style between view controllers? Apple
provides a handful of default animations for view controller transitions. Presenting a view
controller modally usually uses a slide-up animation. The transition between two view
controllers in navigation controller is predefined too. Pushing or popping a controller from the
navigation controller's stack uses a standard slide animation. In older versions of iOS, there
was no easy way to customize the transitions of two view controllers. Starting from iOS 7, iOS
developers are allowed to implement our own transitions through the View Controller
280
Transitioning API. The API gives you full control over how one view controller presents
another.
There are two types of view controller transitions: interactive and non-interactive. In iOS 7 (or
up), you can pan from the leftmost edge of the screen and drag the current view to the right to
pop a view controller from the navigation controller's stack. This is a great example of
interactive transition. In this chapter, we are going to focus on the non-interactive transition
first, as it is easier to understand.
The concept of custom transition is pretty simple. You create an animator object (or so called
transition manager), which implements the required custom transition. This animator object
is called by the UIKit framework when one view controller starts to present or transit to
another. It then performs the animations and informs the framework when the transition
completes.
When implementing non-interactive view controller transitions, you basically deal with the
following protocols:
UIViewControllerAnimatedTransitioning
objects used to present and dismiss a view controller. Interestingly, you can provide
different animator objects to manage the transition between two view controllers.
UIViewControllerContextTransitioning
contextual information for transition animations between view controllers. You do not
need to adopt this protocol in your own class. Your animator object will receive the
context object, provided by the system, during the transition.
It looks a bit complicated, right? Actually, it's not. Once you go through a simple project, you
will have a better idea about how to build custom transitions between view controllers.
Demo Project
We are going to build a simple demo app. To keep your focus on building the animations,
download the project template from
281
If you have a trial run, you will end up with a screen similar to the one shown below.
Each icon indicates a unique custom transition. For now, the icons are not functional. Coming
up next, you will learn how to implement all the transitions using the View Controller
Transitioning API.
Main.storyboard
navigation controller) and the detail view controller showing product information. These two
controllers are not connected yet. Control-drag from the collection view cell of the transition
282
Present modally
If you run the demo app again, it will bring up the detail view controller using the standard
slide-up animation. What we are going to do is implement our own animator object to replace
that animation.
NSObject
UIViewControllerAnimatedTransitioning
and the
UIViewControllerTransitioningDelegate
protocol to vend the animator objects that manage the transition between view controllers.
You have to implement the following methods in the
283
SlideDownTransitionAnimator
class:
The
animationControllerForPresentedController
The
self
, as the
animationControllerForDismissedController
animator object to use when dismissing the view controller. In the above code, we simply
return the current animator object.
Okay, let's move onto the implementation of
UIViewControllerAnimatedTransitioning
protocol,
which provides the actual animation for the transition. When adopting the protocol, you have
to provide the implementation of the following required methods:
transitionDuration(_:)
animateTransition(_ transitionContext: UIViewControllerContextTransitioning)
The first method is simple. You just return the duration (in seconds) of the transition
animation. The second method is where the transition animations take place. When presenting
or dismissing a view controller, UIKit calls the
animateTransition
animations.
Before we dive into the code, let me explain how our own version of slide-down animation
works. Take a look at the illustration below.
284
When a user taps the Slide Down icon, the current view controller begins to slide down off the
screen. The detail view controller will also slide down from the top of the screen. When the
animation ends, the detail view controller completely replaces the current view controller.
Okay, how can we implement an animation like that in code? First, insert the following code
snippet in the
SlideDownTransitionAnimator
285
}
let offScreenUp = CGAffineTransformMakeTranslation(0, container.frame.height)
let offScreenDown = CGAffineTransformMakeTranslation(0,
container.frame.height)
// Make the toView off screen
toView.transform = offScreenUp
// Add both views to the container view
container.addSubview(fromView)
container.addSubview(toView)
// Perform the animation
UIView.animateWithDuration(duration, delay: 0.0, usingSpringWithDamping:
0.8, initialSpringVelocity: 0.8, options: [], animations: {
fromView.transform = offScreenDown
fromView.alpha = 0.5
toView.transform = CGAffineTransformIdentity
}, completion: { finished in
transitionContext.completeTransition(true)
})
}
At the beginning, we set the transition duration to 0.5 seconds. The first method simply
returns the duration.
Let's take a closer look at the
animateTransition
view controllers involved: the current view controller and the detail view controller. When
UIKit calls the
animateTransition
UIViewControllerContextTransitioning
From the context object, we can retrieve the view controllers involved in the transition using
the
viewControllerForKey
that appears at the start of the transition, is referred to as the "from view controller". The detail
view controller, which is going to replace the current view controller, is referred to as the "to
view controller".
We then configure two transforms for moving the views. To implement the slide-down
286
animation,
toView
offScreenUp
fromView
during the transition. The context object also provides a container view that acts as the
superview for the view involved in the transition. It is your responsibility to add both views to
the container view using the
Lastly, we use the
addSubview
animateWithDuration
method.
method of
transform to
fromView
UIView
fromView
and
toView
. By applying the
toView
ViewController.swift
animator object:
let slideDownTransition = SlideDownTransitionAnimator()
prepareForSegue
method:
The app only performs the slide-down transition when the user taps the Slide Down icon, so
we first verify whether the first cell is selected. When the cell is selected, we set our
SlideDownTransitionAnimator
Now compile and run the app. Tap on the Slide Down icon, you should get a nice slide-down
transition to the detail view. However, the reverse transition doesn't work properly when you
tap on the close button.
287
The resulting view, after transition, is dimmed. Obviously, the alpha value is not restored to
the original value. And we expect the main view controller slides from the bottom of the screen
instead of from the top.
isPresenting
This variable keeps track of whether we're presenting the view controller or dismissing one.
Update the
animationControllerForDismissedController
288
and
animationControllerForPresentedController
We simply set
false
isPresenting
to
true
animateTransition
method as
shown below:
func animateTransition(transitionContext: UIViewControllerContextTransitioning)
{
// Get reference to our fromView, toView and the container view
let fromView =
transitionContext.viewForKey(UITransitionContextFromViewKey)!
let toView = transitionContext.viewForKey(UITransitionContextToViewKey)!
// Set up the transform we'll use in the animation
guard let container = transitionContext.containerView() else {
return
}
let offScreenUp = CGAffineTransformMakeTranslation(0, container.frame.height)
let offScreenDown = CGAffineTransformMakeTranslation(0,
container.frame.height)
// Make the toView off screen
if isPresenting {
toView.transform = offScreenUp
}
// Add both views to the container view
container.addSubview(fromView)
container.addSubview(toView)
289
fromView
fromView
and
toView
290
toView
isPresenting
variable is set to
true
is set to
true
. But for
isPresenting
reverse transition, we perform a different animation. We move the detail view (i.e.
off the screen by applying the
position and its
alpha
offScreenUp
value is reset to
1.0
transform. For
toView
fromView
Now run the app again. When you close the detail view, the animation should work like this.
291
292
The detail view controller is first moved off the screen to the left. When the user taps on the
Slide Right icon, the detail view slides into the screen to replace the main view. This time we
keep the main view intact.
Okay, let's go into the implementation. In the project navigator, create a new class named
SlideRightTransitionAnimator
SlideRightTransitionAnimator
NSObject
. Update the
293
transitionContext.viewForKey(UITransitionContextFromViewKey)!
let toView =
transitionContext.viewForKey(UITransitionContextToViewKey)!
// Set up the transform we'll use in the animation
guard let container = transitionContext.containerView() else {
return
}
let offScreenLeft = CGAffineTransformMakeTranslation(container.frame.width, 0)
// Make the toView off screen
if isPresenting {
toView.transform = offScreenLeft
}
// Add both views to the container view
if isPresenting {
container.addSubview(fromView)
container.addSubview(toView)
} else {
container.addSubview(toView)
container.addSubview(fromView)
}
// Perform the animation
UIView.animateWithDuration(duration, delay: 0.0,
usingSpringWithDamping: 0.8, initialSpringVelocity: 0.8, options: [],
animations: {
if self.isPresenting {
toView.transform = CGAffineTransformIdentity
} else {
fromView.transform = offScreenLeft
}
}, completion: { finished in
transitionContext.completeTransition(true)
})
}
func animationControllerForPresentedController(presented: UIViewController,
presentingController presenting: UIViewController, sourceController source:
UIViewController) -> UIViewControllerAnimatedTransitioning? {
isPresenting = true
294
return self
}
func animationControllerForDismissedController(dismissed: UIViewController)
-> UIViewControllerAnimatedTransitioning? {
isPresenting = false
return self
}
}
The code is very similar to the one we developed previously. The changes are highlighted in
yellow.
First, we move the detail view (i.e.
offScreenLeft
transform. If the
placed on top of
by
toView
fromView
toView
isPresenting
variable is set to
fromView
true
toView
should be
fromView
) should be placed
transform
property to
CGAffineTransformIdentity
toView
), we
fromView
Before testing the animation, there is still one thing left. We need to hook up this animator to
the transition delegate. Open the
slideRightTransition
ViewController.swift
variable:
Change the
prepareSegue
switch
switch selectedIndexPaths[0].row {
case 0: toViewController.transitioningDelegate = slideDownTransition
case 1: toViewController.transitioningDelegate = slideRightTransition
default: break
}
Now when you run the project, you should see a slide right transition when tapping the Slide
Right icon.
295
Similar to the slide animation, to implement the pop animation, the detail view (i.e.
toView
) is
first minimized. Once a user taps the Pop icon, the detail view grows in size till it is restored to
its original size.
Now create a new class and name it
of
NSObject
PopTransitionAnimator
296
return duration
}
func animateTransition(transitionContext:
UIViewControllerContextTransitioning) {
// Get reference to our fromView, toView and the container view
let fromView =
transitionContext.viewForKey(UITransitionContextFromViewKey)!
let toView =
transitionContext.viewForKey(UITransitionContextToViewKey)!
// Set up the transform we'll use in the animation
guard let container = transitionContext.containerView() else {
return
}
let minimize = CGAffineTransformMakeScale(0, 0)
let offScreenDown = CGAffineTransformMakeTranslation(0,
container.frame.height)
let shiftDown = CGAffineTransformMakeTranslation(0, 15)
let scaleDown = CGAffineTransformScale(shiftDown, 0.95, 0.95)
// Change the initial size of the toView
toView.transform = minimize
// Add both views to the container view
if isPresenting {
container.addSubview(fromView)
container.addSubview(toView)
} else {
container.addSubview(toView)
container.addSubview(fromView)
}
// Perform the animation
UIView.animateWithDuration(duration, delay: 0.0,
usingSpringWithDamping: 0.8, initialSpringVelocity: 0.8, options: [],
animations: {
if self.isPresenting {
fromView.transform = scaleDown
fromView.alpha = 0.5
toView.transform = CGAffineTransformIdentity
} else {
fromView.transform = offScreenDown
toView.alpha = 1.0
toView.transform = CGAffineTransformIdentity
}
297
}, completion: { finished in
transitionContext.completeTransition(true)
})
}
func animationControllerForPresentedController(presented: UIViewController,
presentingController presenting: UIViewController, sourceController source:
UIViewController) -> UIViewControllerAnimatedTransitioning? {
isPresenting = true
return self
}
func animationControllerForDismissedController(dismissed: UIViewController)
-> UIViewControllerAnimatedTransitioning? {
isPresenting = false
return self
}
}
Again I will not walk you through the code line by line because you should now have a better
understanding of the view controller transition. The logic is very similar to that of the previous
two examples. Here we just define a different set of transforms. For example, we use
CGAffineTransformMakeScale
In the animation block, when presenting the detail view, the main view (i.e.
fromView
) is
shifted down a little bit and reduced in size. In the case of dismissing the detail view, we simply
move the detail view off the screen.
Now go to the
ViewController.swift
popTransitionAnimator
Update the
switch
switch selectedIndexPaths[0].row {
case 0: toViewController.transitioningDelegate = slideDownTransition
case 1: toViewController.transitioningDelegate = slideRightTransition
case 2: toViewController.transitioningDelegate = popTransition
default: break
298
variable:
Now hit the Run button to test out the transition. When you tap the Pop icon, you will get a
nice pop animation.
The detail view is initially turned sideways and moved off the screen. When the transition
begins, the detail view swings back to the original position, while the main view rotates
counterclockwise and swings off the screen. Okay, let's first create a new class called
RotateTransitionAnimator
NSObject
like this:
class RotateTransitionAnimator: NSObject,
299
UIViewControllerAnimatedTransitioning, UIViewControllerTransitioningDelegate {
let duration = 0.5
var isPresenting = false
func transitionDuration(transitionContext:
UIViewControllerContextTransitioning?) -> NSTimeInterval {
return duration
}
func animateTransition(transitionContext:
UIViewControllerContextTransitioning) {
// Get reference to our fromView, toView and the container view
let fromView =
transitionContext.viewForKey(UITransitionContextFromViewKey)!
let toView =
transitionContext.viewForKey(UITransitionContextToViewKey)!
// Set up the transform we'll use in the animation
guard let container = transitionContext.containerView() else {
return
}
// Set up the transform for rotation
// The angle is in radian. To convert from degree to radian, use this
formula
// radian = angle * pi / 180
let rotateOut = CGAffineTransformMakeRotation(-90 * CGFloat(M_PI) /
180)
// Change the anchor point and position
toView.layer.anchorPoint = CGPoint(x:0, y:0)
fromView.layer.anchorPoint = CGPoint(x:0, y:0)
toView.layer.position = CGPoint(x:0, y:0)
fromView.layer.position = CGPoint(x:0, y:0)
// Change the initial position of the toView
toView.transform = rotateOut
// Add both views to the container view
container.addSubview(toView)
container.addSubview(fromView)
// Perform the animation
UIView.animateWithDuration(duration, delay: 0.0,
usingSpringWithDamping: 0.8, initialSpringVelocity: 0.8, options: [],
animations: {
if self.isPresenting {
300
fromView.transform
fromView.alpha = 0
toView.transform =
toView.alpha = 1.0
} else {
fromView.alpha = 0
fromView.transform
toView.alpha = 1.0
toView.transform =
}
= rotateOut
CGAffineTransformIdentity
= rotateOut
CGAffineTransformIdentity
}, completion: { finished in
transitionContext.completeTransition(true)
})
}
func animationControllerForPresentedController(presented: UIViewController,
presentingController presenting: UIViewController, sourceController source:
UIViewController) -> UIViewControllerAnimatedTransitioning? {
isPresenting = true
return self
}
func animationControllerForDismissedController(dismissed: UIViewController)
-> UIViewControllerAnimatedTransitioning? {
isPresenting = false
return self
}
}
Let's discuss the first code snippet highlighted in yellow. To build the animation, the first thing
that comes to your mind is to create a rotation transform using the
CGAffineTransformMakeRotation
which a positive value indicates a clockwise direction while a negative value specifies a counter
clockwise rotation. Here is an example:
let rotateOut = CGAffineTransformMakeRotation(-90 * CGFloat(M_PI) / 180)
If you apply the above transform to the detail view, you will rotate the view by 90 degrees
counter clockwise. However, the rotation happens around the center of the screen. Obviously,
301
to perform our expected animation, the detail view should be rotated around the top-left
corner of the screen.
By default, the anchor point of a view's layer (
CALayer
the value for this property using the unit coordinate space.
To change the anchor point to the top left corner of the layer, we set it to (0, 0) for both
fromView
and
toView
But why do we need to change the layer's position in addition to the anchor point? The layer's
position is set to the center of the view. For instance, if you are using iPhone 5, the position of
the layer is set to (160, 284). Without altering the position, you will end up with an animation
like this:
302
Since the layer's anchor point was changed to (0, 0) and the position is unchanged, the layer
moves so that its new anchor point is at the unchanged position. This is why we have to change
the position of both
fromView
and
toView
to (0, 0).
toView
fromView
and
toView
position and rotate the main view off the screen. We do the reverse when dismissing the detail
view.
Go to the
ViewController.swift
RotateTransitionAnimator
object:
let rotateTransition = RotateTransitionAnimator()
RotateTransitionAnimator
switch selectedIndexPaths[0].row {
case 0: toViewController.transitioningDelegate
case 1: toViewController.transitioningDelegate
case 2: toViewController.transitioningDelegate
case 3: toViewController.transitioningDelegate
=
=
=
=
303
slideDownTransition
slideRightTransition
popTransition
rotateTransition
object:
default: break
}
Now compile and run the project again. Tap the Rotate icon, and you will get an interesting
transition.
In this chapter, I showed you the basics of custom view controller transitions. Now it is time to
create your own animation in your apps. Good design is much more than visuals. Your app has
to feel right. By implementing proper and engaging view controller transitions, you will take
your app to the next level.
For reference, you can download the final project from
https://www.dropbox.com/s/8aiyts5yayxzdrt/NavTransition.zip?dl=0.
304
Chapter 24
Building a Slide Down Menu
Navigation is an important part of every user interface. There are multiple ways to present a
menu for your users to access the app's features. The sidebar menu that we discussed earlier is
an example. Slide down menu is another common menu design. When a user taps the menu
button, the main screen slides down to reveal the menu. The screen below shows a sample
slide down menu used in the older version of the Medium app.
If you have gone through the previous chapter, you should have a basic understanding of
custom view controller transition. In this chapter, you will apply what you have learned to
build an animated slide down menu.
305
As usual, I don't want you to start from scratch. You can download the project template from
https://www.dropbox.com/s/cflrrp9wy6v37f4/SlideMenuStart.zip?dl=0. It includes the
storyboard and view controller classes. You will find two table view controllers. One is for the
main screen (embedded in a navigation controller) and the other is for the navigation menu. If
you run the project, the app should present you the main interface with some dummy data.
Before moving on, take a few minutes to browse through the code template to familiarize
yourself with the project.
Main.storyboard
controllers, which are not connected with any segue yet. In order to bring up the menu when a
user taps the menu button, control-drag from the menu button to the menu table view
controller. Release the buttons and select
present modally
306
If you run the project now, the menu will be presented as a modal view. In order to dismiss the
menu, we will add an unwind segue. Open the
NewsTableViewController.swift
Next, go to the storyboard. Control-drag from the prototype cell of the Menu table view
controller to the exit icon. When prompted, select the
segue.
307
unwindToHome:
Now when a user taps any menu item, the menu controller will dismiss to reveal the main
screen. Through the
unwindToHome:
NewsTableViewController
) retrieves the menu item selected by the user and changes the title of
the navigation bar. To keep things simple, we just change the title of the navigation bar and
will not alter the content of the main screen.
However, the app can't change the title yet. The reason is that the
MenuTableViewController
currentItem
variable of the
MenuTableViewController
class:
308
currentItem
currentItem
navigation bar.
Now the app should be able to update the title of navigation bar. But there is still one thing left.
For example, say you select News in the menu, the app then changes the title to News. If you
tap the menu button again, the menu controller still highlights Home in white, instead of
News.
Let's fix the issue. In the
NewsTableViewController.swift
prepareForSegue
trasitioning to the menu view controller. Here we just update the current item of the controller,
so it can highlight the item in white.
Now compile and run the project. Tap the menu item and the app will present you the menu
modally. When you select a menu item, the menu will dismiss and the navigation bar title will
change accordingly.
309
and
UIViewControllerTransitioningDelegate
protocols. We are going to implement the class. But first, let's take a look at how the slide down
menu works.
When a user taps the menu, the main view begins to slide down until it reaches the predefined
location, which is 150 points away from the bottom of the screen. The below illustration should
give you a better idea of the sliding menu.
310
. In the project navigator, right click to create a new file. Name the class
and set it as a subclass of
NSObject
311
UIViewControllerContextTransitioning) {
// Get reference to our fromView, toView and the container view
let fromView =
transitionContext.viewForKey(UITransitionContextFromViewKey)!
let toView =
transitionContext.viewForKey(UITransitionContextToViewKey)!
// Set up the transform we'll use in the animation
guard let container = transitionContext.containerView() else {
return
}
let moveDown = CGAffineTransformMakeTranslation(0,
container.frame.height - 150)
let moveUp = CGAffineTransformMakeTranslation(0, -50)
// Add both views to the container view
if isPresenting {
toView.transform = moveUp
snapshot = fromView.snapshotViewAfterScreenUpdates(true)
container.addSubview(toView)
container.addSubview(snapshot!)
}
// Perform the animation
UIView.animateWithDuration(duration, delay: 0.0,
usingSpringWithDamping: 0.8, initialSpringVelocity: 0.8, options: [],
animations: {
if self.isPresenting {
self.snapshot?.transform = moveDown
toView.transform = CGAffineTransformIdentity
} else {
self.snapshot?.transform = CGAffineTransformIdentity
fromView.transform = moveUp
}
}, completion: { finished in
transitionContext.completeTransition(true)
if !self.isPresenting {
self.snapshot?.removeFromSuperview()
}
})
}
func animationControllerForPresentedController(presented: UIViewController,
presentingController presenting: UIViewController, sourceController source:
312
UIViewControllerAnimatedTransitioning
UIViewControllerTransitioningDelegate
and
as they are explained in the previous chapter. Let's focus on the animation block (i.e. the
animateTransition
method).
Referring to the illustration displayed earlier, during the transition, the main view is the
fromView
toView
To create the animations, we configure two transforms. The first transform (i.e.
used to move down the main view. The other transform (i.e.
moveUp
moveDown
) is
) is configured to move up
the menu view a bit so that it will also have a slide-down effect when restoring to its original
position. You will understand what I mean when you run the project later.
From iOS 7 and onwards, you can use the UIView-Snapshotting API to quickly and easily
create a light-weight snapshot of a view.
snapshot = fromView.snapshotViewAfterScreenUpdates(true)
By calling the
snapshotViewAfterScreenUpdates
With the snapshot, we can add it to the container view to perform the animation. Note that the
snapshot is added on top of the menu view.
For the actual animation when presenting the menu, the implementation is really simple. We
just apply the
moveDown
transform to the snapshot of the main view and restore the menu view
self.snapshot?.transform = moveDown
toView.transform = CGAffineTransformIdentity
When dismissing the menu, the reverse happens. The snapshot of the main view slides up and
returns to its default position. Additionally, the snapshot is removed from its super view so
that we can bring the actual main view back.
Now open
NewsTableViewController.swift
MenuTransitionManager
object:
In the
prepareForSegue
That's it! You can now compile and run the project. Tap the menu button and you will have a
slide down menu.
UIView
UITapGestureRecognizer
UITapGestureRecognizer
object
object, we need to
pass it the target object that is the recipient of action messages sent by the receiver, and the
action method to be called. Obviously, you can hardcode a particular object as the target object
to dismiss the view, but to keep our design flexible, we will define a protocol and let the
delegate object implement it.
In
MenuTransitionManager.swift
Here we define a
dismiss()
MenuTransitionManagerDelegate
. The beauty of a protocol is that you do not need to provide any implementation for
the methods. Instead the implementation is left to the delegate that implements the protocol.
In other words, the delegate should implement the
dismiss
In the
MenuTransitionManager
var delegate:MenuTransitionManagerDelegate?
Later, the object which is responsible to handle the tap gesture should be set as the delegate
object. Lastly, we need to create a
good way to do this is define a
snapshot
UITapGestureRecognizer
didSet
var snapshot:UIView? {
didSet {
if let delegate = delegate {
let tapGestureRecognizer = UITapGestureRecognizer(target: delegate,
action: "dismiss")
snapshot?.addGestureRecognizer(tapGestureRecognizer)
}
}
}
willSet
didSet
) is
called every time a property's value is set. This provides us a convenient way to perform certain
actions immediately before or after an assignment. The
the value is stored, while the
didSet
willSet
In the above code, we make use of the property observer to create a gesture recognizer and set
it to the snapshot. So every time we assign the snapshot variable an object, it will immediately
315
NewsTableViewController.swift
MenuTransitionManagerDelegate
the following:
class NewsTableViewController: UITableViewController,
MenuTransitionManagerDelegate
dismissViewControllerAnimated
method.
Lastly, insert a line of code in the
prepareForSegue
method of the
NewsTableViewController
Great! You're now ready to test the app again. Hit the Run button to try it out. You should be
able to dismiss the menu by tapping the snapshot of the main view.
316
By applying custom view controller transitions properly, you can greatly improve the user
experience and set your app apart from the crowd. The slide down menu is just an example, so
try to create your own animation in your next app.
For reference, you can download the final project from
https://www.dropbox.com/s/yxxuidy9veqiva8/SlideMenu.zip?dl=0.
317
Chapter 25
Self Sizing Cells and Dynamic Type
UITableView
this is seriously one of the most exciting features for the SDK. Prior to iOS 8, if you wanted to
display dynamic content in table view with variable height you would need to calculate the row
height manually. Now with iOS 8 and iOS 9, self sizing cells provide a solution for displaying
dynamic content. In brief, here is what you need to do when using self sizing cells:
Define auto layout constraints for your prototype cell
Specify the
estimatedRowHeight
Set the
rowHeight
UITableViewAutomaticDimension
With just two lines of code, you instruct the table view to calculate the cell's size to match its
content and render it dynamically. This self sizing cell feature should save you tons of code and
time. You're going to love it.
In the next section we'll develop a simple demo app to demonstrate self sizing cell. There is no
better way to learn a new feature than to use it. In addition to self sizing cell, I will also talk
about Dynamic Type. Dynamic Type was first introduced in iOS 7 - it allows users to customize
the text size to fit their own needs. However, only apps that adopt Dynamic Type respond to
the text change. As of right now, only a fraction of third-party apps have adopted the feature.
You're encouraged to adopt Dynamic Type so as to give your users the flexibility to change text
sizes, and to improve the user experience for vision-challenged users. Therefore, in the later
section you will learn how to adopt dynamic type in your apps.
319
As you can see, some of the addresses and descriptions are truncated; you may have faced the
same issue when developing table-based apps. To fix the issue, one option is to simply reduce
the font size or increase the number of lines of a label. However, this solution is not perfect. As
the length of the addresses and descriptions varies, it will probably result in an imperfect UI
with redundant white spaces. A better solution is to adapt the cell size with respect to the size
of its inner content. Prior to iOS 8, you would need to manually compute the size of each label
and adjust the cell size accordingly, which would involve a lot of code and subsequently a lot of
time.
Starting from iOS 8, all you need to do is use self sizing cells and the cell size can be adapted
automatically. Currently, the project template creates a prototype cell with a fixed height of 95
points. What we are going to do is turn the cells into self sizing cells so that the cell content can
be displayed perfectly.
systemLayoutSizeFittingSize
returns the size of the cell based on the layout constraints. If this is the first time you're
working with auto layout, I recommend that you quickly review chapter 1 about adaptive UI
before continuing.
For the project template I did not define any auto layout constraints for the prototype cell; let's
add a few constraints to the cell before beginning. Select the name label and click the Pin
button of the auto layout menu. Add three spacing constraints for the top, left, and right sides.
321
Once you release the button, Xcode shows a context menu. Hold down the shift key and select
the Vertical Spacing, Left and Right options, and then hit return to add the constraints.
This defines three layout constraints between the name and address labels:
A vertical spacing constraint between the name and address labels. For example, the name
label should be five points away from the address label.
The left side of the address label should align with that of the name label.
The right side of the address label should align with that of the name label.
Similarly, control-drag from the address label to the description label. Again, select the
Vertical Spacing, Left, and Right options in the context menu. This defines a similar set of
layout constraints for the address and description labels.
322
Lastly, we have to define a spacing constraint between the description label and the bottom
part of the cell's content view. Control-drag from the description label to the content view of
the cell.
Once releasing the button, select the Bottom space to container margin option. After you
configured all the layout constraints, Xcode should detect several auto layout issues. Click the
disclosure arrow in the Interface Builder outline view and you will see a list of the issues. Click
the warning or error symbol to fix the issue (either by adding the missing constraints or
updating the frame).
323
If you have configured the constraints correctly, your final layout should look similar to this:
method of
HotelTableViewController
tableView.estimatedRowHeight = 95.0
tableView.rowHeight = UITableViewAutomaticDimension
estimatedRowHeight
rowHeight
property to
property to
95
UITableViewAutomaticDimension
row height in iOS 9. In other words, you ask table view to figure out the appropriate cell size
based on the provided information.
If you test the app now, the cells are still not resized. This is because all labels are restricted to
display one line only. Select the Name label and set the number of lines under the attributes
inspector to
. By doing this, the label should now adjust itself automatically. Repeat the
324
Once you made the changes, you can run the project again. This time the cells should be
resized properly with respect to the content.
325
Self sizing cells are particularly useful to support Dynamic Type. You may not have heard of
Dynamic Type but you probably see the setting screen (Settings > General > Accessibility >
Larger Text or Settings > Display & Brightness > Text Size) shown below.
Dynamic Type was first introduced in iOS 7 - it allows users to customize the text size to fit
their own needs. However, only apps that adopt dynamic type respond to the text change. I
believe most of the users are not aware of this feature because only a fraction of third-party
apps have adopted the feature. Starting from iOS 8, Apple wants to encourage developers to
adopt Dynamic Type. All of the system applications have already adopted Dynamic Type and
the built-in labels automatically have dynamic fonts. When the user changes the text size, the
size of labels are going to change as well.
Furthermore, the introduction of Self Sizing Cells is a way to facilitate the widespread adoption
of Dynamic Type. It saves you a lot of code from developing your own solution to adjust the
row height. Once the cell is self-sized, adopting Dynamic Type is just a piece of cake.
326
Headline
Body
Subhead
detect some auto layout issues. Just click the disclosure indicator on the Interface Builder
outline menu to fix the issues.
That's it. Before testing the app, you should first change the text size. In the simulator, go to
Settings > General > Accessibility > Larger Text and enable the Larger Accessibility Sizes
option. Drag the slider to set to your preferred font size.
327
Now run the app and it should adapt to the text size change.
328
notification is posted when the user changes the preferred content size setting.
All you need to do now is add an observer in the
HotelTableViewController
viewDidLoad
method of the
class:
NSNotificationCenter.defaultCenter().addObserver(self, selector:
"onTextSizeChange:", name: UIContentSizeCategoryDidChangeNotification, object:
nil)
329
onTextSizeChange
When the text size changes, we simply reload the table view to refresh the content. As we use
custom table view cells, we have to explicitly set the fonts each time when the cells are loaded.
Update the
tableView(_:cellForRowAtIndexPath:)
The
preferredFontForTextStyle
method of
UIFont
the user's current content size for a specified text style. Here are the available text style
options:
UIFontTextStyleHeadline
UIFontTextStyleSubheadline
UIFontTextStyleBody
UIFontTextStyleFootnote
330
UIFontTextStyleCaption1
UIFontTextStyleCaption2
Lastly, add the deinit method to remove the observer:
deinit {
NSNotificationCenter.defaultCenter().removeObserver(self)
}
Quick note: A deinitializer, with the keyword deinit, is called immediately
before a class instance is deallocated. When the table view controller is
deallocated, we remove the observer.
Now you can test the app again. When the app is launched in the simulator, press
command+shift+h to go back to the home screen. Then go to Settings > General >
Accessibility > Larger Text and enable the Larger Accessibility Sizes option. Drag the slider to
set to change the text size.
Once changed, press
SelfSizingCell
command+shift+h
UIFontDescriptor
to get a font descriptor for a given text style. Based on the font
UIFont
. Here is an example:
var bodyFontDescriptor =
UIFontDescriptor.preferredFontDescriptorWithTextStyle(UIFontTextStyleBody)
let bodyFont = UIFont(name: "Avenir-Medium", size:
bodyFontDescriptor.pointSize)
cell.descriptionLabel.font = bodyFont
UIFontDescriptor
preferredFontDescriptorWithTextStyle
that
returns a font descriptor of a given text style. In the sample, we ask for the font descriptor of
body text style (
UIFontTextStyleBody
font with respect to the current content size setting. With the font size, we can create our own
331
font using
UIFont
This solution works, but there is an even better way to apply your preferred fonts with
Dynamic Type. Swift offers a feature called extensions that allows developers to add new
functionality to an existing class. You can even extend the functionality of a built-in class (e.g.
UIFont
Objective-C.
We will create an extension for
UIFont
preferredCustomFontForTextStyle
import UIKit
statement:
extension UIFont {
class func preferredCustomFontForTextStyle (textStyle: NSString) -> UIFont
{
let font =
UIFontDescriptor.preferredFontDescriptorWithTextStyle(textStyle as String)
let fontSize: CGFloat = font.pointSize
if textStyle == UIFontTextStyleHeadline {
return UIFont(name: "AvenirNext-Medium", size: fontSize)!
} else if (textStyle == UIFontTextStyleSubheadline) {
return UIFont(name: "Avenir-Medium", size: fontSize)!
} else {
return UIFont(name: "Avenir-Light", size: fontSize)!
}
}
}
The method simply returns a custom font based on the given text style. For instance, we used
AvenirNext-Medium font for the headline text style. You can use the method of the extension
just like any other methods. Simply call up the
method instead of
UIFont.preferredCustomFontForTextStyle
UIFont.preferredFontForTextStyle
tableView(_:cellForRowAtIndexPath:)
. So update the
332
cell.nameLabel.text = hotel.name
cell.addressLabel.text = hotel.address
cell.descriptionLabel.text = hotel.description
// Set the font style
cell.nameLabel.font =
UIFont.preferredCustomFontForTextStyle(UIFontTextStyleHeadline)
cell.addressLabel.font =
UIFont.preferredCustomFontForTextStyle(UIFontTextStyleSubheadline)
cell.descriptionLabel.font =
UIFont.preferredCustomFontForTextStyle(UIFontTextStyleBody)
return cell
}
Now compile and run the project again. The app should use the custom font instead of the
system font. For reference, you can download the final project from
https://www.dropbox.com/s/q5qtvw1adk83jhv/SelfSizingCellDynamicType.zip?dl=0.
333
Chapter 26
XML Parsing and RSS
One of the most important tasks that a developer has to deal with when creating applications is
data handing and manipulation. Data can be expressed in many different formats, and
mastering at least the most common of them is a key ability for every single programmer.
Speaking of mobile applications specifically now, it's quite common nowadays for them to
exchange data with web applications. In such cases, the way that data is expressed may vary,
but usually uses either the JSON or the XML format.
The iOS SDK provides classes for handling both of them. For managing JSON data, there is the
NSJSONSerialization
class. This one allows developers to easily convert JSON data into a
Foundation object, and the other way round. I have covered JSON parsing in chapter 4. In this
334
chapter, we will look into the APIs for parsing XML data.
iOS offers the
NSXMLParser
class, which takes charge of doing all the hard work and, through
some useful delegate methods gives us the tools we need for handling each step of the parsing.
I have to say that
NSXMLParser
piece of cake.
Being more specific, let me introduce you the
NSXMLParserDelegate
what each of the methods is for. The protocol defines the optional methods that should be
implemented for XML parsing. For clarification purpose, every XML data is considered as an
XML document in iOS. Here are the core methods that you will usually deal with:
parserDidStartDocument
- This one is the complement of the first one, and is called when the
during the parsing. The method contains an error object, which you can use to define the
actual error.
parser(_:didStartElement:namespaceURI:qualifiedName:attributes:)
an element. Its second argument is a string value containing the character that was just
parsed.
To help you understand the usage of the methods, we will build a simple RSS reader app
together. The app will consume an RSS feed (in XML format), parse its content and display the
data in a table view.
Demo App
I can show you how to build a plain XML parser that reads an XML file but that would be
boring. Wouldn't it be better to create a simple RSS reader? The RSS Reader app reads an RSS
335
feed of Apple, which is essentially XML formatted plain text. It then parses the content,
extracts the news articles and shows them in a table view.
To help you get started, I have created the project template that comes with a prebuilt
storyboard and view controller classes. You can download the template from
https://www.dropbox.com/s/kaptncr3mp20ibe/SimpleRSSReaderStart.zip?dl=0.
The
NewsTableViewController
NewsTableViewCell
cell is designed to display the title, date and description of a news article. I have also
configured the auto layout constraints of the cell so that it can be self-sized.
336
337
.
</channel>
</rss>
As I said before, an RSS feed is essentially XML formatted plain text. It's human readable.
Every RSS feed should conform to a certain format. I will not go into the details of RSS format.
If you want to learn more about RSS, you can refer to http://en.wikipedia.org/wiki/RSS. The
part that we are particularly interested in are those elements within the item tag. The section
highlighted in yellow represents a single article. Each article basically includes the title,
description, published date and link. For our RSS Reader app, the nodes that we are interested
in are:
title
description
pubDate
Our job is to parse the XML data and get all the items so as to display them in the table view.
When we talk about XML parsing, there are two general approaches: Tree-based and Eventdriven. The
NSXMLParser
NSXMLParserDelegate
protocol. To
better elaborate the concept, let's consider the following simplified XML content:
<item>
<title>Apple Announces Record iPhone 6s and iPhone 6s Plus Sales</title>
<pubDate>Mon, 28 Sep 2015 14:37:05 PDT</pubDate>
</item>
NSXMLParser
following events:
Event
No.
Event
Description
Started
parsing the
XML
document
Found the
start tag for
parserDidStartDocument(_:)
parser(_:didStartElement:namespaceURI:qualifiedName:attributes:)
338
element item
3
Found the
start tag for
element title
parser(_:didStartElement:namespaceURI:qualifiedName:attributes:)
Found the
characters
Apple
Announces
Record
iPhone 6s
and iPhone
6s Plus Sales
parser(_:foundCharacters:)
Found the
end tag for
element title
parser(_:didEndElement:namespaceURI:qualifiedName:)
Found the
start tag for
element
pubDate
parser(_:didStartElement:namespaceURI:qualifiedName:attributes:)
Found the
characters
Mon, 28 Sep
2015
14:37:05
PDT
parser(_:foundCharacters:)
Found the
end tag for
element
pubDate
parser(_:didEndElement:namespaceURI:qualifiedName:)
Found the
end tag for
element item
parser(_:didEndElement:namespaceURI:qualifiedName:)
10
Ended
parsing the
XML
document
parserDidEndDocument(_:)
NSXMLParserDelegate
339
New File...
NSObject
FeedParser
class.
NSXMLParser
class provides a
convenient method to specify a URL of the XML content. If you use the method, the class
automatically downloads the content for further parsing. However, it only works
synchronously. That means that the main thread (or any UI update) is blocked while
retrieving the feed. We don't want to block the UI so we will use
NSURLSession
to
NSXMLParser
parsing.
3. Use the
NSXMLParser
look for the value of title, description and pubDate tags, and then we group them into a
tuple and save the tuple in the rssItems array.
4. Lastly, we call the
parserCompletionHandler
FeedParser.swift
We use tuples to temporarily store the parsed items. If you haven't heard of Tuple, this is one
of the nifty features of Swift. It groups multiple values into a single compound value. Here we
group title, description and pubDate into a single item.
340
Quick Tip: To learn more about Tuples, check out Apple's official documentation
at
https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language
CH5-ID329.
FeedParser
class:
}
}
private var currentDescription:String = "" {
didSet {
currentDescription =
currentDescription.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSe
}
}
private var currentPubDate:String = "" {
didSet {
currentPubDate =
currentPubDate.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSet())
}
}
private var parserCompletionHandler:([(title: String, description: String,
pubDate: String)] -> Void)?
The
currentElement
element (e.g.
The
currentTitle
currentDescription
and
currentPubDate
<title>
method is called. Because the value may contain white space and
parserCompletionHandler
think of it as a callback function. When the parsing finishes, there are certain actions we
should take, such as displaying the items in a table view. This completion handler will be called
to perform the actions at the end of the parsing. We will talk more about it in a later section.
Next, let's add a new method called
parseFeed
feedUrl
and
completionHandler
object containing the link of the RSS feed. The completion handler is the one we just
discussed, and will be called when the parsing finishes. In this method, we create an
NSURLSession
object and a download task to retrieve the XML content asynchronously. When
the download completes, we initialize the parser object with the XML data, set the delegate to
itself, and start the parsing.
Now let's implement the delegate methods one by one. Referring to the event table I
mentioned before, the first delegate method to be invoked is the
method. Implement the method like this:
342
parserDidStartDocument
didStartElement
rssItems
<item>
currentElement
variable. If the
<item>
tag is
found, we reset the temporary variables of title, description, pubDate to blank for later use.
When the value of an element is parsed, the
parser(_:foundCharacters:)
a string representing all or part of the characters of the current element. Implement the
method like this:
func parser(parser: NSXMLParser, foundCharacters string: String) {
switch currentElement {
case "title": currentTitle += string
case "description": currentDescription += string
case "pubDate": currentPubDate += string
default: break
}
}
string
object may only contain part of the characters of the element. Instead of
assigning the string object to the temporary variable, we append it to the end.
When the closing tag (e.g.
</item>
) is found, the
parser(_:didEndElement:namespaceURI:qualifiedName:)
343
We create a tuple using the title, description and pubDate tags just parsed, and then we add
the tuple to the
rssItems
array.
parserDidEndDocument
parseCompletionHandler
as
FeedParser
class:
This method is called when the parser encounters a fatal error. Now that we have completed
the implementation of
is the caller of the
FeedParser
FeedParser
. Let's go to the
NewsTableViewController.swift
file, which
In the
viewDidLoad
344
self.rssItems = rssItems
NSOperationQueue.mainQueue().addOperationWithBlock({ () -> Void in
self.tableView.reloadSections(NSIndexSet(index: 0), withRowAnimation:
.None)
})
})
Here we create a
FeedParser
rssItems
parseFeed
the table data. Note that the UI update should be performed in the main thread.
Lastly, update the following methods to load the items in the table view:
override func tableView(tableView: UITableView, numberOfRowsInSection section:
Int) -> Int {
// Return the number of rows in the section.
guard let rssItems = rssItems else {
return 0
}
return rssItems.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath
indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell",
forIndexPath: indexPath) as! NewsTableViewCell
// Configure the cell...
if let item = rssItems?[indexPath.row] {
cell.titleLabel.text = item.title
cell.descriptionLabel.text = item.description
cell.dateLabel.text = item.pubDate
}
return cell
}
Great! You can now run the project. If you're testing the app using the simulator, make sure
your computer is connected to the Internet. The RSS Reader app should be able to retrieve the
news feed of Apple.
345
For reference, you can download the final Xcode project from
https://www.dropbox.com/s/2x4j2e56dqs4qdk/SimpleRSSReader.zip?dl=0.
346
Chapter 27
Applying a Blurred Background Using
UIVisualEffect
It's been two years now. I still remembered how Jonathan Ive described the user interface
redesign of iOS 7. Other than "Flat" design, the mobile operating system introduced new types
of depth in the words of the Apple's renowned design guru.
One of the ways to achieve depth is to use layering and translucency in the view hierarchy. The
use of blurred backgrounds could be found throughout the mobile operating system. For
347
instance, when you swipe up the Control Center, its background is blurred. Moreover, the
blurring effect is dynamic and keeps changing with respect to the background image. At that
time, Apple did not provide APIs for developers to create such stunning visual effects. To
replicate the effects, developers were required to create their own solutions or make use of the
third-party libraries.
Starting from iOS 8, Apple provided a new method which makes it very easy to create
translucent and blurring-style effects. It introduced a new visual effect API that lets developers
apply visual effects to a view. You can now easily add blurring effect to an existing image.
In this chapter, I will go through the new API with you. Again, we will build a simple app to see
how to apply the blurring effect.
It will work like this: when launched, it randomly picks an image from its image set. The
selected image, with blurring effect applied, is used as a background image for the login screen.
To keep your focus on learning the
UIVisualEffect
- There are only two kinds of visual effects including blur and vibrant. The
UIVisualEffectView
Dark. The
. A blur effect comes with three types of style: ExtraLight, Light and
UIVibrantEffect
that the element (e.g. label) inside a blurred view looks sharper.
UIVisualEffectView
takes in a
- This is the view that actually applies the visual effect. The class
UIVisualEffect
UIBlurEffect
Here we create a blurring effect with a Light style. Once you have created the visual effect, you
initialize a
UIVisualEffectView
UIImageView
addSubView
will be blurred:
backgroundImageView.addSubview(blurEffectView)
Now that you have some ideas about the visual effect API, lets continue to work on the demo
app.
First, open
Main.storyboard
and drag an image view to the view controller. As this image view
is used as a background image, make sure you put the image view beneath the login view (refer
to the document outline).
Once you have added the image view, select it and add the auto layout constraints. You can
simply click the Issue button of the auto layout menu in the Interface Builder. Then select Add
Missing Constraints under Selected Views. Xcode should automatically add the required
layout constraints to the image view. Next, go to the
LoginViewController.swift
Now go back to the storyboard and establish a connection between the outlet variable and the
image view.
351
UIVisualEffectView
object:
var blurEffectView:UIVisualEffectView?
viewDidLoad
352
arc4random_uniform()
number. By specifying 5 as the parameter, the function returns a value between 0 and 4. The
rest of the code is similar to what we have discussed in the previous section. You are free to
configure other blurring styles such as Dark.
To ensure the blur effect works in landscape mode, we have to update the
frame
property
when the device's orientation changes. Insert the following method in the class:
override func traitCollectionDidChange(previousTraitCollection:
UITraitCollection?) {
blurEffectView?.frame = view.bounds
}
traitCollectionDidChange
update the frame property accordingly. Now run the project and see what you get. If you
followed everything correctly, your app will display a blurred background.
353
354
Chapter 28
Using Touch ID For Authentication
Touch ID is Apple's biometric fingerprint authentication technology, which was first seen on
the iPhone 5S in 2013. As of today, the feature is available on most iOS devices including
iPhones and iPads. Touch ID is built into the home button and very simple to use. Once the
steel ring surrounding the home button detects your finger, the Touch ID sensor immediately
reads your fingerprint, analyses it, and provides you access to your phone.
When it was first unveiled, along with the release of iOS 7, you could only use Touch ID to
unlock your iPhone, and authorize your purchases on the App Store or iTunes Store.
Security and privacy are the two biggest concerns for the fingerprint sensor. According to
355
Apple, your device does not store any images of your fingerprints; the scan of your fingerprint
is translated into a mathematical representation, which is encrypted and stored on the Secure
Enclave of the A7, A8, or A8X chip. The fingerprint data is used by the Secure Enclave only for
fingerprint verifications; even the iOS itself has no way of accessing the fingerprint data.
Quick note:To learn more about Secure Enclave, you can refer to the Apple's
Security White Paper for iOS.
In iOS 7, Apple did not allow developers to get hold of the APIs to implement Touch ID
authentication in their own apps. With every major version release of iOS, Apple ships along a
great number of new technologies and frameworks. iOS 8 brings quite exciting new
innovations, among them, is the release of the public APIs for Touch ID authentication. You
can now integrate your apps with fingerprint authentication, potentially replacing passwords
or PINs. The usage of the TouchID is based on a new framework, named Local Authentication.
The framework provides methods to prompt a user to authenticate. It offers a ton of
opportunities for developers. You can use Touch ID authentication for login, or authorize
secure access to sensitive information within an app.
The heart of the Local Authentication framework is the
LAContext
methods:
canEvaluatePolicy(_:error:)
see if we can proceed with the authentication. At the time of this writing,
DeviceOwnerAuthenticationWithBiometrics
returns a positive result, this means the device supports Touch ID authentication. On the
other hand, if a negative result is returned, the device may not have the Touch ID sensor,
or the user has not enabled the fingerprint authentication feature.
evaluatePolicy(_:localizedReason:reply:)
LAError
object indicating
356
The project template is very similar to the demo app we built in the previous chapter. I just
357
in
showLoginDialog
LoginViewController.swift
to create a simple
present modally
Typically, we use the default transition (i.e. slide-up). This time, let's change it to
Dissolve
Cross
. Select the segue and go to the Attributes inspector. Change the transition option
from Default to
Cross Dissolve
showHomeScreen
. Later, we will
Libraries to expand the section and then click on the small plus icon. When prompted, search
for the Local Authentication framework and add it to the project.
To use the framework, all you need is to import it using the following statement:
import LocalAuthentication
Open the
LoginViewController.swift
viewDidLoad
method:
showLoginDialog()
with:
loginView.hidden = true
Normally, the app displays a login dialog when it is launched. Since we are going to replace the
password-based authentication with Touch ID, the login view is hidden by default.
To implement Touch ID, we will create a new method called
class. Let's start with the code snippet:
func authenticateWithTouchID() {
// Get the local authentication context.
let localAuthContext = LAContext()
359
authenticateWithTouchID
in the
LAContext
LAContext
canEvaluatePolicy
true
value, this
indicates the device is capable to use Touch ID and the user has enabled Touch ID as the
authentication mechanism. If a
false
to authenticate the user. In this case, you should provide an alternative authentication method.
Here we just call up the
showLoginDialog
Once we've confirmed that the Touch ID is supported, we can proceed to perform the Touch ID
authentication. Insert the following lines of code in the
authenticateWithTouchID
method:
360
The
evaluatePolicy
method of the local authentication context object handles all the heavy
DeviceOwnerAuthenticationWithBiometrics
is specified
as the policy, the method automatically presents a dialog, requesting a finger scan from the
user. You can provide a reason text, which will be displayed in the sub-title of the
authentication dialog. The method performs Touch ID authentication in an asynchronous
manner. When it finishes, the reply block (i.e. closure in Swift) will be called with the
authentication result and error passed as parameters. In the closure, we first check if the
authentication is successful. If it is true, we simply call up the
performSegueWithIdentifier
the code property of the error object to reveal the possible cause, which includes:
- the authentication failed because the fingerprint does not match
AuthenticationFailed
- the user has canceled the authentication (e.g. by tapping the Cancel button
in the dialog).
UserFallback
the authentication dialog, there is a button called Enter Password. When a user taps the
button, this error code will be returned.
SystemCancel
application came to the foreground while the authentication dialog was up.
PasscodeNotSet
TouchIDNotAvailable
TouchIDNotEnrolled
In the implementation, we simply log the error to the console. And when any error occurs, the
app will fallback to password authentication by calling
showLoginDialog()
Because the reply block is run in background, we have to explicitly perform the visual change
in the main thread. This is why we execute the
showLoginDialog
viewDidLoad
authentication:
authenticateWithTouchID()
Now you're ready to test the app. Make sure you run the app on a real device with Touch ID
support (e.g. iPhone 6). Once launched, the app should ask for Touch ID authentication.
362
If the authentication is successful, you will be able to access the Home screen. If you run the
app on the simulator, you should see the login dialog with the following error shown in the
console:
Optional("No fingers are enrolled with Touch ID.")
Password Authentication
Now you have implemented the Touch ID authentication. However, when the user opts for
password authentication, the login dialog is not fully functional yet. Let's create an action
method called
authenticateWithPassword
363
In reality, you may store the user profiles in your backend and authenticate the user using web
service call. To keep things simple, we just hardcode the login ID and password to
[email protected]
and
1234
and password, the dialog performs a "Shake" animation to indicate the error.
Now go back to the storyboard to connect the Sign In button with the method. Control-drag
from the Sign In button to the Login View Controller and select
authenticateWithPassword
Build and run the project again. You should now be able to login the app even if you choose to
fallback to the password authentication. Tapping the Sign In button without entering the
364
365
Chapter 29
Building a Carousel-Like User Interface
Have you used Kickstarter's iOS app? If not, download it from App Store and give it a shot.
Kickstarter is one of my favorite crowdfunding services. Earlier this year, the company
revamped its iOS app with a beautifully redesigned user interface. As soon as you open up the
app, it displays the featured projects in a carousel, with which you can flick left or right
through the cards to discover more Kickstarter projects. Themed with vivid colors, the carousel
design of the app looks plain awesome.
Carousel is a popular way to showcase a variety of featured content. Not only can you find
carousel design in mobile apps, but it has also been applied to web applications for many years.
A carousel arranges a set of items horizontally, where each item usually includes a thumbnail.
366
Users can scroll through the list of items by flicking left or right.
In this chapter, I will show you how to build a carousel in iOS apps. It's not as hard as you
might think. All you need to do is to implement a
UICollectionView
create a collection view, I recommend you take a look at chapter 18. As usual, to walk you
through the feature we will build a demo app with a simple carousel that displays a list of trips.
Main.storyboard
600
430
view controller. Next, go to the Size inspector. In the cell size option, set the width to
points and height to
380
20
250
points to add
some spacing between cell items. Lastly, set the left and right values of section insets to
points.
367
20
Your storyboard should now look similar to the screenshot above. Now select the collection
view and go to the Attributes inspector. Change the scroll direction from
horizontal
vertical
to
. Once you have made this change, users will be able to scroll through the collection
view horizontally instead of vertically. This is the real trick to building a carousel. Don't forget
to set the identifier of the collection view cell to
Cell
Next, drag a label to the view controller and place it at the top-left corner of the view. Set the
text to
white
size. Then, add another label to the view controller but put it below the view controller. Change
its text to
APPCODA
or whatever you prefer. Your view controller will look similar to this:
368
So far we haven't configured any auto layout constraint. First, select the Most Popular
Destinations label. Click the Issues button and select Adding Missing Constraints. Now let's
add a few layout constraints for the collection view. Select the collection view and click the
Align button of the auto layout menu. Check both the Horizontal Center in Container and
Vertical Center in Container options, and click Add 2 Constraints. This will align the collection
view to the center of the view.
369
Xcode should indicate some missing constraints. Click the Pin button and select the dashed red
line corresponding to the left and right sides. Uncheck the Constrain to margins option and
click Add 2 Constraints. This ensures that the left and right sides of the collection view align
perfectly with the background image view.
370
Lastly, let's add the layout constraints for the APPCODA label. Select the label and click the
Issues button. Select the Add Missing Constraints option and let Xcode configure the layout
constraints for you. Now that you have created the skeleton of the collection view, let's
configure the cell content, which will be used to display trip information. First, select the cell
and change its background to
size to
250x311
light gray
points.
Next, drag a view from the Object Library and place it right below the image view. In the
Attributes inspector, change its background color to Default, set the mode to
enable the
Clip Subviews
Aspect Fill
and
Sometimes it is good to use a view to group multiple UI elements together so that it is easier
for you to define the layout constraints later.
If you follow the procedures correctly, your storyboard should look similar to this:
371
Later we will change the size of the collection view according to the screen height. But I still
want to keep the height of the image view and the view inside the cell proportional. To do that,
control-drag from the image view to the view and select Equal Heights.
372
Next, select the constraint just created and go to the Size inspector. Change the multiplier from
1
to
4.5
View.height
Image View.height
and
respectively. This defines a constraint so that the height of the image view is
Now select the image view and define the spacing constraints. Click the Pin button and select
the dashed red lines of all sides. Click the Add 4 Constraints button to define the layout
constraints.
Select the view inside the collection view cell and click the Pin button. Click the dashed red
lines that correspond to the left, right and bottom sides.
373
If the document outline displays the layout issue indication, simply click on it and follow the
procedures to fix the layout issue.
Okay, you have defined the layout constraints for these two views. It's now time to add some
UI elements to the image view for displaying the trip information.
First, add a label to the image view of the cell. Name it City and change its color to
white
white
white
. Change its
56
points.
374
red
The UI design is almost complete. We simply need to add a few layout constraints for the
elements we just added. First, control-drag from the City label to the image view of the cell.
In the popover menu, select both Vertical Spacing and Center Horizontally (simply hold the
shift key to select multiple options). Next, control-drag from the Country label to the City
label. Release the buttons and select both the Vertical Spacing and Center Horizontally
options.
375
Then, control-drag from the Days label to the Country label. Repeat the procedure and set the
same set of constraints. Lastly, control-drag from the Price label to the Days label and define
the same layout constraints.
For the heart button, I want it to be a fixed size. Control-drag to the right (see below) and set
the Width constraint. Next, control-drag vertically to set the Height constraint for the button.
To ensure the heart button is always displayed at the center of the view, click the Align button
and select Horizontal Center in Container and Vertical Center in Container.
376
Great! You have completed the UI design. Now we will move onto the coding part.
New File...
TripCollectionCell
TripCollectionCell.swift
377
and
} else {
likeButton.setImage(UIImage(named: "heart"), forState: .Normal)
}
}
}
}
The above lines of code should be very familiar to you. We simply define the outlet variables to
associate with the labels, image view and button of the collection view cell in storyboard. The
isLiked
variable is a boolean to indicate whether a user favors a trip or not. In the above code,
we declare a
didSet
isLiked
isLiked
observer will be called immediately. Here we simply set the image of the like button
isLiked
Now go back to the storyboard and select the collection view cell. In the Identity inspector, set
the custom class to
TripCollectionCell
Trip
Trip
TripViewController
to represent a trip. Create a new file using the Swift File template
. Proceed to create and save the
Trip.swift
file. Open
Trip.swift
The
Trip
class contains a few properties for holding the trip data including ID, city, country,
featured image, price, total number of days and isLiked. Other than the ID and isLiked
properties, the rest of the properties are self-explanatory. Regarding the trip ID property, it is
used for holding a unique ID of a trip.
isLiked
TripViewController.swift
379
Go to the storyboard. In the Document Outline, right click Trip View Controller. Connect
collectionView outlet variable with the collection view.
Furthermore, control-drag from collection view to trip view controller to connect the data
source and delegate.
To keep things simple, we will just put the trip data into an array. Declare the following
variable in
TripViewController.swift
380
and
TripViewController
UICollectionViewDataSource
381
I will not go into the details of the implementation as you should be very familiar with the
methods. Finally, insert this line of code in the
viewDidLoad
view transparent:
collectionView.backgroundColor = UIColor.clearColor()
Now it's time to test the app. Hit the Run button, and you should have a carousel showing a list
of trips. The app works properly on devices with at least 4-inch display. If you run the app on
iPhone 4s, however, parts of the collection view are blocked.
382
We have to reduce the height of the collection view specifically for 3.5-inch devices. Insert the
following block of code in the
viewDidLoad
method of
TripViewController.swift
if UIScreen.mainScreen().bounds.size.height == 480.0 {
let flowLayout = self.collectionView.collectionViewLayout as!
UICollectionViewFlowLayout
flowLayout.itemSize = CGSizeMake(250.0, 300.0)
}
Base on the screen height, we can deduce if the device has a 3.5-inch screen. If it meets the
criteria, we adjust the height of the collection view from
380
points to
300
have made the change, try to test the app on iPhone 4s again. This should work now.
in the
TripCollectionCell
class:
protocol TripCollectionCellDelegate {
func didLikeButtonPressed(cell: TripCollectionCell)
}
didLikeButtonPressed
the heart button is tapped. The object that implements the delegate protocol is responsible for
handling the button press.
Add the following action method, which is triggered when a user taps the heart button:
@IBAction func likeButtonTapped(sender: AnyObject) {
delegate?.didLikeButtonPressed(self)
}
Now go back to the storyboard to associate the heart button with this method. Right-click the
heart button and drag from Touch Up Inside to the Cell in the Document Outline. Select
likeButtonTapped:
384
Now open
TripViewController.swift
TripCollectionCellDelegate
protocol:
In the class, implement the required method of the protocol like this:
func didLikeButtonPressed(cell: TripCollectionCell) {
if let indexPath = collectionView.indexPathForCell(cell) {
trips[indexPath.row].isLiked = trips[indexPath.row].isLiked ? false :
true
cell.isLiked = trips[indexPath.row].isLiked
}
}
didLikeButtonPressed
selected cell. Based on selected cell, we can determine the index path using the
indexPathForCell
didSet
isLiked
property of
. The heart button will change its images according to the value of
385
isLiked
cellForItemAtIndexPath
isLiked
is set to
false
cell.delegate = self
Okay, let's test the app again. When it launches, tapping the heart button of a trip can now
favor the trip.
386
Chapter 30
Working with Parse
Some of your apps may need to store data on a server. Take the TripCard app that we
developed in the previous chapter as an example. The app stored the trip information locally
using an array. If you were building a real-world app, you would not keep the data in that way.
The reason is quite obvious: You want the data to be manageable and updatable without rereleasing your app on App Store. The best solution is put your data onto a backend server that
allows your app to communicate with it in order to get or update the data. Here you have
several options:
You can come up with your own home-brewed backend server, plus server-side APIs for
data transfer, user authentication, etc.
387
You can use CloudKit (which was introduced in iOS 8) to store the data in iCloud.
You can make use of a third-party Backend as a Service provider (BaaS) to manage your
data.
The downside of the first option is that you have to develop the backend service on your own.
This requires a different skill set and a huge amount of work. As an iOS developer, you may
want to focus on app development rather than server side development. This is one of the
reasons why Apple introduced CloudKit, which makes developers' lives easier by eliminating
the need to develop their own server solutions. With minimal setup and coding, CloudKit
empowers your app to store data (including structured data and assets) in its new public
database, where the shared data would be accessible by all users of the app. CloudKit works
pretty well and is very easy to integrate (note: it is covered in the Beginning iOS 9
Programming with Swift book). However, CloudKit is only available for iOS. If you are going
to port your app to Android that utilizes the shared data, CloudKit is not a viable option.
Parse, acquired by Facebook in late April 2013, is one of the BaaS that works across nearly all
platforms including iOS, Android, Windows phone and web application. By providing an easyto-use SDK, Parse allows iOS developers to easily manage the app data on the Parse cloud.
This should save you development costs and time spent creating your own backend service.
The service is free (with limits) and quick to set up.
In this chapter, I will walk you through the integration process of Parse. We will use the
TripCard app as a demo and see how to put its trip data onto the Parse cloud. To begin with,
you can download the TripCard project from
https://www.dropbox.com/s/pnnf5inm54ksb0m/ParseDemoStart.zip?dl=0.
If you haven't read chapter 29, I highly recommend you to check it out first. It would be better
to have some basic understandings of the demo app.
Okay, let's get started.
388
TripCard
you'll be brought to the Parse dashboard. If you already have an account, you can move your
mouse cursor to the dropdown menu at the top-left corner to create a new app.
Parse offers various backend services including data, push notification and many more. What
we will focus on in this chapter is the data service. Make sure you've selected
TripCard
from
Core
from the top menu. You will be brought to the data browser. By default, you
do not have any data in the TripCard app. You will need to create and upload the trip data
manually. But before that, you will have to define a
Trip
Trip
class defined in Parse is the cloud version of the counterpart class that we have declared in our
code. Each property of the class (e.g. city) will be mapped to a table column of the
Trip
class
defined in Parse. Now click the Add a Class button to create a new class. Set the name to
and type to
Custom
. Once created, you should see the class under the Data section of the
sidebar menu.
389
Trip
In the
TripCard
Trip ID
City
Country
Featured image
Price
Total number of days
isLiked
With the exception of the trip ID, each of the properties should be mapped to a corresponding
column of the
Trip
390
Trip
+ Col
city
and type to
String
procedures to add the rest of properties with the following column names and types:
Country: Set the column name to
country
and type to
String
and type to
featuredImage
File
. The
File
price
and type to
isLiked
Number
totalDays
and type to
and type to
Boolean
Number
Once you have added the columns, your table should look similar to the screenshot below.
391
You may wonder why we do not create a column for the trip ID. As you can see from the table,
there is a default column named
objectId
generates a unique ID. We will simply use this ID as the trip ID. You may also be wondering
how we can convert the data stored in the Parse cloud to objects in our code? The Parse SDK is
smart enough to handle the translation of native types. For instance, if you retrieve a
type from Parse, it will be translated into a
String
String
details later.
Now let's add some trip data into the data browser.
Click the
+ Row
button to create a new row. Each row represents a single Trip object. You only
need to upload the image of a trip and fill in the city, country, price, totalDays and isLiked
columns. For the objectId, createdAt and updatedAt columns, the values will be generated by
Parse.
If you look into
TripViewController.swift
392
To put the first item of the array into Parse, fill in the values of the row like this:
Trip
of its Parse counterpart. Just note that Parse stores the actual image of the trip in the
featuredImage
column. You should have to upload the paris.jpg file by clicking the Upload File
button.
Quick note: You can find the images in the Images.xcassets folder of the Xcode
project.
Repeat the above procedures and add the rest of the trip data. You will end up with a screen
similar to this:
393
Now that you have configured the trip data on the Parse cloud, we will start to integrate the
TripCard project with Parse. To begin with, download the Parse SDK for iOS from
https://www.parse.com/downloads/ios/parse-library/latest. Unzip the file and drag both
Bolts.framework and Parse.framework into the TripCard project. Optionally, you can create a
new group called Parse to better organize the files. When prompted, make sure you enable the
Copy items if needed option and click Finish to proceed.
The Parse SDK depends on other frameworks in iOS SDK. You will need to add the following
libraries into the project:
AudioToolbox.framework
CFNetwork.framework
CoreGraphics.framework
CoreLocation.framework
MobileCoreServices.framework
QuartzCore.framework
Security.framework
StoreKit.framework
SystemConfiguration.framework
394
libz.dylib
libsqlite3.dylib
Select the TripCard project in the project navigator. Under the TripCard target, select Build
Phases and expand the Link Binary with Libraries. Click the
New File
395
Choose the Objective-C File template and proceed. Name the class
like) and set the file type to
When prompted, click
Empty file
Parse
396
Parse.m
and
TripCard-Bridging-Header.h
TripCard-Bridging-Header.h
#import <Parse/Parse.h>
That's it. Now you should be able to access the Parse framework from the Swift project.
397
Here you can reveal the application ID and client key. Remember to keep these keys safe, as
one can access your Parse data with them.
Open up
AppDelegate.m
didFinishLaunchingWithOptions
Note that you should replace the Application ID and the Client Key with your own keys. With
just a line of code, your app is ready to connect to Parse. Try to compile and run it. If you get
everything correct, you should be able to run the app without any error.
PFObjects
398
PFQuery
for
You create a
PFQuery
object with a specific class name that matches the one created on Parse.
Trip
. By calling the
retrieve the available Trip objects. The method works in an asynchronous manner. When it
finishes, the block of code will be called and you can perform additional processing based on
the returned results.
With a basic understanding of data retrieval, we will modify the TripCard app to get the data
from the Parse cloud.
First, open the
TripViewController.swift
Instead of populating the array with static data, we initialize an empty array. Later we will get
the trip data from Parse at runtime and save them into the array.
If you look into the
property is of
Trip
UIImage
class (i.e.
Trip.swift
PFObject
to a
Trip
File
PFFile
featuredImage
featuredImage
column as a
File
type on
object easily.
type in Parse, that lets you store application files (e.g.
. Now open
Trip.swift
class Trip {
var tripId = ""
var city = ""
var country = ""
var featuredImage:PFFile?
399
var price:Int = 0
var totalDays:Int = 0
var isLiked = false
init(tripId: String, city: String, country: String, featuredImage: PFFile!,
price: Int, totalDays: Int, isLiked: Bool) {
self.tripId = tripId
self.city = city
self.country = country
self.featuredImage = featuredImage
self.price = price
self.totalDays = totalDays
self.isLiked = isLiked
}
init(pfObject: PFObject) {
self.tripId = pfObject.objectId!
self.city = pfObject["city"] as! String
self.country = pfObject["country"] as! String
self.price = pfObject["price"] as! Int
self.totalDays = pfObject["totalDays"] as! Int
self.featuredImage = pfObject["featuredImage"] as? PFFile
self.isLiked = pfObject["isLiked"] as! Bool
}
func toPFObject() -> PFObject {
let tripObject = PFObject(className: "Trip")
tripObject.objectId = tripId
tripObject["city"] = city
tripObject["country"] = country
tripObject["featuredImage"] = featuredImage
tripObject["price"] = price
tripObject["totalDays"] = totalDays
tripObject["isLiked"] = isLiked
return tripObject
}
}
PFObject
featuredImage
from
PFObject
UIImage
PFObject
conversion.
TripViewController.swift
func loadTripsFromParse() {
400
to
PFFile
. For
and another
The
loadTripsFromParse
PFQuery
trips
convert each of the PFObjects into Trip objects and append them to the trips array. Lastly, we
insert the trip to the collection view by calling the
For the
cellForItemAtIndexPath
insertItemsAtIndexPaths
method.
cell.imageView.image = trips[indexPath.row].featuredImage
to:
// Load image in background
cell.imageView.image = UIImage()
if let featuredImage = trips[indexPath.row].featuredImage {
featuredImage.getDataInBackgroundWithBlock({ (imageData: NSData?, error:
NSError?) -> Void in
if let tripImageData = imageData {
401
cell.imageView.image = UIImage(data:tripImageData)
}
})
}
The trip images are no longer bundled in the app. Instead, we will pull them from the Parse
cloud. The time required to load the images varies depending on the network speed. This is
why we handle the image download in background. Parse stores files (such as images, audio
and documents) in the cloud in the form of PFFile. We use PFFile to reference the featured
image. The class provides the
getDataInBackgroundWithBlock
download in background. Once the download completes, we load it onto the screen.
Finally insert this line of code in the
viewDidLoad
loadTripsFromParse()
Now you are ready to go! Hit the Run button to test the app. Make sure your
computer/device is connected to the Internet. The TripCard app should now retrieve the trip
information from Parse. Depending on your network speed, it will take a few seconds for the
images to load.
402
Refreshing Data
Currently, there is no way to refresh the data. Let's add a button to the Trip View Controller in
storyboard. When a user taps the button, the app will go up to Parse and refresh the trip
information.
The project template already bundled a reload image for the button. Simply open
Main.storyboard
and drag a button object to the view controller. Set its width and height to 30
reload
white
button of the auto layout menu and select Add Missing Constraints to add the layout
constraints.
403
Your design should be similar to the screen above. Next, add an action method in
TripViewController.swift
Go back to the storyboard and associate the refresh button with this action method. Controldrag from the
refresh
reloadButtonTapped:
button to the Trip View Controller. After releasing the buttons, select
Now run the app again. Once it's launched, go to the Parse dashboard and add a new trip. Your
app should now retrieve the new trip when the refresh button is tapped.
404
There is a better way to handle this situation. Parse has a built-in support for caching that
makes it a lot easier to save query results on local disk. In case Internet access is not available,
your app can load the result from cache. Caching also improves the app's performance. Instead
of loading data from Parse every time when the app runs, it retrieves the data from cache upon
startup.
In the default setting, caching is disabled. But it can easily be enabled by using a single line of
code. Add the following code in the
PFQuery
loadTripsFromParse
query.cachePolicy = PFCachePolicy.NetworkElseCache
NetworkElseCache
policy is just
one of them. It first loads data from the network, then if that fails, it loads results from the
cache.
Now compile and run the app again. After you run it once (with WiFi enabled), disable the
WiFi or other network connections and launch the app again. This time, your app should be
able to show the trips even if the network is unavailable.
405
PFObject
PFObject
PFObject
method to upload the changes to the cloud. Based on the object ID,
TripViewController.swift
didLikeButtonPressed
toPFObject
toPFObject
PFObject
Trip
method of the
PFObject
saveInBackgroundWithBlock
method to upload
isLiked
406
PFObject
provides various methods for object deletion. In short, you call up the
deleteInBackgroundWithBlock
method of the
PBObject
Currently, the TripCard app does not allow users to remove a trip. We will modify the app to
let users swipe up a trip item to delete it. iOS provides the
recognize swipe gestures. In the
viewDidLoad
UISwipeGestureRecognizer
method of the
TripViewController
class to
class, insert
UISwipeGestureRecognizer
handleSwipe
Up
. When using a gesture recognizer, you must associate it with a certain view
addGestureRecognizer
method to
UIGestureRecognizerDelegate
handleSwipe
407
protocol. Thus,
trips[indexPath.row].toPFObject().deleteInBackgroundWithBlock({
(success, error) -> Void in
if (success) {
print("Successfully updated the trip")
} else {
print("Error: \(error?.description)")
}
self.trips.removeAtIndex(indexPath.row)
self.collectionView.deleteItemsAtIndexPaths([indexPath])
})
}
}
}
When a user swipes up a trip item (i.e. a collection view cell), we first need to determine which
cell is going to be removed. The
the form of
CGPoint
locationInView
. From the point returned, we can compute the index path of the collection
indexPathForItemAtPoint
deleteInBackgroundWithBlock
Great! You've implemented the delete feature. Hit the Run button to launch the app and try to
delete a record from Parse.
I hope that this chapter gave you an idea of how to connect your app to the cloud. Parse, as well
as other cloud providers (e.g. Apple's CloudKit), simplifies the whole development of backend
services. Additionally, the startup cost of using a cloud is nearly zero. Most of the BaaS
providers are free to use, and you only need to pay when the number of requests reaches a
certain volume. If you think it's too hard to integrate your app with the cloud, think again!
Consider building your existing apps with some cloud features.
For reference, you can download the final project from
https://www.dropbox.com/s/vlhzu4c33e6ex57/ParseDemo.zip?dl=0.
408
Chapter 31
How to Preload a SQLite Database Using
Core Data
When working with Core Data, you may have asked these two questions:
How can you preload existing data into the SQLite database?
How can you use an existing SQLite database in my Xcode project?
I recently met a friend who is now working on a dictionary app for a particular industry. He got
the same questions. He knows how to save data into the database and retrieve them back from
409
the Core Data store. The real question is: how could he preload the existing dictionary data
into the database?
I believe some of you may have the same question. This is why I devote a full chapter to talk
about data preloading in Core Data. I will answer the above questions and show you how to
preload your app with existing data.
So how can you preload existing data into the built-in SQLite database of your app? In general
you bundle a data file (in CSV or JSON format or whatever format you like). When the user
launches the app for the very first time, it preloads the data from the data file and puts them
into the database. At the time when the app is fully launched, it will be able to use the
database, which has been pre-filled with data. The data file can be either bundled in the app or
hosted on a cloud server. By storing the file in the cloud or other external sources, this would
allow you to update the data easily, without rebuilding the app. I will walk you through both
approaches by building a simple demo app.
Once you understand how data preloading works, I will show you how to use an existing
SQLite database (again pre-filled with data) in your app. Note that I assume you have a basic
understanding of Core Data. You should know how to insert and retrieve data through Core
Data. If you have no ideas about these operations, you can refer to the Beginning iOS 9
Programming with Swift book.
MenuItemTableViewController
class and
410
CoreDataDemo.xcdatamodeld
for details.
MenuItem
411
Country Breakfast,"Two eggs as you like, Batter Home Fries, country slab bacon,
sausage, scrapple or ham steak and toast", 8.5
Big Batter Breakfast,"3 eggs, Batter Home Fries, toast, and 2 sides of meat
(bacon, sausage, scrapple, or country ham)",13.5
Margherita Pizza,"Rustic style dough topped with tomato, basil, and fresh
mozzarella",15.0
Fish and Chips,Battered cod and fresh cut French fries served with tartar or
cocktail sauce,16.0
The first field represents the name of the food menu item. The next field is the detail of the
food, while the last field is the price. Each food item is one line, separated with a new line
separator.
AppDelegate
shutdown). To preload data during the app launch, we will add a few methods in the
AppDelegate
class. First, insert the following method for parsing the CSV file:
412
413
The method takes in three parameters: the file's URL andencoding. It first loads the file
content into memory, reads the lines into an array and then performs the parsing line by line.
At the end of the method, it returns an array of food menu items in the form of tuples.
A simple CSV file only uses a comma to separate values. Parsing such kind of CSV files
shouldn't be difficult. You can call the
componentsSeparatedByString
delimited string. It'll then return you an array of strings that have been divided by the
separator. For some CSV files, they are more complicated. Field values containing reserved
characters (e.g. comma) are surrounded by double quotes. Here is another example:
Country Breakfast,"Two eggs as you like, Batter Home Fries, country slab bacon,
sausage, scrapple or ham steak and toast", 8.5
NSScanner
componentsSeparatedByString
the field values. If the field value begins with a double quote, we scan through the string until
we find the next double quote character by calling the
scanUpToString
smart enough to extract the value surrounded by the double quotes. Once a field value is
retrieved, we then repeat the same procedure for the remainder of the string.
414
After all the field values are retrieved, we save them into a tuple and then put it into the
items
array.
parseCSV
AppDelegate.swift
file:
func preloadData () {
// Load the data file. For any reasons it can't be loaded, we just return
guard let contentsOfURL = NSBundle.mainBundle().URLForResource("menudata",
withExtension: "csv") else {
return
}
// Remove all the menu items before preloading
removeData()
if let items = parseCSV(contentsOfURL, encoding: NSUTF8StringEncoding) {
// Preload the menu items
for item in items {
let menuItem =
NSEntityDescription.insertNewObjectForEntityForName("MenuItem",
inManagedObjectContext: managedObjectContext) as! MenuItem
menuItem.name = item.name
menuItem.detail = item.detail
menuItem.price = (item.price as NSString).doubleValue
do {
try managedObjectContext.save()
} catch {
print(error)
}
}
}
}
415
func removeData () {
// Remove the existing items
let fetchRequest = NSFetchRequest(entityName: "MenuItem")
do {
let menuItems = try
managedObjectContext.executeFetchRequest(fetchRequest) as! [MenuItem]
for menuItem in menuItems {
managedObjectContext.deleteObject(menuItem)
}
} catch {
print(error)
}
}
The
removeData
method is used to remove any existing menu items from the database. I want
to ensure the database is empty before populating the data extracted from the menudata.csv
file. The implementation of the method is very straightforward if you have a basic
understanding of Core Data. We first execute a query to retrieve all the menu items from the
database and call the
about the
deleteObject
preloadData
method to delete the item one by one. Okay, now let's talk
method.
In the method we first retrieve the file URL of the menudata.csv file using this line of code:
NSBundle.mainBundle().URLForResource("menudata", withExtension: "csv")
removeData
parseCSV
menudata.csv file. With the returned items, we insert them one by one by calling the
NSEntityDescription.insertNewObjectForEntityForName
preloadData()
method in the
method.
didFinishLaunchingWithOptions
method:
Now you're ready to test your app. Hit the Run button to launch the app. If you've followed the
416
implementation correctly, the app should be preloaded with the food items.
But there is an issue with the current implementation. Every time you launch the app, it
preloads the data from the CSV file. Apparently, you only want to perform the preloading once.
Change the
application:didFinishLaunchingWithOptions:
To indicate that the app has preloaded the data, we save a setting to the defaults system using a
specific key (i.e. isPreloaded). Every time when the app is launched, we will first check if the
value of the
isPreloaded
true
417
follow this guide to create a public folder to store your files. Once you
create the public folder, you can use the following direct link to access the
file:
https://googledrive.com/host//
Please replace with your folder ID. You can look up the folder ID by clicking
your public folder. The URL will be something like this:
https://drive.google.com/drive/folders/0ByZhaKOAvtNGTHhXUUpGS3VqZnM
This is just for demo purpose. If you have your own server, feel free to upload the file to the
server and use your own URL. To load the data file from the remote server, all you need to do
is make a little tweak to the code. First, update the
preloadData
func preloadData () {
// Load the data file. For any reasons it can't be loaded, we just return
guard let remoteURL = NSURL(string:
"https://googledrive.com/host/0ByZhaKOAvtNGTHhXUUpGS3VqZnM/menudata.csv") else
{
return
}
// Remove all the menu items before preloading
removeData()
if let items = parseCSV(remoteURL, encoding: NSUTF8StringEncoding) {
// Preload the menu items
for item in items {
let menuItem =
NSEntityDescription.insertNewObjectForEntityForName("MenuItem",
inManagedObjectContext: managedObjectContext) as! MenuItem
menuItem.name = item.name
menuItem.detail = item.detail
menuItem.price = (item.price as NSString).doubleValue
do {
try managedObjectContext.save()
} catch {
print(error)
}
}
418
}
}
The code is very similar to the original one. Instead loading the data file from the bundle, we
specify the remote URL and pass it to the
parseCSV
parseCSV
method
will handle the file download and perform the data parsing accordingly.
Before running the app, you have to update the
application:didFinishLaunchingWithOptions:
method so that the app will load the data every time it runs:
func application(application: UIApplication, didFinishLaunchingWithOptions
launchOptions: [NSObject: AnyObject]?) -> Bool {
preloadData()
return true
}
You're ready to go. Hit the Run button and test the app again. The menu items should be
different from those shown previously.
419
For reference, you can download the complete Xcode project from
https://www.dropbox.com/s/0w12gmg1ldxuui5/CoreDataPreloadDemo.zip?dl=0.
420
Before I show you the procedures, please download the starter project again from
https://www.dropbox.com/s/l3m5gky5qu959jt/CoreDataPreloadDemoStart.zip?dl=0. As a
demo, we will copy the existing database created in the previous section to this starter project.
Now open up the Xcode project that you have worked on earlier. If you've followed me along,
your database should be pre-filled with data. We will now copy it to the starter project that you
have just downloaded.
But where is the SQLite database?
The database is not bundled in the Xcode project but automatically created when you run the
app in the simulator. To locate the database, you will need to add a line of code to reveal the
file path. Update the
application:didFinishLaunchingWithOptions:
The SQLite database is generated under the application's document directory. To find the file
path, we simply print out the value of
applicationDocumentsDirectory.path
variable.
Now run the app again. You should see an output in the console window showing the full path
of the document directory like this:
Optional("/Users/simon/Library/Developer/CoreSimulator/Devices/6BC1B40E-BB8C4CA8-B7FF-54A1684E3ACF/data/Containers/Data/Application/4DD3C821-D5BB-46479169-6AD9705F4E36/Documents")
Copy the file path and go to Finder. In the menu select Go > Go to Folder... and then paste the
path in the pop-up. Click
Go
When running WAL mode, SQLite will also create a shared memory file with .sqlite-shm
extension. In order to backup the database or use it to in other projects, you will need copy
these three files. If you just copy the CoreDataDemo.sqlite file, you will probably end up with
an empty database.
Now, drag these three files to the starter project you just download.
When prompted, please ensure the Copy item if needed option is checked and the
CoreDataPreloadDemo
Finish
to confirm. Now
that you've bundled an existing database in your Xcode project. When you build the app, this
database will be embedded in the app. But you will have to tweak the code a bit before the app
is able to use the database.
By default, the app will create an empty SQLite store if there is no database found in the
document directory. So all you need to do is copy the database files bundled in the app to that
directory. In the
AppDelegate
422
implementation creates and returns a coordinator, having added the store for
the application to it. This property is optional since there are legitimate
error conditions that could cause the creation of the store to fail.
// Create the coordinator and store
let coordinator = NSPersistentStoreCoordinator(managedObjectModel:
self.managedObjectModel)
let url =
self.applicationDocumentsDirectory.URLByAppendingPathComponent("CoreDataDemo.sqlite")
423
We first verify if the database exists in the document folder. If not, we copy the SQLite files
from the bundle folder to the document folder by calling the
NSFileManager
copyItemAtURL
method of
That's it! Before you hit the Run button to test the app, you better delete the
CoreDataPreloadDemo app from the simulator or simply reset it (select iOS Simulator > Reset
Content and Settings). This is to remove any existing SQLite databases from the simulator.
Okay, now you're good to go. When the app is launched, it should be able to use the database
bundled in the Xcode project. For reference, you can download the final Xcode project from
https://www.dropbox.com/s/ox0878t9k0grsek/CoreDataExistingDB.zip?dl=0.
424