【iOS小组件】快捷指令延伸

1,823 阅读5分钟

注意:

  • 快捷指令只要定义便可以在快捷方式工具中心中使用
  • 经过试验快捷指令如果不通过 AppShortcutsProvider 注册无法在 快捷指令Spotlight 及 Siri 中展示和使用

定义Intent

定义 Intent 需要实现 AppIntent 协议

struct MyIntent: AppIntent {
    static var title = LocalizedStringResource("Dialog")
    static var description = IntentDescription("Dialog描述")

    func perform() async throws -> some IntentResult & ProvidesDialog & ReturnsValue<String> {
        return .result(value: "## title", dialog: IntentDialog(stringLiteral: "Result message"))
    }
}

AppIntent 在实现快捷指令时需要根据需求实现以下方法:

  • title(必须实现):快捷指令名称_

  • description(可选):快捷指令描述_

  • openAppWhenRun(可选):执行快捷指令时打开App

  • perform(必须实现):业务逻辑处理

在快捷指令中使用,【快捷指令】->【+】->【搜索指令】添加,添加完效果如下:

image.png

image.png

openAppWhenRun 打开App

struct OpenIntentAppIntent {
    static var title = LocalizedStringResource("输入框")
    static var description = IntentDescription("输入框描述")

    // 意图执行时,是否自动将应用拉起到前台
    static var openAppWhenRun: Bool = true

    func perform() async throws -> some IntentResult & ProvidesDialog {
        return .result(dialog: "app拉起了")
    }
}

在 AppIntent 中配置 openAppWhenRuntrue,快捷指令执行时就会默认拉起主App,效果如下:‍‍‍‍‍

image.png

注册快捷指令

注册快捷指令需要继承 AppShortcutsProvider 协议并实现对应方法。

struct MyShortcuts: AppShortcutsProvider {
    static var appShortcuts: [AppShortcut] {
        AppShortcut(
            intent: MyIntent(),
            phrases: [
                "Start a \(.applicationName)",
                "Start a session with \(.applicationName)"
            ],
            shortTitle: "Start a Task",
            systemImageName: "plus.circle"
        )
    }
}

AppShortcutsProvider 在实现快捷指令时需要根据需求实现以下方法:

image.png

快捷指令返回值类型

注意:

  • 快捷指令 perform() 函数返回值的类型和真实返回的类型必须匹配否则会引起崩溃

快捷指令支持多种返回值类型及组合返回值类型

  • IntentResult: **perform()**的返回值类型,支持无参
  • ProvidesDialog:提供对话框类型
  • ShowsSnippetView:展示自定义视图
  • OpensIntent:打开意图
  • ReturnsValue:返回一个结果值类型

1.IntentResult

struct MyIntent: AppIntent {
    static var title = LocalizedStringResource("MyIntent")
    static var description = IntentDescription("MyIntent描述")
    
    func perform() async throws -> some IntentResult {
        return .result()
    }
}

2.ProvidesDialog

struct MyIntent: AppIntent {
    static var title = LocalizedStringResource("MyIntent")
    static var description = IntentDescription("MyIntent描述")
    
    func perform() async throws -> some ProvidesDialog {
        return .result(dialog: "dialog title")
    }
}

image.png

3.ShowsSnippetView

struct MyIntent: AppIntent {
    static var title = LocalizedStringResource("MyIntent")
    static var description = IntentDescription("MyIntent描述")
    
    func perform() async throws -> some ShowsSnippetView {
        return .result(view: 
            VStack(spacing: 10) {
                Text("this is text")
                Image(systemName: "plus.circle")
                Button("this is a button") {}
            }.padding()
        )
    }
}

image.png

4.OpensIntent

struct MyIntent: AppIntent {
    static var title = LocalizedStringResource("MyIntent")
    static var description = IntentDescription("MyIntent描述")
    
    func perform() async throws -> some OpensIntent {
        print("预处理打印")
        return .result(opensIntent: MyIntent2())
    }
}

struct MyIntent2AppIntent {
    static var title = LocalizedStringResource("MyIntent2")
    static var description = IntentDescription("MyIntent2描述")
    
    func perform() async throws -> some IntentResult {
        print("最终处理打印")
        return .result()
    }
}

image.png

5.ReturnsValue

struct MyIntent: AppIntent {
    static var title = LocalizedStringResource("MyIntent")
    static var description = IntentDescription("MyIntent描述")
    
    func perform() async throws -> some ReturnsValue<Int> {
        return .result(value: 1111)
    }
}

image.png

快捷指令复合类型

1.弹窗dialog

struct MyIntent: AppIntent {
    static var title = LocalizedStringResource("MyIntent")
    static var description = IntentDescription("MyIntent描述")
    
    
    func perform() async throws -> some IntentResult & ProvidesDialog {
        return .result(dialog: "this is a dialog")
    }
}

image.png

2.自定义弹窗

