在本文中,我们将探讨如何为您的 NativeScript iOS 应用开发主屏幕小部件。如果您按照本文中的步骤操作,您将构建一个看起来像这样的简单小部件:
在开始之前,需要注意以下几点
.widget
附加到您的应用程序标识符。让我们创建一个示例项目(将 com.nativescript.ExtensionApp
替换为您的应用程序标识符)
ns create ExtensionApp --template @nativescript/template-hello-world-ts --appid com.nativescript.ExtensionApp
cd ExtensionApp
ns prepare ios
打开 Xcode,并打开我们在 ExtensionApp/platforms/ios/ExtensionApp.xcodeproj
中创建的项目,注意:较大的应用程序将有一个 .xcworkspace
,在这种情况下,您应该打开它。
打开 文件
、新建
、目标..
菜单项。
对于 iOS 平台,选择 小部件扩展
,然后点击下一步。
widget
作为项目名称,并取消选中 包含实时活动
和 包含配置应用程序意图
。该项目现在已经填充了一些示例小部件代码。
创建一个文件夹 App_Resources/iOS/extensions
。
将文件夹 platforms/ios/widget
复制到 App_Resources/iOS/extensions
中,这样您现在将拥有一个文件夹 App_Resources/iOS/extensions/widget
。
创建一个文件 App_Resources/iOS/extensions/widget/extension.json
添加内容
{
"frameworks": ["SwiftUI.framework", "WidgetKit.framework"],
"targetBuildConfigurationProperties": {
"ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME" : "AccentColor",
"ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME" : "WidgetBackground",
"CLANG_ANALYZER_NONNULL" : "YES",
"CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION" : "YES_AGGRESSIVE",
"CLANG_CXX_LANGUAGE_STANDARD" : "\"gnu++20\"",
"CLANG_ENABLE_OBJC_WEAK" : "YES",
"CLANG_WARN_DOCUMENTATION_COMMENTS" : "YES",
"CLANG_WARN_UNGUARDED_AVAILABILITY" : "YES_AGGRESSIVE",
"CURRENT_PROJECT_VERSION" : 1,
"GCC_C_LANGUAGE_STANDARD" : "gnu11",
"GCC_WARN_UNINITIALIZED_AUTOS" : "YES_AGGRESSIVE",
"GENERATE_INFOPLIST_FILE": "YES",
"INFOPLIST_KEY_CFBundleDisplayName" : "widget",
"INFOPLIST_KEY_NSHumanReadableCopyright" : "\"Copyright © 2023 NativeScript. All rights reserved.\"",
"IPHONEOS_DEPLOYMENT_TARGET" : 16.4,
"MARKETING_VERSION" : "1.0",
"MTL_FAST_MATH" : "YES",
"PRODUCT_NAME" : "widget",
"SWIFT_EMIT_LOC_STRINGS" : "YES",
"SWIFT_VERSION" : "5.0",
"TARGETED_DEVICE_FAMILY" : "\"1,2\"",
"MTL_ENABLE_DEBUG_INFO" : "NO",
"SWIFT_OPTIMIZATION_LEVEL" : "\"-O\"",
"COPY_PHASE_STRIP": "NO",
"SWIFT_COMPILATION_MODE": "wholemodule"
},
"targetNamedBuildConfigurationProperties": {
"debug": {
"DEBUG_INFORMATION_FORMAT" : "dwarf",
"GCC_PREPROCESSOR_DEFINITIONS": "(\"DEBUG=1\",\"$(inherited)\",)",
"MTL_ENABLE_DEBUG_INFO" : "INCLUDE_SOURCE",
"SWIFT_ACTIVE_COMPILATION_CONDITIONS" : "DEBUG",
"SWIFT_OPTIMIZATION_LEVEL" : "\"-Onone\""
},
"release": {
"CODE_SIGN_STYLE" : "Manual",
"MTL_ENABLE_DEBUG_INFO" : "NO",
"SWIFT_OPTIMIZATION_LEVEL" : "\"-O\"",
"COPY_PHASE_STRIP": "NO",
"SWIFT_COMPILATION_MODE": "wholemodule"
}
}
}
注意:这些值取自平台/ios pbxproject 文件的扩展目标,不同的扩展类型可能具有不同的值。
使用 ns run ios
运行应用程序
您现在可以将创建的主屏幕小部件添加到主屏幕。
如上所述,扩展需要它们自己的应用程序标识符和配置文件,因此要正确地为分发签名您的应用程序
创建两个应用程序标识符
org.nativescript.ExtensionApp
widget
)附加到主机应用程序标识符,例如 org.nativescript.ExtensionApp.widget
然后创建两个相应的配置文件。
在 Xcode 中下载配置文件。
创建一个新文件 App_Resources/iOS/extensions/provisioning.json
,其内容为
{
"com.nativescript.ExtensionApp.widget": "<The name or UUID of the widget provisioning profile>"
}
将小部件应用程序标识符和配置文件 UUID/名称替换为您自己的。
现在,您可以使用以下命令构建应用程序的发布版本以供发布
ns build ios --release --for-device --provision [Main App Profile Name/UUID] --env.production
为了演示如何处理权利,我们将共享应用程序和扩展之间的数据。为此,我们将使用 应用程序组。
创建一个应用程序组,并为其分配 group.<您的应用程序 ID>
的 ID,例如 group.org.nativescript.ExtensionApp
。
将该应用程序组添加到您的主应用程序和扩展应用程序 ID 配置中(如果通过 Apple 开发者网站进行操作,请记住重新生成配置文件)。
创建一个新文件 App_Resources/iOS/extensions/widget/widget.entitlements
,并添加
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string><!-- your group id --></string>
</array>
</dict>
</plist>
将相同的内容添加到 App_Resources/iOS/app.entitlements
在 App_Resources/iOS/extensions/widget/extension.json
的 targetBuildConfigurationProperties
部分添加
"CODE_SIGN_ENTITLEMENTS": "../../App_Resources/iOS/extensions/widget/widget.entitlements",
应用程序和小部件现在都应该能够访问应用程序组中的项目。
我们将使用 UserDefaults 在应用程序和小部件之间传递数据。
修改文件 app/main-view-model.ts
,并将以下内容添加到 HelloWorldModel
类中
private getGroupId(): string {
return "group." + NSBundle.mainBundle.bundleIdentifier;
}
private readonly VALUE_KEY="COUNTER";
private updateSharedValue(){
const defaults=NSUserDefaults.alloc().initWithSuiteName(this.getGroupId());
defaults.setObjectForKey(this._counter.toString(), this.VALUE_KEY);
}
在 HelloWorldModel
类的 onTap()
方法的末尾添加对 this.updateSharedValue();
的调用。
修改小部件代码以从 UserDefaults 读取,方法是将 App_Resources/iOS/extensions/widget/widget.swift
的内容替换为
import WidgetKit
import SwiftUI
struct Provider: TimelineProvider {
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(date: Date(), emoji: "😀")
}
func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
let entry = SimpleEntry(date: Date(), emoji: "😀")
completion(entry)
}
private func getGroupId() -> String {
var result: String = ""
if let bundleIdentifier = Bundle.main.bundleIdentifier {
let replacedString = bundleIdentifier
.replacingOccurrences(of: ".widget", with: "")
result = "group." + replacedString
}
return result;
}
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
var entries: [SimpleEntry] = [];
var count = UserDefaults(suiteName: getGroupId())!.string(forKey: "COUNTER");
// Generate a timeline consisting of five entries an hour apart, starting from the current date.
let currentDate = Date()
for hourOffset in 0 ..< 5 {
let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
let entry = SimpleEntry(date: entryDate, emoji: "😀", count: count ?? "")
entries.append(entry)
}
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
}
struct SimpleEntry: TimelineEntry {
let date: Date
let emoji: String
var count: String = ""
}
struct widgetEntryView : View {
var entry: Provider.Entry
var body: some View {
VStack {
Text("Time:")
Text(entry.date, style: .time)
Text("Emoji:")
Text(entry.emoji + " " + entry.count)
}
}
}
struct widget: Widget {
let kind: String = "widget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider()) { entry in
if #available(iOS 17.0, *) {
widgetEntryView(entry: entry)
.containerBackground(.fill.tertiary, for: .widget)
} else {
widgetEntryView(entry: entry)
.padding()
.background()
}
}
.configurationDisplayName("My Widget")
.description("This is an example widget.")
}
}
#Preview(as: .systemSmall) {
widget()
} timeline: {
SimpleEntry(date: .now, emoji: "😀")
SimpleEntry(date: .now, emoji: "🤩")
}
有了上面的代码,小部件将定期使用应用程序中点击事件的最新结果进行更新,但我们更希望小部件立即更新。
要实现这一点,我们需要调用 WidgetKit
的相关方法来触发更新。WidgetKit
默认情况下无法从 TypeScript 访问,因此我们必须创建一个 swift
实用类。
创建文件 App_Resources/iOS/src/utility.swift
,其内容为
import Foundation
import WidgetKit
@objcMembers
@objc(NSCUtilsHelper)
public class NSCUtilsHelper: NSObject {
public static func updateWidget(){
if #available(iOS 14.0, *) {
Task.detached(priority: .userInitiated) {
WidgetCenter.shared.reloadAllTimelines()
}
}
}
}
创建文件 types/objc!nsswiftsupport.d.ts
,其内容为
declare class NSCUtilsHelper extends NSObject {
static alloc(): NSCUtilsHelper; // inherited from NSObject
static new(): NSCUtilsHelper; // inherited from NSObject
static updateWidget(): void;
}
将以下行添加到 references.d.ts
中
/// <reference path="./types/objc!nsswiftsupport.d.ts" />
最后,将以下行添加到 app/main-view-model.ts
中的 updateSharedValue
方法中
NSCUtilsHelper.updateWidget();
现在,当您点击 点击
按钮时,小部件将反映应用程序中显示的计数器。
CURRENT_PROJECT_VERSION
和 MARKETING_VERSION
需要与父应用程序版本匹配。