Apple has added quite a few things to UIKit in iOS 9 worth knowing about. One of my favorites is stacked views. We’ll check them out soon. We will also have a look at content sizes, unwind segues, layout guides, and more.
Stacked views are the solution.
Imagine that you want to create a view that looks like Figure 3-1.
Prior to Xcode 7 and its stacked views, we had to set up massive amounts of constraints just to achieve a simple layout like Figure 3-1. 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 3-2.
Grab the top three labels and press the little Stack button at the bottom of IB as shown in Figure 3-3.
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 3-1.
Recipe 3.3, Recipe 3.2, and Recipe 3.7
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 to get the idea more clearly. I want us to achieve the effect shown in Figure 3-4 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 3-5).
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 3-3) 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 3-6).
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 3-3). 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 popup, 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. Put the value of 10 in this box (see Figure 3-7).
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. You will now get a popup. Choose Width in this popup (see Figure 3-8).
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 3-9).
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 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 3-3), under the Selected Views, choose the Add Missing Constraints option (see Figure 3-10).
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 3-11). 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
.
constraintEqualToAnchor
(
btn1
.
trailingAnchor
,
constant
:
10
).
active
=
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 3-12. 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 3-13).
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
.
constraintEqualToAnchor
(
btn2
.
widthAnchor
,
constant
:
0
).
active
=
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
],
forContext
:
.
Default
)
let
settings
=
UIUserNotificationSettings
(
forTypes
:
.
Alert
,
categories
:
[
category
])
UIApplication
.
sharedApplication
()
.
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
:
[
NSObject
:
AnyObject
]
?
)
->
Bool
{
registerForNotifications
()
return
true
}
We also need a method to schedule a local notification whenever asked for:
func
scheduleNotification
(){
let
n
=
UILocalNotification
()
let
c
=
NSCalendar
.
autoupdatingCurrentCalendar
()
let
comp
=
c
.
componentsInTimeZone
(
c
.
timeZone
,
fromDate
:
NSDate
())
comp
.
second
+=
3
let
date
=
c
.
dateFromComponents
(
comp
)
n
.
fireDate
=
date
n
.
alertBody
=
"Please enter your name now"
n
.
alertAction
=
"Enter"
n
.
category
=
"texted"
UIApplication
.
sharedApplication
().
scheduleLocalNotification
(
n
)
}
And we’ll call this method when our app is sent to the background:
func
applicationDidEnterBackground
(
application
:
UIApplication
)
{
scheduleNotification
()
}
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
?
,
forLocalNotification
notification
:
UILocalNotification
,
withResponseInfo
responseInfo
:
[
NSObject
:
AnyObject
],
completionHandler
:
()
->
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 3-15).
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 3-16).
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 3-17).
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 3-18. 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..."
,
forState
:
.
Normal
)
btn
.
addTarget
(
self
,
action
:
"addNewItem"
,
forControlEvents
:
.
TouchUpInside
)
return
btn
}
func
addNewItem
(){
let
n
=
rightStack
.
arrangedSubviews
.
count
let
v
=
lblWithIndex
(
n
)
rightStack
.
insertArrangedSubview
(
v
,
atIndex
:
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
.
constraintEqualToAnchor
(
view
.
trailingAnchor
,
constant
:
-
20
).
active
=
true
rightStack
.
topAnchor
.
constraintEqualToAnchor
(
topLayoutGuide
.
bottomAnchor
).
active
=
true
}
Recipe 3.2 and Recipe 3.3
Let’s go ahead and build the UI. For this recipe, I am aiming for a UI like Figure 3-19.
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
where
t
.
characters
.
count
>
0
,
let
u
=
NSURL
(
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
presentViewController
(
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
)
{
dismissViewControllerAnimated
(
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 just see 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
.
greenColor
()
label
.
text
=
"Hello, World"
label
.
sizeToFit
()
view
.
addSubview
(
label
)
label
.
leadingAnchor
.
constraintEqualToAnchor
(
view
.
readableContentGuide
.
leadingAnchor
).
active
=
true
label
.
topAnchor
.
constraintEqualToAnchor
(
view
.
readableContentGuide
.
topAnchor
).
active
=
true
label
.
trailingAnchor
.
constraintEqualToAnchor
(
view
.
readableContentGuide
.
trailingAnchor
).
active
=
true
label
.
bottomAnchor
.
constraintEqualToAnchor
(
view
.
readableContentGuide
.
bottomAnchor
).
active
=
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 3-20).
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
,
event
=
event
,
allTouches
=
event
.
coalescedTouchesForTouch
(
touch
)
where
allTouches
.
count
>
0
else
{
return
}
points
+=
allTouches
.
map
{
$
0.
locationInView
(
self
)}
setNeedsDisplay
()
}
Now when the user starts touching our view, we start recording the touch points:
override
func
touchesBegan
(
touches
:
Set
<
UITouch
>
,
withEvent
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
>?
,
withEvent
event
:
UIEvent
?
)
{
points
.
removeAll
()
setNeedsDisplayInRect
(
bounds
)
}
Every time the touch location moves, we move with it and record the location:
override
func
touchesCancelled
(
touches
:
Set
&
lt
;
UITouch
&
gt
;
?
,
withEvent
event
:
UIEvent
?
)
{
points
.
removeAll
()
setNeedsDisplayInRect
(
bounds
)
}
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
>
,
withEvent
event
:
UIEvent
?
)
{
guard
let
touch
=
touches
.
first
,
event
=
event
,
predictedTouches
=
event
.
predictedTouchesForTouch
(
touch
)
where
predictedTouches
.
count
>
0
else
{
return
}
points
+=
predictedTouches
.
map
{
$
0.
locationInView
(
self
)}
setNeedsDisplay
()
}
Our drawing code is simple. It goes through all the points and draws lines between them:
override
func
drawRect
(
rect
:
CGRect
)
{
let
con
=
UIGraphicsGetCurrentContext
()
//set background color
CGContextSetFillColorWithColor
(
con
,
UIColor
.
blackColor
().
CGColor
)
CGContextFillRect
(
con
,
rect
)
CGContextSetFillColorWithColor
(
con
,
UIColor
.
redColor
().
CGColor
)
CGContextSetStrokeColorWithColor
(
con
,
UIColor
.
redColor
().
CGColor
)
for
point
in
points
{
CGContextMoveToPoint
(
con
,
point
.
x
,
point
.
y
)
if
let
last
=
points
.
last
where
point
!=
last
{
let
next
=
points
[
points
.
indexOf
(
point
)
!
+
1
]
CGContextAddLineToPoint
(
con
,
next
.
x
,
next
.
y
)
}
}
CGContextStrokePath
(
con
)
}
}
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 semantic 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 3-21. There are 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 3-22). 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 3-23 on IB.
Now choose the main stack view in your UI. In IB, in the Semantic section under 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 3-24).
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 3-25).
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:
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
.
userInterfaceLayoutDirectionForSemanticContentAttribute
(
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 right hand side.
Keyboard shortcuts are very useful for users with external keyboards. Why? Since you asked, it’s because they can use keyboard shortcuts. For instance, on a document editing iOS app, 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
:
"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
:
OptionSetType
where
T
.
RawValue
:
SignedIntegerType
>
(
lhs
:
T
,
rhs
:
T
)
->
T
{
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
))
presentViewController
(
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 3-26). While the focus is on the simulator, press the aforementioned key combinations and see the results for yourself.
ReplayKit
.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(_:)
on the same recorder object.RPPreviewViewController
class.previewControllerDelegate
property of the preview controller to your code and conform to the RPPreviewViewControllerDelegate
protocol.When playing games, you might be given the option to record your screen for later playback or sharing with others. So let’s define our view controller:
import
UIKit
import
ReplayKit
class
ViewController
:
UIViewController
,
RPScreenRecorderDelegate
,
RPPreviewViewControllerDelegate
{
...
Set up your UI as shown in Figure 3-27. 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"
)
dismissViewControllerAnimated
(
true
,
completion
:
nil
)
startBtn
.
enabled
=
true
stopBtn
.
enabled
=
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
:
NSError
,
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
.
sharedRecorder
()
When the record button is pressed, I’ll see whether recording is possible:
startBtn
.
enabled
=
true
stopBtn
.
enabled
=
false
guard
recorder
.
available
else
{
(
"Cannot record the screen"
)
return
}
If it is, I’ll start recording:
recorder
.
delegate
=
self
recorder
.
startRecordingWithMicrophoneEnabled
(
true
){
err
in
guard
err
==
nil
else
{
if
err
!
.
code
==
RPRecordingErrorCode
.
UserDeclined
.
rawValue
{
(
"User declined app recording"
)
}
else
if
err
!
.
code
==
RPRecordingErrorCode
.
InsufficientStorage
.
rawValue
{
(
"Not enough storage to start recording"
)
}
else
{
(
"Error happened = (err!)"
)
}
return
}
(
"Successfully started recording"
)
self
.
startBtn
.
enabled
=
false
self
.
stopBtn
.
enabled
=
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 3-28.
Now when the user is finished recording and presses the stop button, I’ll stop the recording and present the preview controller:
recorder
.
stopRecordingWithHandler
{
controller
,
err
in
guard
let
previewController
=
controller
where
err
==
nil
else
{
self
.
startBtn
.
enabled
=
true
self
.
stopBtn
.
enabled
=
false
(
"Failed to stop recording"
)
return
}
previewController
.
previewControllerDelegate
=
self
self
.
presentViewController
(
previewController
,
animated
:
true
,
completion
:
nil
)
}
The preview controller looks like this Figure 3-29.
Throughout this whole process, your app doesn’t get direct access to the recorded content. This protects the user’s privacy.