Sharing code between iOS and OS X

Session 233 WWDC 2014

Learn what the iWork engineers did to ship iWork for iOS and Mac from a single codebase. Explore the patterns for sharing code between desktop and mobile, and see how you can optimize your code and write great apps.

Good morning everyone.

My name is Elizabeth Reid and I'm an engineer on the iWork Team.

And I'm here today with one my co-workers to talk to you about how to share code between iOS and OS X.

So, you know, what we're going to do today is first talk about what code it's possible for us to share and what we probably don't want to share.

And then how we can share more code or have it easier to write shared code using some frameworks.

And specifically also give an example of some shared rendering code.

And then we're going to talk about file formats and how you can optimize and build those for a better multiplatform experience.

And finally, my co-worker Chris is going to talk to you about how to set up your XCode projects to compile cross-platform.

Last fall iWork had a release where we took our iOS applications on our modern code base and we took that code base and brought it over to compile for OS X.

So now we have all of our applications for iOS and OS X compiling from a single code base.

And that was a really big deal for us.

We really wanted to have a single location for all of our features, to have a better user experience and better engineering experience so that everyone's happier.

And so we're going to talk to you about that specific transform from iOS code to OS X.

But, the concepts and examples we're going to give you are meant to be, you know, examples and not specific directives on what you should do.

And so the ideas and concepts should apply if you're going in the other direction, you're taking an OS X application and bringing it to iOS.

Or even if you're building both at the same time.

These are just, you know, principles rather than an obvious iOS only to OS X only.

And we have a lot of different examples because iWork is a really big suite of applications.

We have well over a million lines of code and over 10,000 unique classes.

And of these 10,000 unique classes, over 75 percent of them are shared between iOS and OS X.

Now, note that this number is not nearly 100 percent.

It's a large chunk of code, a lot of the code, but not all of the code.

And that was intentional on our part.

We wanted to choose which code we wanted to share and which code is really meant to be only run on one platform or the other.

And that gives us a really good common experience for our users in our applications while still targeting and embracing the strengths of each device we're running on.

And that was really, you know, a helpful way to build the best applications we possibly could.

So where did we start?

We started with our iOS applications.

We already had iWork running on iPad and on iPhone.

And we had a team of really, you know, excited and passionate developers who wanted to take this code and bring it to our Mac platform and have everything running from one location.

And our code was set up something like this, a rough estimate.

Hopefully you guys are familiar with this pattern.

But just a review, the Model View Controller design pattern is where the model is the data of your application.

It's the content.

And maybe you're sending it over the wires, saving it on disk.

The view is what your user is looking at or interacting with.

And you don't really want those two ever to talk directly to each other.

If your model changes, and you don't want your view to change, if they're tied directly to each other, that's - you have to change both sides or vice versa.

And so instead, we have controllers that we use as a translator to negotiate between the two of them so that you don't need to worry about having your model and your view too tightly intertwined.

So we look at each section of this.

We started with a model.

And we want to figure out if we could share our model code.

And for us that was part of one of the major points of this particular rewrite.

We really wanted our files to be the same everywhere.

And most - and a very important part of that is having our model code that talks to our files also be the same everywhere.

And since our model code isn't talking to the view code - we have our controllers in between - we should be able to share our model code.

I'll talk a bit more later about the actual content of the model.

But for now we're going to say that the model code, we're going to share.

We'll mean shared in this context.

Next, we looked at the view code.

Can we share our view code?

We want to share our view code.

We really want our applications to have the same content for our users', you know files, when we open and display them onscreen.

We want those to look the same everywhere, because our users carefully crafted their documents to look exactly how they want.

But, there's some problems with sharing view code.

First of all, there's actual interaction per platform.

On OS X, the way that users interact with your applications are with a mouse and a keyboard.

They have hot keys.

You know, they're using the cursor, which is a very precise single pointer.

Whereas on iOS, you have Multi-Touch.

And you have 10 very wonderful but less-precise fingers.

The human interface guideline says on iOS you need your buttons to be at least 44 by 44 points just to make sure that your users are able to tap them.

And that's, you know, less precise than your OS X interactions.

And so that's, you know, one thing we're going to have to take into account with our view code.

Another thing we need to consider is the fact that the actual display size between the different devices is going to be different.

The kinds of content we can show, the space we have for controls, is going to vary based on platform.

It's even more extreme on iPhone versus Mac.

And so we want to take advantage of all of the space we have available to us when it is available and handle the fact that sometimes that space won't be available on smaller devices.

And we're going to have to craft our code for each platform a little bit in that view space.

Also, on iOS your user is looking directly at your content, your application and nothing else.

That's how iOS devices work.

And on OS X you can have multiple windows, multiple applications.

They're doing multiple things at the same time.

And you really need to take that into account that there's going to be other things happening on your OS X platforms.

And you need to build your view accordingly.

You don't get to use all of the room all of the time.

On a more technical note, you literally can't compile view code cross-platform.

If you take a class that inherits from UIView and you try to compile it on OS X, XCode will look something like this and get kind of mad at you.

And so there's that technical hurdle as well.

Now some of you may be looking at this saying ah, I know how to handle this.

I can fix this problem.

And you might be thinking of something that looks like this.

We call this shimming in iWork.

It's also known as conditional compilation.

And it's where your have your subclass inheriting from either UIView on iOS or NSView on OS X.

And that change has made it compile time.

And so you write the same code.

And it has a different super class on each platform.

Now, you know, this will compile, absolutely.

And it does work.

But there are some problems with shimming.

Now, views have - you know, UIView and NSView - have many similarities.

They serve the same basic purpose in your code.

They're there to handle user interaction events and put content onscreen.

But there are a number of differences between them as well.

They have different APIs, they support different features.

And there's some subtle differences in their common behavior as well.

And so if you build, for example, Drag and Drop support on your OS X application with your view, and you go compile that on iOS, those APIs don't exist.

And your code won't compile.

And so if you shim, you're going to break the build fairly often because of the differences in APIs between platforms.

Also, it's going to be very hard to target your fix to the platform that you're trying to fix.

So if you a particular issue or change you want to make only on OS X, but your code is compiled in both places, you're now going to have trouble getting that code to only fix OS X and not iOS.

And so you could do more conditional compilation, absolutely.

You could have that in your code.

That makes your code really hard to read and also really hard to maintain.

And so over time, more and more problems may arise as you reach the edge cases of your behavior.

Also, if you build a view hierarchy on iOS and it looks great on your iPad or your iPhone, and you shim it to bring it over to OS X, and you get everything that can compile compiling, and you put on screen, your UI is going to look like it's built for iOS.

And remember how our buttons are supposed to be larger for the fingers to tap them than you would use for your cursor?

If you build the same view code in both places that view code is going to look like it was designed wherever you built it in the first place and will look a little unnatural for the platform where it wasn't intentionally designed.

And you really want to have the best experience possible and embrace the human interface guidelines and UI styles of the platform on which you're running.

So in general, we think that shimming is something, that, it's not - you know, it's helpful in select cases.

It's not definitely the wrong thing to do all of the time.

You know, it has its moments.

But just be aware of some of the problems that may arise if you're using shimming in your own code.

If nothing else, you're pushing all of the complexity of understanding what the platform differences are onto your clients in a way that isn't necessarily obvious from the beginning, rather than keeping it all in a single location where you could handle all of the details yourself and let everybody else not worry about it.

Also a note about Swift.

If you literally translate how you would shim with Objective-C, this will not compile in Swift.

There are ways to shim your code in Swift.

But it gets more complicated than your basic conditional compilation that we can use in Objective-C.

So just a side note about Swift.

So for now we're going to say that our view code needs to be platform specific so that we don't run into these problems.

Next up is the controller code.

Can we share our controllers?

We found that it really depended on the kind of controller that we're dealing with.

Some things that are called controllers, like ViewControllers, UIViewController or an NSViewController, are really tightly coupled with the views themselves.

And like the views themselves, they won't cross compile.

And so controllers that are meant to be used for, you know, platform-specific behaviors, user interaction events, hot keys, mouse handling, Multi-Touch gestures, all of those are very platform-specific controllers.

We probably don't want to share those for the same reasons we don't want to share the views themselves.

However, a lot of our controllers do contain more shared logic - they're talking to the model more directly rather than dealing with the intricacies of the view itself.

And so, we would like to share those controllers if we possibly can, because that's where a lot of the core logic of our applications live.

So we're going to say that you share some controllers, but not all.

Hopefully you're going to share more controllers than not.

