This chapter covers
All’s well and good reading a book or following a tutorial, but in the real world things go wrong. And often! This is your chance to put your detective hat on and investigate.
In this chapter, we’ll look at what to do when things go wrong by using debugging. We’ll also look at how to prevent things from going wrong with testing.
Along the way, we’ll explore additional concepts:
A friend has kindly offered to look at your app and see if they can find any bugs. You sent them a link to the GitHub repo for your Xcode project, and a few days later you got this email in return:
Hey—I’ve had a look at the app for you. It’s looking good, but I also found a few odd problems:
- The book edit form was working well to begin with, but then it started crashing. Don’t know what that’s about.
- The Cancel button in the book edit form crashes the app.
- After you add an image and save it, the next time you edit the book and save it, the book cover seems to disappear ... strange?
Oh, I also made a couple of little improvements here and there. Hope that’s okay!
- I used a cool third-party framework to detect a nice color palette in the cover art of each book, to use in styling the table view cells and the book edit form. I’ve also added properties for these colors in the Book class. The app seems to freeze, though, for a couple of seconds when you add an image. Is there something you can do about that?
- I added a nice little three-page help section to onboard the app, using a page view controller. It automatically triggers when you first open the app, and you can reopen it with a Help button. There should be a title, blurb, and image, but for some weird reason, only the images are displaying.
Oh, and you should probably add some tests.
Sorry I ran out of time to fix everything up. All the best with it, I look forward to downloading it from the App Store!
Oh, here’s the repo with my updates: https://github.com/iOSAppDevelopmentwithSwift-inAction/Bookcase.git (Chapter15.1.UpdatesNeedFixing).
Well, that was a nice surprise. Your friend made a couple of nice additions to the app. Great! But it seems the app has been left in a buggy state. That email contains a lot of information; let’s go through it step by step, check out what they’ve done, and explore what needs fixing.
The book edit form was working well to begin with, but then it started crashing. Don’t know what that’s about.
Let’s confirm what your friend is saying about the app crashing.
Bam! Your friend was right—the app crashes!
When Xcode crashes, it automatically enters debugging mode (see figure 15.1). Debugging mode can be intimidating, especially at first. Let’s break it down.
Debugging mode consists of
Don’t worry, this has only been a short summary of these tools. In a moment, we’ll look at each in turn.
How does Xcode know to automatically show you the debug navigator and the debug area when the app crashes? Well, it’s all defined in special Xcode preferences called behaviors. Use behaviors to request that Xcode performs specific actions when specific events occur. Xcode comes with certain behaviors already set up for you by default.
Let’s look at the default behavior that opens the debug navigator and debug area. Select Xcode > Behaviors > Edit Behaviors. In the events menu on the left, select Running > Pauses. This behavior is triggered when a running app is paused, such as when the app crashes! In the actions menu on the right, you can specify actions to perform when this event occurs. In addition to showing the debug navigator and debug area, you could, for example, play a sound, display a system notification, or even have an announcement spoken to you.
Sometimes, such as in this case, the red line freezes on your AppDelegate class, indicating that the problem probably occurred in initial setup. One common reason for this is a problem with the storyboard. Let’s look at the console for clues.
At first glance, the output in the console after a crash looks crazy complicated. To give yourself a shock, take a glance at figure 15.2. But don’t panic! You’ll see a number of strange symbols, numbers, and unfamiliar syntax. Where to start?
The trick in interpreting this output is learning what you can ignore 90% of the time and where to find the most relevant information.
The text that automatically outputs to the console when your app crashes is made of two main parts that answer two important questions:
I’ve organized the console output in figure 15.3. I separated the two main parts and emphasized part of the output to help you focus on what’s most important.
First, what caused the problem? The exception information should answer this important question, and ironically, it’s often scrolled offscreen by the call stack! Ignore the time codes and memory addresses and look for the description of the exception in English. According to the exception information in this case, there was an NSUnknownKey-Exception for the key titleL in the BookViewController.
Great—the English description of the exception information is often all you’ll need to look at after a crash, but sometimes it helps to also look at what was happening at the time of the crash. The call stack is a path of method calls called frames that lead to a certain location in the code. You can use the call stack to trace the path backward from the most recent frame marked with a 0 at the top, down to the least recent frame at the bottom.
To identify each frame in the call stack, each line gives you the framework, origin (usually object and method), line number, and even the memory address of each call. See figure 15.4 for a close-up of frame 5.
Calls originating from your own code will have your project name at the left. Note in the call stack that only one call originates from your project, indicated by the project name Bookcase. Look for main at line 29 of figure 15.3.
The main call is a special one—main represents the main entry point for your app, which in your project (and most others as well) is the App-Delegate class. If you take a close look at the AppDelegate class in your project, you’ll notice that it’s preceded by the keyword @UIApplicationMain. This keyword defines the AppDelegate as your app’s entry point. You’ll find this in the call stack too, at line 28.
Sometimes the call stack can give you a peek behind the curtain of certain classes in the iOS SDK that aren’t available to developers. If you look through the objects and method calls in the call stack, you might get an idea as to what was happening when the unknown key exception occurred. Perhaps the connect call to the UIRuntimeOutletConnection object at line 5 could be a clue. Although you don’t have documentation for this object, you could make a reasonable guess by its name that this object is involved in connecting outlets, and perhaps this has something to do with your crash. The plot thickens!
Let’s revise your clues. You know that an outlet problem likely exists in BookViewController related to the key titleL. Let’s look at the storyboard and try to dig deeper.
First problem solved, what’s next?
The Cancel button in the book edit form crashes the app.
With the app running and the book form open, select the Cancel button. Your friend was right!
Another long crash log fills the console, but this time you have a better idea of what to look for. Let’s start with what caused the problem. With memory addresses removed, the exception information reads thus:
-[Bookcase.BookViewController touchCancel:]: unrecognized selector sent to instance
It appears that in the BookViewController class, a selector (that is, a method) called touchCancel is being called but not recognized. Why would that be, and what was happening at the time? You probably have enough information to take a good, educated guess, but let’s look at a portion of the call stack for more clues. See figure 15.8—again, I’ve emphasized part of the output to help you focus on more-interesting details.
Note that sending an action for an event triggered by a UIControl seems to be a theme. The event itself seems to be a touch, according to frame 11, and the control seems to be a UIBarButtonItem.
Let’s revise all of our clues again. When a bar button item in the scene connected to the BookViewController class (assumedly the Cancel button) tries to call the touchCancel method, it’s not recognized. Let’s look at the storyboard to get a clearer idea of the problem.
This time, the app should act as expected, closing the book edit form scene.
What’s next, detective?
After you add an image and save it, the next time you edit the book and save it, the book cover seems to disappear ... strange?
First, check that you can replicate the problem.
Run the app, open a book with a cover image (you’ll have to add a cover image for a book first if none of your sample books have cover art), and select Save. The book image returns to the default cover image. “Strange” is right! What could be happening?
Your immediate suspicion is that for some reason, an existing book cover isn’t being used when the BookViewController generates a book to save. Let’s confirm that by examining the bookToSave variable in the BookViewController class in the touchSave method.
As is so often the case in Xcode, there are many different ways to examine the contents of a variable. Let’s look at a few now, beginning with a method that you’ve seen before, the print method.
To examine the bookToSave variable, let’s print its contents to the console with the print method.
print("Saving book: (bookToSave)")
Saving book: Book(title: "Five on Brexit Island", author: "Enid Blyton", rating: 3.0, isbn: " 9781786488077", notes: "", image: Optional(<UIImage: 0x1c02aeb20>, {128, 202}), backgroundColor: UIExtendedGrayColorSpace 1 1, primaryColor: UIExtendedGrayColorSpace 0 1, detailColor: UIExtendedGrayColorSpace 0 1)Well, that’s great. By default you’re seeing the value of every property of the object, down to its background color. Sometimes, however, when you print an object, you might not need to see its every last detail. You might prefer to see just the important stuff. It would probably be sufficient detail to identify a book, for example, by the title and author. To resolve this bug, you might also want to see whether or not this book has a cover image. There’s a neat little trick for adjusting the string that’s output when you print an object. If your custom type adopts the CustomStringConvertible protocol, you can provide a description property that describes your object as a String, and it will automatically be used by print.
override var description: String { return "(title) by (author) : (hasCoverImage ? "Has" : "No") cover image" }
Saving book: Five on Brexit Island by Enid Blyton : No cover imageIt appears that your suspicion was correct. For some unknown reason, the book object to be saved isn’t being generated with its cover image.
Classes that subclass NSObject, such as UIView, automatically adopt the CustomStringConvertible protocol and contain a description property. To provide your own description, you’ll have to override the default description property.
An alternative approach to print that certain developers prefer is the NSLog statement. While NSLog is a little slower, it does add a timestamp to the log and stores logging data to disk. Having a log history can be useful, but makes it even more important to ensure you remove all NSLog calls from your code before publishing your app to the App Store.
To diagnose problems in your app, sometimes it can help to use a file and line breakpoint to pause execution at a line in your code. File and line breakpoints are ultra--useful for
You’ll use file and line breakpoints to analyze why books aren’t being saved with their images. Let’s start by looking at right after a book object is generated for saving data from the book edit form.
Be careful not to click on the breakpoint again; this will cause the indicator to turn light blue and the breakpoint will toggle to a disabled state.
The same way it did earlier when the app crashed, the Running > Pauses behavior launches into action, automatically opening the debug navigator and debug area for you. One difference you may notice is that the paused line of execution is green this time (see figure 15.12).
Most commonly, you’ll use breakpoints to pause execution at a specific line of code, but they’re capable of doing so much more.
For example, exception breakpoints break execution whenever specific types of exceptions occur, and symbolic breakpoints break execution whenever a specific method is called on all subclasses of a certain type of class. You have to add these types of breakpoints in the breakpoint navigator.
Your breakpoint could be set up to trigger only if a certain condition is true or after a certain number of times. Breakpoints can also be set up to perform one or more actions, such as output to the console or play a sound. Ironically, breakpoints don’t necessarily break execution. If you like, after performing an action, a breakpoint can automatically continue.
Edit your breakpoints by double-clicking on the breakpoint indicator in the source editor or the breakpoint navigator.
Now that your app has paused execution, you can examine the state of the app’s variables. Checking the book object at this point may help diagnose the problem with saving a book cover.
You can use several approaches for examining the state of variables while the app is paused:
We’ll look at each of these in turn. Let’s look first at the variables view.
The variables view contains variables in the context of where the app is currently paused. Instance variables of BookViewController will be contained within the self property, while local variables are shown at the top level. As the book object is unwrapped with optional binding, it’s considered a local variable.
At the left of several variables, you’ll see a disclosure triangle, indicating that you can “open up” the variable to have a closer look at its contents.
This makes sense, as you selected a book with no cover.
Now, let’s resume execution so that you can add an image to this book.
Above the variables view, you’ll find the debug bar, which contains several controls useful for controlling the execution of your app (see figure 15.14).
Table 15.1 lists several elements that could use extra explanation.
Element |
Description |
---|---|
Toggle breakpoints | For convenience, toggle all breakpoints on or off. |
Continue/Pause | Continue execution of the app. |
Step buttons | Three skip buttons allow you to execute your code step by step. Step over and step into differ as to how they act when there’s a method call in the current line. Step into will step through every line of the method, whereas step over will interpret the entire method as one step. Step out, on the other hand, executes the rest of the current function as one step and pauses execution again when it exits the function. |
Debug view hierarchy | View the hierarchy of views in the app. We’ll come back to this soon. |
Memory graph | Visualize the memory allocations in the app. |
Simulate location | Simulate that your app is running from an alternative location. |
Jump bar | Use the jump bar to examine your app state from the context of different threads and stack frames. |
Let’s use the controls in the debug bar to resume execution of the app.
The app should pause execution again after generating a new book to save in the local bookToSave variable. Let’s examine this variable for more clues.
Let’s explore examining variables using another technique, called Quick Look.
The app should pause once again at the breakpoint in the viewDidLoad method after unwrapping the book object to edit.
Let’s use yet another technique for examining the contents of the book object.
Next to the Show Quick Look button, is another useful button that appears as an “i” in a circle. This is called the Print Description button. If you select a variable in the variables view, and select the Print Description button, you get exactly the same output in the console as you did earlier when you printed a variable in code.
This time, you’ll examine the contents of the book object with the Print Description button.
The description property of the Book object that you set up earlier will output to the console (see figure 15.16). Covering all bases, the properties of the Book object also output to the console.
Well, according to the output, it seems no problem exists with the book object. You’ll have to continue execution and save the book to see if the problem is happening there.
But first, what’s that strange lldb message that crops up in the console?
The console is much more than an area for receiving debug logs and outputting print messages. It’s a window into the powerful command-line debugger called lower-level debugger (LLDB), and the lldb message is a prompt for you to enter commands.
Many debugging features in this chapter are GUI representations of lower-level commands that are available to you as command-line commands in the console.
For example, the Print Description button you used to explore details on the book object uses the LLDB po command under the hood.
po bookYou should see the same description appear for Book that you saw for Print Description (see figure 15.17).
p bookSee figure 15.18 for the result from the p command. This time, you should see a much more detailed output of the contents of the book variable.
Once again, the app should pause execution right after generating a book to save. Let’s use one final technique to examine the contents of the book to save.
Believe it or not, there’s yet another way to examine the contents of your variable, and this time, you don’t even need the variables view or the console!
With app execution paused, you can point your cursor in the source editor at a variable you want to examine, and a data tip for that variable will pop up. From there, you can open the variable the way you did in the variables view, select to show Quick Look, or select the Print Description button.
Notice that this time, the image property of bookToSave is equal to nil. You seem to be getting closer to the problem!
Why would the image property be nil? Look at how the bookToSave object is generated—the cover image comes from the coverToSave property. Okay, where’s this property set?
A quick search for the coverToSave property uncovers the problem. The coverToSave property is only set in two places: when the user selects a photo or image for the book or when the booksService returns an image after the user scans a barcode. What about books that already have an image? The coverToSave property is never set.
if let book = book { navigationItem.title = "Edit book" bookCover.image = book.cover if book.hasCoverImage { coverToSave = book.cover } ...
Many methods exist for examining the contents of a variable, each with their own advantages, as shown in table 15.2.
Element |
Best for |
---|---|
If you prefer to not pause execution of your app | |
NSLog | If you want timestamps on your console logs and a log history |
Quick Look | If you want a visualization of the variable’s contents |
Data tips | If you’re short on screen space and prefer to hide the debug area, or if you prefer to explore variables in the context of your source code |
p command in LLDB | If you need information beyond what the default description returns for the variable |
Variables view | If you want a visual representation of the hierarchy of variables in your app |
Let’s check out your friend’s next piece of feedback.
I used a cool third-party framework to detect a nice color palette in the cover art of each book, to use in styling the table view cells and the book edit form. I’ve also added properties for these colors in the Book class. The app seems to freeze, though, for a couple of seconds when you add an image. Is there something you can do about that?
Sounds like quite an interesting addition to the app that your friend has contributed; see figure 15.20 to see it in action.
The freezing interface isn’t so useful, though!
If your app is having playback problems such as a stuttering or freezing interface, the cause may be that you’re performing long operations in the main thread and therefore blocking your interface from updating.
Let’s explore this theory with the debug gauges.
If your app is experiencing performance issues, it can be a good idea to look at your app’s use of system resources. One way to do this is with the debug gauges that you can find in the debug navigator. The debug gauges give you a good summary of how your app is using the device’s CPU, memory, disk access, and network calls. You can click on a gauge to get a more detailed report on your app’s use of this system resource.
You’re going to examine your app’s use of the CPU when adding an image to diagnose why the user interface is freezing temporarily.
Note that the majority of the work is going on in thread 1. Thread 1 is also known as the main thread and is where the user interface is updated. As you’ve seen, if your app is busy working on a time-consuming algorithm such as image color detection in the main thread, the app’s user interface will be prevented from updating and responding to user interaction.
It has become clear that a certain operation that your friend introduced needs to be moved to a background thread. But which operation? You could spend time hunting down this method in the code, but you have yet another debugging trick up your sleeve!
Xcode provides developers with a library containing debugging tools called instruments that build on and supplement the performance and testing tools that are available in debug gauges.
To get a feel for instruments, we’ll have a look at the time profiler instrument. The time profiler measures how frequently your app performs different processes. You could use the time profiler to find any long-running processes that could be holding up the main thread.
Although you could open the time profiler up by selecting Xcode > Open Developer Tool > Instruments > Time Profiler, you have a shortcut right in front of you in the CPU debug report—at the top-right corner is a Profile in Instruments button.
Now that you know for sure what was causing the app to freeze, let’s move it to a background thread.
DispatchQueue.global().async { 1 let colors = image.getColors() DispatchQueue.main.async { 2 self.receiveColors(colors:colors) } }
I think you’re ready for your final debugging challenge!
I added a nice little three-page help section to onboard the app using a page view controller. It automatically triggers when you first open the app, and you can re-open it with a Help button. There should be a title, blurb, and image, but for some weird reason though, only the images are displaying.
Again, this is a nice improvement that your friend has contributed. However, as mentioned, there’s a visual issue—the title and blurb for each page aren’t appearing. Your friend sent through an image showing how the help pages should look, and how they do look (see figure 15.24).
Your friend isn’t a fan of the storyboard and has set up the three pages entirely in code. These three view controllers make use of convenience methods in a structure called InstructionFactory to perform the repetitive tasks of building their interface. They then use a convenience method in another structure called Content-LayoutMachine that automatically sets up their auto layout constraints.
It’s all sophisticated, but what’s going wrong—where’s the title and blurb?
It’s a good idea to walk your users through how to use your app. This sort of introduction is called onboarding your users. Frequently, onboarding requires multiple pages, and the most common approach for displaying these pages is with a page view controller. Rather than the default page turn, it’s more common to use a scroll transition style and a page control at the bottom of the screen, indicating the page you’re currently viewing.
Pages are represented by view controllers, and the next and previous pages are loaded, ready for the user to scroll to them.
Your friend has been kind enough to set up such a page view controller for you in the Bookcase project, but for future reference, these are the general steps you’d take:
When there’s a visual problem with your app, a good place to look for answers is the Debug View Hierarchy. The Debug View Hierarchy helps you visualize your app’s interface and interact with it by separating the layers of the interface and rotating them in 3D space.
You’ll use the Debug View Hierarchy to see if you can get a better idea of what’s going on in the interface of the help pages.
The Issue Navigator gives you more detail on any pending issues. Until now, you’ve probably only noticed build-time issues, but Xcode can also report runtime issues. Ambiguous layouts, problems with threading, and problems with memory allocation can all trigger runtime issues.
Let’s examine the runtime issues to further diagnose the problem with your app’s layout.
Select the Runtime Issues tab in the Issue Navigator. You should find that several labels have ambiguous vertical positions (see figure 15.28).
Select one of the issues and open the Size Inspector. Look at the Constraints section. In addition to reiterating the layout issue, the existing constraints are specified. The description of the ambiguous layout issue makes sense; there doesn’t appear to be a constraint specified for vertical position! See figure 15.29.
Now that you know that certain views aren’t being provided with vertical position constraints, you have an idea of the problem to look for in the layout code.
static func verticalLayout(to rootView: UIView,views: [UIView]) { ... var previousView: UIView? 1 ... for view in views { 2 if let previousView = previousView { 3 constraints += [view.topAnchor.constraint( 4 equalTo: previousView.bottomAnchor) ] 4 } ... } ... }
Going through the logic, you see a significant problem. The previousView is never set, so the constraint is never added!
static func verticalLayout(to rootView:UIView,views:[UIView]) { var previousView:UIView? for view in views { if let previousView = previousView { constraints += [view.topAnchor.constraint( equalTo: previousView.bottomAnchor) ] } previousView = view 1 } }
If you’d like to compare your project with mine at this point, you can check mine out at https://github.com/iOSApp-DevelopmentwithSwiftinAction/Bookcase.git (Chapter15.2.Debugged). Don’t forget to run carthage update to update third-party code.
Well, you solved all the bugs your friend reported in their email, detective. Congratulations! But what was that your friend said about testing?
It’s so easy to make changes to your app to make a minor fix or improvement, only to realize later that you’ve inadvertently caused a major problem elsewhere in your app. Solving one problem can create another, or, like your friend earlier in this chapter, even resting your hand on the Z key for a second could cause it to crash!
Testing your app manually but comprehensively after every small change would be a tedious prospect. Xcode provides you with the tools for automating this testing process.
Xcode can perform two types of tests:
Within both categories, Xcode can focus from two perspectives:
Let’s add tests to the Bookcase app to help prevent the sort of bugs you’ve seen so far in this chapter and to keep the app working in tip-top shape!
Let’s start by adding unit tests to test that the BooksManager is sorting and searching the books array correctly.
Tests are performed in special targets in your project: one test target for unit tests and another test target for UI tests. Targets can contain multiple test classes, which are useful for grouping related tests. Each test class can contain multiple test methods, each performing a single test.
When you create a project, the project option screen gives you two checkboxes to set up your project with unit tests and UI tests. Selecting these checkboxes automatically adds appropriate testing targets to your project and a test class containing test methods.
Open the Test Navigator to see the tests that come in your project by default (see figure 15.30).
If by chance you didn’t select the testing checkboxes when you created your app, don’t despair—it’s easy enough to add test targets to your project. Select the + symbol at the bottom of the Test Navigator, give the target a name, and select the target to be tested. A test class will automatically be created with the same name as the target.
Let’s use the same menu to add another test class (see figure 15.30) to test the BooksManager class.
To perform tests on the BooksManager class, you first need to set it up. To have complete control over the test data, it’d be a good idea to set that up in the test class, too.
You may have noticed your test class has a setup method. This is a good place to specify any code that you want to run before each test method. This’ll be the perfect place to instantiate the BooksManager and pass in test data to the books array. Because you know that these variables will necessarily be instantiated prior to the test methods, you can confidently set these to implicitly unwrapped optionals.
var booksManager: BooksManager! var bookDaVinci: Book! var bookGulliver: Book! var bookOdyssey: Book! override func setUp() { super.setUp() bookDaVinci = Book(title: "The Da Vinci Code", author: "Dan Brown", rating: 5, isbn: "", notes: "") bookGulliver = Book(title: "Gulliver's Travels", author: "Jonathan Swift", rating: 5, isbn: "", notes: "") bookOdyssey = Book(title: "The Odyssey", author: "Homer", rating: 5, isbn: "", notes: "") booksManager = BooksManager() booksManager.addBook(bookDaVinci) booksManager.addBook(bookGulliver) booksManager.addBook(bookOdyssey) }
You’ve probably noticed a teardown method as well. You can specify any code you want to run after each test method here.
@testable import Bookcase
The errors should go away, and you’re ready to start filling out your test methods.
Let’s start by creating a test method that tests that the booksManager is sorting the books correctly by title.
func testSortTitle() { }
booksManager.sortOrder = .titleGreat, so your test method is set up, but how does it perform a test? To create a test, first consider what you’re expecting as the correct result. In this case, after sorting by title, you would expect that the books array will be sorted in a certain order: “Gulliver’s Travels,” “The Da Vinci Code,” then “The Odyssey.” In Xcode, you express this expectation with what’s called an assertion. The basic assertion is expressed with the XCTAssert method. This method requires a Boolean expression—if it returns true, the test has passed. Conversely, if it returns false, the test has failed.
XCTAssert(booksManager.getBook(at: 0) == bookGulliver) XCTAssert(booksManager.getBook(at: 1) == bookDaVinci) XCTAssert(booksManager.getBook(at: 2) == bookOdyssey)That’s it—you’re ready to run your test! Because your method starts with the word “test,” Xcode automatically recognizes that it’s a test method and indicates this with a diamond beside the method.
func testSortAuthor() { booksManager.sortOrder = .author XCTAssert(booksManager.getBook(at: 0) == bookDaVinci) XCTAssert(booksManager.getBook(at: 1) == bookOdyssey) XCTAssert(booksManager.getBook(at: 2) == bookGulliver) }
Create a functional test method to test searching the books array. You’ll find my solution in the repo coming later in this chapter!
Great! If you make changes to your app now, you can be sure by running your tests that your books should still sort and search correctly.
Unit tests aren’t only about whether a unit of code is correct or incorrect—performance unit tests permit you to accurately analyze the efficiency of a unit of code. Performance tests run a unit of code 10 times and give you the average execution time.
Let’s add a performance unit test to analyze the efficiency of the image color detection algorithm that your friend introduced.
var image: UIImage! override func setUp() { super.setUp() image = UIImage(named: "book") }To analyze the performance of a unit of code, run it in a closure passed to the measure method.
func testColorDetection() { self.measure { self.image.getColors() } }Because this UIImage extension comes from a third-party binary framework that’s not compiled by Carthage for testing, the @testable attribute won’t work.
Because you’re only testing the performance of the method, you aren’t interested in the returned result. The Xcode compiler finds this strange and warns you of the unnecessary function call. To silence the warning, you can explicitly ignore the result by assigning it to an underscore:
_ = self.image.getColors()
User interface testing tests your app from a different perspective than unit testing. While functionality and performance can still be tested, UI testing shifts the focus from testing units of code to testing the user experience of your app.
Let’s explore UI tests by creating one to test a user experience in your app. If you select the Info button in the book edit form, the ISBN field should appear. If you select the Info button again, it should disappear. Let’s test that this functionality is working correctly.
UI tests are created in a separate target to the app and unit tests.
let addButton = XCUIApplication().navigationBars["Books"].buttons["Add"]This gets a reference in the application to the navigation bar with the title Books, and then within the navigation bar finds a reference to the Add button. With this reference, you can now simulate the user tapping the button.
addButton.tap()This is great, but with all this syntax, all you’ve achieved is a button tap. What happens when you want to test a longer and more complex user experience with multiple interactions? Setup would be a time-consuming and frustrating process. Fortunately, Xcode allows you to record a user experience live and automatically convert to UI test sequences of code. If you entered the addButton code, delete it now. You’re going to set up this UI test by recording it!
XCUIApplication().navigationBars["Books"].buttons["Add"].tap()
let app = XCUIApplication() app.navigationBars["Books"].buttons["Add"].tap() app.scrollViews.otherElements.buttons["More Info"].tap()To check that the ISBN field has been toggled, you’ll need a reference to the ISBN field.
let elementsQuery = app.scrollViews.otherElements elementsQuery.buttons["More Info"].tap() elementsQuery.staticTexts["ISBN:"].tap()Great, with little effort on your part, you know how to reference the ISBN field! You can stop the recording now, because you’re going to finish writing the test yourself!
elementsQuery.staticTexts["ISBN:"].tap() let isbnExists = elementsQuery.staticTexts["ISBN:"].exists elementsQuery.buttons["More Info"].tap()Now, you’re ready to make an assertion. Tapping the Info button should have toggled the existence of the ISBN field in the interface.
XCTAssertNotEqual(elementsQuery.staticTexts["ISBN:"].exists, isbnExists)You’ve set up your first UI test!
The app will run in the simulator, automatically performing the actions defined in the test method. With any luck, it should eventually highlight a successful test with a green tick.
For a user interface to be testable, its interface elements need to have accessibility enabled. But even if accessibility wasn’t required for UI testing, it’s still best practice to ensure that your interface is accessible.
Select an interface element and open the Identity Inspector. There, you’ll find the accessibility panel. Here, you can provide a label to describe the element, a hint to describe the result of interacting with the element, and a unique identifier for the element.
Beneath these properties are a number of trait checkboxes, such as Button, Selected, Image, Search Field, and Static Text. These properties give the operating system a better understanding of how the element is expected to behave.
Adding accessibility properties to the visual elements in your app will open them up to be described by the VoiceOver accessibility app, and enable users with impaired vision to use your app.
If you’d like to compare your project with mine at this point, you can check mine out at https://github.com/iOSApp-DevelopmentwithSwiftinAction/Bookcase.git (Chapter15.3.Tested).
In this chapter, you learned the following: