Build a macOS Application to Run a Shell Command with Xcode and SwiftUI

A few years ago, I published a post that described how to build a Mac application in Swift that would run a shell command. Surprisingly, this post still gets a lot of traffic. Xcode and Swift have progressed over the last three years and that old example is mostly useless now. It is time for an update!

In this post, I will describe how to build a simple Mac app which runs a shell command using SwiftUI.

SwiftUI is the new framework from Apple to build user interfaces across platforms. Applications built with Swift UI will require 10.15 Catalina and higher. As you will see, SwiftUI does simplify a lot of the work of creating the user interface. I believe that learning SwiftUI now will be a good investment into the future.

I have written this post with Xcode 11.2.1 on macOS Catalina 10.15.1. That means we are using Swift 5.1. You can download Xcode from the Mac App Store or Apple’s developer page.

In this post, we will be using the say command to make the Mac speak. You can test the command by opening the Terminal and entering

> say "Hello World"

The say command is a simple and fun stand in for other command line tools. It also provides instant feedback, so you know whether it is working or not. That said, when you want to provide text-to-speech functionality in a Swift app, you should probably use the text-to-speech frameworks, rather than sending out a shell command.

Nevertheless, for Mac Admin tasks, there are some that are only possible, or at least much easier, with shell tools.

First: Swift UI hello world

Before we get to more complicated things, let’s honor the classics and build a “Hello, World” application using Swift UI.

Open Xcode and from the project picker select “New Project.”

In the template chooser select ‘macOS’ and ‘Application’. Then click ‘Next.’

In the next pane, enter a name for the project: “SayThis.” Verify the other data and make sure the choice for ‘User Interface’ is ‘SwiftUI’. Then click ‘Next.’

The window with the new project will have four vertical panes. You can use the controller element in the top right of the toolbar to hide the right most “Inspector” pane as we will not need it.

Click it the “Play” button in the top left to build and run the template code, a window should open which show the place holder text “Hello World!”

When you return to Xcode the preview pane on the right should now be active and also display the “Hello World!” text. If it does not there should be a “Resume” button at the top of the preview pane (also called “Canvas”) which you can click to make Xcode resume live updating the preview. There are many situations that will stop Xcode from continuously updating the preview and you can click this button to resume.

The left column shows a list of files in this project. Select the ContentView.swift file. This file contains the Swift UI code that sets up the interface. You will see the code in the center pane. The relevant part of the code is:

struct ContentView: View {
    var body: some View {
        Text("Hello, World!")
            .frame(maxWidth: .infinity, maxHeight: .infinity)
    }
}

The ContentView contains a body which contains a Text element with the text Hello World!. at the end of the Text object, you a ‘modifier’ that sets the frame of the Text object to use all available space.

Hello, me!

In the code pane, change World to something else:

Text("Hello, Armin!")

You will see that the text in the preview pane updates as you change the code.

Note: there are several reasons Xcode might stop updating and when it does, you will have to hit the ‘Resume’ button above the preview pane.

The preview pane or canvas allows you to edit as well. When you (command) ⌘-click on the “Hello, …” text in the canvas, you will get an “Action menu.”

The first item in the action menu: “Show SwiftUI Inspector” will an inspector with the attributes or “modifiers” of the text object. Note that, even though there is no visual indication, the inspector view can be scrolled to reveal more options.

Change the font of the text to Body. The preview in the canvas will update, as well as the code. The code will now look like:

Text("Hello, Armin!")
    .font(.body)
    .frame(maxWidth: .infinity, maxHeight: .infinity)

I have broken the modifiers into their own lines for clarity.

Stacking it up… or down… or sideways…

The SwiftUI body can only contain one object. But there are SwiftUI objects that you can use to group multiple objects together. Command-Click on the text and choose “Embed in VStack” from the action menu.

The code will change to:

 VStack {
    Text("Hello, Armin!")
        .font(.body)
        .frame(maxWidth: .infinity, maxHeight: .infinity)
}

The VStack stands for ‘vertical stack.’ The VStack and its sibling the HStack (horizontal) can contain multiple objects. So, you can add another Text over our “Hello” text:

VStack {
    Text("SayThis")
        .font(.largeTitle)
    Text("Hello, Armin!")
        .font(.body)
}.frame(maxWidth: .infinity, maxHeight: .infinity)

This adds the "SayThis" text above the "Hello, ..." text in a larger font. I have also moved the .frame(...) modifier to the VStack because that leads to a nicer output. Feel free to apply the .frame modifer to different objects to see how that affects the layout.

The text is a little bit close together, so we can add some .padding() modifiers:

VStack {
    Text("SayThis")
        .font(.largeTitle)
        .padding()
    Text("Hello, Armin!")
        .font(.body)
        .padding()
}.frame(maxWidth: .infinity, maxHeight: .infinity)

You can choose to change the properties using the inspector UI or in the code. Either way the changes should be reflected in the canvas and the code.

Adding some interaction…

Now we want to add some interaction. Eventually, we want the user to be able to enter some text, which will be sent to the say command. Before we get, we will add a field, to enter some text.

To add a TextField where the user can enter text to the layout, click on the + icon in the top right of the tool bar. A window will appear with SwiftUI objects. Enter TextField in the search area to find the TextField object and drag it between the two text items.

You will want to add a .padding() modifier to the text field as well.

We also need a local variable in our ContentView to store the value of the TextField. Add this variable declaration under the struct ContentView definition and before the body declaration:

@State var message = "Hello, World!"

This adds and initializes a variable named message. The @State marker tells SwiftUI this ‘State’ variable will be used with the view. The variable is part of the ‘state’ of the view. Changes to the variable will immediately update the UI and vice versa.

Use the message variable for the TextField and Text objects:

@State var message = "Hello, World!"

var body: some View {
    VStack {
        Text("SayThis")
            .font(.largeTitle)
            .padding()
        TextField("Message", text: $message)
            .padding()
        Text(message)
            .font(.body)
            .padding()
    }.frame(maxWidth: .infinity, maxHeight: .infinity)
}

When you look at this interface in the preview pane, you will see that the contents of the message variable are reflected in the TextField and Text. To test the interactivity, you need to either ‘build and run’ the application or hit the ‘Play’ button in the preview. Then you can change the text in the text field and immediately see the changes displayed in the Text below.

Declaring the message variable as a @State will automatically set up all the notifications for these seamless updates.

Buttons and Actions

While updating another object is quite cool, it is not what we set out to do. Let’s get back on track.

  • Remove the last Text(message) and all its modifiers.
  • Command-click on the TextField and use the action menu to embed it in an HStack (horizontal stack)
  • Drag a Button from the object Library (you can open this with the ‘+’ button in the top right of the window) next to the text field

The code you generated should look like this:

HStack {
    TextField("Message", text: $message)
        .padding()
    Button(action: {}) {
        Text("Button")
    }
}

You can change the label of the button by changing the Text object inside of it. Change "Button" to "Say". You also want to add a .padding() modifier to the button.

The preview should now look like this:

The action of the Button is a code block that will be run when the button is clicked. Add the code between the action closure brackets:

Button(action: {
    let executableURL = URL(fileURLWithPath: "/usr/bin/say")
    try! Process.run(executableURL,
                     arguments: [self.message],
                     terminationHandler: nil)
}) {
    Text("Say")
}.padding(.trailing)

This uses the run() convenience method of the Process class. You need to provide the full path to the command line tool. You can determine the full path in Terminal with the which command:

> which say
/usr/bin/say

The arguments to the say command need to be provided as an Array of Strings. The process is launched asynchronously, so the main thread continues immediately. We can also provide a code block that will be executed when the Process terminates. For now, we have no code that needs to be run on termination, so we set the terminationHandler to nil.

Note: using the try! statement to crash when this method throws an exception is lazy. I use it here to simplify the code drastically. Process.run() will throw an error when the file at exectableURL does not exist or is not executable. We can be fairly certain the /usr/bin/say executable exists, so the try! statement is justifiable. You should probably add proper error prevention and handling in production code.

Updating the Interface

The say command is executed asynchronously. This means that it will be running in the background and our user interface remains responsive. However, you may want to provide some feedback to the user while the process is running. One way of doing that, is to disable the ‘Say’ button while the process is running. Then you can re-enable it when the process is done.

Add a second @State variable to track whether our process is running:

@State var isRunning = false

Add a .disabled(isRunning) modifier to the Button. This will disable the button when the value of the isRunning variable changes to true.

Then we add a line in the Button action code to set that variable to true. We also add a code block to the terminationHandler which sets its value back to false:

 Button(action: {
    let executableURL = URL(fileURLWithPath: "/usr/bin/say")
    self.isRunning = true
    try! Process.run(executableURL,
                     arguments: [self.message],
                     terminationHandler: { _ in self.isRunning = false })
}) {
    Text("Say")
}.disabled(isRunning)
    .padding(.trailing)

Now, when you press the button, it will disable until the say process is done speaking.

Sample Code

For your convenience, here is the finished code for the ContentView.swift class:

//
//  ContentView.swift
//  SayThis
//

import SwiftUI

struct ContentView: View {
    @State var message = "Hello, World!"
    @State var isRunning = false
    
    var body: some View {
        VStack {
            Text("SayThis")
                .font(.largeTitle)
                .padding()
            HStack {
                TextField("Message", text: $message)
                    .padding(.leading)
                Button(action: {
                    let executableURL = URL(fileURLWithPath: "/usr/bin/say")
                    self.isRunning = true
                    try! Process.run(executableURL,
                                     arguments: [self.message],
                                     terminationHandler: { _ in self.isRunning = false })
                }) {
                    Text("Say")
                }.disabled(isRunning)
                    .padding(.trailing)
            }
        }.frame(maxWidth: .infinity, maxHeight: .infinity)
    }
}


struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

More Tutorials

If you want to learn more about SwiftUI and how it works, Apple has some excellent Tutorials on their developer page:

Update 2023-08-16: nearly five years later, I wrote a second part to this tutorial.

Weekly News Summary for Admins — 2019-12-06

The days are getting shorter and darker… At least here in the northern hemisphere, the readers south of the equator can please stop gloating. Still no sign of the Mac Pro and the Pro Display XDR, which were promised “Fall 2019.” Apple has done this before, when they released the iMac Pro in the last business week of 2017.

The beta 4 for iOS 13.3 just dropped. I get the feeling it’ll be another few frantic weeks at Apple before the end of the year!

If you would rather get the weekly newsletter by email, you can subscribe to the Scripting OS X Weekly Newsletter here!! (Same content, delivered to your Inbox once a week.)

On Scripting OS X

News and Opinion

MacAdmins on Twitter

  • Tim Sutton: “Til brew-cask has a --no-quarantine flag; especially helpful if are weird like me and sometimes use it to install binaries that aren’t notarized: brew cask install jtool --no-quarantine
  • Greg Neagle: “Last call for testers! Munki 4 is likely to be released next week. Test the current beta in your environment and file issues!”
  • Jamf: “ICYMI, the Jamf Online Training Catalog is now open to all Jamf customers! With over 100 modules and 15 series of content, the catalog is structured to help you learn about JamfPro, JamfConnect or JamfSchool anytime and anywhere. Jamf Online Training
  • Kyle Crawford: “Did you know that user-approving kexts on Catalina requires admin rights?!!”
  • Patrick Fergus: “Adobe Customer Feedback survey for IT Admins Link

Bugs and Security

Support and HowTos

Scripting and Automation

Updates and Releases

To Listen

Support

There are no ads on my webpage or this newsletter. If you are enjoying what you are reading here, please spread the word and recommend it to another Mac Admin!

If you want to support me and this website even further, then consider buying one (or all) of my books. It’s like a subscription fee, but you also get a useful book or two extra!

Book Update – Moving to zsh v2

I have pushed an update for the “Moving to zsh” book.

The book is barely two weeks out but I had a few more things to add. The nice thing about self-published digital books is that they can updated quickly and often. I expect more updates over time as I continue to learn more about the subject.

As usual, the update is free when you already own the book.

If you have already purchased the book, you can go to Apple Books application on your Mac and choose ‘Check for available Downloads…’ from the ‘Store’ menu. I have seen the Mac Books app be really slow (or even completely blind) in picking up updates, you can accelerate the process by removing the local download and re-downloading the book. In iOS tap on your iCloud account icon next to ‘Reading Now’ and then choose ‘Updates.’

