Debugging Basics

The simplest debugging uses the console. Interpreting the information provided in the console when an application crashes or intentionally logging information to the console allows you to observe and zero in on your code’s failures. Let’s look at some examples of how the console can support your quest for bug-free code.

Interpreting console messages

Time to add some mayhem to the Buggy project. Suppose that after considering the UI for a while, you decide that a switch would be a better control than a button. Open ViewController.swift and make the following changes to the buttonTapped(_:) method.

@IBAction func buttonTapped(_ sender: UIButton) {
@IBAction func switchToggled(_ sender: UISwitch) {
    print("Called buttonTapped(_:)")
}

You renamed the action to reflect the change of control and you changed the type of sender to UISwitch.

Unfortunately, you forgot to update the interface in Main.storyboard. Build and run the application, then tap on the button. The application will crash and you will see a message logged to the console similar to the one on the next page. (We have truncated some of the information to fit on the page.)

2016-08-24 12:52:38.463 Buggy[1961:47078] -[Buggy.ViewController buttonTapped:]:
unrecognized selector sent to instance 0x7ff6db708870
2016-08-24 12:52:38.470 Buggy[1961:47078] *** Terminating app due to uncaught
exception 'NSInvalidArgumentException',
reason: '-[Buggy.ViewController buttonTapped:]: unrecognized selector sent to
instance 0x7ff6db708870'
*** First throw call stack:
(
  0   CoreFoundation   [...] __exceptionPreprocess + 171
  1   libobjc.A.dylib  [...] objc_exception_throw + 48
  2   CoreFoundation   [...] -[NSObject(NSObject) doesNotRecognizeSelector:] + 132
  3   CoreFoundation   [...] ___forwarding___ + 1013
  4   CoreFoundation   [...] _CF_forwarding_prep_0 + 120
  5   UIKit            [...] -[UIApplication sendAction:to:from:forEvent:] + 83
  6   UIKit            [...] -[UIControl sendAction:to:forEvent:] + 67
  7   UIKit            [...] -[UIControl _sendActionsForEvents:withEvent:] + 444
  8   UIKit            [...] -[UIControl touchesEnded:withEvent:] + 668
  9   UIKit            [...] -[UIWindow _sendTouchesForEvent:] + 2747
  10  UIKit            [...] -[UIWindow sendEvent:] + 4011
  11  UIKit            [...] -[UIApplication sendEvent:] + 371
  12  UIKit            [...] __dispatchPreprocessedEventFromEventQueue + 3248
  13  UIKit            [...] __handleEventQueue + 4879
  14  CoreFoundation   [...] __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION
  15  CoreFoundation   [...] __CFRunLoopDoSources0 + 556
  16  CoreFoundation   [...] __CFRunLoopRun + 918
  17  CoreFoundation   [...] CFRunLoopRunSpecific + 420
  18  GraphicsServices [...] GSEventRunModal + 161
  19  UIKit            [...] UIApplicationMain + 159
  20  Buggy            [...] main + 111
  21  libdyld.dylib    [...] start + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException

The message in the console looks pretty scary and hard to understand, but it is not as bad as it first seems. The really useful information is at the very top. Let’s start with the very first line.

2016-08-24 12:52:38.463 Buggy[1961:47078] -[Buggy.ViewController buttonTapped:]:
unrecognized selector sent to instance 0x7ff6db708870

There is a time stamp, the name of the application, and the statement unrecognized selector sent to instance 0x7ff6db708870. To make sense of this information, remember that an iOS application may be written in Swift, but it is still built on top of Cocoa Touch, which is a collection of frameworks written in Objective-C. Objective-C is a dynamic language, and when a message is sent to an instance, the Objective-C runtime finds the actual method to be called at that precise time based on its selector, a kind of ID.

Thus, the statement that an unrecognized selector [was] sent to instance 0x7ff6db708870 means that the application tried to call a method on an instance that did not have it.

Which instance was it? You have two pieces of information about it. First, it is a Buggy.ViewController. (Why not just ViewController? Swift namespaces include the name of the module, which in this case is the application’s name.) Second, it is located at memory address 0x7ff6db708870 (your actual address will likely be different).

The expression -[Buggy.ViewController buttonTapped:] is a representation of Objective-C code. A message in Objective-C is always enclosed in square brackets in the form [receiver selector]. The receiver is the class or instance to which the message is sent. The dash (-) before the opening square bracket indicates that the receiver is an instance of ViewController. (A plus sign (+) would indicate that the receiver was the class itself.)

In short, this line from the console tells you that the selector buttonTapped: was sent to an instance of Buggy.ViewController but it was not recognized.

The next line of the message adds the information that the app was terminated due to an “uncaught exception” and specifies the type of the exception as NSInvalidArgumentException.

The bulk of the console message is the stack trace, a list of all the functions or methods that were called up to the point of the application crash. Knowing which logical path the application took before crashing can help you reproduce and fix a bug. None of the calls in the stack trace had a chance to return, and they are listed with the most recent call on top. Here is the stack trace again:

*** First throw call stack:
(
  0   CoreFoundation   [...] __exceptionPreprocess + 171
  1   libobjc.A.dylib  [...] objc_exception_throw + 48
  2   CoreFoundation   [...] -[NSObject(NSObject) doesNotRecognizeSelector:] + 132
  3   CoreFoundation   [...] ___forwarding___ + 1013
  4   CoreFoundation   [...] _CF_forwarding_prep_0 + 120
  5   UIKit            [...] -[UIApplication sendAction:to:from:forEvent:] + 83
  6   UIKit            [...] -[UIControl sendAction:to:forEvent:] + 67
  7   UIKit            [...] -[UIControl _sendActionsForEvents:withEvent:] + 444
  8   UIKit            [...] -[UIControl touchesEnded:withEvent:] + 668
  9   UIKit            [...] -[UIWindow _sendTouchesForEvent:] + 2747
  10  UIKit            [...] -[UIWindow sendEvent:] + 4011
  11  UIKit            [...] -[UIApplication sendEvent:] + 371
  12  UIKit            [...] __dispatchPreprocessedEventFromEventQueue + 3248
  13  UIKit            [...] __handleEventQueue + 4879
  14  CoreFoundation   [...] __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION
  15  CoreFoundation   [...] __CFRunLoopDoSources0 + 556
  16  CoreFoundation   [...] __CFRunLoopRun + 918
  17  CoreFoundation   [...] CFRunLoopRunSpecific + 420
  18  GraphicsServices [...] GSEventRunModal + 161
  19  UIKit            [...] UIApplicationMain + 159
  20  Buggy            [...] main + 111
  21  libdyld.dylib    [...] start + 1
)

Each row in the list includes a call number, the module name, a memory address (which we have removed to fit the rest on the page), and a symbol representing the function or method. If you scan the stack trace from the bottom up, you can get a sense that the application starts in the main function of Buggy at the line identified with call number 20, receives an event recognized as a touch at call number 9, and then tries to send the corresponding action to the button’s target at call number 7. The selector for the action is not found (call number 2: -[NSObject(NSObject) doesNotRecognizeSelector:]), resulting in an exception being raised (call number 1: objc_exception_throw).

Although this breakdown of the console message is specific to one error type out of many possibilities, understanding the basic structure of these messages will help you make sense of the error messages you will encounter in the future. As you gain more experience, you will start associating error messages with types of problems and you will become better at debugging code.

Fixing the first bug

Reviewing ViewController.swift, you discover that you changed your action method from buttonTapped(_:) to switchToggled(_:), which is why the selector buttonTapped: is not being recognized.

To fix the bug, you have two choices. You could update the action connected to the button on Main.storyboard to match your new action method. Or you could revert the name change on the switchToggled(_:) method. You decide that you do not want a switch after all, so open ViewController.swift and change your method back to its earlier implementation. (Remember what we told you: Make the changes exactly as shown, even if you see a problem.)

@IBAction func switchToggled(_ sender: UISwitch) {
@IBAction func buttonTapped(_ sender: UISwitch) {
    print("Called buttonTapped(_:)")
}

Build and run the application. It works fine … or does it? Actually, there is a problem, which you will resolve in the next section.

Caveman debugging

The current implementation of ViewController’s buttonTapped(_:) method just logs a statement to the console. This is an example of a technique that is fondly called caveman debugging: strategically placing print() calls in your code to verify that functions and methods are being called (and called in the proper sequence) and to log variable values to the console to keep an eye on important data.

Like the cavemen in the insurance commercials, caveman debugging is not as outmoded as the name might suggest, and modern developers continue to rely on messages logged to the console.

To explore what caveman debugging can do for you, log the state of the sender control when buttonTapped(_:) is called in ViewController.swift.

@IBAction func buttonTapped(_ sender: UISwitch) {
    print("Called buttonTapped(_:)")
    // Log the control state:
    print("Is control on? (sender.isOn)")
}

In the @IBAction methods you have written throughout this book, you have been passing in an argument called sender. This argument is a reference to the control sending the message. A control is a subclass of UIControl; you have worked with a few UIControl subclasses so far, including UIButton, UITextField, and UISegmentedControl. As you can see in buttonTapped(_:)’s signature, the sender in this case is an instance of a UISwitch. The isOn property is a Boolean indicating whether the switch instance is in the on state or not.

Build and run the application. Try tapping the button. Oops! You have an unrecognized selector error again.

Called buttonTapped(_:)
2016-08-30 09:30:57.730 Buggy[9738:1177400] -[UIButton isOn]:
unrecognized selector sent to instance 0x7fcc5d104cd0
2016-08-30 09:30:57.734 Buggy[9738:1177400] *** Terminating app due to uncaught
exception 'NSInvalidArgumentException', reason: '-[UIButton isOn]: unrecognized
selector sent to instance 0x7fcc5d104cd0'

The console message begins with the Called buttonTapped(_:) line, indicating that the action was indeed called. But then the application crashes because the isOn selector is sent to an instance of a UIButton.

You can probably see the problem: sender is typed as a UISwitch in buttonTapped(_:), but the action is actually attached to a UIButton instance in Main.storyboard.

To confirm this hypothesis, log the address of sender in ViewController.swift, just before you call the isOn property.

 @IBAction func buttonTaped(_ sender: UISwitch) {
    print("Called buttonTapped(_:)")
    // Log sender:
    print("sender: (sender)")
    // Log the control state:
    print("Is control on? (sender.isOn)")
}

Build and run the application one more time. After tapping the button and crashing the application, check the first few lines of the console log, which will look something like this:

Called buttonTapped(_:)
sender: <UIButton: 0x7fcf8c508bb0; frame = (160 84; 55 30); opaque = NO;
autoresize = RM+BM; layer = <CALayer: 0x618000220ea0>>
2016-08-30 09:45:00.562 Buggy[9946:1187061] -[UIButton isOn]: unrecognized selector
sent to instance 0x7fcf8c508bb0
2016-08-30 09:45:00.567 Buggy[9946:1187061] *** Terminating app due to uncaught
exception 'NSInvalidArgumentException', reason: '-[UIButton isOn]: unrecognized
selector sent to instance 0x7fcf8c508bb0'

In the line after Called buttonTapped(_:), you get information about the sender. As expected, it is an instance of a UIButton and it exists in memory at address 0x7fcf8c508bb0. Further down the log you can confirm that this is the same instance to which you are sending the isOn message. A button cannot respond to a UISwitch property, so the app crashes.

To fix this problem, correct the buttonTapped(_:) definition in ViewController.swift. While you are there, delete the extra calls to print(), which you will not need again.

 @IBAction func buttonTaped(_ sender: UISwitch) {
 @IBAction func buttonTaped(_ sender: UIButton) {
    print("Called buttonTapped(_:)")
    // Log sender:
    print("sender: (sender)")
    // Log the control state:
    print("Is control on? (sender.isOn)")
}

Swift has four literal expressions that can assist you in logging information to the console (Table 9.1):

Table 9.1  Literal expressions useful for debugging

Literal Type Value
#file String

The name of the file where the expression appears.

#line Int

The line number the expression appears on.

#column Int

The column number the expression begins in.

#function String

The name of the declaration the expression appears in.

To illustrate the use of these literal expressions, update your call to print() in the buttonTapped(_:) method in ViewController.swift.

@IBAction func buttonTapped(_ sender: UIButton) {
    print("Called buttonTapped(_:)")
    print("Method: (#function) in file: (#file) line: (#line) called.")
}

Build and run the application. As you tap the button, you will see a message logged to the console that is equivalent to the one below.

Method: buttonTapped in file: /Users/juampa/Desktop/Buggy/Buggy/ViewController.swift
    at line: 13 was called.

While caveman debugging is useful, be aware that print() statements are not stripped from your code as you build your project for release.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset