Building a real-world iOS app (Part 2): Separating application into frameworks

In this part of the series we'll overview how to properly setup the application.

Creating Frameworks

As we talked in the previous part, we'll begin the creation of the project by creating 3 separate frameworks inside our XCode project (TravelKit, TravelDataKit, TravelFeatureKit). An article on raywenderlich.com has a thorough explanation of the whole process.

After creating frameworks, project navigator should look something like in the picture below.

Frameworks in the Project Navigator

Setting up CocoaPods

We'll be using CocoaPods for managing dependencies in our project. Although setting up CocoaPods is fairly straightforward, there can be some difficulties when having local frameworks involved. The configuration is defined in Podfile which is located in the root folder of the project.

Podfile will be configured in a way that is clean and clear so it would not get messy when number of dependencies in the project grow. Essentially, we'll define the reusable pods at the top of the file and group different groups of pods that can be reused for different frameworks.

The part of Podfile that defines pods of TravelKit.

platform :ios, '11.0'
use_frameworks!
inhibit_all_warnings!

deployment_target = '11.0'

workspace 'TravelApplication.xcworkspace'

#Versions
$swinjectVersion =                    '~> 2.4'
$swiftDateVersion =                   '~> 5.0'
<...>

def shared_TravelKit_pods
    pod 'RxSwift',                    $rxSwiftVersion
    pod 'SwiftDate',                  $swiftDateVersion
end

target 'TravelKit' do
    project 'TravelKit.xcodeproj'
    platform :ios, deployment_target

    shared_TravelKit_pods

  target 'TravelKitTests' do
    project 'TravelKit.xcodeproj'
    inherit! :search_paths

    shared_testing_pods
  end
end
<...>

These different shared pods should be assembled and used of the actual application target.

<...>
def shared_Apps_pods
  shared_TravelKit_pods
  <...>
end

target 'TravelApplication' do
    project 'TravelApplication.xcodeproj'
    platform :ios, deployment_target
    shared_Apps_pods
end
<...>

The full Podfile can be found on GitHub as the rest of the project.

Dependency Injection

I prefer to think early about the way dependencies will be managed inside the application. Although dependency injection can be achieved without 3rd party libraries, for this project we'll use Swinject that has easy to use interfaces for managing dependencies.

Our classes will use initializer injection thus all the dependencies will be given through the initializer. The classes will be initialized in assemblies. Assembly is a Swinject class which has access to a container of already injected dependencies and provides a way to register new dependencies.

For example, this is how the assembly of the Feed that displays the list of flights might looks like:

import Foundation
import Swinject
import TravelKit

public class FeedAssembly: Assembly {

    public init() {
    }

    public func assemble(container: Container) {
        container.register(FeedConfigurator.self) { r in
            FeedConfigurator(
                regionRepository: r.resolve(RegionRepository.self)!,
                tripRepository: r.resolve(TripRepository.self)!,
                airportRepository: r.resolve(AirportRepository.self)!,
                tripImageRepository: r.resolve(TripImageRepository.self)!
                )
            }
            .initCompleted { (resolver, feedConfigurator) in
                feedConfigurator.bookTripConfigurator = resolver.resolve(BookTripConfigurator.self)!
            }
            .inObjectScope(.container)
    }
}

Here, we inject FeedConfigurator class. It is essentially a factory class for the whole Feed feature and its view. Swinject automatically passes the dependencies such as RegionRepository or TripRepository. We expect these dependencies to be injected in another assembly so we can resolve it here.

Our application will have AssemblerFactory that will contain all the different assemblies of the application and create them during initialization process.

import Foundation
import Swinject
import TravelFeatureKit
import TravelDataKit

class AssemblerFactory {

    func create() -> Assembler {
        let assemblies: [Assembly] = [
            ApplicationAssembly(),
            RegionRepositoryAssembly(),
            TripRepositoryAssembly(),
            AirportRepositoryAssembly(),

            MainAssembly(),
            FavoritesAssembly(),
            FeedAssembly(),
            BookTripAssembly(),
            BookURLRepositoryAssembly(affiliateId: Constants.affiliateId)
        ]

        let assembler = Assembler(assemblies)

        return assembler
    }
}

We use this assembly to create the first ViewController of the application and set it as rootViewController. See ApplicationLoader.

   self.assembler = AssemblerFactory().create()
  let rootConfigurator = assembler.resolver.resolve(MainConfigurator.self)!
  let rootViewController = rootConfigurator.createViewController()
  window?.rootViewController = rootViewController
  window?.makeKeyAndVisible()

In the following parts of the series we'll be creating classes for fetching and presenting data that will use assemblies for injecting dependencies. We'll see more closely how having proper dependency injection allows code to be more reusable, safe and testable.