Thứ Ba, 2 tháng 12, 2014

Working with iOS 8 Handoff Part 2: Using Continuation Streams

8 Flares 8 Flares ×

In my last tutorial we met for first time the Handoff capability that was introduced in iOS 8.0. With it, an activity that is started in one iOS device can be continued to another from the point that was left off. Also, an activity can be continued on a Mac too, as long as the operating system is the version 10.10 (Yosemite) and a respective application exists. Obviously, it is a nice new feature that developers can take advantage of in order to create cool applications based on an entirely new philosophy.

Through the demo application of the previous post we managed to see the basics of the Handoff: How it’s integrated into an application, how a user activity is defined and how it can be continued to another device. In this tutorial we’ll extend that application and we will learn a bit more advanced techniques which can become handy in several cases. If you haven’t read the previous tutorial regarding Handoff, then I encourage you to read it first so it’s easier for you to follow here. Before you proceed make sure that you’ve perfectly understood what the prerequisites of the Handoff are, and what a user activity is, as it consists of the base of the Handoff and everything else regarding it.

handoff-stream-featured

A user activity object contains various useful properties, such as the user activity type which uniquely identifies an activity, and a user info dictionary, which contains the data that should be handed off. The most important data is transferred with this dictionary from device to device. However it’s not possible to transfer a lot of data to another device using this dictionary, and to be precise, Apple recommends to send data that it’s less than 3KB in size. In most cases this limit is more than enough, but a question arises here: How is it possible to send more data if needed so? The answer lies to the use of streams, which in the case of Handoff are called continuation streams, and this is exactly the topic of this tutorial.

Continuation streams are optional when working with Handoff, and they are not required in order for it to properly work. However, it’s quite possible that you need it sometime, and as there’s no much help out there, it’s a good opportunity to discuss it here. Prior knowledge about streams is not required, but it would help a lot if you have worked with it in the past. If this isn’t your case, don’t worry. The actual work on streams is little and easy, and I’ll help you get it as much as possible.

So, consider this tutorial as an addition to the previous one, and if you are ready let’s get started. I believe that by the end of this post you’ll have acquired all the necessary knowledge needed to fully support Handoff in your applications.

About the App

Before we start working on the topic of the today’s post, let me do a quick recap of the previous demo application. In it, we implemented a rudimentary contacts manager with three distinct and simple features:

  1. Insertion of a new contact (just the basic info; first name, last name, phone number and e-mail address).
  2. Listing of all the existing ones.
  3. Viewing of the details for a selected contact.

The Handoff capability was implemented in addition to all the above. In order to achieve to hand off data from one device to another, we defined two user activities:

  • The first one regards the editing of a contact. When a new one is being added to the app, the current state of the respective view controller along with any entered data is handed off to the second device once the Return key gets tapped on the keyboard.
  • The second one regards the view of a contact’s details. Once the respective view controller is loaded and the data is displayed, the app hands off the current state to the other device.

In both cases, the work can be continued exactly from the point that was left off.

Focusing on what is coming next now, it’s important to make clear that our goal is to evolve the above application by making it capable of sending and receiving data through continuation streams. More specifically, here’s what we are going to perform:

We will define a new user activity along with a new activity type, and we’ll use it in the view controller where all the existing contacts are listed (the ViewController class). This activity is necessary in order to send all existing contacts through streaming to another device, and obviously the best place to do that is when all contacts are displayed. The user info dictionary of the activity won’t be used to pass “vital” data. Actually, we don’t need it at all. However, because the handoff doesn’t work if no data is sent, we’ll just pass to the other device the total number of the existing contacts. With it, we’ll trigger the streaming functionality.

The continuation streams work as follows: Once the continuing device receives an activity to continue with, it asks from the originating device to provide it with more data using streams by invoking a special method that we’ll see later. A delegate method on the originating device (implemented in the proper place) gets this request, prepares the data that should be sent and then streams it. Note that any data that is about to be transferred using streams, must be of NSData type. When all data has received, the continuing application handles it in the best possible way, and that’s all. The nature of each application specifies how the received data from a stream should be managed. For example, in our demo application we’ll post a notification to the ViewController class telling that new contacts data has received, so any previous data to be replaced.

We’ll handle streams with the help of specific classes. We’ll use the NSInputStream class to manage input streams (streams to read from), and the NSOutputStream class to manage output streams (streams to write to). Also, we’ll adopt the NSStreamDelegate protocol where it’s necessary to do so.

We are going to see everything in details as we move along. For the time being and for your convenience, you can download the demo application of the previous tutorial as our starter project here.

With all the above said, let’s do some programming.

Contact Class: Conforming to NSCoding Protocol

Among the classes of the last tutorial’s project, there is one named Contact. It’s a custom class that was implemented in the previous post in order to describe a single contact object and to perform certain operations regarding contacts. Also, this is the place where we’ll start working from, so let me tell in short what we are about to do.

As I said previously, the data that should be sent using streaming to another device must be expressed as a NSData object. In our case, what we want is to send the array with all existing contacts from one application to another using continuation streams. However, the array is just a NSMutableArray object which contains objects of the Contact class, and in order to convert it to a NSData object we must serialize it. This can’t be done though at once, because the class of any object that is about to be serialized must conform to the NSCoding protocol, and that’s a problem for us because our class does not conform to it.

The Contact class can become quite easy an NSCoding-compliant one, as long as we implement two special methods for encoding and decoding its properties. After having done so, serialization can be performed with no problem at all. So, begin by opening the Contact.swift file. The first move is to adopt the NSCoding protocol. Go to the class header line, and modify it as shown next:

1
class Contact : NSObject, NSCoding

Okay, now let’s define the following method which is provided by the NSCoding protocol, so as to encode (archive) all the properties of the class.

1
2
3
4
5
6
7
func encodeWithCoder (aCoder : NSCoder ) {
    aCoder.encodeObject (firstname, forKey : "firstname" )
    aCoder.encodeObject (lastname, forKey : "lastname" )
    aCoder.encodeObject (phoneNumber, forKey : "phoneNumber" )
    aCoder.encodeObject (email, forKey : "email" )
    aCoder.encodeObject (documentsDirectory, forKey : "docdir" )
}

The encodeObject(_:forKey:) method used above is one of the many similar methods that the NSCoder class supports for encoding almost every type of data. For every single property declared in the class, we call that method and we specify a unique key, so we’re able to decode them properly later.

With the above method any Contact object can be archived, but it’s still unable to be unarchived. To achieve that too, we’ll implement the second and last method that is declared in the NSCoding protocol:

1
2
3
4
5
6
7
required init (coder aDecoder : NSCoder ) {
    firstname = aDecoder.decodeObjectForKey ( "firstname" ) as? NSString
    lastname = aDecoder.decodeObjectForKey ( "lastname" ) as? NSString
    phoneNumber = aDecoder.decodeObjectForKey ( "phoneNumber" ) as? NSString
    email = aDecoder.decodeObjectForKey ( "email" ) as? NSString
    documentsDirectory = aDecoder.decodeObjectForKey ( "docdir" ) as? NSString
}

This time we do the exact opposite work: From the archived object of the parameter named aDecoder, we decode (unarchive) each single property of the class. Notice that the keys used above are the same to those that we set during the encoding of the properties. Also, the optional downcasting (the as?) here is required, as there may be a variable that was not set with an actual value (so it’s just nil) before archiving.

The above are the only additions needed to be made to the Contact class. The “moral” that comes out it is this: You should always adopt to the NSCoding protocol (if the superclass doesn’t) and implement the encoding and decoding methods in your custom made classes, if you are planning to serialize them at some point. Anyway, speaking of serialization is out the scope, so I just recommend to look at the official documentation for further reading.

Defining a New Activity Type

In the next part of the tutorial we are going to configure and use a new user activity in the ViewController class. However, an activity cannot successfully work if an activity type hasn’t been set in the plist file of the project. So, here we’ll add this new type and then we’ll proceed in the user activity itself.

Similarly to what we did in the previous tutorial, in the Project Navigator open the Supporting Files group, and then click in the Info.plist file. There locate the NSUserActivityTypes entry, and then expand it. The NSUserActivityTypes is an array, and each single content of it matches to an activity type that our application want to support. For the time being, there are already two types defined from the previous implementation. Our goal is to add a new one, so simply select the NSUserActivityTypes array, and then go to the Editor > Add Item menu.

A new entry will be added as the first item of the array. Leave the default String type in it, and then specify the new activity type in the value textfield. The name of the new type is:

com.appcoda.handoffdemo.list-contacts

To avoid any mistyping, just copy and paste it from here. The new user activity that we’ll use next will be in the contact listing view controller, therefore we specify a name relevant to that. I remind you that an activity type should have a reverse-DNS style, just like the above, even though that’s not mandatory. It’s the best option for creating unique strings and making sure that there will be no chance to have the same type specified in two or more applications.

In the NSUserActivityTypes array you should now have three items in total. The next image shows that:

t24_1_plist_sample

Lastly, before we move forward, let me just remind you that even if an activity type is properly set in code to the respective user activity, the Handoff won’t work if the type is not specified in the plist file. That’s why what we did here is the first step regarding the new activity.

Creating a New User Activity

As I said many times until now, we are going to configure a new user activity which we’ll use for managing continuation streams. Now that we specified the activity type in the plist file, the app is capable of recognizing the new activity as a valid one, and eventually make the Handoff function.

In the last post, in both the EditContactViewController and the ViewContactViewController class we created a custom method named createUserActivity(). In its body we set up the userActivity property of each view controller, and we finally called it upon the view appearance. The implementation of a similar method will be the first step in our effort to create a new user activity here too. Remember that the new user activity must be configured in the view controller in which the contacts are listed, therefore in the Project Navigator click on the ViewController.swift file to edit the respective class.

Begin by adding the next code segment:

