7
Multi-Module App
Written by Aaqib Hussain
Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as
text.You can unlock the rest of this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.
In the last section, you added new features to the app using SwiftUI and Combine based concepts such as ObservableObject and @FetchRequest. You used these concepts to implement features like Search and Locating animals near you. These features made the app more usable. But what about making our code more reusable?
In this section, you’ll learn how to modularize the app and navigate between different modules. In this chapter, you’ll learn about the benefits of modularization and what tools are at your disposal to support it.
More specifically, you’ll learn about:
- Xcode support in the build process using build targets and workspace compilation.
- The different types of frameworks you can create for your apps.
- Some of the dependency management options available for iOS development
With the acquired skills, you’ll create an onboarding framework for PetSave. So users can have a nice introduction to your app.
Onboarding screens welcome users the first time they launch your app. Since their first impression could be the last impression, it’s quite important for the developers to get this right.
Are you excited to get onboarded? Here you go!
Modularization
Modularization is a software design technique that lets you separate an app’s features into many smaller, independent modules. To achieve modularization, one must think out of the box. You use encapsulation and abstraction by exposing the methods you want the app to use and hiding all the unnecessary or complex details.
Benefits of modularization
Modularization comes with many benefits, including:
Reusability
Consider you develop an onboarding framework where you customize texts and images before showing them in the app. To create such a framework, you must implement it so it’s independent of the app. Then, you share and reuse it in other projects.
Time and cost savings
Reusability leads to time and cost savings. In the example of creating an onboarding module, you can easily integrate the onboarding module into a different project. It’s like plug and play that saves you both time and development cost.
Community support
By publishing the onboarding module as public on a platform like GitHub, you get support from the open-source community on fixing bugs you might have missed. Developers simply open a pull request for a bug fix or add a new feature.
Build time
When you rebuild the project after changing the onboarding framework, Xcode won’t recompile the entire app. Instead, it’ll only compile the changed module. This results in faster build times and, in general, accelerated development. But for you to guarantee this, you have two know the different kinds of frameworks and the use case for each one, you’ll go over that later in this chapter.
Xcode support in the build process
While Xcode comes with many features, the two features you’ll learn about in this chapter are build targets and workspace compilation.
Build targets
A target is a modularized structure. A target takes its instruction through build settings and build phases. A project can contain more than one target, and one target can depend on another. These targets can be something like the watchOS version of your app or represent your app test suite.
Workspace compilation
A workspace combines projects and other documents under one roof so you can work on them together. It can have multiple projects or documents you want to work on. It also manages implicit and explicit dependencies among the included targets.
What is a framework?
A framework is a bundle that can contain resources of any type, such as classes, assets, nib files or localizable strings. Frameworks encapsulate and modularize code, making it reusable. Common iOS frameworks include Foundation, UIKit and SwiftUI.
Types of Frameworks
There are two types of frameworks in iOS: Static and Dynamic. Take a moment to learn about these frameworks and how they differ.
Static Framework
Static frameworks consist of code that doesn’t change because it’s linked at compile time. Static frameworks generate a .a extension. They only hold code and gets copied with the app’s executable, making the executable size larger.
Dynamic Framework
Unlike static frameworks, dynamic frameworks have a codebase that may change and contain other resources, like images. Dynamic frameworks generate the extension .dylib. It’s not copied, but linked with the app’s executable at runtime, thus, resulting in a smaller app size.
Creating a dynamic onboarding framework
It’s finally time to start coding your very first framework! Your goal is to reach this:
extension Bundle {
public static var module: Bundle? {
Bundle(identifier: "com.raywenderlich.PetSaveOnboarding")
}
}
import SwiftUI
extension Color {
static var rwGreen: Color {
Color("rw-green", bundle: .module)
}
static var rwDark: Color {
Color("rw-dark", bundle: .module)
}
}
import SwiftUI
public extension Image {
static var bird: Image {
Image("creature-bird-blue-fly", bundle: .module)
}
static var catPurple: Image {
Image("creature-cat-purple-cute", bundle: .module)
}
static var catPurr: Image {
Image("creature-cat-purr", bundle: .module)
}
static var chameleon: Image {
Image("creature-chameleon", bundle: .module)
}
static var dogBoneStand: Image {
Image("creature-dog-and-bone", bundle: .module)
}
static var dogBone: Image {
Image("creature-dog-bone", bundle: .module)
}
static var dogTennisBall: Image {
Image("creature-dog-tennis-ball", bundle: .module)
}
}
import SwiftUI
public struct OnboardingModel: Identifiable {
public let id = UUID()
// 1
let title: String
let description: String
let image: Image
// 2
let nextButtonTitle: String
let skipButtonTitle: String
// 3
public init(
title: String,
description: String,
image: Image,
nextButtonTitle: String = "Next",
skipButtonTitle: String = "Skip") {
self.title = title
self.description = description
self.image = image
self.nextButtonTitle = nextButtonTitle
self.skipButtonTitle = skipButtonTitle
}
}
import SwiftUI
// 1
struct Pet: Identifiable {
let id = UUID()
let petImage: Image
let position: CGPoint
}
// 2
extension Pet {
static let backgroundPets: [Pet] = {
let bounds = UIScreen.main.bounds
return [
Pet(petImage: .bird,
position: .init(x: bounds.minX + 50, y: 20)),
Pet(petImage: .catPurple,
position: .init(x: bounds.maxX, y: bounds.maxY / 2)),
Pet(petImage: .catPurr,
position: .init(x: bounds.maxX, y: bounds.maxY - 100)),
Pet(petImage: .chameleon,
position: .init(x: bounds.minX, y: bounds.maxY / 2)),
Pet(petImage: .dogBoneStand,
position: .init(x: bounds.minX, y: bounds.maxY / 1.5)),
Pet(petImage: .dogBone,
position: .init(x: bounds.maxX - 50, y: 50)),
Pet(petImage: .dogTennisBall,
position: .init(x: bounds.minX, y: bounds.maxY - 10))
]
}()
}
import SwiftUI
struct OnboardingView: View {
// 1
let onboarding: OnboardingModel
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 12, style: .circular)
.fill(.white)
.shadow(radius: 12)
.padding(.horizontal, 20)
VStack(alignment: .center) {
VStack {
// 2
Text(onboarding.title)
.foregroundColor(.rwDark)
.font(.largeTitle)
.bold()
.multilineTextAlignment(.center)
.padding(.horizontal, 10)
Text(onboarding.description)
.foregroundColor(.rwDark)
.multilineTextAlignment(.center)
.padding([.top, .bottom], 10)
.padding(.horizontal, 10)
onboarding.image
.resizable()
.frame(width: 140, height: 140, alignment: .center)
.foregroundColor(.rwDark)
.aspectRatio(contentMode: .fit)
}
.padding()
}
}
}
}
import SwiftUI
struct OnboardingBackgroundView: View {
// 1
let backgroundPets = Pet.backgroundPets
// 2
var body: some View {
ZStack {
ForEach(backgroundPets) { pet in
pet.petImage
.resizable()
.frame(width: 200, height: 200, alignment: .center)
.position(pet.position)
}
}
}
}
@State var currentPageIndex = 0
// 2
public init(items: [OnboardingModel]) {
self.items = items
}
// 3
private var onNext: (_ currentIndex: Int) -> Void = { _ in }
private var onSkip: () -> Void = {}
// 4
private var items: [OnboardingModel] = []
// 5
private var nextButtonTitle: String {
items[currentPageIndex].nextButtonTitle
}
private var skipButtonTitle: String {
items[currentPageIndex].skipButtonTitle
}
public var body: some View {
if items.isEmpty {
Text("No items to show.")
} else {
VStack {
TabView(selection: $currentPageIndex) {
// 1
ForEach(0..<items.count) { index in
OnboardingView(onboarding: items[index])
.tag(index)
}
}
.padding(.bottom, 10)
.tabViewStyle(.page)
.indexViewStyle(.page(backgroundDisplayMode: .always))
.onAppear(perform: setupPageControlAppearance)
// 2
Button(action: next) {
Text(nextButtonTitle)
.frame(maxWidth: .infinity, maxHeight: 44)
}
.animation(nil, value: currentPageIndex)
.buttonStyle(OnboardingButtonStyle(color: .rwDark))
Button(action: onSkip) {
Text(skipButtonTitle)
.frame(maxWidth: .infinity, maxHeight: 44)
}
.animation(nil, value: currentPageIndex)
.buttonStyle(OnboardingButtonStyle(color: .rwGreen))
.padding(.bottom, 20)
}
.background(OnboardingBackgroundView())
}
}
// 1
public func onNext(
action: @escaping (_ currentIndex: Int) -> Void
) -> Self {
var petSaveOnboardingView = self
petSaveOnboardingView.onNext = action
return petSaveOnboardingView
}
public func onSkip(action: @escaping () -> Void) -> Self {
var petSaveOnboardingView = self
petSaveOnboardingView.onSkip = action
return petSaveOnboardingView
}
// 2
private func setupPageControlAppearance() {
UIPageControl.appearance().currentPageIndicatorTintColor =
UIColor(.rwGreen)
}
// 3
private func next() {
withAnimation {
if currentPageIndex + 1 < items.count {
currentPageIndex += 1
} else {
currentPageIndex = 0
}
}
onNext(currentPageIndex)
}
struct OnboardingButtonStyle: ButtonStyle {
let color: Color
func makeBody(configuration: Configuration) -> some View {
configuration.label
.background(color)
.clipShape(Capsule())
.buttonStyle(.plain)
.padding(.horizontal, 20)
.foregroundColor(.white)
}
}
private extension PreviewProvider {
static var mockOboardingModel: [OnboardingModel] {
[
OnboardingModel(
title: "Welcome to\n PetSave",
description:
"Looking for a Pet?\n Then you're at the right place",
image: .bird
),
OnboardingModel(
title: "Search...",
description:
"Search from a list of our huge database of animals.",
image: .dogBoneStand,
nextButtonTitle: "Allow"
),
OnboardingModel(
title: "Nearby",
description:
"Find pets to adopt from nearby your place...",
image: .chameleon
)
]
}
}
struct PetSaveOnboardingView_Previews: PreviewProvider {
static var previews: some View {
PetSaveOnboardingView(items: mockOboardingModel)
}
}
static let onboarding = "onboarding"
import PetSaveOnboarding
// 1
@AppStorage(AppUserDefaultsKeys.onboarding)
var shouldPresentOnboarding = true
// 2
var onboardingModels: [OnboardingModel] {
[
OnboardingModel(
title: "Welcome to\n PetSave",
description:
"Looking for a Pet?\n Then you're at the right place",
image: .bird
),
OnboardingModel(
title: "Search...",
description:
"Search from a list of our huge database of animals.",
image: .dogBoneStand
),
OnboardingModel(
title: "Nearby",
description:
"Find pets to adopt from nearby your place...",
image: .chameleon
)
]
}
var body: some Scene {
WindowGroup {
ContentView()
// 1
.fullScreenCover(
isPresented: $shouldPresentOnboarding, onDismiss: nil
) {
// 2
PetSaveOnboardingView(items: onboardingModels)
.onSkip { // 3
shouldPresentOnboarding = false
}
}
}
}
What is Cocoapods?
Cocoapods is a dependency manager that supports publishing and maintaining libraries in Swift and Objective-C. You can use it to import multiple libraries in your project. It’s built with Ruby, and you can use the default version of Ruby on Mac to install it.
Using Cocoapods
There’s a large variety of third-party libraries written with Cocoapods on GitHub. To consume these libraries, initialize Cocoapods in your project and put all your dependencies in a file called Podfile.
pod install
What is Carthage?
Like Cocoapods, Carthage is a dependency manager. It’s the first one to support Swift that was also written in Swift. It supports macOS and iOS applications.
carthage update --use-xcframeworks
What are Swift packages?
Swift Packages are repositories that enable developers to create, publish and maintain a package. Furthermore, they help to add, remove and manage Swift package dependencies. Besides Swift language, they allow porting of code from Objective-C, Objective-C++, C or C++.
Differences between dependency managers
You’ve so far studied Cocoapods, Carthage and Swift Package. Here, you’ll learn the basic differences between them:
Properties | Cocoapods | Carthage | Swift Package |
---|---|---|---|
Agnostic of the project | ❌ | ✅ | ✅ |
Easy to manage | ❌ | ❌ | ✅ |
Supported by Apple | ❌ | ❌ | ✅ |
Thousands of open source libraries | ✅ | ✅ | ❌ |
Requires manual setup | ❌ | ✅ | ❌ |
Supports dynamic and static frameworks | ✅ | ✅ | ✅ |
Faster build time | ❌ | ✅ | ✅ |
Dependent dependency management | ✅ | ✅ | ✅ |
Creating and configuring a Swift package
Start by selecting Package from File ▸ New.
// 1
// swift-tools-version:5.5
// The swift-tools-version declares the minimum version of
// Swift required to build this package.
import PackageDescription
let package = Package(
// 2
name: "PetSaveOnboarding",
// 3
platforms: [.iOS(.v15), .macOS(.v10_15)],
// 4
products: [
.library(
name: "PetSaveOnboarding",
targets: ["PetSaveOnboarding"]),
],
// 5
dependencies: [],
// 6
targets: [
.target(
name: "PetSaveOnboarding",
resources: [.copy("Resources/Assets.xcassets")]),
]
)
Adding code and resources
Now, using Finder, replace Sources/PetSaveOnboarding in your package with the PetSaveOnboarding framework.
Publishing the Swift package
Note: To follow the rest of the chapter you’ll need a Github account. If you don’t have one already, create one by going to https://github.com/signup.
git remote add origin https://github.com/<---github-user-name--->/PetSaveOnboarding.git
git add --all
git commit -m "Add package sources"
git push --set-upstream origin main
Consuming the Swift package
Open the PetSave app with the framework you created earlier. Now, you’ll replace the framework with the published GitHub package.
Key points
- Modularization leads to time and cost savings, reusability and faster build times.
- A framework is an encapsulated and modularized piece of reusable bundle.
- Static frameworks link code at compile time. Dynamic frameworks link code at runtime.
-
@AppStorage
is a SwiftUI property wrapper for saving values inUserDefaults
. - Swift Packages are repositories that enable developers to create, publish and maintain a package. They are managed using Swift Package Manager (SPM).
- Cocoapods and Carthage are alternatives to SPM, which you can use to create and use libraries.
Where to go from here?
This marks the end of this chapter. You got familiarized and grasped a lot of concepts related to modularization. You learned how modularization can play a vital role in making your overall development faster.