Build the app for development and testing. Py2app builds the standalone application based on the definition in setup.py. For testing and development, py2app provides an “alias mode”, which builds an app with symbolic links to the development files.
Streaming is available in most browsers, and in the WWDC app. App Extension Best Practices App extensions allow powerful ways to expose your application's abilities throughout iOS and other apps.
Discover best practices for various extension types and see how to effectively communicate between your extension and its parent application or the network. Learn techniques for using the shared keychain and see how to improve discovery of your share and action extensions. WWDC 2015 - Session 224 - iOS, watchOS Resources. Download SOPHIA TEUTSCHLER: Good afternoon. Welcome to App Extension Best Practices. I am Sophia Teutschler, I am an engineer in the UIKit Framework Team. Later in this session I will be joined by my colleague, Ian Baird from CoreOS.
The first part of the session I would like to talk about the two main types of extensions on iOS, Action and Share extensions. The later session, Ian will join us to talk about Today Widget enhancements. Throughout the session we will show you many real-world examples to help you make the most out of those types of extensions. Now, Action and Share extensions.
Let me give you a brief overview of the two types of extensions, how they differ. Now, Action extensions are all about changing content in place, where the Share extensions are all about moving content from the current host application to your application or Web Service. Now, let me give you more examples here. As I just mentioned, Action extensions act on a current content. Because of that, they use the content as the main user interface. I will show an example later on about that.
But you also have the opportunity to present additional options before you perform that action. Also, your application functionality can be separated in different Action extensions that live within that container application. Now, let me show an example here. We are in Safari.
I am going to tap on the action item on the top right, bring up our Share Sheet with our two types of extensions, the Share extension is on the top and the Action extension on the bottom. We made this Translate extension, and when we tap it, the following will happen. Safari will transfer a webpage to that extension. There it is translated and then the translated corners move back into place in Safari. Here's a variation of the same one.
When you tap Translate, you bring up this form sheet where a user can select one of the languages. Then when you tap Language, we transfer the webpage to the extension, then again it's translated and moved back into place in Safari So so much about Action extensions.
So how are Share extensions different? As I said earlier, Share extensions are all about sharing content from the host application to your app or Web Service. And because of that, it's very important that you offer the user to validate and edit that content before it moves out to your Web Service. And we offer you an API for that that I will show you a few examples on later on. Also, your Share extension represents your application in the Share Sheet, and to prevent confusion, we only allow you to offer one Share extension per containing app. Now, let me show an example for the Share extension here. We have the Share in the bottom and our Share extension on the top.
Now, we select four photos, and when we tap on the iCloud Photo Sharing extension, the following will happen. We get a little preview sheet where a user can add a comment, maybe tap the Shared album, and a little preview on the right. But we haven't transferred any data yet. Now, if the information is correct, the user can decide to either cancel out or tap Post to actually perform the Action.
Now let's take a closer look at this sheet here in the Center. This is actually the API I was talking about earlier. It's called the SLComposeServiceViewController. This is how it looks straight out of the box. There's a text field, a little preview on the right. However, it's very customizable, and for our special service, for example, we like to customize it to make it look more like this.
We added a place holder, we even added a character remaining property. We even change the preview view, and we have some options on the bottom part. Now let's go through some examples to build that up. First, to set a place holder, set a place holder property on that sheet. And your Web Service might have a maximum character limit, so we have a little indicator on the bottom left for that. To update it, just set a property there. And again, we offer you a preview view out of the box, but you can have your own one.
To support that simply subclass the composer's controller and override load preview view, and then return your custom view controller for the preview. One thing to keep in mind, however, make sure that the view has - otherwise it might not show up on the sheet. Now, there are these options on the bottom.
Those are simply cells with a value on the left, value on the right, and to tap on them, we push an option view controller that can select your options, your custom options there. Let me show you how you set that up in code. Pretty simple. Subclass SL - composer, then override the - method. Each row is represented as an SL compose sheet configuration item.
Quite a mouthful. On the left is the title. The value is on the right-hand side. Now, if the user taps on a cell, we call this tap handler here, and there you are supposed to create your custom view controller for your custom option. And to push it, simply call push configuration view controller.
This works the same way as the API from your navigation controller. And then finally, return the area of items.
Now, we even offer you auto complete support on the compose sheet. To auto complete, you set a view controller to update the button part here. In this case, it's a table view where we auto complete names, but it could be really anything. To set it, just set the auto completion view property, we do all the animations for you. When you are done auto completing, you set it back to nil. All pretty simple. Again, our SLComposeServiceViewController provides a consistent and familiar UI.
It's very customizable for you to adopt it in your app. However, if you have different needs for your service extension, you can always go back subclassing your view controller directly.
Now, I will just show you how to make Action Share extensions. Now let's change gears a little bit and discuss how you can support extensions made by someone else in your host application. For example, let's say we made a text edit application, and we'd like to share those text documents as well, plain text. However, some extensions might not understand text. They might only support PDFs or other extensions might only support HTML.
Now, you would like to support as many extensions as possible, so a good way would be to offer all those three file types. But when you think about that, those are not really three separate documents when you share a text document, but what you really have is a single document that supports different file formats. And exactly for that we have an API called NSItemProvider. In one sentence, NSItemProvider is a single item with multiple representations. Let me show you a code example to explain it better. Now, we create NSItemProvider, then we call register the item for type identifier to add a new representation to that item provider. In that case, it's plain text.
Now, if the extension asks for the plain text representation, the system will call this load handler block here. And there you are supposed to create your text document on the fly and call the completion handler to return the data back to the extension. Same for PDF. As soon as the extension asks for a PDF representation, we call the load handler, there you create your PDF on the fly and call the completion and let it return data back to the extension. To display Share Sheet, simply create a UI activity view controller and offer the item provider options in the initializer.
Now, in the first example with the compose sheet you saw, we have this little preview in the top right. The hosts should offer previews as well.
Those previews represent what will be shared, and also they need to be simple and efficient to create. They can't be too large. Again, for that, we have an API in NSItemProvider as well. It's called preview image handler. And as soon as the extension asks for a preview, we call this block here, and there you are supposed to create your thumbnail or your preview representation as an image on the fly and call the completion handler to return that data back.
Now we need to change our perspective yet again. We talked first about implementing extensions. Then we talked about supporting extensions as a host app. Now let's look at what it's like to be an extension developer again and how to declare your support with the kind of data the host app can offer to your extension.
For example, maybe you like to have full sharing service, and you want to declare that you are sharing extension support images and movie. Now, let's make three extensions in that case. One extension should support - one extension supports images, another one can handle movies, and a third one handles images and movies.
Now let's see what happens if the host app shares the single image. In that case, the first and third extension will appear in the Share Sheet. Now, same way for the movie case. If the host application shares a single movie, the second and third extension in the Share Sheet, that makes sense. However, what happens if the host of a share supports image and movie?
You would think all the extensions would appear in the sheet; however, this is currently not the case since in iOS 8 the implementation requires that extension can handle all two types of file formats. It's a bit unfortunate since user extension developer would probably like to support as many extensions as possible, and to do that, you should offer as many representations as possible as well.
But in that case, you actually get less extensions than by just sharing a single representation, like movie or image. So we added a new behavior in iOS 9. You can opt in by adding the NSExtension activation dictionary version in new extensions info plist, set it to two. After that, your extension will appear even if the host app shares both image and movie, and even if you are only interested in movies or images. Now, those are high-level activation rules, but we also support activation rules that are just predicates. Again, predicates can be very simple but also very, very powerful like this one here. I won't go into details right now, but check out the App Extension Developer Guide for more information.
Now, let's talk about icons for extensions. As I mentioned before, Share extensions represents your application of Web Service within a Share Sheet. Because of that, we simply use your application icon or a containing applications icon as the icon in the Share Sheet, so there's no additional work for you needed. Action extensions, on the other hand, require template images. We require two sizes, one for the iPad, one for the iPhone. And they reside in the extension bundle.
Now, let me explain how template images work. Think of it as, like, a stencil. A template image is a black-and-white representation with a transparent background, and the system then takes that stencil of that black-and-white image and creates the actual icon for the Share Sheet. And again, require two sizes, 60 points for the iPhone, 76 points for the iPad.
However, I encourage you to use image assets in your extension and offer all different - all the different icon sizes that are for application icons. That way your app extension will be far more future-proof. So much about Action Share extensions. Let's invite Ian on stage to talk about Today Widget enhancements. IAN BAIRD: Thank you, Sophia. I'm Ian Baird, CoreOS engineer, and today I am going to talk to you about Today Widget enhancements.
First a quick recap. As you remember from last year, Today Widgets give you quick information at a glance, so your stock prices, your sports scores. It tells you how long it's going to take for you to get home from work.
So today I am going to talk to you about how to enhance your Today Widget, how to make sure that the model data is always up-to-date and in sync with your containing app. I'm also going to tell you about how to make sure that your visual representation is up-to-date and reflects your widget's content as well. And then I am going to go over some general best practices that you can use for all of your extensions, including your Today Widgets. Let's get started. This is a view of the Notification Center.
It's populated by Today Widgets. You can scroll it to see all of the widgets, and if you want more information about anything you can see here, you can simply tap on the widget, and it will take you to the containing app for more information. Now, how do we facilitate this interaction between the Today Widget and its containing app? Well, we do this by the use of URL schemes. And now I am going to tell you a little bit more about those.
Interactions use these URL schemes and some open URL API on NSExtension contexts to take the user to the app. Now, we have some best practices that I'd like to talk about today governing the use of these URL schemes. First, we'd really like you to use and concentrate on your app's registered URL schemes. This is great. The next thing you want to use is you can use system URL schemes, like HTTPS.
That's going to open a webpage in Safari. You can also create messages, start phone calls, and perform other interactions and start interesting workflows with system components. And here's how you do it. You'll notice that I am showing a table view, did select row at index path callback because a lot of widgets are simply table views. The first thing we are going to want to do is actually construct a URL. We are going to do this with the myapp URL scheme here, just for the sake of example.
This is going to take us to my containing app. The next thing we are going to want to do is we are actually going to want to call that open URL API that you can find on the extension context, which is probably attached to your view controller. Now, this is a little bit different from the API we expose on UI application in that it takes a call-back block, and this call-back block has one boolean parameter that I am going to tell you more about now. The success parameter is going to be set to true if we were able to take you to the containing app or to the system component, which has registered the URL scheme.
It's going to be set to false if we were not able to do this, like if the user has not unlocked the phone and pulls down the Notification Center when the phone is locked. There are many other ways of interacting with the containing app and sharing data, and today I want to tell you about how to use defaults with your containing app; containers; Keychain items; and framework data. Framework data is going to be scribbled into Shared containers on your behalf by system frameworks. And all of this is neatly grouped up into app groups.
First, user defaults. You probably know what user defaults are if you've been developing for Cocoa or Cocoa Touch for a while. They are small pieces of configuration data.
They are like little strings, numbers, booleans, and other things that affect the configuration of your app. You can share these pieces of configuration data between your containing app and your extensions by using the NSUser defaults init with suite name API.
You are going to pass the app group identifier to this API. Now there's an important thing to remember about this, this API doesn't merely unlock your containing app's defaults, your standard user defaults in your containing app.
What it does is it creates a new default suite which your containing app can also have access to if it participates in the app group, so it's super important that you use this API, not only in your extension, but in your containing app when you want to change these configuration items in the defaults. The next thing I want to talk to you about are things that you can use inside of the shared container which the containing app and its extension all have access to. The first thing you can keep in there is model data. Model data is stuff like SQLite files, core data data stores and maybe even model objects persisted to things like plist files. You can store all of this inside of a shared container where the containing application and its extensions can all have access to it.
You can also store documents in there. Earlier, Sophia was talking about a text application, and maybe this text application should store its text documents inside of the shared container, where it can edit them and the extension can edit it.
Next, you can also store media, media items like images, video clips, audio files, and other things that you want to have accessible to both your containing application and its extensions. Now, if you are storing cacheable items in the shared container, that's probably not all that good. I think you want to keep that in caches where it can be purged if we start running low on space. Now, to set up core data to use the shared container, I want to show you a little bit of sample code, and this is sample code that you are going to use both in your containing app and any of its extensions. You are simply modifying the code that Xcode already produced for you via the template. So the first thing you will want to do is create a new property or change the existing one. And in this case, we are calling it secure app group presentation store URL.
Of course, the first thing we need to do is get an instance of the NSFile manager. And next, on file manager, we are going to call container URL for security application group identifier, and again, pass the app group identifier that we had set up previously. After that, we are going to append the store file name to that URL. That URL points in to the shared container, where both the containing app and the extension can access it. Now we need to use this store URL, so we are going to set up our persistent store coordinator.
And the way we are going to do this is by first creating a store coordinator using our manage object model, and then we are going to grab the instance of the store URL we just created. Finally, we are going to add this persistent store to the store coordinator using the URL we just created. This is going to point - remember - to the SQLite file that's backing this persistent store in the Shared container. Finally, we return it to the caller. Lastly, we actually want to set up the manage object context, and we will do this by grabbing our persistent store coordinator and then creating a manage object context. And then we will set this coordinator to be the persistent store coordinator for the manage object context and then return it. And that's all you have to do to set up core data inside of your containing app and any of its extensions to use a shared persistent store which is located in the shared container.
So core data may not be what you need for your application. Let's pretend that you are using the plist files that I talked about earlier to serialize your model objects into the shared container. At this point, you are going to have to worry about synchronization, access, and all this kind of stuff in the shared container because very potentially, your extension and your application could be attempting to simultaneously modify these files, which is not good for data consistency. So you may actually have to use exclusive locks. And you need to be really super careful about using exclusive locks for data in the shared container because when an extension is killed, if it's suspended while holding on to an exclusive lock. Again, this isn't good for data consistency.
So what do you need to do about this? Well, I'd like to talk to you a little bit more about task assertions.
Task assertions are a great way for your extension to tell the operating system that, hey, I'm doing something that you probably shouldn't interrupt. And if you interrupt it, well, I'd like to get some sort of call-back so I can clean things up first. Remember that extensions are suspended pretty aggressively when they are no longer in use. When the user swipes down to expose the Notification Center and then swipes back up to dismiss it, we are going to suspend all of the extensions in play at that time. So that could be pretty quick.
So you are going to want to protect serialization and other cleanup tasks with these background task assertions. And I am going to show you how to do that now using the NSProcess info API. So the first thing you are going to want to do is get the NSProcess info instance by calling the process info factory method. Next you are going to want to call the API perform expiring activity with reason, and I am going to take you through how to set up this call.
The first parameter you are going to pass to this method is a very short string. This short string is for you, not for the operating system. This tells you what you are doing inside of the protected task. The next thing you are going to pass to this API is a call-back block, and the call-back block is going to be used in one of two ways.
And let me go through the first way with you. First, it's going to be called if we were able to acquire a background task assertion on your behalf with expired set to false. This means to you that it's safe for you to perform some sensitive task like serializing data that you don't want to be interrupted. Now, since a task assertion cannot be taken out indefinitely, when the task assertion is about to expire, it's on the cusp of being let go, the operating system is going to call your call-back again, this time with expired set to true. This means that you need to cancel whatever task you have in flight and prepare to be suspended.
Now, when you exit the block, we are going to release the task assertion on your behalf. There's nothing else that you need to do. So the second way that this can work is that we were unable to acquire a task assertion for you at all.
What we are going to do in this case is we are going to immediately call your call-back block with expired set to true. You probably don't want to take an exclusive lock in the shared container at this point because if you get suspended, nothing is protecting you.
At this point, you want to clean up and get ready to be suspended. You only have a few brief seconds to make sure to clean everything up. So that's a complex topic, but I think there are three things that you need to come away with this talk about task assertions. The first is that they're released when your code exits the call-back block.
We grab one at the beginning, and if everything's successful, we set expired to false, and then we hold on to that task assertion for you until your code exits that block scope. The next thing to remember is that there is potentially re-enter and execution of your call-back for the purposes of notifying you about expiration. Again, we can call your call-back block again with expired set to true to give you a hint that, hey, this task assertion is about to expire and it's time for you to clean up.
And then finally, task assertions are not always available for your extension. Sometimes you will call and expired will be immediately set to true and your call-back block will be executed that way and you will not get a chance to perform some critical operation. You need to be prepared to deal with that because it will happen. Next, these task assertions only protect the code in the scope of that block, so they are generally used for very simple things, like quick serialization, something that's synchronous.
What do you do if you need to coordinate with work on another queue? Because you remember that the task assertions are scoped to that call-back block. Well, a call-back block must synchronize with protected work on other queues. So for instance, if you have something on the main queue that you wish to protect, you need to make sure that that call-back block does not exit the original protected block scope before that work has completed. So here's an example of how not to do it.
By dispatch asyncing to the main queue from within the block, execution will exit that block's scope, possibly before perform interrupt will work has completed. This is pretty bad.
So in this case, your block needs to synchronize via dispatch sync or dispatch semaphores or whatever method you choose with the main queue. And remember that this work that you are dispatching to another queue - it doesn't necessarily have to be the main queue - needs to be cancelable because we might call back your block with expired set to true, and at this point, you will need to clean up whatever you are doing, exit, go back to the block, drop out, and release the task assertion.
And it's actually safe for you to do this because this call-back block is executing on a private system queue. You are not going to dead-lock because we are not going to call back into it. Now, that was a lot. I understand.
Moving along here. In our new multitasking world, we can now run into situations where your extension and the containing application are running simultaneously. And this means that if something happens inside of your containing app that changes your model data state, potentially your extension could be out of sync with the model state, and this is bad. So one way, one thing you can use to keep everyone on the same page is the Darwin Notification Center. Now, the API is similar to the NSNotification Center, but it's a lot simpler, and there's a smaller number of use cases for which it is applicable to your containing app and its extension. For example today, we are going to show you how to use it to hint your extension to reload the model.
And this is how you do it. First, inside of that containing app, you are going to want to get an instance of the Darwin Notification Center, and you will do this by calling CFNotificationCenter, get Darwin notify Center. Then you are going to pass this notification center instance to CFNotificationCenter post notification.
The next thing you are going to want to do is to pick a string. This string is going to represent your notification, and you are going to use it in your containing app and any extensions with which to observe this notification. And then you want to pass true. In your extension, again, you are going to want to get an instance of the Darwin Notification Center, and you are going to want to pass this to CFNotificationCenter at observer.
This allows you to observe for this notification. The next thing you are going to want to do is you are going to want to pass a call-back block. This is the call-back block which is executed when the system notices that this notification has been emitted. Then you want to pass the short string. And finally, deliver immediately.
This makes sure that everything stays up-to-date and the model inside of your extension is reloaded when the containing app passes it this hint. Now, remember I am calling it a hint so you probably don't want to use this for coordinating locks between your extension and your containing app because it's not always guaranteed that your extension is going to be around to receive these notifications. So that's how to keep your data model up-to-date. How do you keep your widgets visual representation up-to-date?
And next I would like to tell you about that. We are going to use something called background refresh. And background refresh is a way that the system works with your widget to keep it up-to-date, to keep the visual representation of it up-to-date.
So again, we are going back to the Notification Center, and here you can see our stocks widget. The stocks widget, again, shows the latest stock prices. What happens if, in the course of the day, Apple stock changes? Well, this notification will probably be emitted to your phone, at which point the containing app will probably be expected to do something with it. And this will probably change the visual representation of your widget. Unfortunately, we will notice here that the widget is now out of sync with the stock price.
It's showing $130.12. It should be $132.12.
So how do we fix this? How do we make sure that our visual representation is always up-to-date? Well, for starters, the system is going to opportunistically refresh this content. It's going to talk to the widgets and see if they need to be updated without having to pull down the Notification Center. Each Today Widget view controller conforms to NC widget providing. And if the widget's view controller implements the update delegate method, it gets to participate in this system, and I will show you how to do that now.
The first thing we are going to do is we are going to add widget perform update with completion handler to our Today Widget view controller that conforms to NC widget providing. As a first step, we are going to refresh the model, and this is going to do one thing for us, one very important thing. It's going to tell us whether or not there is a change in the model that is going to materially affect the widget's visual representation. If it did, then we're going to tell the view hierarchy that it needs to redraw. And finally, inside of it, we are going to take the completion handler that was passed to us, and if there was a visual change, we are going to pass new data.
If there was no material change that was going to affect the visual representation of our widget, then we are going to pass no data, and this lets our widget be a good citizen in the Notification Center because at this point, we are hinting the operating system that it doesn't need to expend any resources to update the visual representation of our Today Widget. Since we've done this, our widget has been updated, and you'll notice that the stock price is correct. Next, networking. Extensions are ephemeral objects, really ephemeral processes. They come and they go.
Remember, I was talking about the user swiping down and then quickly swiping up. And this can play havoc with things that take a while to work, like networking sessions. And widgets are expected to call out to cloud services and other things on the network in order to process data. So how do we deal with this? Well, I'd recommend that you take a look at using NSURLSession background sessions. And what are these?
These are tasks, networking tasks, that are performed on your behalf by the system. The updates and events are delivered to your extension for as long as it stays active. But if your extension gets suspended or killed or for some reason hangs up the phone with this critical system - or with this system component, then the containing application takes over error and event handling on its behalf. And I will show you how to set that up. Inside of your extension you are going to want to first create an NSURLSession configuration background session configuration with identifier object.
Or sorry, use the method. And, again, following in the pattern, you are going to want to create an identifier that's going to be used by the extension and the containing app which are participating in this background session. In our case, we are using com.example.my downloadsession. The next thing you are going to want to do is set the shared container identifier on the session configuration. Otherwise, you are going to have a lot of fun debugging while all our sessions come back as invalid as soon as you start a task. At least I know I did.
Next, you are going to want to create an NSURLSession, passing this session configuration to the initializer. Now, you will notice that we are using the delegate form with the initializer because the completion handler form of the initializer is not valid for use with background URL sessions.
Next, for the sake of the example, we are going to download the apple.com website. We are going to create the task and resume it.
And now our extension has successfully created an NSURLSession in the background and handed it off to the system, and the system will begin to deliver events to the extension. Next, inside of our containing app, as I was talking about before, we need to prepare it for any eventualities, for cases where the extension was suspended or killed or otherwise dismissed and can no longer handle the events coming from the system networking component. We do this by adding application, handle events for background URL session to our UI application delegate. The first step we want to take and the step I am taking here again, for the sake of example, is to make sure the identifier is the identifier that we expected. The system is going to pass in the identifier associated with the NSURLSession for the events that you are being asked to handle in the application delegate.
Again, just like in the extension, we are going to set up an NSURLSession configuration item by calling the background session configuration with identifier factory method. And we need to set up the shared container identifier again. And then we need to call NSURLSession, passing this shared configuration, and again, using the delegate variant of the initializer. And then last but not least - and this is very important - you'll miss it if you don't look - is we are going to store this completion handler back. We are going to save it back in our properties for when we're completed, when we are done handling events for this URL session. And you might ask, how do you know when you are done handling events?
Well, I will show you. In our NSURLSession delegate, we need to add this method. URL session did finish events for background URL session.
It will be called when your containing app, or in this case the delegate that your containing app dutifully appointed to take care of these events, is done processing all the URL session events from the background URL session. And the first thing we are going to do is get the session configuration object, and then we are going to look to make sure that the session configuration's identifier matches the identifier that we expect. Then we are going to call this completion handler that we saved earlier and release our reference to the session. And at this point, we are done. We have successfully handled any of the events that have come to our containing app from the system component which was previously started by the extension. Now, you can use this technique for any of your other extension types, not just Today Widgets. It's a really great technique.
But one place where you can't use it is for your Watch OS 1 extensions. Instead, we would prefer that you use background task assertions to protect these small tasks, and you can find out more about it by going to WatchKit Tips and Tricks in the Presidio tomorrow at 10 a.m., given to you by Jake Behrens, also known as the man in plaid. Next, when you are doing network transactions, it's inevitable that you are going to be challenged for credentials. How do you safely and securely protect your users' privacy by sharing these secrets between your containing app and its extensions? We recommend that you use Keychain access groups for this, as shown here. You set this up inside of the capabilities for your extension and for your containing app. And then, using the identifier that you set up in your containing app and its extensions, you simply pass this identifier as the value for the k - access group key whenever you add items to the Keychain.
As shown here. The next thing to tell you about this is that there's automatic search behavior for query API using the Keychain API. So sec item update.
Sec item delete. And sec item copy matching all automatically do the right thing by searching every accessible Keychain for you so you don't have to pass the Keychain access group identifier to these APIs. It's just a tip to remember. And you can find out more about these APIs by watching the Security and Your Apps video.
In summary, today Sophia told you all about Action and Share extensions, about what they were meant to do and how best to use them to share data with your app and the web. She also showed you how to use NS item provider to its fullest possible extent and how to make it do the hard work of sharing data types for you. She also showed you how to make your host application a great environment for Action and Share extensions, and you know what? This increases the value of your host app. Then I showed you how to enhance your Today Widget by making sure that your model objects and its visual representation always stayed fresh and up-to-date. And then we showed you a collection of general best practices, like how to securely share secrets and guard your customers' privacy. There are the related sessions I referenced earlier, the WatchKit Tips and Tricks one tomorrow, and Security and Your Apps that you can view.
And for more information, we'd recommend that you go to the App Extension Programming Guide or talk to our Evangelist, Curt Rothert. Looking for something specific? Enter a topic above and jump straight to the good stuff.
An error occurred when submitting your query. Please check your Internet connection and try again.