Service oriented architecture is common for server-side development — and it’s great because it breaks up the functionality into logical independent components. But complex iOS apps also get large over time and become hard to manage. Inconsistencies with what responsibilities are shared by what objects result in an app with many different design patterns.
This talk by Mustafa Furniturewala, a Coursera software engineer working on the Swift version of their app, will show you how you can use dynamic frameworks, Swift and new iOS8 SDK features, to build modular iOS apps with common design guidelines.
Background & Motivation (0:00)
Coursera’s mission is to provide universal access to the world’s best education. One of the main reasons they decided to build a new architecture for their mobile app was the large number of feature teams working at Coursera. So many teams adding features, each with their own design patterns and coding styles, can cause a lot of trouble. To solve this, a modular architecture would allow teams to build with a certain set of design principles laid out prior.
Maintainance of the app was another main reason for the app re-design. After building a core foundation layer, adding new features would be easy. For example, doing persistence would just be a call to the interface. The feature module developer can then concentrate on just developing the feature itself.
This kind of modular architecture implies that testing things individually can be done very well. Part of it is also moving most of the code of the UI and into different pieces, so that those pieces are testable individually. The UI still needs to be tested, but those integration tests can then be really simple.
Why Swift? (3:45)
Programming with shared mutable state is really, really bad. Your whole app can break if the shared mutable state goes awry. Swift has really good immutability constructs, so it makes that really clear in the code. Since structs are value types, it really helps to build these immutable modules in your app that can go across different features.
Type safety is another feature that was a big reason for us in choosing Swift. The compiler can do much more in helping to make sure your code is type safe, since Swift is type safe. In addition, the bridge between Swift and Objective-C was made really simple by Apple. We knew we would have situations where we would have to use Objective-C, and having this bridge has really helped us keep moving forward.
Why iOS8? (5:48)
We didn’t want our new architecture to be polluted with so many checks for the different views available to iPhones and iPads, or landscape and portrait views. Size classes make code really clean, so we can specify different views for different size classes without all those checks. In fact, storyboards help with this. You tend not to have so many module conflicts, because each module is really small and has its own storyboard.
View Controllers (6:38)
There’s been a lot of view controller advancements in iOS8 that we ended up using. We also use APIs like showViewController, which is really useful since it’s able to inject itself onto a navigation controller stack. When you’re trying to add UI elements to an existing navigation stack, you need those kinds of APIs. Having these kinds of advancements in iOS8 really helped us in building the architecture we wanted.
Presentation Controllers (7:24)
Presentation controllers were useful as well because you can animate alongside the existing animations really easily, using things like transition delegates. Doing custom presentations was important to us as well, since views are coming and going from different features all the time and we wanted to be able to do it individually. For example, we used presentation controllers to animate our video player while it was going in landscape and portrait.
Dynamic Frameworks (8:13)
We leveraged dynamic frameworks quite a bit, since that was iOS8 only. Our project setup is constantly evolving, but we’re currently using a really simple setup. In our project are sub-projects that are dependencies, and each of these build dynamic frameworks. We had to change a couple of build settings so that these frameworks would work not just on the simulator but on the device as well.
Existing Objective-C (9:41)
We still use Objective-C when required. Objective-C is kind of like a first-class language, and frameworks make it really easy to bridge between it and Swift. Since we’re using frameworks, all you have to really do is set the @Objective-C annotation on the class and then you can use it. You can also use the class modifier on protocols when you need to specify that the protocol has to conform to a reference and not a value type.
The Architecture (10:17)
We have lots of features, with the Coursera data and foundation as our core components that all our features can use if needed. Each of those features are also built as dynamic frameworks. We have this concept of a shell app as the skeleton containing all the features, and it’s able to find the features and load them dynamically when needed. Basically, it just listens to URLs and loads the right feature when needed.
The Skeleton (11:17)
The internals of this shell app have multiple features that may or may not use foundation or data, depending on if they need to do networking. There might be features that need to do persistence, and so the features depend on either the Coursera foundation or data.
VIPER is something we looked into; the architecture in each of these features is very much VIPER-like. We have a modified version that we’re using. WHen the feature gets called, it creates a flow controller, which manages all the navigation pieces of the app. Anything you doing with presenting, pushing, view controllers - all that logic is in this flow controller. A presenter’s job is to format in the view data, which is from the interacter where the business logic exists. The interacter than can go into the data module if it needs to get data from the persistence layer or from the network.
We’re using MVVM methodology. We wanted to be really careful about the line between the view and the view module. Since we’re using KVO, we don’t differentiate between the view and the view controller, and that’s what MVVM does as well. The view is basically the view controller, and we just observe KVO properties on the view module to change things in the view.
Coursera data is like an interface for us. We want different teams to be able to add different data modules that they need, dynamically into the app, and so we’re looking at CoreData alternatives. Defining relationships between different data module files can get tricky.
Feature Interactions (15:17)
Obviously, features need to communicate with each other. For example, a feature like the course homepage might need to launch the video feature, and these features need to somehow communicate with each other. How do we do that? The feature course URL, in this case would be like “/video” and the video ID, would tell the shell to launch this feature. The shell then dynamically loads it through Coursera foundation and thus launches the feature. We leverage URLs a lot, since they are the only piece connecting the features.
KVO + Swift (16:18)
How does KVO and Swift work out for us? You have to sub-class from object because you need the observe for key value and the methods required for KVO. Dynamic dispatch in Swift is not enabled by default, so you have to add the dynamic modifier to enable it. This was done for performance reasons in Swift, but for KVO, you need to enable this. Adding the dynamic modifier guarantees that the properties and the metas will be dynamically dispatched. When you observe the fields, you get something called an AnyObject because they’re no typed arrays in Objective-C. You can use the ? operator to get what you need. We also use property observers: we listen to the view module when it’s set using a property observer, then we add and remove observers when we set the score. That way, we’re keeping the KVO registration methods really clean.
Introspection in Swift (18:05)
We need to do quite a bit of introspection in Swift becauase of the way we’re dynamically loading features. We load features based on a protocol, so we need to convert to that protocol. You can’t cast down to a non Objective-C protocol, so you have to again add the @Objective-C annotation. In that way, we’re able to load the feature and then handle the URL on that.
There’s also something called a Metatype type in Swift, or the type of a type. This is where you can see some of the Swift dynamic things that are available. The dynamic type will actually get you the type of the object at runtime.
It’s interesting to learn from other programming languages and borrow patterns from them in Swift. In Haskell, there’s something called Either, in which you get one value or the other. In Objective-C, the general way to do this is to use completion blocks for errors. You can do this in Swift by making the error optional. Even better, a response enum can have two cases of a result or an error. Code becomes very readable, and based on the two cases, you can do different things.
So what does the future hold for this architecture for us? We replaced things in our app incrementally, so we want to replace all the features in our app with this new architecture. We want to have flexible data modules, so that we can enable developers to add different modules that all work together seamlessly. We’re still trying to figure out the best way to do that, but we want to use more functional programming concepts. We’re using KVO, but we want to look into reactive Swift for the line between the view and the view module. We also want to enhance our core foundation layer, which has networking and JSON parsing, because all the features are dependent on that as teh core piece of our infrastructure.
Lessons Learned (24:17)
We learned a few lessons from our experiments with Swift and iOS8. It’s important to build incrementally. We could have rewritten our entire app, but what we did instead was write the new features with Swift and iOS8 and then plug them into the existing app. We were also able to test how modular our features were. Before we began, we had a blueprint of the architecture that has changed quite a bit since. It keeps getting better and better over time as we implement different things and find loopholes in the architecture.
Q: Do you use playgrounds a lot for your Swift development?
Mustafa: Yes we do, to try things out, but sometimes Playground seems to be a bit finicky and you have to restart it. It’ll just stop responding to what you’re typing and you have to restart XCode, but we do use it to experiment.
Q: How do you build and handle features that span across multiple view controllers?
Mustafa: The way we do it is using APIs like showViewController because that allows us to hook into the existing navigation stack. It works pretty seamlessly in that when you’re using the app, you don’t really know that you’re launching a different view controller.
Q: Based on the fact that there’s so many parsing libraries out there, what led to the decision to writing your own JSON parser backend?
Mustafa: We actually haven’t written our own. We have a simple JSON parser right now. We’ve looked into libraries like SwiftyJSON, but we want to keep our parsing really simple and just use existing frameworks.
Q: How much trouble have you had from Swift syntax and its features evolving?
Mustafa: We had the most trouble with XCode 6.1, since a lot of things changed when that happened. It was a bit of a pain, but not too much trouble. The compiler helped us fix a lot of things, so it was probably a couple hours of work to get that fixed.
Q: You mentioned using KVO and property observers from Swift. Could you elaborate more on how you’re using them together?
Mustafa: Property observers and KVO are different things. Property observers, you still need to know the properties. With KVO, you don’t need to know about those properties. The way we use them together is a view module that is what we do the property observing on. We don’t actually do property observing on the properties themselves.
Q: Did you run into any issues with XCode and Swift?
Mustafa: Yes. Code completion breaks very often, so we’ve become really good Swift programmers because we’re really working without code completion. Anothing thing is that the unit testing is not as good as we’d like it to be. Because of the access modifiers, you have to put all your source files into the unit test target. XCode crashes a lot too, so that doesn’t help, but we’ve been able to work through most problems.