78、iOS 开发:活动视图、扩展与音频基础

iOS 开发:活动视图、扩展与音频基础

1. UIActivity 与视图控制器交互

UIActivity 提供一个视图控制器作为其 activityViewController 时,需要提前将自身的引用传递给该视图控制器,以便视图控制器在合适的时候调用 activityDidFinish(_:) 方法。

例如,假设活动是让用户在某人的照片上画胡子。视图控制器会提供相应的界面,包括取消和完成按钮。当用户点击这些按钮时,会执行必要的操作(如用户点击完成时保存修改后的照片),然后调用 activityDidFinish(_:)

以下是实现 activityViewController 属性的代码:

override var activityViewController : UIViewController? {
    let mvc = MustacheViewController(activity: self, items: self.items!)
    return mvc
}

MustacheViewController 的代码如下:

weak var activity : UIActivity?
var items: [Any]
init(activity:UIActivity, items:[Any]) {
    self.activity = activity
    self.items = items
    super.init(nibName: "MustacheViewController", bundle: nil)
}
// ... other stuff ...
@IBAction func doCancel(_ sender: Any) {
    self.activity?.activityDidFinish(false)
}
@IBAction func doDone(_ sender: Any) {
    self.activity?.activityDidFinish(true)
}

需要注意的是, MustacheViewController UIActivity 的引用( self.activity )是弱引用,否则会导致循环引用。

2. SFSafariViewController 代理方法

SFSafariViewController 代理方法 safariViewController(_:activityItemsFor:title:) 的目的是为活动视图添加自定义 UIActivity 项。因为该视图控制器的视图虽然显示在你的应用中,但它不是你的视图控制器,其分享按钮和活动视图也不是你的,所以需要实现此方法来添加自定义项。

3. 动作扩展(Action Extensions)

为了提供系统级的活动(即在其他应用弹出活动视图时显示),可以编写分享扩展(显示在上方行)或动作扩展(显示在下方行)。一个应用可以提供一个分享扩展,但可以提供多个动作扩展。

以下是编写动作扩展的基本步骤:
1. 选择目标模板 :从 iOS → Application Extension → Action Extension 开始。创建目标时,在第二个面板中选择是否带有界面。
2. 配置 Info.plist :除了设置捆绑包名称(将显示在活动视图中活动图标下方),还需要指定该活动接受的数据类型。在 NSExtensionActivationRule 字典中提供一个或多个键,例如:
- NSExtensionActivationSupportsFileWithMaxCount
- NSExtensionActivationSupportsImageWithMaxCount
- NSExtensionActivationSupportsMovieWithMaxCount
- NSExtensionActivationSupportsText
- NSExtensionActivationSupportsWebURLWithMaxCount
也可以通过编写 NSPredicate 字符串作为 NSExtensionActivationRule 键的值,以更复杂的方式声明活动接受的数据类型。
3. 指定图标 :动作扩展在活动视图中由一个图标表示,需要在动作扩展目标中指定该图标。图标大小与应用图标相同,可以使用应用图标。在动作扩展目标中添加资产目录,并在其中创建 iOS 应用图标,该图标将被视为模板图像。

以下是一个带有界面的动作扩展示例,该扩展接受一个可能是美国州缩写的字符串,并提供该州的实际名称:

let list : [String:String] = {
    let path = Bundle.main.url(forResource:"abbrevs", withExtension:"txt")!
    // ... load the text file as a string, parse into dictionary (result)
    return result
}()

func state(for abbrev:String) -> String? {
    return self.list[abbrev]
}

override func viewDidLoad() {
    super.viewDidLoad()
    self.doneButton.isEnabled = false
    self.lab.text = "No expansion available."
    // ...
}

if self.extensionContext == nil {
    return
}
let items = self.extensionContext!.inputItems
let desiredType = kUTTypePlainText as String
guard let extensionItem = items[0] as? NSExtensionItem
    else {return}