If you have not yet purchased the book, go get it on Apple Books!

The changes are listed here, but you can also them in the ‘Version History’ section in the book. There it links to the relevant section of the book, so you can find the changes quickly.

  • Learnt about is-at-least and updated ‘Sharing across macOS and zsh versions,’ accordingly
  • Added a section on zmv
  • Added a note to keep changes to the PATH variable up-to-date across multiple shells
  • More feedback from proof readers. (Thank you!)

If yo have read and enjoyed the book, please leave a review on the Apple Books store!

Weekly News Summary for Admins — 2019-11-29

Happy day after Thanksgiving to the US readers! Happy “week where we can take a breath from tech news” for everyone else!

It is tradition to reflect on things you can be grateful for. There are many standard replies, such as family, friends, and—more particular to this newsletter—the amazing MacAdmins community. But, this year I wanted to say that I am grateful to be working in a field that is always changing.

I started out using and managing Macs in the nineties. Back then Apple had a strong market in the print and publishing business and education, but nearly nowhere else. It was obvious that the Macintosh operating system needed a complete overhaul to remain relevant, but Apple’s attempts to build a new system failed. Apple was considered stagnant and irrelevant. The change came in 1996 when Apple bought NeXT and Steve Jobs returned to the company. The iMac, iBook, and Mac OS X saved Apple.

Even though, in hindsight, it is obvious that Mac OS X was an important part of that transition, there were many back then who rejected the new system. There were many reasons for being conservative: the iMac, iBook, and even the blue PowerMac towers looked like “toys,” third party applications were slow to adopt Mac OS X, the new OS was not yet ready for some critical workflows, Windows and Linux were tempting alternatives.

In the beginning, it was mainly new users who would embrace the new Mac OS. Java and web developers, as well as scientists, who realized there was now a Unix(-like) platform with a nice UI and MS Office (one of the first major commercial app suites to have a Mac OS X version). It always felt like the “traditional” Mac users had to be dragged along.

Apple has changed multiple times since then: online and retail stores, the iPod, the iTunes Store, the iPhone, the App Store, the iPad, the Apple Watch, and a focus on services.

There were also failures, dead ends, and missteps along the way: the hockey puck mouse, the Cube, the various incarnations of iTools/dotMac/Mobile me/iCloud, server hardware and software, the butterfly keyboards… In the MacAdmin space we had WebObjects, NetInfo, MCX, Workgroup Manager, NetBoot, NetRestore, Profile Manager, Mac OS (X) Server, bash3 and Python2…

We often complain about the pace of change in the technology field. I agree the entire field would be well-served with more consideration, rather than rushing features at all cost to fulfill an arbitrary, self-imposed deadline. But the absence of change is stagnation and irrelevancy.

I remember when Apple was considered stagnant and irrelevant. I really don’t miss those times.

Happy Thanksgiving!

Talking about change: My new book “Moving to zsh” is now available in the Apple Books Store.

Howard Oakley from the Eclectic Light Company calls it: “[T]imely, invaluable, and very clearly written for anyone who ever uses the command line.”

Packaging for Apple Administrators” and “Property List, Preferences, and Profiles for Apple Admnistrators” are on sale until Dec 3. I also permanently dropped the price for “macOS Installation.”

If you would rather get the weekly newsletter by email, you can subscribe to the Scripting OS X Weekly Newsletter here!! (Same content, delivered to your Inbox once a week.)

On Scripting OS X

News and Opinion

MacAdmins on Twitter

  • Rich Trouton: “Need a quick way to check Apple’s system status boards? System status: isappleup.com Developer system status: developer.isappleup.com
  • Victor (groob): “Adding async command information to @micromdm_io Admins will be able to check the status of each command in the queue and see the raw request/response at any time.” (video)

Bugs and Security

Support and HowTos

Scripting and Automation

Updates and Releases

To Watch

To Listen

Support

There are no ads on my webpage or this newsletter. If you are enjoying what you are reading here, please spread the word and recommend it to another Mac Admin!

If you want to support me and this website even further, then consider buying one (or all) of my books. It’s like a subscription fee, but you also get a useful book or two extra!

Comparing Version strings in zsh

Another excerpt from the book “Moving to zsh.” I found this one so useful, I thought I’d like to share it.

You can get the version of zsh with the ZSH_VERSION variable:

% echo $ZSH_VERSION
5.7.1

And you can get the version of macOS with the sw_vers command:

% sw_vers -productVersion
10.15.1

Comparing version strings is usually fraught with potential errors. Strings are compared by the character code for each character.‘2’ is alphabetically greater than ‘10’ when compared as strings, because the character code for 2 is greater than the character code for 1. So, a string comparison of macOS version numbers will return that 10.9.5 is greater than 10.15.1.

Zsh, however, provides a function is-at-least which helps with version string comparisons.

With a single argument, is-at-least will return if the current zsh version matches or is higher than a given number:

if ! is-at-least 2.6-17; then
  echo "is-at-least is not available"
fi

When you provide two arguments to is-at-least, then the second argument is compared (using version string rules) with the first and needs to match or be higher:

autoload is-at-least
if is-at-least 10.9 $(sw_vers -productVersion); then
  echo "can run Catalina installer"
else
  echo "cannot run Catalina installer"
fi

Note: when used in a script, you will probably have to autoload is-at-least before using it. In an interactive shell, it is often already loaded, because many other autoloader functions will have already loaded it.

Black Friday/Cyber Monday Sale

It is Thanksgiving week in the US, which means that all real-world and online retailers are luring buyers with all kinds of crazy sales.

Here at Scripting OS X, I keep the book prices low all year round. My latest book “Moving to zsh” is US$9.99 all the time.

I also just permanently lowered the price on “macOS Installation.”

Nevertheless, starting today and up to Dec 3, I will also put “Packaging for Apple Administrators” and “Property Lists, Preferences, and Profiles for Apple Administrators” on sale!

And not just for US readers, but on all regions where the books are available in the Apple Books store!

Happy Thanksgiving!

Weekly News Summary for Admins — 2019-11-22

My new book, “Moving to zsh,” is now available in the Apple Books Store!

If you have been reading this newsletter for a while, you know what this book is about, otherwise you can read all the details in my blog post. This is the first of my books that is not targeted specifically at Mac Admins, but is useful to anyone who use the Terminal on the Mac.

Please, spread the word to friends, co-workers, fellow admins, developers, and power-users who you think would benefit from this book. Thank you all for your support.

If you would rather get the weekly newsletter by email, you can subscribe to the Scripting OS X Weekly Newsletter here!! (Same content, delivered to your Inbox once a week.)

On Scripting OS X

News and Opinion

MacAdmins on Twitter

  • Timo Perfitt: “New MacBook Pro 16” in the house!… ” (Thread)
  • Tech Girl: “It’s weird jamf sells jamf protect and treats patch like their child they’re pretending doesn’t exist. How many #macadmin pay for jamf also need jamjar/autopkgr/Munki to properly patch & notify users?”
  • Victor (groob): “Is there someone at Apple who understands that security updates are not optional and that an enterprise might want to enforce a deadline? Lack of MDM options to make this possible suggests otherwise.” (Thread)
  • Carl Ashley: “Things your postinstall scripts are doing that are 100% bad 100% of the time: cp /tmp/foo.app /Applications/foo.app; chmod 777 /Applications/Foo.app; chown root:admin /Applications/Foo.app” (Thread)

Bugs and Security

Support and HowTos

Scripting and Automation

Apple Support

Updates and Releases

To Watch

To Listen

Support

There are no ads on my webpage or this newsletter. If you are enjoying what you are reading here, please spread the word and recommend it to another Mac Admin!

If you want to support me and this website even further, then consider buying one (or all) of my books. It’s like a subscription fee, but you also get a useful book or two extra!

Install shellcheck binary on macOS (updated)

A few months back I wrote a post on how to compile and build an installer for the shellcheck binary for macOS.

Just a few weeks later, the shellcheck project added a download for a pre-compiled binary for macOS. You can now download the binary with this link:

https://shellcheck.storage.googleapis.com/shellcheck-latest.darwin.x86_64.tar.xz

