Tải bản đầy đủ
Chapter 14. Multimedia and Location attachments]

Chapter 14. Multimedia and Location attachments]

Tải bản đầy đủ

2. Add the Audio, Record, Play, and Stop icons to the asset catalog.
Next, we’ll add an entry to the list of attachment types for audio.
1. Add the following code to the addAttachment method:
func addAttachment(_ sourceView : UIView) {
let actionSheet
= UIAlertController(title: "Add attachment",
message: nil,
preferredStyle: UIAlertControllerStyle
.actionSheet)
// If a camera is available to use...
if UIImagePickerController
.isSourceTypeAvailable(UIImagePickerControllerSourceType.camera) {
// This variable contains a closure that shows the image picker,
// or asks the user to grant permission.
var handler : (_ action:UIAlertAction) -> Void
switch AVCaptureDevice
.authorizationStatus(forMediaType: AVMediaTypeVideo) {
case .authorized:
fallthrough
case .notDetermined:
// If we have permission, or we don't know if it's been denied,
// then the closure shows the image picker.
handler = { (action) in
self.addPhoto()
}
default:
// Otherwise, when the button is tapped, ask for permission.
handler = { (action) in
let title = "Camera access required"
let message = "Go to Settings to grant permission to" +
"access the camera."
let cancelButton = "Cancel"
let settingsButton = "Settings"
let alert = UIAlertController(title: title, message: message,
preferredStyle: .alert)
// The Cancel button just closes the alert.
alert.addAction(UIAlertAction(title: cancelButton,
style: .cancel, handler: nil))
// The Settings button opens this app's settings page,
// allowing the user to grant us permission.
alert.addAction(UIAlertAction(title: settingsButton,
style: .default, handler: { (action) in

380

|

Chapter 14: Multimedia and Location attachments]

if let settingsURL = URL(
string: UIApplicationOpenSettingsURLString) {
UIApplication.shared
.openURL(settingsURL)
}
}))
self.present(alert,
animated: true,
completion: nil)
}
}
// Either way, show the Camera item; when it's selected, the
// appropriate code will run.
actionSheet.addAction(UIAlertAction(title: "Camera",
style: UIAlertActionStyle.default, handler: handler))
}
actionSheet.addAction(UIAlertAction(title: "Location",
style: UIAlertActionStyle.default, handler: { (action) -> Void in
self.addLocation()
}))
>
>
>
>

actionSheet.addAction(UIAlertAction(title: "Audio",
style: UIAlertActionStyle.default, handler: { (action) -> Void in
self.addAudio()
}))
actionSheet.addAction(UIAlertAction(title: "Cancel",
style: UIAlertActionStyle.cancel, handler: nil))
// If this is on an iPad, present it in a popover connected
// to the source view
if UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiom.pad {
actionSheet.modalPresentationStyle
= .popover
actionSheet.popoverPresentationController?.sourceView
= sourceView
actionSheet.popoverPresentationController?.sourceRect
= sourceView.bounds
}
self.present(actionSheet, animated: true, completion: nil)
}

Audio Attachments

|

381

+ Just like when we added support for photo attachments, we also need to add a new
entry for audio attachments.
1. Add the addAudio to DocumentViewController:
func addAudio() {
self.performSegue(withIdentifier: "ShowAudioAttachment", sender: nil)
}

+ Additionally, we need to trigger the right segue when the user decides to add an
audio attachment.
1. Open the File menu and choose New→File.
2. Create a new UIViewController subclass named AudioAttachmentViewControl
ler.
3. Open AudioAttachmentViewController.swift.
4. Import the AVFoundation framework into view controller; this framework
includes everything we could possibly need for loading, playing, and pausing
audio and video content.
5. Make AudioAttachmentViewController conform to the AttachmentViewer and
AVAudioPlayerDelegate protocols:
class AudioAttachmentViewController: UIViewController, AttachmentViewer,
AVAudioPlayerDelegate

1. Add the attachmentFile and document properties, which are required by the
AttachmentViewer protocol:
var attachmentFile : FileWrapper?
var document : Document?

1. Add outlet properties for the record, play, and stop buttons that we’re about to
add:
@IBOutlet weak var stopButton: UIButton!
@IBOutlet weak var playButton: UIButton!
@IBOutlet weak var recordButton: UIButton!

1. Finally, add an audio player and audio recorder:
var audioPlayer : AVAudioPlayer?
var audioRecorder : AVAudioRecorder?

Time to create the user interface!
1. Open Main.storyboard.

382

|

Chapter 14: Multimedia and Location attachments]