It really depends on how your code is set up.

To give you an example of what this might look like within your controller code, in iWork, we had a file.

It was called Canvas View Controller.

It inherited from UIViewController and it had a lot of stuff in it.

It did a lot of different things for us.

And we realized that, you know, the split of the code in that file was kind of similar ratio here where some of the code was absolutely platform specific.

It was part of its job as a UIViewController.

It was controlling the view as specified.

But a lot of the stuff we had in there was more shareable logic.

It was about, you know, managing states and handling some rendering stuff and, you know, scrolling and zooming.

Some of the things that we can handle in a more shared way.

And so we really wanted to keep that code and use it on both platforms.

So to solve this problem we split it in half.

And now we have the iOS Canvas View Controller that's really in charge of actual view controller behavior rather than view controller behavior plus some other stuff.

And then we have a shared Canvas Controller whose job it is is to actually control our shared Canvas Object, which I'll talk more about later.

And then we created a counterpart on OS X, the OS X Canvas View Controller, to handle the platform-specific view controller needs on that side as well.

And here we have a very clear separation of concerns.

So we know exactly what each object's job is.

And you can put the code in the right place based on what its real purpose is and how it should be used.

A real-life example of this, you know, with actual pictures and so forth, is rotation.

And in iWork there are a few different ways you can rotate objects.

And it also really depends on the platform you're using.

On iOS you put two fingers down and you rotate them relative to each other, and the object will rotate.

On OS X you hold down the Command Hot Key, you click on the handle and you drag it, and the object will rotate.

And those are really different interaction paradigms.

It's really - you know, both of them are very much using the platform-specific interactions that you can't do on the other platform.

But the underlying model changes are, in fact, exactly the same.

It's just an angle that we're applying to an object and all the math that goes along with it.

So we wanted to share as much of that code as possible because there are some, you know, exciting math in there, while keeping the platform-specific logic separate.

So we set our code up like this.

We have a shared rotation controller that does all of the, you know, the real math and state management.

And it talks to our model object when appropriate.

And we have a platform-specific gesture recognizer on iOS that handles the Multi-Touch and translates that into an angle that our rotation controller can understand.

And we have a platform-specific mouse and keyboard handler on OS X that handles both our hot key and clicking interactions to tell us that, you know, to again translate that into an angle for our rotation controller.

So that's already pretty helpful.

We have shared code here and we have platform-specific logic.

But there's actually more ways for us to rotate in our iWork applications.

On OS X we have inspectors.

And you can actually just type an angle directly into a text field, hit Enter and it will rotate the object.

And so, we also have a handler that calls into the exact same rotation code that you would call if you were doing it with your mouse, or if you were doing it with your fingers on iOS.

And everything is exactly the same except the thing that translates the user interaction into an angle for our rotation controller.

Also new in OS X Yosemite we have NSGestureRecognizer APIs.

And they handle a lot like iOS UIGestureRecognizer.

And we could also theoretically plug the same thing into our rotation logic and have yet a third way for our user to interact and rotate our objects.

So if you look at just the bottom half of this diagram, you know, sharing code between platforms aside, this is a really powerful abstraction for us.

We have three completely different ways to rotate an object, but most of our logic is still shared.

And we could also plug this in for things like testing.

If we wanted to just test our rotation logic, we could add a fourth path, add just a tester that says: I would like you to rotate it by 10 degrees.

And all the rest of the code is the same.

So we're actually testing our logic and not just our interface.

We can also use this sort of abstraction for Quick Look or plug-ins.

There's a lot of different ways you can apply this abstraction in your own code.

So now we have shared model code.

Not shared U code and some controller shared and some not.

And we wanted to share more code than that.

So we looked at the - at the frameworks supplied by Apple.

And we noticed that most of them are the same one both platforms.

They have the same APIs, they have the same behaviors.

And so we could use them in both places.

If you write some core data code, it's going to run the same on both sides.

There are some frameworks that, however, that aren't the same on both sides.

If you're using QTKit, that won't compile on iOS.

It's just not supported.

We recommend you try out AVFoundation.

And an important thing to note is that AppKit and UIKit, while they serve the same purpose in your code, are not the same framework.

They have many similar APIs.

You can use them in many similar ways.

But you should treat them as separate objects in your own architecture and handle them accordingly.

