Geofencing in iOS with Core Location Swift 3

Setting Up a Location Manager and Permissions

Before any geofence monitoring can happen, though, you need to set up a Location Manager instance and request the appropriate permissions.

Open GeotificationsViewController.swift and declare a constant instance of a CLLocationManager near the top of the class, as shown below:

class GeotificationsViewController: UIViewController {

  @IBOutlet weak var mapView: MKMapView!

  var geotifications = [Geotification]()
  let locationManager = CLLocationManager() // Add this statement

  ...
}

Next, replace viewDidLoad() with the following code:

override func viewDidLoad() {
  super.viewDidLoad()
  // 1
  locationManager.delegate = self
  // 2
  locationManager.requestAlwaysAuthorization()
  // 3
  loadAllGeotifications()
}

Let’s run through this method step by step:

  1. You set the view controller as the delegate of the locationManager instance so that the view controller can receive the relevant delegate method calls.
  2. You make a call to requestAlwaysAuthorization(), which invokes a prompt to the user requesting for Always authorization to use location services. Apps with geofencing capabilities need Always authorization, due to the need to monitor geofences even when the app isn’t running. Info.plist has already been setup with a message to show the user when requesting the user’s location under the key NSLocationAlwaysUsageDescription.
  3. You call loadAllGeotifications(), which deserializes the list of geotifications previously saved to NSUserDefaults and loads them into a local geotifications array. The method also loads the geotifications as annotations on the map view.

When the app prompts the user for authorization, it will show NSLocationAlwaysUsageDescription, a user-friendly explanation of why the app requires access to the user’s location. This key is mandatory when you request authorization for location services. If it’s missing, the system will ignore the request and prevent location services from starting altogether.

Build and run the project, and you’ll see a user prompt with the aforementioned description that’s been set:

You’ve set up your app to request the required permission. Great! Click or tap Allow to ensure the location manager will receive delegate callbacks at the appropriate times.

Before you proceed to implement the geofencing, there’s a small issue you have to resolve: the user’s current location isn’t showing up on the map view! This feature is disabled, and as a result, the zoom button on the top-left of the navigation bar doesn’t work.

Fortunately, the fix is not difficult — you’ll simply enable the current location only after the app is authorized.

In GeotificationsViewController.swift, add the following delegate method to the CLLocationManagerDelegate extension:

extension GeotificationsViewController: CLLocationManagerDelegate {
  func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
    mapView.showsUserLocation = (status == .authorizedAlways)
  }
}

The location manager calls locationManager(_:didChangeAuthorizationStatus:) whenever the authorization status changes. If the user has already granted the app permission to use Location Services, this method will be called by the location manager after you’ve initialized the location manager and set its delegate.

That makes this method an ideal place to check if the app is authorized. If it is, you enable the map view to show the user’s current location.

Build and run the app. If you’re running it on a device, you’ll see the location marker appear on the main map view. If you’re running on the simulator, click Debug\Location\Apple in the menu to see the location marker:

In addition, the zoom button on the navigation bar now works. :]

Registering Your Geofences

With the location manager properly configured, the next order of business is to allow your app to register user geofences for monitoring.

In your app, the user geofence information is stored within your custom Geotification model. However, Core Location requires each geofence to be represented as a CLCircularRegion instance before it can be registered for monitoring. To handle this requirement, you’ll create a helper method that returns a CLCircularRegion from a given Geotification object.

Open GeotificationsViewController.swift and add the following method to the main body:

func region(withGeotification geotification: Geotification) -> CLCircularRegion {
  // 1
  let region = CLCircularRegion(center: geotification.coordinate, radius: geotification.radius, identifier: geotification.identifier)
  // 2
  region.notifyOnEntry = (geotification.eventType == .onEntry)
  region.notifyOnExit = !region.notifyOnEntry
  return region
}

Here’s what the above method does:

  1. You initialize a CLCircularRegion with the location of the geofence, the radius of the geofence and an identifier that allows iOS to distinguish between the registered geofences of a given app. The initialization is rather straightforward, as the Geotification model already contains the required properties.
  2. The CLCircularRegion instance also has two Boolean properties, notifyOnEntry and notifyOnExit. These flags specify whether geofence events will be triggered when the device enters and leaves the defined geofence, respectively. Since you’re designing your app to allow only one notification type per geofence, you set one of the flags to true while you set the other to false, based on the enum value stored in the Geotification object.

Next, you need a method to start monitoring a given geotification whenever the user adds one.

Add the following method to the body of GeotificationsViewController:

func startMonitoring(geotification: Geotification) {
  // 1
  if !CLLocationManager.isMonitoringAvailable(for: CLCircularRegion.self) {
    showAlert(withTitle:"Error", message: "Geofencing is not supported on this device!")
    return
  }
  // 2
  if CLLocationManager.authorizationStatus() != .authorizedAlways {
    showAlert(withTitle:"Warning", message: "Your geotification is saved but will only be activated once you grant Geotify permission to access the device location.")
  }
  // 3
  let region = self.region(withGeotification: geotification)
  // 4
  locationManager.startMonitoring(for: region)
}

Let’s walk through the method step by step:

  1. isMonitoringAvailableForClass(_:) determines if the device has the required hardware to support the monitoring of geofences. If monitoring is unavailable, you bail out entirely and alert the user accordingly. showSimpleAlertWithTitle(_:message:viewController) is a helper function in Utilities.swift that takes in a title and message and displays an alert view.
  2. Next, you check the authorization status to ensure that the app has also been granted the required permission to use Location Services. If the app isn’t authorized, it won’t receive any geofence-related notifications.However, in this case, you’ll still allow the user to save the geotification, since Core Location lets you register geofences even when the app isn’t authorized. When the user subsequently grants authorization to the app, monitoring for those geofences will begin automatically.
  3. You create a CLCircularRegion instance from the given geotification using the helper method you defined earlier.
  4. Finally, you register the CLCircularRegion instance with Core Location for monitoring.

With your start method done, you also need a method to stop monitoring a given geotification when the user removes it from the app.

In GeotificationsViewController.swift, add the following method below startMonitoringGeotificiation(_:):

func stopMonitoring(geotification: Geotification) {
  for region in locationManager.monitoredRegions {
    guard let circularRegion = region as? CLCircularRegion, circularRegion.identifier == geotification.identifier else { continue }
    locationManager.stopMonitoring(for: circularRegion)
  }
}

The method simply instructs the locationManager to stop monitoring the CLCircularRegion associated with the given geotification.

Now that you have both the start and stop methods complete, you’ll use them whenever you add or remove a geotification. You’ll begin with the adding part.

First, take a look at addGeotificationViewController(_:didAddCoordinate) in GeotificationsViewController.swift.

The method is the delegate call invoked by the AddGeotificationViewController upon creating a geotification; it’s responsible for creating a new Geotification object using the values passed from AddGeotificationsViewController, and updating both the map view and the geotifications list accordingly. Then you call saveAllGeotifications(), which takes the newly-updated geotifications list and persists it via NSUserDefaults.

Now, replace the method with the following code:

func addGeotificationViewController(controller: AddGeotificationViewController, didAddCoordinate coordinate: CLLocationCoordinate2D, radius: Double, identifier: String, note: String, eventType: EventType) {
  controller.dismiss(animated: true, completion: nil)
  // 1
  let clampedRadius = min(radius, locationManager.maximumRegionMonitoringDistance)
  let geotification = Geotification(coordinate: coordinate, radius: clampedRadius, identifier: identifier, note: note, eventType: eventType)
  add(geotification: geotification)
  // 2
  startMonitoring(geotification: geotification)
  saveAllGeotifications()
}

You’ve made two key changes to the code:

  1. You ensure that the value of the radius is clamped to the maximumRegionMonitoringDistance property of locationManager, which is defined as the largest radius in meters that can be assigned to a geofence. This is important, as any value that exceeds this maximum will cause monitoring to fail.
  2. You add a call to startMonitoringGeotification(_:) to ensure that the geofence associated with the newly-added geotification is registered with Core Location for monitoring.

At this point, the app is fully capable of registering new geofences for monitoring. There is, however, a limitation: As geofences are a shared system resource, Core Location restricts the number of registered geofences to a maximum of 20 per app.

While there are workarounds to this limitation (See Where to Go From Here? for a short discussion), for the purposes of this tutorial, you’ll take the approach of limiting the number of geotifications the user can add.

 

Finally, let’s deal with the removal of geotifications. This functionality is handled in mapView(_:annotationView:calloutAccessoryControlTapped:), which is invoked whenever the user taps the “delete” accessory control on each annotation.

Add a call to stopMonitoring(geotification:) to mapView(_:annotationView:calloutAccessoryControlTapped:), as shown below:

func mapView(_ mapView: MKMapView, annotationView view: MKAnnotationView, calloutAccessoryControlTapped control: UIControl) {
  // Delete geotification
  let geotification = view.annotation as! Geotification
  stopMonitoring(geotification: geotification)   // Add this statement
  removeGeotification(geotification)
  saveAllGeotifications()
}

The additional statement stops monitoring the geofence associated with the geotification, before removing it and saving the changes to NSUserDefaults.

At this point, your app is fully capable of monitoring and un-monitoring user geofences. Hurray!

Build and run the project. You won’t see any changes, but the app will now be able to register geofence regions for monitoring. However, it won’t be able to react to any geofence events just yet. Not to worry—that will be your next order of business!

Reacting to Geofence Events

You’ll start by implementing some of the delegate methods to facilitate error handling – these are important to add in case anything goes wrong.

In GeotificationsViewController.swift, add the following methods to the CLLocationManagerDelegateextension:

func locationManager(_ manager: CLLocationManager, monitoringDidFailFor region: CLRegion?, withError error: Error) {
  print("Monitoring failed for region with identifier: \(region!.identifier)")
}

func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
  print("Location Manager failed with the following error: \(error)")
}

These delegate methods simply log any errors that the location manager encounters to facilitate your debugging.

Note: You’ll definitely want to handle these errors more robustly in your production apps. For example, instead of failing silently, you could inform the user what went wrong.

Next, open AppDelegate.swift; this is where you’ll add code to properly listen and react to geofence entry and exit events.

Add the following line at the top of the file to import the CoreLocation framework:

import CoreLocation

Ensure that the AppDelegate has a CLLocationManager instance near the top of the class, as shown below:

class AppDelegate: UIResponder, UIApplicationDelegate {
  var window: UIWindow?

  let locationManager = CLLocationManager() // Add this statement
  ...
}

Replace application(_:didFinishLaunchingWithOptions:) with the following implementation:

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]? = nil) -> Bool {
  locationManager.delegate = self
  locationManager.requestAlwaysAuthorization()
  return true
}

You’ve set up your AppDelegate to receive geofence-related events. But you might wonder, “Why did I designate the AppDelegate to do this instead of the view controller?”

Geofences registered by an app are monitored at all times, including when the app isn’t running. If the device triggers a geofence event while the app isn’t running, iOS automatically relaunches the app directly into the background. This makes the AppDelegate an ideal entry point to handle the event, as the view controller may not be loaded or ready.

Now you might also wonder, “How will a newly-created CLLocationManager instance be able to know about the monitored geofences?”

It turns out that all geofences registered by your app for monitoring are conveniently accessible by all location managers in your app, so it doesn’t matter where the location managers are initialized. Pretty nifty, right? :]

Now all that’s left is to implement the relevant delegate methods to react to the geofence events. Before you do so, you’ll create a method to handle a geofence event.

Add the following method to AppDelegate.swift:

func handleEvent(forRegion region: CLRegion!) {
  print("Geofence triggered!")
}

At this point, the method takes in a CLRegion and simply logs a statement. Not to worry—you’ll implement the event handling later.

Next, add the following delegate methods in the CLLocationManagerDelegate extension of AppDelegate.swift, as well as a call to the handleRegionEvent(_:) function you just created, as shown in the code below:

extension AppDelegate: CLLocationManagerDelegate {
  
  func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) {
    if region is CLCircularRegion {
      handleEvent(forRegion: region)
    }
  }
  
  func locationManager(_ manager: CLLocationManager, didExitRegion region: CLRegion) {
    if region is CLCircularRegion {
      handleEvent(forRegion: region)
    }
  }
}

As the method names aptly suggest, you fire locationManager(_:didEnterRegion:) when the device enters a CLRegion, while you fire locationManager(_:didExitRegion:) when the device exits a CLRegion.

Both methods return the CLRegion in question, which you need to check to ensure it’s a CLCircularRegion, since it could be a CLBeaconRegion if your app happens to be monitoring iBeacons, too. If the region is indeed a CLCircularRegion, you accordingly call handleRegionEvent(_:).

Note: A geofence event is triggered only when iOS detects a boundary crossing. If the user is already within a geofence at the point of registration, iOS won’t generate an event. If you need to query whether the device location falls within or outside a given geofence, Apple provides a method called requestStateForRegion(_:).

Now that your app is able to receive geofence events, you’re ready to give it a proper test run. If that doesn’t excite you, it really ought to, because for the first time in this tutorial, you’re going to see some results. :]

The most accurate way to test your app is to deploy it on your device, add some geotifications and take the app for a walk or a drive. However, it wouldn’t be wise to do so right now, as you wouldn’t be able to verify the print logs emitted by the geofence events with the device unplugged. Besides, it would be nice to get assurance that the app works before you commit to taking it for a spin.

Fortunately, there’s an easy way do this without leaving the comfort of your home. Xcode lets you include a hardcoded waypoint GPX file in your project that you can use to simulate test locations. Lucky for you, the starter project includes one for your convenience. :]

Open up TestLocations.gpx, which you can find in the Supporting Files group, and inspect its contents. You’ll see the following:

<?xml version="1.0"?>
<gpx version="1.1" creator="Xcode">
  <wpt lat="37.422" lon="-122.084058">
    <name>Google</name>
  </wpt>
  <wpt lat="37.3270145" lon="-122.0310273">
    <name>Apple</name>
  </wpt>
</gpx>

The GPX file is essentially an XML file that contains two waypoints: Google’s Googleplex in Mountain View and Apple’s Headquarters in Cupertino.

To begin simulating the locations in the GPX file, build and run the project. When the app launches the main view controller, go back to Xcode, select the Location icon in the Debug bar and choose TestLocations:

Back in the app, use the Zoom button on the top-left of the navigation bar to zoom to the current location. Once you get close to the area, you’ll see the location marker moving repeatedly from the Googleplex to Apple, Inc. and back.

Test the app by adding a few geotifications along the path defined by the two waypoints. If you added any geotifications earlier in the tutorial before you enabled geofence registration, those geotifications will obviously not work, so you might want to clear them out and start afresh.

For the test locations, it’s a good idea to place a geotification roughly at each waypoint. Here’s a possible test scenario:

  • Google: Radius: 1000m, Message: “Say Bye to Google!”, Notify on Exit
  • Apple: Radius: 1000m, Message: “Say Hi to Apple!”, Notify on Entry

Once you’ve added your geotifications, you’ll see a log in the console each time the location marker enters or leaves a geofence. If you activate the home button or lock the screen to send the app to the background, you’ll also see the logs each time the device crosses a geofence, though you obviously won’t be able to verify that behavior visually.

Note: Location simulation works both in iOS Simulator and on a real device. However, the iOS Simulator can be quite inaccurate in this case; the timings of the triggered events do not coincide very well with the visual movement of the simulated location in and out of each geofence. You would do better to simulate locations on your device, or better still, take the app for a walk!

Notifying the User of Geofence Events

You’ve made a lot of progress with the app. At this point, it simply remains for you to notify the user whenever the device crosses the geofence of a geotification—so prepare yourself to do just that.

To obtain the note associated with a triggering CLCircularRegion returned by the delegate calls, you need to retrieve the corresponding geotification that was persisted in NSUserDefaults. This turns out to be trivial, as you can use the unique identifier you assigned to the CLCircularRegion during registration to find the right geotification.

In AppDelegate.swift, add the following helper method at the bottom of the class:

func note(fromRegionIdentifier identifier: String) -> String? {
  let savedItems = UserDefaults.standard.array(forKey: PreferencesKeys.savedItems) as? [NSData]
  let geotifications = savedItems?.map { NSKeyedUnarchiver.unarchiveObject(with: $0 as Data) as? Geotification }
  let index = geotifications?.index { $0?.identifier == identifier }
  return index != nil ? geotifications?[index!]?.note : nil
}

This helper method retrieves the geotification note from the persistent store, based on its identifier, and returns the note for that geotification.

Now that you’re able to retrieve the note associated with a geofence, you’ll write code to trigger a notification whenever a geofence event is fired, using the note as the message.

Add the following statements to the end of application(_:didFinishLaunchingWithOptions:), just before the method returns:

application.registerUserNotificationSettings(UIUserNotificationSettings(types: [.sound, .alert, .badge], categories: nil))
UIApplication.shared.cancelAllLocalNotifications()

The code you’ve added prompts the user for permission to enable notifications for this app. In addition, it does some housekeeping by clearing out all existing notifications.

Next, replace the contents of handleRegionEvent(_:) with the following:

func handleEvent(forRegion region: CLRegion!) {
  // Show an alert if application is active
  if UIApplication.shared.applicationState == .active {
    guard let message = note(fromRegionIdentifier: region.identifier) else { return }
    window?.rootViewController?.showAlert(withTitle: nil, message: message)
  } else {
    // Otherwise present a local notification
    let notification = UILocalNotification()
    notification.alertBody = note(fromRegionIdentifier: region.identifier)
    notification.soundName = "Default"
    UIApplication.shared.presentLocalNotificationNow(notification)
  }
}

If the app is active, the code above simply shows an alert controller with the note as the message. Otherwise, it presents a location notification with the same message.

Build and run the project, and run through the test procedure covered in the previous section. Whenever your test triggers a geofence event, you’ll see an alert controller displaying the reminder note:

Send the app to the background by activating the Home button or locking the device while the test is running. You’ll continue to receive notifications periodically that signal geofence events:

And with that, you have a fully functional, location-based reminder app in your hands. And yes, get out there and take that app for a spin!

Note: When you test the app, you may encounter situations where the notifications don’t fire exactly at the point of boundary crossing.

This is because before iOS considers a boundary as crossed, there is an additional cushion distance that must be traversed and a minimum time period that the device must linger at the new location. iOS internally defines these thresholds, seemingly to mitigate the spurious firing of notifications in the event the user is traveling very close to a geofence boundary.

In addition, these thresholds seem to be affected by the available location hardware capabilities. From experience, the geofencing behavior is a lot more accurate when Wi-Fi is enabled on the device.

Geofencing in iOS with Core Location Swift 3

Distributing iOS Frameworks Using CocoaPods

Creating a CocoaPod

