Evolution of the Medium iOS app architecture

A follow-up to my previous article

Last June, we started to transition from a monolithic architecture to a package-based one.

You can read the previous article above to get familiar with what we were trying to do and where we wanted to go. The goal of this new article is mainly to give an update on the status of our iOS journey and project, and how this new architecture has been going on.

Also, I feel like writing this before the WWDC 2022 is the right thing to do!

First, it’s been a huge success! Our codebase is definitely more organised, we can iterate, build & debug faster, all thanks to both Swift package and SwiftUI!

Before beginning, I want to state that this is still in progress, at the moment, only a small part of the app is properly decoupled into independent packages. We’re actively cleaning up and rewriting huge parts of the app, but this is not a purely engineering effort. We’re also using this as an opportunity to rebuild features, incorporating a lot of products & design improvements, alongside bug fixes.

Also, this is a team effort involving my teammates Zouhair Mahieddine and Alla Dubovska as well as yours truly, but also past iOS team members who helped build the foundations of our new architecture.

Swift Packages

We split our codebase into two proper Swift packages, Model and Features. And then Model and Features define several different library products. Those are not just folders / groups.

Here an extract of the Model package.swift

And here is the same for Features

You can find some helpful Apple documentation about it here.

The Model package

Model is the underlying layer that any Features can import. While features library won’t be available in Model, Model is available for any features.

Model is probably not the right name, but it’s the closest one we found that describe what we’re nesting inside.

Modern Medium API is built on GraphQL, so Model have an ApolloModels product, this is where all the code generated by the Apollo client from our schema.graphql live. It’s available to all the app.

Our old API is sharing models with clients using protobuf, recently we migrated from Objective-C proto codegen (with custom template), to plain Swift protobuf. So like for GraphQL we’ve been able to move all of that to a Swift Package. We mostly use this to share our analytics / events code between platform these days.

We also have a very light networking layer over the Apollo GraphQL client, but also over our legacy REST API. The goal for us was to be able to use modern Swift concurrency feature, so we have a wrapper around the Apollo iOS client that expose all we need using async / await.

Here is our legacy REST API client, we wrap some very specific queries that still live over our REST API and have not been migrated over GraphQL as of yet.

And our modern GraphQL one, which is generic and can take any queries, mutation or watcher.

Notice how watching a queries can be done using AsyncThrowingStream, I could write a whole article about this. Wait.. I actually did, as it’s really easy to create retain cycle if you’re not careful.

And to go even further, all these dependencies are available using something we called env. At the app level, or from any package, by simply importing Model you can use the various properties available on env, and so making a graphql query is as simple as that.

Env

Env is a really nice piece of our Model package, its goal is to replace the way we were doing dependencies injection with something simpler. You don’t need to pass down dependencies with env anymore, you just access it from anywhere, and it’ll always be available. You can still swap the implementation of each env easily, for different targets (live, RC, dev env) and in tests.

We even have a navigator available at the package level.

Navigator

Navigator is also a very niece piece of our model package, and talking about it will allow me to dig into one of the pains to move features to Swift packages when you’re coming from a monolithic architecture.

In the Medium iOS project, we have a centralised navigation at the app level, the problem when making packages, is that you don’t have access to app level features. For a long time we were using closures to forward, for every feature built into packages, navigation decisions to the app level. This is working up to a point, for large features with many navigation options, this was not cutting it anymore.

Enter Navigator, it’s an abstraction layer over our navigation at the package level. It offer a simple go function and a Destination enum.

At the package level, this is it, and then at the app level we implement the go function and forward every case of Destination to our app level navigator.

Features are also able to do local navigation, especially with SwiftUI, it’s totally fine to show feature-specific screen using local navigation like the .sheet modifier and NavigationLink. Navigator is purely to navigate to other app level or package level features.

Other Model product library

While I can’t get over all the stuff we have in the Model package, I can mention a few of them, like Localizable, which hosts all our localised strings, and some other stuff like some shared data controllers for users & stories we share between features.

