- Start by creating two frameworks, one as a separate project for code you share with other apps (which code can be compiled independently), the second as an additional target for files you won't share. The Swift checkbox should be checked if you will ever use Swift, now or in the future.
- Make sure the checkbox for App Extension is checked.
- Note that Objective-C can/should only use Swift in their .m files. If you try to import Swift classes from frameworks into a .h file you will spend several hours regretting that decision. Import an intermediate protocol or similar instead.
- To use Obj-C files FROM Swift, you need to make their .h files public (in the file info sidebar), then expose them in the frameworks umbrella header.
- Now starts the fun task (sarcasm) of splitting your app into these three sections, stuff that is imported by the appDelegate or uses UIApplication remains in the app, everything else goes into these two frameworks depending on usage.
Now you are done with stage one. Not much “architecturing” at this point. But you have already gained two great things:
- Speed when having App Extensions is multiplied. Just having extensions causes everything to recompile once for every extension - in addition to the slow compile times of Swift!
- Testing is now instant! Frameworks can be tested without launching the app, so tests you have moved or will write are going to be lightning fast! If you need the bundle or the app, you will need to rewrite some, but it’s still doable (more on that in a future post).
Stage two, splitting what's left of the app. Since backgroundTasks make use of the UIApplication, all long running tasks are probably left in the app. Separating these tasks can be tedious but there is a shortcut! NSProcessInfo can also disable termination, but it has no callback. So you better make sure you only need those few seconds you get. Otherwise you will simply need to leave the classes in the app, splitting them into two classes is quite pointless. If you are using Objc, you can trick the compiler into importing UIApplication only when it exists. Like so:
static Class uiApplication = NSClassFromString(@"UIApplication"); [[uiApplication sharedApplication] anyFunction];
Stage three, splitting the frameworks into smaller frameworks. Having lots of Swift really takes a toll out of your patience when compiling, especially with SwiftUI and switching between testing/running - three tasks that keeps rebuilding your code. Here you can go many routes, but what I've done in Feeds is to layer everything, larger UI-classes and Controllers on top, data-objects/models below, and on the bottom there are standalone UI components, utilities and frameworks shared with other projects. The important take-away is that the framework above can use those below but never the other way around (since we don't want circular dependencies). In addition it forces a strict separation of UI from data, which should not affect anything as it should always be separated. With this setup it is also easy to split up UI-frameworks when compile times for those grow too large.
In my experience it seems that SwiftUI and Combine benefits heavily from small structs, compile times seems to improve massively when split into smaller pieces even inside the same framework. So if Swift compile times is your main problem that should be the first order of business. I have not measured it but I will eat all my hats if I'm wrong.
Side notes
Swift in frameworks tend to become public all over, if you have a tight relationship between two classes/structs in separate frameworks at least one will need to become public. This degrades compile times since Xcode now needs to make sure the code is sharable (probably due to ABI-stability). This makes it important that classes that uses each other stays in the same framework. Also it makes it clear that frameworks is not a silver-bullet that solves all your problems, it can even make things worse. Secondly dynamic frameworks slows down your app's startup, so you should be sure that it is necessary when creating new ones. You could use static frameworks instead, but these take longer to compile. One could imagine a situation where you switch between static and dynamic before building for the App Store, but since this is not done automatically and since Apple recommends dynamic frameworks I assume there are more to the picture than just compile and startup times.
If you know the answer to that riddle, or have a hunch - you are more than welcome to send me a suggestion: @olof_t