guard let provider = extensionItem.attachments?.first
    else {return}
guard provider.hasItemConformingToTypeIdentifier(self.desiredType)
    else {return}

provider.loadItem(forTypeIdentifier: desiredType) { item, err in
    DispatchQueue.main.async {
        if let orig = (item as? String)?.uppercased() {
            self.orig = orig
            if let exp = self.state(for:orig) {
                self.expansion = exp
                self.lab.text = """
                    Can expand \(orig) to \(exp).
                    Tap Done to place on clipboard.
                    """
                self.doneButton.isEnabled = true
            }
        }
    }
}

@IBAction func cancel(_ sender: Any) {
    self.extensionContext?.completeRequest(returningItems: nil)
}
@IBAction func done(_ sender: Any) {
    UIPasteboard.general.string = self.expansion!
    self.extensionContext?.completeRequest(returningItems: nil)
}
4. 扩展调试方法

扩展不在应用进程中运行,因此断点和日志记录无效。可以使用以下简单技术解决此问题:
1. 运行宿主应用,将其复制到目标设备。
2. 在 Xcode 窗口工具栏的方案弹出菜单中切换到扩展,然后运行扩展。
3. 弹出一个对话框,询问要运行的应用,选择宿主应用并点击运行。
4. 宿主应用将运行,然后调用扩展并进行测试。此时调试的是扩展,所有调试功能将按预期工作。

5. 分享扩展(Share Extensions)

如果应用提供分享扩展,它可以出现在活动视图的顶部行。分享扩展类似于动作扩展,但它不是处理接收到的数据,而是将数据以某种方式存储或发布到服务器。

分享扩展在活动视图的顶部行由应用自己的图标表示。用户点击该图标后,可以在完成分享操作之前进一步与数据交互,可能会修改或取消操作。创建分享扩展目标( iOS → Application Extension → Share Extension )时,模板会提供一个故事板和一个视图控制器,视图控制器可以是以下两种类型之一:
- SLComposeServiceViewController :提供一个标准界面,用于在 UITextView 中显示可编辑文本,可能带有预览,还有用户可配置的选项按钮,以及取消和发布按钮。
- 普通视图控制器子类 :如果选择普通视图控制器子类,则需要自己设计界面,包括提供关闭界面的方式。

无论选择哪种界面形式,关闭界面的方式都是调用:

self.extensionContext?.completeRequest(returningItems:nil)

以下是使用 SLComposeServiceViewController 的基本示例:

weak var config : SLComposeSheetConfigurationItem?
var selectedText = "Large" {
    didSet {
        self.config?.value = self.selectedText
    }
}

override func configurationItems() -> [Any]! {
    let c = SLComposeSheetConfigurationItem()!
    c.title = "Size"
    c.value = self.selectedText
    c.tapHandler = { [unowned self] in
        let tvc = TableViewController(style: .grouped)
        tvc.selectedSize = self.selectedText
        tvc.delegate = self
        self.pushConfigurationViewController(tvc)
    }
    self.config = c
    return [c]
}

protocol SizeDelegate : class {
    var selectedText : String {get set}
}

class TableViewController: UITableViewController {
    var selectedSize : String?
    weak var delegate : SizeDelegate?
    override func tableView(_ tableView: UITableView,
        didSelectRowAt indexPath: IndexPath) {
            let cell = tableView.cellForRow(at:indexPath)!
            let s = cell.textLabel!.text!
            self.selectedSize = s
            self.delegate?.selectedText = s
            tableView.reloadData()
    }
    // ...
}

override func didSelectPost() {
    let s = self.contentText
    // ... do something with it ...
    self.extensionContext?.completeRequest(returningItems:nil)
}

可以通过以下代码将发布按钮改为保存按钮,但这种方法的合法性和在未来系统中的可用性不确定:

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    self.navigationController?.navigationBar.topItem?
        .rightBarButtonItem?.title = "Save"
}
6. 音频基础

