Buttons: class UIButton

When you press the button, the text will flash and a sound will play. See Buttons in the iOS Human Interface Guidelines. A button is the simplest example of a control.

Source code in Button.zip

  1. AppDelegate.swift: unchanged.
  2. ViewController.swift: added the property sid and the init(coder:) that takes a NSCoder; added touchUpInside(_:). Import AudioToolbox.
  3. View.swift has the objects we see on the screen. Added the init(coder:) that takes an NSCoder. To specify the button’s border color, we have to go down into the layer of software beneath the button. (We did this in Pinch.) This layer uses CGColors (“Core Graphics”) instead of UIColors (“User Interface”).
  4. Main.storyboard: changed the class of the view controller’s UIView to my class View.
  5. Assets.xcassets
    1. Contents.json
    2. chinese.dataset
      1. Contents.json
      2. chinese.mp3
  6. Info.plist: unchanged.

Create the project

The documentation for the function AudioServicesPlaySystemSound demands a .caf, .aif, or .wav file that is no longer than 30 seconds, but it successfully played chinese.mp3. Other functions may have a wider list of supported audio formats.

To get a sound file, point your Safari at this page, control-click on the green word Preview on the right, and download the file onto your Macintosh Desktop, renaming it chinese.mp3. To make sure that the file you downloaded is an intact .mp3 file, select it and type control-i (the “information” command in the Macintosh Finder). In the information window, open ▼General to make sure that chinese.mp3 is an MP3 audio file occupying a healthy number of bytes (not zero bytes!); and ▼More Info to make sure its duration is no longer than 30 seconds. To be totally sure that the file you downloaded is an intact .mp3 file, try playing it.

In previous versions of iOS we had to convert chinese.mp3 to .wav, but this is no longer necessary. Drag chinese.mp3 into the set list of Assets.xcassets, as in America.

The view controller

A view should contain only low-level, visual code: foreground colors, background colors, x and y coördinates. On the other hand, the application delegate should concern itself only with high-level lifecycle events: initialization and termination, entering the foreground and background. In between these levels, the working intelligence of the app—in this case, the audio code—should go on the view controller. Later, we’ll have another component: the “model”.

Output from print

On iPhone 6s Plus simulator:

path = /Users/myname/Library/Developer/CoreSimulator/Devices/4248FDA4-DE7B-4D16-8A71-4BBF066C98F4
/data/Containers/Bundle/Application/EDEB4AEB-A42F-4766-B045-F5B88936F775/Button.app/chinese.mp3

url = file:///Users/myname/Library/Developer/CoreSimulator/Devices/4248FDA4-DE7B-4D16-8A71-4BBF066C98F4
/data/Containers/Bundle/Application/EDEB4AEB-A42F-4766-B045-F5B88936F775/Button.app/chinese.mp3

sid = 4097

touchUpInside(_:) title = Chinese sound effect

On iPod Touch with iOS 9.3.3:

path = /var/containers/Bundle/Application/51788FC8-4112-47FC-B833-5D884F4CDA39/Button.app/chinese.mp3

url = file:///var/containers/Bundle/Application/51788FC8-4112-47FC-B833-5D884F4CDA39/Button.app/chinese.mp3

sid = 4097

touchUpInside(_:) title = Chinese sound effect

Things to try

  1. Use the standard size for the button’s font. The titleLabel is the UILabel inside the button.
    		let fontSize: CGFloat = UIFont.buttonFontSize();
    		button.titleLabel!.font = UIFont.systemFontOfSize(fontSize);
    

  2. Don’t hardcode in the button’s dimensions (200 × 40 points). Measure the size of the text and fit the button around it, with padding. We saw sizeWithAttributes(_:) in Hello.
    		let s: String = "Chinese sound effect";
    		let attributes: [String: AnyObject] = [NSFontAttributeName: button.titleLabel!.font];
    		var textSize: CGSize = s.sizeWithAttributes(attributes);
    		textSize.width  += UIFont.buttonFontSize();	//Add some padding.
    		textSize.height += UIFont.buttonFontSize();
    		button.bounds.size = textSize;
    

  3. Would the button be more æsthetically pleasing if its aspect ratio were a golden rectangle?


  4. Instead of UIButtonType.System, try UIButtonType.DetailDisclosure or UIButtonType.ContactAdd. These types of buttons have no titleLabel.

  5. The third parameter of the method addTarget(_:action:forControlEvents:) could be an array of two or more control events:
    [UIControlEvents.TouchDown, UIControlEvents.TouchDragEnter]
    
    or
    [.TouchDown, .TouchDragEnter]
    
    (.TouchDragEnter didn’t work for me.)

  6. If AudioServicesCreateSystemSoundID(_:_:) fails, print the name of the error instead of just the status number. In the viewDidLoad() method of the view controller, change
    			print("could not create system sound ID, status = \(status)");
    
    to the following.
    			let d: [OSStatus: String] = [   //a dictionary
    				kAudioServicesNoError:                        "kAudioServicesNoError",
    				kAudioServicesUnsupportedPropertyError:       "kAudioServicesUnsupportedPropertyError",
    				kAudioServicesBadPropertySizeError:           "kAudioServicesBadPropertySizeError",
    				kAudioServicesBadSpecifierSizeError:          "kAudioServicesBadSpecifierSizeError",
    				kAudioServicesSystemSoundUnspecifiedError:    "kAudioServicesSystemSoundUnspecifiedError",
    				kAudioServicesSystemSoundClientTimedOutError: "kAudioServicesSystemSoundClientTimedOutError"
    			];
    
    			var s: String? = d[status];
    			if s == nil {
    				s = "unknown error \(status)";
    			}
    			print("could not create system sound ID, status = \(s!)");
    
    How could you test this code?

  7. Disable the button while the audio file is playing, and re-enable it when the playback is complete. Insert the following statements into the init(coder:) method of the View after setting the color and title for control state normal:
    		//A low alpha level will make the title dim when disabled.
                    let disabledColor: UIColor = UIColor(red: 1, green: 0, blue: 0, alpha: 0.2);
    		button.setTitleColor(disabledColor, forState: UIControlState.Disabled);
    
    Add the following statement to the start of the touchUpInside(:) method of the view controller.
    		button.enabled = false;	//Make the button unresponsive to touch.
    

    Append the following to the viewDidLoad() method of the view controller. When the playback of the audio file has completed, we will execute the closure which is the fourth parameter of AudioServicesAddSystemSoundCompletion. This closure must be of type AudioServicesSystemSoundCompletionProc. The fifth parameter will be passed as the second parameter of the closure.

    		//The button is the subview most recently added to the view.
    		let button: UIButton = view.subviews[view.subviews.count - 1] as! UIButton;
    
    		//Store the address of the button into an UnsafeMutablePointer.
    		let unmanagedButton: Unmanaged<UIButton> = Unmanaged<UIButton>.passRetained(button);
    		let opaquePointer: COpaquePointer = unmanagedButton.toOpaque();
    		let unsafeMutablePointer: UnsafeMutablePointer<Void> = UnsafeMutablePointer<Void>(opaquePointer);
    
    		let status: OSStatus = AudioServicesAddSystemSoundCompletion(
    			sid,
    			nil,
    			nil,
    			{(soundID: SystemSoundID, clientData: UnsafeMutablePointer<Void>) -> Void in
    				//Arrive here when the sound file has finished playing.
    				//Extract the button from the UnsafeMutablePointer.
    				let opaquePointer: COpaquePointer = COpaquePointer(clientData);
    				let unmanagedButton: Unmanaged<UIButton> = Unmanaged<UIButton>.fromOpaque(opaquePointer);
    				let button: UIButton = unmanagedButton.takeRetainedValue();
    				button.enabled = true;
    			},
    			unsafeMutablePointer
    		);
    
    		if status != kAudioServicesNoError {
    			print("could not add system sound completion, status = \(status)");
    		}
    

  8. Add the following method to the view controller.
    	deinit {
    		if sid != 0 {
    			let status: OSStatus = AudioServicesDisposeSystemSoundID(sid);
    			if status != kAudioServicesNoError {
    				print("could not dispose of system sound, status = \(status)");
    			}
    		}
    	}
    

  9. Add an Erase button to the Etch-a-Sketch. The button will be a subview of the big white View. When pressed, the button will call the clearPath method of the View.