1
2
3
4
5
6
7
func createUserActivity ( ) {
    userActivity = NSUserActivity (activityType : "com.appcoda.handoffdemo.list-contacts" )
    userActivity?.title = "List Contacts"
    userActivity?.supportsContinuationStreams = true
    userActivity?.delegate = self
    userActivity?.becomeCurrent ( )
}

The first two commands, as well as the last one, are already known from the previous implementation. At start we initialize the user activity by specifying the proper type. Next we set a title to it. With the last command we make the activity current, meaning that this will be the one that will be handed of when needed.

The interesting and important lines here are those two between. With this:

1
userActivity?.supportsContinuationStreams = true

we make the activity capable of supporting continuation streams. The name of the property is quite explanatory.

With the next command:

1
userActivity?.delegate = self

the ViewController class becomes the delegate of the activity. This is a mandatory command, because we’ll need to implement a delegate method later which will handle the request for streaming from the continuing application.

At this point Xcode complains because we set our class as the delegate of the activity. To silence the error, we must adopt a new protocol named NSUserActivityDelegate. Go to the top of the class (to the header line of it) and add the mentioned protocol:

1
class ViewController : UIViewController, UITableViewDelegate, UITableViewDataSource, EditContactViewControllerDelegate, NSUserActivityDelegate

Now that the createUserActivity function is ready, it’s time to call it. We will do that in the viewDidAppear method:

1
2
3
4
5
override func viewDidAppear (animated : Bool ) {
    super.viewDidAppear (animated )

    createUserActivity ( )
}

The first step is ready. Next, we must implement a method of the NSUserActivity class which we’ll use to update the state of the activity. Actually, if there’s data to send, then the Handoff will function when this method is called automatically by the iOS,. Let’s implement it first and then we’ll comment it:

1
2
3
4
5
6
7
override func updateUserActivityState (activity : NSUserActivity ) {
    var contactsInfoDictionary : [String : Int ] = [ "totalContacts" : contactsArray.count ]

    userActivity?.addUserInfoEntriesFromDictionary (contactsInfoDictionary )

    super.updateUserActivityState (activity )
}

I said previously that we are not going to hand off important data using the user info dictionary of the activity. We’ll do that with the continuation streams. However, if no data is send using the above method the hand off doesn’t work, and this fact makes it impossible to start streaming. So, we set the total number of the existing contacts to the user info dictionary just to trigger the hand off functionality. Specifically, in the first line we create a new dictionary which contains one value only: The number of the objects in the contactsArray. Next, this dictionary is assigned to the user info dictionary of the activity, and finally, the respective super method is called to update the super class as well.

The implementation of the above method means that the user activity will be updated every time its state its changed, and in this case we want this to happen when the number of the existing contacts gets increased or decreased. The NSUserActivity class has a property (a flag) named needsSave, which when is true makes the system call the above method. With that in mind, it’s up to us to decide when we want this flag to become true. There are two places that this should happen: When a new contact is saved, and when an existing contact has been deleted.

Start by going to the contactWasSaved(contact:) delegate method of the EditContactViewControllerDelegate protocol. For the time being the current implementation is the following:

1
2
3
4
5
6
func contactWasSaved (contact : Contact ) {
    contactsArray.addObject (contact )

    self.tblContacts.reloadData ( )

}

By adding the flag I said before, here’s how it should look like:

1
2
3
4
5
6
7
func contactWasSaved (contact : Contact ) {
    contactsArray.addObject (contact )

    self.tblContacts.reloadData ( )

    userActivity?.needsSave = true
}

With the above simple addition, every time that a new contact gets added to the app the state of the user activity will be updated, and that means that the handoff will work.

Now, let’s do the same when a contact is deleted. This will take place in the deleteContactAtIndex(index:) method. Similarly as before, add the exact same command as before at the end of that method:

1
2
3
4
5
6
7
func deleteContactAtIndex (index : Int ) {
    contactsArray.removeObjectAtIndex (index )
    Contact.updateSavedContacts (contactsArray )
    tblContacts.reloadData ( )

    userActivity?.needsSave = true
}

Now you can be sure that when a contact is deleted, the handoff will work.

The new activity has been set up and we can proceed to the continuation streams. However, before doing so let’s perform one more small addition to the class, this time to the restoreUserActivityState(activity:) method. As it’s known, this method is called when a user activity is received by the app (when an activity is handed off), and it must be implemented to each view controller existing in the hierarchy up to the top most one. In the existing implementation that is shown below, we simply perform the proper segue depending on the view controller that the handed off activity regards:

1
2
3
4
5
6
7
8
9
10
11
12
override func restoreUserActivityState (activity : NSUserActivity ) {
    continuedActivity = activity

    if activity.activityType == "com.appcoda.handoffdemo.edit-contact" {
        self.performSegueWithIdentifier ( "idSegueEditContact", sender : self )
    }
    else {
        self.performSegueWithIdentifier ( "idSegueViewContact", sender : self )
    }

    super.restoreUserActivityState (activity )
}