CocoaPods is a popular dependency manager for iOS projects. It’s a tool for managing and versioning dependencies. Similar to a framework, a CocoaPod, or ‘pod’ for short, contains code, resources, etc., as well as metadata, dependencies, and set up for libraries. In fact, CocoaPods are built as frameworks that are included in the main app.

Anyone can contribute libraries and frameworks to the public repository, which is open to other iOS app developers. Almost all of the popular third-party frameworks, such as Alamofire, React native and SDWebImage, distribute their code as a pod.

Here’s why you should care: by making a framework into a pod, you give yourself a mechanism for distributing the code, resolving the dependencies, including and building the framework source, and easily sharing it with your organization or the wider development community.

Clean out the Project

Perform the following steps to remove the current link to ThreeRingControl.

  1. Select ThreeRingControl.xcodeproj in the project navigator and delete it.
  2. Choose Remove Reference in the confirmation dialog, since you’ll need to keep the files on disk to create the pod.

Install CocoaPods

If you’ve never used CocoaPods before, you’ll need to follow a brief installation process before going any further. Go to the CocoaPods Installation Guide and come back here when you’re finished. Don’t worry, we’ll wait!

Create the Pod

Open Terminal in the ThreeRingControl directory.

Run the following command:

pod spec create ThreeRingControl

This creates the file ThreeRingControl.podspec in the current directory. It’s a template that describes the pod and how to build it. Open it in a text editor.

The template contains plenty of comment descriptions and suggestions for the commonly used settings.

  1. Replace the Spec Metadata section with:
    s.name         = "ThreeRingControl"
    s.version      = "1.0.0"
    s.summary      = "A three-ring control like the Activity status bars"
    s.description  = "The three-ring is a completely customizable widget that can be used in any iOS app. It also plays a little victory fanfare."
    s.homepage     = "http://raywenderlich.com"
    

    Normally, the description would be a little more descriptive, and the homepage would point to a project page for the framework.

  2. Replace the Spec License section with the below code, as this iOS frameworks tutorial code uses an MIT License:
    s.license      = "MIT"
    
  3. You can keep the Author Metadata section as is, or set it u with how you’d lke to be credited and contacted.
  4. Replace the Platform Specifics section with the below code, because this is an iOS-only framework.:
    s.platform     = :ios, "10.0"
    
  5. Replace the Source Location with the below code. When you’re ready to share the pod, this will be a link to the GitHub repository and the commit tag for this version.
    s.source       = { :path => '.' }
    
  6. Replace the Source Code section with:
    s.source_files = "ThreeRingControl", "ThreeRingControl/**/*.{h,m,swift}"
    
  7. And the Resources section with:
    s.resources    = "ThreeRingControl/*.mp3"
    

    This will include the audio resources into the pod’s framework bundle.

  8. Remove the Project Linking and Project Settings sections.
  9. Add the following line. This line helps the application project understand that this pod’s code was written for Swift 3.
    s.pod_target_xcconfig = { 'SWIFT_VERSION' => '3' }
    
  10. Remove all the remaining comments — the lines that start with #.

You now have a workable development Podspec.

Note: If you run pod spec lint to verify the Podspec in Terminal, it’ll show an error because the source was not set to a valid URL. If you push the project to GitHub and fix that link, it will pass. However, having the linter pass is not necessary for local pod development. The Publish the Pod section below covers this.

Use the Pod

At this point, you’ve got a pod ready to rock and roll. Test it out by implementing it in the Phonercise app.

Back in Terminal, navigate up to the Phonercise directory, and then run the following command:

pod init

This steathily creates a new file named Podfile that lists all pods that the app uses, along with their versions and optional configuration information.

Open Podfile in a text editor.

Next, add the following line under the comment, # Pods for Phonercise:

pod 'ThreeRingControl', :path => '../ThreeRingControl'

Save the file.

Run this in Terminal:

pod install 

With this command, you’re searching the CocoaPods repository and downloading any new or updated pods that match the Podfile criteria. It also resolves any dependencies, updates the Xcode project files so it knows how to build and link the pods, and performs any other required configuration.

Finally, it creates a Phonercise.xcworkspace file. Use this file from now on — instead of the xcodeproj — because it has the reference to the Pods project and the app project.

Check it Out

Close the Phonercise and ThreeRingControl projects if they are open, and then open Phonercise.xcworkspace.

Build and run. Like magic, the app should work exactly the same before. This ease of use is brought to you by these two facts:

  1. You already did the work to separate the ThreeRingControl files and use them as a framework, e.g. adding import statements.
  2. CocoaPods does the heavy lifting of building and packaging those files; it also takes care of all the business around embedding and linking.

Pod Organization

Take a look at the Pods project, and you’ll notice two targets:

  • Pods-phonercise: a pod project builds all the individual pods as their own framework, and then combines them into one single framework: Pods-Phonercise.
  • ThreeRingControl: this replicates the same framework logic used for building it on its own. It even adds the music files as framework resources.

Inside the project organizer, you’ll see several groups. ThreeRingControl is under Development Pods. This is a development pod because you defined the pod with :path link in the app’s Podfile. These files are symlinked, so you can edit and develop this code side-by-side with the main app code.

Pods that come from a repository, such as those from a third party, are copied into the Pods directory and listed in a Pods group. Any modifications you make are not pushed to the repository and are overwritten whenever you update the pods.

Congratulations! You’ve now created and deployed a CocoaPod — and you’re probably thinking about what to pack into a pod first.

You’re welcome to stop here, congratulate yourself and move on to Where to Go From Here, but if you do, you’ll miss out on learning an advanced maneuver.

Publish the Pod

This section walks you through the natural next step of publishing the pod to GitHub and using it like a third party framework.

Create a Repository

If you don’t already have a GitHub account, create one.

Now create a new repository to host the pod. ThreeRingControl is the obvious best fit for the name, but you can name it whatever you want. Be sure to select Swift as the .gitignore language and MIT as the license.

Click Create Repository. From the dashboard page that follows, copy the HTTPS link.

Clone the Repository

Go back to Terminal and create a new directory. The following commands will create a repo directory from the project’s folder.

mkdir repo
cd repo

From there, clone the GitHub repository. Replace the URL below with the HTTPS link from the GitHub page.

git clone URL

This will set up a Git folder and go on to copy the pre-created README and LICENSE files.

Add the Code to the Repository

Next, copy the files from the previous ThreeRingControl directory to the repo/ThreeRingControl directory.

Open the copied version of ThreeRingControl.podspec, and update the s.source line to:

