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. You can find the finished package here.

I will evolve the package with support for CocoaPods, Carthage, Fastlane and Bitrise etc. in upcoming posts. At the end of this post, we’ll have a fully functional package, but it will lack some features that every decent open source project should have.

Background

I have several open source projects and was very excited when Apple last WWDC announced that SPM was coming to iOS. CocoaPods and Carthage are great dependency managers, but have problems that SPM has the potential to solve, since it’s integrated into Xcode.

SPM is young and has many childhood problems and limitations. As the package evolves and you add more features, like demo projects, support for 3rd party dependency managers, CI integrations etc. you’ll run into some nasty problems. However, in more basic setups, SPM will let you create a nice, useful package quickly and with minimum hassle.

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.

Create the package

Let’s start by creating a Swift package. In the Terminal, create a new folder for the package, navigate to it and create the 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 here
  • Tests - 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

However, since the package will use gestures, it’s not really applicable for tvOS and macOS either, so let’s specify that it can only be used on platforms that support SwiftUI and swipe gestures - iOS 13 and later.

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 the class in which all the swipe gesture logic will go - SwipeGesture.

In SwiftUI, we can build this gesture as a view modifier, a function etc. but since we’re going use UISwipeGestureRecognizers and UIViews, let’s build it as a View instead, or rather as a UIViewRepresentable, which will wrap a UIView in a SwiftUI 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) {}
}

This view should trigger individual actions when it’s swiped in different directions. Let’s define such an action as a typealias:

import Foundation

public extension SwipeGesture {
    
    typealias Action = () -> Void
}

Since SwipeGesture should support four swipe directions (up, down, left, right), let’s create a struct that can contain four distinct actions:

import Foundation

public extension SwipeGesture {
    
    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
    }
}

Now, let’s add a SwipeGesture initializer that lets people define actions when the gesture is created. Let’s also add empty default values to reduce the amount of code people have to write when they only want to use one or some of these actions:

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

    ...
}

Now, let’s create the content of our SwipeGesture, which will be a UIView with gestures added to it. Let’s create it 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 to be a view per se, but rather a swipe gesture capture area that can be added to any other view.

It’s time to add gestures to the view. Since we have a regular UIView, we can do it by calling addGestureRecognizer on the view. There’s only one problem. addGestureRecognizer requires a target and a selector, and since SwipeGesture is a struct, we can’t use it as target or in the selector.

To fix this, we can use a coordinator. Let’s define a nested Coordinator class 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)
    }
}

The coordinator takes a swipe gesture as init argument and defines four selector-compatible functions that call each action of the gesture. By passing in itself in makeCoordinator, the function’s context will now have a coordinator of the above type.

We can now use this coordinator as target and selector when we add gestures to the view. Let’s first create a UIView extension that simplifies adding gestures to the 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 up, 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 very nice (nor declarative) compared to SwiftUI’s onTapGesture and onLongPressGesture. Let’s create such a 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)
    }
}

In the code above, we create a View extension that creates a GestureView and adds it as an overlay to the view. You 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") }
    )
)

That’s it! We now have a fully functional swipe gesture view. However, we won’t be able to try it out until we have a demo project. Until then, let’s do something even more fun - unit testing.

Add unit tests

I use Quick and 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 to see that we can make the tests run. Let’s unit test the UIView+Gestures extension that 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 the following 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.

Now press Cmd+U to launch the test runner. It should run without problems and the test should pass. As a good tester, you know that you always need more tests, but that’s for another time.

Publish the package

The last part now is to publish the package. This can easily be as much work as creating the package in the first place, since you should put effort into:

  • A great readme to explain to people what your package is all about.
  • Documentation to make it easy for people to use your package.
  • A stunning logo to…well, you don’t have to, but it’s fun.

For now, though, just push it up to GitHub so that we can try it our in a real Xcode project. You can find my version of the package here.

Add the package to a new app

Let’s create a new iOS app and see if the package works. Create a SwiftUI project and add an SPM dependency under Project/Swift Packages. If you haven’t published the package, you can use this url instead: https://github.com/danielsaidi/SwiftUIGestures.

After Xcode has synced SPM dependencies, you can add import SwiftUIGestures to ContentView and add this code to it:

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 now 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:

  • CocoaPods support
  • Carthage support
  • A demo app
  • Fastlane support
  • Bitrise integration
  • Terminal support for unit tests

I will expand the package further with these features in upcoming posts and will add links to the list above as I do. Until then, I hope that you enjoyed this article. Feel free to leave comments and feedback in the discussion section below.