Creating an SPM Package for SwiftUI
In this post, we’ll create a package for the Swift Package Manager. The result will be a package that adds more gestures to SwiftUI.
Background
I have several open source projects and was excited when Apple last WWDC announced that SPM was coming to iOS. CocoaPods and Carthage are great dependency managers, but have problems that SPM can solve since it’s integrated into Xcode.
SPM is young and has some childhood problems. As your package evolves and you add a demo project, support for 3rd party dependency managers, CI integrations, etc. SPM may provide you with some unexpected problems.
Therefore, let’s focus on the most essential features in this post. Let’s save these problems for later, and take them on as we add more features to the package.
Creating the package
Let’s start by creating a Swift package. In Terminal, create a new folder for the package, navigate to it, then create a new package within it:
mkdir SwiftUIGestures
cd SwiftUIGestures
swift package init
This will create a couple of files and folders:
.gitignore
- a standard, but badly configured ignore file. Read more here.Package.swift
- a manifest file that describes your package.Readme.md
- a basic readme for the package.Sources
- Source files go hereTests
- Unit tests go here
Unlike apps, this is a package and not an Xcode project. If you ask Xcode to open the folder, it will open the package, which has a different structure than Xcode projects.
Before we add functionality to the package, let’s remove two files that are generated by SPM and that we will not use: SwiftUIGestures.swift
and SwiftUIGesturesTests.swift
.
Specify supported platforms
Since the package will use SwiftUI, we can only target iOS 13, tvOS 13, watchOS 6 and macOS 10.10 or later. If we don’t, we would get the following build error:
'...' is only available in iOS 13.0 or newer
Since the package will use gestures, it’s not really applicable for tvOS and macOS, so let’s specify that it can only be used on platforms that support SwiftUI and swipe gestures.
Open Package.json
and add the following code below name
:
platforms: [
.iOS(.v13)
]
This tells the package that it can only be used on iOS 13 and later. Save the file, let Xcode refresh the package and you will now be able to build for SwiftUI.
Create a swipe gesture
It’s time to start adding functionality to the package. Let’s start by creating a class in which all the swipe gesture logic will go - SwipeGesture
.
In SwiftUI, we could build this as a view modifier, a function etc. but since we’re going use UISwipeGestureRecognizer
and UIViewRepresentable
, which will wrap a UIView
in a View
:
import SwiftUI
import UIKit
public struct SwipeGesture: UIViewRepresentable {
typealias Context = UIViewRepresentableContext<SwipeGesture>
public func makeUIView(context: Context) -> UIView {
// TODO: Coming soon
}
public func updateUIView(_ uiView: UIView, context: Context) {}
}
Since SwipeGesture
should support four swipe directions (up, down, left, right), let’s create a struct that can contain four distinct actions, with an Action
typealias:
import Foundation
public extension SwipeGesture {
typealias Action = () -> Void
struct Actions {
init(
up: @escaping Action = {},
left: @escaping Action = {},
right: @escaping Action = {},
down: @escaping Action = {}
) {
self.up = up
self.left = left
self.right = right
self.down = down
}
let up: Action
let left: Action
let right: Action
let down: Action
}
}
Let’s add an initializer that lets people define actions when creating a SwipeGesture
, using empty default values to reduce the amount of actions that people must provide:
public struct SwipeGesture: UIViewRepresentable {
init(
up: @escaping Action = {},
left: @escaping Action = {},
right: @escaping Action = {},
down: @escaping Action = {}
) {
self.actions = Actions(
up: up,
left: left,
right: right,
down: down
)
}
private let actions: Actions
...
}
Let’s create the content of our SwipeGesture
, which will be a UIView
with gestures. Let’s create this view inside makeUIView
:
public func makeUIView(context: Context) -> UIView {
let view = UIView(frame: .zero)
view.backgroundColor = .clear
// TODO: Add gestures to the view
return view
}
The view has a clear background and no content, since it should not be considered a view, but rather a swipe gesture capture area that can be added to any other view.
It’s time to add gestures. Since we have a UIView
, we can use addGestureRecognizer
, but it requires a target and selector. Since SwipeGesture
is a struct, we can’t use it as a target.
To fix this, let’s define a Coordinator
inside SwipeGesture
and return it in makeCoordinator
:
public extension SwipeGesture {
class Coordinator: NSObject {
public init(gesture: SwipeGesture) {
self.gesture = gesture
}
private let gesture: SwipeGesture
@objc public func swipeLeft() { gesture.actions.left() }
@objc public func swipeRight() { gesture.actions.right() }
@objc public func swipeUp() { gesture.actions.up() }
@objc public func swipeDown() { gesture.actions.down() }
}
func makeCoordinator() -> SwipeGesture.Coordinator {
Coordinator(gesture: self)
}
}
Returning this coordinator in makeCoordinator
causes the makeUIView
context
to get a coordinator
of this type. This lets us set the target and selectors with this coordinator.
Before we proceed, let’s add a UIView
extension that simplifies adding gestures to a view:
extension UIView {
func addSwipeGesture(
_ direction: UISwipeGestureRecognizer.Direction,
target: Any?,
action: Selector
) {
let swipe = UISwipeGestureRecognizer(target: target, action: action)
swipe.direction = direction
addGestureRecognizer(swipe)
}
}
We can now use this extension to easily add gestures in makeUIView
:
public func makeUIView(context: Context) -> UIView {
let coord = context.coordinator
let view = UIView(frame: .zero)
view.backgroundColor = .clear
view.addSwipeGesture(.up, target: coord, action: #selector(coord.swipeUp))
view.addSwipeGesture(.left, target: coord, action: #selector(coord.swipeLeft))
view.addSwipeGesture(.right, target: coord, action: #selector(coord.swipeRight))
view.addSwipeGesture(.down, target: coord, action: #selector(coord.swipeDown))
return view
}
That’s it! We now have a SwiftUI view that can be used to capture swipe gestures on any view it is added to. Next, let’s add this swipe gesture to a SwiftUI View
.
Add swipe gesture to any SwiftUI View
Since SwipeGesture
is a View
, you could just add it as an overlay
to any other view:
Color.red
.overlay(
SwipeGesture(
up: { print("UP") },
left: { print("LEFT") },
right: { print("RIGHT") },
down: { print("DOWN") }
)
)
)
However, this is not nice compared to SwiftUI’s onTapGesture
and onLongPressGesture
.
Let’s create a similar view modifier for this gesture as well:
import SwiftUI
extension View {
func onSwipeGesture(
up: @escaping SwipeGesture.Action = {},
left: @escaping SwipeGesture.Action = {},
right: @escaping SwipeGesture.Action = {},
down: @escaping SwipeGesture.Action = {}
) -> some View {
let gesture = SwipeGesture(up: up, left: left, right: right, down: down)
return overlay(gesture)
}
}
With this code, we can now add a SwipeGesture
to any View
like this:
Color.red
.onSwipeGesture(
up: { print("UP") },
left: { print("LEFT") },
right: { print("RIGHT") },
down: { print("DOWN") }
)
)
We now have a fully functional swipe gesture view. Before we try it out in a demo app, let’s do something even more fun - unit testing!
Add unit tests
I use Quick & Nimble for unit testing in most of my projects, but let’s reduce the complexity of this post by using XCTests
.
Let’s add a tiny test that tests the UIView+Gestures
extension we defined earlier:
import UIKit
extension UIView {
func addGesture(_ direction: UISwipeGestureRecognizer.Direction, target: Any?, action: Selector) {
let swipe = UISwipeGestureRecognizer(target: target, action: action)
swipe.direction = direction
addGestureRecognizer(swipe)
}
}
In Tests/SwiftUIGestureTests
, create a UIView+GesturesTests.swift
file with this code:
import XCTest
@testable import SwiftUIGestures
final class UIView_GesturesTests: XCTestCase {
func testVisualEffectViewIsCorrectlyAdded() {
let view = UIView()
let obj = TestClass()
view.addGesture(.up, target: obj, action: #selector(obj.doStuff))
let gestures = view.gestureRecognizers
XCTAssertEqual(gestures?.count, 1)
XCTAssertTrue(gestures?[0] is UISwipeGestureRecognizer)
}
}
private class TestClass: NSObject {
@objc func doStuff() {}
}
We need the @testable import
since the extension is internal and can only be accessed within the library, as well as in testable imports.
Press Cmd+U
to launch the test runner. It should run without problems and all tests pass.
Publishing the package
The last part is to publish the package. This can easily involve as much work as creating the package, since you should put effort into:
A great name
that communicates what it’s all about.A great readme
that explains what it does, how it’s used etc.A stunning logo
to…well, you don’t have to, but it’s pretty fun.A basic demo app
to make it easy for users to test your package.
For now, though, just push it up to GitHub so that we can try it our in a real Xcode project. You can then add it with SPM, using the branch name, since we don’t have a version yet.
Adding the package to a new app
Let’s create a new iOS app test the package. Create a SwiftUI project and navigate to the SPM Dependency Manager under Project/Swift Packages
.
Add a new dependency and enter the url to your repo. If you haven’t created a repo of your own, you can use this url instead: https://github.com/danielsaidi/SwiftUIKit
.
After Xcode has synced dependencies, you can import
the library and use it in your app:
import YourPackageName // or SwiftUIKit if you use my library
...
Color.red
.onSwipeGesture(
up: { print("UP") },
left: { print("LEFT") },
right: { print("RIGHT") },
down: { print("DOWN") }
)
)
If you run the project and swipe the red part of the screen, you should get a printout of the direction you swiped. This means that everything worked well and that the package works.
Congratulations - you have just created your first SPM Package! 🥳
Going further
You now have a working SPM package, but it still lacks a bunch of features, like the things I mentioned earlier list, as well as other useful workflow utilities.
You can have a look at my various projects to see how I generally handle this.