App Frameworks • iOS, macOS, tvOS, watchOS • 35:54
Gain tips from seasoned framework engineers about how to get the most out of your Cocoa development efforts. For example, find out how you can get perfect hashing behavior from NSDictionary with a simple API. From best practices to lesser known APIs, there is something for everyone to learn.
Speakers: Rachel Goldeen, Vincent Hittson
Unlisted on Apple Developer site
Downloads from Apple
Transcript
This transcript has potential transcription errors. We are working on an improved version.
- Good morning everybody, I'm Vincent Hittson, Cocoa engineer along with Rachel Goldeen, Cocoa engineer and welcome to Cocoa Development Tips. Rachel is going to start us off.
- Good morning, we have a wide array of tips to show you this morning, we're going to speed through them. And it was a little tricky to figure out how to order them, what kind of sequence or numbering system to use, we think we got something really good. I'm going to start you out with a quick tip to get things going.
Number pi internationalization because it's all Greek to us. Just take a look at the Xcode Scheme Editor and in the run section you'll notice there's this location debugging section. You can check the show nonlocalized strings checkbox and see where your app is missing localized strings. But I'd like to focus your attention on this application language menu. Down at the bottom there's a list of pseudolanguages that you can use before your translations have been made and this will show you if your app is ready for those translations. Okay that's it, back to Vince.
Okay tip number zero, user defaults since zero makes a pretty good default value. So, you have your standard user defaults and this is what you use to get and set your user preferences, but it's not just a big bag of preferences it's actually a set of domains and each of those domains have their own bags and here's a subset of those. And when you ask the standard user defaults for a value it checks each of these domains in order returning the first value it finds.
And you can set values in these domains using their own techniques and we're going to go through them in reverse order. So, starting at the bottom is the registration domain, it's last in the list and you use the register method, passing in the dictionary of the user defaults, keys and values. And it's temporary so it's only available for the lifetime of this particular process. The next time your app launches you'll have to call register again.
So, use this to provide initial values for your defaults without setting them permanently. Next up is the global domain. This is shared by all applications and it's commonly used by system frameworks to store systemwide preferences. And you can set it using the defaults command line tool, but it's persistent so any changes you make are going to be permanent so be aware of that.
Next up is the application domain and when you set a value on the standard user defaults using the set method it goes here. And this domain is persistent so the next time your user launches your app it'll have the value that you set so you'll want to use this when your user actually changes a preference from the initial value.
Last or first, but not least is the argument domain and the argument domain is really cool. So, you can pass your user defaults key as an argument to your application along with the value and you prepend your user defaults key with a hyphen. And it's first in the list, so it overrides the values of all those other domains.
And it's temporary, so it's only set when your app is launched with these particular arguments. If you run your app and these arguments aren't there the values won't be there. So, this is really handy if you're trying to reproduce an issue involving user preferences and you don't want to actually set them on your machine or for just enabling a debug flag.
And speaking of debug flags, AppKit has a couple, here's some that you can use. NSiewLayoutFeedback LoopDebuggingEnabled, you can set this to yes if your app is getting stuck repeatedly doing layout. The feedback loop debugger will help you track down that that issue. Next is NSApplicationCrash OnExceptions and use this in NSApplication if it catches an exception it'll crash your app. So, it's a good thing to have on during development so you find those issues. And you probably want to use the argument domain to set these so you don't shift with them so that's a good idea.
Here's a neat thing about the standard user defaults, it supports key-value observing or KVO. You just add your observer with the user defaults key as your keyPath and any time that value changes even if it's from another process you'll get notified. So, here's an example of what that looks like. We add an observer using the addObserver for keypad options context method and pass our user defaults key as a sting. And of course, when we're no longer interested in watching for these changes we remove our observer.
And in the meantime, you'll get an observeValue for keyPath call out any time your defaults value changes. So that's about that, here's what that looks like in diagram form. So, if your application sets a user default and you have an observer registered it will be notified of that change. And similarly, if another process sets a value for one of your user defaults that you're observing, you will be notified of that as well. So, it's pretty straightforward.
Here's another neat trick. So, if you want that new keyPath syntax and that block-based observe method that you heard about in what's new in Foundation you can do that with user defaults. You just need to add an extension and add at an @objc dynamic property that has the same name as your user defaults key and that's really important. If the names don't match this isn't going to work. And you can as a convenience implement your property getter to return the value. And then you can use the block-based observe method along with the keyPath literal syntax that's new and it works just like you'd expect.
So that's it for user defaults. Next up is tip number 64, what is that? Oh, base 64 right. So, for those that aren't familiar base 64 is an encoding that lets you represent any data using a simple set of ASCII. So, this is handy of you want to have a test or an easy to copy paste and share snippet of code that includes some binary data and you don't want to worry about packaging that up and having to look it up and all that. And NSData supports base 64 and you can create a data from the base 64 string using the base 64 encoded initializer on data and you'll get back your data. And then you can use it [sound effect] for example, to play a sound.
And now if you want to create these base 64 strings you can use the base 64 encoded string method on data to get those strings. There's also a base 64 command line utility that you can use in terminal to get your base 64 strings and that's base 64. Now back to Rachel.
Number 2X, we're going to talk about asset catalogs a little bit. Many of you are already using asset catalogs, you know that they organizer your images, speed up launch times, reduce storage space. And I'd like to talk about a few things that were added last year in macOS Sierra and iOS 10 in case you hadn't heard about them.
First, there's layout direction. This action image is symmetrical and it doesn't matter, it'll be the same both left to right and right to left so the direction is fixed. But here's a GoForward image and in right to left languages GoForward means to the left whereas left to right means it's going to the right.
So, we want it to mirror for the other language. And then a little more sophisticated is when you have an image that needs to be different for the layout directions, but isn't a mirror image, such as this autofill image where the dots indicating what has been written are to the left of the pencil in left to right and to the right of the pencil in right to left, but the pencil still tilts to the right. So, they're not mirror images and the designer decided it was worth it to provide two different images, but asset catalogs will handle this for you.
Next, I'd like to talk about display gamut and that's the different kind of color representations that are available on different displays, the older displaces use sRGB, the newer ones with brighter, richer colors use Display P3. I've overexaggerated the difference between these images, but just to show you that asset catalogs allow you to specify specific images for the two different display gamuts.
In iOS 11 and macOS Sierra we added colors to asset catalogs. So, you can make a named color just my simple accent color here and you can also if you need to specify sRGB and Display P3 versions of that color. To use it in code we want to use this new API. You can add your name to the extensible NSColor.Name enum and then use the color named initializer with your name to create your color it's as easy as that.
Next up number one, unit tests. Sometimes there's a psychological barrier to writing unit tests, it seems like it's going to be hard, but I'm here to show you how easy it can be. Create a new file in Xcode, make it a unit test case class, Xcode will give you a template, you can fill it in with something as easy as this.
Here I've created my addCatImage and I'm checking to see that it's not nil. It's just a sanity check, is my asset catalog on and working. And once you've got one unit test written it's much easier to write additional tests and prevent bugs from getting out to your users. Back to Vince.
Okay tip number 27, NSBox. Sometimes all you want is a simple view that shows a background color. And you control it may be like this, so you can implement one and you can make it layered back even and that's fine or you can let AppKit do it for you. We have NSBox all you need to do is set the box type to custom and then configure it setting the fill color.
And this would be more dynamic than just setting the background color on a layer because if the color's a system color that changes dynamically based on appearance or context NSBox will update automatically. And even better, you can drag it out of Interface Builder and configure it from there.
And there are several properties that you can use to configure a custom box. There's border width, corner radius, border color and of course, fill color so use that. And you can even set a content view on a box to show another view inside the box, so it's really easy.
And that's not all, sometimes all you want is a simple separator and NSBox has you covered there too. All you need to do is create a box with the separator type and position it however you like. And of course, it's available from Interface Builder. So that's NSBox it should make creating your user interfaces a lot easier. Next up is tip number eight, restorable state.
So, macOS has a feature where you can restart your machine and all your running applications come back and using this invaluable trio of methods on NSResponder you can bring your user right back to where they left off. And most AppKit controls will have these implemented for you, but if you have your own controls or restorable state, you can implement them yourselves. And this is what that might look like. So, you have your properties that are your restorable state, when they change you invalidate restorable state.
And then at some point, the system will call in code restorable state so you take the values from your properties and put them in the coder and then when your application is relaunched we'll call restore state with coder and you take the values out your coder and put them in the properties.
It's pretty straightforward and pretty easy, but there is an easier way. NSResponder has this class property restorableStateKeyPaths and this can point to any NSCoding conforming type and AppKit will use key value observing to watch for changes to these properties. So that means when restoring will automatically set the values for you, so it's really easy.
Do note that these properties are marked @objc dynamic and so that's key value observing can do its automatic notification. And you can still use this with the other state restoration callouts. You can use this for your simple properties and then use those other callouts for other bits of restorable state you may have, they work together. All right, now back to Rachel.
Okay I'm going to tell you a sequence of tips about Core Data that build upon each other and I'm starting with number 13 because you may have been unlucky enough in the past to have to set up a Core Data Stack before the existence of NSPersistentContainer that was added last year in Sierra and iOS 10.
Core Data Stack consists of a managed object model and then you have a persistent store coordinator that coordinates one or more persistent stores and a managed object context. And to set that up took a fair amount of code. This is a reduced version of the code you needed to write to get that going. But last year with the hard work of the Core Data team there's the NSPersistentContainer class setting encapsulates the stack and it's easy now. You just have to do this. The key line is to initialize your persistent container with your model name. Moving on to number 21, arrays.
I've got my Cat Wrangler application, one of those Cat management applications and I have a cat entity in my data model with a name and a photo attribute. And I realize cats also have behaviors, such as attacking dust particles, staring at walls, and playing keyboards. So, I'd like to add an array of behaviors because I think a string is a good representation of a behavior and I just want an array of strings. But there's no array type here, well I just transformable because that can be anything. So, I make an array that's not a good idea.
There's performance issues, there's overhead for serializing and deserializing arrays and their contents and also any such requests are going to be a lot slower if you do things this way. So, we recommend instead making a behavior entity and making a relationship to the behaviors. This would be a too many relationships because there's many behaviors and I want to check the ordered checkbox if I care that they're in a certain order.
In addition, this has the benefit of allowing me to use a richer data model for my behavior, I can add a duration for example. And so, your data model will become more representative of what you're trying to do. Tip number 34, core data migration. Migration is one of the real strengths of using Core Data. I shipped Cat Wrangler and then I realized cats also eat food like grass, cupcakes, meat, and of course broccoli.
So, I need a new data model, I copy my Cat Wrangler data model, make Cat Wrangler 2 and I can add my food entity to it. In Xcode, I simply change the current model version to Cat Wrangler 2 and then lightweight migration is handled automatically if you've used NSPersistentContainer. If you set up your own stack you have a couple of options that you need to set to make the migration happen and that's all you have to do.
Tip number 55, error handling. In a perfect world we'd handle all errors, but everybody knows it's not a perfect world, the world itself isn't entirely round. So, if you can only handle errors in one area in Core Data the most important part is when you've added a persistent store. If things don't work there nothing's going to go right in your application.
If you use persistent container you can see there's the loadPersistentStores method and some of the things that can happen is you can run out of storage space, you can have permission issues or data protection or you can have an older file that can't be opened, you don't have a proper migration strategy for it. And if you're wondering how to present those errors to users Vince has something to tell you.
All right, tip number 404, NSError. So, in the rare event that something goes wrong in your application you want to give your users great error messages and Cocoa APIs like Core Data give you error messages that are fully localized and ready to present to the user and that's really easy to do. Any responder has a presentError method, you pass in your error and it'll go up the responder chain, if anything NSApplication will catch it and show a dialogue.
And if you have your own responder subclass that's well-suited to presenting an error you can override this and present the error yourself. And here's the trick about these Cocoa API errors, you can create them yourself all you need to do is create an NSError, pass in the NS Cocoa error domain, a code that matches what went wrong and then a user info that might have some additional information like the file URL you're acting on.
And you'll get a nice localized user presentable error just like that, just like the ones we return. And we have a lot of error codes, so hopefully you can find something that matches what went wrong. This is a snippet of foundation errors.h, the generated interface, so feel free to look through this, we're not going to go through them all.
And if the error is close, but could be better you can customize them with the user info. There are several user info keys that will let you alter pieces of the error message. Here we're altering the recovery suggestion because we know that this error occurred in the context of a download and we can tell the user that to help them recover.
And by the way, if you're using Xcode 9 you can use this new Cocoa error method to make creating Cocoa domain errors even more convenient. Now if you need an error that Cocoa doesn't provide it's still easy to make your own. All you need to do is define a domain, your error codes, and then you create an NSError with that domain, one of those codes and a user info with keys that describe the error message, provide the error message.
And you don't need to give those keys up front in your user info you can call this NSErrorClass method, set user info value provider, and you get at your domain. And when an NSError with your domain looks up at a info key and it's not found it will call this block. And so, you can lazily return values for those keys. And what that means is now it's super easy to create your errors you just make an NSError with your domain and code and that's it.
Now there are a lot of properties on NSError, check out the header for more. I do want to note that a lot of them have user info keys that correspond so you can use them in your user info value provider and return values lazily, so give those a look.
Now next up is tip number six, shared keysets. So, if you were creating a lot of dictionaries with the same known keys you can use a shared keyset and your dictionaries will be more compact and performant, thanks to proof of hashing. So, you create a shared keyset using this NSDictionary class function and it comes up with the perfect hash and then you can initialize your NSMutableDictationaries with your shared keyset. Here's what this might look like.
So, you create your shared keyset, it is not trivial to compute a shared keyset so it's a good idea to stash it away and reuse it and create as many mutable dictionaries as you want. And you create your mutable dictionary and then you can use it like any other mutable dictionary and even you can put in keys that aren't in your shared keyset they just won't be as performant. So that's shared keyset. Now back to Rachel.
We've come to hexadecimal A for accessibility, get AX accessibility? And Apple has many ways to make your products accessible to all users. I'm just going to go over a few small things. VoiceOver is not small, but I'll talk about it briefly. It comes built-in on all Apple products and it's easy to set up in Interface Builder, I have my add cat button and I just fill in the accessibility description add cat and then VoiceOver knows what to say.
Also remember to test your apps at low resolutions, low vision users want to see things bigger on the screen. So, for example, here's my Cat Wrangler/screen that I'm really happy with. I try it at a lower resolution it goes off the screen, don't let this happen to you. Last, I'd like to point out the existence of the Accessibility Inspector that's available from the Xcode menu. Launch it, run it on your apps and see where you can improve your apps in terms of being accessible. Back to Vince.
Okay and now toward infinity and documents. So, users today are very demanding, they expect a lot from document-based apps. They don't want to worry about saving. They want it to be easy to name and organize their documents and they want powerful version control. And they even want features like iCloud document sharing that's new in macOS High Sierra that you will never even have heard about until this week.
What's a developer to do? Well you can use NSDocument and NSDocument has a class property autosavesInPlace. By default, it returns false, but you can override it and return true and you get all of those features for free, so it's pretty great. Check out NSDocument if you have a document-based application.
Next up is number 42, the answer to reporting exceptions. For the most part, when NSApplication catches an exception it will just log it and let your app continue limping along. But if you want to add information or handle those exceptions differently you can override this NSApplication method to do your own thing. Now back to Rachel.
All right, to err is human and to [inaudible] computer, especially when the humans have erred so we'll talk a little bit about debugging. You know here's many, many debugging tools and I'm just going to show you a couple that are somewhat hidden in Xcode. Right above the debug console there's this row of buttons, there's the debug view hierarchy button, debug memory graph and simulate location. I'm just going to show you a little bit about the view debugger.
Here I have this simple browser sample application and I can expand out the views in order to see what's going on, is there a view that's hidden by another one, which view is doing what. I can see where the views extend off when they're clipped views, there's many other things with auto layout that you can use in this view debugger so check it out and it'll simplify your ability to debug your view problems.
Sometimes the bugs are in our code, in which case I've come to tip 30512012 right, bug reports. To make it easier for us to fix the bugs include steps to reproduce that's the very most important thing. We love it when you put sample apps in your bug reports, especially when they build and show the problem on the platform that you care about. Include any resources that are needed to reproduce your problem.
And then logs, such as sysdiagnose. There's a page on developer.apple.com that shows how to collect logs for all of our platforms and look through there see whatever might be helpful to the person who's taking a look at the issue and that'll help it all go faster. Back to Vince.
Okay tip number 44.1, here's a really easy way to add bells and whistles to your application. NSButton has a property it's called sound, it takes an NSSound and it's really easy to set. And if you do when the user clicks the button it will play the sound and you can even set it in Interface Builder.
So, use this to give your UI that extra pop that your users are clamoring for [sound effect]. So now tip number 29, I'm going to give a demo not of an NSSound playing from a button that would be a silly thing to demo, we're going to show you a macOS app. It's a daring entry into the overcrowded market of cat management applications it's called Cat Herder and it's completely unrelated to that Cat Wrangler app you might have heard about and we're going to build it from the ground up.
Okay, so we have our project already and here we have our specification for our Cat Herder application and it looks like we have a list of cats on the left-hand side and some add and remove buttons. And then for the selected cat we can edit the name and photo. Okay that seems pretty simple and pretty straightforward. Luckily, we have a designer that already created a nib for us and we can go ahead and look at that and it looks pretty good. We can run it.
And there we go, but nothing's really hooked up, nothing's working so nothing does anything. So, it's time to write some code to get this all hooked up together. But what if I told you that there's a way we can get this user interface working without needing any code. And macOS has a technology called bindings that will let us do exactly that.
So, we can go ahead and go to our storyboard and we have our table view here and a table view is going to show a list of cats, which is probably going to be backed by an array. And it turns out we have a class called array controller in the object browser here and we can go ahead and drag that into our scene.
And an array controller lets you create bindings to an array of objects. So, we can go to the attributes inspector here in the upper right and select attributes and you can have your array controller manage an array of whatever class you like or a Core Data entity. And we're going to use an NSMutableDictionary as a default since that just lets us get things up and running. So, we want our table view to show our list of cats and we can do that by drilling down into our table view until we get to the table column.
And we can go over here to the Bindings Inspector, it's in the upper right it's that little swirly boxy looking thing and we can click on that. And bind the value of the table column to the array controller's arranged objects. And the arranged objects is a property on the array controller that is your array of cats that's meant for the view side of things.
So now our table view will create a row for each cat in our array and that means it will create a table cell view for each cat in our array and have the object value of that cell view set to that particular cat. So, we can go into our table cell view and we have an image view, a thumbnail and the label for the name.
And we can go ahead over back again to our Bindings Inspector and select bind to the table cell view's object value and that object value again is going to be one of our cats. And we can keyPath into that and specify the name of the cat and it will look that up in the dictionary and show it. Similarly, we combined the photo of the cat to the image view here just like that.
Now that's our table view that's it, it's now bound and wired up to the array controller and it'll show its contents. Next up we have these buttons here in the corner, so array controller actually has ID actions for adding and removing objects. And the add method does about what you'd expect, it creates a new instance of the object it's managing, in our case an NSMutableDictionary and adds it to the array so we can just wire that up. And there's also of course, a remove action that does what we expect, it takes the selected items and removes them from the array. And of course, we need to add some bells and whistles to our application, that is what we're known for.
Just like that and that's the buttons. All that's left now is the detail view on the side. So, we can go back to our text field -- go to our text field here that is the name that will allow us to edit the name and we go back to the Bindings Inspector and we can bind to the array controller selection. And the selection of an array controller it's a property and it's a proxy object that represents the currently selected items in the array.
So, we can keyPath into that selection to actually allow us to edit and show the name of our cat and similarly, we can do the same thing with the selection for the photo and that's it. We've now wired up our application with no code. So, let's go ahead and run it.
And we can add our cats, give them names, give them pictures, add more cats, it's a really amazing application it's so handy. And since we're using bindings you can update these and they'll get updated immediately in the table view. Just like that we can drag in a new picture or a Quick Look and drag in a new picture and it gets updated in the table view as well. And of course, we need to make sure our bells and whistles are working [sound effect], great. So that's bindings.
[ Applause ]
So, you can use bindings to wire up your user interface to your model without having to write any of that glue code. Next steps might be using a real cat class instead of a dictionary or even a Core Data entity that way you'll get persistent -- your list of cats will persist. But you'll never have to write and for that you will have to write some code, but you won't have to write that glue code for your user interfaces and that's bindings. And back to Rachel.
Okay, so number 29 may have seemed like the end, but we have tip and plus one, add your own tips. Some ideas for how to get information and learn more. We have new revised documentation this year, topics are grouped by task. There's a hierarchical structure that allows you to drill down, such as this example of going down and looking at NSView.
There's three different styles of presentation, there's reference documents, there's conceptual articles and sample code. So, there's different ways of approaching the material. We also highly recommend that people check out the release notes on all of our platforms. This is information straight from the engineer's fingertips about what's new in every release. Header files are always a good source of information with line by line comments about all of our APIs.
On Wednesday in what's new in Cocoa we asked people to tweet their own Cocoa development tips and we collected some of our favorites from Twitter. It was hard to choose, but here's some of the tips that we liked. NSHashTable is like NSSet, but it can contain arbitrary pointers such as void star. And also, you can weekly reference objects from NSHashTable. Sometimes generics in Objective-C are a little bit cumbersome looking and you can use a typedef to simplify the way they look and clean up your code. In Swift, you can use typealias to do the same thing.
And if you're coming to the Mac from iOS you may remember that NSWindowController is often a class that you want to use rather than NSViewController. So, check it out and think about it. And our last tip from Twitter is that in Xcode you can use Add Expression to get Quick Look previews for arbitrary addresses.
So, here are some screenshots showing how that would work. I'm stopped in the debugger and I print out the image, this add cat image. I grab the pointer here and I can use the Add Expression menu item and paste in my image pointer. And then if I hit the spacebar I'll get a Quick Look preview of my image.
There's documentation on how to make your own classes Quick Lookable, so check that out. And we've come to the end. For more information, you can go to this page, we have several related sessions that are all in the past. Check them out on video and, you know, cats because we all want to see them. Thanks for being here.