Developer Tools • iOS, OS X • 35:23
Objective-C has evolved tremendously in recent years. In this tutorial-style session, learn how the latest features in Objective-C work together to dramatically reduce the amount of code you write, while simultaneously creating cleaner, safer, and better structured code.
Speaker: Malcolm Crawford
Unlisted on Apple Developer site
Downloads from Apple
Transcript
This transcript was generated using Whisper, it has known transcription errors. We are working on an improved version.
Good afternoon. I'm Malcolm Crawford. I work for Developer Publications. Welcome to Migrating to Modern Objective-C. Consider an unfortunate developer who tragically went into a coma in 2005 at the height of his game. Miraculously, a few weeks ago, he came to. It wasn't miraculous that he came to, it was miraculous that he managed to come to at exactly the right moment to get a WWDC ticket. So, he's here now, and obviously, as he regained consciousness, many thoughts went through his mind, but eventually, he came to the really important issues.
What's happened to Objective-C? Why? And perhaps most importantly, how do I use it? So, in addition to just thinking of Rip Van and S. Winkle, think about this yourselves as well, as you're looking at code that you might have had for the better part of a decade or more that might be improved in some way or another.
The agenda is as follows. To start, we'll consider a bit of philosophy. Think about why it is that some things have changed. Then we'll just start looking at code to see what code you might have written some time ago, what we'd expect you to do now, and why. So, to start off with some of the philosophy, Objective-C certainly has evolved. Originally, it was just a thin layer atop of C. Now, it is still atop C, but there's much more to it. It's certainly not such a thin layer anymore.
If you sort of start looking at some of the things that RIP might look at, there's a whole load of other features that have come into play, different patterns and that sort of thing. In general, and you might have seen this slide elsewhere before, this fits in with our general evolution of Objective-C as a language. Starting from pure C, we've added object orientation on top and these other features to progress it in the direction of making it simpler and safer.
Together with these changes in the language, then, have come changes in the way that we use the language, the way that we use the environment, and so on. In particular, then, some of the conventions that we use have evolved. And this is really important because Cocoa has very strong conventions. You know pretty quickly if somebody isn't following the right conventions. We talk about fighting the framework and so on.
In general, again, we hope that the new features are making your code simpler, more compact, and so on, in addition to being safer. But we're also moving in the direction, then, of building some of the conventions that previously you just had to know about actually into the language.
And in particular, into the language so that you don't have to think about them. The philosophical coder here, a quote from Bertrand Russell's thesis supervisor. You don't get much more philosophical. in that. The goal is to reduce the number of mundane, repetitive things that you have to consider, so that you can concentrate on the interesting code that differentiates your application.
So, by way of warming up, a couple of trivial little aspects. Importing headers, fairly mundane thing, but you might see, from a while back, code that looks like this. You might import individual headers files for individual classes. You might forward declare classes and so on, possibly in some effort to make things easier for the compiler or whatever. Don't do that. It doesn't actually make things easier for the compiler. The appropriate thing to do now is simply import the umbrella header for the framework. Straightforward enough, it also reduces then the cognitive load on you in maintaining the list of import statements. So, trivial start.
That all came into play a while back, something much more recent, and again a small change, but a useful one. Enums of fixed underlying type. Historically, we might have had enums that were declared rather like this. standard C. We tried to make things rather better by indicating their type with a type def.
But this doesn't give you an awful lot of help when you are actually writing your code. Adopting the new pattern means you actually get compiler help or Xcode help with code completion and so on. So you can be guaranteed then that the constant that you use in a particular situation is an appropriate one. So small but useful increment. Moving on to rather bigger issues and perhaps rather more contentious issues. Accessor methods.
Start off by looking at the use of accessor methods. In this example, you can see that following appropriate conventions, we're using the accessor method for the area property. This is appropriate. Almost certainly, given what we'll see in a moment, the area property is a derived property. If you try to access it directly, it won't exist.
Have a look, though, at the surrounding context. There are memory management methods. Dealing with memory management methods in this scenario is almost certainly going to lead to problems at some stage. You'll forget to put a method in, put an extra method in, or whatever. Using accessor methods pervasively gives you safer code. You're less likely to make a mistake.
Moreover, it's the appropriate thing to do for following encapsulation. You don't necessarily know what the side effects might be of any accessor method. Using accessor methods pervasively insulates you from those. Quick aside, I said we were doing contentious. It's also possible to use dot syntax in these. That's as much as I'm going to say.
Personal preference. Having used square brackets for a little bit more than two decades now, I'm actually fine with .syntax, but personal preference. If you don't like it, don't use it. The important thing is always use accessor methods, except -- and we'll come back to this in a bit -- in initializer methods and dialog.
Another favorite subject, memory management. We just touched on that in the preceding. Memory management has always been a thorny issue. I'll abide by my own dictum of not repeating the rules. I hope you will appreciate, though, that the code shown here does abide by the rules. So does this code. So does this code.
Choosing between them is more stuff for you to think about. Hopefully, though, if you're sufficiently familiar with the rules, it becomes second nature, and you don't think about it too much until you get to a situation like this. If you start removing objects from a collection and using them, now you're not necessarily sure of the lifetime of that object.
This is a pattern that perhaps over time, and with a certain amount of debugging, you get to recognize. And you realize then that this is the sort of thing that you've got to do in response. You must make sure that you claim ownership of any object that you pull out of a collection, and then relinquish ownership after you've finished using it.
Great. Okay, so you learned that. Next up, though, we get to a slightly more complex situation. I'll leave a second or two if anybody can see what the problem is there. It's a little bit more tricky just to follow through there. It actually follows pretty much, though, the same pattern as before.
The solution to all of these is simply to switch over to Arc. Two other sessions to look at, one we did yesterday, the other tomorrow, to look at adopting Arc. If you adopt Arc, all of the code that you write simply works as it should do, without you having to think about it. Modulo, perhaps.
Interaction with Core Foundation. Core Foundation is still a little bit problematic in that you might have to put in casts to indicate to the compiler what the memory management semantics are, whether you're transferring ownership or whatever. In general, a better solution, if you can find it, is to not use Core Foundation objects in the first place.
In the case of this example, instead of CFUUID, we now have NSUUID. So, elevate things to Objective-C objects, if you can. A quick aside on that, in general, there's a certain amount of lore about why you might want to use Core Foundation-style objects rather than Objective-C objects. Most of that is just now lore.
There is no reason, really, certainly in terms of performance and so on, to prefer CF-style objects over Objective-C objects. One of the things that Arc doesn't prevent you from doing is a sort of thing that you were able to do before Arc, and that's create retain cycles, or as we call them now, strong reference cycles. The answer is still exactly the same as you had before, to use an appropriate pattern to include, for example, weak references.
Weak references include, say, from child back to parent objects, delegate objects, data source objects, and so on, should typically be weak. So follow the same patterns that you would have followed if you were using manual memory management. Now to a rather bigger subject for a variety of reasons. If we have a look at properties, this is one case where I'll give a sort of big before and after.
Here's code that you or Rip might have written back in 2005. As code, it's fine. The left-hand side actually shows the complete declaration of a putative thing class. The right-hand side shows the beginning of the implementation of the thing class. As you can see, that pretty much fills up the whole slide, and there's a lot of implementation missing.
What we're aiming for is this: I hope everyone will agree that there's less code. Trick question. It's also, hopefully, rather more approachable. We've got a very clear contract expressed in the interface, and almost nothing then to write in the implementation. Let's drill down into that in detail. Start off with the instance variables.
Historically, you might have declared the instance variables in the interface of a class. The trouble with this is that this is exposing what's really a private implementation detail of the class. There's no reason why anybody who's actually using your class needs to know what the instance variables are that it uses. Moreover, it's just space that's taken up in your interface for people to read past to get to the actual interesting stuff. You can now, if you absolutely need to declare instance variables independently, move those to the implementation block.
So put instance variable declarations in the @implementation block hidden away in your .m file. Better still, you can use the instance variables don't declare instance variables at all, and we'll come back to that in the subject of properties, or the declared properties, in a moment. Next up, the accessor method declarations.
Again, code that would have been perfectly fine a decade or so ago, now we can do rather better. If we start off with the title declaration, there's two lines of code there. We can do rather better in a single line of code, rather more expressive. You just use a declared property to replace the two lines of the access and method declarations.
We can actually go a little bit further than that, though. Usually, with value-style properties, you actually want your object to have a private copy of those. So, simply adding a copy attribute indicates then that this -- whatever value -- if a new value is passed in, it's going to be copied rather than simply owned. Concentrate next on the radius. Again, we can do a similar thing.
straightforward replacement of the two accessor methods. We're left, though, with the declaration of the derived property. Because it's still a characteristic of the object, it's reasonable then to replace that with A Property Declaration as well. Now, the audience participation part. What's wrong with this? They survived through a lot of revisions. The default declaration of a property is atomic. So, strictly speaking, since a property declaration represents a contract, we should add the non-atomic decorator. Because our implementation, that I skipped past rather quickly perhaps earlier, of the area method was not atomic.
Switch then to delegate. Again, follow the same sort of thing as before, but following conventions, we can now actually specify, as part of a declared contract, that our relationship to the delegate is going to be weak. That goes through the method declarations, turning then to the implementation of the accessor methods.
Again, a subject that caused endless debate on various mailing lists over the years. What's the right implementation of an accessor method? Most of the debates ended up missing the simple fact there isn't any one right implementation of an accessor method. Exactly how you implement it depends on what it is that the accessor method is intended to do. For example, whether you want to copy a value that's passed in.
Given the contract that you specify in the declared property now, you can ask the compiler to come up with the appropriate implementation of the accessor method to meet the specification that you gave in the declared property. So, we can represent... We can replace the entire implementation of the accessor method simply with a synthesize statement.
One of the things that's been a little bit in transition, though, and another thing that's been subject of law, generally, we would actually now encourage you to prepend instance variable names with an underscore. Now, let's just clarify something about this. This is an instance variable name, not accessor method name.
Apple does reserve underscore prefix for accessor method names, not, however, for instance variable names. The reason for prepending an instance variable name with an underscore is to try to dissuade people from accessing the instance variable directly. Remember what I said about using accessor methods pervasively? This is to help people in that direction.
As a quick tangent on this, one of the most common mistakes, one of the most common reasons for errors with core data applications over the years turns out to have been because people have been trying to access the managed object context directly using the instance variable. And lo and behold, it's actually not initialized yet, so they're accessing a nil value. Prepending the instance variable name with an underscore helps mitigate that problem. We're actually going a stage further now, though, with the most recent compiler.
Using auto-synthesize, we do one better, and... it just goes away completely. So, declare the property and you're done with it. Switching to a slightly different area, but still under the general heading of properties, in some situations you might want your object to maintain private state. It's not supposed to be accessed by any other object. Putting it into the @interface declaration is clearly then not the right thing to do. That's an open invitation then to people to meddle with it. What you can do is, first of all, replace that with a property declaration.
You can, though, move the instance variable declaration to the implementation block, as suggested earlier, better still actually provide a property for that as well. Notice in this case, the property is declared in a class extension. So the class extension you are able to hide away in your .m file.
So, takeaway message: If you want to keep private state, maintain private state, use a class extension hidden away in the .m file. A similar thing might have applied to methods, private methods that you wanted to declare. In order to keep the compiler happy and avoid complaints about missing method declarations, you might in the past have had to declare methods in a category in your .m file.
The problem with this is now you have to keep this up to date. You might actually change your method name somewhere along the line, and now what you've declared in your private category is out of sync with what you've actually implemented. A possible way of getting around that is to then use a class extension. This way, the compiler will actually tell you, at least, if you haven't provided an implementation of the methods that you've promised it.
Better still, Again, actually using the latest version of the compiler, you don't need to provide a forward declaration of methods. The compiler will be able to find them for you anyway. So the benefit here is less work again for you to do in maintaining the private category, maintaining the class extension, keeping the declared methods in sync with the methods you've actually implemented. So less work for you to do.
Further in the direction of state maintenance and properties, outlets. Historically, given the strong conventions that we had, outlets were the least well-specified, least well-understood aspect, perhaps, of our entire interface. There is nothing in the traditional declaration of an outlet that tells you anything about its memory management semantics. There's nothing that says how you're expected to use it, whether it's supposed to be a strong reference, a weak reference, and so on.
It's also, again, exposing a private implementation detail. It shouldn't matter to any other object what outlets you have. Instead... Now use a property declaration that makes the memory management semantics explicit. At the moment, to help with tool support, you might well make these declarations in the interface block.
In the future, what we'd actually-- a big one-- in the .h file, in the implement interface block in the .h file. In the future, what we're actually probably likely to do is to move that into a-- Private Class Extension, again in the .m file. The reason for this is, for the most part, the outlets that you declare are likely to be a private implementation detail of your view controller or whatever. Obviously, there may be some that you need to expose, but in general, whatever clients your view controller has should be passing information, data or whatever, to the view controller directly, rather than accessing the outlets.
Consider also whether you want a weak or a strong reference in general. Outlets to subviews of a view controller's main view, or to a window controller's window, are likely to be weak. It's the main view or the window that is expected to manage the lifetime of the views contained within it. Outlets to top-level objects, such as the main view itself or a window controller's window, should be strong.
Switching to other things that weren't particularly tidy in the past, a variety of different ways have been used over the years of implementing init methods. Here's one example. Historically, we might have invoked the superclass's designated initializer and done a test to see if you got a value return. More recent implementations of the compiler actually provided a warning on this, so we changed that then to add an additional pair of parentheses, just to confuse people who looked at it and said, "This is clearly a mistake, isn't it?" No, it was to keep the compiler happy.
Instead, adopt now the -- oh, I beg your pardon. There was another thing to call out here. Also notice, typically in the past, you've used accessor methods to initialize values in an initializer method. Again, don't want to do that. In an initializer method, the state of your object is not defined. It hasn't been fully initialized yet.
To be safe, therefore, don't invoke accessor methods. Simply set property values directly if you need to. And then this is a new pattern that we recommend for the implementation of the initializer method itself. For dialog, again, historically you might well have used accessor methods. The object during a dialog method is in the process of being torn down. Its state is not fully determined. So don't use accessor methods in the dialog either.
Message the instance variable directly. Ideally, of course, if you use Arc, you don't have to worry about this at all anyway. Moving away from properties onto a different subject entirely, something that's somewhat small, but in some respects rather profound. Protocols. Protocols provide a mechanism for formalizing communication channels. Imagine, for example, that you're hiring a butler, as I'm sure many of you are.
butlers, as I'm sure you're aware, have a variety of roles or tasks that you might want them to perform. Obviously, they need to be able to make tea, serve sandwiches. A rather unusual thing that you might ask a butler to do would be to mow the lawn. If you had a reference to a butler as a property, You might then want to impose your will on the butler and say, "If you used a formal protocol, you're specifying then that the butler must mow the lawn." That's a rather heavy imposition.
It might be rather difficult to get a butler that's also willing to mow the lawn. To sort of work around that, we introduced the idea of -- historically, there'd been the idea of informal protocols. An informal protocol is simply a list of methods that are declared on a category of NSObject.
The problem here is instead of being overly specific, now we're under-specific, as it were. We haven't sufficiently well specified what we're actually expecting the butler to do. The only thing that we can say is the butler is likely to be an object. We're not actually making any demands of them at all, or specifying the sorts of tasks that we're actually expecting of them.
The advance that was made with protocols was to add optional methods in a protocol. Now we can say we are going to require our butler to make tea and serve sandwiches. It's up to them, however, whether they're willing to mow the lawn, which puts far less constraints on the sort of object then that we're expecting to be a butler, but also still allows us to be quite specific in our expectations. So, again, we're getting a much clearer establishment of an API contract here.
So, any places where in your code at the moment you might be using an informal protocol to provide a list of methods, now move across to using formal protocols with optional methods, if appropriate. Places where these particularly come into play, you'll have seen these in data source and delegate methods, particularly in table views and so on, on both platforms. On iOS in particular, you might have seen these used as a communication channel between a view controller and its parent view controller.
So you're encouraged to use formal protocols to define the communication channel between view controllers in an iOS application. Other smaller but hopefully -- I believe, actually, welcome features that save an awful lot on typing, the new support for collections and literals. Historically, you'd have written code that looks like this.
rather long-winded code to create a number as an object, to create a dictionary or an array, other collections and so on. If you're creating an array or a dictionary, two problems. First of all, you have to remember to end your list with nil. The compiler actually reminds you now if you make that mistake. Secondly, if you're creating a dictionary specifically, you have to remember to put your keys and values in the wrong order.
I suspect that most people have discovered now that the compiler now supports number and collection literals, Which considerably reduce the amount of code that you have to write, and again make it safer, in that there's no requirement now to terminate your initializer with a nil, and you get to put your, in the case of the dictionary, Keys and values in the right order. Just one example that I was particularly happy with myself, this came out of a sample code that I was updating, and so the code went from this to this. Oh, an applaud. Thank you.
I'll take that on behalf of the compiler team. So thank you very much to them for providing these features. There are additional features. So subscripting then makes some of the other code that you might have written to deal with arrays and dictionaries rather easier. So extracting values and replacing values become rather easier, and hopefully the syntax is more amenable to people coming from other environments and so on.
So arrays and dictionaries. I certainly find it easier now to use the latter in the case of setting a value for the dictionary. The compiler team also provided, a while back, better ways of enumerating over the contents of collections. Historically, you might have written rubbish code like this.
In particular, this tends to crop up a lot, invoking count on each time around the array, which isn't very good. But you're still left, even when you do that properly, you have to hoist out a separate count variable. You're still left within the body of the enumeration with the task of actually pulling out the value that you're interested in. Any code that you see Like that, replace now with fast enumeration. As the name suggests, first of all, it's fast.
It's also a more compact syntax. It saves you from having to declare local variables separately and so on. So, in addition to being more compact, it's also considerably more efficient. However, potentially in some situations, The for-in syntax, the fast enumeration syntax, may not be quite as general as you want. There are some situations in which you want a little bit more flexibility. It's also perhaps sometimes a little bit difficult to add support for fast enumeration to your own classes and so on. To address that and other issues, we now have blocks.
To illustrate that, consider what you might have used in the past to sort the contents of an array. Historically, you might have used sorted arrays using function and passed in a reference to a function. The problem here is now you have a reference to a function that you have to implement somewhere else, outside of the scope of the actual use of that sort operation.
So somewhere you have to think about, well, what did I actually want to do in that context? How do I implement sort function? And especially if you actually want to make use of any contextual information, you've got to pass that in through a rather unpleasant void star. Instead, You can now use the-- you should now use the block-based API Which has the advantage then of being in place. You get to implement the sort function exactly where it is that you're using it.
If we go back to the issue of enumerating the contents of the array, it also means that you have access to additional local information, like the actual index. of the object that you're looking at. So if for some reason you need to actually get at the index of the item that you're looking at, you have access to that now within the block as well.
This is particularly handy in the case of dictionaries. With a dictionary, the fast enumeration gave you all of the keys in the dictionary. It was still up to you, however, to extract the corresponding objects. If you switch this over to using the block-based API, you're conveniently given both key and object at the same time without having to do additional work. A place where blocks have had a somewhat more profound impact still is in what you might call callbacks.
If you consider the way that you might use a notification center, historically, you'd have added an object as an observer and told the notification center the name of the method that you want to be invoked if this notification occurs. First of all, that's error-prone. You must make sure that you get the name of your selector correct.
Secondly, again, you're left with the problem of having to implement the method somewhere else, outside of the context of where you're actually using it. Better now, switch that code over to using the block-based API. With a block-based API, you get to implement the callback in exactly this place that you actually use it. Moreover, again, because it's block-based, you get to use any additional contextual information that you wanted.
As a bonus, One of the perennial problems with notifications was getting notifications on a queue or thread that you weren't expecting to get them on. With block-based API, you get to say, I want to have this delivered to me on, typically, the main queue, the main thread. So if you need to update the user interface, you get it on the right place. Sorry for finishing off on one caveat.
One thing to call out, though, is be careful in blocks of capturing strong references to self and setting up retain cycles. A pattern you can use here is to create a local weak reference to self and use that. So, to wrap up, for more information about all of this, talk to Michael Jurowicz.
There are several sessions, two of which have already happened. There is also now a new document that actually describes -- well, hopefully everybody is adopting all of the latest features and using the latest version of Xcode anyway, but if you do need to support a particular version of the tools, there's now a document that tells you what features are available in which particular release of the tools. The important thing, though, is adopt these new features. They'll make your code better and give you less to type. Thank you.