Working with File Wrappers in Swift

July 17th, 2018

Filed under: Cocoa, Mac Development | Be the first to comment!

Back in 2010 I wrote an article on working with file wrappers. Recently I needed to work with file wrappers, and I noticed the file wrapper code is much different in Swift so I figured working with file wrappers in Swift would be a good topic for an article.

This article assumes you’re creating a document-based application. I have only used file wrappers in document-based applications.

What Is a File Wrapper?

A file wrapper is a bundle, which is a collection of one or more directories that looks like a single file in the Finder. Most Mac applications are bundles. You can examine their contents by selecting an application, right-clicking, and choosing Show Package Contents.

When should you use a file wrapper? Use a file wrapper when you want to save your application’s document data in multiple files. A level editor for a game may want to save the level layout, the level’s enemy list, and the level’s treasure list in separate files.

The simplest file wrapper is a single directory that stores all the files. But you can create multiple directories to store your files to keep them better organized.

There are three tasks to perform to add file package support to a document-based application. First, you must tell Xcode that your document type is a file wrapper. Second, you must implement the fileWrapperOfType method in your NSDocument subclass to save to a file wrapper. Third, you must implement the readFromFileWrapper method in your NSDocument subclass to open a file wrapper and read its contents.

Making Your Document Type a File Wrapper

You can access your document type from Xcode’s project editor.

  1. Select your project from the project navigator to open the project editor.
  2. Select the application target from the target list on the left side of the project editor.
  3. Click the Info button at the top of the project editor.

DocumentTypeAsBundleHighlighted

Go to the Document Types section and select the Bundle checkbox. Now your document will be saved as a file wrapper.

Writing a Document File Wrapper

To write a document file wrapper, you must override and implement the fileWrapperOfType method. Here is the Swift signature for fileWrapperOfType.

override func fileWrapper(ofType typeName: String) throws -> FileWrapper

The file wrapper fileWrapperOfType returns is the root directory for the file wrapper.

In your implementation of fileWrapperOfType, you must perform the following tasks:

  • Create a directory file wrapper.
  • Get your app’s data into a Data object.
  • Create a file wrapper and add the file to a directory file wrapper.

Creating a Directory File Wrapper

At a minimum you must create a root directory for the file wrapper. To create a directory in a file wrapper, call the FileWrapper method directoryWithFileWrappers and supply an empty Swift dictionary.

let rootDirectory = FileWrapper(directoryWithFileWrappers: [:])

If you want to add any other directories to your file wrapper, call directoryWithFileWrappers again and supply an empty Swift dictionary.

let htmlDirectory = FileWrapper(directoryWithFileWrappers: [:])

The root directory in a file wrapper does not need a name, but any other directories you create need a name. Set the filename or preferredFilename property for the directory file wrapper to name the directory. The following code gives the HTML directory the name HTML:

htmlDirectory.filename = "HTML"

Getting Your Data into a Data Object

Files in file wrappers store their data in a Data (formerly NSData) object. Converting to Data depends on what you are storing, but the following code converts a string to Data:

let htmlData = htmlString.data(using: .utf8)

Creating a File Wrapper

To create a file wrapper for a regular file, call the FileWrapper method regularFileWithContents and supply the Data object that contains the file’s data.

let wrapper = FileWrapper(regularFileWithContents: htmlData)

Set the filename or preferredFilename property to name the file.

wrapper.filename = "index.html"

In a real app you won’t be hardcoding filenames much. Suppose you have a document that has a list of articles. You would use the title of the article as the filename instead of giving the file a specific name. Remember that you use file wrappers to store multiple files. If you have 20 files to save, hardcoding the name of each file is going to be a pain.

After creating the file wrapper, call the directory’s addFileWrapper method to add it to the directory.

htmlDirectory.addFileWrapper(wrapper)

Call addFileWrapper to add a subdirectory to a file wrapper. The following code adds the HTML directory to the root directory:

rootDirectory.addFileWrapper(htmlDirectory)

Reading from a File Wrapper

At this point you know how to write to a file wrapper. The next step is to read the file wrapper. To read the file wrapper, you must override the readFromFileWrapper method. Here is the signature for readFromFileWrapper.

override func read(from fileWrapper: FileWrapper, ofType typeName: String) throws

The fileWrapper argument is the root directory of the file wrapper.

The fileWrappers property contains the contents of a file wrapper. The contents appear in a Swift dictionary, where the key is the name of a file or directory and the value is a file wrapper, which can be either a file or a directory.

To access a directory or file from the dictionary, supply the key as the dictionary subscript. The following code shows how to access a HTML directory inside the root file wrapper:

if let wrappers = fileWrapper.fileWrappers {
    let htmlDirectory = wrappers["HTML"]
}

Make sure the subscript you supply matches the filename or preferred filename you gave to the file or directory. If you make a typing mistake, you won’t be able to read from the file wrapper.

Reading the Individual Files

Use the fileWrappers property to access the individual files in a file wrapper directory. The following code provides a list of files inside a HTML directory:

htmlFiles = htmlDirectory.fileWrappers

