Jamf Blog
Words How To on background of technical icons.
November 21, 2022 by Armin Briegel

Use Swift with the Jamf API, Part 3: Optimizing the code

Part three in a technical series focused on using Apple’s Swift programming language to manage devices in Jamf Pro. Working with Swift code, establishing error handling and adding optimizations when calling and handling objects.

In part two of this technical series, we discussed decoding data objects.

What we have done so far

In the first part, we built a command line tool that would fetch an authorization token from the Jamf API and use it to fetch JSON data from the Jamf Pro API. In the second part, we added the code to decode the JSON into usable Swift objects representing the Category and Computer data in Jamf Pro.

Note: You can find the sample code for both parts here. The sample code for part 2 is also the starting point for the code in this part.

With that, our command line tool has the basic functionality we had set as our goal. However, there are some things that will be worth improving, before we continue development on a SwiftUI app.

Clean up code: Encapsulation

Having all of the code in our main function in Jamf List is neither pretty nor convenient. It is better to encapsulate the code in functions or methods.

Let's start with the JamfAuthToken: the code that gets a new JamfAuthToken is intrinsically connected to the object type and should be part of the JamfAuthToken struct definition.

In the file list, select the JamfAuthToken file and add this function, below the properties, and above the final closing brace:

This creates a static function for the JamfAuthToken object. You will see a list of red errors in Xcode which we will fix in a moment. If you compare the code to what we have in the main function, you will see it is mostly the same — with some minor differences.

Dealing with Errors

When something went wrong in the main code, we simply did an exit(1) to abort the tool with an error. This was fine to start out with, but we need to handle errors better as we abstract and encapsulate code. In this function, I have replaced all exit(1) statements with throw statements that throw errors to the calling code. Xcode will notify us about every single one of these statements since we have not defined these errors yet.

In Xcode, create a new Swift file with the name JamfAPIError. Add the following code under the line with the import statement:

This creates a list, or more specifically, an enum (short for 'enumeration') of errors we can use to communicate what went wrong when performing Jamf Pro API calls. Once you have added this file, the red errors should disappear. While this is just a short declaration, we will be using JamfAPIError across different object types going forward, so it makes sense to put this code in its own file.

Pro Tip: You can read up on error handling in Swift.

In a similar fashion, we can also move the code to get and decode the Computer objects from the main function to the Computer object. In the Computer file, add the following code to the end of the Computer struct, but before its closing brace:

Now, we change the main() function to use this new code. In the JamfList file, replace the entire main() function with this:

As you can see, this code is much shorter, which is to be expected, because we moved the code to other Swift objects.

But this also makes our main function much more readable. Instead of a wall of code to fetch the data from the server twice, we have two calls to our objects which get a token and then the list of computers. The wall of code is moved to the respective type definition, where it can be accessed when necessary.

We have also made the error handling code more granular and added error messages for the user. The error-handling code is now longer than the actual code itself, which is a common occurrence.

Error handling

Since these calls might throw errors, we need to handle them. We wrap the code that might throw errors in a do block, and after that block, we catch the various errors. Since there are different errors that can be thrown, we catch each type of error. We do not catch every type of error we have declared, only those that actually might be thrown.

Now we do the extra work to print a different message for each error and return a different exit code. This will give you much more information when you are debugging issues.

We can leverage this in other places as well. For example, JSONDecoder throws errors of type DecodingError and instead of just throwing our error, we can get the information out of the decoding errors first. Again, this will be a huge help when you need to debug decoding complex JSON structures.

In the getAll function in Computer, replace the part starting with the guard and decode to the end of the function with this:

The guard statements we used before are useful because they provide a concise means of dealing with errors and nil values. But explicitly catching errors in a 'do.. catch' statement allows you to react in a very nuanced way. Since this code is removed from the type definitions, the extra code and detail are not much of a detriment while the gain in information when something breaks is worth it. We will see that later when we create more Swift types for other objects in the Jamf API.

Use protocols for shared code and functionality

We have changed the Computer struct to include the getAll() function. Even though we aren't using it right now, you may have already been tempted to do the same thing for the Category struct, as well.

You can add the following method to the Category struct:

Note: You can also review the sample code to this point here.

This is nearly exactly the same as the method in the Computer struct, barring two differences:

  1. The URL has a different endpoint path and does not add query items to the URL and the method returns Category objects.
  2. Replicated code like this is generally a sign that we need to use some mechanism to further encapsulate or abstract the code.

Standardize object behavior

One way to unify behavior between objects would be to use class inheritance. A superclass defines properties and methods common to its subclasses and the subclass objects can further refine or add to this behavior.

Swift classes can inherit behavior, but there are two reasons we cannot or do not want to use class inheritance here.

  1. Only Swift classes can use inheritance and we are using structs for our data objects.
  2. While a bit more subtle, superclasses have no “knowledge” of the subclasses. Methods in the superclass can only have return types of the superclass, but never of the subclasses.