We looked at this list and we thought that we could use Core Animation and Core Graphics to have more shared rendering code that none.

Right now we have non-rendering code shared.

And we'd really like to have some commented code there.

Core Animation is useful because it has these things called CA layers.

CA Layers are used to put content on the screen and animate them.

And they behave exactly the same on both platforms.

And that's really helpful for us.

Core Graphics is good for more advanced graphics operations.

You can build content with Core Graphics that you can't build with Core Animation alone.

You can take that content and put it into Core Animation Layer to get it on screen and animate it.

There are some caveats.

Core Graphics contexts in UIViews and NSViews are flipped relative to each other in the y-axis.

The origin for UIViews is on the upper left.

And for NSViews is on the lower left.

And, you know, that makes our math more complicated.

But, you can fix this by overriding isFlipped on NSView.

And that makes our origins in the same location in both places.

And this is really helpful for us not to shim our views, but instead to have shared rendering code.

It doesn't need to worry about that extra flip while we're calculating where things should appear on the screen.

And so we use Core Animation and Core Graphics to build our layer tree, a layer tree for our - the iWork Canvas.

The iWork Canvas is this part here.

It's the actual user content that they interact with onscreen.

And the code that renders that canvas is exactly the same on both platforms.

We use Core Animation and Core Graphics to have the same rendering code in both places so we can make sure that the user's content will look the same.

Now, because Core Animation and Core Graphics are platform specific, or are not platform specific, rather, we're losing the platform-specific support that we could get using Views.

So, you know, on iOS, if you have your content and, you know, you have a chart that's in its own view, you can put a gesture on that view directly to turn it - to interact with just that chart.

However, if that chart is in just a layer, you can't directly target a gesture to a layer, so you have to add an extra layer of support to handle the platform-specific aspects of that rendering system.

So there are some downsides to this.

But we found it was very helpful for us in iWork.

So now we have a little bit of shared view code.

But that view code was very carefully crafted to have a stronger user experience and a more unified rendering situation.

But there's one last step that we need before we can actually get that shared rendering logic to compile.

We render a lot of images.

And you have NSImage on an OS X and UIImage on iOS.

And they're both really fantastic and really helpful objects that do some really powerful things for us.

They help, you know, memory management and caching.

They get information out of your bundle.

And we don't really want to reproduce that logic ourselves.

But we also don't want our shared rendering code to have to care which one it's talking to.

For us, all we really wanted from our images was the ability to get a CG image out from the image or to render that image directly into a Core Graphics context.

And because those behaviors are pretty simple, we built what we call an Image Wrapper.

And Image Wrapper is a way for the calling code to not care about the platform-specific issues or the implementation details.

You define a simple API, and you implement it twice, one for each subclass that's private internal for a class cluster.

So we have a myImage super class.

It's an abstract super class.

And a platform-specific implementation that talks either directly to an NSImage or directly to a UIImage and translates our APIs based on how each of them work.

And this is really helpful because we're not losing the power of NSImage and UIImage.

All of their great features we still get to use.

But our shared code doesn't have to worry about which one is actually getting used when it's calling the code.

Now note here that we're going to have to translate each of our API calls directly ourselves in our wrapper layer.

And that means that, you know, wrappers are really useful for simpler objects like images.

But it's harder to wrap things like view controllers, which are very complicated and powerful and behave a little, you know, there's enough different behaviors that trying to translate them all yourself would be rather difficult.

So we found that wrappers are most useful for simple objects.

So now we have our application compiling and running on both platforms.

We have a shared rendering system, so our content should look the same in both places.

And one of our designers made a deck.

And they put this image in it.

And they opened it on their Mac.

It looks like this.

But then they opened it on their iPad, and the image looked funny.

The colors were off.

And since we really wanted our content to look the same, that was part of the point of having a shared rendering system, we, you know, scratched our heads and debugged for a while.

And we realized that the problem was that the image that we had used had a CMYK color profile.

For those of you unfamiliar with color models, CMYK is cyan, magenta, yellow, black.

It's designed - best used for printers.

Those are the standard printer ink colors.

And so, computers don't, you know, your pixels - you have pixels on your screen rather than ink.

And so a lot of our content on computers is made with the RGB or red, green, blue color model.

And there are other color models out in the world.

And so, you know, there's a lot of different ways you can specify colors in the universe.

But iOS devices do the best job of rendering sRGB content, which is this particular kind of RGB.

And so we found that, you know, any image that wasn't sRGB was probably going to look a little bit different when you open it on an iOS device.

And we didn't want that to happen.

So what we did is we took all of our content that wasn't already sRGB and we converted it to sRGB when the user inserted it into our documents.

And so that was - and that way our content could look the same on both platforms.

So this is, you know, if you want to learn more about color management in general, there's a session you can see online, "Best Practices for Color Management."

There's also a lab right after this talk downstairs at 10:15.

So now we have our images looking the same on both platforms.

So now we can open our documents.

And they look the same, for real this time, on both platforms.

But we built some large documents with lots of images.

We were testing out this behavior.

And we noticed that it took a while to open the documents when they got big enough.

And we figured, you know, OS X devices have a lot of resources available to us.

We should be able to make this a better user experience and be faster.

So what are the different resources we have available to us?

Well, there's, first of all, between iOS and OS X there are different chips that are actually running in your hardware.

And that's going to, you know, those will affect how the hardware behaves.

The performance will be slightly different.

You know, things are going to be faster on one platform or the other.

Also, the memory bin, what's available to you is different based on the device upon which you're running.

The total RAM you have available is also going to vary.

And the exact media that's supported will be different, again, based on the chips.

And so this, you know, it's a combination of all these factors.

We could have slightly different performance profiles depending on both the platform we're running on and also the specific hardware.

And so we wanted to optimize, you know, various parts of our applications per platform.

In the case of opening the document slowly, and we want to get quickly, we made a lazily loaded model.

Different parts of our documents, you know, each slide in Keynote is a self-contained unit.

It doesn't need to reference things on other slides immediately when you open them.

And so if I have a 300-slide deck, and I'm only looking at the first 10 slides on my screen, I don't need to read the 300th slide from disk.

So we're only going to read the parts of the model the user is actually looking at and what they actually need to interact with.

And also we can load each of those in parallel, because again, they aren't referencing each other.

And these things combined will give us a faster experience opening our documents because we're reading less data and we're doing more of it at the same time.

And again, this was an optimization for, you know, multicore.

You know, the more cores we have, the more our parallel reading would help us.

And so this was kind of targeted for OS X specifically.

We also had other optimizations on iOS.

But you know, this is just to give you an example of the kinds of things you're going to want to do while building your applications for each platform.

So now we have, you know, our documents and we have a model that we can load lazily and in parallel.

What's actually in our model?

What's in our documents?

One thing to keep in mind is that your user's not going to update all of your applications at exactly the same time.

They're going to be running different versions on different devices at the same time.

And you're, unfortunately, just - you know, this is a fact of life and you'll have to deal with this.

And so, you know, the most fundamental first step of this is you need to version your data so that you know for a fact what version of your application actually built this document so that if you change how your model is written to disk, you can handle that appropriately.

So that's the first important step.

However, you can do something a little bit more interesting than that.

Just instead of versioning it purely on the application version, you can version it based on the features in the document.

So, if I have a version of Keynote that has some great features and kinds of things we write to disk, and we update our application, and we add a chart feature so that our chart model will be different but everything else is exactly the same, if I have a document in my new Keynote and I send it to somebody who's using the older version of Keynote or I open it on my own device that's running an older version of Keynote, and my document doesn't have a chart in it, that should open.

Ideally, you know, if you're not using a new feature, you should still be able to open your document.

And that way you have - you're more likely to have your users have a positive experience if they can open their documents as much as possible.

One real life example of this that you might be more familiar with are XCode xibs.

You can actually save a xib.

There's two different formats.

And you can choose the format based on the features you actually want to use, and XCode will handle that.

Another thing to keep in mind when you're building your on-disk model and also translating it to your memory in Memory Model, is that, you know, the way that you store your application data in memory, if you rearrange that, that doesn't necessarily mean you're going to have to change your on disk representation.

They don't need to look exactly the same as long as the overall data that you're saving on disk is the same.

And you can translate from one to the other.

And this way, even if you update how you are in fact handling things in your application to make your life easier, try not to change the actual document model unless you absolutely have to.

Finally, if you're working with a document-based application, we recommend that you check out the "Creating Document-Based Apps" session, which is probably online now because it's Friday.

So, just to summarize what we've been talking about.

