18、iOS开发:搜索、多任务功能实现全解析

iOS开发:搜索、多任务功能实现全解析

在iOS开发中,实现用户活动搜索、多任务处理等功能能显著提升用户体验。下面将详细介绍如何实现这些功能。

让用户活动可搜索

在应用中,我们可能希望用户在应用内的活动能够被搜索到。用户活动的类型为 NSUserActivity

解决方案 :使用 NSUserActivity 类的 isEligibleForSearch eligibleForPublicIndexing 属性将活动标记为可搜索。

操作步骤
1. 设置UI :在视图控制器中添加一个文本字段和一个文本视图。文本字段用于用户输入文本,文本视图用于记录日志信息。将文本字段和文本视图与代码关联,并将文本字段的代理设置为视图控制器。
2. 遵循协议并实现方法 :让视图控制器遵循 UITextFieldDelegate NSUserActivityDelegate 协议,并实现相关的代理方法。

func userActivityWasContinued(_ userActivity: NSUserActivity) {
    log("Activity was continued")
}
func userActivityWillSave(_ userActivity: NSUserActivity) {
    log("Activity will save")
}
  1. 编写辅助方法 :编写用于记录日志和读取文本字段内容的方法。
func log(_ t: String){
    DispatchQueue.main.async {
        self.status.text = t + "\n" + self.status.text
    }
}
func textFieldText() -> String{
    if let txt = self.textField.text{
        return txt
    } else {
        return ""
    }
}
  1. 创建可搜索的用户活动 :创建一个懒加载的用户活动,并将其标记为可搜索。
// TODO: change this ID to something relevant to your app
let activityType = "se.pixolity.Making-User-Activities-Searchable.editText"
let activityTxtKey = "se.pixolity.Making-User-Activities-Searchable.txt"
lazy var activity: NSUserActivity = {
    let a = NSUserActivity(activityType: self.activityType)
    a.title = "Text Editing"
    a.isEligibleForHandoff = true
    a.isEligibleForSearch = true
    // do this only if it makes sense
    // a.isEligibleForPublicIndexing = true
    a.delegate = self
    a.keywords = ["txt", "text", "edit", "update"]

    let att = CSSearchableItemAttributeSet(
        itemContentType: kUTTypeText as String)
    att.title = a.title
    att.contentDescription = "Editing text right in the app"
    att.keywords = Array(a.keywords)

    if let u = Bundle.main.url(forResource: "Icon", withExtension: "png"){
        att.thumbnailData = try? Data(contentsOf: u)
    }
    a.contentAttributeSet = att

    return a
}()
  1. 处理文本字段事件 :当文本字段开始编辑时,将活动标记为当前活动;当文本字段结束编辑时,取消活动的当前状态。
func textFieldDidBeginEditing(_ textField: UITextField) {
    log("Activity is current")
    userActivity = activity
    activity.becomeCurrent()
}
func textFieldDidEndEditing(_ textField: UITextField) {
    log("Activity resigns being current")
    activity.resignCurrent()
    userActivity = nil
}
  1. 更新用户活动状态 :当文本字段内容发生变化时,标记用户活动需要更新。
func textField(_ textField: UITextField,
               shouldChangeCharactersIn range: NSRange,
               replacementString string: String) -> Bool {

    activity.needsSave = true

    return true
}
  1. 定期更新活动状态 :在视图控制器的 updateUserActivityState 方法中更新活动的用户信息字典。
override func updateUserActivityState(_ a: NSUserActivity) {

    log("We are asked to update the activity state")

    a.addUserInfoEntries(
        from: [self.activityTxtKey : self.textFieldText()])

    super.updateUserActivityState(a)

}

通过以上步骤,当用户在文本字段中输入文本并将应用置于后台后,用户可以在主屏幕上搜索该活动并继续之前的操作。

删除应用的可搜索内容

如果我们在Spotlight中索引了一些项目,现在想删除这些内容,可以使用 CSSearchableIndex 类的相关方法。

解决方案 :使用 CSSearchableIndex 类的以下方法组合:
- deleteAllSearchableItems(completionHandler:)
- deleteSearchableItems(withDomainIdentifiers:completionHandler:)
- deleteSearchableItems(withIdentifiers:completionHandler:)

操作步骤
1. 获取 CSSearchableIndex 实例

