UIKit is the main framework for working with various UI components on iOS. You can use other frameworks, such as OpenGL, to build your own UI the way you want, without being constrained by UIKit, but almost all developers use UIKit at some stage in their applications to bring intuitive user interfaces to their apps. One of the main reasons for this is that UIKit by default takes advantage of all the latest technologies in iOS and is kept up to date. For instance, many years back when Apple started producing Retina displays for iOS devices, all apps that were using UIKit could take advantage of the much sharper resolution afforded by Retina displays without requiring an update to their UIKit components. Applications that were using other technologies for rendering text had to update their apps to conform with Retina displays.
In this chapter, we will have a look at some of the most interesting features of UIKit and playgrounds.
Use the UIViewPropertyAnimator
class and specify the properties of your views that you would like to animate, including their new values. For instance, you can instantiate UIViewPropertyAnimator
and set a delay and an animation length, and then change the background color of your view instances inside the animation block of your UIViewPropertyAnimator
instance. You can then simply call the startAnimation()
function on this instance to start the animation(s).
Let’s have a look at an example. Create a single view application in Xcode (see Figure 6-1). In your Main.storyboard file, place a UIView
instance in the middle of the screen and then connect it to your view controller, under the name animatingView
. So now the top part of your view controller should look like this:
import
UIKit
class
ViewController
:
UIViewController
{
@IBOutlet
var
animatingView
:
UIView
!
...
Our goal in this recipe is to change the background color of this new view to a random color every time the user taps on the view; in addition, we would like this color change to be animated. So go to Interface Builder and in the Object Library, find Tap Gesture Recognizer (see Figure 6-2) and drag and drop it into your newly created view. Then connect the tap gesture recognizer’s Sent Actions outlet to your view controller under a new method called animatingViewTapped(_:)
(see Figure 6-2). The tap gesture recognizer placed on our view controller associates the gesture recognizer with that view.
In our view controller we will define an array of colors of type UIColor
. Later we will pick a random one and assign it to this view whenever the user taps on it:
let
colors
:
[
UIColor
]
=
[
.
red
,
.
blue
,
.
yellow
,
.
orange
,
.
green
,
.
brown
]
Imagine picking a random color from this array of colors. What if that random color is the same color as the one currently assigned to the view? We need an algorithm that can pick a color that is not equal to the view’s current color. So let’s write that function.
func
randomColor
(
notEqualTo
currentColor
:
UIColor
)
->
UIColor
{
var
foundColor
=
currentColor
repeat
{
let
index
=
Int
(
arc4random_uniform
(
UInt32
(
colors
.
count
)))
foundColor
=
colors
[
index
]
}
while
foundColor
.
isEqual
(
currentColor
)
return
foundColor
}
In this function we use the repeat...while
syntax in order to find a random value. We then compare it with the current color and if they are the same, repeat this process until we find a color that is not the same as the old one.
Last but not least, we need to program our animatingViewTapped(_:)
function and use an instance of UIViewPropertyAnimator
to animate the change of background color of our view. And for that we can use the init(duration:curve:animations:)
initializer of UIViewPropertyAnimator
. duration
is a value of type TimeInterval
, which is the duration of the animation in seconds. curve
is of type UIViewAnimationCurve
. animations
, which is where you will actually do your animations, is a block that has no parameters and no return value. Once done, we call the startAnimation()
method of our property animator:
@IBAction
func
animatingViewTapped
(
_
sender
:
AnyObject
)
{
let
animator
=
UIViewPropertyAnimator
(
duration
:
1.0
,
curve
:
.
easeIn
){
[
weak
animatingView
,
weak
self
]
in
guard
let
view
=
animatingView
,
let
strongSelf
=
self
,
let
viewBackgroundColor
=
view
.
backgroundColor
else
{
return
}
view
.
backgroundColor
=
strongSelf
.
randomColor
(
notEqualTo
:
viewBackgroundColor
)
}
animator
.
startAnimation
()
}
Have a look at the code now in the simulator. When you see the view in the center of the screen, tap on it and watch how the background color changes!
Xcode now allows you to simulate screens the way the user sees them in special environments known as playgrounds. Follow these steps to add a live view to your playground:
PlaygroundSupport
framework into your playground with the import
statement.UIView
or UIViewController
to the PlaygroundPage.current.liveView
property, which is of type PlaygroundLiveViewable?
.Live views are great for seeing what you’re doing while making rapid changes to a view or a view controller. The traditional way of making rapid changes to a view or a view controller and seeing the changes was to write the code first, then compile and run the application, which takes a lot more time than seeing your changes live in the playground.
The liveView
property of the current playground is of type PlaygroundLiveViewable?
, which itself is a protocol that is defined as shown here:
public
protocol
PlaygroundLiveViewable
{
/// A custom `PlaygroundLiveViewRepresentation` for this instance.
///
/// The value of this property can but does not need to be the same every time;
/// PlaygroundLiveViewables may choose to create a new view or view controller
/// every time.
/// - seealso: `PlaygroundLiveViewRepresentation`
public
var
playgroundLiveViewRepresentation
:
PlaygroundSupport
.
PlaygroundLiveViewRepresentation
{
get
}
}
It expects conforming objects to it to implement a playgroundLiveViewRepresentation
property of type PlaygroundSupport.PlaygroundLiveViewRepresentation
. That’s an enumeration defined in this way:
public
enum
PlaygroundLiveViewRepresentation
{
/// A view which will be displayed as the live view.
///
/// - note: This view must be the root of a view hierarchy
/// (i.e., it must not have a superview), and it must *not* be
/// owned by a view controller.
case
view
(
UIView
)
/// A view controller whose view will be displayed as the live
/// view.
/// - note: This view controller must be the root of a view
/// controller hierarchy (i.e., it has no parent view controller),
/// and its view must *not* have a superview.
case
viewController
(
UIViewController
)
}
In other words, every UIView
or UIViewController
instance can be placed inside the liveView
property:
import
UIKit
import
PlaygroundSupport
extension
Double
{
var
toSize
:
CGSize
{
return
.
init
(
width
:
self
,
height
:
self
)
}
}
extension
CGSize
{
var
toRectWithZeroOrigin
:
CGRect
{
return
CGRect
(
origin
:
.
zero
,
size
:
self
)
}
}
let
view
=
UIView
(
frame
:
300.
toSize
.
toRectWithZeroOrigin
)
view
.
backgroundColor
=
.
blue
PlaygroundPage
.
current
.
liveView
=
view
This means that custom objects that can be represented and drawn in a UIView
instance, such as a Person
structure, can conform to the PlaygroundLiveViewable
protocol and then be assigned to the liveView
property of your playground. This procedure allows you to modify the view representation of the object rapidly and see the changes immediately in the playground.
You want your playground code to have a main loop to emulate a real iOS app that doesn’t just run from start to finish, but rather lives for as long as the user presses the stop (or home) button. This will allow you to create interactive applications even in your playgrounds, when mixed with what you learned in Recipe 6.2.
Let’s have a look at an example. Say that you are designing a view similar to the one we saw in Recipe 6.2 and you are testing the addition of a new tap gesture recognizer. You want to make sure you get a callback when the user taps on the view. Follow these steps:
Make sure to ask for infinite execution time for your playground so that your app can run until you tap on the view, at which point your code can take action, such as to terminate execution:
import
UIKit
import
PlaygroundSupport
PlaygroundPage
.
current
.
needsIndefiniteExecution
=
true
Subclass UIView
and add your own tap gesture recognizer to it upon initialization. When the tap has come in, finish the execution of the playground with PlaygroundPage.current.finishExecution():
class
TappableView
:
UIView
{
@objc
func
handleTaps
(
_
sender
:
UITapGestureRecognizer
){
PlaygroundPage
.
current
.
finishExecution
()
}
override
init
(
frame
:
CGRect
)
{
super
.
init
(
frame
:
frame
)
let
recognizer
=
UITapGestureRecognizer
(
target
:
self
,
action
:
#selector
(
TappableView
.
handleTaps
(
_
:)))
addGestureRecognizer
(
recognizer
)
}
required
init
?(
coder
aDecoder
:
NSCoder
)
{
fatalError
(
"init(coder:) has not been implemented"
)
}
}
liveView
of your playground:extension
Double
{
var
toSize
:
CGSize
{
return
.
init
(
width
:
self
,
height
:
self
)
}
}
extension
CGSize
{
var
toRectWithZeroOrigin
:
CGRect
{
return
CGRect
(
origin
:
.
zero
,
size
:
self
)
}
}
let
view
=
TappableView
(
frame
:
300.
toSize
.
toRectWithZeroOrigin
)
view
.
backgroundColor
=
.
blue
PlaygroundPage
.
current
.
liveView
=
view
Stacked views are the solution.
Imagine that you want to create a view that looks like Figure 6-4.
Prior to the latest Xcode version with support for stacked views, we had to set up massive amounts of constraints just to achieve a simple layout like Figure 6-4. Well, no more. Let’s head to IB and drop an image view, three labels arranged vertically, and three arranged horizontally, like the previous figure. Our image and labels look initially like Figure 6-5.
Grab the top three labels and press the little Stack button at the bottom of IB, as shown in Figure 6-6.
Now you will notice that your components are aligned as you wanted them. Now select the top stack (your vertical components). Then, from the Attributes inspector, under Spacing, choose 20. Then select your horizontal group and do the same. Bring your horizontal group up and align it to the bottom of the image view to end up with something like Figure 6-4.
You might have noticed tiny + buttons in various places inside IB. But what are they? Have you used them before? If not, you are missing out on a lot and I’m going to show you how to take advantage of them.
Size classes are encapsulated information about the dimensions of the current screen: possible values are regular, compact, and any. These sizes have been defined to stop us from thinking in terms of pixels. You either have a regular size or compact size.
Imagine your iPhone 6+ in portrait mode. The screen width is compact, and the screen height is regular. Once you go to landscape mode, your screen width is regular and your height is compact. Now imagine an iPad in portrait mode. Your screen width is regular and so is your height. Landscape, ditto.
Let’s work on a project so that we can see more clearly how this works. I want us to achieve the effect shown in Figure 6-7 when running our app on iPhone in portrait mode.
And when we go to landscape, I want us to have 10 points spacing between the items, but only when the height of the screen is compact (Figure 6-8).
We get started by creating three colorful views on our main storyboard. I leave the colors to you to decide. Select all your views and then press the little stack button (Figure 6-6) in IB to group your views horizontally. Then place your stacked view on the top left of the view with proper top and left margin spacing (see Figure 6-9).
Once done, make sure your stacked view is the selected view and then press the Resolve Auto Layout issues button (the rightmost button in Figure 6-6). Under Selected Views, choose “Reset to Suggested Constraints.”
Now choose your stack view. In the Attributes inspector, under the Spacing section, find the little + button and press it. In the pop up, choose Any Width and then under that choose Compact Height. This will give you an additional text field to write the desired spacing value for any screen width while the height of the screen is compact. In this box, set the value to 10 (see Figure 6-10).
If you run your app on an iPhone 6+ and then switch to landscape, you won’t see any spacing between the items—so what happened? The problem is that in landscape mode we are not increasing the width of our stack view. It doesn’t currently have extra width to show the spaces between the views. To account for this, let’s first add a normal width constraint to our stack view. You can do that by selecting the stack view in the list of views that you have, holding down the Control button on your keyboard, and dragging from the stack view to the stack view itself. From the pop up that appears, choose Width (see Figure 6-11).
While your stack view is selected, go to the Size inspector and double-click the Width constraint that we just created. This will allow you to edit this constraint with size classes. How awesome is that? Next to the Constant text box, I can see the value of 300. You might see a different value based on the width of the views you placed in your stack view. My views were each 100 points wide, hence x3 comes to 300 points. I can also see a little + button next to the Constant box. Press that button and add a new constant for “Any Width and Compact Height” and set the value to N+20, where N is the value of your current constant. For me N is 300, so I’ll enter the value of 320 in the new box (see Figure 6-12).
There is one more thing that we need to tell the stack view in order for it to stack our views correctly when its width changes. Select the stack view and, in the Attributes inspector, under the Distribution section, change the default value to Equal Spacing. Now run your app and enjoy the awesomeness that you just created. Rotate from portrait to landscape under any iPhone simulator (not iPad).
Layout anchors are very useful for arranging your components on the screen. Let’s say that you have two buttons on your view, arranged horizontally, and you want the second button to be placed 10 points to the right of the first button.
First create two buttons on your view using IB and then place them next to each other, horizontally. The horizontal space between them does not matter so much right now. Then select both of them and in the Resolve Auto Layout issues button (rightmost button in Figure 6-6), under the Selected Views, choose the Add Missing Constraints option (see Figure 6-13).
Then select the second button (on the right). Under the Size inspector, find the “Leading Space to” constraint, double-click it, and choose the “Remove at build time” option (see Figure 6-14). This will make sure that the leading constraint, which we are going to create in code, will be present in IB while checking things out, but that during the project run the constraint will be removed, giving us the ability to replace it.
Now link your buttons into your code with names such as btn1
and btn2
. In the viewDidLoad
method of your view controller, write the following code:
override
func
viewDidLoad
()
{
super
.
viewDidLoad
()
btn2
.
leadingAnchor
.
constraint
(
equalTo
:
btn1
.
trailingAnchor
,
constant
:
10
).
isActive
=
true
}
Now run your app and see how your second button is trailing your first button horizontally with a 10-point space between them. You can use the following anchors in your views:
bottomAnchor
centerXAnchor
centerYAnchor
firstBaselineAnchor
heightAnchor
lastBaselineAnchor
leadingAnchor
leftAnchor
rightAnchor
topAnchor
trailingAnchor
widthAnchor
All of these anchors are direct or indirect subclasses of the NSLayoutAnchor
class. The horizontal anchors specifically are subclasses of the NSLayoutXAxisAnchor
class and the vertical ones are subclasses of NSLayoutYAxisAnchor
.
Now, just to play with some more anchors, let’s create a view hierarchy like the one in Figure 6-15. We are going to place a red view under the first button and set the width of this view to the width of the button in our code.
In IB, drag and drop a view onto your main view and set the background color of it to red so that you can see it better. Drag and drop it so that it is aligned under the two buttons with proper left and top margins (see Figure 6-16).
Anchor the views as follows:
Now create an outlet for this red view in your code (I’ve named mine “v”) and add the following code to your viewDidLoad()
method:
v
.
widthAnchor
.
constraint
(
equalTo
:
btn2
.
widthAnchor
,
constant
:
0
).
isActive
=
true
Let’s say that we want our app to register for local notifications and then ask the user for her name once the app has been sent to the background. The user enters her name and then we come to the foreground and take action on that name.
We start by writing a method that allows us to register for local notifications:
func
registerForNotifications
(){
let
enterInfo
=
UIMutableUserNotificationAction
()
enterInfo
.
identifier
=
"enter"
enterInfo
.
title
=
"Enter your name"
enterInfo
.
behavior
=
.
textInput
// this is the key to this example
enterInfo
.
activationMode
=
.
foreground
let
cancel
=
UIMutableUserNotificationAction
()
cancel
.
identifier
=
"cancel"
cancel
.
title
=
"Cancel"
let
category
=
UIMutableUserNotificationCategory
()
category
.
identifier
=
"texted"
category
.
setActions
([
enterInfo
,
cancel
],
for
:
.
default
)
let
settings
=
UIUserNotificationSettings
(
types
:
.
alert
,
categories
:
[
category
])
UIApplication
.
shared
.
registerUserNotificationSettings
(
settings
)
}
We set the behavior
property on the UIMutableUserNotificationAction
instance to .TextInput
to allow this particular action to receive text input from the user. Now we will move on to calling this method when our app is launched:
func
application
(
_
application
:
UIApplication
,
didFinishLaunchingWithOptions
launchOptions
:
[
UIApplicationLaunchOptionsKey
:
Any
]?
=
nil
)
->
Bool
{
registerForNotifications
()
return
true
}
We also need a method to schedule a local notification whenever asked for:
func
application
(
_
application
:
UIApplication
,
didFinishLaunchingWithOptions
launchOptions
:
[
UIApplicationLaunchOptionsKey
:
Any
]?
=
nil
)
->
Bool
{
registerForNotifications
()
return
true
}
And we’ll call this method when our app is sent to the background:
func
application
(
_
application
:
UIApplication
,
didFinishLaunchingWithOptions
launchOptions
:
[
UIApplicationLaunchOptionsKey
:
Any
]?
=
nil
)
->
Bool
{
registerForNotifications
()
return
true
}
Once that is done, we will read the text that the user has entered and do our work with it (I’ll leave this to you):
func
application
(
_
application
:
UIApplication
,
handleActionWithIdentifier
identifier
:
String
?,
for
notification
:
UILocalNotification
,
withResponseInfo
responseInfo
:
[
AnyHashable
:
Any
],
completionHandler
:
@
escaping
()
->
Void
)
{
if
let
text
=
responseInfo
[
UIUserNotificationActionResponseTypedTextKey
]
as
?
String
{
(
text
)
//
TODO:
now you have access to this text
}
completionHandler
()
}
Let’s run it and then send the app to the background and see what happens (see Figure 6-18).
Then take that little bar at the bottom of the notification and drag it down to show the actions that are possible on the notification (see Figure 6-19).
Now if the user just taps the Enter button, she will see a text field and can then enter her information. Upon submitting the text, she will be redirected to our app where we will receive the text (see Figure 6-20).
For whatever reason, you might want to construct your stack views programmatically. I do not recommend this way of working with stack views because IB already can handle most of the situations where you would want to use stack views, and then some. But if you absolutely have to use stack views in your app, simply instantiate UIStackView
and pass it your arranged views.
You can also then set the axis
property to either vertical
or horizontal
. Remember to set the distribution
property as well, of type UIStackViewDistribution
. Some of the values of this type are fill
, fillEqually
, and equalSpacing
. I also like to set the spacing property of the stack view manually so that I know how much space there is between my items.
Let’s say that we want to create a stack view like Figure 6-21. The stack view is tucked to the right side of the screen and every time we press the button, a new label will be appended to the stack view.
First define a stack view in your view controller:
var
rightStack
:
UIStackView
!
Then a few handy methods for creating labels and a button:
func
lblWithIndex
(
_
idx
:
Int
)
->
UILabel
{
let
label
=
UILabel
()
label
.
text
=
"Item
(
idx
)
"
label
.
sizeToFit
()
return
label
}
func
newButton
()
->
UIButton
{
let
btn
=
UIButton
(
type
:
.
system
)
btn
.
setTitle
(
"Add new items..."
,
for
:
UIControlState
())
btn
.
addTarget
(
self
,
action
:
#selector
(
ViewController
.
addNewItem
),
for
:
.
touchUpInside
)
return
btn
}
func
addNewItem
(){
let
n
=
rightStack
.
arrangedSubviews
.
count
let
v
=
lblWithIndex
(
n
)
rightStack
.
insertArrangedSubview
(
v
,
at
:
n
-
1
)
}
The addNewItem
function will be called when the button is pressed.
When our view is loaded on the screen, we will create the stack view and fill it with the three initial labels and the button. Then we will set up its axis, spacing, and distribution. Once done, we’ll create its constraints:
override
func
viewDidLoad
()
{
super
.
viewDidLoad
()
rightStack
=
UIStackView
(
arrangedSubviews
:
[
lblWithIndex
(
1
),
lblWithIndex
(
2
),
lblWithIndex
(
3
),
newButton
()])
view
.
addSubview
(
rightStack
)
rightStack
.
translatesAutoresizingMaskIntoConstraints
=
false
rightStack
.
axis
=
.
vertical
rightStack
.
distribution
=
.
equalSpacing
rightStack
.
spacing
=
5
rightStack
.
trailingAnchor
.
constraint
(
equalTo
:
view
.
trailingAnchor
,
constant
:
-
20
).
isActive
=
true
rightStack
.
topAnchor
.
constraint
(
equalTo
:
topLayoutGuide
.
bottomAnchor
).
isActive
=
true
}
Let’s go ahead and build the UI. For this recipe, I am aiming for a UI like Figure 6-22.
Then hook up the text field and button to your code. Once the button is tapped, the code that runs is:
@IBAction
func
openInSafari
()
{
guard
let
t
=
textField
.
text
,
t
.
characters
.
count
>
0
,
let
u
=
URL
(
string
:
t
)
else
{
// the URL is missing, you can further code this method if you want
return
}
let
controller
=
SFSafariViewController
(
url
:
u
,
entersReaderIfAvailable
:
true
)
controller
.
delegate
=
self
present
(
controller
,
animated
:
true
,
completion
:
nil
)
}
Now make your view controller conform to the SFSafariViewControllerDelegate
protocol. Program the safariViewControllerDidFinish(_:)
method to ensure that, when the user closes the Safari view controller, the view disappears:
func
safariViewControllerDidFinish
(
_
controller
:
SFSafariViewController
)
{
dismiss
(
animated
:
true
,
completion
:
nil
)
}
In the initializer of the Safari controller, I also specified that I would like to take advantage of the Reader Mode if it is available.
The readableContentGuide
property of UIView
gives you the margins that you need to place your text content on the screen properly. On a typical iPhone 6 screen, this margin is around 20 points on both the left and the right. The top and bottom margins on the same device are usually set near 0. But don’t take these numbers at face value. They might change and you should never think about them as hardcoded values. That is why we should use the readableContentGuide
property to place our components correctly on the screen.
There isn’t really much more to it than that, so let’s jump right into an example. In this code, I will create a label and stretch it horizontally and vertically to fill the readable section of my view. I will also make sure the top and left positioning of the label is according to the readable section’s guides:
let
label
=
UILabel
()
label
.
translatesAutoresizingMaskIntoConstraints
=
false
label
.
backgroundColor
=
UIColor
.
green
label
.
text
=
"Hello, World"
label
.
sizeToFit
()
view
.
addSubview
(
label
)
label
.
leadingAnchor
.
constraint
(
equalTo
:
view
.
readableContentGuide
.
leadingAnchor
).
isActive
=
true
label
.
topAnchor
.
constraint
(
equalTo
:
view
.
readableContentGuide
.
topAnchor
).
isActive
=
true
label
.
trailingAnchor
.
constraint
(
equalTo
:
view
.
readableContentGuide
.
trailingAnchor
).
isActive
=
true
label
.
bottomAnchor
.
constraint
(
equalTo
:
view
.
readableContentGuide
.
bottomAnchor
).
isActive
=
true
On selected devices such as iPad Air 2, the display refresh rate is 60Hz like other iOS devices, but the touch scan rate is 120Hz. This means that iOS on iPad Air 2 scans the screen for updated touch events twice as fast as the display’s refresh rate. These events obviously cannot be delivered to your app faster than the display refresh rate (60 times per second), so they are coalesced. At every touch event, you can ask for these coalesced touches and base your app’s reactions on them.
In this recipe, imagine that we are just going to draw a line based on where the user’s finger has been touching the screen. The user can move her finger over our view any way she wants and we just draw a line on that path.
Create a single view app. In the same file as your view controller’s Swift source file, define a new class of type UIView
and name it MyView
:
class
MyView
:
UIView
{
}
In your storyboard, set your view controller’s view class to MyView
(see Figure 6-23).
Make sure that you are running this code on a device at least as advanced as an iPad Air 2. iPhone 6 and 6+ do not have a 120Hz touch scan rate.
Then in your view, define an array of points and a method that can take a set of touches and an event object, read the coalesced touch points inside the event, and place them inside our array:
var
points
=
[
CGPoint
]()
func
drawForFirstTouchInSet
(
_
s
:
Set
<
UITouch
>,
event
:
UIEvent
?){
guard
let
touch
=
s
.
first
,
let
event
=
event
,
let
allTouches
=
event
.
coalescedTouches
(
for
:
touch
),
allTouches
.
count
>
0
else
{
return
}
points
+=
allTouches
.
map
{
$0
.
location
(
in
:
self
)}
setNeedsDisplay
()
}
Now when the user starts touching our view, we start recording the touch points:
override
func
touchesBegan
(
_
touches
:
Set
<
UITouch
>,
with
event
:
UIEvent
?)
{
points
.
removeAll
()
drawForFirstTouchInSet
(
touches
,
event
:
event
)
}
Should we be told that the touch events sent to our app were by accident, and that the user really meant to touch another UI component on the screen, such as the notification center, we have to clear our display:
override
func
touchesCancelled
(
_
touches
:
Set
<
UITouch
>,
with
event
:
UIEvent
?)
{
points
.
removeAll
()
setNeedsDisplay
(
bounds
)
}
Every time the touch location moves, we move with it and record the location:
override
func
touchesMoved
(
_
touches
:
Set
<
UITouch
>,
with
event
:
UIEvent
?)
{
drawForFirstTouchInSet
(
touches
,
event
:
event
)
}
Once the touches end, we also ask iOS for any predicted touch events that might have been calculated, and we will draw them too:
override
func
touchesEnded
(
_
touches
:
Set
<
UITouch
>,
with
event
:
UIEvent
?)
{
guard
let
touch
=
touches
.
first
,
let
event
=
event
,
let
predictedTouches
=
event
.
predictedTouches
(
for
:
touch
),
predictedTouches
.
count
>
0
else
{
return
}
points
+=
predictedTouches
.
map
{
$0
.
location
(
in
:
self
)}
setNeedsDisplay
()
}
Our drawing code is simple. It goes through all the points and draws lines between them:
override
func
draw
(
_
rect
:
CGRect
)
{
let
con
=
UIGraphicsGetCurrentContext
()
// set background color
con
?.
setFillColor
(
UIColor
.
black
.
cgColor
)
con
?.
fill
(
rect
)
con
?.
setFillColor
(
UIColor
.
red
.
cgColor
)
con
?.
setStrokeColor
(
UIColor
.
red
.
cgColor
)
for
point
in
points
{
con
?.
move
(
to
:
point
)
if
let
last
=
points
.
last
,
point
!=
last
{
let
next
=
points
[
points
.
index
(
of
:
point
)
!
+
1
]
con
?.
addLine
(
to
:
next
)
}
}
con
?.
strokePath
()
}
Now run this on an iPad Air 2 and compare the smoothness of the lines that you draw with those on an iPhone 6 or 6+, for instance.
Use a combination of the following:
UIView
’s userInterfaceLayoutDirectionForSemanticContentAttribute(_:)
class method to find the direction of the user interface based on the semantic attributes that are part of the UISemanticContentAttribute
enum.semanticContentAttribute
property of your views to set their semantics correctly.Let’s create an app that has a text view on top and four buttons arranged like the arrow keys on the keyboard: up, left, down, right. When each one of these buttons is pressed, we will display the corresponding word in the text field. The text field will be read-only, and when displaying right-to-left languages, it will of course show the text on the righthand side. Make sure that your UI looks (for now) something like Figure 6-24. There is one text field and four buttons.
Now select the left, down, and right buttons on the UI (exclude the up button for now) and stack them up together. In the new stack that was created, set the spacing to 20 (see Figure 6-25). Set the horizontal stack view’s spacing so that the buttons will be horizontally stacked with the proper distance from each other.
Then select the newly created stack and the up button on IB and stack those up together. This will create a vertical stack view for you. Set the spacing for this new stack view to 10. Place the main stack view at the center of the screen. Use IB’s “Resolve Auto Layout Issues” feature to add all missing constraints for all the components. Also make sure that you disable editing of the text field. Then hook up the text field to your code as an outlet and hook up the four buttons’ touch events to your view controller as well. Now your UI should look like Figure 6-26 on IB.
Now choose the main stack view in your UI. In IB, in the Semantic section under the Attributes inspector, choose Playback. This will ensure that the views inside this stack view will not be mirrored right to left when the language changes to a right-to-left language (see Figure 6-27).
Now from Xcode, create a new strings file, name it Localizable.strings, and place your string keys in there:
"up"
=
"Up"
;
"down"
=
"Down"
;
"right"
=
"Right"
;
"left"
=
"Left"
;
Under your main project’s info page in Xcode, choose Localizations and add Arabic as a localization. Then move over to your newly created strings
file and enable the Arabic language on it (see Figure 6-28).
You will now have two strings files. Go into the Arabic one and localize the file:
"up"
=
"Up in Arabic"
;
"down"
=
"Down in Arabic"
;
"right"
=
"Right in Arabic"
;
"left"
=
"Left in Arabic"
;
In your code now, we have to set the text field’s text direction based on the orientation that we get from UIView
. That orientation itself depends on the semantics that we set on our text field before:
import
UIKit
class
ViewController
:
UIViewController
{
@IBOutlet
var
txtField
:
UITextField
!
@IBAction
func
up
()
{
txtField
.
text
=
NSLocalizedString
(
"up"
,
comment
:
""
)
}
@IBAction
func
left
()
{
txtField
.
text
=
NSLocalizedString
(
"left"
,
comment
:
""
)
}
@IBAction
func
down
()
{
txtField
.
text
=
NSLocalizedString
(
"down"
,
comment
:
""
)
}
@IBAction
func
right
()
{
txtField
.
text
=
NSLocalizedString
(
"right"
,
comment
:
""
)
}
override
func
viewDidAppear
(
_
animated
:
Bool
)
{
let
direction
=
UIView
.
userInterfaceLayoutDirection
(
for
:
txtField
.
semanticContentAttribute
)
switch
direction
{
case
.
leftToRight
:
txtField
.
textAlignment
=
.
left
case
.
rightToLeft
:
txtField
.
textAlignment
=
.
right
}
}
}
Now run the app on an English device and you will see English content in the text field aligned from left to right. Run it on an Arabic localized device and you’ll see the text aligned on the righthand side.
Keyboard shortcuts are very useful for users with external keyboards. In a word processing program, the user might expect to press Command-N to create a new document, whereas on an iOS device this may be achieved by the user pressing a button such as “New.”
Let’s say that we want to write a single view app that allows users with an external keyboard to press Command-Alt-Control-N to see an alert controller. When our view is loaded, we will create the command and add it to our view controller:
override
func
viewDidLoad
()
{
super
.
viewDidLoad
()
let
command
=
UIKeyCommand
(
input
:
"N"
,
modifierFlags
:
.
command
+
.
alternate
+
.
control
,
action
:
#selector
(
ViewController
.
handleCommand
(
_
:)))
addKeyCommand
(
command
)
}
As you can see, I am using the + operator between items of type UIKeyModifierFlags
. This operator by default does not exist, so let’s write a generic operator method that enables this functionality for us:
func
+<
T
:
OptionSet
>
(
lhs
:
T
,
rhs
:
T
)
->
T
where
T
.
RawValue
:
SignedInteger
{
return
T
(
rawValue
:
lhs
.
rawValue
|
rhs
.
rawValue
)
}
When the command is issued, iOS will attempt to call the method that we have specified. In there, let’s show the alert:
func
handleCommand
(
_
cmd
:
UIKeyCommand
){
let
c
=
UIAlertController
(
title
:
"Shortcut pressed"
,
message
:
"You pressed the shortcut key"
,
preferredStyle
:
.
alert
)
c
.
addAction
(
UIAlertAction
(
title
:
"Ok!"
,
style
:
.
destructive
,
handler
:
nil
))
present
(
c
,
animated
:
true
,
completion
:
nil
)
}
Open this in the simulator. From the Hardware menu, select Keyboard, and then select the Connect Hardware Keyboard menu item (see Figure 6-29). While the focus is on the simulator, press the aforementioned key combinations and see the results for yourself.
ReplayKit
.ReplayKit
, get a recorder of type RPScreenRecorder
using RPScreenRecorder.sharedRecorder()
.available
property of the recorder to see whether recording is available.delegate
property of the recorder to your code and conform to the RPScreenRecorderDelegate
protocol.startRecordingWithMicrophoneEnabled(_:handler:)
method of the recorder.stopRecordingWithHandler(_:)
method on the same recorder object.RPPreviewViewController
class.previewControllerDelegate
property of the preview controller to your code and conform to the RPPreviewViewControllerDelegate
protocol.The ability to record what’s happening on the screen often comes in handy for users, particularly gamers who might want to share a particularly cool sequence of game play with their friends. To enable this, we first need to define our view controller:
import
UIKit
import
ReplayKit
class
ViewController
:
UIViewController
,
RPScreenRecorderDelegate
,
RPPreviewViewControllerDelegate
{
...
Set up your UI as shown in Figure 6-30. The start and stop buttons are self-explanatory. The segmented control is there just so you can play with it while recording and then see the results after you’ve stopped the playback.
I hook up the buttons to my code:
@IBOutlet
var
startBtn
:
UIButton
!
@IBOutlet
var
stopBtn
:
UIButton
!
And here I’ll define my delegate methods:
func
previewControllerDidFinish
(
_
previewController
:
RPPreviewViewController
)
{
(
"Finished the preview"
)
dismiss
(
animated
:
true
,
completion
:
nil
)
startBtn
.
isEnabled
=
true
stopBtn
.
isEnabled
=
false
}
func
previewController
(
_
previewController
:
RPPreviewViewController
,
didFinishWithActivityTypes
activityTypes
:
Set
<
String
>)
{
(
"Preview finished activities
(
activityTypes
)
"
)
}
func
screenRecorderDidChangeAvailability
(
_
screenRecorder
:
RPScreenRecorder
)
{
(
"Screen recording availability changed"
)
}
func
screenRecorder
(
_
screenRecorder
:
RPScreenRecorder
,
didStopRecordingWithError
error
:
Error
,
previewViewController
:
RPPreviewViewController
?)
{
(
"Screen recording finished"
)
}
The previewControllerDidFinish(_:)
method is important, because it gets called when the user is finished with the preview controller. Here you’ll need to dismiss the preview controller.
Then I’ll define my recorder object:
let
recorder
=
RPScreenRecorder
.
shared
()
When the record button is pressed, I’ll see whether recording is possible:
startBtn
.
isEnabled
=
true
stopBtn
.
isEnabled
=
false
guard
recorder
.
isAvailable
else
{
(
"Cannot record the screen"
)
return
}
If it is, I’ll start recording:
recorder
.
delegate
=
self
recorder
.
startRecording
{[
weak
self
]
err
in
guard
let
strongSelf
=
self
else
{
return
}
if
let
error
=
err
as
?
NSError
{
if
error
.
code
==
RPRecordingErrorCode
.
userDeclined
.
rawValue
{
(
"User declined app recording"
)
}
else
if
error
.
code
==
RPRecordingErrorCode
.
insufficientStorage
.
rawValue
{
(
"Not enough storage to start recording"
)
}
else
{
(
"Error happened =
(
err
!
)
"
)
}
return
}
else
{
(
"Successfully started recording"
)
strongSelf
.
startBtn
.
isEnabled
=
false
strongSelf
.
stopBtn
.
isEnabled
=
true
}
}
I am checking the error codes for specific ReplayKit
errors such as RPRecordingErrorCode.UserDeclined
and RPRecordingErrorCode.InsufficientStorage
.
The first time you attempt to record the user screen in any app, the user will be prompted to allow or disallow this with a dialog that looks similar to that shown in Figure 6-31.
Now when the user is finished recording and presses the stop button, I’ll stop the recording and present the preview controller:
recorder
.
stopRecording
{
controller
,
err
in
guard
let
previewController
=
controller
,
err
==
nil
else
{
self
.
startBtn
.
isEnabled
=
true
self
.
stopBtn
.
isEnabled
=
false
(
"Failed to stop recording"
)
return
}
previewController
.
previewControllerDelegate
=
self
self
.
present
(
previewController
,
animated
:
true
,
completion
:
nil
)
}
The preview controller looks like that shown in Figure 6-32.
Throughout this whole process, your app doesn’t get direct access to the recorded content. This protects the user’s privacy.