注意:

  • perform()返回值类型为需要是 some IntentResult & ProvidesDialog & ShowsSnippetView 或者 some ProvidesDialog & ShowsSnippetView 否则会报错
// 自定义弹窗
struct CustomDialogIntent: AppIntent {
    static var title = LocalizedStringResource("自定义弹窗信息")
    static var description = IntentDescription("自定义弹窗信息描述")
    

    func perform() async throws -> some IntentResult & ProvidesDialog & ShowsSnippetView {
        return .result(dialog: .init("Dialog标题")) {
            VStack(spacing: 10) {
                Text("自定义的弹窗内容")
                Text("自定义的弹窗内容")
                Text("自定义的弹窗内容")
            }
        }
    }
}

image.png

3. 自定义返回及视图

struct TrailEntity: AppEntity {
    let id: String
    let name: String
    let currentConditions: String
    
    static var typeDisplayRepresentation: TypeDisplayRepresentation = "Trail"
    
    var displayRepresentation: DisplayRepresentation {
        DisplayRepresentation(
            title: "\(name)",
            subtitle: "Current conditions: \(currentConditions)"
        )
    }
    
    static var defaultQuery = TrailQuery()
    
    struct TrailQuery: EntityQuery {
        func entities(for identifiers: [String]) async throws -> [TrailEntity] {
            // 在实际应用中,这里应该从数据源获取实体
            // 这里我们只返回一个示例实体
            return [TrailEntity(id: "11", name: "Sample Trail", currentConditions: "Good")]
        }
        
        func suggestedEntities() async throws -> [TrailEntity] {
            // 返回建议的实体列表
            return [TrailEntity(id: "11", name: "Popular Trail", currentConditions: "Excellent")]
        }
    }
}

struct TrailInfoView: View {
    let trail: TrailEntity
    let includeConditions: Bool
    
    var body: some View {
        VStack(alignment: .leading) {
            Text(trail.name)
                .font(.headline)
            if includeConditions {
                Text("Current Conditions: \(trail.currentConditions)")
                    .font(.subheadline)
            }
        }
    }
}

struct TrailInfoIntent: AppIntent {
    static var title: LocalizedStringResource = "Get Trail Info"
    static var description = IntentDescription("Retrieves information about a specific trail.")

    @Parameter(title: "Trail")
    var trail: TrailEntity?

    func perform() async throws -> some IntentResult & ReturnsValue<TrailEntity> & ProvidesDialog & ShowsSnippetView {

        guard let trail else {
            throw $trail.needsValueError(.init("trail?"))
        }

        let snippet = TrailInfoView(trail: trail, includeConditions: true)
        
        let dialog = IntentDialog(
            full: "The latest conditions reported for \(trail.name) indicate: \(trail.currentConditions).",
            supporting: "Here's the latest information on trail conditions."
        )
        
        return .result(value: trail, dialog: dialog, view: snippet)
    }
}


struct TrailInfoShortcuts: AppShortcutsProvider {
    static var appShortcuts: [AppShortcut] {
        AppShortcut(
            intent: TrailInfoIntent(),
            phrases: ["\(.applicationName) 创建用户表单"],
            shortTitle: "创建用户表",
            systemImageName: "plus.circle"
        )
    }
}

image.png

image.png

快捷指令自定义参数

1.基本数据类型

struct MyIntent: AppIntent {
    static var title = LocalizedStringResource("MyIntent")
    static var description = IntentDescription("MyIntent描述")
    
    @Parameter(title: "name", description: "用户姓名")
    var name: String?
    
    @Parameter(title: "age", description: "用户年龄")
    var age: Int?
    
    @Parameter(title: "isChinese", description: "用户国籍为中国?", default: true)
    var isChinese: Bool
    
    func perform() async throws -> some IntentResult & ShowsSnippetView {
        return .result(view: VStack{
            Text("name: \(name ?? "")")
            Text("age: \(age ?? 18)")
            Text("isChinese: \(isChinese)")
        })
    }
}

image.png

2.输入框

import AppIntents
import SwiftUI

// 输入框
struct InputIntent: AppIntent {
    static var title = LocalizedStringResource("输入框")
    static var description = IntentDescription("输入框描述")

    // 定义参数
    @Parameter(title:"用户昵称")
    var username: String?

    func perform() async throws -> some IntentResult & ProvidesDialog {
       
        return .result(dialog: "显示用户昵称: \(username)")
    }
}
  • 使用 @Parameter 定义一个参数
  • 使用($参数名)内置对象调用 needsValueError 方法弹出一个提示弹窗进行数据录入和验证

image.png

3.枚举Intent

// 枚举
enum TemperatureUnit: String, CaseIterable, AppEnum {
    case celsius
    case fahrenheit

    // 类型描述
    static var typeDisplayRepresentation: TypeDisplayRepresentation = "Temperature Unit"