let identifiers = [
    "com.yourcompany.etc1",
    "com.yourcompany.etc2",
    "com.yourcompany.etc3"
]
let i = CSSearchableIndex(name: Bundle.main.bundleIdentifier!)
  1. 获取最新应用状态 :使用 fetchLastClientState(_:completionHandler:) 方法获取最新的应用状态。
  2. 开始批量删除 :使用 beginIndexBatch() 方法开始批量更新,然后使用 deleteSearchableItems(withIdentifiers:completionHandler:) 方法删除指定标识符的项目。
i.fetchLastClientState {clientState, err in
    guard err == nil else{
        print("Could not fetch last client state")
        return
    }

    let state: Data
    if let s = clientState{
        state = s
    } else {
        state = Data()
    }

    i.beginBatch()

    i.deleteSearchableItems(withIdentifiers: identifiers) {err in
        if let e = err{
            print("Error happened \(e)")
        } else {
            print("Successfully deleted the given identifiers")
        }
    }
    i.endBatch(withClientState: state, completionHandler: {err in
        guard err == nil else{
            print("Error happened in ending batch updates = \(err!)")
            return
        }
        print("Successfully batch updated the index")
    })

}
支持分屏视图

在iPad上,我们可能希望通用应用支持分屏视图,即用户可以在应用运行时将另一个应用拖到屏幕右侧,使两个应用并排显示。

解决方案
- 新项目 :使用最新版本的Xcode创建项目,默认情况下,应用将在较大的显示屏(如iPad)上启用分屏视图。
- 旧项目 :如果是旧项目,需要进行以下操作:
1. 添加一个名为 LaunchScreen.storyboard 的文件,并将其设置为应用的启动屏幕故事板。
2. 将项目的基础SDK设置为最新的Xcode版本中的可用SDK。
3. 在 info.plist 文件中,在 UISupportedInterfaceOrientations~ipad 键下声明支持所有方向。
4. 确保 UIRequiresFullScreen 键在 plist 文件中被移除,或者其值为 NO

代码示例 :为了确保UI组件在分屏视图下正确工作,我们可以添加约束来调整视图的大小。

import UIKit
class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        let newView = UIView()
        newView.backgroundColor = .orange
        newView.translatesAutoresizingMaskIntoConstraints = false

        view.addSubview(newView)

        newView.leadingAnchor.constraint(equalTo:
            view.leadingAnchor).isActive = true

        newView.trailingAnchor.constraint(equalTo:
            view.trailingAnchor).isActive = true

        newView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
        newView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true

    }
}
添加画中画播放功能

我们可能希望用户能够将视频缩小到屏幕的一部分,以便在查看和与其他应用中的内容进行交互时继续观看视频。

解决方案
1. 创建视图 :创建一个具有 AVPlayerLayer 类型图层的视图。
2. 实例化视频项目 :实例化一个 AVPlayerItem 来表示视频。
3. 创建播放器 :将播放器项目放入 AVPlayer 实例中。
4. 设置播放器 :将播放器分配给视图的图层播放器对象。
5. 开始播放 :将视图分配给视图控制器的主视图,并调用 play() 方法开始正常播放。
6. 监听状态变化 :使用KVO监听播放器的 currentItem.status 属性变化,当状态变为 ReadyToPlay 时,创建 AVPictureInPictureController 实例。
7. 检查画中画可用性 :监听 AVPictureInPictureController pictureInPicturePossible 属性变化,当该值变为 true 时,通知用户可以进入画中画模式。
8. 启动画中画 :当用户按下按钮启动画中画时,检查 pictureInPicturePossible 的值,如果为 true ,调用 startPictureInPicture() 方法启动画中画。

操作步骤
1. 创建 PipView :创建一个名为 PipView 的视图类,并使其具有 AVPlayerLayer 类型的图层。

import Foundation
import UIKit
import AVFoundation
protocol Pippable{
    var pipLayer: AVPlayerLayer{get}
    var pipLayerPlayer: AVPlayer? {get set}
}
extension UIView : Pippable{

    var pipLayer: AVPlayerLayer{
        get{return layer as! AVPlayerLayer}
    }

    // shortcut into pipLayer.player
    var pipLayerPlayer: AVPlayer?{
        get{return pipLayer.player}
        set{pipLayer.player = newValue}
    }

    open public func awakeFromNib() {
        super.awakeFromNib()
        backgroundColor = .black

    }

}
class PipView : UIView{

    override class var layerClass: AnyClass{
        return AVPlayerLayer.self
    }
}
  1. 设置视图控制器 :在视图控制器的故事板中,将主视图的类设置为 PipView ,并在导航栏上添加“Play”和“PiP”按钮。
  2. 导入框架并定义属性 :在视图控制器中导入必要的框架,并定义KVO上下文和相关属性。
import UIKit
import AVKit
import AVFoundation
import SharedCode
private var kvoContext = 0
let pipPossible = "pictureInPicturePossible"
let currentItemStatus = "currentItem.status"
protocol PippableViewController{
    var pipView: PipView {get}
}
extension ViewController : PippableViewController{
    var pipView: PipView{
        return view as! PipView
    }
}
@IBOutlet var beginPipBtn: UIBarButtonItem!
lazy var player: AVPlayer = {
    let p = AVPlayer()
    p.addObserver(self, forKeyPath: currentItemStatus,
                  options: .new, context: &kvoContext)
    return p
}()
var pipController: AVPictureInPictureController?
var videoUrl: URL? = nil{
    didSet{
        if let u = videoUrl{
            let asset = AVAsset(url: u)
            let item = AVPlayerItem(asset: asset,
                                    automaticallyLoadedAssetKeys: ["playable"])
            player.replaceCurrentItem(with: item)
            pipView.pipLayerPlayer = player
        }
    }
}
  1. 实现播放和画中画方法 :实现 play() beginPip() 方法。
@IBAction func play() {
    guard setAudioCategory() else{
        alert("Could not set the audio category")
        return
    }

    guard let u = embeddedVideo else{
        alert("Cannot find the embedded video")
        return
    }

    videoUrl = u
    player.play()

}
@IBAction func beginPip() {

    guard isPipSupported() else{
        alert("PiP is not supported on your machine")
        return
    }

    guard let controller = pipController else{
        alert("Could not instantiate the pip controller")
        return
    }

    controller.addObserver(self, forKeyPath: pipPossible,
                           options: .new, context: &kvoContext)

    if controller.isPictureInPicturePossible{
        controller.startPictureInPicture()
    } else {
        alert("Pip is not possible")
    }

}
  1. 监听KVO消息 :实现 observeValue 方法监听KVO消息。
override func observeValue(
    forKeyPath keyPath: String?,
    of object: Any?, change: [NSKeyValueChangeKey : Any]?,
    context: UnsafeMutableRawPointer?) {

    guard context == &kvoContext else{
        return
    }

    if keyPath == pipPossible{
        guard let possibleInt = change?[NSKeyValueChangeKey.newKey]
            as? NSNumber else{
                beginPipBtn.isEnabled = false
                return
        }

        beginPipBtn.isEnabled = possibleInt.boolValue

    }

    else if keyPath == currentItemStatus{

        guard let statusInt = change?[NSKeyValueChangeKey.newKey] as? NSNumber,
            let status = AVPlayerItemStatus(rawValue: statusInt.intValue),
            status == .readyToPlay else{
                return
        }

        startPipController()

    }

}
处理低电量模式并提供替代方案

我们可能需要知道设备是否处于低电量模式,并在用户更改该模式状态时得到更新。

解决方案 :读取 NSProcessInfo lowPowerModeEnabled 属性值来确定设备是否处于低电量模式,并监听 NSProcessInfoPowerStateDidChangeNotification 通知以了解状态变化。

操作步骤
1. 监听通知 :在视图控制器的 viewDidLoad 方法中添加通知监听器。

override func viewDidLoad() {
    super.viewDidLoad()

    NotificationCenter.default.addObserver(
        self,
        selector: #selector(ViewController.powerModeChanged(_:)),
        name: NSNotification.Name.NSProcessInfoPowerStateDidChange, object: nil)

    downloadNow()

}
  1. 实现下载方法 :实现 downloadNow 方法,避免在低电量模式下下载文件。
func downloadNow(){

    guard let url = URL(string: "http://localhost:8888/video.mp4"),
        !processInfo.isLowPowerModeEnabled else{
            return
    }

    // do the download here
    print(url)

    mustDownloadVideo = false

}
  1. 处理电量模式变化 :实现 powerModeChanged 方法,当电量模式变化时检查是否需要下载文件。
import UIKit
class ViewController: UIViewController {

    var mustDownloadVideo = true
    let processInfo = ProcessInfo.processInfo

    func powerModeChanged(_ notif: Notification){

        guard mustDownloadVideo else{
            return
        }

        downloadNow()

    }

}

通过以上方法,我们可以在iOS应用中实现用户活动搜索、删除可搜索内容、支持分屏视图、添加画中画播放功能以及处理低电量模式等功能,提升应用的用户体验和实用性。

iOS开发:搜索、多任务功能实现全解析

技术点总结与对比

为了更清晰地展示各项功能的关键信息,我们将上述功能的关键技术点整理成如下表格:
| 功能 | 关键类/协议 | 关键方法 | 主要操作步骤 |
| — | — | — | — |
| 让用户活动可搜索 | NSUserActivity UITextFieldDelegate NSUserActivityDelegate | isEligibleForSearch becomeCurrent() resignCurrent() updateUserActivityState | 设置UI、遵循协议并实现方法、编写辅助方法、创建可搜索的用户活动、处理文本字段事件、更新用户活动状态 |
| 删除应用的可搜索内容 | CSSearchableIndex | deleteAllSearchableItems deleteSearchableItems(withIdentifiers:) | 获取 CSSearchableIndex 实例、获取最新应用状态、开始批量删除 |
| 支持分屏视图 | - | - | 新项目:用最新Xcode创建;旧项目:添加 LaunchScreen.storyboard 、设置基础SDK、声明支持方向、处理 UIRequiresFullScreen 键 |
| 添加画中画播放功能 | AVPlayerLayer AVPlayerItem AVPlayer AVPictureInPictureController Pippable PippableViewController | startPictureInPicture() observeValue | 创建视图、实例化视频项目、创建播放器、设置播放器、开始播放、监听状态变化、检查画中画可用性、启动画中画 |
| 处理低电量模式并提供替代方案 | NSProcessInfo | - | 监听通知、实现下载方法、处理电量模式变化 |

功能实现流程梳理

下面通过mermaid格式的流程图来梳理各个功能的实现流程。

让用户活动可搜索流程

graph LR
    A[设置UI] --> B[遵循协议并实现方法]
    B --> C[编写辅助方法]
    C --> D[创建可搜索的用户活动]
    D --> E[处理文本字段事件]
    E --> F[更新用户活动状态]

添加画中画播放功能流程

graph LR
    A[创建视图] --> B[实例化视频项目]
    B --> C[创建播放器]
    C --> D[设置播放器]
    D --> E[开始播放]
    E --> F[监听状态变化]
    F --> G[检查画中画可用性]
    G --> H[启动画中画]
代码优化建议

在实现这些功能的过程中,我们可以对代码进行一些优化,以提高代码的可读性和可维护性。

  • 代码复用 :对于一些重复使用的代码片段,我们可以将其封装成独立的函数或类。例如,在处理低电量模式时, downloadNow 方法中的URL可以作为参数传入,这样可以提高代码的灵活性。
func downloadNow(url: URL) {
    guard !processInfo.isLowPowerModeEnabled else {
        return
    }
    // do the download here
    print(url)
    mustDownloadVideo = false
}
  • 错误处理 :在代码中添加更详细的错误处理逻辑,以提高代码的健壮性。例如,在获取视频URL时,如果URL无效,可以给出更明确的错误提示。
@IBAction func play() {
    guard setAudioCategory() else {
        alert("Could not set the audio category")
        return
    }
    guard let u = embeddedVideo else {
        alert("Cannot find the embedded video. Please check the video resource.")
        return
    }
    videoUrl = u
    player.play()
}
注意事项

在实现这些功能时,还需要注意以下几点:

  • 设备兼容性 :分屏视图和画中画功能需要设备具有足够的屏幕空间和资源,如iPad Pro支持分屏视图,而画中画功能需要特定的设备支持。在开发过程中,要进行充分的测试,确保功能在不同设备上都能正常工作。
  • 权限问题 :在处理音频和视频播放时,可能需要获取相应的权限。例如,在设置音频类别时,要确保应用具有音频播放的权限。
  • 性能优化 :在监听状态变化时,要注意避免频繁的状态检查,以免影响应用的性能。例如,在监听播放器状态时,可以合理设置KVO的监听频率。

通过以上对iOS开发中搜索、多任务功能的详细介绍,我们可以看到这些功能的实现虽然有一定的复杂度,但只要按照正确的步骤和方法进行,就可以在应用中实现这些功能,为用户带来更好的体验。希望本文能对iOS开发者有所帮助,让大家在开发过程中更加得心应手。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值