EP 111 · Designing Dependencies · Aug 3, 2020 ·Members

Video #111: Designing Dependencies: Modularization

smart_display

Loading stream…

Video #111: Designing Dependencies: Modularization

Episode: Video #111 Date: Aug 3, 2020 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep111-designing-dependencies-modularization

Episode thumbnail

Description

Let’s scrap the protocols for designing our dependencies and just use plain data types. Not only will we gain lots of new benefits that were previously impossible with protocols, but we’ll also be able to modularize our application to improve compile times.

Video

Cloudflare Stream video ID: 89c232ee8663e740e709cf9b663a9913 Local file: video_111_designing-dependencies-modularization.mp4 *(download with --video 111)*

References

Transcript

0:05

And so this one single conformance seems to have replaced the previous 3 conformances, and so that seems like a win. But again, we are back to the situation where we only have two conformances for this protocol: a live one and a mock one. And the mock implementation requires quite a bit of boilerplate to make it versatile.

0:29

Turns out we have essentially just recreated a technique that we have discussed a number of times on Point-Free, first in our episodes on dependency injection ( made simple , made comfortable ) and then later in our episodes on protocol witnesses . For the times that a protocol is not sufficiently abstracting away some functionality, which is most evident in those cases where we only have 1 or 2 conformances, it can be advantageous to scrap the protocols and just use a simple, concrete data type. That is basically what this MockWeatherClient type is now.

1:05

So, let’s just take this all the way. Let’s comment out the protocol: // protocol WeatherClientProtocol { // func weather() -> AnyPublisher<WeatherResponse, Error> // func searchLocations(coordinate: CLLocationCoordinate2D) // -> AnyPublisher<[Location], Error> // }

1:19

This will break some things we need to fix. First we have the live weather client, the one that actually makes the API requests. We are going to fix this in a moment so let’s skip it for now.

1:33

Next we have the MockWeatherClient . Instead of thinking of this type as our “mock” we are now going to think of it as our interface to a weather client’s functionality. One will construct instances of this type to represent a weather client, rather than create types that conform to a protocol.

1:47

So, we are going to get rid of the protocol conformance, and we can even get rid of the methods and underscores: struct WeatherClient { var weather: () -> AnyPublisher<WeatherResponse, Error> var searchLocations: (CLLocationCoordinate2D) -> AnyPublisher<[Location], Error> }

2:13

And now, instead of creating conformances of the WeatherClient protocol, we will be creating instances of the WeatherClient struct.

2:28

We can first create the “live” version of this dependency, which is the one that actually makes the API requests. We don’t current need the searchLocations endpoint so I’ll just use a fatalError in there for now: extension WeatherClient { static let live = Self( weather: { URLSession.shared .dataTaskPublisher( for: URL( string: "https://www.metaweather.com/api/location/2459115" )! ) .map { data, _ in data } .decode(type: WeatherResponse.self, decoder: weatherJsonDecoder) .receive(on: DispatchQueue.main) .eraseToAnyPublisher() }, searchLocations: { coordinate in fatalError() } ) }

3:11

We can recreate the other 3 conformances of the protocol by simply creating instances of this type. A natural place to house these values is as statics inside the WeatherClient type: extension WeatherClient { static let empty = Self( weather: { Just(WeatherResponse(consolidatedWeather: [])) .setFailureType(to: Error.self) .eraseToAnyPublisher() }, searchLocations: { _ in Just([]) .setFailureType(to: Error.self) .eraseToAnyPublisher() } ) static let happyPath = Self( weather: { Just( WeatherResponse( consolidatedWeather: [ .init( applicableDate: Date(), id: 1, maxTemp: 30, minTemp: 10, theTemp: 20 ), .init( applicableDate: Date().addingTimeInterval(86400), id: 2, maxTemp: -10, minTemp: -30, theTemp: -20 ) ] ) ) .setFailureType(to: Error.self) .eraseToAnyPublisher() }, searchLocations: { _ in Just([]) .setFailureType(to: Error.self) .eraseToAnyPublisher() } ) static let failed = Self( weather: { Fail(error: NSError(domain: "", code: 1)) .eraseToAnyPublisher() }, searchLocations: { _ in Just([]) .setFailureType(to: Error.self) .eraseToAnyPublisher() } ) }