When you're trying to share code between iOS and OS X, first look at how the code is actually used.

Is it used in a way that should be shared?

Or is it handling something that should be platform specific?

In which case, you don't necessarily want to share it.

Also, really recognize and embrace that the platforms do have differences in the way that users should use them and the way that your UI should look should be different based on the platform upon which you're running.

Also, consider using shared frameworks when possible, which will make your life easier and make it so that a lot of your code doesn't need to be rewritten per platform.

And investigate the design patterns of Model View Controller and Wrappers.

And again, keep in mind that we're not saying that these examples that I'm giving you are absolutely going to apply for you in your applications, and what works for us won't necessarily work for you.

But these are meant to be more like universal concepts that we're giving you examples of, rather than universal solutions.

And now I'm going to hand this over to Chris.

And he's going to talk to you about how to set up your XCode projects.

Thank you.

[ Applause ]

Thanks Elizabeth.

So now that we've learned patterns in technologies that made sharing code easier for us, I'd love to walk you through how we made this transition from an XCode configuration perspective.

So as Elizabeth was saying earlier, we had three iOS apps that we were very proud of.

And we wanted to bring each back to the Mac.

I work as a large code base, so all of the things I'm going to share with you may not impact you now.

And in the event that they do not today, you'll be all the more prepared should your app ever evolve in a direction similar to ours.

And since the techniques I'll be sharing with you transcend into all of our apps, I'm going to focus just on what we did with Keynote, because the same overarching principles apply to both pages and numbers as well.

The first small, yet pivotal step to moving Keynote to the Mac was to create a new Mac target for the app.

So, what's a target?

A target contains instructions for building a single product.

The most common type of target is an application target, which builds apps.

But there's also unit test targets, among many others.

Targets also organize everything that gets passed into XCode Build, which is XCode's build system.

And lastly, targets are a part of projects.

It's pretty easy to add the Mac target to the current iOS projects.

All we need to do is click on the project in the navigator on the left hand side and click on the Plus button on the bottom.

Then, we select the Cocoa Application Template from the OS X category.

We give it a witty name, something profound.

In this case, Keynote for Mac.

And we press Finish.

And voila!

We've accomplished the first fundamental step.

We now have two application targets, one for the Keynote for iOS, and one for the Mac.

So right now, it doesn't do anything too fancy.

We press Build and Run, we'd see something that looks like this.

And this is, of course, a far cry from the Keynote experience.

And the reason for this is because we haven't added any functionality to our Mac target yet.

So let's take a look at what functionality there is available to add.

Luckily for us, we have a fair number of subsystems that we'll want to take advantage of for our Mac application.

One example is the iWork Canvas, which Elizabeth was sharing with us earlier.

Being able to use the same canvas in all of our apps is incredibly important to us because we want all of our documents to look the same across all of the devices that we support.

We also want to make sure that our file format and model is the same for both the iOS and OS X versions of our apps.

So, we would want to pull in our common persistence framework as well.

And we also want to bring over all of our common utilities.

These utilities include mechanisms for logging, convenience methods, common data types and many other classes whose functionalities span across our entire suite.

These elements we've separated into libraries.

So, what's a library?

Simply put, it's a bundled collection of code.

More specifically, they are targets in XCode, but they are not executable.

They just exist as a way for you to share code, primarily across projects.

To that end, we found it didn't make sense to break code into libraries unless it was going to be shared by multiple projects.

Otherwise, we could just add the shared code to both the iOS and Mac targets for our app.

And it's important to note that if you have a single project app, this may be all you need to do.

And that is absolutely fine.

Again, we feel that sharing libraries makes sense as a means to share code across projects and not necessarily inside projects.

And lastly, libraries can be either static or dynamic.

So, what does that mean?

All static libraries are built with a project whenever any other targets reference them.

And, they're included as part of the final app that you build.

Dynamic libraries can be external to the project.

And this makes them favorable for a few reasons.

First, once you build them, you don't need to build them again until you change them.

And second, they can be put in a shared location and not rolled into the final application you build, which makes your application smaller.

A concrete example of a dynamic library is a framework.

Simply put, they're a packaged up and pre-compiled dynamic library.

Frameworks have just been brought to iOS 8.

And if you'd like to learn more about how to make modern frameworks, I encourage you to look at the "Building Modern Frameworks" talk video available on the Apple Developer Website.

