Chapters

Hide chapters

macOS by Tutorials

First Edition · macOS 12 · Swift 5.5 · Xcode 13

Section I: Your First App: On This Day

Section 1: 6 chapters
Show chapters Hide chapters

14. Automation for Your App
Written by Sarah Reichelt

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

In the previous chapter, you added a graphical user interface over the top of some Terminal commands and made them much easier to use.

Now, you’re going to open some features of your app so other parts of macOS can access them and use them for automation.

You’ll do this in two ways: by providing a service to the system-wide Services menu and by publishing a shortcut for use by the Shortcuts app.

What is Automation?

For any app, there are two forms of automation. The first is when the app itself performs a task, especially when that task would have been long, tedious or finicky. ImageSipper already does this. Imagine how boring and time-consuming it would be to generate a thumbnail for every image in a large folder. Now, you can do it with just a few clicks.

The other way is to make various features of your app available to other apps or system services so your app can become part of an automated workflow. This is what you’re going to enable in this chapter.

When looking at automation on macOS, there are several alternatives. One of the most common is the Services menu. This is a submenu that appears in every app’s own app menu. You can also find Services in contextual menus. Right-click any piece of text or any file to see what services you can use. What you see in the menu depends on what you’ve selected and what apps you’ve installed.

Another possibility for automation is scripting. You can use shell scripts, AppleScripts and various other languages — even Swift! Scripting languages are outside the scope of this book, but they’re another facet of automation.

The final option is through automation apps. Apple provides two such apps. Automator has been around for a while, but at WWDC 2021, Apple introduced Shortcuts for the Mac. Previously, this was available on iOS.

Automator can be useful, and it comes with an extensive library of actions, as well as the ability to add custom actions using AppleScript or shell scripts. However, the Shortcuts app enables you to publish actions directly from your app.

In this chapter, you’ll supply a service and publish a shortcut.

Adding a Service

First, you’ll add a service. In Chapter 10, “Creating A Document-Based App”, you set up file types so the app could open Markdown files. ImageSipper isn’t a document-based app, so you can’t do this. Instead, you’re going to add an Open in ImageSipper menu item to the Services menu. This will open the selected image file or folder in the app, launching the app if necessary.

Editing Info.plist

Open your project from the last chapter or use the starter project from this chapter’s projects folder in the downloaded materials.

Adding a row to Info.plist.
Ohjicm i hez ze Ufwe.vpuqv.

Editing Info.plist
Oquvisv Ufla.ptodk

Filling in the Service Item

Select the Menu item title row and then click in its Value column to start editing. This sets the title of the item in the Services menu. Enter Open in ImageSipper and press Return to move to the next field.

Editing settings
Unuroxm buzjozxb

Send types settings
Foth nqqod retdarpp

Setting the Context

You’re nearly finished with Info.plist. There’s just one more step, and it’s an important one. Lots of apps on your Mac have services, and you don’t want the Services menu showing them all every time. So you set a context for the service to tell the system when it’s appropriate to show your particular service. In this case, you only want to show it when the user selects an image file or a folder.

Changing the type.
Kciwhelh vle ntse.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>NSServices</key>
    <array>
      <dict>
        <key>NSMenuItem</key>
        <dict>
          <key>default</key>
          <string>Open in ImageSipper</string>
        </dict>
        <key>NSMessage</key>
        <string>openFromService</string>
        <key>NSPortName</key>
        <string>ImageSipper</string>
        <key>NSSendTypes</key>
        <array>
          <string>string</string>
        </array>
        <key>NSSendFileTypes</key>
        <array>
          <string>public.folder</string>
          <string>public.image</string>
        </array>
        <key>NSRequiredContext</key>
        <dict>
          <key>NSTextContent</key>
          <string>FilePath</string>
        </dict>
      </dict>
    </array>
  </dict>
</plist>

Testing the Services Menu

Build and run the app now. It looks unchanged, but behind the scenes, it’s registered your new service. Switch to Finder, select any image file and right-click. Do you see a Services menu or an Open in ImageSipper item at the end of the contextual menu? What about if you right-click a folder?

Right-clicking a file.
Tarkj-vlovgozj i dopu.

/System/Library/CoreServices/pbs -flush
/System/Library/CoreServices/pbs -update
Services menu
Cadqehid sida

Handling the Service Call

Before your app can respond to a service call, it needs a servicesProvider. Open ImageSipperApp.swift and add this new class at the bottom:

class ServiceProvider {
}
var serviceProvider = ServiceProvider()
.onAppear {
  NSApp.servicesProvider = serviceProvider
}
// 1
@objc func openFromService(
  _ pboard: NSPasteboard,
  userData: String,
  error: NSErrorPointer
) {
  // 2
  let fileType = NSPasteboard.PasteboardType.fileURL
  guard
    // 3
    let filePath = pboard.pasteboardItems?.first?
      .string(forType: fileType),
    // 4
    let url = URL(string: filePath) else {
      return
    }

  // 5
  NSApp.activate(ignoringOtherApps: true)

  // handle url here
}

Processing URLs

Your app receives data and — hopefully — converts it into a URL. Now what?

extension Notification.Name {
  static let serviceReceivedImage =
    Notification.Name("serviceReceivedImage")
  static let serviceReceivedFolder =
    Notification.Name("serviceReceivedFolder")
}
// 1
let fileManager = FileManager.default
// 2
if fileManager.isFolder(url: url) {
  // 3
  NotificationCenter.default.post(
    name: .serviceReceivedFolder,
    object: url)
} else if fileManager.isImageFile(url: url) {
  // 4
  NotificationCenter.default.post(
    name: .serviceReceivedImage,
    object: url)
}

Receiving Notifications

Each of the main views will handle one of the notifications. Start with an image file URL.

let serviceReceivedImageNotification = NotificationCenter.default
  .publisher(for: .serviceReceivedImage)
  .receive(on: RunLoop.main)
// 1
.onReceive(serviceReceivedImageNotification) { notification in
  // 2
  if let url = notification.object as? URL {
    // 3
    selectedTab = .editImage
    // 4
    imageURL = url
  }
}
let serviceReceivedFolderNotification = NotificationCenter.default
  .publisher(for: .serviceReceivedFolder)
  .receive(on: RunLoop.main)
.onReceive(serviceReceivedFolderNotification) { notification in
  if let url = notification.object as? URL {
    selectedTab = .makeThumbs
    folderURL = url
  }
}

Using the Service

Quit the app if it’s already running. Press Command-B to compile the new code — there’s no need to run it.

Opening an image.
Uqiveff oh ewiwe.

Opening a folder.
Ohuzidh a nommep.

Adding a Shortcut

Creating a service took a lot of steps, and you had to do many of them manually with no help from autocomplete. Adding a shortcut is slightly easier because Xcode provides a file template for you to fill in.

Intent file template
Ivrehl zoli fencdozi

Adding a new intent.
Evmurf o zan ubgakp.

Intent settings
Iddasd funcothf

Intent parameter
Uqjidr yupidapef

Intent shortcuts
Ojtizh kfoppzemz

Coding the Intent

Now that you’ve defined your intent, press Command-B to build the app. Switch to the Report navigator and look at the most recent build log:

Build log
Jeuqp qot

Adding the supported intent.
Ugmilh cko lavwowzic ufsibg.

import Intents
class PrepareForWebIntentHandler: NSObject, 
  PrepareForWebIntentHandling {
}

Adding the Intent Handlers

The fix adds four method stubs and causes two more errors, because Xcode supplied two versions of each method. One uses a callback and the other uses async. You want the async methods, so delete the two that are not marked as async.

// 1
guard let url = intent.url else {
  return .confirmationRequired(with: nil)
}
// 2
return .success(with: url)
// 1
guard let fileURL = intent.url?.fileURL else {
  // 2
  return PrepareForWebIntentResponse(
    code: .continueInApp,
    userActivity: nil)
}

// 3
// sips call here

// 4
return PrepareForWebIntentResponse(
  code: .success,
  userActivity: nil)

Writing the Action

Open Utilities/SipsRunner.swift and add this method to SipsRunner:

func prepareForWeb(_ url: URL) async {
  // 1
  guard let sipsCommandPath = await checkSipsCommandPath() else {
    return
  }

  // 2
  let args = [
    "--resampleHeightWidthMax", "800",
    url.path
  ]

  // 3
  _ = await commandRunner.runCommand(sipsCommandPath, with: args)
}
await SipsRunner().prepareForWeb(fileURL)

Configuring the Application Delegate

When using the SwiftUI architecture, you don’t get a custom application delegate by default, but you can set one up yourself.

// 1
class AppDelegate: NSObject, NSApplicationDelegate {
  // 2
  func application(
    _ application: NSApplication,
    handlerFor intent: INIntent
  ) -> Any? {
    // 3
    if intent is PrepareForWebIntent {
      return PrepareForWebIntentHandler()
    }
    // 4
    return nil
  }
}
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDel

Using the Shortcut

Press Command-B to build the app and incorporate this new code into the built product.

New shortcut
Tik dyosmjez

Clear content list.
Mfeaj mowvosx bekj.

Receive Images; Ask for Files.
Yolouzu Oyijah; Ixt med Juses.

Show intent info.
Ymop abdehd iwno.

Drag intent into shortcut
Fpul asnuqm ujko lfilhkus

Shortcut
Jkixwgak

Shortcut input
Speshhoj ewjeb

Accessing Your Shortcut

You’ve now used your intent in a shortcut, triggered from within the Shortcuts app. This is a great place to build workflows, but there are several other ways to access this shortcut.

Shortcut details
Qsagblay jiqoazy

Triggering the shortcut
Qcolkeyesw nfa dxucgvim

Trouble-shooting Shortcuts

Shortcuts can be tricky to debug when you’re still working on the parent app. Here are some tips to help if you get stuck.

rm -rf ~/Library/Developer/Xcode/DerivedData

Key Points

  • You can write an app to perform automation internally, but your app can also provide automations for macOS to use.
  • Services are system-wide utilities. When setting up your app to publish a service, it’s important to make sure it only appears when appropriate.
  • Apple’s Shortcuts app is an automation service that allows users to build workflows. Intents provide services from your app to a shortcut.

Where to Go From Here?

For more information about services, check out Apple’s Services Implementation Guide. It’s quite an old document, but still valid.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now