s.source       = { :git => "URL", :tag => "1.0.0" }

Set the URL to be the link to your repository.

Make the Commitment

Now it gets real. In this step, you’ll commit and push the code to GitHub. Your little pod is about to enter the big kid’s pool.

Run the following commands in Terminal to commit those files to the repository and push them back to the server.

cd ThreeRingControl/
git add .
git commit -m "initial commit"
git push

Visit the GitHub page and refresh it to see all the files.

Tag It

You need to tag the repository so it matches the Podspec. Run this command to set the tags:

git tag 1.0.0
git push --tags

Check your handiwork by running:

pod spec lint

The response you’re looking for is ThreeRingControl.podspec passed validation.

Update the Podfile

Change the Podfile in the Phonercise directory. Replace the existing ThreeRingControl line with:

pod 'ThreeRingControl', :git => 'URL', :tag => '1.0.0'

Replace URL with your GitHub link.

From Terminal, run this in the Phonercise directory:

pod update

Now the code will pull the framework from the GitHub repository, and it is no longer be a development pod!

Distributing iOS Frameworks Using CocoaPods

Creating iOS Frameworks

Have you ever wanted to share a chunk of code between two or more of your apps, or wanted to share a part of your program with other developers?

Maybe you wanted to modularize your code similarly to how the iOS SDK separates its API by functionality, or perhaps you want to distribute your code in the same way as popular 3rd parties do.

In this iOS frameworks tutorial you’ll learn how to do all of the above!

In iOS 8 and Xcode 6, Apple provided a new template, Cocoa Touch Framework. As you’ll see, it makes creating custom frameworks much easier than before.

Frameworks have three major purposes:

  • Code encapsulation
  • Code modularity
  • Code reuse

You can share your framework with your other apps, team members, or the iOS community. When combined with Swift’s access control, frameworks help define strong, testable interfaces between code modules.

In Swift parlance, a module is a compiled group of code that is distributed together. A framework is one type of module, and an app is another example.

In this iOS frameworks tutorial, you’ll extract a piece of an existing app and set it free, and by doing so, you’ll learn the ins and outs of frameworks by:

  • Creating a new framework for the rings widget
  • Migrating the existing code and tests
  • Importing the whole thing back into the app.
  • Packing it up as an uber-portable CocoaPods
  • Bonus: Setting up a repository for your framework

By the time you’re done, the app will behave exactly as it did before, but it will use the portable framework you developed! :]

Getting Started

Download the Phonercise Starter Project.

Phonercise is a simple application that replicates the Apple Watch Activity app, except it measures your phone’s physical activity. The three rings on the main view represent movement, standing and exercise.

To get the most out of the project, you’ll need to build and run on an actual iOS device, and turn on the volume. Go ahead now, build and run!

Move the phone around to get credit for movement, and “exercise” the phone by shaking it vigorously — that’ll get its heartrate up. To get credit for standing up, just hold the phone upright.

The logic for the app is pretty simple:

  • ActionViewController.swift contains the view lifecycle and motion logic.
  • All the view logic is in the files in the Three Ring View folder, where you’ll find ThreeRingView.swift, which handles the view, and Fanfare.swift, which handles the audio. The other files handle the custom drawing of the shading, gradient and shapes.

The ring controls are pretty sweet. They’ve got an addictive quality and they’re easy to understand. Wouldn’t it be nice to use them in a number of applications beyond this fun, but completely silly app? Frameworks to the rescue!

Creating a Framework

Frameworks are self-contained, reusable chunks of code and resources that you can import into any number of apps and even share across iOS, tvOS, watchOS, and macOS apps.

If you’ve programmed in other languages, you may have heard of node modules, packages, gems, jars, etc. Frameworks are the Xcode version of these. Some examples of common frameworks in the iOS SDK are: Foundation, UIKit, AVFoundation, CloudKit, etc.

Framework Set Up

In Xcode 6, Apple introduced the Cocoa Touch Framework template along with access control, so creating frameworks has never been easier. The first thing to do is to create the project for the framework.

    1. Create a new project. In Xcode, go to File/New/Project.
    2. Choose iOS/Framework & Library/Cocoa Touch Framework to create a new framework.
  1. Click Next.
  2. Set the Product Name to ThreeRingControl. Use your own Organization Name and Organization Identifier. Check Include Unit Tests. That’s right! You’re going to have automated tests ensure your framework is bug free
  3. Click Next.
  4. In the file chooser, choose to create the project at the same level as the Phonercise project.
  5. Click Create.

Now you have a project (albeit a boring one) that creates a framework!

Add Code and Resources

Your current state is a framework without code, and that is about as appealing as straight chocolate without sugar. In this section, you’ll put the pod in CocoaPods by adding the existing files to the framework.

From the Phonercise source directory, drag the following eight files into the ThreeRingControl project in Xcode:

  • CircularGradient.swift
  • coin07.mp3
  • Fanfare.swift
  • RingLayer.swift
  • RingTip.swift
  • ThreeRingView.swift
  • Utilities.swift
  • winning.mp3

Make sure to check Copy items if needed, so that the files actually copy into the new project instead of just adding a reference. Frameworks need their own code, not references, to be independent.

Double-check that each of the files has Target Membership in ThreeRingControl to make sure they appear in the final framework. You can see this in the File Inspector for each file.

Build the framework project to make sure that you get Build Succeeded with no build warnings or errors.

Add the Framework to the Project

Close the ThreeRingControl project, and go back to the Phonercise project. Delete the six files under the Three Ring View group as well as the two MP3 files in Helper Files. Select Move to Trash in the confirmation dialog.

Build the project, and you’ll see several predictable errors where Xcode complains about not knowing what the heck a ThreeRingView is. Well, you’ll actually see messages along the lines of “Use of undeclared type 'ThreeRingView'“, among others.

Adding the Three Ring Control framework project to the workspace is the solution to these problems.

Add the Framework to the Project

Right-click on the root Phonercise node in the project navigator. Click Add Files to “Phonercise”. In the file chooser, navigate to and select ThreeRingControl.xcodeproj. This will add ThreeRingControl.xcodeproj as a sub-project.

Note: It isn’t strictly necessary to add the framework project to the app project; you could just add the ThreeRingControl.framework output.

However, combining the projects makes it easier to develop both the framework and app simultaneously. Any changes you make to the framework project are automatically propagated up to the app. It also makes it easier for Xcode to resolve the paths and know when to rebuild the project.