So we currently have a framework encapsulating functionality shared across all iWork for iOS apps.

And let's see what happens when we try to extend our shared app framework to work with OS X as well as iOS.

So here's the iWork shared XCode project.

And we're going to add a new framework target just like we did earlier, by pressing the Plus button on the bottom.

We select Cocoa Framework.

We click Next.

Again, we give it a name.

We press Finish.

And again, we have a new target.

The first part of the battle is won.

So let's roll up our sleeves, break out the sledgehammer and create a new common group in XCode.

So let's talk about what sort of things we might want to put in this common group.

So we currently have a class in iOS that represents a document.

And it's platform specific because it extends UIDocument.

UIDocument does not exist in OS X.

So we'll need to introduce a new Mac document that extends something else as a basis for our document functionality.

Luckily for us, NSDocument does everything we need to read, update and save our documents.

That said, we'd like all callers of our model to be as agnostic as the platform as possible.

So we'll want to expose a common interface between both documents.

And this we can express in a shared interface called iWorkDocument.

This interface that can declare common behavior shared across both the iOS and the Mac.

So let's take a look at what iWorkDocument might look like.

Here's the shared iWorkDocument interface.

It declares methods for retrieving, saving and updating objects in a document.

And then, we can create a Mac NSDocument subclass and declare conformance to the shared interface we just created.

And then, we implement the required methods as declared in the interface.

Now, let's dive into the project itself.

Bear with me.

I know this is going to be a little tedious, which is actually part of our point.

I'll show you how to later make this easier to manage, but we want to show you what's happening under the hood.

So let's - so there's one thing we need to do to the iWork shared project before we move on to the Keynote project.

And it's very important that we do so.

Let's click on the iWork App Shared Mac target in the middle sidebar and double click on the Public Headers Folder Path option.

A popover appears.

Let's remove what's in there already and give it a name.

This name is the first part that goes after our import declaration and before the slash in classes that import logic from this framework.

OK. Now we're ready to go back to Keynote and use this new Mac document object.

So I've opened up my document window controller and the header for the Keynote for Mac project.

I add the import declaration.

I press Build.

And it fails.

What happened?

I thought I had just declared the iWork Shared Header Folder path.

Well, we still need to tell the Mac target to look in the place we just said we were going to deposit the headers.

So we navigate to the Header Search Path section of the Mac Build Settings.

We double-click on the item.

A popover appears.

We type $(CONFIGURATION BUILD DIR) and then we hit Enter.

This tells XCode that when we're building Keynote for the Mac, that it should look in the Headers directory where things are built, which in this case is the location of the shared framework.

Then we dismiss the popover.

And now the Keynote Mac target knows how to look for the shared Mac library headers.

So, I start coding my implementation and then I try to build it again.

And I get a bunch of linker errors.

Yikes! So, what happened here is that I didn't link the Mac app target with the library.

Nor did I list the library as a dependency.

So that's why it failed.

Let's fix that.

So I go back to the Keynote for Mac Target in the Keynote project.

I click on the Build Phases tab.

I expand the disclosure triangle next to Target Dependencies.

I click the Plus button.

I select our library, and I press Add.

Note that if you're not seeing the library as an option, you will need to drag in the project owning the library into your containing app project.

Our shared library now shows up as a dependency.

This means that before we build the Keynote for Mac target, XCode will make sure this library is built first.

Next, we need to link the app binary with the framework.

To do this, we click on the disclosure triangle next to the Link Binary With Libraries section and click on the plus.

We select our framework.

We press Add.

And then it shows up.

Then, when we try to build and run, it works.

So now, Keynote for Mac is building on top of the shared Mac framework.

And it's not too hard to imagine extending the same process to other frameworks and libraries used by Keynote as well.

So combined with extending our shared frameworks to be Mac friendly, we also need to create Mac-specific views and view controllers that use these frameworks to produce the Keynote experience on the Mac.

And with a bit of time, elbow grease and hand waving, a wonderful thing happens.

We have a fully functioning Mac app.

So at a high level, one can now imagine our projects looking something like this.

We have XCode projects for pages, numbers and Keynote.

And each project has a Mac and iOS target.

These targets build on top of app-specific libraries, which in turn build on top of the cross-platform Canvas, Persistence and Shared Core utilities frameworks.