Ironically, macOS can unarchive xz archives when you double click them in the Finder, but there is no command line tool on macOS to unarchive them. In the previous post, I ran into the same problem, and there you can find instructions on how to install the xz tools on macOS.

Update 2020-03-26: I was unnecessarily complicating this. You can use tar to unarchive this:

tar -xf shellcheck-latest.darwin.x86_64.tar.xz

After downloading and un-archiving, you can manually move the shellcheck binary to a suitable directory. The standard location is /usr/local/bin.

For manual installations, this is it! Much simpler than before. Thank you!

Note: if you want the man page as well, you still need to build it with pandoc from the source.

Build a pkg for managed deployment

If you are a MacAdmin and want to distribute shellcheck with your management system, you will need to build an installer package (pkg).

Instead of copying the binary to /usr/local/bin, place it in a payload folder in a project folder. Then build the pkg with pkgbuild:

% mkdir -p ShellcheckPkg/payload
% cp ~/Downloads/shellcheck-latest/shellcheck ShellcheckPkg/payload
% pkgbuild --root ShellcheckPkg/payload --identifier com.example.shellcheck --version 0.7.0 --install-location /usr/local/bin shellcheck-0.7.0.pkg

Replace the 0.7.0 with the actual version number.

Automated Package creation with autopkg

And because all of this isn’t really that difficult, I built autopkg recipes for Shellcheck You can find them in my recipe repository or with autopkg search shellcheck. Enjoy!

New Book Release Day: Moving to zsh

My new book: “Moving to zsh” is now available on the Apple Books Store!

The book should be useful for anyone who uses macOS Terminal and is wondering what the change of the default shell in Catalina means and how to best handle the transition. The book describes the motivation for Apple (and the user) to “move to zsh” and how to get the most out of the new shell.

It is based on the series of blog posts that I posted from June through August, but reworked and expanded with more detail and more topics. Some of the information from my MacSysAdmin presentation also made it into the book.

The blog series added up to about 11K words, and the book, in its current form, is more than 22K words. Compared to the series, I have added images, movies, clarifications, more examples, and several new sections and appendices.

This books explains:
– why Apple is changing the shell
– implications for infrequent and expert Terminal users
– how to move from bash to zsh
– configuring zsh to be more productive
– moving scripts from bash to zsh

And this will certainly not be the end for “Moving to zsh.” Like my other books, I plan to update and add to it after release as well, keeping it relevant and useful.

This is my first book that is not targeted mainly at MacAdmins. I believe this book will be useful for any Mac user that uses the Terminal frequently: Developers, web admins, scientists, and other power users. Please help spread the news by sharing this post and the book link with friends, co-workers, and across social media. Thank you!

Go get “Moving to zsh” on the Apple Books Store!

Weekly News Summary for Admins — 2019-11-15

Hard to believe, but Apple software updates this week (except for new beta releases). Instead we got a new MacBook Pro! (And yes, it has a new keyboard—I really hope we can put this behind us now…)

Also Jamf Nation User Conference in Minneapolis! Hope everyone had a great time and safe travels home!

Progress update: my new book “Moving to zsh.” has been sent to the proof readers!

If you would rather get the weekly newsletter by email, you can subscribe to the Scripting OS X Weekly Newsletter here!! (Same content, delivered to your Inbox once a week.)

Headlines

On Scripting OS X

News and Opinion

MacAdmins on Twitter

  • Timo Perfitt: “In prep for the MDS 2 release, I created some new videos and postings for the new MDM service in MDS.” (Thread)
  • Patrick Fergus: “If anyone was using a CLI uninstall of an Adobe product, the path to Setup changed, assumedly with CCDA 5”
  • Rosyna Keller: “Some fun new stuff with altool 4.0 (Xcode 11.x) is now available.” (Thread)

Jamfnation User Conference

Bugs and Security

Support and HowTos

Scripting and Automation

Updates and Releases

To Listen

Support

There are no ads on my webpage or this newsletter. If you are enjoying what you are reading here, please spread the word and recommend it to another Mac Admin!

If you want to support me and this website even further, then consider buying one (or all) of my books. It’s like a subscription fee, but you also get a useful book or two extra!