Even though the two projects are now together in the workspace, Phonercise still doesn’t get ThreeRingControl. It’s like they’re sitting in the same room, but Phonercise can’t see the new framework.

Try linking the framework to the app’s target to fix this problem. First, expand the ThreeRingControl project to see the Products folder, and then look for for ThreeRingControl.framework beneath it. This file is the output of the framework project that packages up the binary code, headers, resources and metadata.

Select the top level Phonercise node to open the project editor. Click the Phonercise target, and then go to the General tab.

Scroll down to the Embedded Binaries section. Drag ThreeRingControl.framework from the Products folder of ThreeRingControl.xcodeproj onto this section.

You just added an entry for the framework in both Embedded Binaries and Linked Frameworks and Binaries.

Now the app knows about the framework and where to find it, so that should be enough, right?

Build the Phonercise project. More of the same errors.

Access Control

Your problem is that although the framework is part of the project, the project’s code doesn’t know about it — out of sight, out of mind.

Go to ActionViewController.swift, and add the following line to the list of imports at the top of the file.

import ThreeRingControl

It’s critical, but this inclusion won’t fix the build errors. This is because Swift uses access control to let you determine whether constructs are visible to other files or modules.

By default, Swift makes everything internal or visible only within its own module.

To restore functionality to the app, you have to update the access control on two Phonercise classes.

Although it’s a bit tedious, the process of updating access control improves modularity by hiding code not meant to appear outside the framework. You do this by leaving certain functions with no access modifier, or by explicitly declaring them internal.

Swift has three levels of access control. Use the following rules of thumb when creating your own frameworks:

  • Public: for code called by the app or other frameworks, e.g., a custom view.
  • Internal: for code used between functions and classes within the framework, e.g., custom layers in that view.
  • Fileprivate: for code used within a single file, e.g., a helper function that computes layout heights.
  • Private: for code used within an enclosing declaration, such as a single class block. Private code will not be visible to other blocks, such as extensions of that class, even in the same file, e.g., private variables, setters, or helper sub-functions.

Ultimately, making frameworks is so much easier than in the past, thanks to the new Cocoa Touch Framework template. Apple provided both of these in Xcode 6 and iOS 8.

Update the Code

When ThreeRingView.swift was part of the Phonercise app, internal access wasn’t a problem. Now that it’s in a separate module, it must be made public for the app to use it. The same is true of the code in Fanfare.swift.

Open ThreeRingView.swift inside of ThreeRingControlProject.

Make the class public by adding the public keyword to the class definition, like so:

public class ThreeRingView : UIView {

ThreeRingView will now be visible to any app file that imports the ThreeRingControl framework.

Add the public keyword to:

  • Both init functions
  • The variables RingCompletedNotification, AllRingsCompletedNotification, layoutSubviews()
  • Everything marked @IBInspectable — there will be nine of these

Note: You might wonder why you have to declare inits as public. Apple explains this and other finer points of access control in their Access Control Documentation.

The next step is to do essentially the same thing as you did for ThreeRingView.swift, and add the public keyword to the appropriate parts of Fanfare.swift. For your convenience, this is already done.

Note that the following variables are public: ringSound, allRingSound, and sharedInstance. The function playSoundsWhenReady() is public as well.

Now build and run. The good news is that the errors are gone, and the bad news is that you’ve got a big, white square. Not a ring in sight. Oh no! What’s going on?

Update the Storyboard

When using storyboards, references to custom classes need to have both the class name and module set in the Identity Inspector. At the time of this storyboard’s creation, ThreeRingView was in the app’s module, but now it’s in the framework.

Update the storyboard, as explained below, by telling it where to find the custom view; this will get rid of the white square.

  1. Open Main.Storyboard in the Phonercise project.
  2. Select the Ring Control in the view hierarchy.
  3. In the Identity Inspector, under Custom Class, change the Module to ThreeRingControl.

Once you set the module, Interface Builder should update and show the control in the editor area.

Build and run. Now you’ll get some rings.

Creating iOS Frameworks

Cocoa Touch Static Library vs. Cocoa Touch Framework

At AddThis we have the best geeks on the planet, and it’s our duty to share their knowledge and passion with the world. From time to time we hand the reins over to one of our teammates in a series we call, Talk Nerdy to Me.

Principal Software Engineer Kirk Elliott’s first post, 7 Things to Consider When Making iOS and Android Apps with Cordova or PhoneGap, got a lot of love, so we’re excited to have him back. Kirk enjoys DJing in the Baltimore Club Music scene, tidy code frameworks, and definitely being called Cappin’ Kirk by his co-workers.

