-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathreducing-ios-build-times-by-using-interface-targets.html
335 lines (290 loc) · 23.9 KB
/
reducing-ios-build-times-by-using-interface-targets.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
<!DOCTYPE html>
<html lang="en">
<head>
<script src="https://use.fontawesome.com/afd448ce82.js"></script>
<!-- Meta Tag -->
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!-- SEO -->
<meta name="author" content="Bruno Rocha">
<meta name="keywords" content="Software, Engineering, Blog, Posts, iOS, Xcode, Swift, Articles, Tutorials, OBJ-C, Objective-C, Apple">
<meta name="description" content="While dividing an app into several modules can improve the build time of an iOS app, the result heavily depends on what is being changed. If you're not careful with how your dependency graph is laid out, you can often have results that are worse< than non-modularized apps. Let's take a look at a technique used at iFood to have big and consistent improvements to the build times of our app.">
<meta name="title" content="Reducing iOS Build Times by using Interface Modules">
<meta name="url" content="https://swiftrocks.com/reducing-ios-build-times-by-using-interface-targets">
<meta name="image" content="https://swiftrocks.com/images/thumbs/thumb.jpg?4">
<meta name="copyright" content="Bruno Rocha">
<meta name="robots" content="index,follow">
<meta property="og:title" content="Reducing iOS Build Times by using Interface Modules"/>
<meta property="og:image" content="https://swiftrocks.com/images/thumbs/thumb.jpg?4"/>
<meta property="og:description" content="While dividing an app into several modules can improve the build time of an iOS app, the result heavily depends on what is being changed. If you're not careful with how your dependency graph is laid out, you can often have results that are worse< than non-modularized apps. Let's take a look at a technique used at iFood to have big and consistent improvements to the build times of our app."/>
<meta property="og:type" content="website"/>
<meta property="og:url" content="https://swiftrocks.com/reducing-ios-build-times-by-using-interface-targets"/>
<meta name="twitter:card" content="summary_large_image"/>
<meta name="twitter:image" content="https://swiftrocks.com/images/thumbs/thumb.jpg?4"/>
<meta name="twitter:image:alt" content="Page Thumbnail"/>
<meta name="twitter:title" content="Reducing iOS Build Times by using Interface Modules"/>
<meta name="twitter:description" content="While dividing an app into several modules can improve the build time of an iOS app, the result heavily depends on what is being changed. If you're not careful with how your dependency graph is laid out, you can often have results that are worse< than non-modularized apps. Let's take a look at a technique used at iFood to have big and consistent improvements to the build times of our app."/>
<meta name="twitter:site" content="@rockbruno_"/>
<!-- Favicon -->
<link rel="icon" type="image/png" href="images/favicon/iconsmall2.png" sizes="32x32" />
<link rel="apple-touch-icon" href="images/favicon/iconsmall2.png">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Source+Sans+3:ital,wght@0,200..900;1,200..900&display=swap" rel="stylesheet">
<!-- Bootstrap CSS Plugins -->
<link rel="stylesheet" type="text/css" href="css/bootstrap.css">
<!-- Prism CSS Stylesheet -->
<link rel="stylesheet" type="text/css" href="css/prism4.css">
<!-- Main CSS Stylesheet -->
<link rel="stylesheet" type="text/css" href="css/style48.css">
<link rel="stylesheet" type="text/css" href="css/sponsor4.css">
<!-- HTML5 shiv and Respond.js support IE8 or Older for HTML5 elements and media queries -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
<![endif]-->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BlogPosting",
"mainEntityOfPage": {
"@type": "WebPage",
"@id": "https://swiftrocks.com/reducing-ios-build-times-by-using-interface-targets"
},
"image": [
"https://swiftrocks.com/images/thumbs/thumb.jpg"
],
"datePublished": "2020-01-23T06:00:00+00:00",
"dateModified": "2020-06-15T11:00:00+02:00",
"author": {
"@type": "Person",
"name": "Bruno Rocha"
},
"publisher": {
"@type": "Organization",
"name": "SwiftRocks",
"logo": {
"@type": "ImageObject",
"url": "https://swiftrocks.com/images/thumbs/thumb.jpg"
}
},
"headline": "Reducing iOS Build Times by using Interface Modules",
"abstract": "While dividing an app into several modules can improve the build time of an iOS app, the result heavily depends on what is being changed. If you're not careful with how your dependency graph is laid out, you can often have results that are worse< than non-modularized apps. Let's take a look at a technique used at iFood to have big and consistent improvements to the build times of our app."
}
</script>
</head>
<body>
<div id="main">
<!-- Blog Header -->
<!-- Blog Post (Right Sidebar) Start -->
<div class="container">
<div class="col-xs-12">
<div class="page-body">
<div class="row">
<div><a href="https://swiftrocks.com">
<img id="logo" class="logo" alt="SwiftRocks" src="images/bg/logo2light.png">
</a>
<div class="menu-large">
<div class="menu-arrow-right"></div>
<div class="menu-header menu-header-large">
<div class="menu-item">
<a href="blog">blog</a>
</div>
<div class="menu-item">
<a href="about">about</a>
</div>
<div class="menu-item">
<a href="talks">talks</a>
</div>
<div class="menu-item">
<a href="projects">projects</a>
</div>
<div class="menu-item">
<a href="software-engineering-book-recommendations">book recs</a>
</div>
<div class="menu-item">
<a href="games">game recs</a>
</div>
<div class="menu-arrow-right-2"></div>
</div>
</div>
<div class="menu-small">
<div class="menu-arrow-right"></div>
<div class="menu-header menu-header-small-1">
<div class="menu-item">
<a href="blog">blog</a>
</div>
<div class="menu-item">
<a href="about">about</a>
</div>
<div class="menu-item">
<a href="talks">talks</a>
</div>
<div class="menu-item">
<a href="projects">projects</a>
</div>
<div class="menu-arrow-right-2"></div>
</div>
<div class="menu-arrow-right"></div>
<div class="menu-header menu-header-small-2">
<div class="menu-item">
<a href="software-engineering-book-recommendations">book recs</a>
</div>
<div class="menu-item">
<a href="games">game recs</a>
</div>
<div class="menu-arrow-right-2"></div>
</div>
</div>
</div>
<div class="content-page" id="WRITEIT_DYNAMIC_CONTENT">
<!--WRITEIT_POST_NAME=Reducing iOS Build Times by using Interface Modules-->
<!--WRITEIT_POST_HTML_NAME=reducing-ios-build-times-by-using-interface-targets-->
<!--WRITEIT_POST_SITEMAP_DATE_LAST_MOD=2020-06-15T11:00:00+02:00-->
<!--WRITEIT_POST_SITEMAP_DATE=2020-01-23T06:00:00+00:00-->
<!--Add here the additional properties that you want each page to possess.-->
<!--These properties can be used to change content in the template page or in the page itself as shown here.-->
<!--Properties must start with 'WRITEIT_POST'.-->
<!--Writeit provides and injects WRITEIT_POST_NAME and WRITEIT_POST_HTML_NAME by default.-->
<!--WRITEIT_POST_SHORT_DESCRIPTION=While dividing an app into several modules can improve the build time of an iOS app, the result heavily depends on what is being changed. If you're not careful with how your dependency graph is laid out, you can often have results that are worse< than non-modularized apps. Let's take a look at a technique used at iFood to have big and consistent improvements to the build times of our app.-->
<title>Reducing iOS Build Times by using Interface Modules</title>
<div class="blog-post">
<div class="post-title-index">
<h1>Reducing iOS Build Times by using Interface Modules</h1>
</div>
<div class="post-info">
<div class="post-info-text">
Published on 23 Jan 2020
</div>
</div>
<p><b>Update (2022-08-31):</b> A more detailed version of this article <a href="https://www.runway.team/blog/how-to-improve-ios-build-times-with-modularization">is available here</a>. Alternatively, you can watch <a href="https://www.youtube.com/watch?v=sZuI6z8qSmc">my SwiftHeroes talk</a> about the same subject.</p>
<p>While dividing an app into several modules can improve the build time of an iOS app, the result heavily depends on what is being changed. If you're not careful with how your dependency graph is laid out, you can often have results that are <b>worse</b> than non-modularized apps. Let's take a look at a technique used at iFood to have big and consistent improvements to the build times of our app.</p>
<h2>Context: The process to modularize an app</h2>
<div class="sponsor-article-ad-auto hidden"></div>
<p>Before jumping into the actual techniques, let's add some context to this so we have a better idea of <b>why</b> they are necessary.</p>
<p><i>(For the purposes of this article, we ignore the concept of Xcode's incremental build feature as I personally never saw it make a considerable difference in build times compared to the improvements you get by properly modularizing the app. This is geared towards apps that are built on top of monorepo systems that cache builds between CI builds)</i></p>
<p>When tasked to develop a small project, a developer's first idea will usually be to create a <b>single module</b> that contains absolutely all the code and resources:</p>
<div class="post-image margin-top-40 margin-bottom-40" style="text-align: center;">
<img src="https://i.imgur.com/Td17sGi.png" alt="Big module">
</div>
<p>This is how beginners usually develop their first few apps, and how seasoned developers still develop apps that are sufficiently small for this approach to be useful.</p>
<p>This approach is the easiest one in terms of project maintainability, but the worse one in terms of build times. Because everything is packed together, changing <b>anything</b> results in <b>everything</b> being recompiled, even if they have nothing to do with what's changed. While this not much of a problem for small apps, big ones take massive performance hits with this approach, easily reaching build times of over twenty minutes.</p>
<p>Because of that, more seasoned developers will often opt for a modularized structure when developing apps. This time, instead of having a single monster modules, we divide our code and resources into several smaller ones, which are then linked together either manually inside Xcode, or through a dependency manager like CocoaPods / monorepo build system like Buck (what we personally use at iFood)</p>
<div class="post-image margin-top-40 margin-bottom-40" style="text-align: center;">
<img src="https://i.imgur.com/M69uv4R.png" alt="App with some modules">
</div>
<p>In this specific diagram, because the modules have no connection to each other, making changes to a feature will make so that other features <b>do not</b> get recompiled, which provides a massive boost to the app's build time. Hooray!</p>
<p>Unfortunately, this statement is only true to this specific diagram. As an app grows, it's more likely that you will end with something like this:</p>
<div class="post-image margin-top-40 margin-bottom-40" style="text-align: center;">
<img src="https://i.imgur.com/7R1dewm.png" alt="Bad dependency cycle">
</div>
<p>While the previous condition is still true if the changes are made to the modules in the lower end of this graph (<code>AppFeature1</code>, <code>AppFeature2</code>, <code>AppFeature3</code>), it is not true for the rest.</p>
<p>Take the <code>HTTPClient</code> module for example: Because <b>everyone</b> depends on it, making changes to it will make the <b>entire app need to be recompiled</b>, even if the changes have nothing to do with the modules themselves, like a simple code quality improvement!</p>
<p>Another common problem of this approach is how <code>AppFeature1</code> is structured: It has a dependency on <code>AppFeature4</code>, which on its turn has a dependency on <code>AppFeature5</code>. An example of how this can happen is if these features represent <code>UIViewControllers</code> -- making so a module needs to import another one for a view to be able to be pushed. This <b>linear dependency</b> between these modules has three major problems:</p>
<p>- Because the dependency is linear, you are unable to compile them in parallel, which is a major performance problem.</p>
<p>- Changing <code>AppFeature5</code> will recompile <code>AppFeature4</code> and <code>AppFeature1</code> even if the changes have nothing to do with them, just because they depend on it!</p>
<p>- Finally, the fact that they completely depend on each other is overkill. They are dependencies only for the purpose of being able to navigate between each other -- they don't need access to anything else of the respective modules. This makes the previous problem even more critical as these features will be recompiled for no reason almost 100% of the time!</p>
<p>The problems of this diagram are a good representation of how iFood looked like for a long time, and although the modularization itself improved build times in some cases, most of the cases still provided very bad build times.</p>
<p>In order to achieve the best possible build times, we need to make our dependency graph as <b>horizontal</b> as possible. If everything is independent, everything can be compiled in parallel.</p>
<div class="post-image margin-top-40 margin-bottom-40" style="text-align: center;">
<img src="https://i.imgur.com/gnne0rL.png" alt="Horizontal dependency">
</div>
<p>Unfortunately, this is impossible in our case. Because our modules need to navigate between each other's <code>UIViewControllers</code>, they need to be able to reference each other somehow. The same applies to components like <code>HTTPClient</code> -- as they need to somehow have access to it in order to make HTTP requests, they can never be truly independent. ...or can they?</p>
<p>The true answer is, well, no. However, through dependency injection, there's a technique we can apply to get pretty close to it.</p>
<h2>"Interface" modules: Never depend on concrete modules</h2>
<p>Let's focus on the problems caused by the previously mentioned <b>linear dependency</b> of some of the modules: These features depend on the modules of the features they navigate to, but they don't access anything of these modules besides the <code>UIViewController</code> that they are navigating to.</p>
<div class="post-image margin-top-40 margin-bottom-40" style="text-align: center;">
<img src="https://i.imgur.com/XAQ4CtN.png" alt="Problem">
</div>
<p>If this problem is caused because the features are depending on more than they need to, what if we divide the navigation aspect of a feature from its actual contents? Having this in mind, instead of a having a massive "feature" module that has everything, we can separate the relevant navigation content into its own "interface" module:</p>
<div class="post-image margin-top-40 margin-bottom-40" style="text-align: center;">
<img src="https://i.imgur.com/u0sF8SF.png" alt="App with interfaces" style="height: 250px; width: 300px;">
</div>
<p>"Interface" modules don't contain any concrete code or dependencies -- they just contain protocols that are used by the modules that depend on it to reference some piece of code that is defined in the real, concrete module.</p>
<p>For example, assuming that <code>AppFeature1</code> wants to push a <code>UIViewController</code> that lives in <code>AppFeature2</code>, instead of having <code>AppFeature1</code> depend on <code>AppFeature2</code> and directly reference such <code>UIViewController</code>, we can have it depend on a hypothetical <code>AppFeature2Interface</code> module that has a protocol that serves no purpose but to expose the existence of that view to <code>AppFeature1</code>:</p>
<pre><code>protocol Feature2ViewProtocol {}</code></pre>
<p><code>AppFeature2</code> can then implement this protocol into the related, concrete <code>UIViewController</code>:</p>
<pre><code>import AppFeature2Interface
class Feature2ViewController: UIViewController, Feature2ViewProtocol</code></pre>
<p>With this setup, <code>AppFeature1</code> can now reference <code>AppFeature2's</code> <code>UIViewController</code> without actually importing it:</p>
<pre><code>import AppFeature2Interface
let viewTypeToBePushed = Feature2ViewProtocol.self</code></pre>
<p>While <code>AppFeature1</code> still imports the interface itself, its size is considerably smaller than the actual feature's module, while the final result (getting a view to be pushed) is the same. <code>AppFeature1</code> now imports only what it really needs, making so it will only be recompiled by changes that actually affect it. </p>
<p>However, there's a small catch. You might notice that this example doesn't make sense: While <code>AppFeature1</code> has access to the protocol that references <code>AppFeature2's</code> <code>UIViewController</code>, it can't actually push it. If we only have access to the bare <code>protocol</code>, we are unable to create an instance of the concrete <code>class</code> that only exists in <code>AppFeature2</code>.</p>
<p>This is because we're missing a key component in this structure: <b>a dependency injector.</b> In an app like this, because there's no way for modules to reference the concrete information from other modules, there needs to be a global system that is capable of returning the (now hidden) concrete information to modules when a certain protocol is given. At iFood, we solved this by creating a dependency injector called <code>RouterService</code> -- here, each feature's module's interface only exposes a series of <code>Route</code> structs, while each feature's concrete module connects these <code>Routes</code> to the related <code>UIViewController</code>.</p>
<p>With this information exposed, iFood's AppDelegate creates an instance of a <code>RouterService</code> which receives every feature's routes and related <code>UIViewControllers</code>. When a feature asks for a specific <code>Route</code> from another feature's interface to be executed, the <code>RouterService</code> automatically locates which <code>UIViewController</code> should be pushed. The process to create something like was detailed in my previous article, <a href="https://swiftrocks.com/using-type-erasure-to-build-a-dependency-injector-in-swift">Using Type Erasure to Build a Dependency Injecting Routing Framework in Swift.</a> Here's a simplified example of how the previous example looks like in our app:</p>
<pre><code>// AppFeature2Interface, which depends on RouterServiceInterface
public struct Feature2Route: Route { ... }</code></pre>
<pre><code>// AppFeature2
import AppFeature2Interface
class Feature2ViewController: Feature { ... }
public class Feature2RouteHandler: RouteHandler {
var routes: [Route.Type] {
return [Feature2Route.self]
}
func featureFor(route: Route) -> AnyFeature {
return AnyFeature(Feature2ViewController.self)
}
}</code></pre>
<pre><code>// iFood's AppDelegate
import AppFeature1
import AppFeature2
let routerService = RouterService()
routerService.register(Feature1RouteHandler())
routerService.register(Feature2RouteHandler())
routerService.start(fromRoute: Feature1Route.self)</code></pre>
<pre><code>// AppFeature1
import AppFeature2Interface
func goToFeature2() {
routerService.navigate(toRoute: Feature2Route())
// RouterService translates Feature2Route into the actual Feature2ViewController
// and pushes the related UIViewController.
}</code></pre>
<p>In terms of build time, because <code>AppFeature1</code> doesn't depend on <code>AppFeature2</code> anymore, changes to <code>AppFeature2</code> will <b>not</b> recompile <code>AppFeature1</code>. If you had multiple modules depending on each other, an app that runs entirely on this structure will provide a massive boost in build performance!</p>
<p>As a bonus, this structure can be applied to everything that can be injected. Using the previous complete diagram as an example, we could also add an interface to the <code>HTTPClient</code> that contains only the protocol that defines how requests are made. This allows modules to only reference this protocol, while the dependency injector becomes responsible for injecting the actual concrete class into the modules that reference such protocol. In the end, we can end up with the following diagram:</p>
<div class="post-image margin-top-40 margin-bottom-40" style="text-align: center;">
<img src="https://i.imgur.com/6sn22b4.png" alt="Full app with interfaces">
</div>
<div class="sponsor-article-ad-auto hidden"></div>
<p><i>(Not shown: In our case, everyone depends on something called a <code>RouterServiceInterface</code>, with the main module depending on the concrete <code>RouterService</code>, which dynamically links the interfaces to their actual classes.)</i></p>
<p>Note how the graph is considerably more <b>horizontal</b> than its counterpart -- even though everyone depends on <code>HTTPClientInterface</code>, changes to the actual <code>HTTPClient</code> will have no repercussions on the other modules, making the app compile considerably faster. With an entire app running on this structure, you should only have bad compilation times if the interfaces themselves are changed -- something that should be a rare occurrence. In general, every module is going to be completely independent of each other, which can be especially useful and productive when developing inside the module's specific scheme in Xcode.</p>
</div></div>
<div class="blog-post footer-main">
<div class="footer-logos">
<a href="https://swiftrocks.com/rss.xml"><i class="fa fa-rss"></i></a>
<a href="https://twitter.com/rockbruno_"><i class="fa fa-twitter"></i></a>
<a href="https://github.com/rockbruno"><i class="fa fa-github"></i></a>
</div>
<div class="footer-text">
© 2025 Bruno Rocha
</div>
<div class="footer-text">
<p><a href="https://swiftrocks.com">Home</a> / <a href="blog">See all posts</a></p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Blog Post (Right Sidebar) End -->
</div>
</div>
</div>
<!-- All Javascript Plugins -->
<script type="text/javascript" src="js/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
<script type="text/javascript" src="js/prism4.js"></script>
<!-- Main Javascript File -->
<script type="text/javascript" src="js/scripts30.js"></script>
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-H8KZTWSQ1R"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-H8KZTWSQ1R');
</script>
</body>
</html>