Our getAll methods return an array of Computer or Category objects, respectively. If we changed those to classes and implemented a superclass, called JamfObject the methods of the superclass would return an array of JamfObjects and we would have to do a lot of down-casting to actually get Computer or Category objects. This works fairly well in languages like Objective-C or Python which are less stringent about types, but in Swift, the code gets very complicated and messy.

But, Swift has an alternative solution, that solves both of our issues: Protocols.

In Swift, a protocol defines a "blueprint" or "contract" for a type of implementation. A protocol defines certain properties and methods that a type has to implement to conform. We can use protocol extensions to provide default implementations of methods that all conforming types will automatically gain. The actual types that implement the protocol can return objects of the correct type.

Protocols are even more abstract than classes and inheritance. There are many subtleties in how they work. Nevertheless, they are very useful and a key part of Swift. If you feel a little confused now, don't worry. Things will make (a bit) more sense when we implement our code.

We have already used protocols when we made our structs Codable. By merely declaring our objects to be Codable, they gained functionality from the Codable protocol in the Swift Foundation framework.

Other parts of the framework, like JSONDecoder, now “see” that our objects can be decoded and act accordingly. These framework types and methods do not care about the underlying type of a Swift object, but only if an object implements the Codable protocol.

We will encounter more framework protocols like this going forward, like Comparable and Identifiable

Creating the JamfObject protocol

In the Xcode project, create a new Swift file and call it “JamfObject” and add the following code after the import line:

This is our protocol definition. Here we define all the pieces of code that a struct or class has to implement to conform to the JamfObject protocol.

In the protocol section, we list the properties and methods that the protocol requires. The syntax for properties is a bit odd and has to use { get } for read-only properties and { get set } for read-writable properties.

Then we list the methods that the protocol requires. You will recognize the signature of the second method. It matches the methods that we implemented in the Computer and Category structs, with one little detail changed: the type returned is[Self]. The capitalized Self is a placeholder for the type of object implementing this protocol. In this case, the brackets denoting this method will return an array of this object type, solving one of the problems we would have with class inheritance.

The protocol definition does not contain any actual code. The protocol only tells us what properties and methods the type needs to implement to conform. However, we can provide a default implementation with a protocol extension by adding the following code to the JamfObject file using the protocol definition below:

You should recognize this code as it is mostly the same as the code we have been using, save for a few changes.

First, we split out the code to create URLComponents into their own method. This will allow us to override this particular functionality separately without having to override the entire getAll() method.

Then, there is also a new struct, JamfResults which, again, looks very similar to the CategoryResults and ComputerResults objects we used earlier, but it uses a generic instead. With generics, we don't have to create a ...Results struct for every JamfObject type we want to use, but we can reuse the same struct. Generics and protocols often go together.

Now that we have a protocol, we have to make our structs conform to it. Go to the Category file, delete everything below the import line (both the Category and the CategoryResult structs), and replace it with this:

Our new Category struct implements the JamfObject protocol. That means we have to implement the id and the getAllEndpoint properties. We would also have to implement JamfObject's getAllURLComponents and getAllmethods, but we already get default implementations of those from the protocol extension. We only need to implement those, when we need to change their behavior, but for the Category object, the default behavior works fine.

Obviously, this greatly simplifies the code for the Category struct. It also reduces code replication all over our project. The approach for the Computer struct is very similar. Replace both structs in the Computer file with this:

The Computer type has more complex data, so it still remains more complex code. We have moved the code that is not specific to the JamfObject protocol extension. However, the API request for computer objects needs a more complex URL than the default implementation of getAll in JamfObject provides. We need to add query items to the request URL so that the API call returns all the data we need.

We prepared for this by splitting out the generation of the URLComponents object into its own method. For the Category type, the default implementation from the JamfObject protocol extension of getAllURLComponents was sufficient. But for the Computer type, we need to override the default implementation with a custom method, which replaces the default implementation from the JamfObject protocol extension. We do not need to replace the getAll default implementation since that will work just fine together with our custom getAllURLComponent method.

Note: You can find the sample code at this point here.

Conclusion

Armed with the knowledge of modeling JSON data in Swift structs and the JamfObject protocol, a large part of the Jamf Pro API should now be accessible to your Swift skills.

There are some objects and calls that do not match the behavior of the JamfObject model and will need different implementations. One such example is the JamfAuthToken. But for those, we can fall back to implementing URLSession and Codable functions directly.

In the next part, we will use the new protocol to add more Jamf API objects to the command line tool. We will also learn how to get the username and password out of the keychain, so we do not have to keep those in clear text the code anymore. After that, we finally have the foundation to tackle building an app with a user interface.

Continue taking your management workflows higher, further, faster with Jamf Pro!

Photo of Armin Briegel
Armin Briegel
Jamf
Armin Briegel, Senior Consulting Engineer
Subscribe to the Jamf Blog

Have market trends, Apple updates and Jamf news delivered directly to your inbox.

To learn more about how we collect, use, disclose, transfer, and store your information, please visit our Privacy Policy.