  • Should you use a Framework or a Static Library?
  • Can you use Swift or Objective-C?
  • How can you export your final builds?
  • How will you provide instructions to use your library in a manner that will be easy to understand?

This post will attempt to answer these questions based on your project’s concerns and provide additional information and resources to make your static library or framework project go smoothly.

Know Your Supported Platforms

The first thing to know about is the difference between a Cocoa Touch Static Library (aka static library) and a Cocoa Touch Framework (aka framework). From a practical perspective, frameworks are only supported from iOS 8 onwards, whereas static libraries go “all the way back” to iOS 6, which is as far back as you can build an Xcode project using Xcode 7. If you need to support iOS versions before iOS 8, you should consider using a Cocoa Touch Static Library and writing your code in Objective-C.

Be Mindful of Swift Support

The next thing you should be aware of is that Cocoa Touch Static Libraries do not support Swift. So if you decide to use a Cocoa Touch Static Library, you need to use Objective-C. On that topic, Swift is supported back to iOS 7. If you need to support iOS 6 then Swift is definitely “off the table”. If you need to support iOS 7 and you want to use Swift, you will need to provide your source files to be included as raw files in the host app project. Otherwise if you only need to support iOS 8 and onwards, then you can create a Cocoa Touch Framework in Swift.

Plan Out Your Exporting

Platforms
The main thing to consider thus far is the base deployment target of apps that will be using your framework. If you are creating a framework for large scale distribution, you should be aware that many big publishers are still (at the time of this writing) supporting iOS 7 as a base deployment target. Generally the base deployment target is two versions below the latest one, so when iOS 10 is released (likely around 14-09-2016), we can expect the base to shift to iOS 8 for many publishers. At the time of writing this iOS 9 has 84% adoption, iOS 8 has 11%, and earlier versions of iOS comprise the remaining 5%.

Distribution
After you have built your framework, distribution will become your most important concern. Even though you have an Xcode project of type framework or static library, how do you provide your resource to others? Additionally, how do you provide instructions that are simple and easy to understand for the users of your product? If you are going to create a Cocoa Touch Framework, kodmunki has the best guide out there.

If you go with a Cocoa Touch Static Library instead, you should consider the Ray Wenderlich guide. Start with the section titled “Universal Frameworks”. Both of these are excellent guides that I have personally built frameworks from and these have been selected from all of the other pages on the Internet as the best sources have found to date.

In both of these approaches you will want to make sure that your Archive project target Build Settings has Build Active Architecture Only set to NO and Defines Module set to YES. Aside from that you should probably also define a User-Defined Build Setting of “BITCODE_GENERATION_MODE” with a value of “bitcode”, especially if you have bit code-related errors or warnings in your host app. It is not recommended “bitcode”, especially if you have bit code-related errors or warnings in your host app. It is not recommended that you disable bitcode generation in your host app in order to remove errors generated from your framework/static library product.

Prepare For Testing on Simulator

After you build and export your framework, it is important that you are able to include it in a host app for testing. You should be able to build and run on Simulator, physical device, and for “Generic iOS Device” without error. Beyond that you should also be certain that you can Archive your project with the framework included and export the app as well as submit to the App Store without error. I have seen failures occur at various stages in this process without previous error or warning and you should not consider your product complete until all of these boxes have been checked. Cocoa Touch Framework aka dynamic library Swift or Objective-C only supported on iOS 8 and later, fails during app submission if target <= iOS7 compiles to .framework Cocoa Touch Static Library aka static library only Objective-C (Swift is not supported) compiles to .lib supported “all the way down” to iOS 6.

Hopefully these resources will save you time and effort in your next iOS project!

Cocoa Touch Static Library vs. Cocoa Touch Framework

When Should You Use Implicitly Unwrapped Optionals

When should you use implicitly unwrapped optionals? The short answer to this question is “Never.” But the answer is more nuanced than that. Implicitly unwrapped optionals were added to the Swift language for a reason.

What Is an Implicitly Unwrapped Optional?

The concept underlying implicitly unwrapped optionals is easy to understand. As the name implies, an implicitly unwrapped optional behaves as an optional. I write behave because optionals and implicitly unwrapped optionals are defined differently in the Swift standard library. What’s important is that an implicitly unwrapped optional is allowed to have no value. It shares this trait with optionals.

We declare an implicitly unwrapped optional by appending an exclamation mark to the type.

var text: String!

Remember that the exclamation mark is a warning sign in Swift. It indicates that we can access the value of the implicitly unwrapped optional without unwrapping it using optional binding or optional chaining.

I need to mention that we can still use optional binding and optional chaining to safely access the value of the implicitly unwrapped optional.

var text: String!

if let text = text {
    ...
}
var text: String!

let startIndex = text?.startIndex

We can also check it for nil. But this shouldn’t be necessary since an implicitly unwrapped optional promises the compiler it has a value when it is accessed.

var text: String!

if text == nil {
    ...
}

You could see an implicitly unwrapped optional as an optional whose value is always accessed by forced unwrapping. This also means that a runtime error is thrown if you access an implicitly unwrapped optional that has no value.

Why Would I Need Implicitly Unwrapped Optionals?

Like I said, it is perfectly fine and possible to never use implicitly unwrapped optionals. Implicitly unwrapped optionals are a compromise between safety and convenience. Whenever you use an implicitly unwrapped optional instead of an optional, you trade safety for convenience. If safety is more important to you, then don’t use implicitly unwrapped optionals.

Implicitly unwrapped optionals are a compromise between safety and convenience.

If you declare a property as optional, then you need to use optional binding or optional chaining every time you access the value of the optional. While this isn’t a problem, it makes the code you write more verbose and the result may be less readable. This isn’t true if you use implicitly unwrapped optionals.

When Should You Use Implicitly Unwrapped Optionals?

The Swift Programming Language states the following.

Sometimes it is clear from a program’s structure that an optional will always have a value, after the value is first set. In these cases, it is useful to remove the need to check and unwrap the optional’s value every time it is accessed, because it can be safely assumed to have a value all of the time. – The Swift Programming Language

Outlets are a common example when discussing implicitly unwrapped optionals. Why does Apple use implicitly unwrapped optionals to declare outlets? The reason is simple.

Every stored property of a class needs to have a valid value before the initialization of the instance is complete. If you don’t know the value of a stored property during initialization, you can assign a sensible default value or declare it as an optional, which has a default value of nil. But there is a third option, use an implicitly unwrapped optional.

The value of an outlet is set after the instance of the class is initialized. This is inconvenient since you know that the outlet will have a valid value when it is later accessed. For these reasons, outlets are often declared as implicitly unwrapped optionals. The only reason for doing so is convenience. That is important to understand. It is perfectly fine to declare outlets as optionals.

import UIKit

class ViewController: UIViewController {

    @IBOutlet var titleLabel: UILabel!

    ...
}

Being Bitten by Implicitly Unwrapped Optionals

If you use implicitly unwrapped optionals for anything else but outlets, I can assure you that you will be bitten at some point. When I first started working with Swift, I noticed (looking back) how easily implicitly unwrapped optionals made their way into my code. These implicitly unwrapped optionals were always properties that I couldn’t give a value during initialization. This is a common issue when working with storyboards and segues, for example.

A few months ago, I took over a project that makes ample use of implicitly unwrapped optionals and that has backfired several times since I started working on the project. While I can appreciate the above statement in The Swift Programming Language, it may be obvious that an optional will always have a value when you write the code … but code changes and assumptions change.

Someone might take over the project and not know about the assumptions you made when you wrote the code that uses implicitly unwrapped optionals.

Be Careful

I continue to declare outlets as implicitly unwrapped optionals, but I never use implicitly unwrapped optionals for anything else. Be careful when you make assumptions or promises you may not be able to keep. The code you write today won’t look the same a year from now.

Don’t try to bypass Swift’s type safety. It is a cornerstone of the language and that is for good reason. Implicitly unwrapped optionals were added to the language for convenience, but that problem can also be solved with optionals.

When Should You Use Implicitly Unwrapped Optionals

Swift 3, iOS and Xcode 8: UIPageController and UIPageControl

These two elements allow you to swipe through UIViewControllers with little dots at the bottom of the screen.

First drag a UIPageController onto your storyboard. And give it a custom class. That custom class will look like this:

class MyPagesViewController : UIPageViewController, UIPageViewControllerDelegate, UIPageViewControllerDataSource {