So while this is great, we're not quite out of the woods yet.

One big problem is that our build settings are somewhat disorganized.

Remember how we manually set the header search path on the Keynote Mac target and the same thing with the public header search path?

So at present, each target has its own build settings associated with it.

While this works, if ever I wanted to change the build setting somewhere, I would need to change it for every target in the project.

And I would need to trust myself to make that change everywhere consistently.

How we fix this is with XCode Config files.

And they look like this.

They simply contain a title and a value.

Each line controls something specific about your build process.

And you can customize every aspect of your build here.

Some common examples of properties you can set are header search paths, which dictate the directories in the file system that XCode should use for finding public headers.

Compiler warnings or what sorts of potential bugs in the code you'd like XCode to tell you about.

Architecture, which defines the types of CPUs your target can build.

SDKs, which define the platform and version of the SDK of the target you'd like to use.

And deployment targets, which declare the minimum version of OS X and iOS a user needs to have in order to build and run your app.

These are a few of the examples of popular properties, but many other remain.

And, for the full list, you can look at for configuring your XCode configuration files, please refer to the XCode Build Settings Reference available on Apple's Developer Website.

The real advantage of XCode Config files is that you can use inheritance.

In other words, you can create a common XCode Config file for iOS and Mac, and then have that extend a common one.

In a common file, you would place build settings that apply to your whole project.

And in a platform-specific configuration files, you would add directives that are specific to either iOS or the Mac.

Another strength of XCode Config files is that they can be reused across projects.

In other words, you can write an iOS Config file for numbers, pages and Keynote and then have all of your iOS apps use the same iOS Config.

So let's create an XCode Config file for our Mac target and implement it together.

To create an XCode Config file, we select File New from the XCode Main Menu.

And we select the Configuration Settings File and press Next.

We give it a name, in this case Mac.

And for starters, let's associate it with the Keynote Mac target.

We click Finish.

And now we have an XCode Config file for the Mac.

But this Config file does not know about the common configuration it is building on top of yet.

To accomplish this, we simply add #include to this child config followed by the name of the xcconfig file we'd like to extend.

And of course, we later add any Mac-specific settings that we care about.

Once we've done that, we can associate our target with these configurations using the project editor.

We'll still need to set this up for each target in each project.

But after we set this up, we may never need to touch build settings of each target ever again.

So, let's try it for the Keynote project.

We can do this by expanding the Configurations disclosure triangle.

And then for every configuration, setting the appropriate Config file.

And we can verify that these settings of our Config files propagated to the targets when we take a closer look at their build settings.

And here is what's provided by the Mac XCode Config file.

For example, we can see here that we're building against the latest OS X SDK as opposed to the current SDK.

And we can also see that the Config file had an impact on the supported platforms and valid architectures of the Mac target as well.

So now we have a sustainable, highly customizable build process and a great native Keynote experience running on the Mac.

And as such, our work is done here.

So let's take a step back and review what we accomplished together this morning.

We made a target for each platform that we wanted to support.

Then we broke some of the functionality across multiple projects and platforms into reusable frameworks.

We told the app targets how to use these shared frameworks.

And then we streamlined our build configuration across every one of our targets using XCode Config files.

So, while there's no silver bullet, as Elizabeth mentioned earlier, we found the design principles and XCode Project Configuration we shared with you this morning to be incredibly helpful to us.

And we can only hope that you find the ideas we've shared with you as useful to you as they were to us.

Also, we compiled a simple project called Photo Memories, which exercises some of the cross-platform concepts and XCode Configurations mentioned in this talk.

This sample code is available on Apple's Developer Website and explains how to implement some of the patterns and practices that we talked about today in even deeper detail.

If you're interested in learning more about cross-platform applications, there is no shortage of other sessions that take a deep dive into some of the topics we touched on this morning.

And if you're interested, all the session presentation videos and demo materials will be available on Apple's Developer Website.

And finally, if you have any questions about what you heard today, please don't hesitate to contact Jake Behrens or Dave DeLong or consult the developer forums at devforums.apple.com.

And with that, we wish you the best of luck in your cross-platform application adventures, and enjoy the rest of your time in San Francisco.

Thank you very much.

[ Applause ]

Apple, Inc. AAPL
1 Infinite Loop Cupertino CA 95014 US