Local Packages with Swift Package Manager

5 minute read

Local packages with Swift Package Manager

I have recently been using Swift Package Manager, in this post I will focus on an SPM feature that has been very useful for the creation of internal frameworks and modularization in general: local swift packages.

For a complete guide to this powerful dependency manager, I recommend starting here:

Why use swift packages?

  • They allow you to group related code and organize it into a single unit that can be compiled on its own.
  • It allows the reuse of packages in multiple projects, reduces development time, you can work in parallel with your colleagues.
  • If I am working with the main application or another package, the packages do not have to be recompiled.
  • Support for parallel compilation.
  • It is the Single Responsibility (SOLID) application applied at a higher level.

How to start.

Inside your project create a folder where the packages to be created will be grouped, you could call it DevPackages for example. Execute swift package init inside the folder Ready, I already have my first package created.

What is the package.swift file?

This file describes the configuration of your package, you can include here dependencies, platforms to support, minimum version, etc.

To see all the attributes it supports: https://github.com/apple/swift-package-manager/blob/main/Documentation/PackageDescription.md

Unlike Cocoa Pods the configuration file uses syntax in Swift.

As an example I will create a package that groups the logic to obtain Emojis.

The idea is to encapsulate a task within a package, internally the package can grow and be more complex (cache emojis, get them from a remote API, etc) and even use other packages.

Starting to work on my package

Basically you can work it in 2 ways

  1. Using the swift.package file By default Xcode has support to handle these files, you just open it and start working on it.

The fact that you can work independently of your main app is very useful, compilation times and the execution of tests are usually very fast.

  1. Integrating it into my main project. This is a not so clear step, but you can drag the folder that contains your package to your project.

Xcode detects Swift packages automatically.

To start your package you will need to do a couple of additional steps:

From the configuration of your project -> General -> Add Framework Select the Package previously added to your project.

Ready Xcode now you can start using it and even edit it from your main project.

If from your main project at some point you are going to modify code related to the package, I recommend selecting the autogenerated Schema, to avoid compiling the entire project, this will reduce compilation times in development times.

Adding external dependencies to a package

It is a common case that within a package you need a dependency from a third party.

To do this we edit our package.swift file

As simple as that, after this we can use a dependency in our package.

import PackageDescription

let package = Package(
  name: "EmojiMapper",
  products: [
    .library( name: "EmojiMapper", targets: ["EmojiMapper"]) ],
  dependencies: [
    .package(url: "https://github.com/Alamofire/Alamofire.git", 
    .upToNextMajor(from: "5.4.0")),
  ],
  targets: [
    .target(
      name: "EmojiMapper",
      dependencies: [ .product(name: "Alamofire", package: "Alamofire") ]),
    .testTarget(
      name: "EmojiMapperTests", 
      dependencies: ["EmojiMapper"])
  ]
)

A package needs to use another Local Package

Another common use case, a Package A depends on a Package B

For this case you will have to use another argument of the package function, this will refer to the relative path of our project

let package = Package(
  name: "EmojiMapper",
  products: [
    .library(
      name: "EmojiMapper",
      targets: ["EmojiMapper"])
  ],
  dependencies: [
    .package(url: "https://github.com/Alamofire/Alamofire.git", .upToNextMajor(from: "5.4.0")),
    .package(path: "../ResourcesUI"),  // πŸ‘ˆ  Reference to a Local Package
  ],
  targets: [
    .target(
      name: "EmojiMapper",
      dependencies: [
        .product(name: "Alamofire", package: "Alamofire"),
        .product(name: "ResourcesUI", package: "ResourcesUI"), // πŸ‘ˆ  Reference to a Local Package
      ]),
    .testTarget(
      name: "EmojiMapperTests",
      dependencies: ["EmojiMapper"])
  ]
)

Access control

By using local packages, it forces you to think more about the access control of your classes, structs.

By default, only what is declared as public will be accessible from another module, so this forces us to manage which models should be public and which ones should be kept hidden.

A common case is the initializers of the structures that swift assigns us automatically.

By default this init is internal, so if we want to use the structure outside of our module, we will have to declare an explicit initializer.

public struct LoggerModel {
  let caller: String
  let someValue: Bool
  
  // 🚫 Default init I'ts not accessible from another package
}

public struct LoggerModel {
  let caller: String
  let someValue: Bool

  // βœ… This init should be declared public
  public init(caller: String, someValue: Bool) {
    self.caller = caller
    self.someValue = someValue
  }
}

Accessing Assets from a Package

SPM automatically generates an extension with the variable β€œmodule”, which makes it easier to access resources within it.

let image = UIImage(named: "home", in: .module, compatibleWith: nil)

How inject dependencies into my package?

This forces me to think about designing in a way where I can work on my package logic regardless of dependencies.

For example we have a Logger package that we want to use within our EmojiMapper package.

To achieve this we declare a protocol and a concrete class.

The idea is that EmojiMapper depends on the protocol instead of the specific class.

Because the interfaces of a module change less frequently than the implementations, this allows me to achieve some abstraction, favoring decoupling and the advantages that this brings.

public protocol LoggerProtocol {
  func log(caller: String, message: String)
}

public class ConcreteLogger: LoggerProtocol {
  public func log(caller: String, message: String) {
    // Do some stuff
  }
}

Packages as Third Party Dependency Wrappers

It is common for some packages to end up becoming a kind of wrapper for some third-party dependency.

This is a common practice to avoid using a dependency throughout the entire application and having it centralized at a single point.

Although it is true it could not be applicable for dependencies of the magnitude like RxSwift or IGLisKit, if we could do it for dependencies where we depend on a few methods.

The idea is to create your own protocol and encapsulate the dependency within a specific class of our protocol.

Now my packages or my main app would depend on my protocol, well actually if it depends on the third party dependency but only at runtime.

Ideas for your own Packages

Here are some ideas for creating your own packages:

  • Analytics
  • Logging
  • Persistence (Realm Wrapper or CoreData
  • Image Manager
  • UI
  • Networking
  • Feature Package (Wrapper around certain functionality of your app, Login, Onboarding, Home, Account, Settings)

Conclusion

I invite you to create your first package.

Think of a package as certain functionality that can naturally be grouped in one place, abstracting the functionality within a module and exposing it through a simple interface, easy to use, mock up and test.

Read more: