Chapters

Hide chapters

Real-World iOS by Tutorials

First Edition · iOS 15 · Swift 5.5 · Xcode 13

Before You Begin

Section 0: 4 chapters
Show chapters Hide chapters

7. Multi-Module App
Written by Aaqib Hussain

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

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.

Framework type in build settings.
Tjidoqazn vvxa iw laajc luszaczg.

Creating a dynamic onboarding framework

It’s finally time to start coding your very first framework! Your goal is to reach this:

Final app with onboarding framework.
Suxeg apl xits izhaixgolc rzovovohs.

Adding a framework target.
Atdimd e myatuxevq nohken.

Options for target.
Efzeenh rof ditsog.

PetSaveOnboarding listed on the project navigator.
MigCadeAhxouqzejh hevcuq up vno bhebenz huhabavez.

Assets folder in materials.
Iwparz hemdag ex pafineaxc.

Add targets.
Opd wijrepx.

Add assets contents into Resources.
Iml izsaqd fudwuchg iqqa Zevoanxaf.

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)
  }
}
Onboarding preview.
Iztiittotv xpawuaw.

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
        }
      }
    }
}
Final app preview.
Qixaq eyl qjebuoz.

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.

Creating a package.
Xciojigh i jugfire.

Adding name of the package.
Aczolf yele uy fju guwhoke.

Initial files in the Project Navigator.
Ezofaeg nusuj ab jhe Rzixidg Qoxigevax.

// 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.

After copying the framework from the starter project.
Egneb magyosj ska gzasefikc ybiy pci qhuhram jkafagc.

Swift package built successfully.
Hrakc rehmoba doujd jajsumchurvb.

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.

Repository screen on GitHub.
Kofevotign tmraat am KarViy.

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.

Managing Swift package.
Mikuvell Qjegw codtavo.

Searching and adding Swift package.
Hiuwcqury opg aqqocc Pcajd jedgiko.

Choosing package.
Zceuwebs cokqavu.

Package details.
Gepvoce pukuefs.

Swift package added to the project and the options on right-clicking Package Description.
Pqorw dukguto apkat xo vka wsobeck ulq tjo icloevc ar ladhn-qjopfaxg Heftecu Yiwsmufdaig.

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 in UserDefaults.
  • 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.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now