    // 枚举值
    static var caseDisplayRepresentations: [TemperatureUnit : DisplayRepresentation] = [
        .celsius: "Celsius (°C)",
        .fahrenheit: "Fahrenheit (°F)"
    ]
}

struct InputIntent: AppIntent {
    static var title = LocalizedStringResource("输入框")
    static var description = IntentDescription("输入框描述")

    // 定义参数
    @Parameter(title:"用户昵称")
    var username: String?

    // 温度枚举
    // @Parameter(title: "Temperature Unit", default: TemperatureUnit.celsius)
    // var temperatureUnit: TemperatureUnit
    // add --- 设置为可选
    @Parameter(title: "Temperature Unit")
    var temperatureUnit: TemperatureUnit?


    func perform() async throws -> some IntentResult & ProvidesDialog {
        // 数据验证
        guard let username else {
            throw $username.needsValueError()
        }
        // 验证数据长度
        if username.count < 3 {
            throw $username.needsValueError(.init("用户昵称小于3个字符"))
        }
        
        // add --- 验证枚举
        guard let temperatureUnit else {
            throw $temperatureUnit.needsValueError()
        }
        return .result(dialog: "显示用户昵称: \(username), 温度: \(temperatureUnit)")
    }
}
...

image.png

3.对象Intent

构建一个建议对象列表,选取返回一个对象信息

import AppIntents
import SwiftUI

var WeatherLocations: [String: WeatherLocation] =  [
    "london": WeatherLocation(id: "london", name: "London", latitude: 51.5074, longitude: -0.1278),
    "new-york": WeatherLocation(id: "new-york", name: "New York", latitude: 40.7128, longitude: -74.0060),
    "tokyo": WeatherLocation(id: "tokyo", name: "Tokyo", latitude: 35.6762, longitude: 139.6503)
]

struct WeatherLocation: AppEntity {
    let id: String
    let name: String
    let latitude: Double
    let longitude: Double
    
    // 配置界面中的显示方式
    static var typeDisplayRepresentation: TypeDisplayRepresentation = "Weather Location"

    // 显示的参数名称
    var displayRepresentation: DisplayRepresentation {
        DisplayRepresentation(title: "\(name)")
    }
    
    // 默认数据查询
    static var defaultQuery = WeatherLocationQuery()
}

struct WeatherLocationQuery: EntityQuery {
    // 根据 ID 获取位置实体
    func entities(for identifiers: [WeatherLocation.ID]) async throws -> [WeatherLocation] {
        return identifiers.compactMap { id in
            return WeatherLocations[id]
        }
    }
    
    // 建议列表
    func suggestedEntities() async throws -> [WeatherLocation] {
        return Array(WeatherLocations.values)
    }
}

struct ConfigurationAppIntent: AppIntent {
    static var title: LocalizedStringResource = "Configuration"
    static var description = IntentDescription("This is an example widget.")
    
    @Parameter(title: "Location")
    var location: WeatherLocation?
    
    func perform() async throws -> some ProvidesDialog & ShowsSnippetView {
        guard (location != nil) else {
            throw $location.needsValueError()
        }
        return .result(dialog: "") {
            VStack(spacing: 10) {
                Text("location info: \(String(describing: location))")
            }
        }
    }
}

struct ObjectShortcuts: AppShortcutsProvider {
    static var appShortcuts: [AppShortcut] {
        AppShortcut(
            intent: ConfigurationAppIntent(),
            phrases: ["\(.applicationName) 对象"],
            shortTitle: "对象",
            systemImageName: "lock.open"
        )
    }
}

image.png

数据验证

1.强校验

import AppIntents
import SwiftUI

// 输入框
struct InputIntent: AppIntent {
    static var title = LocalizedStringResource("输入框")
    static var description = IntentDescription("输入框描述")

    // 定义参数
    @Parameter(title:"用户昵称")
    var username: String?

    func perform() async throws -> some IntentResult & ProvidesDialog {
        // 数据验证
        guard let username else {
            throw $username.needsValueError()
        }
        // 验证数据长度
        if username.count < 3 {
            throw $username.needsValueError(.init("用户昵称小于3个字符"))
        }
        
        return .result(dialog: "显示用户昵称: \(username), 温度: \(temperatureUnit)")
    }
}

2.校验建议

struct MyIntent: AppIntent {
    static var title = LocalizedStringResource("MyIntent")
    static var description = IntentDescription("MyIntent描述")
    
    @Parameter(title: "name", description: "用户姓名")
    var name: String?
    
    func perform() async throws -> some IntentResult & ShowsSnippetView {
        guard let name else {
            throw $name.needsDisambiguationError(among: ["1", "2"])
        }
        return .result(view: VStack{
            Text("name: \(name)")
        })
    }
}

image.png

本文同步自微信公众号 "程序员小溪" ,这里只是同步,想看及时消息请移步我的公众号,不定时更新我的学习经验。