iOS 提供了多种技术,允许应用产生、录制和处理声音。以下是一些基本信息:
- 播放界面选项 :本章讨论的类都没有在应用中提供传输界面(即允许用户停止和开始声音播放的界面)。如果需要传输界面,可以选择以下方法:
- 创建自己的界面。
- 将内置的“远程控制”按钮与应用关联。
- 网页视图支持 HTML5 <audio> 标签,这是一种简单、轻量级的播放音频和控制播放的方式(包括使用 AirPlay)。
- 将声音视为电影,并使用相关的界面提供类,这也是播放远程网络上声音文件的好方法。

7. 系统声音

系统声音是 iOS 中类似于基本计算机“哔声”的最简单声音形式,通过音频工具箱框架的系统声音服务实现,需要导入 AudioToolbox 。播放系统声音的 API 有两种形式:旧形式和新形式(iOS 9 引入),先介绍旧形式(仍然可用且未被弃用)。

旧形式涉及调用两个 C 函数之一:
- AudioServicesPlayAlertSound :在 iPhone 上,根据用户设置可能还会使设备振动。
- AudioServicesPlaySystemSound :在 iPhone 上不会伴随振动,但可以通过传递 kSystemSoundID_Vibrate 作为“声音”名称,将此“声音”指定为设备振动。

要播放的声音文件需要是未压缩的 AIFF 或 WAV 文件(或包装其中之一的 Apple CAF 文件)。需要通过调用 AudioServicesCreateSystemSoundID 并传入指向声音文件的 URL 来获取 SystemSoundID

以下是一个简单的示例:

let sndurl = Bundle.main.url(forResource:"test", withExtension: "aif")!
var snd : SystemSoundID = 0
AudioServicesCreateSystemSoundID(sndurl as CFURL, &snd)
AudioServicesPlaySystemSound(snd)

上述代码可以播放声音,但存在内存管理问题。需要调用 AudioServicesDisposeSystemSoundID 释放 SystemSoundID ,但 AudioServicesPlaySystemSound 是异步执行的,不能在调用播放函数的下一行调用释放函数,否则会在声音即将开始播放时释放声音,导致无声。

正确的方法是实现一个声音完成函数,在声音播放完成时调用。通过调用 AudioServicesAddSystemSoundCompletion 指定声音完成函数,该函数必须作为 C 函数指针提供,但 Swift 允许在需要 C 函数指针的地方传递全局或局部 Swift 函数(包括匿名函数)。以下是改进后的代码:

let sndurl = Bundle.main.url(forResource:"test", withExtension: "aif")!
var snd : SystemSoundID = 0
AudioServicesCreateSystemSoundID(sndurl as CFURL, &snd)
AudioServicesAddSystemSoundCompletion(snd, nil, nil, { snd, _ in
    // 声音播放完成后的操作
})

综上所述,本文介绍了 iOS 开发中活动视图、扩展和音频基础的相关内容,包括 UIActivity 与视图控制器的交互、动作扩展和分享扩展的编写方法,以及系统声音的播放和内存管理。这些知识对于开发功能丰富的 iOS 应用非常重要。

iOS 开发:活动视图、扩展与音频基础

8. 音频播放与控制

在 iOS 开发中,除了系统声音,还有多种方式实现音频的播放与控制。以下为你详细介绍不同的实现途径及相关代码示例。

8.1 创建自定义播放界面

若要创建自定义的音频播放界面,可借助 UIKit 中的控件,如 UIButton 用于播放、暂停操作, UISlider 用于调节音量等。以下是一个简单示例:

import UIKit
import AVFoundation

class AudioPlayerViewController: UIViewController {
    var audioPlayer: AVAudioPlayer?