2. Drag in a new view controller, and set its class to AudioAttachmentViewControl
ler in the Identity Inspector.
3. Hold down the Control key and drag from the document view controller to this
new view controller. Choose “popover” from the list of segue types.
• Set the newly created segue’s Anchor View to the document view controller’s
view.
• Set the identifier for this segue to ShowAudioAttachment.
We’ll use a stack view to manage the three different buttons. Only one of them
will appear at a time, and we want the currently visible button to appear in the
center of the screen. Rather than overlay the buttons, we’ll put them all in a
centered stack view.
4. Search for UIStackView in the Object library and drag a vertical stack view into
the audio attachment view controller’s interface (Figure 14-1).

Figure 14-1. A vertical stack view
5. Center the stack view in the screen. Next, click the Align button at the lowerright corner, and turn on both “Horizontally in container” and “Vertically in con‐
tainer.” Click Add 2 Constraints This will add centering constraints to the stack
view.

Audio Attachments

|

383

6. Drag a new UIButton into the stack view. In the Attributes Inspector, set type to
Custom, delete the label text, and set image to Record.
The stack view will resize to match the size of the button when
you add the button. This is expected!

7. Repeat this process, adding two more buttons, with play and stop icons.
When you’re done, the stack view should look like Figure 14-2.

Figure 14-2. The view controller’s interface

384

| Chapter 14: Multimedia and Location attachments]

The order of the buttons doesn’t matter, so if you added the
buttons in different positions and you like that, stick with it.

8. Next, connect each button to its corresponding outlet; the record button should
be connected to recordButton, and so on for the rest.
9. Connect each button to new actions in AudioAttachmentViewController, called
recordTapped, playTapped, and stopTapped:
@IBAction func recordTapped(_ sender: AnyObject) {
beginRecording()
}
@IBAction func playTapped(_ sender: AnyObject) {
beginPlaying()
}
@IBAction func stopTapped(_ sender: AnyObject) {
stopRecording()
stopPlaying()
}

+ These methods simply respond to the buttons being tapped. The stop button serves
a dual purpose—when tapped, it stops both the recorder and the player. . Implement
the updateButtonState method:
+
func updateButtonState() {
if self.audioRecorder?.isRecording == true ||
self.audioPlayer?.isPlaying == true {
// We are either recording or playing, so
// show the stop button
self.recordButton.isHidden = true
self.playButton.isHidden = true
self.stopButton.isHidden = false
} else if self.audioPlayer != nil {
// We have a recording ready to go
self.recordButton.isHidden = true
self.stopButton.isHidden = true
self.playButton.isHidden = false
} else {
// We have no recording.
self.playButton.isHidden = true
self.stopButton.isHidden = true

Audio Attachments

|

385

self.recordButton.isHidden = false
}
}

+ The updateButtonState method is called from multiple places in this class. All it
does is ensure that the right button is visible, based on whether the audio player is
playing, or whether the audio recorder is recording.
1. Implement the beginRecording and stopRecording methods:
func beginRecording () {
// Ensure that we have permission. If we don't,
// we can't record, but should display a dialog that prompts
// the user to change the settings.
AVAudioSession.sharedInstance().requestRecordPermission {
(hasPermission) -> Void in
guard hasPermission else {
// We don't have permission. Let the user know.
let title = "Microphone access required"
let message = "We need access to the microphone to record audio."
let cancelButton = "Cancel"
let settingsButton = "Settings"
let alert = UIAlertController(title: title, message: message,
preferredStyle: .alert)
// The Cancel button just closes the alert.
alert.addAction(UIAlertAction(title: cancelButton,
style: .cancel, handler: nil))
// The Settings button opens this app's settings page,
// allowing the user to grant us permission.
alert.addAction(UIAlertAction(title: settingsButton,
style: .default, handler: { (action) in
if let settingsURL
= URL(string: UIApplicationOpenSettingsURLString) {
UIApplication.shared
.openURL(settingsURL)
}
}))
self.present(alert,
animated: true,
completion: nil)

386

| Chapter 14: Multimedia and Location attachments]

return
}
// We have permission!
// Try to use the same filename as before, if possible
let fileName = self.attachmentFile?.preferredFilename ??
"Recording \(Int(arc4random())).wav"
let temporaryURL = URL(fileURLWithPath: NSTemporaryDirectory())
.appendingPathComponent(fileName)
do {
self.audioRecorder = try AVAudioRecorder(url: temporaryURL,
settings: [:])
self.audioRecorder?.record()
} catch let error as NSError {
NSLog("Failed to start recording: \(error)")
}
self.updateButtonState()
}
}
func stopRecording () {
guard let recorder = self.audioRecorder else {
return
}
recorder.stop()
self.audioPlayer = try? AVAudioPlayer(contentsOf: recorder.url)
updateButtonState()
}

+ The beginRecording method first determines if the user has granted permission to
access the microphone. If permission is not granted, we create and display an alert
box letting the user know that it’s not possible to record. If it is, we create a URL that
points to a temporary location, and ask the audio recorder to begin recording.
+ Much like what we had to do with the camera permissions we need to set a specific
key inside the applications info.plist, without this key-value pair iOS will refuse to
let our app use the microphone. Inside info.plist add a new row into the dictio‐
nary. Type in Privacy - Microphone Usage Description for the key and type in a
message you want to be displayed when the app first tries to use the microphone, we
used We’ll use the microphone to record audio attachments but you should go with
whatever works best for you.

Audio Attachments

|

387

You can’t assume that the user has given permission to access the
microphone. As a result, if you want to record audio, you need to
first check to see if the app has permission by calling the AVSession
method requestRecordPermission. This method takes a closure as
a parameter, which receives as its parameter a bool value indicating
whether the app has permission to record.
This closure may not be called immediately. If it’s the first time the
app has ever asked for permission, then iOS will ask if the user
wants to grant your app permission. After the user answers, the
closure will be called.
If you really need the user to grant permission, and it’s been previ‐
ously withheld, you can send the user to the app’s Settings page,
which contains the controls for granting permission. Be careful
about annoying the user about this, though!

1. Implement the beginPlaying and stopPlaying methods:
func beginPlaying() {
self.audioPlayer?.delegate = self
self.audioPlayer?.play()
updateButtonState()
}
func stopPlaying() {
audioPlayer?.stop()
updateButtonState()
}

+ The beginPlaying and stopPlaying methods are quite straightforward: they start
and stop the audio player, and then call updateButtonState to ensure that the correct
button is appearing. Importantly, beginPlaying also sets the delegate of the audio
player so that the AudioAttachmentViewController receives a method call when the
audio finishes playing.
1. Implement the prepareAudioPlayer method, which works out the location of
the file to play from and prepares the audio player:
func prepareAudioPlayer()

{

guard let data = self.attachmentFile?.regularFileContents else {
return
}
do {
self.audioPlayer = try AVAudioPlayer(data: data)

388

|

Chapter 14: Multimedia and Location attachments]

} catch let error as NSError {
NSLog("Failed to prepare audio player: \(error)")
}
self.updateButtonState()
}

+ The prepareAudioPlayer method checks to see if the AudioAttachmentViewCon
troller has an attachment to work with; if it does, it attempts to create the audio

player, using the data stored inside the attachment.

1. Implement the audioPlayerDidFinishPlaying method, which is part of the
AVAudioPlayerDelegate protocol:
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer,
successfully flag: Bool) {
updateButtonState()
}

+ When the audio finishes playing, we have a very simple task to complete: we update
the state of the button. Because the audio player is no longer playing, it will change
from the “stop” symbol to the “play” symbol.
1. Finally, implement the viewDidLoad and viewWillDisappear methods:
override func viewDidLoad() {
if attachmentFile != nil {
prepareAudioPlayer()
}
// Indicate to the system that we will be both recording audio,
// and also playing back audio
do {
try AVAudioSession.sharedInstance()
.setCategory(AVAudioSessionCategoryPlayAndRecord)
} catch let error as NSError {
print("Error preparing for recording! \(error)")
}
updateButtonState()
}
override func viewWillDisappear(_ animated: Bool) {
if let recorder = self.audioRecorder {
// We have a recorder, which means we have a recording to attach
do {
attachmentFile =
try self.document?.addAttachmentAtURL(recorder.url)

Audio Attachments

|

389

prepareAudioPlayer()
} catch let error as NSError {
NSLog("Failed to attach recording: \(error)")
}
}
}

+ The viewDidLoad method first gets the audio player prepared, if an audio attach‐
ment is present. It then signals to the system that the application will be both playing
back and recording audio; this enables the microphone, and permits simultaneous
use of the microphone and the speaker. Finally, it updates the button to whatever
state is appropriate, depending on whether we have audio to play.
+ The viewWillDisappear method is responsible for saving any recorded audio.
Because the AVAudioRecorder saves directly to a temporary URL, we simply need to
copy it into the Document by calling addAttachmentAtURL.
Now we’ll add support for working with audio attachments in the document view
controller. First, we’ll make the Document class return a suitable image for audio
attachments, and then we’ll make the DocumentViewController present the AudioAt
tachmentViewController when an audio attachment is tapped.
1. Open Document.swift, and add the following code to FileWrapper’s thumbnail
Image method:
func thumbnailImage() -> UIImage? {
if self.conformsToType(kUTTypeImage) {
// If it's an image, return it as a UIImage
// Ensure that we can get the contents of the file
guard let attachmentContent = self.regularFileContents else {
return nil
}
// Attempt to convert the file's contents to text
return UIImage(data: attachmentContent)
}
if self.conformsToType(kUTTypeJSON) {
// JSON files used to store locations
return UIImage(named: "Location")
}
>
>
>

if (self.conformsToType(kUTTypeAudio)) {
return UIImage(named: "Audio")
}
// We don't know what type it is, so return nil

390

|

Chapter 14: Multimedia and Location attachments]