    let pages = ["PagesContentController1", "PagesContentController2"]

    override func viewDidLoad() {
        super.viewDidLoad()
        self.delegate = self
        self.dataSource = self

        ...
    }

    func pageViewController(_ pageViewController: UIPageViewController,
                            viewControllerBefore viewController: UIViewController) -> UIViewController? {
      ...
    }

    func pageViewController(_ pageViewController: UIPageViewController,
                            viewControllerAfter viewController: UIViewController) -> UIViewController? {
      ...
    }

    func presentationCount(for pageViewController: UIPageViewController) -> Int {
      ...
    }

    func presentationIndex(for pageViewController: UIPageViewController) -> Int {
      ...
    }
}

We’re extending from UIPageViewController and we have a delegate and data source protocols for our page view controller. We’re setting ourself as the delegate and datasource.

The pages array references view controller restoration identifiers. So create two new view controllers in your story board and ensure they have those identifiers.

The first thing we want to do is initialise our first view controller to show in viewDidShow.

  let vc = self.storyboard?.instantiateViewController(withIdentifier: "PagesContentController1")
  setViewControllers([vc!], // Has to be a single item array, unless you're doing double sided stuff I believe
                     direction: .forward,
                     animated: true,
                     completion: nil)

If you start the app now, you’ll get your first screen. Our first two delegate methods will allow us to page to the next screen, however:

func pageViewController(_ pageViewController: UIPageViewController,
                        viewControllerBefore viewController: UIViewController) -> UIViewController? {
    if let identifier = viewController.restorationIdentifier {
        if let index = pages.index(of: identifier) {
            if index > 0 {
                return self.storyboard?.instantiateViewController(withIdentifier: pages[index-1])
            }
        }
    }
    return nil
}

func pageViewController(_ pageViewController: UIPageViewController,
                        viewControllerAfter viewController: UIViewController) -> UIViewController? {
    if let identifier = viewController.restorationIdentifier {
        if let index = pages.index(of: identifier) {
            if index < pages.count - 1 {
                return self.storyboard?.instantiateViewController(withIdentifier: pages[index+1])
            }
        }
    }
    return nil
}

In both, we look for the restoration identifiers we set previously, then get get the index of such in our pages array, and we then return the previous view controller, if we’re in the method that says viewControllerAfter, otherwise we attempt to go forwards.

The other two delegate methods deal with the UIPageControl, which is automatically given to us when we created the UIPageViewController, although it does not appear in the storyboard.

func presentationCount(for pageViewController: UIPageViewController) -> Int {
    return pages.count
}

func presentationIndex(for pageViewController: UIPageViewController) -> Int {
    if let identifier = viewControllers?.first?.restorationIdentifier {
        if let index = pages.index(of: identifier) {
            return index
        }
    }
    return 0
}

We’re returning the number of pages, and in the latter we look for our current view controller, get its restoration id, and return the index of that to designate the page we’re on currently.

Finally, in your storyboad, on the UIPageViewController set the transition type to ‘scroll’, thereby showing those little dots on the bottom of the screen. To make them transparent this voodoo code that I found in a youtube video seems to work:

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    for view in view.subviews {
        if view is UIScrollView {
            view.frame = UIScreen.main.bounds // Why? I don't know.
        }
        else if view is UIPageControl {
            view.backgroundColor = UIColor.clear
        }
    }
}

And voila.

Swift 3, iOS and Xcode 8: UIPageController and UIPageControl

Create A Stack View With Custom Spacing iOS 11 Swift 4 XCode 9

When Apple introduced stack views in iOS 9 they made it much easier to use Auto Layout by reducing the number of constraints you needed to create yourself for many common layouts. One edge case that was not well covered was the need for custom spacing between views. You could do it by nesting stack views but that always seemed an unnecessary complication. In iOS 11 you can create stack views with custom spacing between views.

The Problem

Here is the example layout I want to create:

I have a total of five labels. A header label with a larger font size, three labels and small font size footer. The three middle labels have spacing of 8 points but I want the header and footer to have 32 points of spacing.

This vertical arrangement of views is perfect for a UIStackView apart from the spacing. The spacing property of a stack view applies equally to the arranged subviews.

Stacking Stacks

One way to create the layout without needing spacer views is to embed an inner stack view.

The inner stack view with the three labels has 8 point spacing and the outer stack view has 32 points of spacing. This works but can quickly get messy. For example, if I wanted different spacing for the header and footer.

Custom Spacing (iOS 11)

New in iOS 11 it is possible to customize the spacing after any of the arranged subviews of a stack view. Assuming we have our labels already created the code to setup the stack view might be something like this:

let stackView = UIStackView(arrangedSubviews: [headerLabel, topLabel, middleLabel, bottomLabel, footerLabel])
stackView.axis = .vertical
stackView.alignment = .fill
stackView.spacing = 8.0

This gives us 8 points of spacing between the arranged subviews. To get the extra spacing after the header label and before the footer label:

// iOS 11 only
stackView.setCustomSpacing(32.0, after: headerLabel)
stackView.setCustomSpacing(32.0, after: bottomLabel)

Notes:

  • You always specify the spacing after the arranged subview. There is no method to specify spacing before a view.
  • There is no way to specify this custom spacing in Interface Builder. You need to do it in code.
  • When you remove the subview from the stack view the system removes the custom spacing.

Standard and Default Spacing

There are two new type properties on the UIStackView class in iOS 11 that define default and system spacing:

class let spacingUseDefault: CGFloat
class let spacingUseSystem: CGFloat

You cannot rely on the values that these properties return. The actual values are tokens used to ask for the default or system spacing. System spacing seems to give 8 points of spacing on an iPhone but again you should not rely on the actual value.

You can use these to set or reset the custom spacing after a view. For example to use the system defined spacing after the top label:

stackview.setCustomSpacing(UIStackView.spacingUseSystem, after: topLabel)

To reset the spacing after this label back to the value of the spacing property (removing the custom spacing):

stackview.setCustomSpacing(UIStackView.spacingUseDefault, after: topLabel)
Create A Stack View With Custom Spacing iOS 11 Swift 4 XCode 9