The Features package

The Features package name is more appropriate than Model, as you’ll see, even if not long time ago it was simply named UI, this is not true anymore, we do have fully self contained features (including GQL queries and protocol extensions over GQL type) in there. So Features matches what we’re doing better than UI nowadays.

I’ll won’t detail every features in this section, but talk about one we did recently, the new activity feed.

This is an UI-heavy isolated feature and was the perfect candidate to truly test our new architecture.

At the package level it looks like that

One key point we decided is that the GraphQL part needs to live close to the features. Because the way GraphQL works, your queries, mutations and fragment should be tailored to exactly what you need for the feature you’re working on. So while the auto generated code lives in Model/ApolloModels package, the .gql file, protocol and protocol extension are defined in the feature.

And this is generally how we work with GraphQL. We define a protocol which describes what we need for the feature:

And then extend the auto generated GQL type (all the auto generated code by Apollo is prefixed with GQL) with our protocol.

At the view level, it allows us to work with clean, immutable protocols.

As you can see in this view model, we only need an Activity to make it work.

Our SwiftUI architecture is a very basic, vanilla MVVM.

We try to keep every view as simple as possible, especially the body psart, it needs to be split into different functions, views or computed properties on the same view. The view model will hold everything else.

Here is an extract of the view model associated with this view so you can get an idea.

You can even see how we use env and navigator Model products I’ve talked about in the previous section.

For the service part, which can be used from within this feature but also externally, we define it like so:

This is a pattern we use a lot, and allows us to have an easy fake / mock implementation alongside a live one for all the functions of this service.

The live one being implemented like the above, you can have very clean call sites at the app level.

This is literally the function that is being called when you tap the bell icon in the top right corner of the app from the home screen. This code is at the app level and initialises a feature that is at the package level.

And I think that’s about it for this specific feature.

The Features package also have a lot of other stuff, from other app features, alongside our SharedUI code and our design system. The Design System implements all the colors, text styles, etc… we have from design, and SharedUI implements various components we can share between features using this design system.

SwiftUI

I want to touch base a bit into into our usage of SwiftUI usage at Medium, because it’s been growing ever since we started to use it. Since Medium for iOS supports only iOS 14 & 15, this is a no brainer for us when working in a UI -heavy features. Swift previews + Swift package allow us to iterate quickly, and we can work with product and design feedback loop much faster. Even swapping the whole implementation is a matter of seconds in SwiftUI, where in UIKit, imagine transitioning from a UITableView to a UICollectionView.

The issue with SwiftUI remains that its behaviour between iOS 14 & 15 is quite different for the same components. Every part of the UI needs to be heavily tested on iOS 14, and sometimes we have to make some tradeoff because some features of SwiftUI are not working, not available or buggy on iOS 14.

Overall we have a good portion of the UI in SwiftUI, the whole List features is in SwiftUI, alongside the activity feed, responses too. And recently, part of the UI of the post page started using it too.

Conclusion

I want to emphasis some key points, because while this looks quite straightforward while writing this article, after more than a year of work on our app, this did not happen all at once

  • This was a very iterative process, I can’t stress this enough but we didn’t just wrote code to use it later. Many patterns emerged because we needed them.
  • I would not recommend anyone to go the other way, to draft a whole new architecture, write a ton of unused code around it, and only later start implementing features into it.
  • This is far from done and we’re ok with this. We try to write all our new pieces into this architecture, but a lot of app-level component are still not available at the package level, and there is always a tradeoff of how much the engineering effort would impair the product innovation.

And we’re currently hiring iOS engineers across the US and France, don’t hesitate to write to me or anyone at Medium!

Thanks for reading! ?

首页 - Wiki
Copyright © 2011-2024 iteam. Current version is 2.137.1. UTC+08:00, 2024-11-23 01:03
浙ICP备14020137号-1 $访客地图$