Now that we have set up another activity, the above method is incomplete. We must modify it so it handles the new user activity, and of course to check for the new user activity type. In the case of this demo application we don’t really want to do something specific, so we’ll just log to the console the total number of the contacts existing in the user info dictionary of the received activity. Here’s the same method updated as needed:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
override func restoreUserActivityState (activity : NSUserActivity ) {
    continuedActivity = activity

    if activity.activityType == "com.appcoda.handoffdemo.list-contacts" {
        let totalNewContacts = activity.userInfo ! [ "totalContacts" ] as Int
        println (totalNewContacts )
    }
    else if activity.activityType == "com.appcoda.handoffdemo.edit-contact" {
        self.performSegueWithIdentifier ( "idSegueEditContact", sender : self )
    }
    else {
        self.performSegueWithIdentifier ( "idSegueViewContact", sender : self )
    }

    super.restoreUserActivityState (activity )
}

At this point all the initial implementation and preparation in the ViewController class is over. From now on we’ll focus on the continuation streams, and we’ll return here later to prepare the output stream that will contain the contact data.

Initiating Continuation Streams

A continuation streaming process is always triggered by the continuing device/application. If you recall, there are certain methods that are implemented in the application delegate of an app either mandatorily or optionally to handle the continuity, and among them the most important one is the application(application:continueUserActivity:restorationHandler:). This delegate method is called at the moment a handed off user activity is received by the device, and its job is to promote that activity to the proper responder (i.e. to the proper view controller).

What I just said is already known, as we met it in the last post. The new element now is that the above delegate method is extra valuable, as it consists of the place where the continuation streams begin to work. However, this is a quite generic word, so let me be more specific and say that there’s a special method of the NSUserActivity class that should be called in the body of the application(application:continueUserActivity:restorationHandler:) when continuations streams should be used.

Let’s see everything in code now. In the AppDelegate.swift file the current implementation of the above application delegate method is this:

1
2
3
4
5
6
7
8
9
10
func application (application : UIApplication, continueUserActivity userActivity : NSUserActivity, restorationHandler : ( [AnyObject ] ! ) -> Void ) -> Bool {
    if let win = window {
        let navController = win.rootViewController as UINavigationController
        let viewController = navController.topViewController as ViewController

        viewController.restoreUserActivityState (userActivity )
    }

    return true
}

What the above does is simple: It uses the restoreUserActivityState(activity:) method to propagate the received user activity to the proper view controller. Now, we’ll extend it a bit more, checking if continuation streams can actually work with the current activity:

1
2
3
4
5
6
7
8
9
func application (application : UIApplication, continueUserActivity userActivity : NSUserActivity, restorationHandler : ( [AnyObject ] ! ) -> Void ) -> Bool {
    ...    

    if userActivity.supportsContinuationStreams {

    }

    return true
}

The supportsContintuationStreams property of the activity object tells for sure if streaming is supported by the current activity. It’s the same property that we set to true when we initialized the user activity in the ViewController class. This check is meaningful if you have more than one activities that support continuation streams, and not just one, like in this application. Here exists just for demonstrative reasons.

The next step is to begin the continuation stream process by asking the originating device for more data:

1
2
3
4
5
6
7
8
9
10
11
12
func application (application : UIApplication, continueUserActivity userActivity : NSUserActivity, restorationHandler : ( [AnyObject ] ! ) -> Void ) -> Bool {
    ...

    if userActivity.supportsContinuationStreams {

        userActivity.getContinuationStreamsWithCompletionHandler ( { (inputStream : NSInputStream !, outputStream : NSOutputStream !, error : NSError ! ) -> Void in

        } )
    }

    return true
}

The getContinuationStreamsWithCompletionHandler() method does all the job. Simply by calling it, the originating device is provided with the stream objects of the completion handler, while the app here awaits to get data back through streaming. The input stream of the completion handler is the output stream that the originating device will write data to.

For the time being we won’t do anything else here. Optionally, we could send some data using the output stream, but that’s not necessary. The next step is to handle the originating side of the app, where we are going to write the actual data to the stream, and after that we’ll get back here.

Writing Data to Output Stream

When the originating device/application is asked for continuation streams, the userActivity(userActivity:didReceiveInputStream:outputStream:) delegate method of the current activity is called. This delegate method is provided by the NSUserActivityDelegate protocol, which we have already adopted in the ViewController class. It’s our duty to prepare the data that will be streamed in this method, and eventually write to the output stream. Notice that the output stream that we’ll write data to, will become the input stream in the continuing device.

Open the ViewController.swift file, and define that delegate method:

1
2
3
func userActivity (userActivity : NSUserActivity, didReceiveInputStream inputStream : NSInputStream, outputStream : NSOutputStream ) {

}

Before we implement it, let’s declare the following three instance variables (properties) to the class:

1
2
3
4
5
var outputStream : NSOutputStream !

var dataToStream : NSData !

var byteIndex : Int !

