Swift App Bundle Sizes

I’m looking at building a casual game for iOS and one of the extremely important things is to make sure that the app bundle size is as small as possible. I want users to be able to quickly download, and most importantly, not be blocked by the cellular download size limit, which is currently 100MB.

I did a directly listing of the Frameworks directory in an app bundle and was a little dismayed:

Filename                       Size (b)
------------------------------------
libswiftAVFoundation.dylib      254000
libswiftCore.dylib            14221040
libswiftCoreAudio.dylib         396048
libswiftCoreGraphics.dylib      543632
libswiftCoreImage.dylib         207888
libswiftCoreMedia.dylib         253072
libswiftDarwin.dylib            295808
libswiftDispatch.dylib         1093360
libswiftFoundation.dylib       5418720
libswiftGLKit.dylib             246576
libswiftGameplayKit.dylib       212160
libswiftObjectiveC.dylib        258544
libswiftSceneKit.dylib          250544
libswiftSpriteKit.dylib         250064
libswiftUIKit.dylib             348048
libswiftos.dylib                247696
libswiftsimd.dylib             1003200

That’s a total of 25,500,400 bytes (or approx 25 MB)!! Yikes!

Surely, that’s not really the size, right!? The xcarchive must be smaller do some thinning, yeah?

Filename                       Size (b)
------------------------------------
libswiftAVFoundation.dylib      451440
libswiftCore.dylib            45120816
libswiftCoreAudio.dylib         774336
libswiftCoreGraphics.dylib     1135952
libswiftCoreImage.dylib         257248
libswiftCoreMedia.dylib         450512
libswiftDarwin.dylib            542608
libswiftDispatch.dylib         2541392
libswiftFoundation.dylib      16854896
libswiftGameplayKit.dylib       345296
libswiftGLKit.dylib             310880
libswiftObjectiveC.dylib        488960
libswiftos.dylib                398624
libswiftSceneKit.dylib          348784
libswiftsimd.dylib              693568
libswiftSpriteKit.dylib         395776
libswiftUIKit.dylib            3010368

This is a total of 74,121,456 bytes (or approx 74 MB)… ugh, that’s a lot.

I reached out on Twitter to see who’s actually shipping Swift apps. If the app bundle size is going to really be between 25MB and 75MB for Swift support alone, that’s going to be a deal breaker for me.

Fortunately, all of these file sizes are just what the local size. The App Store is where all of the thinning is being done. The actual size bump for a shipping product with Swift support is much smaller:

Filename                       Size (b)
------------------------------------
libswiftContacts.dylib          147200
libswiftCore.dylib             7680608
libswiftCoreData.dylib          147568
libswiftCoreGraphics.dylib      268016
libswiftCoreImage.dylib         141568
libswiftDarwin.dylib            180576
libswiftDispatch.dylib          147104
libswiftFoundation.dylib        902016
libswiftObjectiveC.dylib        173168
libswiftUIKit.dylib             203408
libswiftWebKit.dylib            147392

Alrighty, so this is looking better, but still not great: 10,138,624 bytes (approx 10 MB). This of course is missing some of the frameworks that I was using above, like Swift support for AV Foundation, but seeing as libswiftCore is the primary culprit of the size, I think it’s safe to say that budgeting for 15 MB for Swift support should be sufficient.

This is still a large amount, but vastly better than I initially feared. I’m still not sure what I’m going to do, but at least I have a better ball park estimation.

Update:

I should also note that the App Store does compress your bundle as well. At the end, it’s really hard to know exactly how big your app bundle is going to be without actually publishing it up to the store. If anyone has any tips or tricks on that, please share on Twitter.

Swift App Bundle Sizes

Xcode, Frameworks, and Embedded Frameworks

So last week I spent the better part of the day trying to figure out what exactly was going when I was trying to build a component using SourceKittenFramework.

It turns out that not all frameworks are created equal in Xcode. Honestly, this wouldn’t be such a big deal if Swift properly supported static libraries, as the rabbit hole for this problem is rooted in a bunch of hacks to make command-line tools work properly with Swift that have dependencies.

I’ll provide a little story about my experience this week.

Embedded Frameworks

Xcode supports the concept of embedding frameworks into your bundle. This is essentially the same thing as the old “Copy Files” build phase where you can copy a dependency into your app bundle under a particular directly, such as “Frameworks”.

However, there is an extremely important distinction between the “Copy Files” build phase and the “Embed Frameworks” option.

The normal output of a framework looks like this:

├── Headers -> Versions/Current/Headers
├── Modules -> Versions/Current/Modules
├── MyFramework -> Versions/Current/MyFramework
├── Resources -> Versions/Current/Resources
└── Versions
├── A
│   ├── Headers
│   │   ├── MyFramework-Swift.h
│   │   └── MyFramework.h
│   ├── Modules
│   │   ├── MyFramework.swiftmodule
│   │   │   ├── x86_64.swiftdoc
│   │   │   └── x86_64.swiftmodule
│   │   └── module.modulemap
│   ├── MyFramework
│   └── Resources
│       └── Info.plist
└── Current -> A

This provides all of the necessary content to be able to use this framework both at runtime and as a developer-friendly framework; it has the headers and the module definitions necessary when building and linking against the library.

However, the frameworks that get embedded strip out all of that information and you end up with something like this:

├── MyFramework -> Versions/Current/MyFramework
├── Resources -> Versions/Current/Resources
└── Versions
├── A
│   ├── MyFramework
│   ├── Resources
│   │   └── Info.plist
│   └── _CodeSignature
│       └── CodeResources
└── Current -> A

This contains only the content required to be used at runtime. Now, it makes sense why Xcode would do this, after all, it’s being packaged up for use within a target so it’s already built. Also, this can help reduce the size of bundles by removing all of the information that simply isn’t necessary to work at runtime.

Had I only known this before…

Unexpected Outcomes

Now… the problem with all this of course is that when we start doing hacks to make thing work in the ever changing landscape of Swift and Xcode.

I ran into this when trying to use SourceKitten. As Swift doesn’t really have a good way to build testable command-line tools or static libraries, SourceKitten follows the pattern that a lot of other tools do: it builds an app target and then copies out the CLI tool and packages up its dependencies.

The start of the problems…

I don’t use Carthage or CocoaPods… but the reasons for that is outside of the scope for this. Needless to say, I simply clone the repo and ran make install as the ReadMe told me to do.

Everything is happily pulled down and everything is built properly. Then, SourceKitten is installed into my /usr/local path. Great!

Specifically, it creates a structure like this:

/usr/local/Frameworks/SourceKittenFramework.framework
/usr/local/bin/sourcekitten

The @rpath for sourcekitten is then set to @executable_path/../Frameworks.

There is nothing wrong with this setup. It works great.

However… remember, my intention is to now take SourceKittenFramework.framework, link it into my own app, and start converting my prototype that was shelling out to sourcekitten to directly use the API.

So I do what seems reasonable:

  1. Create my app
  2. Create a lib folder
  3. Copy the SourceKittenFramework.framework into lib
  4. Link the framework
  5. Add import SourceKittenFramework
  6. Build

And then I’m greeted with this:

<path/to/file:line:column> error: no such module 'SourceKittenFramework'
import SourceKittenFramework
       ^

Um… what!?

So I spend some time making sure my framework search paths are correct, inspecting the SourceKitten project, searching the web… no idea what is going on.

Ok… so I tried building from within Xcode and looking at the output of the SourceKittenFramework target itself. I still don’t understand the problem yet.

I copy over the version of SourceKittenFramework that is built from the target and I get this:

<path/to/file:line:col>: error: missing required modules: 'SWXMLHash', 'Yaml'
import SourceKittenFramework
       ^

Ok… what is happening!? I still haven’t figured out all of the specialness of “embedded frameworks” at this point.

I tried mucking with the framework search paths to point to the Yaml and SWXMLHash frameworks that I clearly see are within the frameworks, but nothing seems to be working… Including copying the Yaml and SWXMLHash frameworks to be siblings of the SourceKittenFramework.framework.

Ok… maybe source doesn’t work!? I download the framework from the releases page… SAME FLIPPING ERROR!

At this point, I’m quite frustrated, have no idea what is going on, and decide to call it for the day.

/ragequit

Taking Time

I come back the next day. OK, I’m going to get this to work!

I do, what I thought were mostly identical steps above.

Build success… ok, what the flying flip?

The key difference that I did this time was that I copied the Yaml.framework, SWXMLHash.framework, and SourceKittenFramework.framework from the target output of the SourceKittenFramework target.

At this point I was curious as to what had happened. This is where I started doing an analysis of what is different between each of the frameworks. See the “Embedded Frameworks” section above.

Conclusion

If you are providing frameworks to people that you expect to be able to develop with and not just use at runtime, please be sure to distribute the non-embedded framework version! Otherwise, well, all of your consumers will face the above issues.

SourceKitten tracking issue: https://github.com/jpsim/SourceKitten/issues/232

SourceKitten potential fix: https://github.com/jpsim/SourceKitten/pull/233

Xcode, Frameworks, and Embedded Frameworks