Version 2 of watchOS gives us developers a lot more control and brings cool features to the users as well. Now that we can download files directly and get access to sensors directly on the watch, the users will benefit.
In this chapter, I am going to assume that you have a simple iOS application in Xcode already created and you want to add a watchOS 2 target to your app. So go to Xcode and create a new Target. On the new window, on the left-hand side, under the watchOS category, choose WatchKit App (see Figure 2-1) and proceed to the next stage.
In the next stage, make sure that you have enabled complications (we’ll talk about it later) and the glance scene (see Figure 2-2).
After you have created your watch extension, you want to be able to run it on the simulator. To do this, simply choose your app from the targets in Xcode and click the run button (see Figure 2-3).
Use NSURLSession
as you would on a phone, but with more consideration toward resources and the size of the file you are downloading.
Always consider whether or not you need the file immediately. If you need the file and the size is quite manageable, download it on the watch itself. If the file is big, try to download it on the companion app on the iOS device first and then send the file over to the watch, which itself takes some time.
Let’s create an interface similar to Figure 2-4 in our watch extension.
Make sure the label can contain at least four lines of text (see Figure 2-5).
Hook up your button’s action to a method in your code named download
. Also hook up your label to code under the name statusLbl
.
import
WatchKit
import
Foundation
class
InterfaceController
:
WKInterfaceController
,
NSURLSessionDelegate
,
NSURLSessionDownloadDelegate
{
@
IBOutlet
var
statusLbl
:
WKInterfaceLabel
!
var
status
:
String
=
""
{
didSet
{
dispatch_async
(
dispatch_get_main_queue
()){[
unowned
self
]
in
self
.
statusLbl
.
setText
(
self
.
status
)
}
}
}
...
Because NSURLSession
delegate methods get called on private queues (not the main thread), I’ve coded a property on our class called status
. This is a string
property that functions on the private thread can set to indicate what they’re doing, and that is displayed as the text on our label by the main thread.
The most important method of the NSURLSessionDownloadDelegate
protocol that we are going to have to implement is the URLSession(_:downloadTask:didFinishDownloadingToURL:)
method. It gets called when our file has been downloaded into a URL onto the disk, accessible to the watch. The file there is temporary: when this method returns, the file will be deleted by watchOS. In this method, you can do two things:
NSURLSession
’s private queue.NSFileManager
to another location that is accessible to your extension and then read it later.We are going to move this file to a location that will later be accessible to our app.
func
URLSession
(
session
:
NSURLSession
,
downloadTask
:
NSURLSessionDownloadTask
,
didFinishDownloadingToURL
location
:
NSURL
)
{
let
fm
=
NSFileManager
()
let
url
=
try
!
fm
.
URLForDirectory
(.
DownloadsDirectory
,
inDomain
:
.
UserDomainMask
,
appropriateForURL
:
location
,
create
:
true
)
.
URLByAppendingPathComponent
(
"file.txt"
)
do
{
try
fm
.
removeItemAtURL
(
url
)
try
fm
.
moveItemAtURL
(
location
,
toURL
:
url
)
self
.
status
=
"Download finished"
}
catch
let
err
{
self
.
status
=
"Error = (err)"
}
session
.
invalidateAndCancel
()
}
The task that we are going to start in order to download the file (you’ll see that soon) will have an identifier. This identifier is quite important for controlling the task after we have started it.
You can see that we also have to call the invalidateAndCancel()
method on our task so that we can reuse the same task identifier later. If you don’t do this, the next time you tap the button to redownload the item you won’t be able to.
We are then going to implement a few more useful methods from NSURLSessionDelegate
and NSURLSessionDownloadDelegate
just so we can show relevant status messages to the user as we are downloading the file:
func
URLSession
(
session
:
NSURLSession
,
downloadTask
:
NSURLSessionDownloadTask
,
didWriteData
bytesWritten
:
Int64
,
totalBytesWritten
:
Int64
,
totalBytesExpectedToWrite
:
Int64
)
{
status
=
"Downloaded (bytesWritten) bytes"
}
func
URLSession
(
session
:
NSURLSession
,
downloadTask
:
NSURLSessionDownloadTask
,
didResumeAtOffset
fileOffset
:
Int64
,
expectedTotalBytes
:
Int64
)
{
status
=
"Resuming the download"
}
func
URLSession
(
session
:
NSURLSession
,
task
:
NSURLSessionTask
,
didCompleteWithError
error
:
NSError
?
)
{
if
let
e
=
error
{
status
=
"Completed with error = (e)"
}
else
{
status
=
"Finished"
}
}
func
URLSession
(
session
:
NSURLSession
,
didBecomeInvalidWithError
error
:
NSError
?
)
{
if
let
e
=
error
{
status
=
"Invalidated (e)"
}
else
{
//no errors occurred, so that's alright
}
}
When the user taps the download button, we first define our URL:
let
url
=
NSURL
(
string
:
"http://localhost:8888/file.txt"
)
!
I am running MAMP and hosting my own file called file.txt. This URL won’t get downloaded successfully on your machine if you are not hosting the exact same file with the same name on your local machine on the same port! So I suggest that you change this URL to something that makes more sense for your app.
Then use the backgroundSessionConfigurationWithIdentifier(_:)
class method of NSURLSessionConfiguration
to create a background URL configuration that you can use with NSURLSession
:
let
id
=
"se.pixolity.app.backgroundtask"
let
conf
=
NSURLSessionConfiguration
.
backgroundSessionConfigurationWithIdentifier
(
id
)
Once all of that is done, you can go ahead and create a download task and start it (see Figure 2-6):
let
session
=
NSURLSession
(
configuration
:
conf
,
delegate
:
self
,
delegateQueue
:
NSOperationQueue
())
let
request
=
NSURLRequest
(
URL
:
url
)
session
.
downloadTaskWithRequest
(
request
).
resume
()
Import the WatchConnectivity
framework on both projects. Then use the WCSession
’s delegate of type WCSessionDelegate
to implement the sessionWatchStateDidChange(_:)
method on your iOS side and the sessionReachabilityDidChange(_:)
method on the watch side. These methods get called by WatchConnectivity
whenever the state of the companion app is changed (whether that is on the iOS side or on the watchOS side).
Both devices contain a flag called reachability that indicates whether the device can connect to the other. This is represented by a property on WCSession
called reachable
, of type Bool
. On the iOS side, if you check this flag, it tells you whether your companion watch app is reachable, and if you check it on the watchOS side, it tells you whether your companion iOS app is reachable.
The idea here is to use the WCSession
object to listen for state changes. Before doing that, we need to find out whether the session is actually supported. We do that using the isSupported()
class function of WCWCSession
. Once you know that sessions are supported, you have to do the following on the iOS app side:
WCSession.defaultSession()
.delegate
property of your session.WCSessionDelegate
.sessionWatchStateDidChange(_:)
function of your session delegate and in there, check the reachable
flag of the session.activateSession()
method of your session.Make sure that you do this in a function that can be called even if your app is launched in the background.
On the watch side, do the exact same things as you did on the iOS side, but instead of implementing the sessionWatchStateDidChange(_:)
method, implement the sessionReachabilityDidChange(_:)
method.
The sessionWatchStateDidChange(_:)
delegate method is called on the iOS side when at least one of the properties of the session changes. These properties include paired
, watchAppInstalled
, complicationEnabled
, and watchDirectoryURL
, all of type Bool
. In contrast, the sessionReachabilityDidChange(_:)
method is called on the watch only when the reachable
flag of the companion iOS app is changed, as the name of the delegate method suggests.
So on the iOS side, let’s implement an extension on WCSession
that can print all its relevant states, so that when the sessionWatchStateDidChange(_:)
method is called, we can print the session’s information:
import
UIKit
import
WatchConnectivity
extension
WCSession
{
public
func
printInfo
(){
//paired
(
"Paired: "
,
terminator
:
""
)
(
self
.
paired
?
"Yes"
:
"No"
)
//watch app installed
(
"Watch app installed: "
,
terminator
:
""
)
(
self
.
watchAppInstalled
?
"Yes"
:
"No"
)
//complication enabled
(
"Complication enabled: "
,
terminator
:
""
)
(
self
.
complicationEnabled
?
"Yes"
:
"No"
)
//watch directory
(
"Watch directory url"
,
terminator
:
""
)
(
self
.
watchDirectoryURL
)
}
}
Make your app delegate the delegate of the session as well:
@
UIApplicationMain
class
AppDelegate
:
UIResponder
,
UIApplicationDelegate
,
WCSessionDelegate
{
var
window
:
UIWindow
?
...
Now start listening for state and reachablity changes:
func
sessionReachabilityDidChange
(
session
:
WCSession
)
{
(
"Reachable: "
,
terminator
:
""
)
(
session
.
reachable
?
"Yes"
:
"No"
)
}
func
sessionWatchStateDidChange
(
session
:
WCSession
)
{
(
"Watch state is changed"
)
session
.
printInfo
()
}
Last but not least, on the iOS side, set up the session and start listening to its events:
guard
WCSession
.
isSupported
()
else
{
(
"Session is not supported"
)
return
}
let
session
=
WCSession
.
defaultSession
()
session
.
delegate
=
self
session
.
activateSession
()
Now on the watch side, in the ExtensionDelegate
class, import WatchConnectivity
and become the session delegate as well:
import
WatchKit
import
WatchConnectivity
class
ExtensionDelegate
:
NSObject
,
WKExtensionDelegate
,
WCSessionDelegate
{
...
And listen for reachablity changes:
func
sessionReachabilityDidChange
(
session
:
WCSession
)
{
(
"Reachablity changed. Reachable?"
,
terminator
:
""
)
(
session
.
reachable
?
"Yes"
:
"No"
)
}
Then in the applicationDidFinishLaunching()
function of our extension delegate, set up the session:
guard
WCSession
.
isSupported
()
else
{
(
"Session is not supported"
)
return
}
let
session
=
WCSession
.
defaultSession
()
session
.
delegate
=
self
session
.
activateSession
()
You want to transfer some plist-serializable content between your apps (iOS and watchOS). This content can be anything: for instance, information about where a user is inside a game on an iOS device, or more random information that you can serialize into a plist (strings, integers, booleans, dictionaries, and arrays). Information can be sent in either direction.
updateApplicationContext(_:)
method of your session to send the content over to the other app.session(_:didReceiveApplicationContext:)
delegate method of WCSessionDelegate
, where you will be given access to the transmitted content.The content that you transmit must be of type [String : AnyObject]
.
Various types of content can be sent between iOS and watchOS. One is plist-serializable content, also called an application context. Let’s say that you are playing a game on watchOS and you want to send the user’s game status to iOS. You can use the application context for this.
Let’s begin by creating a sample application. Create a single-view iOS app and add a watchOS target to it as well (see Figure 2-1). Design your main interface like Figure 2-7. We’ll use the top label to show the download status. The buttons are self-explanatory. The bottom label will show the pairing status between our watchOS and iOS apps.
Hook up the top label to your view controller as statusLbl
, the first button as sendBtn
, the second button as downloadBtn
, and the bottom label as reachabilityStatusLbl
. Hook up the action of the download button to a method called download()
and the send button to a method called send()
.
Download and install MAMP (it’s free) and host the following contents as a file called people.json on your local web server’s root folder:
{
"people"
:
[
{
"name"
:
"Foo"
,
"age"
:
30
},
{
"name"
:
"Bar"
,
"age"
:
50
}
]
}
Now the top part of your iOS app’s view controller should look like this:
import
UIKit
import
WatchConnectivity
class
ViewController
:
UIViewController
,
WCSessionDelegate
,
NSURLSessionDownloadDelegate
{
@
IBOutlet
var
statusLbl
:
UILabel
!
@
IBOutlet
var
sendBtn
:
UIButton
!
@
IBOutlet
var
downloadBtn
:
UIButton
!
@
IBOutlet
var
reachabilityStatusLbl
:
UILabel
!
...
When you download that JSON file, it will become a dictionary of type [String : AnyObject]
, so let’s define that as a variable in our vc:
var
people
:
[
String
:
AnyObject
]
?
{
didSet
{
dispatch_async
(
dispatch_get_main_queue
()){
self
.
updateSendButton
()
}
}
}
func
updateSendButton
(){
sendBtn
.
enabled
=
isReachable
&&
isDownloadFinished
&&
people
!=
nil
}
Setting the value of the people
variable will call the updateSendButton()
function, which in turn enables the send button only if all the following conditions are met:
The watch app is reachable.
The file is downloaded.
The file was correctly parsed into the people
variable.
Also define a variable that can write into your status label whenever the reachability flag is changed:
var
isReachable
=
false
{
didSet
{
dispatch_async
(
dispatch_get_main_queue
()){
self
.
updateSendButton
()
if
self
.
isReachable
{
self
.
reachabilityStatusLbl
.
text
=
"Watch is reachable"
}
else
{
self
.
reachabilityStatusLbl
.
text
=
"Watch is not reachable"
}
}
}
}
We need two more properties: one that sets the status label and another that keeps track of when our file is downloaded successfully:
var
isDownloadFinished
=
false
{
didSet
{
dispatch_async
(
dispatch_get_main_queue
()){
self
.
updateSendButton
()
}
}
}
var
status
:
String
?
{
get
{
return
self
.
statusLbl
.
text
}
set
{
dispatch_async
(
dispatch_get_main_queue
()){
self
.
statusLbl
.
text
=
newValue
}
}
}
All three variables (people
, isReachable
, and isDownloadFinished
) that we defined call the updateSendButton()
function so that our send button will be disabled if conditions are not met, and enabled otherwise.
Now when the download button is pressed, start a download task:
@
IBAction
func
download
()
{
//if loading HTTP content, make sure you have disabled ATS
//for that domain
let
url
=
NSURL
(
string
:
"http://localhost:8888/people.json"
)
!
let
req
=
NSURLRequest
(
URL
:
url
)
let
id
=
"se.pixolity.app.backgroundtask"
let
conf
=
NSURLSessionConfiguration
.
backgroundSessionConfigurationWithIdentifier
(
id
)
let
sess
=
NSURLSession
(
configuration
:
conf
,
delegate
:
self
,
delegateQueue
:
NSOperationQueue
())
sess
.
downloadTaskWithRequest
(
req
).
resume
()
}
After that, check if you got any errors while trying to download the file:
func
URLSession
(
session
:
NSURLSession
,
task
:
NSURLSessionTask
,
didCompleteWithError
error
:
NSError
?
)
{
if
error
!=
nil
{
status
=
"Error happened"
isDownloadFinished
=
false
}
session
.
finishTasksAndInvalidate
()
}
Now implement the URLSession(_:downloadTask:didFinishDownloadingToURL:)
method of NSURLSessionDownloadDelegate
. Inside there, tell your view controller that you have downloaded the file by setting isDownloadFinished
to true
. Then construct a more permanent URL for the temporary URL to which our JSON file was downloaded by iOS:
func
URLSession
(
session
:
NSURLSession
,
downloadTask
:
NSURLSessionDownloadTask
,
didFinishDownloadingToURL
location
:
NSURL
){
isDownloadFinished
=
true
//got the data, parse as JSON
let
fm
=
NSFileManager
()
let
url
=
try
!
fm
.
URLForDirectory
(.
DownloadsDirectory
,
inDomain
:
.
UserDomainMask
,
appropriateForURL
:
location
,
create
:
true
).
URLByAppendingPathComponent
(
"file.json"
)
...
Then move the file over:
do
{
try
fm
.
removeItemAtURL
(
url
)}
catch
{}
do
{
try
fm
.
moveItemAtURL
(
location
,
toURL
:
url
)
}
catch
{
status
=
"Could not save the file"
return
}
After that, simply read the file as a JSON file with NSJSONSerialization
:
//now read the file from url
guard
let
data
=
NSData
(
contentsOfURL
:
url
)
else
{
status
=
"Could not read the file"
return
}
do
{
let
json
=
try
NSJSONSerialization
.
JSONObjectWithData
(
data
,
options
:
.
AllowFragments
)
as
!
[
String
:
AnyObject
]
self
.
people
=
json
status
=
"Successfully downloaded and parsed the file"
}
catch
{
status
=
"Could not read the file as json"
}
Great—now go to your watch interface, place a label there, and hook it up to your code under the name statusLabel
(see Figure 2-8).
In the interface controller file, place a variable that can set the status:
import
WatchKit
import
Foundation
class
InterfaceController
:
WKInterfaceController
{
@
IBOutlet
var
statusLabel
:
WKInterfaceLabel
!
var
status
=
"Waiting"
{
didSet
{
statusLabel
.
setText
(
status
)
}
}
}
Go to your ExtensionDelegate file on the watch side and do these things:
status
that when set, will set the status
property of the interface controller:import
WatchKit
import
WatchConnectivity
struct
Person
{
let
name
:
String
let
age
:
Int
}
class
ExtensionDelegate
:
NSObject
,
WKExtensionDelegate
,
WCSessionDelegate
{
var
status
=
""
{
didSet
{
dispatch_async
(
dispatch_get_main_queue
()){
guard
let
interface
=
WKExtension
.
sharedExtension
().
rootInterfaceController
as
?
InterfaceController
else
{
return
}
interface
.
status
=
self
.
status
}
}
}
...
Now activate the session using what you learned in Recipe 2.2. I won’t write the code for that in this recipe again. Then the session will wait for the session(_:didReceiveApplicationContext:)
method of the WCSessionDelegate
protocol to come in. When that happens, just read the application context and convert it into Person
instances:
func
session
(
session
:
WCSession
,
didReceiveApplicationContext
applicationContext
:
[
String
:
AnyObject
])
{
guard
let
people
=
applicationContext
[
"people"
]
as
?
Array
<
[
String
:
AnyObject
]
>
where
people
.
count
>
0
else
{
status
=
"Did not find the people array"
return
}
var
persons
=
[
Person
]()
for
p
in
people
where
p
[
"name"
]
is
String
&&
p
[
"age"
]
is
Int
{
let
person
=
Person
(
name
:
p
[
"name"
]
as
!
String
,
age
:
p
[
"age"
]
as
!
Int
)
persons
.
append
(
person
)
}
status
=
"Received (persons.count) people from the iOS app"
}
Now run both your watch app and your iOS app. At first glance, your watch app will look like Figure 2-9.
Your iOS app in its initial state will look like Figure 2-10.
When I press the download button, my iOS app’s interface will change to Figure 2-11.
After pressing the send button, the watch app’s interface will change to something like Figure 2-12.
Call the transferUserInfo(_:)
method on your WCSession
on the sending part. On the receiving part, implement the session(_:didReceiveUserInfo:)
method of the WCSessionDelegate
protocol.
A lot of the things that I’ll refer to in this recipe have been discussed already in Recipe 2.3, so have a look if you feel a bit confused.
Create a single-view app in iOS and put your root view controller in a nav controller. Then add a watch target to your app (see this chapter’s introduction for an explanation). Make sure that your root view controller in IB looks like Figure 2-13.
Hook up the label to a variable in your code named statusLbl
and hook up the button to a variable named sendBtn
. Hook up your button’s action to a method in your code called send()
. The top of your vc should now look like:
import
UIKit
import
WatchConnectivity
class
ViewController
:
UIViewController
,
WCSessionDelegate
{
@
IBOutlet
var
statusLbl
:
UILabel
!
@
IBOutlet
var
sendBtn
:
UIButton
!
...
You also need a property that can set the status for you on your label. The property must be on the main thread, because WCSession
methods (where we may want to set our status property) usually are not called on the main thread:
var
status
:
String
?
{
get
{
return
self
.
statusLbl
.
text
}
set
{
dispatch_async
(
dispatch_get_main_queue
()){
self
.
statusLbl
.
text
=
newValue
}
}
}
When the user presses the send button, we will use the WCSession.defaultSession().transferUserInfo(_:)
method to send a simple dictionary whose only key is kCFBundleIdentifierKey
and a value that will be our Info.plist’s bundle identifier:
@
IBAction
func
send
()
{
guard
let
infoPlist
=
NSBundle
.
mainBundle
().
infoDictionary
else
{
status
=
"Could not get the Info.plist"
return
}
let
key
=
kCFBundleIdentifierKey
as
String
let
plist
=
[
key
:
infoPlist
[
key
]
as
!
String
]
let
transfer
=
WCSession
.
defaultSession
().
transferUserInfo
(
plist
)
status
=
transfer
.
transferring
?
"Sent"
:
"Could not send yet"
}
The transferUserInfo(_:)
method returns an object of type WCSessionUserInfoTransfer
that has properties such as userInfo
and transferring
and a method called cancel()
. You can always use the cancel()
method of an instance of WCSessionUserInfoTransfer
to cancel the transfer of this item if it is not already transferring
. You can also find all the user info transfers that are ongoing by using the outstandingUserInfoTransfers
property of your session object.
The app also contains code to disable the button if the watch app is not reachable, but I won’t discuss that code here because we have already discussed it in Recipe 2.2 and Recipe 2.3.
On the watch side, in InterfaceController
, write the exact same code that you wrote in Recipe 2.3. In the ExtensionDelegate
class, however, our code will be a bit different. Its status
property is exactly how we wrote it in Recipe 2.3.
When the applicationDidFinishLaunching()
method of our delegate is called, we set up the session just as we did in Recipe 2.2. We will wait for the session(_:didReceiveUserInfo:)
method of the WCSessionDelegate
protocol to be called. There, we will simply read the bundle identifier from the user info and display it in our view controller:
func
session
(
session
:
WCSession
,
didReceiveUserInfo
userInfo
:
[
String
:
AnyObject
])
{
guard
let
bundleVersion
=
userInfo
[
kCFBundleIdentifierKey
as
String
]
as
?
String
else
{
status
=
"Could not read the bundle version"
return
}
status
=
bundleVersion
}
If you run the iOS app, your UI should look like Figure 2-14.
And your watch app should look like Figure 2-15.
When you press the send button, the user interface will change to Figure 2-16.
And the watch app will look like Figure 2-17.
Recipe 2.1 and Recipe 2.3
transferFile(_:metadata:)
method of your WCSession
object on the sending device.WCSessionDelegate
protocol on the sender and wait for the session(_:didFinishFileTransfer:error:)
delegate method to be called. If the optional error
parameter is nil
, it indicates that the file is transferred successfully.WCSession
and wait for the session(_:didReceiveFile:)
delegate method to be called.WCSessionFile
and has properties such as fileURL
and metadata
. The metadata is the same metadata of type [String : AnyObject]
that the sender sent with the transferFile(_:metadata:)
method.Let’s have a look at a simple UI on the sending device (the iOS side in this example). It contains a label that shows our status and a button that sends our file. When the button is pressed, we create a file in the iOS app’s caches folder and then send that file through to the watch app if it is reachable (see Recipe 2.2).
Make your UI on the iOS (sender) side look like Figure 2-18. The button will be disabled if the watch app is not reachable (see Recipe 2.2).
Hook up your button’s action code to a method in your view controller called send()
and make sure your view controller conforms to WCSessionDelegate:
import
UIKit
import
WatchConnectivity
class
ViewController
:
UIViewController
,
WCSessionDelegate
{
@
IBOutlet
var
statusLbl
:
UILabel
!
@
IBOutlet
var
sendBtn
:
UIButton
!
var
status
:
String
?
{
get
{
return
self
.
statusLbl
.
text
}
set
{
dispatch_async
(
dispatch_get_main_queue
()){
self
.
statusLbl
.
text
=
newValue
}
}
}
...
We implemented and talked about the status
property of our view controller in Recipe 2.3, so I won’t explain it here.
Then, when the send button is pressed, construct a URL that will point to your file. It doesn’t exist yet, but you will write it to disk soon:
let
fileName
=
"file.txt"
let
fm
=
NSFileManager
()
let
url
=
try
!
fm
.
URLForDirectory
(.
CachesDirectory
,
inDomain
:
.
UserDomainMask
,
appropriateForURL
:
nil
,
create
:
true
).
URLByAppendingPathComponent
(
fileName
)
Now write some text to disk, reachable through the URL:
let
text
=
"Foo Bar"
do
{
try
text
.
writeToURL
(
url
,
atomically
:
true
,
encoding
:
NSUTF8StringEncoding
)
}
catch
{
status
=
"Could not write the file"
return
}
Once that is done, send the file over:
let
metadata
=
[
"fileName"
:
fileName
]
WCSession
.
defaultSession
().
transferFile
(
url
,
metadata
:
metadata
)
Also, when your session’s reachability state changes, enable or disable your button:
func
updateUiForSession
(
session
:
WCSession
){
status
=
session
.
reachable
?
"Ready to send"
:
"Not reachable"
sendBtn
.
enabled
=
session
.
reachable
}
func
sessionReachabilityDidChange
(
session
:
WCSession
)
{
updateUiForSession
(
session
)
}
On the watch side, make your UI look like Figure 2-8. Then, in your ExtensionDelegate
class, implement the exact same status property that we implemented in Recipe 2.3.
Now implement the session(_:didReceiveFile:)
method of WCSessionDelegate
. Start by double-checking that the metadata is as you expected it:
guard
let
metadata
=
file
.
metadata
where
metadata
[
"fileName"
]
is
String
else
{
status
=
"No metadata came through"
return
}
If it is, read the file and show it in the user interface:
do
{
let
str
=
try
String
(
NSString
(
contentsOfURL
:
file
.
fileURL
,
encoding
:
NSUTF8StringEncoding
))
guard
str
.
characters
.
count
>
0
else
{
status
=
"No file came through"
return
}
status
=
str
}
catch
{
status
=
"Could not read the file"
return
}
When you run the watch app, it will look like Figure 2-15. When you run the iOS app, it will look like Figure 2-19.
When the file is sent, your user interface on iOS will look like Figure 2-20.
And the UI on your receiver (watchOS) will look like Figure 2-21.
Recipe 2.1, Recipe 2.2, and Recipe 2.3
On the sender side, use the sendMessage(_:replyHandler:errorHandler:)
method of WCSession
. On the receiving side, implement the session(_:didReceiveMessage:replyHandler:)
method to handle the incoming message if your sender expected a reply, or implement session(_:didReceiveMessage:)
if no reply was expected from you. Messages and replies are of type [String : AnyObject]
.
Let’s implement a chat program where the iOS app and the watch app can send messages to each other. On the iOS app, we will allow the user to type text and then send it over to the watch. On the watch, since we cannot type anything, we will have four predefined messages that the user can send us. In order to decrease the amount of data the watch sends us, we define these messages as Int
and send the integers instead. The iOS app will read the integers and then print the correct message onto the screen. So let’s first define these messages. Create a file called PredefinedMessages and write the following Swift code there:
import
Foundation
enum
PredefinedMessage
:
Int
{
case
Hello
case
ThankYou
case
HowAreYou
case
IHearYou
}
Add this file to both your watch extension and your iOS app so that they both can use it (see Figure 2-22).
Now move to your main iOS app’s storyboard and design a UI that looks like Figure 2-23. There are two labels that say “...” at the moment. They will be populated dynamically in our code.
Hook up your UI to your code as follows:
sendBtn
. Hook up its action method to a function called send(_:)
in your vc.textField
.watchStatusLbl
.watchReplyLbl
.So now the top part of your vc on the iOS side should look like this:
import
UIKit
import
WatchConnectivity
class
ViewController
:
UIViewController
,
WCSessionDelegate
{
@
IBOutlet
var
sendBtn
:
UIBarButtonItem
!
@
IBOutlet
var
textField
:
UITextField
!
@
IBOutlet
var
watchStatusLbl
:
UILabel
!
@
IBOutlet
var
watchReplyLbl
:
UILabel
!
...
As we have done before, we need two variables that can populate the text inside the watchStatusLbl
and watchReplyLbl
labels, always on the main thread:
var
watchStatus
:
String
{
get
{
return
self
.
watchStatusLbl
.
text
??
""
}
set
{
onMainThread
{
self
.
watchStatusLbl
.
text
=
newValue
}}
}
var
watchReply
:
String
{
get
{
return
self
.
watchReplyLbl
.
text
??
""
}
set
{
onMainThread
{
self
.
watchReplyLbl
.
text
=
newValue
}}
}
The definition of onMainThread
is very simple. It’s a custom function I’ve written in a library to make life easier:
import
Foundation
public
func
onMainThread
(
f
:
()
->
Void
){
dispatch_async
(
dispatch_get_main_queue
(),
f
)
}
When the send button is pressed, we first have to make sure that the user has entered some text into the text field:
guard
let
txt
=
textField
.
text
where
txt
.
characters
.
count
>
0
else
{
textField
.
placeholder
=
"Enter some text here first"
return
}
Then we will use the sendMessage(_:replyHandler:errorHandler:)
method of our session to send our text over:
WCSession
.
defaultSession
().
sendMessage
([
"msg"
:
txt
],
replyHandler
:
{
dict
in
guard
dict
[
"msg"
]
is
String
&&
dict
[
"msg"
]
as
!
String
==
"delivered"
else
{
self
.
watchReply
=
"Could not deliver the message"
return
}
self
.
watchReply
=
dict
[
"msg"
]
as
!
String
}){
err
in
self
.
watchReply
=
"An error happened in sending the message"
}
Later, when we implement our watch side, we will also be sending messages from the watch over to the iOS app. Those messages will be inside a dictionary whose only key is “msg” and the value of this key will be an integer. The integers are already defined in the PredefinedMessage
enum that we saw earlier. So in our iOS app, we will wait for messages from the watch app, translate the integer we get to its string counterpart, and show it on our iOS UI. Remember, we send integers (instead of strings) from the watch to make the transfer snappier. So let’s implement the session(_:didReceiveMessage:)
delegate method in our iOS app:
func
session
(
session
:
WCSession
,
didReceiveMessage
message
:
[
String
:
AnyObject
])
{
guard
let
msg
=
message
[
"msg"
]
as
?
Int
,
let
value
=
PredefinedMessage
(
rawValue
:
msg
)
else
{
watchReply
=
"Received invalid message"
return
}
switch
value
{
case
.
Hello
:
watchReply
=
"Hello"
case
.
HowAreYou
:
watchReply
=
"How are you?"
case
.
IHearYou
:
watchReply
=
"I hear you"
case
.
ThankYou
:
watchReply
=
"Thank you"
}
}
Let’s use what we learned in Recipe 2.2 to enable or disable our send button when the watch’s reachability changes:
func
updateUiForSession
(
session
:
WCSession
){
watchStatus
=
session
.
reachable
?
"Reachable"
:
"Not reachable"
sendBtn
.
enabled
=
session
.
reachable
}
func
sessionReachabilityDidChange
(
session
:
WCSession
)
{
updateUiForSession
(
session
)
}
On the watch side, design your UI like Figure 2-24. On the watch, the user cannot type, but she can press a predefined message in order to send it (remember PredefinedMessage
?). That little line between “Waiting...” and “Send a reply” is a separator.
Hook up your watch UI to your code by following these steps:
iosAppReplyLbl
. We will show the text that our iOS app has sent to us in this label.repliesGroup
. We will hide this whole group if the iOS app is not reachable to our watch app.sendHello()
.sendThankYou()
.sendHowAreYou()
.sendIHearYou()
.In our InterfaceController
on the watch side, we need a generic method that takes in an Int
(our predefined message) and sends it over to the iOS side with the sendMessage(_:replyHandler:errorHandler:)
method of the session:
import
WatchKit
import
Foundation
import
WatchConnectivity
class
InterfaceController
:
WKInterfaceController
{
@
IBOutlet
var
iosAppReplyLbl
:
WKInterfaceLabel
!
@
IBOutlet
var
repliesGroup
:
WKInterfaceGroup
!
func
send
(
int
:
Int
){
WCSession
.
defaultSession
().
sendMessage
([
"msg"
:
int
],
replyHandler
:
nil
,
errorHandler
:
nil
)
}
...
And whenever any of the buttons is pressed, we call the send(_:)
method with the right predefined message:
@
IBAction
func
sendHello
()
{
send
(
PredefinedMessage
.
Hello
.
hashValue
)
}
@
IBAction
func
sendThankYou
()
{
send
(
PredefinedMessage
.
ThankYou
.
hashValue
)
}
@
IBAction
func
sendHowAreYou
()
{
send
(
PredefinedMessage
.
HowAreYou
.
hashValue
)
}
@
IBAction
func
sendIHearYou
()
{
send
(
PredefinedMessage
.
IHearYou
.
hashValue
)
}
In the ExtensionDelegate
class on the watch side, we want to hide all the reply buttons if the iOS app is not reachable. To do that, write a property called isReachable
of type Bool
. Whenever this property is set, the code sets the hidden
property of our replies group:
import
WatchKit
import
WatchConnectivity
class
ExtensionDelegate
:
NSObject
,
WKExtensionDelegate
,
WCSessionDelegate
{
var
isReachable
=
false
{
willSet
{
self
.
rootController
?
.
repliesGroup
.
setHidden
(
!
newValue
)
}
}
var
rootController
:
InterfaceController
?
{
get
{
guard
let
interface
=
WKExtension
.
sharedExtension
().
rootInterfaceController
as
?
InterfaceController
else
{
return
nil
}
return
interface
}
}
...
You also are going to need a String
property that will be your iOS app’s reply. Whenever you get a reply from the iOS app, place it inside this property. As soon as this property is set, the watch extension will write this text on our UI:
var
iosAppReply
=
""
{
didSet
{
dispatch_async
(
dispatch_get_main_queue
()){
self
.
rootController
?
.
iosAppReplyLbl
.
setText
(
self
.
iosAppReply
)
}
}
}
Now let’s wait for messages from the iOS app and display those messages on our UI:
func
session
(
session
:
WCSession
,
didReceiveMessage
message
:
[
String
:
AnyObject
],
replyHandler
:
([
String
:
AnyObject
])
->
Void
)
{
guard
message
[
"msg"
]
is
String
else
{
replyHandler
([
"msg"
:
"failed"
])
return
}
iosAppReply
=
message
[
"msg"
]
as
!
String
replyHandler
([
"msg"
:
"delivered"
])
}
Also when our iOS app’s reachability changes, we want to update our UI and disable the reply buttons:
func
sessionReachabilityDidChange
(
session
:
WCSession
)
{
isReachable
=
session
.
reachable
}
func
applicationDidFinishLaunching
()
{
guard
WCSession
.
isSupported
()
else
{
iosAppReply
=
"Sessions are not supported"
return
}
let
session
=
WCSession
.
defaultSession
()
session
.
delegate
=
self
session
.
activateSession
()
isReachable
=
session
.
reachable
}
Running our app on the watch first, we will see an interface similar to Figure 2-25. The user can scroll to see the rest of the buttons.
And when we run our app on iOS while the watch app is reachable, the UI will look like Figure 2-26.
Type “Hello from iOS” in the iOS UI and press the send button. The watch app will receive the message (see Figure 2-27).
Now press the How are you? button on the watch UI and see the results in the iOS app (Figure 2-28).
Recipe 2.1, Recipe 2.2, Recipe 2.3, Recipe 2.4, and Recipe 2.5
ComplicationController
class. We’ll discuss this code soon.Complications are pieces of information that apps can display on a watch face. They are divided into a few main categories:
Assuming that you have already created a watch target with a complication attached to it, go into your ComplicationController
class and find the getPlaceholderTemplateForComplication(_:withHandler:)
method. This method gets called by iOS when your complication is being added to a watch face. This gives you the chance to provide a placeholder for what the user has to see while adjusting her watch face. It won’t usually be real data.
After this method is called, you need to create a complication template of type CLKComplicationTemplate
(or one of its many subclasses) and return that into the replyHandler
block that you are given. For now, implement the template like this:
func
getPlaceholderTemplateForComplication
(
complication
:
CLKComplication
,
withHandler
handler
:
(
CLKComplicationTemplate
?
)
->
Void
)
{
let
temp
=
CLKComplicationTemplateModularSmallSimpleText
()
temp
.
textProvider
=
CLKSimpleTextProvider
(
text
:
"22"
)
handler
(
temp
)
}
I am not going to discuss the details of this code right now. You’ll learn them in other recipes in this chapter.
One more thing that you have to know is that once you have provided watchOS with your placeholder template, you won’t be asked to do it again unless the user uninstalls your watchOS app and installs it again from her iPhone (see Figure 2-38).
If you are working on the getPlaceholderTemplateForComplication(_:withHandler:)
method and want to test out different templates, you can simply reset the watch simulator and then run your app again. This will retrigger the getPlaceholderTemplateForComplication(_:withHandler:)
method on your complication controller.
Recipe 2.8 and Recipe 2.9
You want to construct a small-modular complication and provide the user with past, present, and future data. In this example, a small modular complication (Figure 2-39, bottom left) shows the current hour with a ring swallowing it. The ring is divided into 24 sections and increments for every 1 hour in the day. At the end of the day, the ring will be completely filled and the number inside the ring will show 24.
getSupportedTimeTravelDirectionsForComplication(_:withHandler:)
method of the CLKComplicationDataSource
protocol. In this method, return your supported time travel directions (more on this later). The directions are of type CLKComplicationTimeTravelDirections
.getTimelineStartDateForComplication(_:withHandler:)
method inside your complication class and call the given handler with an NSDate
that indicates the start date of your available data.getTimelineEndDateForComplication(_:withHandler:)
method of your complication and call the handler with the last date for which your data is valid.getTimelineEntriesForComplication(_:beforeDate:limit:withHandler:)
method of your complication, create an array of type CLKComplicationTimelineEntry
, and send that array into the given handler object. These will be the timeline entries before the given date that you would want to return to the watch (more on this later).getTimelineEntriesForComplication(_:afterDate:limit:withHandler:)
method of your complication and return all the events that your complication supports, after the given date.getNextRequestedUpdateDateWithHandler(_:)
method of your complication and let watchOS know when it has to ask you next for more content.When providing complications, you are expected to provide data to the watchOS as the time changes. In our example, for every hour in the day, we want to change our complication. So each day we’ll return 24 events to the runtime.
With the digital crown on the watch, the user can scroll up and down while on the watch face to engage in a feature called “time travel.” This allows the user to change the time known to the watch just so she can see how various components on screen change with the new time. For instance, if you provide a complication to the user that shows all football match results of the day, the user can then go back in time a few hours to see the results of a match she has just missed. Similarly, in the context of a complication that shows the next fast train time to the city where the user lives, she can scroll forward, with the digital crown on the watch face, to see the future times that the train leaves from the current station.
The time is an absolute value on any watch, so let’s say that you want to provide the time of the next football match in your complication. Let’s say it’s 14:00 right now and the football match starts at 15:00. If you give 15:00 as the start of that event to your complication, watchOS will show the football match (or the data that you provide for that match to your user through your complication) to the user at 15:00, not before. That is a bit useless, if you ask me. You want to provide that information to the user before the match starts so she knows what to look forward to, and when. So keep that in mind when providing a starting date for your events.
watchOS complications conform to the CLKComplicationDataSource
protocol. They get a lot of delegate messages from this protocol calling methods that you have to implement even if you don’t want to return any data. For instance, in the getNextRequestedUpdateDateWithHandler(_:)
method, you get a handler as a parameter that you must call with an NSDate
object, specifying when you want to be asked for more data next time. If you don’t want to be asked for any more data, you still have to call this handler object but with a nil
date. You’ll find out soon that most of these handlers ask for optional values, so you can call them with nil
if you want to.
While working with complications, you can tell watchOS which directions of time travel you support, or if you support time travel at all. If you don’t support it, your complication returns only data for the current time. And if the user scrolls the watch face with the digital crown, your complication won’t update its information. I don’t suggest you opt out of time travel unless your complication really cannot provide relevant data to the user. Certainly, if your complication shows match results, it cannot show results for matches that have not happened. But even then, you can still support forward and backward time travel. If the user chooses forward time travel, just hide the scores, show a question mark, or do something similar.
As you work with complications, it’s important to construct a data model to return to the watch. What you usually return to the watch for your complication is either of type CLKComplicationTemplate
or of type CLKComplicationTimelineEntry
. The template defines how your data is viewed on screen. The timeline entry only binds your template (your visible data) to a date of type NSDate
that dictates to the watch when it has to show your data. As simple as that. In the case of small-modular complications, you can provide the following templates to the watch:
CLKComplicationTemplateModularSmallSimpleText
CLKComplicationTemplateModularSmallSimpleImage
CLKComplicationTemplateModularSmallRingText
CLKComplicationTemplateModularSmallRingImage
CLKComplicationTemplateModularSmallStackText
CLKComplicationTemplateModularSmallStackImage
CLKComplicationTemplateModularSmallColumnsText
As you saw in Figure 2-33, this example bases our small-modular template on CLKComplicationTemplateModularSmallRingText
. So we provide only a text (the current hour) and a value between 0 and 1 that will tell watchOS how much of the ring around our number it has to fill (0...100%).
Let’s now begin defining our data for this example. For every hour, we want our template to show the current hour. Just before midnight, we provide another 24 new complication data points for that day to the watch. So let’s define a data structure that can contain a date, the hour value, and the fraction (between 0...1) to set for our complication. Start off by creating a file called DataProvider.swift and write all this code in that:
protocol
WithDate
{
var
hour
:
Int
{
get
}
var
date
:
NSDate
{
get
}
var
fraction
:
Float
{
get
}
}
Now we can define our actual structure that conforms to this protocol:
struct
Data
:
WithDate
{
let
hour
:
Int
let
date
:
NSDate
let
fraction
:
Float
var
hourAsStr
:
String
{
return
"(hour)"
}
}
Later, when we work on our complication, we will be asked to provide, inside the getCurrentTimelineEntryForComplication(_:withHandler:)
method of CLKComplicationDataSource
, a template to show to the user for the current time. We are also going to create an array of 24 Data
structures. So it would be great if we could always, inside this array, easily find the Data
object for the current date:
extension
NSDate
{
func
hour
()
->
Int
{
let
cal
=
NSCalendar
.
currentCalendar
()
return
cal
.
components
(
NSCalendarUnit
.
Hour
,
fromDate
:
self
).
hour
}
}
extension
CollectionType
where
Generator
.
Element
:
WithDate
{
func
dataForNow
()
->
Generator
.
Element
?
{
let
thisHour
=
NSDate
().
hour
()
for
d
in
self
{
if
d
.
hour
==
thisHour
{
return
d
}
}
return
nil
}
}
The dataForNow()
function goes through any collection that has objects that conform to the WithDate
protocol that we specified earlier, and finds the object whose current hour is the same as that returned for the current moment by NSDate()
.
Let’s now create our array of 24 Data
objects. We do this by iterating from 1 to 24, creating NSDate
objects using NSDateComponents
and NSCalendar
. Then, using those objects, we construct instances of the Data
structure that we just wrote:
struct
DataProvider
{
func
allDataForToday
()
->
[
Data
]{
var
all
=
[
Data
]()
let
now
=
NSDate
()
let
cal
=
NSCalendar
.
currentCalendar
()
let
units
=
NSCalendarUnit
.
Year
.
union
(.
Month
).
union
(.
Day
)
let
comps
=
cal
.
components
(
units
,
fromDate
:
now
)
comps
.
minute
=
0
comps
.
second
=
0
for
i
in
1.
.
.24
{
comps
.
hour
=
i
let
date
=
cal
.
dateFromComponents
(
comps
)
!
let
fraction
=
Float
(
comps
.
hour
)
/
24.0
let
data
=
Data
(
hour
:
comps
.
hour
,
date
:
date
,
fraction
:
fraction
)
all
.
append
(
data
)
}
return
all
}
}
That was our entire data model. Now let’s move onto the complication class of our watch app. In the getNextRequestedUpdateDateWithHandler(_:)
method of the CLKComplicationDataSource
protocol to which our complication conforms, we are going to be asked when watchOS should next call our complication and ask for new data. Because we are going to provide data for the whole day, today, we would want to be asked for new data for tomorrow. So we need to ask to be updated a few seconds before the start of the next day. For that, we need an NSDate
object that tells watchOS when the next day is. So let’s extend NSDate
:
extension
NSDate
{
class
func
endOfToday
()
->
NSDate
{
let
cal
=
NSCalendar
.
currentCalendar
()
let
units
=
NSCalendarUnit
.
Year
.
union
(
NSCalendarUnit
.
Month
)
.
union
(
NSCalendarUnit
.
Day
)
let
comps
=
cal
.
components
(
units
,
fromDate
:
NSDate
())
comps
.
hour
=
23
comps
.
minute
=
59
comps
.
second
=
59
return
cal
.
dateFromComponents
(
comps
)
!
}
}
Moving to our complication, let’s define our data provider first:
class
ComplicationController
:
NSObject
,
CLKComplicationDataSource
{
let
dataProvider
=
DataProvider
()
...
We know that our data provider can give us an array of Data
objects, so we need a way of turning those objects into our templates so they that can be displayed on the screen:
func
templateForData
(
data
:
Data
)
->
CLKComplicationTemplate
{
let
template
=
CLKComplicationTemplateModularSmallRingText
()
template
.
textProvider
=
CLKSimpleTextProvider
(
text
:
data
.
hourAsStr
)
template
.
fillFraction
=
data
.
fraction
template
.
ringStyle
=
.
Closed
return
template
}
Our template of type CLKComplicationTemplateModularSmallRingText
has a few important properties:
textProvider
of type CLKTextProvider
CLKTextProvider
directly, though. We use one of its subclasses, such as the CLKSimpleTextProvider
class. There are other text providers that we will talk about later.fillFraction
of type Float
ringStyle
of type CLKComplicationRingStyle
Open
or Closed
.Later we are also going to be asked for timeline entries of type CLKComplicationTimelineEntry
for the data that we provide to watchOS. So for every Data
object, we need to be able to create a timeline entry:
func
timelineEntryForData
(
data
:
Data
)
->
CLKComplicationTimelineEntry
{
let
template
=
templateForData
(
data
)
return
CLKComplicationTimelineEntry
(
date
:
data
.
date
,
complicationTemplate
:
template
)
}
In this example, we support forward and backward time travel (of type CLKComplicationTimeTravelDirections
) so let’s tell watchOS that:
func
getSupportedTimeTravelDirectionsForComplication
(
complication
:
CLKComplication
,
withHandler
handler
:
(
CLKComplicationTimeTravelDirections
)
->
Void
)
{
handler
([.
Forward
,
.
Backward
])
}
If you don’t want to support time travel, call the handler
argument with the value of CLKComplicationTimeTravelDirections.None
.
The next thing we have to do is implement the getTimelineStartDateForComplication(_:withHandler:)
method of CLKComplicationDataSource
. This method gets called on our delegate whenever watchOS wants to find out the beginning of the date/time range of our time travel. For our example, since we want to provide 24 templates, one for each hour in the day, we tell watchOS the date of the first template:
func
getTimelineStartDateForComplication
(
complication
:
CLKComplication
,
withHandler
handler
:
(
NSDate
?
)
->
Void
)
{
handler
(
dataProvider
.
allDataForToday
().
first
!
.
date
)
}
Similarly, for the getTimelineEndDateForComplication(_:withHandler:)
method, we provide the date of the last event:
func
getTimelineEndDateForComplication
(
complication
:
CLKComplication
,
withHandler
handler
:
(
NSDate
?
)
->
Void
)
{
handler
(
dataProvider
.
allDataForToday
().
last
!
.
date
)
}
Complications can be displayed on the watch’s lock screen. Some complications might contain sensitive data, so they might want to opt out of appearing on the lock screen. For this, we have to implement the getPrivacyBehaviorForComplication(_:withHandler:)
method as well. We call the handler with an object of type CLKComplicationPrivacyBehavior
, such as ShowOnLockScreen
or HideOnLockScreen
. Because we don’t have any sensitive data, we show our complication on the lock screen:
func
getPrivacyBehaviorForComplication
(
complication
:
CLKComplication
,
withHandler
handler
:
(
CLKComplicationPrivacyBehavior
)
->
Void
)
{
handler
(.
ShowOnLockScreen
)
}
Now to the stuff that I like. The getCurrentTimelineEntryForComplication(_:withHandler:)
method will get called on our delegate whenever the runtime needs to get the complication timeline (the template plus the date to display) for the complication to display no. Do you remember the dataForNow()
method that we wrote a while ago as an extension on CollectionType
? Well, we are going to use that now:
func
getCurrentTimelineEntryForComplication
(
complication
:
CLKComplication
,
withHandler
handler
:
((
CLKComplicationTimelineEntry
?
)
->
Void
))
{
if
let
data
=
dataProvider
.
allDataForToday
().
dataForNow
(){
handler
(
timelineEntryForData
(
data
))
}
else
{
handler
(
nil
)
}
}
Always implement the handlers that the class gives you. If they accept optional values and you don’t have any data to pass, just pass nil
.
Now we have to implement the getTimelineEntriesForComplication(_:beforeDate:limit:beforeDate:)
method of our complication delegate. This method gets called whenever watchOS needs timeline entries for data before a certain date, with a maximum of limit entries. So let’s say that you have 1,000 templates to return but the limit is 100. Do not return more than 100 in that case. In our example, I will go through all the data items that we have, filter them by their dates, find the ones coming before the given date (the beforeDate
parameter), and create a timeline entry for all of those with the timelineEntryForData(_:)
method that we wrote:
func
getTimelineEntriesForComplication
(
complication
:
CLKComplication
,
beforeDate
date
:
NSDate
,
limit
:
Int
,
withHandler
handler
:
(([
CLKComplicationTimelineEntry
]
?
)
->
Void
))
{
let
entries
=
dataProvider
.
allDataForToday
().
filter
{
date
.
compare
(
$
0.
date
)
==
.
OrderedDescending
}.
map
{
self
.
timelineEntryForData
(
$
0
)
}
handler
(
entries
)
}
Similarly, we have to implement the getTimelineEntriesForComplication(_:afterDate:limit:withHandler:)
method to return the timeline entries after a certain date (afterDate
parameter):
func
getTimelineEntriesForComplication
(
complication
:
CLKComplication
,
afterDate
date
:
NSDate
,
limit
:
Int
,
withHandler
handler
:
(([
CLKComplicationTimelineEntry
]
?
)
->
Void
))
{
let
entries
=
dataProvider
.
allDataForToday
().
filter
{
date
.
compare
(
$
0.
date
)
==
.
OrderedAscending
}.
map
{
self
.
timelineEntryForData
(
$
0
)
}
handler
(
entries
)
}
The getNextRequestedUpdateDateWithHandler(_:)
method is the next method we need to implement. This method gets called to ask us when we would like to be asked for more data later. For our app we specify the next day, because we have already provided all the data for today:
func
getNextRequestedUpdateDateWithHandler
(
handler
:
(
NSDate
?
)
->
Void
)
{
handler
(
NSDate
.
endOfToday
());
}
Last but not least, we have to implement the getPlaceholderTemplateForComplication(_:withHandler:)
method that we talked about before. This is where we provide our placeholder template:
func
getPlaceholderTemplateForComplication
(
complication
:
CLKComplication
,
withHandler
handler
:
(
CLKComplicationTemplate
?
)
->
Void
)
{
if
let
data
=
dataProvider
.
allDataForToday
().
dataForNow
(){
handler
(
templateForData
(
data
))
}
else
{
handler
(
nil
)
}
}
Now when I run my app on my watch, because the time is 10:24 and the hour is 10, our complication will show 10 and fill the circle around it to show how much of the day has passed by 10:00 (see Figure 2-40).
And if I engage time travel and move forward to 18:23, our complication updates itself as well, showing 18 as the hour (see Figure 2-41).
In this recipe, let’s create a watch app that shows the next available train that the user can take to get home. Trains can have different properties:
In our example, I want the complication to look like Figure 2-42. The complication shows the next train (a Coastal service) and how many minutes away that train departs.
When you create your watchOS project, enable only the modular large complication in the target settings (see Figure 2-43).
Now create your data model. It will be similar to what we did in Recipe 2.8, but this time we want to provide train times. For the train type and the train company, create enumerations:
enum
TrainType
:
String
{
case
HighSpeed
=
"High Speed"
case
Commuter
=
"Commuter"
case
Coastal
=
"Coastal"
}
enum
TrainCompany
:
String
{
case
SJ
=
"SJ"
case
Southern
=
"Souther"
case
OldRail
=
"Old Rail"
}
These enumerations are of type String
, so you can display them on your UI easily without having to write a switch
statement.
Then define a protocol to which your train object will conform. Protocol-oriented programming offers many possibilities (see Recipe 1.12), so let’s do that now:
protocol
OnRailable
{
var
type
:
TrainType
{
get
}
var
company
:
TrainCompany
{
get
}
var
service
:
String
{
get
}
var
departureTime
:
NSDate
{
get
}
}
struct
Train
:
OnRailable
{
let
type
:
TrainType
let
company
:
TrainCompany
let
service
:
String
let
departureTime
:
NSDate
}
As we did in Recipe 2.8, we are going to define a data provider. In this example, we create a few trains that depart at specific times with different types of services and from different operators:
struct
DataProvider
{
func
allTrainsForToday
()
->
[
Train
]{
var
all
=
[
Train
]()
let
now
=
NSDate
()
let
cal
=
NSCalendar
.
currentCalendar
()
let
units
=
NSCalendarUnit
.
Year
.
union
(.
Month
).
union
(.
Day
)
let
comps
=
cal
.
components
(
units
,
fromDate
:
now
)
//first train
comps
.
hour
=
6
comps
.
minute
=
30
comps
.
second
=
0
let
date1
=
cal
.
dateFromComponents
(
comps
)
!
all
.
append
(
Train
(
type
:
.
Commuter
,
company
:
.
SJ
,
service
:
"3296"
,
departureTime
:
date1
))
//second train
comps
.
hour
=
9
comps
.
minute
=
57
let
date2
=
cal
.
dateFromComponents
(
comps
)
!
all
.
append
(
Train
(
type
:
.
HighSpeed
,
company
:
.
Southern
,
service
:
"2307"
,
departureTime
:
date2
))
//third train
comps
.
hour
=
12
comps
.
minute
=
22
let
date3
=
cal
.
dateFromComponents
(
comps
)
!
all
.
append
(
Train
(
type
:
.
Coastal
,
company
:
.
OldRail
,
service
:
"3206"
,
departureTime
:
date3
))
//fourth train
comps
.
hour
=
15
comps
.
minute
=
45
let
date4
=
cal
.
dateFromComponents
(
comps
)
!
all
.
append
(
Train
(
type
:
.
HighSpeed
,
company
:
.
SJ
,
service
:
"3703"
,
departureTime
:
date4
))
//fifth train
comps
.
hour
=
18
comps
.
minute
=
19
let
date5
=
cal
.
dateFromComponents
(
comps
)
!
all
.
append
(
Train
(
type
:
.
Coastal
,
company
:
.
Southern
,
service
:
"8307"
,
departureTime
:
date5
))
//sixth train
comps
.
hour
=
22
comps
.
minute
=
11
let
date6
=
cal
.
dateFromComponents
(
comps
)
!
all
.
append
(
Train
(
type
:
.
Commuter
,
company
:
.
OldRail
,
service
:
"6802"
,
departureTime
:
date6
))
return
all
}
}
Move now to the ComplicationController
class of your watch extension. Here, you will provide watchOS with the data it needs to display your complication. The first task is to extend CollectionType
so that you can find the next train in the array that the allTrainsForToday()
function of DataProvider
returns:
extension
CollectionType
where
Generator
.
Element
:
OnRailable
{
func
nextTrain
()
->
Generator
.
Element
?
{
let
now
=
NSDate
()
for
d
in
self
{
if
now
.
compare
(
d
.
departureTime
)
==
.
OrderedAscending
{
return
d
}
}
return
nil
}
}
And you need a data provider in your complication:
class
ComplicationController
:
NSObject
,
CLKComplicationDataSource
{
let
dataProvider
=
DataProvider
()
...
For every train, you need to create a template that watchOS can display on the screen. All templates are of type CLKComplicationTemplate
, but don’t initialize that class directly. Instead, create a template of type CLKComplicationTemplateModularLargeStandardBody
that has a header, two lines of text with the second line being optional, and an optional image. The header will show a constant text (see Figure 2-42), so instantiate it of type CLKSimpleTextProvider
. For the first line of text, you want to show how many minutes away the next train is, so that would require a text provider of type CLKRelativeDateTextProvider
as we talked about it before.
The initializer for CLKRelativeDateTextProvider
takes in a parameter of type CLKRelativeDateStyle
that defines the way the given date has to be shown. In our example, we use CLKRelativeDateStyle.Offset
:
func
templateForTrain
(
train
:
Train
)
->
CLKComplicationTemplate
{
let
template
=
CLKComplicationTemplateModularLargeStandardBody
()
template
.
headerTextProvider
=
CLKSimpleTextProvider
(
text
:
"Next train"
)
template
.
body1TextProvider
=
CLKRelativeDateTextProvider
(
date
:
train
.
departureTime
,
style
:
.
Offset
,
units
:
NSCalendarUnit
.
Hour
.
union
(.
Minute
))
let
secondLine
=
"(train.service) - (train.type)"
template
.
body2TextProvider
=
CLKSimpleTextProvider
(
text
:
secondLine
,
shortText
:
train
.
type
.
rawValue
)
return
template
}
The second line of text we are providing has a shortText
alternative. If the watch UI has no space to show our secondLine
text, it will show the shortText
alternative.
We are going to need to provide timeline entries (date plus template) for every train as well, so let’s create a helper method for that:
func
timelineEntryForTrain
(
train
:
Train
)
->
CLKComplicationTimelineEntry
{
let
template
=
templateForTrain
(
train
)
return
CLKComplicationTimelineEntry
(
date
:
train
.
departureTime
,
complicationTemplate
:
template
)
}
When we are asked for the first and the last date of the data we provide, we read our data provider’s array of trains and return the first and the last train’s dates, respectively:
func
getTimelineStartDateForComplication
(
complication
:
CLKComplication
,
withHandler
handler
:
(
NSDate
?
)
->
Void
)
{
handler
(
dataProvider
.
allTrainsForToday
().
first
!
.
departureTime
)
}
func
getTimelineEndDateForComplication
(
complication
:
CLKComplication
,
withHandler
handler
:
(
NSDate
?
)
->
Void
)
{
handler
(
dataProvider
.
allTrainsForToday
().
last
!
.
departureTime
)
}
I want to allow the user to be able to time travel so that she can see the next train as she changes the time with the digital crown. I also believe our data is not sensitive, so I’ll allow viewing this data on the lock screen:
func
getSupportedTimeTravelDirectionsForComplication
(
complication
:
CLKComplication
,
withHandler
handler
:
(
CLKComplicationTimeTravelDirections
)
->
Void
)
{
handler
([.
Forward
,
.
Backward
])
}
func
getPrivacyBehaviorForComplication
(
complication
:
CLKComplication
,
withHandler
handler
:
(
CLKComplicationPrivacyBehavior
)
->
Void
)
{
handler
(.
ShowOnLockScreen
)
}
Regarding time travel, when asked for trains after and before a certain time, your code should go through all the trains and filter out the times you don’t want displayed, as we did in Recipe 2.8:
UU
func
getTimelineEntriesForComplication
(
complication
:
CLKComplication
,
beforeDate
date
:
NSDate
,
limit
:
Int
,
withHandler
handler
:
(([
CLKComplicationTimelineEntry
]
?
)
->
Void
))
{
let
entries
=
dataProvider
.
allTrainsForToday
().
filter
{
date
.
compare
(
$
0.
departureTime
)
==
.
OrderedDescending
}.
map
{
self
.
timelineEntryForTrain
(
$
0
)
}
handler
(
entries
)
}
func
getTimelineEntriesForComplication
(
complication
:
CLKComplication
,
afterDate
date
:
NSDate
,
limit
:
Int
,
withHandler
handler
:
(([
CLKComplicationTimelineEntry
]
?
)
->
Void
))
{
let
entries
=
dataProvider
.
allTrainsForToday
().
filter
{
date
.
compare
(
$
0.
departureTime
)
==
.
OrderedAscending
}.
map
{
self
.
timelineEntryForTrain
(
$
0
)
}
handler
(
entries
)
}
When the getCurrentTimelineEntryForComplication(_:withHandler:)
method is called on our delegate, we get the next train’s timeline entry and return it:
func
getCurrentTimelineEntryForComplication
(
complication
:
CLKComplication
,
withHandler
handler
:
((
CLKComplicationTimelineEntry
?
)
->
Void
))
{
if
let
train
=
dataProvider
.
allTrainsForToday
().
nextTrain
(){
handler
(
timelineEntryForTrain
(
train
))
}
else
{
handler
(
nil
)
}
}
Because we provide data until the end of today, we ask watchOS to ask us for new data tomorrow:
func
getNextRequestedUpdateDateWithHandler
(
handler
:
(
NSDate
?
)
->
Void
)
{
handler
(
NSDate
.
endOfToday
());
}
Last but not least, we provide our placeholder template:
func
getPlaceholderTemplateForComplication
(
complication
:
CLKComplication
,
withHandler
handler
:
(
CLKComplicationTemplate
?
)
->
Void
)
{
if
let
data
=
dataProvider
.
allTrainsForToday
().
nextTrain
(){
handler
(
templateForTrain
(
data
))
}
else
{
handler
(
nil
)
}
}
We saw an example of our app showing the next train (see Figure 2-42), but our app can also participate in time travel (see Figure 2-44). The user can use the digital crown on the watch to move forward or backward and see the next available train at the new time.
Use an instance of the CLKDateTextProvider
class, which is a subclass of CLKTextProvider
, as your text provider.
I am going to use CLKComplicationTemplateModularLargeColumns
(a modular large template) for this recipe. So configure your watch target to provide only large-modular templates (see Figure 2-43).
Let’s develop a modular large complication that provides us with the name and the date of the next three public holidays (see Figure 2-45). We are not formatting the date ourselves. We leave it to watchOS to decide how to display the date by using an instance of CLKDateTextProvider
.
Just as in Recipe 2.8 and Recipe 2.9, we are going to add a new class to our watch app called DataProvider
. In there, we are going to program all the holidays this year. Let’s start off by defining what a holiday object looks like:
protocol
Holidayable
{
var
date
:
NSDate
{
get
}
var
name
:
String
{
get
}
}
struct
Holiday
:
Holidayable
{
let
date
:
NSDate
let
name
:
String
}
In our data provider class, we start off by defining some holiday names:
struct
DataProvider
{
private
let
holidayNames
=
[
"Father's Day"
,
"Mother's Day"
,
"Bank Holiday"
,
"Nobel Day"
,
"Man Day"
,
"Woman Day"
,
"Boyfriend Day"
,
"Girlfriend Day"
,
"Dog Day"
,
"Cat Day"
,
"Mouse Day"
,
"Cow Day"
,
]
private
func
randomDay
()
->
Int
{
return
Int
(
arc4random_uniform
(
20
)
+
1
)
}
...
Then we move on to providing our instances of Holiday
:
func
allHolidays
()
->
[
Holiday
]{
var
all
=
[
Holiday
]()
let
now
=
NSDate
()
let
cal
=
NSCalendar
.
currentCalendar
()
let
units
=
NSCalendarUnit
.
Year
.
union
(.
Month
).
union
(.
Day
)
let
comps
=
cal
.
components
(
units
,
fromDate
:
now
)
var
dates
=
[
NSDate
]()
for
month
in
1.
.
.12
{
comps
.
day
=
randomDay
()
comps
.
month
=
month
dates
.
append
(
cal
.
dateFromComponents
(
comps
)
!
)
}
var
i
=
0
for
date
in
dates
{
all
.
append
(
Holiday
(
date
:
date
,
name
:
holidayNames
[
i
++
]))
}
return
all
}
It’s worth noting that the allHolidays()
function we just wrote simply goes through all months inside this year, and sets the day of the month to a random day. So we will get 12 holidays, one in each month, at a random day inside that month.
Over to our ComplicationController
. When we get asked later when we would like to provide more data or updated data to watchOS, we are going to ask for 10 minutes in the future. So if our data changes, watchOS will have a chance to ask us for updated information:
extension
NSDate
{
func
plus10Minutes
()
->
NSDate
{
return
self
.
dateByAddingTimeInterval
(
10
*
60
)
}
}
Because the template we are going to provide allows a maximum of three items, I would like to have methods on Array
to return the second and the third items inside the array, just like the prebuilt first
property that the class offers:
extension
Array
{
var
second
:
Generator
.
Element
?
{
return
self
.
count
>=
1
?
self
[
1
]
:
nil
}
var
third
:
Generator
.
Element
?
{
return
self
.
count
>=
2
?
self
[
2
]
:
nil
}
}
DataProvider
’s allHolidays()
method returns 12 holidays. How about extending the built-in array type to always give us the next three holidays? It would have to read today’s date, go through the items in our array, compare the dates, and give us just the upcoming three holidays:
extension
CollectionType
where
Generator
.
Element
:
Holidayable
{
//may contain less than 3 holidays
func
nextThreeHolidays
()
->
Array
<
Self
.
Generator
.
Element
>
{
let
now
=
NSDate
()
let
orderedArray
=
Array
(
self
.
filter
{
now
.
compare
(
$
0.
date
)
==
.
OrderedAscending
})
let
result
=
Array
(
orderedArray
[
0.
.
<
min
(
orderedArray
.
count
,
3
)])
return
result
}
}
Now we start defining our complication:
class
ComplicationController
:
NSObject
,
CLKComplicationDataSource
{
let
dataProvider
=
DataProvider
()
...
We need a method that can take in a Holiday
object and give us a template of type CLKComplicationTemplate
for that. Our specific template for this recipe is of type CLKComplicationTemplateModularLargeColumns
. This template is like a 3 × 3 table. It has three rows and three columns (see Figure 2-45). If we are at the end of the year and we have no more holidays, we return a template that is of type CLKComplicationTemplateModularLargeStandardBody
and tell the user that there are no more upcoming holidays. Note that both templates have the words “ModularLarge” in their name. Because we have specified in our target setting that we support only modular large templates (see Figure 2-43), this example can return only templates that have those words in their name:
func
templateForHoliday
(
holiday
:
Holiday
)
->
CLKComplicationTemplate
{
let
next3Holidays
=
dataProvider
.
allHolidays
().
nextThreeHolidays
()
let
headerTitle
=
"Next 3 Holidays"
guard
next3Holidays
.
count
>
0
else
{
let
template
=
CLKComplicationTemplateModularLargeStandardBody
()
template
.
headerTextProvider
=
CLKSimpleTextProvider
(
text
:
headerTitle
)
template
.
body1TextProvider
=
CLKSimpleTextProvider
(
text
:
"Sorry!"
)
return
template
}
let
dateUnits
=
NSCalendarUnit
.
Month
.
union
(.
Day
)
let
template
=
CLKComplicationTemplateModularLargeColumns
()
//first holiday
if
let
firstHoliday
=
next3Holidays
.
first
{
template
.
row1Column1TextProvider
=
CLKSimpleTextProvider
(
text
:
firstHoliday
.
name
)
template
.
row1Column2TextProvider
=
CLKDateTextProvider
(
date
:
firstHoliday
.
date
,
units
:
dateUnits
)
}
//second holiday
if
let
secondHoliday
=
next3Holidays
.
second
{
template
.
row2Column1TextProvider
=
CLKSimpleTextProvider
(
text
:
secondHoliday
.
name
)
template
.
row2Column2TextProvider
=
CLKDateTextProvider
(
date
:
secondHoliday
.
date
,
units
:
dateUnits
)
}
//third holiday
if
let
thirdHoliday
=
next3Holidays
.
third
{
template
.
row3Column1TextProvider
=
CLKSimpleTextProvider
(
text
:
thirdHoliday
.
name
)
template
.
row3Column2TextProvider
=
CLKDateTextProvider
(
date
:
thirdHoliday
.
date
,
units
:
dateUnits
)
}
return
template
}
You need to provide a timeline entry (date plus template) for your holidays as well:
func
timelineEntryForHoliday
(
holiday
:
Holiday
)
->
CLKComplicationTimelineEntry
{
let
template
=
templateForHoliday
(
holiday
)
return
CLKComplicationTimelineEntry
(
date
:
holiday
.
date
,
complicationTemplate
:
template
)
}
Also provide the first and the last holidays:
func
getTimelineStartDateForComplication
(
complication
:
CLKComplication
,
withHandler
handler
:
(
NSDate
?
)
->
Void
)
{
handler
(
dataProvider
.
allHolidays
().
first
!
.
date
)
}
func
getTimelineEndDateForComplication
(
complication
:
CLKComplication
,
withHandler
handler
:
(
NSDate
?
)
->
Void
)
{
handler
(
dataProvider
.
allHolidays
().
last
!
.
date
)
}
Also support time travel and provide your content on the lock screen, because it is not private:
func
getSupportedTimeTravelDirectionsForComplication
(
complication
:
CLKComplication
,
withHandler
handler
:
(
CLKComplicationTimeTravelDirections
)
->
Void
)
{
handler
([.
Forward
,
.
Backward
])
}
func
getPrivacyBehaviorForComplication
(
complication
:
CLKComplication
,
withHandler
handler
:
(
CLKComplicationPrivacyBehavior
)
->
Void
)
{
handler
(.
ShowOnLockScreen
)
}
Now let’s give watchOS information about previous and upcoming holidays:
func
getTimelineEntriesForComplication
(
complication
:
CLKComplication
,
beforeDate
date
:
NSDate
,
limit
:
Int
,
withHandler
handler
:
(([
CLKComplicationTimelineEntry
]
?
)
->
Void
))
{
let
entries
=
dataProvider
.
allHolidays
().
filter
{
date
.
compare
(
$
0.
date
)
==
.
OrderedDescending
}.
map
{
self
.
timelineEntryForHoliday
(
$
0
)
}
handler
(
entries
)
}
func
getTimelineEntriesForComplication
(
complication
:
CLKComplication
,
afterDate
date
:
NSDate
,
limit
:
Int
,
withHandler
handler
:
(([
CLKComplicationTimelineEntry
]
?
)
->
Void
))
{
let
entries
=
dataProvider
.
allHolidays
().
filter
{
date
.
compare
(
$
0.
date
)
==
.
OrderedAscending
}.
map
{
self
.
timelineEntryForHoliday
(
$
0
)
}
handler
(
entries
)
}
Last but not least, provide the upcoming three holidays when you are asked to provide them now:
func
getCurrentTimelineEntryForComplication
(
complication
:
CLKComplication
,
withHandler
handler
:
((
CLKComplicationTimelineEntry
?
)
->
Void
))
{
if
let
first
=
dataProvider
.
allHolidays
().
nextThreeHolidays
().
first
{
handler
(
timelineEntryForHoliday
(
first
))
}
else
{
handler
(
nil
)
}
}
func
getNextRequestedUpdateDateWithHandler
(
handler
:
(
NSDate
?
)
->
Void
)
{
handler
(
NSDate
().
plus10Minutes
());
}
func
getPlaceholderTemplateForComplication
(
complication
:
CLKComplication
,
withHandler
handler
:
(
CLKComplicationTemplate
?
)
->
Void
)
{
if
let
holiday
=
dataProvider
.
allHolidays
().
nextThreeHolidays
().
first
{
handler
(
templateForHoliday
(
holiday
))
}
else
{
handler
(
nil
)
}
}
Recipe 2.7 and Recipe 2.9
Provide your time (in form of NSDate
) to an instance of CLKTimeTextProvider
and use it inside a template (see Figure 2-46). Our large and modular complication on the center of the screen is showing the next pause that we can take at work, which happens to be a coffee pause.
In this recipe, we are going to rely a lot on what we have learned in Recipe 2.8 and other complication recipes in this chapter. I suggest reading Recipe 2.8 at least to get an idea of how our data provider works. Otherwise, you will still be able to read this recipe; however, I will skip over some details that I’ve already explained in Recipe 2.8.
This recipe uses a large-modular template, so make sure that your project is set up for that (see Figure 2-43). Here, I want to build an app that shows the different breaks or pauses that I can take at work, and when they occur: for instance, when the first pause is after I get to work, when lunch happens, when the next pause between lunch and dinner is, and if I want to have dinner as well, when that should happen.
So we have breaks at work and we need to define them. Create a Swift file in your watch extension and call it DataProvider. In there, define your break:
import
Foundation
protocol
Pausable
{
var
name
:
String
{
get
}
var
date
:
NSDate
{
get
}
}
struct
PauseAtWork
:
Pausable
{
let
name
:
String
let
date
:
NSDate
}
Now in your DataProvider
structure, create four pauses that we can take at work at different times and provide them as an array:
struct
DataProvider
{
func
allPausesToday
()
->
[
PauseAtWork
]{
var
all
=
[
PauseAtWork
]()
let
now
=
NSDate
()
let
cal
=
NSCalendar
.
currentCalendar
()
let
units
=
NSCalendarUnit
.
Year
.
union
(.
Month
).
union
(.
Day
)
let
comps
=
cal
.
components
(
units
,
fromDate
:
now
)
comps
.
calendar
=
cal
comps
.
minute
=
30
comps
.
hour
=
11
all
.
append
(
PauseAtWork
(
name
:
"Coffee"
,
date
:
comps
.
date
!
))
comps
.
minute
=
30
comps
.
hour
=
14
all
.
append
(
PauseAtWork
(
name
:
"Lunch"
,
date
:
comps
.
date
!
))
comps
.
minute
=
0
comps
.
hour
=
16
all
.
append
(
PauseAtWork
(
name
:
"Tea"
,
date
:
comps
.
date
!
))
comps
.
hour
=
17
all
.
append
(
PauseAtWork
(
name
:
"Dinner"
,
date
:
comps
.
date
!
))
return
all
}
}
Here we have just obtained the date and time of today and then gone from coffee break in the morning to dinner in the evening, adding each pause to the array. The method is called allPausesToday()
and we are going to invoke it from our watch complication.
Before, we created a protocol called Pausable
and now we have all our pauses in an array. When we are asked to provide a template for the next pause to show in the complication, we have to get the current time and find the pause whose time is after the current time. So let’s bundle that up by extending CollectionType
like we have done in other recipes in this chapter:
extension
CollectionType
where
Generator
.
Element
:
Pausable
{
func
nextPause
()
->
Self
.
Generator
.
Element
?
{
let
now
=
NSDate
()
for
pause
in
self
{
if
now
.
compare
(
pause
.
date
)
==
.
OrderedAscending
{
return
pause
}
}
return
nil
}
}
In our complication now, we instantiate our data provider:
class
ComplicationController
:
NSObject
,
CLKComplicationDataSource
{
let
dataProvider
=
DataProvider
()
...
For every pause that we want to display to the user (see Figure 2-46), we need to provide a template of type CLKComplicationTemplate
to the runtime. We never instantiate that class directly. Instead, we return an instance of a subclass of that class. In this particular example, we display an instance of CLKComplicationTemplateModularLargeTallBody
. However, if there are no more pauses to take at work (e.g., if time is 21:00 and we are no longer at work), we display a placeholder to the user to tell her there are no more pauses. The template for that is of type CLKComplicationTemplateModularLargeStandardBody
. The difference between the two templates is visible if you read their names. We set the time on our template by setting the bodyTextProvider
property of our CLKComplicationTemplateModularLargeTallBody
instance:
func
templateForPause
(
pause
:
PauseAtWork
)
->
CLKComplicationTemplate
{
guard
let
nextPause
=
dataProvider
.
allPausesToday
().
nextPause
()
else
{
let
template
=
CLKComplicationTemplateModularLargeStandardBody
()
template
.
headerTextProvider
=
CLKSimpleTextProvider
(
text
:
"Next Break"
)
template
.
body1TextProvider
=
CLKSimpleTextProvider
(
text
:
"None"
)
return
template
}
let
template
=
CLKComplicationTemplateModularLargeTallBody
()
template
.
headerTextProvider
=
CLKSimpleTextProvider
(
text
:
nextPause
.
name
)
template
.
bodyTextProvider
=
CLKTimeTextProvider
(
date
:
nextPause
.
date
)
return
template
}
We also have to provide some of the other delegate methods of CLKComplicationDataSource
, such as the timeline entry (date plus template) for every pause that we can take at work. We also need to support time travel for this example. On top of that, our information is not sensitive, so when asked whether we want to display our complication on the lock screen, we happily say yes:
func
timelineEntryForPause
(
pause
:
PauseAtWork
)
->
CLKComplicationTimelineEntry
{
let
template
=
templateForPause
(
pause
)
return
CLKComplicationTimelineEntry
(
date
:
pause
.
date
,
complicationTemplate
:
template
)
}
func
getSupportedTimeTravelDirectionsForComplication
(
complication
:
CLKComplication
,
withHandler
handler
:
(
CLKComplicationTimeTravelDirections
)
->
Void
)
{
handler
([.
Forward
,
.
Backward
])
}
func
getPrivacyBehaviorForComplication
(
complication
:
CLKComplication
,
withHandler
handler
:
(
CLKComplicationPrivacyBehavior
)
->
Void
)
{
handler
(.
ShowOnLockScreen
)
}
When asked the beginning and the end range of dates for our complications, we will return the dates for the first and the last pause that we want to take at work today. Remember, in this complication, we will return all the pauses that we can take at work today. When the time comes to display the pauses to take at work tomorrow, we will provide a whole set of new pauses:
func
getTimelineStartDateForComplication
(
complication
:
CLKComplication
,
withHandler
handler
:
(
NSDate
?
)
->
Void
)
{
handler
(
dataProvider
.
allPausesToday
().
first
!
.
date
)
}
func
getTimelineEndDateForComplication
(
complication
:
CLKComplication
,
withHandler
handler
:
(
NSDate
?
)
->
Void
)
{
handler
(
dataProvider
.
allPausesToday
().
last
!
.
date
)
}
When the runtime calls the getTimelineEntriesForComplication(_:beforeDate:limit:withHandler:)
method, provide all the pauses that are available before the given date:
func
getTimelineEntriesForComplication
(
complication
:
CLKComplication
,
beforeDate
date
:
NSDate
,
limit
:
Int
,
withHandler
handler
:
(([
CLKComplicationTimelineEntry
]
?
)
->
Void
))
{
let
entries
=
dataProvider
.
allPausesToday
().
filter
{
date
.
compare
(
$
0.
date
)
==
.
OrderedDescending
}.
map
{
self
.
timelineEntryForPause
(
$
0
)
}
handler
(
entries
)
}
Similarly, when the getTimelineEntriesForComplication(_:afterDate:limit:withHandler:)
method is called, return all the available pauses after the given date:
func
getTimelineEntriesForComplication
(
complication
:
CLKComplication
,
afterDate
date
:
NSDate
,
limit
:
Int
,
withHandler
handler
:
(([
CLKComplicationTimelineEntry
]
?
)
->
Void
))
{
let
entries
=
dataProvider
.
allPausesToday
().
filter
{
date
.
compare
(
$
0.
date
)
==
.
OrderedAscending
}.
map
{
self
.
timelineEntryForPause
(
$
0
)
}
handler
(
entries
)
}
In the getCurrentTimelineEntryForComplication(_:withHandler:)
method, you will be asked to provide the template for the current data (the next pause) to show on screen. We already have a method on CollectionType
called nextPause()
, so let’s use that to provide a template to watchOS:
func
getCurrentTimelineEntryForComplication
(
complication
:
CLKComplication
,
withHandler
handler
:
((
CLKComplicationTimelineEntry
?
)
->
Void
))
{
if
let
pause
=
dataProvider
.
allPausesToday
().
nextPause
(){
handler
(
timelineEntryForPause
(
pause
))
}
else
{
handler
(
nil
)
}
}
Because, in a typical watch app, our data would probably come from a backend, we would like the runtime to task us for up-to-date information as soon as possible, but not too soon. So let’s do that after 10 minutes:
func
getNextRequestedUpdateDateWithHandler
(
handler
:
(
NSDate
?
)
->
Void
)
{
handler
(
NSDate
().
plus10Minutes
());
}
Last but not least, we also need to provide a placeholder template when the user is adding our complication to her watch face:
func
getPlaceholderTemplateForComplication
(
complication
:
CLKComplication
,
withHandler
handler
:
(
CLKComplicationTemplate
?
)
->
Void
)
{
if
let
pause
=
dataProvider
.
allPausesToday
().
nextPause
(){
handler
(
templateForPause
(
pause
))
}
else
{
handler
(
nil
)
}
}
You want to display a time interval (start date–end date) on your watchOS UI (see Figure 2-47). Our template shows today’s meetings on the screen. Right now, it’s brunch time, so the screen shows the description and location of where we are going to have brunch, along with the time interval of the brunch (start–end).
Use an instance of CLKTimeIntervalTextProvider
as your text provider (see Figure 2-47).
I will base this recipe on other recipes such as Recipe 2.10 and Recipe 2.11.
Let’s say that we want to have an app that shows us all our meetings today. Every meeting has the following properties:
Because text providers of type CLKSimpleTextProvider
accept a short text in addition to the full text, we also have a short version of the location and the name. For instance, the location can be “Stockholm Central Train Station,” whereas the short version of this could be “Central Station” or even “Centralen” in Swedish, which means the center. So let’s define this meeting object:
protocol
Timable
{
var
name
:
String
{
get
}
var
shortName
:
String
{
get
}
var
location
:
String
{
get
}
var
shortLocation
:
String
{
get
}
var
startDate
:
NSDate
{
get
}
var
endDate
:
NSDate
{
get
}
var
previous
:
Timable
?
{
get
}
}
struct
Meeting
:
Timable
{
let
name
:
String
let
shortName
:
String
let
location
:
String
let
shortLocation
:
String
let
startDate
:
NSDate
let
endDate
:
NSDate
let
previous
:
Timable
?
}
Create a Swift file in your project called DataProvider. Put all the meetings for today in there and return an array:
struct
DataProvider
{
func
allMeetingsToday
()
->
[
Meeting
]{
var
all
=
[
Meeting
]()
let
oneHour
:
NSTimeInterval
=
1
*
60.0
*
60
let
now
=
NSDate
()
let
cal
=
NSCalendar
.
currentCalendar
()
let
units
=
NSCalendarUnit
.
Year
.
union
(.
Month
).
union
(.
Day
)
let
comps
=
cal
.
components
(
units
,
fromDate
:
now
)
comps
.
calendar
=
cal
comps
.
minute
=
30
comps
.
hour
=
11
let
meeting1
=
Meeting
(
name
:
"Brunch with Sarah"
,
shortName
:
"Brunch"
,
location
:
"Stockholm Central"
,
shortLocation
:
"Central"
,
startDate
:
comps
.
date
!
,
endDate
:
comps
.
date
!
.
dateByAddingTimeInterval
(
oneHour
),
previous
:
nil
)
all
.
append
(
meeting1
)
comps
.
minute
=
30
comps
.
hour
=
14
let
meeting2
=
Meeting
(
name
:
"Lunch with Gabriella"
,
shortName
:
"Lunch"
,
location
:
"At home"
,
shortLocation
:
"Home"
,
startDate
:
comps
.
date
!
,
endDate
:
comps
.
date
!
.
dateByAddingTimeInterval
(
oneHour
),
previous
:
meeting1
)
all
.
append
(
meeting2
)
comps
.
minute
=
0
comps
.
hour
=
16
let
meeting3
=
Meeting
(
name
:
"Snack with Leif"
,
shortName
:
"Snack"
,
location
:
"Flags Cafe"
,
shortLocation
:
"Flags"
,
startDate
:
comps
.
date
!
,
endDate
:
comps
.
date
!
.
dateByAddingTimeInterval
(
oneHour
),
previous
:
meeting2
)
all
.
append
(
meeting3
)
comps
.
hour
=
17
let
meeting4
=
Meeting
(
name
:
"Dinner with Family"
,
shortName
:
"Dinner"
,
location
:
"At home"
,
shortLocation
:
"Home"
,
startDate
:
comps
.
date
!
,
endDate
:
comps
.
date
!
.
dateByAddingTimeInterval
(
oneHour
),
previous
:
meeting3
)
all
.
append
(
meeting4
)
return
all
}
}
In your complication class, extend CollectionType
so that it can return the upcoming meeting:
extension
CollectionType
where
Generator
.
Element
:
Timable
{
func
nextMeeting
()
->
Self
.
Generator
.
Element
?
{
let
now
=
NSDate
()
for
meeting
in
self
{
if
now
.
compare
(
meeting
.
startDate
)
==
.
OrderedAscending
{
return
meeting
}
}
return
nil
}
}
I have extended CollectionType
, but only if the items are Timable
. I explained this technique in Recipe 1.12.
In your complication handler, create an instance of the data provider:
class
ComplicationController
:
NSObject
,
CLKComplicationDataSource
{
let
dataProvider
=
DataProvider
()
...
Our template is of type CLKComplicationTemplateModularLargeStandardBody
, which has a few important properties that we set as follows:
headerTextProvider
body1TextProvider
body2TextProvider
To display the time range of the meeting, instantiate CLKTimeIntervalTextProvider
:
func
templateForMeeting
(
meeting
:
Meeting
)
->
CLKComplicationTemplate
{
let
template
=
CLKComplicationTemplateModularLargeStandardBody
()
guard
let
nextMeeting
=
dataProvider
.
allMeetingsToday
().
nextMeeting
()
else
{
template
.
headerTextProvider
=
CLKSimpleTextProvider
(
text
:
"Next Break"
)
template
.
body1TextProvider
=
CLKSimpleTextProvider
(
text
:
"None"
)
return
template
}
template
.
headerTextProvider
=
CLKTimeIntervalTextProvider
(
startDate
:
nextMeeting
.
startDate
,
endDate
:
nextMeeting
.
endDate
)
template
.
body1TextProvider
=
CLKSimpleTextProvider
(
text
:
nextMeeting
.
name
,
shortText
:
nextMeeting
.
shortName
)
template
.
body2TextProvider
=
CLKSimpleTextProvider
(
text
:
nextMeeting
.
location
,
shortText
:
nextMeeting
.
shortLocation
)
return
template
}
Using this method, you can also create timeline entries (date plus template). In this example, I set every new event’s start date to the end date of the previous event (if it is available). That way, as soon as the current ongoing meeting ends, the next meeting shows up on the list:
If the event has no previous events, its timeline entry date will be its start date, instead of the end date of the previous event.
func
timelineEntryForMeeting
(
meeting
:
Meeting
)
->
CLKComplicationTimelineEntry
{
let
template
=
templateForMeeting
(
meeting
)
let
date
=
meeting
.
previous
?
.
endDate
??
meeting
.
startDate
return
CLKComplicationTimelineEntry
(
date
:
date
,
complicationTemplate
:
template
)
}
Let’s also participate in time travel and show our content on the lock screen as well:
func
getSupportedTimeTravelDirectionsForComplication
(
complication
:
CLKComplication
,
withHandler
handler
:
(
CLKComplicationTimeTravelDirections
)
->
Void
)
{
handler
([.
Forward
,
.
Backward
])
}
func
getPrivacyBehaviorForComplication
(
complication
:
CLKComplication
,
withHandler
handler
:
(
CLKComplicationPrivacyBehavior
)
->
Void
)
{
handler
(.
ShowOnLockScreen
)
}
Then we have to provide the date range for which we have available meetings. The start of the range is the start date of the first meeting and the end date is the end date of the last meeting:
func
getTimelineStartDateForComplication
(
complication
:
CLKComplication
,
withHandler
handler
:
(
NSDate
?
)
->
Void
)
{
handler
(
dataProvider
.
allMeetingsToday
().
first
!
.
startDate
)
}
func
getTimelineEndDateForComplication
(
complication
:
CLKComplication
,
withHandler
handler
:
(
NSDate
?
)
->
Void
)
{
handler
(
dataProvider
.
allMeetingsToday
().
last
!
.
endDate
)
}
We’ll also be asked to provide all the available meetings before a certain date, so let’s do that:
func
getTimelineEntriesForComplication
(
complication
:
CLKComplication
,
beforeDate
date
:
NSDate
,
limit
:
Int
,
withHandler
handler
:
(([
CLKComplicationTimelineEntry
]
?
)
->
Void
))
{
let
entries
=
dataProvider
.
allMeetingsToday
().
filter
{
date
.
compare
(
$
0.
startDate
)
==
.
OrderedDescending
}.
map
{
self
.
timelineEntryForMeeting
(
$
0
)
}
handler
(
entries
)
}
Similarly, we have to provide all our available meetings after a given date:
func
getTimelineEntriesForComplication
(
complication
:
CLKComplication
,
afterDate
date
:
NSDate
,
limit
:
Int
,
withHandler
handler
:
(([
CLKComplicationTimelineEntry
]
?
)
->
Void
))
{
let
entries
=
dataProvider
.
allMeetingsToday
().
filter
{
date
.
compare
(
$
0.
startDate
)
==
.
OrderedAscending
}.
map
{
self
.
timelineEntryForMeeting
(
$
0
)
}
handler
(
entries
)
}
Last but not least, provide your placeholder template, the template for now, and the next time we would like watchOS to ask us for updated information:
func
getCurrentTimelineEntryForComplication
(
complication
:
CLKComplication
,
withHandler
handler
:
((
CLKComplicationTimelineEntry
?
)
->
Void
))
{
if
let
meeting
=
dataProvider
.
allMeetingsToday
().
nextMeeting
(){
handler
(
timelineEntryForMeeting
(
meeting
))
}
else
{
handler
(
nil
)
}
}
func
getNextRequestedUpdateDateWithHandler
(
handler
:
(
NSDate
?
)
->
Void
)
{
handler
(
NSDate
().
plus10Minutes
());
}
func
getPlaceholderTemplateForComplication
(
complication
:
CLKComplication
,
withHandler
handler
:
(
CLKComplicationTemplate
?
)
->
Void
)
{
if
let
pause
=
dataProvider
.
allMeetingsToday
().
nextMeeting
(){
handler
(
templateForMeeting
(
pause
))
}
else
{
handler
(
nil
)
}
}
We coded the plus10Minutes()
method on NSDate
in Recipe 2.10.
Recipe 2.9, Recipe 2.11, and Recipe 2.12
Use the presentAudioRecorderControllerWithOutputURL(_:preset:options:completion:)
method of your WKInterfaceController
class to present a system dialog that can take care of audio recording. If you want to dismiss the dialog, use the dismissAudioRecordingController()
method of your controller.
The options
parameter of the presentAudioRecorderControllerWithOutputURL(_:preset:options:completion:)
method accepts a dictionary that can contain the following keys:
WKAudioRecorderControllerOptionsActionTitleKey
String
, will be the title of our recorder.WKAudioRecorderControllerOptionsAlwaysShowActionTitleKey
NSNumber
, contains a Bool
value to dictates whether the title should always be shown on the recorder.WKAudioRecorderControllerOptionsAutorecordKey
NSNumber
, contains a Bool
value to indicate whether recording should begin automatically when the dialog is presented.WKAudioRecorderControllerOptionsMaximumDurationKey
NSNumber
, contains an NSTimeInterval
value to dictate the maximum duration of the audio content.For this recipe, we are going to create a watch app whose UI looks like that shown in Figure 2-48). It holds a label to show our current status (started recording, failed recording, etc.) and a button that, upon pressing, can show our recording dialog.
Hook the label up to your code with the name statusLbl
. Then hook your record button to your interface under a method named record()
. Your interface code should look like this now:
class
InterfaceController
:
WKInterfaceController
{
@
IBOutlet
var
statusLbl
:
WKInterfaceLabel
!
...
Define the URL where your recording will be saved:
var
url
:
NSURL
{
let
fm
=
NSFileManager
()
let
url
=
try
!
fm
.
URLForDirectory
(
NSSearchPathDirectory
.
MusicDirectory
,
inDomain
:
NSSearchPathDomainMask
.
UserDomainMask
,
appropriateForURL
:
nil
,
create
:
true
)
.
URLByAppendingPathComponent
(
"recording"
)
return
url
}
Also, because the completion block of our recording screen might not get called on the main thread, create a variable that can set the text inside our status label on the main thread:
var
status
=
""
{
willSet
{
dispatch_async
(
dispatch_get_main_queue
()){
self
.
statusLbl
.
setText
(
newValue
)
}
}
}
When your record button is pressed, construct your options for the recording:
let
oneMinute
:
NSTimeInterval
=
1
*
60
let
yes
=
NSNumber
(
bool
:
true
)
let
no
=
NSNumber
(
bool
:
false
)
let
options
=
[
WKAudioRecorderControllerOptionsActionTitleKey
:
"Audio Recorder"
,
WKAudioRecorderControllerOptionsAlwaysShowActionTitleKey
:
yes
,
WKAudioRecorderControllerOptionsAutorecordKey
:
no
,
WKAudioRecorderControllerOptionsMaximumDurationKey
:
oneMinute
]
Last but not least, present your audio recorder to the user and then set the status accordingly:
presentAudioRecorderControllerWithOutputURL
(
url
,
preset
:
WKAudioRecorderPreset
.
WideBandSpeech
,
options
:
options
){
success
,
error
in
defer
{
self
.
dismissAudioRecorderController
()
}
guard
success
&&
error
==
nil
else
{
self
.
status
=
"Failed to record"
return
}
self
.
status
=
"Successfully recorded"
}
The first parameter to this method is just the URL from which the media must be loaded. The options
parameter is a dictionary that can have the following keys:
WKMediaPlayerControllerOptionsAutoplayKey
NSNumber
instance) that dictates whether the media should autoplay when it is opened. This is set to false
by default.WKMediaPlayerControllerOptionsStartTimeKey
NSTimeInterval
) into the media where you want to start it.WKMediaPlayerControllerOptionsVideoGravityKey
WKVideoGravity
(place its raw integer value in your dictionary) that dictates the scaling of the video. You can, for instance, specify WKVideoGravity.ResizeAspectFill
.WKMediaPlayerControllerOptionsLoopsKey
NSNumber
) that specifies whether the media has to loop automatically. The default is false
.For this recipe, we are going to create a UI similar to that in Recipe 2.13 (see Figure 2-48). Our UI looks like Figure 2-49.
Hook up the label to an outlet called statusLbl
and the action of the button to a method called play()
. Then create a variable in your code called status
of type String
, just as we did in Recipe 2.13. In the play
method, first construct your URL:
guard
let
url
=
NSURL
(
string
:
"http://localhost:8888/video.mp4"
)
else
{
status
=
"Could not create url"
return
}
I am running MAMP (free version) on my computer and I’m hosting a video called video.mp4. You can download lots of public domain files by just searching online.
Now construct your options dictionary. I want the media player to do the following:
let
gravity
=
WKVideoGravity
.
ResizeAspectFill
.
rawValue
let
options
=
[
WKMediaPlayerControllerOptionsAutoplayKey
:
NSNumber
(
bool
:
true
),
WKMediaPlayerControllerOptionsStartTimeKey
:
4.0
as
NSTimeInterval
,
WKMediaPlayerControllerOptionsVideoGravityKey
:
gravity
,
WKMediaPlayerControllerOptionsLoopsKey
:
NSNumber
(
bool
:
true
),
]
Now start the media player and handle any possible errors:
presentMediaPlayerControllerWithURL
(
url
,
options
:
options
)
{
didPlayToEnd
,
endTime
,
error
in
self
.
dismissMediaPlayerController
()
guard
error
==
nil
else
{
self
.
status
=
"Error occurred (error)"
return
}
if
didPlayToEnd
{
self
.
status
=
"Played to end of the file"
}
else
{
self
.
status
=
"Did not play to end of file. End time = (endTime)"
}
}