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 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   UIKitCore        [...] -[UIResponder doesNotRecognizeSelector:] + 302
      4   CoreFoundation   [...] ___forwarding___ + 1013
      5   CoreFoundation   [...] _CF_forwarding_prep_0 + 120
      6   UIKitCore        [...] -[UIApplication sendAction:to:from:forEvent:] + 83
      7   UIKitCore        [...] -[UIControl sendAction:to:forEvent:] + 67
      8   UIKitCore        [...] -[UIControl _sendActionsForEvents:withEvent:] + 444
      9   UIKitCore        [...] -[UIControl touchesEnded:withEvent:] + 668
      10  UIKitCore        [...] -[UIWindow _sendTouchesForEvent:] + 2747
      11  UIKitCore        [...] -[UIWindow sendEvent:] + 4011
      12  UIKitCore        [...] -[UIApplication sendEvent:] + 356
      13  UIKitCore        [...] -[UIApplication sendEvent:] + 371
      14  UIKitCore        [...] __dispatchPreprocessedEventFromEventQueue + 3248
      15  UIKit            [...] __handleEventQueue + 4879
      16  CoreFoundation   [...] __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM[...]
      17  CoreFoundation   [...] __CFRunLoopDoSource0 + 76
      18  CoreFoundation   [...] __CFRunLoopDoSources0 + 556
      19  CoreFoundation   [...] __CFRunLoopRun + 918
      20  CoreFoundation   [...] CFRunLoopRunSpecific + 420
      21  GraphicsServices [...] GSEventRunModal + 161
      22  UIKit            [...] UIApplicationMain + 159
      23  Buggy            [...] main + 111
      24  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   UIKitCore        [...] -[UIResponder doesNotRecognizeSelector:] + 302
      4   CoreFoundation   [...] ___forwarding___ + 1013
      5   CoreFoundation   [...] _CF_forwarding_prep_0 + 120
      6   UIKitCore        [...] -[UIApplication sendAction:to:from:forEvent:] + 83
      7   UIKitCore        [...] -[UIControl sendAction:to:forEvent:] + 67
      8   UIKitCore        [...] -[UIControl _sendActionsForEvents:withEvent:] + 444
      9   UIKitCore        [...] -[UIControl touchesEnded:withEvent:] + 668
      10   UIKitCore       [...] -[UIWindow _sendTouchesForEvent:] + 2747
      11  UIKitCore        [...] -[UIWindow sendEvent:] + 4011
      12  UIKitCore        [...] -[UIApplication sendEvent:] + 356
      13  UIKitCore        [...] -[UIApplication sendEvent:] + 371
      14  UIKitCore        [...] __dispatchPreprocessedEventFromEventQueue + 3248
      15  UIKit            [...] __handleEventQueue + 4879
      16  CoreFoundation   [...] __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM[...]
      17  CoreFoundation   [...] __CFRunLoopDoSource0 + 76
      18  CoreFoundation   [...] __CFRunLoopDoSources0 + 556
      19  CoreFoundation   [...] __CFRunLoopRun + 918
      20  CoreFoundation   [...] CFRunLoopRunSpecific + 420
      21  GraphicsServices [...] GSEventRunModal + 161
      22  UIKit            [...] UIApplicationMain + 159
      23  Buggy            [...] main + 111
      24  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 23 (note that your call numbers may be slightly different), receives an event recognized as a touch at call number 10, and then tries to send the corresponding action to the button’s target at call number 8. The selector for the action is not found (call number 3: -[UIResponder 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.

You can explicitly see the connection to the missing action in Interface Builder. Open Main.storyboard and Control-click the View Controller on the document outline. The black panel will show a warning icon beside the buttonTapped: action in the Received Actions section, as shown in Figure 8.3.

Figure 8.3  Nonexistent button action flagged in Interface Builder

Nonexistent button action flagged in Interface Builder

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: 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 and tap the button. 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 those old 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.

In the @IBAction methods you have written throughout this book, you have been passing in an argument – usually called sender - that 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.

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)")
}

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. For many controls, you want to check some state on the sender like this, as you did with the UISegmentedControl in Chapter 5.

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 buttonTapped(_ 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 buttonTapped(_ sender: UISwitch) {
 @IBAction func buttonTapped(_ sender: UIButton) {
    print("Called buttonTapped(_:)")
    // Log sender:
    print("sender: (sender)")
    // Log the control state:
    print("Is control on? (sender.isOn)")
}

Caveman debugging gets a little more sophisticated when you use literal expressions to make the console messages more explicit. Swift has four literal expressions that can assist you in logging information to the console (Table 8.1):

Table 8.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 see these literal expressions in action, 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 like the one below.

    Method: buttonTapped(_:) in file: /Users/cbkeur/iOSDevelopment/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. These print statements can be viewed if a device is connected to a Mac, so be careful about printing sensitive information.

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

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