    override func viewDidLoad() {
        super.viewDidLoad()

        // 加载音频文件
        guard let audioURL = Bundle.main.url(forResource: "audio_file", withExtension: "mp3") else { return }
        do {
            audioPlayer = try AVAudioPlayer(contentsOf: audioURL)
            audioPlayer?.prepareToPlay()
        } catch {
            print("音频加载失败: \(error)")
        }

        // 创建播放按钮
        let playButton = UIButton(frame: CGRect(x: 100, y: 200, width: 100, height: 50))
        playButton.setTitle("播放", for: .normal)
        playButton.addTarget(self, action: #selector(playAudio), for: .touchUpInside)
        view.addSubview(playButton)

        // 创建暂停按钮
        let pauseButton = UIButton(frame: CGRect(x: 250, y: 200, width: 100, height: 50))
        pauseButton.setTitle("暂停", for: .normal)
        pauseButton.addTarget(self, action: #selector(pauseAudio), for: .touchUpInside)
        view.addSubview(pauseButton)
    }

    @objc func playAudio() {
        audioPlayer?.play()
    }

    @objc func pauseAudio() {
        audioPlayer?.pause()
    }
}
8.2 关联远程控制按钮

将内置的“远程控制”按钮与应用关联,可让用户通过设备的物理按钮控制音频播放。实现步骤如下:
1. 激活应用的音频会话:

import AVFoundation

do {
    try AVAudioSession.sharedInstance().setCategory(.playback)
    try AVAudioSession.sharedInstance().setActive(true)
} catch {
    print("音频会话设置失败: \(error)")
}
  1. 注册远程控制事件:
UIApplication.shared.beginReceivingRemoteControlEvents()
self.becomeFirstResponder()
  1. 处理远程控制事件:
override func remoteControlReceived(with event: UIEvent?) {
    guard let event = event, event.type == .remoteControl else { return }
    switch event.subtype {
    case .remoteControlPlay:
        audioPlayer?.play()
    case .remoteControlPause:
        audioPlayer?.pause()
    default:
        break
    }
}
8.3 使用 HTML5 <audio> 标签

在 Web 视图中使用 HTML5 <audio> 标签播放音频,可实现简单、轻量级的音频播放与控制。示例代码如下:

import UIKit
import WebKit

class WebAudioViewController: UIViewController, WKUIDelegate {
    var webView: WKWebView!

    override func loadView() {
        let webConfig = WKWebViewConfiguration()
        webView = WKWebView(frame: .zero, configuration: webConfig)
        webView.uiDelegate = self
        view = webView
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        let html = """
        <html>
        <body>
            <audio controls>
                <source src="audio_file.mp3" type="audio/mpeg">
                Your browser does not support the audio element.
            </audio>
        </body>
        </html>
        """
        webView.loadHTMLString(html, baseURL: Bundle.main.resourceURL)
    }
}
9. 音频录制

iOS 还支持音频录制功能,可使用 AVAudioRecorder 类实现。以下是一个简单的音频录制示例:

import UIKit
import AVFoundation

class AudioRecorderViewController: UIViewController {
    var audioRecorder: AVAudioRecorder?
    var recordingURL: URL?

    override func viewDidLoad() {
        super.viewDidLoad()

        // 设置音频会话
        do {
            try AVAudioSession.sharedInstance().setCategory(.record)
            try AVAudioSession.sharedInstance().setActive(true)
        } catch {
            print("音频会话设置失败: \(error)")
        }

        // 创建录制按钮
        let recordButton = UIButton(frame: CGRect(x: 100, y: 200, width: 100, height: 50))
        recordButton.setTitle("开始录制", for: .normal)
        recordButton.addTarget(self, action: #selector(startRecording), for: .touchUpInside)
        view.addSubview(recordButton)

        // 创建停止按钮
        let stopButton = UIButton(frame: CGRect(x: 250, y: 200, width: 100, height: 50))
        stopButton.setTitle("停止录制", for: .normal)
        stopButton.addTarget(self, action: #selector(stopRecording), for: .touchUpInside)
        stopButton.isEnabled = false
        view.addSubview(stopButton)
    }

    @objc func startRecording() {
        let recordingSettings: [String: Any] = [
            AVFormatIDKey: Int(kAudioFormatMPEG4AAC),
            AVSampleRateKey: 44100.0,
            AVNumberOfChannelsKey: 2,
            AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue
        ]

        let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
        recordingURL = documentsDirectory.appendingPathComponent("recording.m4a")

        do {
            audioRecorder = try AVAudioRecorder(url: recordingURL!, settings: recordingSettings)
            audioRecorder?.delegate = self
            audioRecorder?.record()
            // 更新按钮状态
            view.viewWithTag(1)?.isEnabled = false
            view.viewWithTag(2)?.isEnabled = true
        } catch {
            print("录制初始化失败: \(error)")
        }
    }

    @objc func stopRecording() {
        audioRecorder?.stop()
        // 更新按钮状态
        view.viewWithTag(1)?.isEnabled = true
        view.viewWithTag(2)?.isEnabled = false
    }
}

extension AudioRecorderViewController: AVAudioRecorderDelegate {
    func audioRecorderDidFinishRecording(_ recorder: AVAudioRecorder, successfully flag: Bool) {
        if flag {
            print("录制成功,文件路径: \(recorder.url)")
        } else {
            print("录制失败")
        }
    }
}
10. 音频处理

除了播放和录制音频,还可以对音频进行处理,如音频剪辑、混音等。以下是一个简单的音频剪辑示例:

import AVFoundation

func trimAudio(inputURL: URL, outputURL: URL, startTime: TimeInterval, endTime: TimeInterval) {
    let asset = AVAsset(url: inputURL)
    let composition = AVMutableComposition()

    let audioTrack = composition.addMutableTrack(withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid)
    do {
        let assetTrack = asset.tracks(withMediaType: .audio).first!
        try audioTrack?.insertTimeRange(CMTimeRange(start: CMTime(seconds: startTime, preferredTimescale: 1000), duration: CMTime(seconds: endTime - startTime, preferredTimescale: 1000)), of: assetTrack, at: .zero)
    } catch {
        print("音频剪辑失败: \(error)")
        return
    }

    let exporter = AVAssetExportSession(asset: composition, presetName: AVAssetExportPresetAppleM4A)
    exporter?.outputURL = outputURL
    exporter?.outputFileType = .m4a
    exporter?.exportAsynchronously(completionHandler: {
        switch exporter?.status {
        case .completed:
            print("音频剪辑成功,文件路径: \(outputURL)")
        case .failed:
            if let error = exporter?.error {
                print("音频导出失败: \(error)")
            }
        default:
            break
        }
    })
}

总结

本文全面介绍了 iOS 开发中活动视图、扩展以及音频相关的知识。在活动视图方面,涵盖了 UIActivity 与视图控制器的交互,以及 SFSafariViewController 代理方法的使用。扩展部分详细讲解了动作扩展和分享扩展的编写步骤、调试方法。音频部分则从系统声音的播放,到音频的播放、录制和处理,为开发者提供了丰富的实现思路和代码示例。掌握这些知识,有助于开发者开发出功能更加丰富、交互更加友好的 iOS 应用。

以下是整个开发流程的 mermaid 流程图:

graph LR
    classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
    A(开始):::process --> B(UIActivity与视图控制器交互):::process
    B --> C(SFSafariViewController代理方法):::process
    C --> D(动作扩展):::process
    D --> E(扩展调试):::process
    E --> F(分享扩展):::process
    F --> G(音频基础):::process
    G --> H(系统声音):::process
    H --> I(音频播放与控制):::process
    I --> J(音频录制):::process
    J --> K(音频处理):::process
    K --> L(结束):::process

通过这个流程图,可以清晰地看到整个开发过程的各个环节和顺序,帮助开发者更好地理解和应用这些知识。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值