In the outputStream variable we will assign the output stream that the above delegate method has as a parameter. This is necessary to do, as after having configured the output stream and set the data to send, we’ll work with a delegate method of the NSStreamDelegate protocol that requires the stream object to remain alive, even if the program execution exits from the userActivity(userActivity:didReceiveInputStream:outputStream:) method.

In the second variable named dataToStream we’ll assign the data that will be streamed. As you see, this is a NSData object. Actually, the contacts array will be serialized, and the results of the serialization will be assigned to that variable. All will become clear about this in just a while.

Finally, the byteIndex integer variable is just a pointer that will indicate to the program the exact byte of the dataToStream object that should start writing to the output stream from. Also more about this in code.

With the above three instance variables already declared, let’s go back to the userActivity(userActivity:didReceiveInputStream:outputStream:) method implementation. Right next I give it in one piece, and then we’ll discuss about it:

1
2
3
4
5
6
7
8
9
10
11
12
func userActivity (userActivity : NSUserActivity, didReceiveInputStream inputStream : NSInputStream, outputStream : NSOutputStream ) {
    if let allContacts = contactsArray {
        byteIndex = 0

        dataToStream = NSKeyedArchiver.archivedDataWithRootObject (allContacts )

        self.outputStream = outputStream
        self.outputStream.delegate = self
        self.outputStream.scheduleInRunLoop ( NSRunLoop.mainRunLoop ( ), forMode : NSDefaultRunLoopMode )
        self.outputStream.open ( )
    }
}

First of all, using an optional binding we make sure that there’s data to send. If the allContacts variable gets assigned with a value, then the app can proceed to the stream preparation.

In the if let statement body now, at first the byteIndex variable gets its initial value. As this variable points to the byte position that should be grabbed data from and then written to the output stream, it’s obvious that initially it should read from the beginning of the dataToStream object. Note that all this will happen later.

The next line is important: The contacts array is archived and serialized using the NSKeyedArchiver class, and the resulting data is assigned to the dataToStream object. Note that the serialization wouldn’t work and an error would be produced here during the app execution in case we skipped to make the Contact class NSCoding-compliant. Even though you can’t see directly how the encodeWithCoder(_:) affects the serialization, be sure that without it we would be unable to convert the contacts data from a NSMutableArray to a NSData object.

Lastly, the output stream is being prepared. At first, we assign the outputStream parameter stream object to the instance variable we previously declared. Next, the ViewController class becomes the delegate of that output stream, as we’ll implement a delegate method right next to handle the data that will be streamed. Then, the thread to which the output stream will run is specified. To keep it simple, we let it run to the main thread of the application. Ultimately, using the open() method, the stream is opening for writing. Keep in mind that both the input and output streams this delegate method accepts are not opened, therefore you should do so manually as shown above.

Now, our app knows what to do when it’s asked for continuation streams. Our next step is to adopt the NSStreamDelegate protocol, and then to implement a specific delegate method that is required to handle the data that will be written to the stream. Begin by going to the header of the class, and add the mentioned protocol as shown next:

1
class ViewController : UIViewController, UITableViewDelegate, UITableViewDataSource, EditContactViewControllerDelegate, NSUserActivityDelegate, NSStreamDelegate

The delegate method that we’re about to implement now is called stream(aStream:handleEvent:). This method should be always defined when working with streams for two main reasons: At first, this is the proper place to write data to output streams, or to read from input streams. At second, using the handleEvent parameter value, you are given the ability to define and check each possible state that a stream may enter to (open, ended, has available space, and more…), and of course to respectively act.

Let’s get started. At first we must make sure that the current stream is the output one we want to edit, and that there’s available space on that stream to write to:

1
2
3
4
5
6
7
func stream (aStream : NSStream, handleEvent eventCode : NSStreamEvent ) {
    if aStream == self.outputStream {
        if eventCode == NSStreamEvent.HasSpaceAvailable {

        }
    }    
}

In this demo application the first if statement is not actually necessary, however I’m just showing you that you should always make sure that the stream you’re about to work with is the correct one.

Next, the actual work for writing to the output stream is taking place. Note that the presented code is the Apple’s code from the documentation regarding the writing to output streams. I intentionally used it, because I considered to be a good idea to to have the same code in case you want to read more about streams (how to write to and how to read from them). There’s one difference only, the Apple’s example is written in Objective-C, while here I’ve converted it in Swift.

Having said the above, let me give you the code and then explain how it works:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
func stream (aStream : NSStream, handleEvent eventCode : NSStreamEvent ) {
    if aStream == self.outputStream {
        if eventCode == NSStreamEvent.HasSpaceAvailable {
            var readBytes = dataToStream.bytes            
            readBytes += byteIndex

            var dataLength = dataToStream.length

            var len : Int !
            if dataLength - byteIndex > = 1024 {
                len = 1024
            }
            else {
                len = dataLength - byteIndex
            }

            var buffer = Array<UInt8> (count : len, repeatedValue : 0 )

            memcpy (UnsafeMutablePointer (buffer ), readBytes, UInt (len ) )

            len = outputStream.write (buffer, maxLength : len )

            byteIndex = byteIndex + len
        }
    }

}

Let’s begin “decoding” the above. If you’re new to this then keep reading. On the other hand, if you’re familiarized with streams you may skip the following. So, the first two lines…

1
2
var readBytes = dataToStream.bytes            
readBytes += byteIndex

…are important for two reasons: At first, we make the readBytes variable to point to the contents of the dataToStream object. These contents are returned by the bytes property of the NSData class. This pointer defines where the program should start reading in the source data object from, with aim to write this data later to the output stream. By default, it points to the beginning of the data, so we use the byteIndex variable (the one we declared previously) for making the readBytes pointer point to the proper byte.

Next, with the var dataLength = dataToStream.length line we just keep in a local variable the length of the whole data that’s about to be sent. It’s used right below.

The data that will be streamed will be in packets of 1024 bytes in size (you can change that if you want), and that number specifies the length of an array to which the read bytes will be added to. In the next few lines this length is specified.

1
2
3
4
5
6
7
var len : Int !
if dataLength - byteIndex > = 1024 {
    len = 1024
}
else {
    len = dataLength - byteIndex
}

Obviously, as long as there are enough bytes, the len variable will be equal to the 1024 value, however, it’s quite impossible for the last bytes to be 1024 exactly, therefore the length of the last “packet” is calculated on the fly.

After that, an array known as buffer is being allocated. Note that its size is equal to the len value calculated above.

1
var buffer = Array<UInt8> (count : len, repeatedValue : 0 )

With the following line, the read bytes from the source data is written to the buffer:

1
memcpy (UnsafeMutablePointer (buffer ), readBytes, UInt (len ) )

But the most important one is the next command. It writes the buffer to the output stream:

1
len = outputStream.write (buffer, maxLength : len )

The above write stream method gets two arguments, the buffer with the data, and the size of the buffer. What it returns is the actual length of bytes that was written to the stream. Normally, this value is equal to the calculated one, however if any error occurs, it just returns -1. Keep that in mind if you want to check whether the output stream was written indeed or not.

Lastly, we increase the byteIndex variable by the size of the written data to the stream:

1
byteIndex = byteIndex + len

The above delegate method is called repeatedly until all the data has been sent, so upon the next calling the byteIndex variable will make the readBytes point to the proper byte.

There’s one last thing that we should take care about before we say that we’ve finished our work here. If you remember, previously we used the open() function to open the output stream, and that means that we’re also responsible of closing it once there’s no more data to write. It doesn’t close automatically. To do that, we must check when its end is found, and then do so. Let’s add some more code to the above delegate method:

1
2
3
4
5
6
7
8
9
10
11
12
13
func stream (aStream : NSStream, handleEvent eventCode : NSStreamEvent ) {
    if aStream == self.outputStream {
        if eventCode == NSStreamEvent.HasSpaceAvailable {
            ...
        }

        if eventCode == NSStreamEvent.EndEncountered {
            outputStream.close ( )
            outputStream.removeFromRunLoop ( NSRunLoop.mainRunLoop ( ), forMode : NSDefaultRunLoopMode )
            outputStream = nil
        }
    }    
}

As you see, a new if statement was added. In it, once we are sure that the end of the stream has been found, we close it, we remove it from the running thread, and finally we make it nil, so we can reuse it if needed (in this demo application this isn’t going to happen).

Now everything is ready from the point of view of the originating device. Once it’s asked to send data using continuation streams, it prepares it and it sends it. Let’s see next how the continuing part will handle the received streamed data.

Reading Data From The Input Stream

Let’s see now what actions must be taken so the app can successfully handle the input continuation streams. In this part, what we’ll do is more or less the exact opposite thing of what we just did. Specifically, we will get the input stream, we’ll open it and then we’ll read the data that contains. Ultimately, when the reading is over we’ll convert the received contacts data to an array and we’ll replace the existing ones. The last action will take place in the last part. For now, we’ll focus on the input stream reading only.

As you probably assume, we’ll work in the AppDelegate.swift file, and more specifically to the application(application:continueUserActivity:restorationHandler:) method. Previously, we made a call to the getContinuationStreamsWithCompletionHandler() there, but we didn’t add any code at all in the completion handler’s closure. Now we’ll add the proper code to handle the input stream.

Once again, make sure that you’ve selected the AppDelegate.swift file in the Project Navigator. Initially, we’ll declare three instance variables that we’ll make use of in the upcoming implementation:

1
2
3
4
5
var inputStream : NSInputStream !

var receivedData : NSMutableData !

var currentActivityType : String !

Obviously, in the inputStream variable we’ll assign the incoming stream and then we’ll handle it. In the receivedData variable we’ll store the received data. Later, we’ll send this object to the ViewController class, so it converts it to an array object and use the contacts data. Note that the receivedData object is of type NSMutableData and not NSData. That’s important, because we want to be able to change its contents, and the NSData type is an immutable one. Lastly, the currentActivityType will keep the activity type of the current activity, and based on that we’ll make sure that we handle the data of the right activity.

