How we build SwiftUI features in the Medium application

A tale about transitioning from a monolithic architecture to a packages based one.

Thomas Ricouard
Medium Engineering

--

SwiftUI illustration from Apple (https://developer.apple.com/documentation/swiftui/)

Glose was acquired by Medium last January, and since then, the Glose team has been busy integrating with all our new colleagues and building shiny new features.

Glose is still here. Our Paris (and Ukraine and UK) based team is now part of the same group working toward a unified goal on a yet to be revealed set of features.

We’re also hiring, so you can apply below if you want to join this epic journey from the new Medium Paris office.

And on my side with Alla Dubovska we’ve been onboarded successfully in the iOS team working on the Medium application. I would like to thanks Alaina Kafkes for the amazing onboarding experience!

Even if I can’t yet talk about the feature, I can speak about its technicalities. The iOS codebase is a mix of legacy and modern code with a very fluid architecture. We still have Objective-C code powering some foundations and UI. Even if now all the code is written in Swift and most of the front facing features of the app have been remade from the ground up.

As Apple is about to release iOS 15 and most of our user base is on iOS 14 and iOS 13, we felt like it was the right time to build our new feature using SwiftUI for iOS 13+ only.

The idea is that SwiftUI will allow us to build the UI faster and iterate with design to be much easier. And so far, it’s all been confirmed! And while we’re here, we also totally embraced Combine. And here I have to thanks Logan Moseley for his ongoing and weekly workshop on Combine!

As you might know, I’m the biggest SwiftUI fan, and I’ve been doing many open source projects since the day it was released at the 2019 WWDC.

But to be transparent, I’ve yet to use it in a production app used by millions of users.

And I want to note that the first engineers to introduce SwiftUI into our codebase were Mike McGhie, Michael Barrett and Joseph Behar! This was the stepping stone in what is our modern architecture. Some of your might already be running the SwiftUI version of the replies / responses views.

The architecture

As I’ve said previously, the iOS project is very fluid, and there is an ongoing effort to have the architecture ready for a future based on SwiftUI, Swift packages, and Combine.

This is exactly what we’ve been doing lately. We moved our GraphQL/Apollo generated models at the app level to an ApolloModel Swift package. And thanks to Mike McGhie previous iteration on various moving pieces in our backend iOS systems, it was quite seamless to put it all together and making it ready for Combine.

In order to do that we first had to move some Cocoapods dependencies to Swift Package. So we did exactly that with Apollo-iOS, our GraphQL client. We’ve moved a few other pods to Swift Packages too. We can’t do that for everything yet because we still have Objective-C code depending on some pods.

This allowed us to create many more self-contained feature packages. We also made SharedUI, where we have many standalone UIKit and SwiftUI components. And then we have various feature packages, such as the one we’re working on for our new feature.

We want to take it further, too. Logan Moseley has been working on architectural documentation. The idea is to create a MediumModel package to move our data and view model to it. The idea is that instead of feature packages, we have pure UI packages. It’ll allow us for an even faster build and SwiftUI preview time because we don’t have to compile the whole Application codebase to preview the UI of the new feature. We simply build the feature package we’re working on to get instant SwiftUI previews. This is a huge productivity boost!

We also want to have a LiveService and FakeService packages, but more on that a bit later.

Here is a small redacted compliant screenshot of our current Xcode source tree:

The service layer

Talking about services, we’ve been trying something new with Combine for iOS 13 only features. At Medium we have a lot of tests, and we take them very seriously. So we need to have good code and responsibilities separations. Our standard Services are registered at app launch, and we can retrieve them and inject them anywhere in the app.

And in the testing environment, we register and inject a mock version of them wherever necessary.

But for our new SwiftUI code, we now write them a bit differently, and we don’t need to register them in our global services registry. We define a prototype that looks like so:

As you can see in the image above, Services are primarily for our GraphQL requests. They take some parameters and return a publisher. They do all the processing to ensure that we get exactly what we need to build our UI components.

We then have two implementations of them depending on if it’s a live version of the service in our LiveService package:

And a fake version of the service in our FakeService for testing purpose:

We can pass various parameters to our fake version to return any necessary result for testing purposes.

This makes our new components have a very clean initializer for both the actual application and the testing environment. Here it is for our app / live environment.

And if you need to test it, you pass the .fake implementation, and you’re done.

We took inspiration from this pointfree.co video about dependencies. It explains the concept of modularisation and separated services really well. Like them we preferred struct over protocols in our case.

We also have some other services that don’t do network processing and look a bit more standard. Those are still registered in our services registry:

The UI layer

The UI for our new feature is mostly done in its own Swift Package. This ensures that we can build this package entirely separately from the app. We have a speedy build time and so very quick SwiftUI previews build time. This is what allows us to iterate so quickly on the UI. We don’t need to launch the app to tests all our tweaks.

All our views also take easy to mock view models or simplified GraphQL/Fragment models. We usually define a fragment like so:

Make a protocol that makes it easy to read:

And then extend the fragment with our protocol and implement anything we need.

So we don’t need to pass the GQL concrete type around, we can just pass anything that conform to out protocol, it doesn’t matter if it’s backed by GraphQL generated type or some object we defined for preview or testing purpose.

And for previews or placeholders, we can create a static version using a struct implementing our protocol.

This doesn’t fit all our use cases, of course. We also have the concept of stores, which is probably not the right name for this kind of object. Those store have published properties and handle the communication between the hosting controller and the root SwiftUI views. We have various published properties which ensure the view is always at the most up-to-date state.

And yes, we do have custom UIHostingController and some root SwiftUI views at the app level. It helps us ensure that we can adequately communicate between the app and the UI package.

When we decided to build all the UI component in the independent Swift Package, we decided to define how the communication between the package and the app would happen. When you tap a button in a view in the package, the app would know about it and change the state or trigger an action.

Well, nothing truly groundbreaking for that part. We went with the concept of action handlers defined as an enum. Our views define them, and we then forward them back to our hosting controller.

So we can initialize our SwiftUI view in our UIHostingController and connect our action handlers like so:

Then can then trigger app-level actions such as navigation etc., from SwiftUI components defined deep down in our package.

We also decided that some navigation could stay within SwiftUI. If this is a component or a screen we don’t need to navigate outside of this particular flow, then we’re very comfortable using .sheet it works very well.

We don’t use NavigationView, and we probably won’t use it in the foreseeable future. We would need a robust programmatic API to make it works for our use cases.

We also do use the new Menu API extensively for Button. Alla Dubovska wrote an ingenious component that can present a menu or a sheet when tapping a button depending on the iOS version using the same interface. Too bad the SwiftUI Menu still don’t support destructive action… maybe for iOS 15

One of the other pain points is with List. We do use it on iOS 13 even if it doesn’t fit out UI/UX completely. We found some reliable ways to remove the separator. And we use LazyVStack instead of List on iOS 14. It doesn’t bring any custom unwanted behaviors but has some performance issues in some cases. This is so magical with SwiftUI because this is just a top level / container view component swap depending on the OS version, and that’s it. We don’t have to rewrite UITableViewCell to some other views, for example.

We have a very simple navigation bar implementation with title fade in depending of the scroll offset. Without me telling you it’s in SwiftUI you would never know:

I can’t say it enough, SwiftUI allows you to make your UI much faster once you get comfortable with it. We had a slow couple of weeks in the beginning when we were adapting all our tools to SwiftUI. We had to bridge the Design System (which is also in its package) and some other essential components to attain maximum velocity.

A few closing notes

I was very skeptical before using SwiftUI in a codebase that heavily relied on Objective-C and it tooks us two features to get it right. But now that we have the right architecture in place, I couldn’t be happier. Before using SwiftUI in the Medium app, I was only using it in pure SwiftUI projects, I’ve shipped my fully SwiftUI open-source app to the App Store, and it’s been a lovely experience. But until now, I’ve always been shy about introducing it in an existing codebase, non SwiftUI codebase.

Don’t be like me, don’t be shy!

SwiftUI is the future and it’s here to stay.

Let me know what you thought of this article, and if you want to know more, don’t hesitate to leave questions and feedback.

Thanks for reading 🚀

--

--

📱 🚀 🇫🇷 [Entrepreneur, iOS/Mac & Web dev] | Now @Medium, @Glose 📖| Past @google 🔍 | Co-founded few companies before, a movies 🎥 app and smart browser one.