4:33

Only a few more errors to fix. Next is in the view model’s initializer, where we are explicitly requiring that whoever creates the view model must pass in a conformance to the WeatherClientProtocol . Well, now we can require just a WeatherClient and we can default it to the live one: init( isConnected: Bool = true, weatherClient: WeatherClient = .live ) {

5:02

And finally in our SwiftUI preview, instead of constructing a MockWeatherClient from scratch we can just use the live one: ContentView( viewModel: AppViewModel( weatherClient: .live ) )

5:27

Or we could use any of the other ones too: // weatherClient: .live // weatherClient: .happyPath weatherClient: .failed

5:43

We can even do fun stuff like start with the happy path client, and then change one of the endpoints to be a failure: var client = WeatherClient.happyPath client.searchLocations = { _ in Fail(error: NSError(domain: "", code: 1)) .eraseToAnyPublisher() } return client

6:25

This kind of transformation is completely new to this style of designing this dependency, and was not easily done with the protocol style.

6:50

We could even just open up a scope to make a custom client based off the live client right inline: weatherClient: { var client = WeatherClient.live client.searchLocations = { _ in Fail(error: NSError(domain: "", code: 1)) .eraseToAnyPublisher() } return client }()

7:25

It’s a little noisy, but it’s also incredibly powerful. It is super easy to create all new weather clients from existing ones, and that just isn’t possible with protocols and their conformances. And we have seen this many times in the past, such as when we developed a snapshot testing library in this style and could create all new snapshot strategies from existing ones, allowing us to build up a complex library of strategies with very little work. Modularizing a dependency

8:03

Something we like to do when creating dependencies like this is to separate some of the parts into files dedicated to one aspect.

8:25

For example, we can create a new file called WeatherClientInterface.swift that holds just the interface struct for our client, as well as the models it needs.

9:27

This is the file where we can put all the documentation for how to use the client, and where users of the client should go to see what all is available to them: /// A client for accessing weather data for locations. struct WeatherClient { … }

9:46

Then we like to put the live implementation of the client in its own file, called WeatherClientLive.swift .

10:19

Note that we also had to bring along the weatherJsonDecoder , and it can remain private because only the live implementation needs it. This is removing more clutter from the main view Swift file, which is nice.

10:31

And then finally we like to put the mocks in their own file, called WeatherClientMocks.swift .

10:51

And now our ContentView.swift file is back to being nice and succinct. It just has a view model, a view, a little helper for date formatting, and the SwiftUI preview. Extracting dependencies to packages

11:03

This separation is what we would consider to be the bare minimum we should do to organize our dependencies. But we can take it even further, if you have an appetite for it, and this will allow us to even better carve out a space for our dependencies to be truly separate. We can create a whole new Swift package that houses just the weather client code, which will make it very explicit where and how our application depends on the client, and will enforce that the weather client code never incorrectly accesses things from the main application target since it will live in a completely different module.

11:36

To do this we can: Go to File > New > Swift Package… Select the directory of our app, type in a name for the package, and have it added to our application Go to build settings and add the WeatherClient module as a dependency to the app target.

11:55

Now we can delete the stub file that Xcode created, and then drag and drop all of the weather client files we created into the WeatherClient package. We need to make a few small changes, so let’s try to build just the WeatherClient package. We will get a compile error: ‘AnyPublisher’ is only available in iOS 13.0 or newer

12:09

This means we just need to properly specify what platforms we plan on supporting in this package. We can do that in Package.swift : let package = Package( name: "WeatherClient", platforms: [.iOS(.v13)], … )

12:32

And now the package builds, but when we add and import it to our application, we’ll see that we need to mark a bunch of things public.

13:39

Everything builds and should work exactly the same as before. But something really great has happened.

13:44

We now have compile time proof that our application is depending on this weather client and that the weather client cannot access our application code. It is very explicit, and something we can see for ourselves by just navigating over to the build settings. In contrast, before we did this separation, we would have no idea what all our view depends on unless we actually read the code to see that it was using a WeatherClient under the hood.

14:09

And there are two main reasons we care about this.

14:12

The better we understand our dependencies the better chance we have at sharing our code and using it more places in the future than we had planned in the present. For example, sometime in the future we may want to work on extensions for this app, such as a share extension or a widget, and that requires creating all new app targets that are distinct from this app target. If we are capable of separating out our dependencies into separate modules, we increase the chance that we can reuse code written for one application in the other, which can allow us to adopt these new fancy iOS features faster and with less of a complexity cost.

14:48

And as if that weren’t cool enough, if we split out dependencies from our main application we can greatly improve compile times. If the dependency modules don’t pick up too many dependencies themselves, then their builds can be parallelized, which will allow you to build and iterate on your main app target more quickly.

15:05

So, there are a lot of benefits to splitting dependencies out into their own modules, but we can take it even further. Interface vs. implementation

15:11

Right now the WeatherClient module contains both the interface of how one interacts with a client, as well as the actual live implementation that calls out into the real world.

15:27

In practice, the interface will compile super fast, pretty much instantly. After all it’s just a few model structs and another struct that holds a bunch of function fields. On the other hand, the implementation not only takes longer to compile, as it contains some actual Swift code that needs to do real work, but it can also just take a long time to compile, period. For example, if you like to use Alamofire for your API clients instead of URLSession , then you will need to build it in this module. Or perhaps your dependency wraps a socket library like Starscream, then this module would need to build Starscream, which would slow down building your main app target. We’ve even worked with companies that have very large dependencies that are shared across many teams, and even cross-platform C++ libraries, that take minutes or tens of minutes to compile.

16:37

So, although it’s nice that the dependencies can be built in parallel, if the dependency itself takes a long time to build then you are still going to slow down your development cycle. What we can do is further separate the interface of the client from the live implementation of the client. Now this may be one too many modules for some of our viewers, especially if you are working on a smaller project, but there are many projects out there that will benefit from this, and it’s useful to know this technique so that in the future you can use it when necessary.

17:26

But, just to show that it really is a problem that can happen in the real world, let’s simulate what it would look like for this dependency to take a long time to compile. I’m going to paste the following into the live implementation file: public let __tmp0 = 2 * 2 * 2 * 2.0 / 2 + 2 public let __tmp1 = 2 * 2 * 2 * 2.0 / 2 + 2 public let __tmp2 = 2 * 2 * 2 * 2.0 / 2 + 2 public let __tmp3 = 2 * 2 * 2 * 2.0 / 2 + 2 public let __tmp4 = 2 * 2 * 2 * 2.0 / 2 + 2 public let __tmp5 = 2 * 2 * 2 * 2.0 / 2 + 2 public let __tmp6 = 2 * 2 * 2 * 2.0 / 2 + 2 public let __tmp7 = 2 * 2 * 2 * 2.0 / 2 + 2 public let __tmp8 = 2 * 2 * 2 * 2.0 / 2 + 2 public let __tmp9 = 2 * 2 * 2 * 2.0 / 2 + 2

17:50

Compiling these expressions takes a while due to type inference, operator overloads, and expressible by literal protocols. When we try to build our app it now hangs for a very long time. And that’s not great.

18:19

This may seem contrived, but this is a real problem that manifests itself in real code bases, and we’ve personally seen it happen with many teams. Heavy dependencies start to slow down compiling the app, but we can fix this by separating the lightweight stuff, such as the interface and models, from the heavyweight stuff, such as the live implementation.

18:40

So we are going to create yet another library, and this time it will house just the live implementation of the WeatherClient . Luckily we don’t need to create a whole new package because a single SPM package can hold multiple libraries. We just need to hop over to the Package.swift file and make the following changes: // swift-tools-version:5.2 import PackageDescription let package = Package( name: "WeatherClient", platforms: [.iOS(.v13)], products: [ .library( name: "WeatherClient", targets: ["WeatherClient"]), .library( name: "WeatherClientLive", targets: ["WeatherClientLive"]), ], dependencies: [ ], targets: [ .target(name: "WeatherClient", dependencies: []), .testTarget( name: "WeatherClientTests", dependencies: ["WeatherClient"] ), .target(name: "WeatherClientLive", dependencies: ["WeatherClient"]), ] )

19:21

Next we create a WeatherClientLive directory in the Sources directory, and we can drag and drop the live file into that directory, being sure to add import WeatherClient to the top of the file.

19:44

The WeatherClientLive module isn’t yet building because we need to make WeatherClient ‘s initializer public. This requires creating it from scratch, which Xcode’s refactoring tools can mostly do for us, but it’s still a bit of a bummer that we have to do this manually when we modularize: public init( weather: @escaping () -> AnyPublisher<WeatherResponse, Error>, searchLocations: @escaping (CLLocationCoordinate2D) -> AnyPublisher< [Location], Error > ) { self.weather = weather self.searchLocations = searchLocations }

20:22

The WeatherClient module still builds super quickly, and the WeatherClientLive module seems to be in building order, but it’s still taking a very long time to build.

20:38

We have now separated the lightweight interface, models, and mocks, from the heavyweight live implementation.

21:34

Now, over in the app target, things aren’t building because we have a reference to the live implementation of the weather client. Note that the preview is building just fine because its weather client implementation is mocked out. However we are referencing the live client in the view model’s initializer to provide a default, but we can get rid of that and instead require the caller to pass it in explicitly: init( isConnected: Bool = true, weatherClient: WeatherClient ) { That’s probably better anyway.

22:30

And then finally over in the scene delegate we a compiler error because we need to provide a weather client when constructing the view model: let contentView = ContentView(viewModel: AppViewModel()) Missing argument for parameter ‘weatherClient’ in call

22:36

This is the only place we actually want a live weather client. The other file only needs the interface of a client because it just wants to execute the endpoint for fetching weather. Whereas this file is where the app is kicked off and so we’d like to supply the live implementation so that the main screen is working with a client that can actually make API requests. So, let’s add WeatherClientLive as a dependency to the app and then we can import the module and use it: import WeatherClientLive … ContentView(viewModel: AppViewModel(weatherClient: .live))

23:21

Now the application seems to be building, although it takes about an entire minute to do so. Isolating features from live dependencies

23:55

But we haven’t really accomplished anything new yet. The app currently imports both WeatherClient and WeatherClientLive modules, even though the majority of the code only needs access to the interface. It would be ideal if we only had to build the live dependency when building App.swift for the application. And when building just the feature view we could maybe get by with only compiling the interface, which is nearly instantaneous.

24:42

In order to do that we need to create yet another module, this time to just hold the functionality for the core weather feature that is separate from the app’s entry point. However, due to a bug in Xcode we cannot create this module via the Swift Package Manager, because it seems that SwiftUI previews don’t seem to work in packages with dependencies. Hopefully that will be fixed someday, but luckily we can work around by creating a standard framework target in our Xcode project.

25:57

And now we will move the ContentView.swift file to this target. We can build this target by itself, and amazingly it builds pretty much immediately. And this is because it is only depending on the interface of the weather client, not the actual implementation. Even better, we hope that we can run the SwiftUI preview immediately, which would mean our development cycle will not be slowed down one bit by bringing in this dependency. But unfortunately, due to a bug in Xcode 11, the preview will build the app target even when it’s located in a framework. This bug has been fixed in the Xcode 12 beta, though, so let’s hop over and see.

27:58

But we’ve now broken our app target. Swift package target ‘WeatherClient’ is linked as a static library by ‘WeatherApp’ and ‘WeatherFeature’. This will result in duplication of library code.

28:16

The problem is that WeatherClient and WeatherClientLive are static and WeatherClientLive includes WeatherClient inside of it, so we can’t link to both from the same app target. To work around this problem we can explicitly tell SPM to produce these libraries as dynamic frameworks, instead: products: [ .library( name: "WeatherClient", type: .dynamic, targets: ["WeatherClient"]), .library( name: "WeatherClientLive", type: .dynamic, targets: ["WeatherClientLive"]), ],

28:40

And we must also make sure that the app depends on just the framework and not the static library to avoid that duplication error we just saw.

28:59

Alright, we can finally import WeatherFeature and use it in our app entry point: import WeatherFeature

29:11

We just need to make a few things in the WeatherFeature module public: public class AppViewModel: ObservableObject { … public init( isConnected: Bool = true, weatherClient: WeatherClient ) { … } … } public struct ContentView: View { … public var body: some View { … } }

29:21

And add a public initializer to the view. public struct ContentView: View { … public init(viewModel: AppViewModel) { self.viewModel = viewModel } }

30:53

And now everything builds, albeit a little slowly because our live implementation is purposely taking a long time to build. The WeatherFeature framework builds instantly, and this means we can rapidly iterate on it, even on a clean build.

31:07

The SPM linker gotchas were definitely a pain, but we were luckily able to work through them. In the future Xcode will hopefully make everything a bit more streamlined here and take care of these kinds of linker issues automatically.

31:39

But let’s stop to appreciate what we’ve done here because this is really powerful. We are getting a ton of benefits all at once just by properly designing this dependency.

31:48

First we are able to load up our screen in many different states and edge cases, such as when the API returns no data, or some data, or a failure, and even when the request just takes a long time to finish.

32:01

And the mere fact that we can control the dependency in this way means this feature will be easy to test. We haven’t done that yet, but we will do that soon.

32:09

And last, by pushing ourselves to properly control and separate the dependency from our application code we opened ourselves up to some nice compile time optimizations. We can now build features in isolation without building their dependencies. The only time we need to actually build a live dependency, whether it be Alamofire, Starscream, or some huge shared C++ library, is when building the full app that will actually run in the simulator or on your device.

32:53

So, there is a lot of power in extracting a dependency from application code and separating its interface from its implementation. I think a lot of people would assume that the only way to do this is to use protocols, but that’s just not true. You can do it just as easily using plain, concrete data types, and there’s even a few perks to doing it in this style. Next time: a long-living dependency

33:28

Although this is very cool, let’s not stop here. Let’s add a new feature to the app that makes use of an even more complicated dependency in order to see how it can be designed to be as powerful as the weather client.

33:41

We are going to implement the functionality that makes our screen listen for internet connectivity status, so that when the device loses its internet connection we will clear the weather results and show a small error prompt at the bottom of the screen. And then, when we regain internet access we will fetch the freshest weather results from the network. And we’ll do that…next time! References Protocol-Oriented Programming in Swift Apple • Jun 16, 2015 Apple’s eponymous WWDC talk on protocol-oriented programming: At the heart of Swift’s design are two incredibly powerful ideas protocol-oriented programming and first class value semantics. Each of these concepts benefit predictability, performance, and productivity, but together they can change the way we think about programming. Find out how you can apply these ideas to improve the code you write. https://developer.apple.com/videos/play/wwdc2015/408/ Collection: Protocol Witnesses Brandon Williams & Stephen Celis Note Protocols are great! We love them, you probably love them, and Apple certainly loves them! However, they aren’t without their drawbacks. There are many times that using protocols can become cumbersome, such as when using associated types, and there are some things that are just impossible to do using protocols. We will explore some alternatives to protocols that allow us to solve some of these problems, and open up whole new worlds of composability that were previously impossible to see. https://www.pointfree.co/collections/protocol-witnesses Downloads Sample code 0111-designing-dependencies-pt2 Point-Free A hub for advanced Swift programming. Brought to you by Brandon Williams and Stephen Celis . Content Become a member The Point-Free Way Beta previews Gifts Videos Collections Free clips Blog More About Us Community Slack Mastodon Twitter BlueSky GitHub Contact Us Privacy Policy © 2026 Point-Free, Inc. All rights are reserved for the videos and transcripts on this site. All other content is licensed under CC BY-NC-SA 4.0 , and the underlying source code to run this site is licensed under the MIT License .