The contents appear in a Swift dictionary, where the key contains the name of the file and the value contains a file wrapper.

Use the regularFileContents property to access the contents of a file. The regularFileContents property has type Data?, which means it’s either a Data object or nil. By using regularFileContents, you can get the Data object.

if let homepage = htmlFiles["index.html"] {
    let homepageData = homepage.regularFileContents
}

Loading the Data

Accessing the file wrapper’s regularFileContents property gives you a Data object to load the file’s contents in your app. Loading the file’s contents depends on the type of data being stored, but the following code loads a file’s contents into a string variable:

let htmlString = String(data: homepageData, encoding: .utf8)
Facebooktwittergoogle_plusredditmail

Xcode Turns on the App Sandbox for New Cocoa Projects

July 11th, 2018

Filed under: Cocoa, Mac Development, Xcode | Be the first to comment!

I ran into an issue with a new document-based Cocoa app throwing exceptions when I tried to save the document. It turns out the issue was the App Sandbox, which is turned on when you create a new Cocoa application project in Xcode.

You can see the App Sandbox settings by selecting your project from the project navigator to open the project editor. Select your target and click the Capabilities button at the top of the editor. The following screenshot shows the initial settings:

AppSandboxDefaultSettingsHighlighted

Notice that the App Sandbox initially doesn’t give your app access to anything. It won’t let you print or make any network connections. Surprisingly the App Sandbox does not initially give you permission to open a Save panel to let someone choose where to save a document on their Mac. Apparently opening a Save panel is insecure. Access to user selected files is read only initially. To allow a Save panel to open, you must change the permission for user selected files to Read/Write.

Summary

If Xcode is throwing exceptions when your Cocoa app does simple things like print, open a Save panel, or show a website in a web view, it might not be a problem in your code. It might be the App Sandbox.

There are two ways to work around the App Sandbox. The first workaround is to turn off the App Sandbox. The second workaround is to give your app permission to do things like print and display web content in the App Sandbox.

Facebooktwittergoogle_plusredditmail

Getting Started with Unit Testing in Swift 4

June 13th, 2018

Filed under: Cocoa, iOS Development, Mac Development, Xcode | Be the first to comment!

One of the most read articles on this blog is An Introduction to Swift Unit Testing. The article is almost four years old and uses Swift 1. Because of that I have decided to build upon that article here, updating the code to Swift 4 and adding an example project to unit test.

The Project

I am going to add unit tests to TemperatureConverter, a project that converts temperatures from Celsius to Fahrenheit. I wrote TemperatureConverter as an introduction to Cocoa in my Creating a Simple Mac Application Using Cocoa, Swift, and Storyboards article. You can either go through the article and create the project, or you can download the project from GitHub. The unit-testing branch contains the unit tests.

If you go through the article and create the project, make sure you select the Include Unit Tests checkbox when creating the project. You need a unit test target to do unit testing in Xcode.

Whether you follow the article and create the project or download it from GitHub, keep in mind that I removed the UI test target in my version of the project. I don’t want the additional target to get in the way. If you download the project from GitHub and keep the UI test target, you may get build errors when you unit test. The errors occur because I set the deployment target of the project to macOS 10.10, but Xcode UI tests require 10.11. Either change the deployment target to 10.11 or remove the UI test target from the project.

The Unit Tests

In the course of this article, I’m going to add the following unit tests to the TemperatureConverter project:

  • A test that the freezing point of water is 32 degrees Fahrenheit.
  • A test that the boiling point of water is 212 degrees Fahrenheit.
  • A test for negative Celsius temperatures.

The Unit Test File

When you create a project in Xcode that has a unit test target, Xcode creates a unit test file for you. Select the file TemperatureConverterTests.swift to open the unit test file. You should see the following test functions:

  • setUp
  • tearDown
  • testExample
  • testPerformanceExample

You can remove the testExample and testPerformanceExample functions. You’re not going to use them.

The setUp function gets called before every test. If you need to initialize some properties before running your tests, put that code in setUp.

The tearDown function gets called after every test. If there’s any cleanup work to do after running your tests, put that code in tearDown. I’m not going to use setUp or tearDown in this article.

Make sure you see the following line of code before the declaration of the unit testing class:

@testable import TemperatureConverter

If you don’t see it, add that line of code. You need to import the module for the app so you can access the app’s classes in your unit tests.

The First Unit Test

Now it’s time to write the first unit test. Swift unit test functions have the following properties:

  • They start with the word test.
  • They take no arguments.
  • They return no value.

The first unit test is going to test the freezing point of water. You have to perform the following steps to create the unit test:

  1. Create a view controller because the view controller contains the function to convert the temperature.
  2. Supply a Celsius temperature, which will be 0 for this test.
  3. Convert the Celsius temperature to Fahrenheit.
  4. Perform an assertion that the converted temperature equals the freezing point of water.