With the above variables being ready, let’s add the missing code from the getContinuationStreamsWithCompletionHandler() method’s completion handler:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
func application (application : UIApplication, continueUserActivity userActivity : NSUserActivity, restorationHandler : ( [AnyObject ] ! ) -> Void ) -> Bool {
    ...

    if userActivity.supportsContinuationStreams {    
        userActivity.getContinuationStreamsWithCompletionHandler ( { (inputStream : NSInputStream !, outputStream : NSOutputStream !, error : NSError ! ) -> Void in
            if let someError = error {
                println (someError.localizedDescription )
            }
            else {
                if let inStream = inputStream {
                    self.receivedData = NSMutableData ( )

                    self.currentActivityType = userActivity.activityType

                    self.inputStream = inStream
                    self.inputStream.delegate = self
                    self.inputStream.scheduleInRunLoop ( NSRunLoop.mainRunLoop ( ), forMode : NSDefaultRunLoopMode )
                    self.inputStream.open ( )
                }
            }
        } )
    }    

    return true
}

At first, we make sure that no error has occurred. In case that it actually has, then we simply print the error description. Don’t forget that in a real-world application you should implement a better approach and error handling than that.

The interesting part begins if no error occurs. In this case, the first thing we do is to initialize the mutable data object, the receivedData. Next, we store to the currentActivityType instance variable the type of the current activity. No matter if you don’t get the meaning of that right now; you’ll understand it later.

Finally, we assign the input stream to the inputStream instance variable that we previously declared. We make the AppDelegate class the delegate of the input stream, and then we schedule it to run in the main thread. Once all those have been done, we just open it so we read it.

Let me note two things here: First, you can easily assume that we’ll implement the stream(aStream:handleEvent:) delegate method of the NSStreamDelegate protocol. At second, pay special attention to the optional binding of the completion handler’s input stream. We don’t proceed unless we make sure that the input stream is not nil. In case we don’t do that and for some reason the input stream is nil, the application will crash.

Let’s get going. Undoubtably, before we continue we must adopt the NSStreamDelegate protocol. Therefore, move at the beginning of the AppDelegate class and do so:

1
class AppDelegate : UIResponder, UIApplicationDelegate, NSStreamDelegate

Let’s handle the input stream now. Let me give you the implementation first, and then we’ll talk about it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func stream (aStream : NSStream, handleEvent eventCode : NSStreamEvent ) {
    if aStream == inputStream {
        if eventCode == NSStreamEvent.HasBytesAvailable {
            var buffer = Array<UInt8> (count : 1024, repeatedValue : 0 )

            var len = 0

            len = inputStream.read (UnsafeMutablePointer (buffer ), maxLength : 1024 )

            if len > 0 {
                receivedData.appendBytes (buffer, length : len )
            }
        }
    }
}

As I also said in the previous section regarding the writing to the output stream, similarly you can find the respective Objective-C code of the above snippet to the official Apple documentation regarding the stream reading.

Let’s discuss now what happens in the above segment. Once again, we implement the stream(aStream:handleEvent:) delegate method to handle the stream. At first, we check if the current stream is the one we are interested in. If that’s the case, then we make sure that there is data to read from using the HasBytesAvailable event code value. Assuming that everything is okay, we proceed. What we do here, is the opposite thing from what we did when we wrote to the output stream. Let’s see it with a bit more details:

As a first step, we initialize an array (the buffer of the data) and we make its length equal to 1024. If you remember, that was the size of the buffer to the output stream too. We set this value, so the buffer can store data up to 1024 bytes in length.

1
var buffer = Array<UInt8> (count : 1024, repeatedValue : 0 )

Next, we initialize an integer variable. In this one, the actual length of the read data is going to be stored:

1
var len = 0

The actual reading is taking place right next:

1
len = inputStream.read (UnsafeMutablePointer (buffer ), maxLength : 1024 )

The read method of the input stream object accepts two arguments; the buffer and the size of the data that should be read. In the len variable the actual length of the read data is stored.

Finally, we append to the mutable data object the received data:

1
2
3
if len > 0 {
    receivedData.appendBytes (buffer, length : len )
}

Note that the receivedData object gets more and more bytes until the end of the input stream is reached.

So, the above code is good enough for reading from the input stream, but there’s a final touch missing here. The input stream must be also closed once its end is found. In the stream(aStream:handleEvent:) method add the following snippet:

1
2
3
4
5
6
7
8
9
10
11
12
13
func stream (aStream : NSStream, handleEvent eventCode : NSStreamEvent ) {
    if aStream == inputStream {
        if eventCode == NSStreamEvent.HasBytesAvailable {
            ...
        }

        if eventCode == NSStreamEvent.EndEncountered {
            inputStream.close ( )
            inputStream.removeFromRunLoop ( NSRunLoop.mainRunLoop ( ), forMode : NSDefaultRunLoopMode )
            inputStream = nil
        }
    }
}

