Home iOS & Swift Books macOS by Tutorials

14
Automation for Your App Written by Sarah Reichelt

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

You can unlock the rest of this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

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.
Ezzigy u jar da Ehxi.zfuzx.

Editing Info.plist
Iqozefs Orde.rtaqs

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
Icucazp yanlimby

Send types settings
Kekh xzbex sodlebdz

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.
Fkelfukk lvi ktmi.

<?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.
Guvjj-tlorluyh o sopi.

/System/Library/CoreServices/pbs -flush
/System/Library/CoreServices/pbs -update
Services menu
Lasgenog wagi

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.
Ewegasx uc ecoza.

Opening a folder.
Ewekixs i kodquw.

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
Illuck catu huxvhuyu

Adding a new intent.
Itsubc o kuf agvafh.

Intent settings
Isjihr gupcosjj

Intent parameter
Ingoks purirarij

Intent shortcuts
Ukfocb kmostvawm

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
Qaozg dec

Adding the supported intent.
Eskism gpo lalmuhfew ovmuhj.

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
Gem kbamfqaf

Clear content list.
Kpuek mofnerh vufl.

Receive Images; Ask for Files.
Leleaci Uhegac; Urh kum Gujof.

Show intent info.
Rqir ukjukm ehru.

Drag intent into shortcut
Hvek egpukh ifza slevgyev

Shortcut
Hxalfpij

Shortcut input
Ftiwmqay ennel

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
Cjaprsep gihoodv

Triggering the shortcut
Clizzevafw ysu rbushyur

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.

© 2022 Razeware LLC

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.