Xcode unit test assertions start with XCTAssert. If you type XCTAssert in Xcode, the code completion should show you a list of all the possible assertions. The assertion to call for the first test is XCTAssertEqual. XCTAssertEqual asserts that two values are equal. Swift has multiple versions of XCTAssertEqual. The version I use takes the following arguments:

  • Two floating-point values.
  • An accuracy threshold to determine if the two values are equal. Floating-point math can result in values with slight differences, such as giving you the value 31.99999999 instead of 32.0. With a suitable accuracy threshold, 31.99999999 and 32.0 will test as equal.
  • An error message that appears if the test fails.

If this test fails, you want to know what the converted temperature is. To print the value of a variable in an error message, use the following notation:

\(variableName)

The following is the initial version of the test to make sure the test fails:

func testWaterFreezingPoint() {
    let vc = ViewController()
    let celsiusTemperature = Float(0)
    let fahrenheitFreezingPoint = Float(40)

    let fahrenheitTemperature = vc.convert(celsius: celsiusTemperature)
    XCTAssertEqual(fahrenheitTemperature, fahrenheitFreezingPoint, accuracy: 0.01, 
        "The freezing point of water should be 40 degrees Fahrenheit. 
        Fahrenheit Temperature: \(fahrenheitTemperature)")
}

To run the test choose Product > Test in Xcode. The test should fail with an error message telling you that the Fahrenheit temperature is 32.0 degrees. Click the Red icon on the right side of the editor to see the error message. To make the test pass, change the value of fahrenheitFreezingPoint from 40 to 32. You should also change the error message in the assertion to say that the freezing point of water should be 32 degrees.

After making the corrections, choose Product > Test. The test should pass. By supplying the value 0.01 for the accuracy argument, the test will pass if the converted temperature is greater than 31.99 and less than 32.01.

The Second Unit Test

The second test tests the boiling point of water. It’s going to be similar to the first test.

  1. Create a view controller because the view controller contains the function to convert the temperature.
  2. Supply a Celsius temperature, which will be 100 for this test.
  3. Convert the Celsius temperature to Fahrenheit.
  4. Perform an assertion that the converted temperature equals the boiling point of water.

The main difference is using the boiling point of water instead of the freezing point. The following code tests the boiling point of water and should pass:

func testWaterBoilingPoint() {
    let vc = ViewController()
    let celsiusTemperature = Float(100)
    let fahrenheitBoilingPoint = Float(212)

    let fahrenheitTemperature = vc.convert(celsius: celsiusTemperature)
    XCTAssertEqual(fahrenheitTemperature, fahrenheitBoilingPoint, accuracy: 0.01, 
        "The boiling point of water should be 212 degrees Fahrenheit. 
        Fahrenheit Temperature: \(fahrenheitTemperature)")
}

The Final Unit Test

I have one last test to write for this article. The first two tests used non-negative Celsius temperatures. The last test uses a negative Celsius temperature to see if the temperature conversion works.

func testNegativeCelsiusTemperature() {
    let vc = ViewController()
    let celsiusTemperature = Float(-10)
    let fahrenheitExpected = Float(14)

    let fahrenheitTemperature = vc.convert(celsius: celsiusTemperature)
    XCTAssertEqual(fahrenheitTemperature, fahrenheitExpected, accuracy: 0.01, 
        "-10 degrees Celsius should equal 14 degrees Fahrenheit. 
        Fahrenheit Temperature: \(fahrenheitTemperature)")
}

I supply a temperature of –10 degrees Celsius, which should convert to 14 degrees Fahrenheit.

Where to Go from Here?

Three tests do not make an exhaustive test suite. You can practice creating unit tests by adding to the three tests I wrote in this article. I can think of at least two more tests to write: one to test a Celsius temperature that yields a non-integer Fahrenheit temperature and one to test a non-integer Celsius temperature.

To learn more about unit testing with XCTest, read the XCTest documentation, which you can also read in Xcode.

Facebooktwittergoogle_plusredditmail

Resources for Creating Custom Instruments with Xcode 10

June 11th, 2018

Filed under: Instruments, Xcode | Be the first to comment!

At WWDC 2018 Apple unveiled improved support for creating custom instruments for Instruments. The following list contains helpful resources from Apple on how to create custom instruments:

Facebooktwittergoogle_plusredditmail

Is There a More Misused Stack Overflow Tag than Xcode?

June 8th, 2018

Filed under: Uncategorized | Be the first to comment!

I tend to focus on Stack Overflow tags in the Apple ecosystem so there may be other tags that are misused. But for Apple developers the Xcode tag is misused constantly. If you go to the Stack Overflow tag for Xcode page, you will see the following explanation for the Xcode tag at the top of the page:

Xcode is Apple’s integrated development environment (IDE). USAGE NOTE: Use this tag only for questions about the Xcode IDE itself, and not for general Mac or iOS programming topics. Use [cocoa] for Mac programming questions, and [cocoa-touch] or [ios] or [Swift] for iOS programming questions.

The tag should be used only for questions involving the Xcode tool. iOS development questions should use the iOS or Cocoa Touch tags. But if you read through the questions tagged with Xcode, you’ll notice most of the questions are about iOS development and have nothing to do with Xcode besides the fact that the people asking the questions are using it.

What’s the point of having an explanation of the tag if most people ignore it?

Facebooktwittergoogle_plusredditmail