When the program execution encounters the end of the input stream, we can safely close it, remove it from the thread that is being executed, and finally make it nil.

That’s it! The app can now handle an input continuation stream and store the incoming data. What we have only left to do, is to manage this data.

Handling The Received Data

At this point, we know that when the reading process from the input stream is finished the receivedData property contains the data we expect (also supposing that no error has occurred). Even though in the previous part we managed to successfully handle the input stream, we took no actions at all regarding the conversion of the receivedData variable from a NSMutableData to a NSMutableArray object. So, let’s do that here and we’re then ready to test the app.

Initially, we’ll keep working in the stream(aStream:handleEvent:) delegate method of the AppDelegate.swift file. Specifically, in the if eventCode == NSStreamEvent.EndEncountered statement, we’ll post a notification with the receivedData variable as its object. Later, we’ll handle this notification in the ViewController class. Let’s see it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func stream (aStream : NSStream, handleEvent eventCode : NSStreamEvent ) {
    if aStream == inputStream {
        if eventCode == NSStreamEvent.HasBytesAvailable {
            ...
        }

        if eventCode == NSStreamEvent.EndEncountered {
            if let activityType = currentActivityType {
                if activityType == "com.appcoda.handoffdemo.list-contacts" {
                    NSNotificationCenter.defaultCenter ( ).postNotificationName ( "receivedContactsDataNotification", object :receivedData )
                }
            }

            ...
        }
    }
}

In the above snippet, you can see that we use the currentActivityType instance variable to make sure that the activity type matches to the activity which we handle the input stream for. Using the optional binding we ensure that the currentActivityType is not nil. Besides any check, the only thing we do is to post a new notification, named receivedContactsDataNotification. With it, we post the receivedData as its object, and we’re okay.

Now, let’s open the ViewController.swift file for last time, and let’s begin by adding an observer for the above notification. In the viewDidLoad method add the next line:

1
2
3
4
5
override func viewDidLoad ( ) {
    ...

    NSNotificationCenter.defaultCenter ( ).addObserver (self, selector : "handleReceivedContactsDataWithNotification:", name : "receivedContactsDataNotification", object : nil )
}

The handleReceivedContactsDataWithNotification(_:) is a new method that we are about to implement just now:

1
2
3
4
5
6
7
8
9
10
11
12
13
func handleReceivedContactsDataWithNotification (notification : NSNotification ) {
    let contactsData = notification.object as NSData

    contactsArray.removeAllObjects ( )

    contactsArray = NSKeyedUnarchiver.unarchiveObjectWithData (contactsData ) as NSMutableArray

    Contact.updateSavedContacts (contactsArray )

    dispatch_async (dispatch_get_main_queue ( ), {
        self.tblContacts.reloadData ( )
    } )
}

In the above code segment, initially we assign the notification’s object to a constant. That’s supposed to be the received data object. Next, we remove any Contact objects currently existing in the contactsArray array, and we assign the new data to it by unarchiving the contactsData object. Of course, we should not forget to save the new contacts permanently by calling the updateSavedContacts(_:) class method. Lastly, we reload the tableview data in the main thread, so it reflects all the changes that have been done.

Everything is ready, so let’s go and give it a try.

Compile and Run the App

Even to an application as simple as this one, it looks pretty amazing to start adding contacts on one device and then continue doing so them to another one. And as we have made it through this point, this is the best time to plug your devices into your Mac, and build and run the application. I remind you that the Simulator can’t be used, so you will have to use real devices, no matter what they are. Personally, I used an iPhone and an iPad mini. The iPhone was my originating device, while the iPad the continuing one.

Before you test continuation streams, make sure that you have added a few contacts to the originating device. Then, use Handoff and see them being transferred to the continuing device. The next screenshots demonstrate just that.

The iPad app initially with just one sample contact:

t24_2_handoff_1

The iPhone app with the contacts that will be transferred to iPad using continuation streams:

t24_3_handoff_2

Using the app’s icon to Handoff:

t24_4_handoff_3

The iPad app after having received the contacts data:

t24_5_handoff_4

Summary

The last couple posts were dedicated to the Handoff, a brand new capability that Apple brought with iOS 8. Through them, I want to believe that you have acquired the minimum knowledge (at least) that’s required so as to integrate Handoff to your applications. Personally, I think that Handoff can unleash new possibilities to both existing and new apps, so give it a serious consideration when you design your next application. Attention though; Handoff should not be confused with existing iOS capabilities or to be thought as a replacement of other technologies. Always remember that its purpose is to let you continue an activity to another device, but it’s not meant to be used for simply exchanging data or communicating between devices. There are other frameworks and APIs to do that. So, with that we’ve come to the end of the tutorial. I hope you’ve enjoyed it as much as I did at the time of the writing. We always like to hear back from you, so take the chance and drop us a line. So long!

For your reference, you can download the complete Xcode project here.

Source : appcoda[dot]com
post from sitemap

Không có nhận xét nào:

Đăng nhận xét