Video #172: Modularization: Part 2
Episode: Video #172 Date: Dec 20, 2021 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep172-modularization-part-2

Description
We finish modularizing our application by extracting its deep linking logic across feature modules. We will then show the full power of modularization by building a “preview” application that can accomplish much more than an Xcode preview can.
Video
Cloudflare Stream video ID: 00992416a20d15d2abca03c1ae54754d Local file: video_172_modularization-part-2.mp4 *(download with --video 172)*
References
- Discussions
- Meet the microapps architecture
- Increment magazine
- Introducing XCRemoteCache: The iOS Remote Caching Tool that Cut Our Clean Build Times by 70%
- isowords
- 0172-modularization-pt2
- Brandon Williams
- Stephen Celis
- Mastodon
- GitHub
- CC BY-NC-SA 4.0
- source code
- MIT License
Transcript
— 0:05
We have now completed a full modularization of our application. What used to be a single application target with 8 Swift files and many hundreds of lines of code is now 7 Swift modules, each with just one or two files, and each file under 200 lines except for our SwiftUI helpers file and UIKit files.
— 0:24
Previously any change to a file, no matter how small, would trigger a build of the application target, and we would just have to hope that Swift’s incremental compilation algorithm was smart enough to not build more than is necessary. Swift’s incremental compilation is really, really good, but there are still times it gets tripped up and a build will take a lot longer than you expect. Or worse, if you need to merge main into your branch to get up-to-date with what your colleagues are doing, you will most likely trigger a full re-compilation of your entire project because many things have probably changed.
— 0:55
Now, with feature modules, we have a lot more control over what gets built and what doesn’t. If you are deep in focus mode on just the item view, then you can choose to build only the “ItemFeature”. Then you should feel free to merge main into your branch as often as you want because, at worse, you will only trigger a rebuild of the “ItemFeature” module, which is a lot smaller than the full application. This can be a huge boon to productivity.
— 1:20
But, we can take this even further. Right now the “AppFeature” has a pretty significant amount of logic that spans the responsibilities of many feature modules we have just created, and that’s the deep linking functionality. The “AppFeature” is handling deep linking for the entire application, even though the only view the module holds is a tab view, and the only deep linking logic important for that view is to figure out which tab we should switch to. All the other deep linking logic just delegates to navigate(to:) methods that are defined on the child view models.
— 1:52
What if we could fully modularize our deep linking logic? Not only would we move the navigate(to:) methods to each feature’s view model, but we would even move the parsers themselves to the feature modules. That would mean we could even work on parsing and deep linking logic in complete isolation from the rest of the application, which would be pretty incredible.
— 2:17
Let’s give it a shot. Deep link modularity
— 2:20
The AppFeature module has an AppRoute type that describes every single route in the application that we can navigate to from a deep link URL. It’s a deeply nested enum so that we can represent the idea of switching to a particular tab, and then navigating to the next screen, and next screen, and so on.
— 3:02
As far as the AppFeature is concerned, it only cares about which tab we need to switch to. The InventoryRoute is of no interest to the “AppFeature”. Only the “InventoryFeature” cares about it because it wants to interpret the route to see whether it should further navigate the user.
— 3:22
So, let’s move that type to the “InventoryFeature”, let’s put it in a dedicated “Routing.swift” file, and make it public. import Models public enum InventoryRoute { case add(Item, ItemRoute? = nil) case row(Item.ID, RowRoute) public enum RowRoute { case delete case duplicate case edit } } public enum ItemRoute { case colorPicker }
— 3:55
The “AppFeature” module should still build, but we’ve now moved a bit of the deep linking domain to the feature that actually cares about it.
— 4:07
But we can go even further. Two parsers are constructed in the “AppFeature” that are completely concerned with only the inventory domain. There’s no reason for these parsers to live in the “AppFeature.”
— 4:44
In order to move them into the “InventoryFeature” we can cut-and-paste the item and inventoryDeepLinker parsers into the “Routing.swift” file.
— 4:52
And we’ll need to import both the ParsingHelpers library we created that houses the URL parsers, and we need to depend on our Parsing library to get access to its types and operators: import Foundation import Parsing import ParsingHelpers
— 5:00
In order to import them we need to add them as dependencies to the “InventoryFeature” target. .target( name: "InventoryFeature", dependencies: [ "ItemRowFeature", "Models", "ParsingHelpers", "SwiftUIHelpers", .product(name: "CasePaths", package: "swift-case-paths"), .product( name: "IdentifiedCollections", package: "swift-identified-collections" ), .product(name: "Parsing", package: "swift-parsing"), ] )
— 5:30
This file is not building now because we are trying to reference the AppRoute type, which doesn’t exist in this module and shouldn’t. The AppFeature manages that type because it cares about navigating users to different tabs, but the InventoryFeature doesn’t care about that at all.
— 5:52
This gives us an opportunity to simplify. We can make the inventoryDeepLinker a parser of an optional InventoryRoute , where nil represents don’t navigate any deeper than the root inventory list view: let inventoryDeepLinker = PathEnd() .map { InventoryRoute?.none } .orElse( PathComponent("add") .skip(PathEnd()) .take(item) .map { .add($0) } ) .orElse( PathComponent("add") .skip(PathComponent("colorPicker")) .skip(PathEnd()) .take(item) .map { .add($0, .colorPicker) } ) .orElse( PathComponent(UUID.parser()) .skip(PathComponent("edit")) .skip(PathEnd()) .map { id in .row(id, .edit) } ) .orElse( PathComponent(UUID.parser()) .skip(PathComponent("delete")) .skip(PathEnd()) .map { id in .row(id, .delete) } )
— 6:32
This builds, and allowed us to remove quite a bit of nesting from the parser, and so it has now gotten simpler.
— 6:48
In order to use the parser from outside this module we need to make it public: public let inventoryDeepLinker = …
— 6:53
If we try to go back and build the AppFeature we will get a compile error, and that’s just because the inventoryDeepLinker no longer parsers into an AppRoute type, but instead just an optional InventoryRoute : PathComponent("inventory") .take(inventoryDeepLinker)
— 7:16
We need to further .map on this parser to bundle it up into a AppRoute value: PathComponent("inventory") .take(inventoryDeepLinker) .map(AppRoute.inventory)
— 7:23
Now everything is building, and everything should work exactly as it did before, but we’ve now relieved the AppFeature of even more responsibilities, that of constructing parsers for deep linking.
— 7:55
But, alongside the construction of parsers there’s also the responsibility of actually interpreting the route and performing the navigation logic. Currently that is housed in a few .navigate(to:) methods, which are invoked in the .open(url:) method on the AppViewModel .
— 8:22
This logic should also live in the child feature modules, not all the way at the root in the AppFeature. So, let’s cut-and-paste them into the Routing.swift file, and make them public.
— 8:40
And we’ll have to import the ItemRowFeature: import ItemRowFeature
— 8:50
And now the AppFeature is building, and it should work exactly as it did before, but we have completely relieved it of any parsing or deep linking logic that isn’t just simple tab switching. The “ContentView.swift” file is now quite slim compared to how it was before.
— 9:07
But we can take it even further, because now the InventoryFeature has more parsing and routing responsibilities than it really needs. It has the RowRoute type, which is an enum of all the routing destinations for a particularly row, it has parsers for parsing the item id and route from a URL, and it has an extension on ItemRowViewModel for interpreting a RowRoute in order to navigate the user to a destination.
— 9:31
Ideally all of these things would live in the ItemRowFeature, which is the code that actually cares about this kind of routing. Let’s create a new “Routing.swift” file in the ItemRowFeature module.
— 9:47
And let’s cut-and-paste the RowRoute type over, and let’s also rename it to something more specific: public enum ItemRowRoute { case delete case duplicate case edit }
— 9:57
And then we can cut-and-paste the parsers that deal specifically with the row routes over to the new Routing.swift file, as well as the ItemRowViewModel extension that implements the navigate(to:) method and move it over the the new Routing.swift file. extension ItemRowViewModel { public func navigate(to route: ItemRowRoute) { switch route { case .delete: self.route = .deleteAlert case .duplicate: self.route = .duplicate(.init(item: self.item)) case .edit: self.route = .edit(.init(item: self.item)) } } }
— 10:35
What we want this module to expose is a deep linker specific to item rows. We can start by extracting the edit linker, but instead of mapping into the row case of InventoryRoute , we just need to map into an ItemRowRoute : public let itemRowDeepLinker = PathComponent(UUID.parser()) .skip(PathComponent("edit")) .skip(PathEnd()) .map { id in ItemRowRoute.edit }
— 11:17
To get access to these helpers we need to import our parsing helpers and the parsing library: import Parsing import ParsingHelpers
— 11:23
And we need to make the ItemRowFeature module depend on them: .target( name: "ItemRowFeature", dependencies: [ "ItemFeature", "Models", "ParsingHelpers", "SwiftUIHelpers", .product(name: "CasePaths", package: "swift-case-paths"), .product(name: "Parsing", package: "swift-parsing") ] )
— 11:46
The deep linker now compiles, but there is something strange about it. It starts with a
UUID 12:02
The id is something only the InventoryFeature cares about, because it must manage routing to a full list of items.
UUID 12:29
So we’re parsing too much here, and should simplify, by starting with what comes after the UUID: public let itemRowDeepLinker = PathComponent("edit") .skip(PathEnd()) .map { ItemRowRoute.edit }
UUID 12:47
We now just need to recognize the other routes: public let itemRowDeepLinker = PathComponent("edit") .skip(PathEnd()) .map { ItemRowRoute.edit } .orElse( PathComponent("delete") .skip(PathEnd()) .map { .delete } ) .orElse( PathComponent("duplicate") .skip(PathEnd()) .map { .duplicate } )
UUID 13:31
The ItemRowFeature is now compiling, but we need to do a few things to get the InventoryFeature compiling. First we need to update the name of the RowRoute type to ItemRowRoute : public enum InventoryRoute { case add(Item, ItemRoute? = nil) case row(Item.ID, ItemRowRoute) }
UUID 13:55
And then we need to add a new .orElse parser to our inventoryDeepLinker to make use of the new itemRowDeepLinker . The interesting thing here is that we can perform the upfront UUID parsing to grab that path component from the URL, and then we can layer on all the functionality of the row deep linker, before finally mapping on the output to wrap it up in an InventoryRoute : .orElse( PathComponent(UUID.parser()) .take(itemRowDeepLinker) .map(InventoryRoute.row) )
UUID 15:15
And now everything builds, even all the way back to the root app target, and everything should work exactly as it did before.
UUID 15:40
But we have now moved all parsing and deep linking logic closer to the feature that actually cares about the particular routes being used. We could even go further by extracting some routes and parsing logic into the ItemFeature, but we will leave that as an exercise for the viewer. Preview apps
UUID 16:00
So this is pretty incredible. We get to work on deep link parsing and navigation logic in complete isolation if we wanted. If we wanted to add a new destination to the item row domain, like say a sharing interface, we could do all of that work without ever building the inventory feature, or any of the other tab features in the application.
UUID 16:16
We would do the work in the ItemRowViewModel to implement the logic and functionality of the share interaction. We would add a new route to ItemRowRoute to represent that we can deep link into that destination from an external URL. We would beef up the itemRowDeepLinker to do parse an expected URL into the new ItemRowRoute , and then update the navigate(to:) method to create the state necessary in the view model to active the share interface.
UUID 16:40
We could do all of that work without building the vast majority of the application, without worrying about merging main into our branch for fear of messing up incremental compilation, and we could even write tests for the functionality and even run the row view in a dedicated SwiftUI preview to make sure it all works.
UUID 17:01
But, can you believe it gets even better?
UUID 17:03
The most amazing part of extracting out feature modules is that you have a single module you can build that represents all of the functionality necessary to run that one feature in isolation. This is of course no surprise because we can run the feature’s SwiftUI preview right in Xcode.
UUID 17:19
However, what if we wanted to run the feature in a simulator, or even on a real iOS device? SwiftUI previews are amazing, but they do have their limitations:
UUID 17:29
First of all, Xcode previews don’t have all the features that the simulator has. For example, in the simulator can you enable slow animations to clearly see how things transition in your application. You can also simulate application lifecycle events such as backgrounding and foregrounding the app. You can also simulate hardware features, such as touch pressure, volume controls, keyboards and more. None of those things are possible in Xcode previews.
UUID 17:53
Further, previews tend to work best in short spurts of development time since editing code and navigating through files can invalidate your previews. However the simulator can stay open for a long time since it’s a whole separate process, and so is great for testing parts of your application that take a long time to experience.
UUID 18:09
Also, some technologies don’t work in previews, such as CoreMotion, CoreLocation, StoreKit and more.
UUID 18:16
Xcode previews also don’t work great with the debugger. It’s supposed to be possible to attach a debugger, but we haven’t been able to get it to work in a long time. So, if you need to put a breakpoint in your code to debug just a small part of the feature you are working on you have no choice but to run the application in the simulator.
UUID 18:35
And if all of that wasn’t enough, sometimes you just want to run your application on an actual device, and although Xcode previews are supposed to be runnable on devices that functionality hasn’t worked for us for a very long time. Hopefully it will be fixed sometime soon, but until then running small, focused features of your application on a device just isn’t possible from a preview.
UUID 18:57
All of this motivates us to create something we call “preview apps”, which are similar to Xcode previews in that they allow us to run a particular view in isolation, but they are entire app targets unto themselves. This allows us to accomplish all the things we just mentioned aren’t possible in SwiftUI previews, but with many benefits over running the root application.
UUID 19:18
Let’s give it a shot.
UUID 19:22
We’ll try it out on the ItemRowFeature. We’ll hop over to the SwiftUINavigation project settings and add a new iOS application called ItemRowPreviewApp.
UUID 19:38
We can make this new app target depend on the ItemRowFeature module.
UUID 19:47
We can delete the “ContentView.swift” file from the target because we won’t be actually creating in new views in this target. We only need the entry point of the application to display the ItemRowView .
UUID 19:53
And we can update the entry point of the ItemRowPreviewApp to wrap and ItemRowView in a List and a NavigationView : import ItemRowFeature import SwiftUI @main struct ItemRowPreviewAppApp: App { var body: some Scene { WindowGroup { NavigationView { List { ItemRowView( viewModel: .init( item: .init(name: "Keyboard", status: .inStock(quantity: 1)) ) ) } } } } }
UUID 20:28
And with just those few steps we now have an entire preview app dedicated to demonstrating the behavior of the item row view. Everything works exactly as it did in the Xcode preview, just now running in a simulator, and we could run on a device if we wanted.
UUID 20:56
This means that if our feature used more advanced APIs, such as location managers, motion managers, and who knows what else, we would not be limited by what previews can do. We would have the full powers of simulators and our devices to play with this feature.
UUID 21:10
In fact, we can demonstrate an example of this right now. Recall that the way we actually listen for deep link events is to use the .onOpenURL view modifier and then pass the URL on to the view model so that it can do the work of parsing and figuring out where to navigate the user: .onOpenURL { url in self.viewModel.open(url: url) }
UUID 21:26
It is not possible to invoke this API from a preview. If we want to test or debug this functionality we have no choice but to load up the entire application and visit a URL in Safari.
UUID 21:37
Well, that was true until we added our preview app. We can quickly give our preview app a custom URL scheme.
UUID 21:48
And then we can listen for URL opens from the root entry point of the preview application, parse the URL, and pass the route to the view model: import ParsingHelpers import ItemRowFeature import SwiftUI @main struct ItemRowPreviewAppApp: App { let viewModel = ItemRowViewModel( item: .init(name: "Keyboard", status: .inStock(quantity: 1)) ) var body: some Scene { WindowGroup { NavigationView { List { ItemRowView(viewModel: viewModel) } } .onOpenURL { var request = DeepLinkRequest(url: $0) if let route = itemRowDeepLinker.parse(&request) { self.viewModel.navigate(to: route) } } } } }
UUID 22:50
Now when we run the preview application we can further jump over to Safari, enter a URL such as itemRow:///edit , and we will instantly be deep linked into our preview application with the edit navigation drill down already activated. Or if we visit itemRow:///delete we will open the application with the delete alert presented.
UUID 23:41
And that’s pretty amazing. We can test full deep linking capabilities in this preview app without even building the main application.
UUID 23:48
Note that we only need to provide the fragment of the URL that this feature module actually cares about. We don’t need to worry about also parsing the “inventory” path component and then the UUID path component because only the parent domains care about that. That makes it even easier to test deep linking. We don”t have to worry about the full request necessary to navigate us to a particular screen, and vice-versa the parent domain doesn’t need to worry about all the ways in which we parse the URL to figure out our local route. They have been fully decoupled.
UUID 24:15
There’s another interesting aspect to what we have done here. Notice that we added new code only to the preview app target without touching any code in the feature module. This means this new code will never see the light of day when it comes to building the main app target, whether that be for local dev, a TestFlight build, or the production App Store. The preview app is a full sandbox to play in.
UUID 24:34
This means we could make small tweaks in just this little preview app, and that code would never be compiled in an application that actually ships to the App Store. We could stub in a mock API client to return the data we want to test, or a user session to pretend that we are logged in, or stub a mock analytics client so that we don’t accidentally track live analytics while testing the application, or any number of things.
UUID 24:56
We could of course put that kind of test code directly in the root application target also, just to test things out, but then we’d have to remember to remove that code before shipping our app to the App Store, or we’d have to put systems into place to make sure we don’t accidentally ship debug code.
UUID 25:10
But a sandboxed, preview app avoids all of those worries. We can be free to sprinkle in as much testing code as we want and we will know it will never make its way into the production app.
UUID 25:21
The benefits really start to amplify as you work on larger teams. If you have many colleagues working on the same code base, then the team can be split into feature teams that primarily iterate on just their feature modules. A feature preview app even allows them to test things like deep linking in total isolation.
UUID 25:39
Further, feature modules and preview apps also make it much easier to onboard new colleagues to the code base. It can be a lot less intimidating to familiarize yourself with a feature when it is entirely bundled up into its own isolated module and when you can run it in a little preview app without having to bootstrap the entire application. Horizontal vs. vertical modularity
UUID 25:59
We could keep going and build out an “InventoryPreviewApp” that allows us to run the full inventory feature in a dedicated, sandboxed application, but we’ll leave that as an exercise for the viewer.
UUID 26:09
Instead we will end the episode by reflecting a little bit on what we have accomplished here. At the beginning of the episode we talked about how we like to think of modularity in two ways: there’s the model/helper/dependency layer and the feature layer.
UUID 26:25
The modules that make up the model layer, the helper layer, and the dependency interfaces and live implementations are usually the easiest to get started with since the code in those kinds of modules tends to not depend on feature specific code. For example, it would be very strange if our models needed to know something about the ItemView , or if our parser helpers needed something from the inventory feature.
UUID 26:47
Once these tools are extracted out into their own modules we can think of them as forming a big slab of the foundation of our application: /*----------------------------------------------------------------------------------| | | | MODEL/HELPER/DEPENDENCY MODULES | | |---------------|----------------|----------------|-----------|---------------| | | | Models | SwiftUIHelpers | ParsingHelpers | ApiClient | ApiClientLive | | | |---------------|----------------|----------------|-----------|---------------| | | | |----------------------------------------------------------------------------------*/
UUID 27:41
Nothing in this layer of modules should ever depend on things in our feature modules, but modules in this layer can depend on other modules in the layer. For example, the ApiClient module might need to depend on the Models module in order to do its job, and the ApiClientLive module must depend on the ApiClient module.
UUID 27:59
Next, with this foundation set, we can start to layer on feature modules. We can represent them as tall slices of modules sitting on top of the foundation.
UUID 28:09
For example, the InventoryFeature depends on the ItemRowFeature which depends on the ItemFeature, which we represent as a big tower on top of the foundation: /*------| | I | | t | | e | | m | |-------| | I | | t | | e | | m | | R | | o | | w | |-------| | I | | n | | v | | e | | n | | t | ... | o | | r | | y | |-------| |--------------------------------------------------------------------------------| | | | MODEL/HELPER/DEPENDENCY MODULES | | |---------------|----------------|----------------|-----------|---------------| | | Models | SwiftUIHelpers | ParsingHelpers | ApiClient | ApiClientLive | | |---------------|----------------|----------------|-----------|---------------| | | |-------------------------------------------------------------------------------*/
UUID 28:23
Then as we build out more features they will be represented by additional slices. For example, suppose we had a search feature which was one of the tabs in root view: /*------| | I | | t | | e | | m | |-------| | I | | t | | e | | m | | R | | o | | w | |-------| | I | | n | | v |-------| | e | S | | n | e | | t | a | ... | o | r | | r | c | | y | h | |-------|-------| |--------------------------------------------------------------------------------| | | | MODEL/HELPER/DEPENDENCY MODULES | | |---------------|----------------|----------------|-----------|---------------| | | Models | SwiftUIHelpers | ParsingHelpers | ApiClient | ApiClientLive | | |---------------|----------------|----------------|-----------|---------------| | | |-------------------------------------------------------------------------------*/
UUID 28:41
And then later we add a user profile feature: /*------| | I | | t | | e | | m | |-------| | I | | t | | e | | m | | R | | o | |-------| | w | | U | |-------| | s | | I | | e | | n | | r | | v |-------| P | | e | S | r | | n | e | o | | t | a | f | ... | o | r | i | | r | c | l | | y | h | e | |-------|-------|-------| |--------------------------------------------------------------------------------| | | | MODEL/HELPER/DEPENDENCY MODULES | | |---------------|----------------|----------------|-----------|---------------| | | Models | SwiftUIHelpers | ParsingHelpers | ApiClient | ApiClientLive | | |---------------|----------------|----------------|-----------|---------------| | | |-------------------------------------------------------------------------------*/
UUID 28:53
And then later a settings feature: /*------| | I | | t | | e | | m | |-------| | I | | t | | e | | m | | R | | o | |-------| | w | | U | |-------| | s | | I | | e |-------| | n | | r | S | | v |-------| P | e | | e | S | r | t | | n | e | o | t | | t | a | f | i | ... | o | r | i | n | | r | c | l | g | | y | h | e | s | |-------|-------|-------|-------| |--------------------------------------------------------------------------------| | | | MODEL/HELPER/DEPENDENCY MODULES | | |---------------|----------------|----------------|-----------|---------------| | | Models | SwiftUIHelpers | ParsingHelpers | ApiClient | ApiClientLive | | |---------------|----------------|----------------|-----------|---------------| | | |-------------------------------------------------------------------------------*/
UUID 28:59
Each of these feature modules are free to depend on the modules in the foundation, although ideally they should only depend on the interfaces of hefty modules and not the live implementations.
UUID 29:16
Further, we should try to minimize how much feature modules depend on other feature modules. Currently we have a very tall slice of feature modules due to how inventory depends on item row and item row depends on item. The shorter these slices are, meaning the fewer dependencies between features, the more Swift can parallelize building them, the more we can build and run them without building other parts of the application, and the better chance we have at reusing them in other places of the application.
UUID 29:48
Theoretically it’s possible to further modularize this application so that it’s possible to build the ItemRowFeature without building the ItemFeature, or even build the InventoryFeature without building either of the ItemRowFeature or ItemFeature. Then our dependency slices would look like this: /*------| | U | |-------| | s | | I | | e |-------| |-------| n | | r | S | | I | v |-------| P | e | | t | e | S | r | t | |-------| e | n | e | o | t | | I | m | t | a | f | i | ... | t | R | o | r | i | n | | e | o | r | c | l | g | | m | w | y | h | e | s | |-------|-------|-------|-------|-------|------*/ /*----------------------------------------------------------------------------------| | | | MODEL/HELPER/DEPENDENCY MODULES | | |---------------|----------------|----------------|-----------|---------------| | | | Models | SwiftUIHelpers | ParsingHelpers | ApiClient | ApiClientLive | | | |---------------|----------------|----------------|-----------|---------------| | | | |----------------------------------------------------------------------------------*/
UUID 30:16
Which means Swift means we can build even fewer things when focusing on a feature, and when building the full application Swift can be smarter about parallelizing.
UUID 30:23
However, the topic of how to break apart feature modules so that they do not depend on each other is a big one, and something we plan on covering in the future. It’s very complicated. How can one design the InventoryFeature so that it doesn’t know about what type of domain and behavior you are sticking in for each row of the inventory list? Further, how can you design the ItemRowFeature so that it doesn’t know about the item domain that you can navigate to via popovers and links?
UUID 30:53
If you could accomplish this then you would have even further decoupled your application, allowing you to build the inventory feature without even building the row feature or item feature.
UUID 31:04
But, that also comes with additional complexities. If you can build the inventory feature without the row or item feature, what does that mean the inventory preview app? What is even at that point? The inventory feature has become so decoupled from its content, so completely generic, that it becomes harder to work on it.
UUID 31:25
There are things you can do to mitigate this, but we must also warn to not go overboard with trying to break apart all dependencies between feature modules. There is a happy medium to be had where particularly hefty dependencies between feature modules can be broken, while still allowing some feature modules to depend other feature modules when it makes our day-to-day work simpler. Conclusion
UUID 31:47
These are all topics we will be getting into in the future, but for now this ends our series of episodes on modularizing the navigation app we build many weeks ago. We hope that you have learned some new tricks on how to maintain a modularize code base with a modern Xcode structure using SPM.
UUID 32:09
Until next time! References Meet the microapps architecture Gio Lodi • Aug 1, 2021 An article from Increment magazine about modularizing a code base into small feature applications: Note How an emerging architecture pattern inspired by microservices can invigorate feature development and amplify developer velocity. https://increment.com/mobile/microapps-architecture/ Introducing XCRemoteCache: The iOS Remote Caching Tool that Cut Our Clean Build Times by 70% Bartosz Polaczyk • Nov 16, 2021 Once you modularize your code base you can begin uncovering new ways to speed up build times. This tool from Spotify allows you to cache and share build artifacts so that you can minimize the number of times you must build your project from scratch: Note At Spotify, we constantly work on creating the best developer experience possible for our iOS engineers. Improving build times is one of the most common requests for infrastructure teams and, as such, we constantly seek to improve our infrastructure toolchain. We are excited to be open sourcing XCRemoteCache, the library we created to mitigate long local builds. https://engineering.atspotify.com/2021/11/16/introducing-xcremotecache-the-ios-remote-caching-tool-that-cut-our-clean-build-times-by-70/ Collection: isowords Brandon Williams & Stephen Celis • Apr 19, 2021 We previously discussed modularity and modern Xcode projects in our tour of isowords . Note isowords is our new word game for iOS, built in SwiftUI and the Composable Architecture. We open sourced the entire code base (including the server, which is also written in Swift!), and in this multi-part tour we show how we’ve applied many concepts from Point-Free episodes to build a large, complex application. https://www.pointfree.co/collections/tours/isowords Downloads Sample code 0172-modularization-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 .