From acda0234124d2b4cc7df3c1fedec9c553716bac5 Mon Sep 17 00:00:00 2001 From: Nathan Walker Date: Tue, 11 Feb 2025 22:48:39 -0800 Subject: [PATCH 01/11] feat: add array support to project config handling --- lib/services/project-config-service.ts | 14 +++++++++++--- .../config-manipulation/config-transformer.ts | 12 ++++++++++-- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/lib/services/project-config-service.ts b/lib/services/project-config-service.ts index df655a281b..b86db515a1 100644 --- a/lib/services/project-config-service.ts +++ b/lib/services/project-config-service.ts @@ -255,7 +255,7 @@ export default { this.writeDefaultConfig(this.projectHelper.projectDir); } - if (typeof value === "object") { + if (!Array.isArray(value) && typeof value === "object") { let allSuccessful = true; for (const prop of this.flattenObjectToPaths(value)) { @@ -298,7 +298,7 @@ export default { this.$logger.error(`Failed to update config.` + error); } finally { // verify config is updated correctly - if (this.getValue(key) !== value) { + if (!Array.isArray(this.getValue(key)) && this.getValue(key) !== value) { this.$logger.error( `${EOL}Failed to update ${ hasTSConfig ? CONFIG_FILE_NAME_TS : CONFIG_FILE_NAME_JS @@ -465,7 +465,15 @@ You may add \`nsconfig.json\` to \`.gitignore\` as the CLI will regenerate it as ): Array<{ key: string; value: any }> { const toPath = (key: any) => [basePath, key].filter(Boolean).join("."); return Object.keys(obj).reduce((all: any, key) => { - if (typeof obj[key] === "object") { + if (Array.isArray(obj[key])) { + return [ + ...all, + { + key: toPath(key), + value: obj[key], // Preserve arrays as they are + }, + ]; + } else if (typeof obj[key] === "object" && obj[key] !== null) { return [...all, ...this.flattenObjectToPaths(obj[key], toPath(key))]; } return [ diff --git a/lib/tools/config-manipulation/config-transformer.ts b/lib/tools/config-manipulation/config-transformer.ts index 77d4bd4946..e7a8e8a52f 100644 --- a/lib/tools/config-manipulation/config-transformer.ts +++ b/lib/tools/config-manipulation/config-transformer.ts @@ -21,7 +21,8 @@ export type SupportedConfigValues = | string | number | boolean - | { [key: string]: SupportedConfigValues }; + | { [key: string]: SupportedConfigValues } + | any[]; export interface IConfigTransformer { /** @@ -167,11 +168,18 @@ export class ConfigTransformer implements IConfigTransformer { return this.addProperty(key, value, this.getDefaultExportValue()); } - private createInitializer(value: SupportedConfigValues | {}): string { + private createInitializer(value: SupportedConfigValues): any { if (typeof value === "string") { return `'${value}'`; } else if (typeof value === "number" || typeof value === "boolean") { return `${value}`; + } else if (Array.isArray(value)) { + return `[${value.map((v) => this.createInitializer(v)).join(", ")}]`; + } else if (typeof value === "object" && value !== null) { + const properties = Object.entries(value) + .map(([key, val]) => `${key}: ${this.createInitializer(val)}`) + .join(", "); + return `{ ${properties} }`; } return `{}`; } From 492e9f67e24f3329a3d1523b59c281006007f78f Mon Sep 17 00:00:00 2001 From: Nathan Walker Date: Tue, 11 Feb 2025 22:49:19 -0800 Subject: [PATCH 02/11] feat(ios): 'ns widget ios' for single command widget generator --- lib/bootstrap.ts | 1 + lib/commands/config.ts | 1 + lib/commands/widget.ts | 858 +++++++++++++++++++++++++++++++++++ lib/definitions/project.d.ts | 26 ++ 4 files changed, 886 insertions(+) create mode 100644 lib/commands/widget.ts diff --git a/lib/bootstrap.ts b/lib/bootstrap.ts index bffaf7abf5..7d9cea5e18 100644 --- a/lib/bootstrap.ts +++ b/lib/bootstrap.ts @@ -470,4 +470,5 @@ injector.requireCommand( ], "./commands/native-add" ); +injector.requireCommand(["widget", "widget|ios"], "./commands/widget"); require("./key-commands/bootstrap"); diff --git a/lib/commands/config.ts b/lib/commands/config.ts index 80839a1cfa..70592ceb7f 100644 --- a/lib/commands/config.ts +++ b/lib/commands/config.ts @@ -31,6 +31,7 @@ export class ConfigListCommand implements ICommand { .map((key) => { return ( color.green(`${indent()}${key}: `) + + // @ts-ignore this.getValueString(value[key], depth + 1) ); }) diff --git a/lib/commands/widget.ts b/lib/commands/widget.ts new file mode 100644 index 0000000000..d5d8b2d4e5 --- /dev/null +++ b/lib/commands/widget.ts @@ -0,0 +1,858 @@ +import { IProjectConfigService, IProjectData } from "../definitions/project"; +import * as fs from "fs"; +import * as prompts from "prompts"; +import { ICommandParameter, ICommand } from "../common/definitions/commands"; +import { IErrors } from "../common/declarations"; +import * as path from "path"; +import * as plist from "plist"; +import { injector } from "../common/yok"; +import { capitalizeFirstLetter } from "../common/utils"; +import { EOL } from "os"; +import { SupportedConfigValues } from "../tools/config-manipulation/config-transformer"; + +export class WidgetCommand implements ICommand { + public allowedParameters: ICommandParameter[] = []; + + constructor( + protected $projectData: IProjectData, + protected $projectConfigService: IProjectConfigService, + protected $logger: ILogger, + protected $errors: IErrors + ) { + this.$projectData.initializeProjectData(); + } + + public async execute(args: string[]): Promise { + this.failWithUsage(); + + return Promise.resolve(); + } + + protected failWithUsage(): void { + this.$errors.failWithHelp("Usage: ns widget ios"); + } + public async canExecute(args: string[]): Promise { + this.failWithUsage(); + return false; + } + + protected getIosSourcePathBase() { + const resources = this.$projectData.getAppResourcesDirectoryPath(); + return path.join(resources, "iOS", "src"); + } +} +export class WidgetIOSCommand extends WidgetCommand { + constructor( + $projectData: IProjectData, + $projectConfigService: IProjectConfigService, + $logger: ILogger, + $errors: IErrors + ) { + super($projectData, $projectConfigService, $logger, $errors); + } + public async canExecute(args: string[]): Promise { + return true; + } + + public async execute(args: string[]): Promise { + this.startPrompt(args); + } + + private async startPrompt(args: string[]) { + let result = await prompts.prompt({ + type: "text", + name: "name", + message: `What name would you like for this widget? (Default is 'widget')`, + }); + + const name = (result.name || "widget").toLowerCase(); + + result = await prompts.prompt({ + type: "select", + name: "value", + message: `What type of widget would you like? (Request more options: https://github.com/NativeScript/nativescript-cli/issues)`, + choices: [ + { + title: "Live Activity", + description: + "This will create a Live Activity that will display on the iOS Lock Screen.", + value: 0, + }, + { + title: "Live Activity with Home Screen Widget", + description: + "This will create a Live Activity that will display on the iOS Lock Screen with an optional Widget.", + value: 1, + }, + { + title: "Home Screen Widget", + description: "This will create just a Home Screen Widget.", + value: 2, + }, + ], + initial: 1, + }); + + switch (result.value) { + case 0: + this.$logger.info("TODO"); + break; + case 1: + await this.generateSharedWidgetPackage( + this.$projectData.projectDir, + name + ); + this.generateWidget(this.$projectData.projectDir, name, result.value); + this.generateAppleUtility(this.$projectData.projectDir, name); + break; + case 2: + this.$logger.info("TODO"); + break; + } + } + + private async generateSharedWidgetPackage(projectDir: string, name: string) { + const sharedWidgetDir = "Shared_Resources/iOS/SharedWidget"; + const sharedWidgetPath = path.join(projectDir, sharedWidgetDir); + const sharedWidgetSourceDir = "Sources/SharedWidget"; + const sharedWidgetPackagePath = path.join( + projectDir, + `${sharedWidgetDir}/Package.swift` + ); + const sharedWidgetSourcePath = path.join( + sharedWidgetPath, + `${sharedWidgetSourceDir}/${capitalizeFirstLetter(name)}Model.swift` + ); + const gitIgnorePath = path.join(projectDir, ".gitignore"); + + if (!fs.existsSync(sharedWidgetPackagePath)) { + fs.mkdirSync(sharedWidgetPath, { recursive: true }); + fs.mkdirSync(path.join(sharedWidgetPath, sharedWidgetSourceDir), { + recursive: true, + }); + + let content = `// swift-tools-version:5.9 +import PackageDescription + +let package = Package( + name: "SharedWidget", + platforms: [ + .iOS(.v13) + ], + products: [ + .library( + name: "SharedWidget", + targets: ["SharedWidget"]) + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + ], + targets: [ + .target( + name: "SharedWidget", + dependencies: [] + ) + ] +)${EOL}`; + + fs.writeFileSync(sharedWidgetPackagePath, content); + + content = `import ActivityKit +import WidgetKit + +public struct ${capitalizeFirstLetter(name)}Model: ActivityAttributes { + public typealias DeliveryStatus = ContentState + + public struct ContentState: Codable, Hashable { + // Dynamic stateful properties about your activity go here! + public var driverName: String + public var estimatedDeliveryTime: ClosedRange + + public init(driverName: String, estimatedDeliveryTime: ClosedRange) { + self.driverName = driverName + self.estimatedDeliveryTime = estimatedDeliveryTime + } + } + + // Fixed non-changing properties about your activity go here! + public var numberOfPizzas: Int + public var totalAmount: String + + public init(numberOfPizzas: Int, totalAmount: String) { + self.numberOfPizzas = numberOfPizzas + self.totalAmount = totalAmount + } +}${EOL}`; + + fs.writeFileSync(sharedWidgetSourcePath, content); + + // update spm package + const configData = this.$projectConfigService.readConfig(projectDir); + if (!configData.ios) { + configData.ios = {}; + } + if (!configData.ios.SPMPackages) { + configData.ios.SPMPackages = []; + } + const spmPackages = configData.ios.SPMPackages; + const sharedWidgetPackage = spmPackages?.find( + (p) => p.name === "SharedWidget" + ); + if (!sharedWidgetPackage) { + spmPackages.push({ + name: "SharedWidget", + libs: ["SharedWidget"], + path: "./Shared_Resources/iOS/SharedWidget", + // @ts-ignore + targets: [name], + }); + } else { + // add target if needed + if (!sharedWidgetPackage.targets?.includes(name)) { + sharedWidgetPackage.targets.push(name); + } + } + + configData.ios.SPMPackages = spmPackages; + await this.$projectConfigService.setValue( + "", // root + configData as { [key: string]: SupportedConfigValues } + ); + + if (fs.existsSync(gitIgnorePath)) { + const gitIgnore = fs.readFileSync(gitIgnorePath, { + encoding: "utf-8", + }); + const swiftBuildIgnore = `# Swift +.build +.swiftpm`; + if (gitIgnore.indexOf(swiftBuildIgnore) === -1) { + content = `${gitIgnore}${EOL}${swiftBuildIgnore}${EOL}`; + fs.writeFileSync(gitIgnorePath, content); + } + } + + console.log(`\nCreated Shared Resources: ${sharedWidgetDir}.\n`); + } + } + + private generateWidget(projectDir: string, name: string, type: number): void { + const appResourcePath = this.$projectData.appResourcesDirectoryPath; + const capitalName = capitalizeFirstLetter(name); + const appInfoPlistPath = path.join(appResourcePath, "iOS", "Info.plist"); + const extensionDir = path.join(appResourcePath, "iOS", "extensions"); + const widgetPath = path.join(extensionDir, name); + const extensionProvisionPath = path.join(extensionDir, `provisioning.json`); + const extensionsInfoPath = path.join(widgetPath, `Info.plist`); + const extensionsPrivacyPath = path.join( + widgetPath, + `PrivacyInfo.xcprivacy` + ); + const extensionsConfigPath = path.join(widgetPath, `extension.json`); + const entitlementsPath = path.join(widgetPath, `${name}.entitlements`); + const widgetBundlePath = path.join( + widgetPath, + `${capitalName}Bundle.swift` + ); + const widgetHomeScreenPath = path.join( + widgetPath, + `${capitalName}HomeScreenWidget.swift` + ); + const widgetLiveActivityPath = path.join( + widgetPath, + `${capitalName}LiveActivity.swift` + ); + const appIntentPath = path.join(widgetPath, `AppIntent.swift`); + // const widgetLockScreenControlPath = path.join( + // widgetPath, + // `${capitalName}LockScreenControl.swift` + // ); + const appEntitlementsPath = path.join( + appResourcePath, + "iOS", + "app.entitlements" + ); + + if (!fs.existsSync(extensionsConfigPath)) { + fs.mkdirSync(widgetPath, { recursive: true }); + + let content = ` + + + + NSExtension + + NSExtensionPointIdentifier + com.apple.widgetkit-extension + + +${EOL}`; + + fs.writeFileSync(extensionsInfoPath, content); + + content = ` + + + + NSPrivacyAccessedAPITypes + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryUserDefaults + NSPrivacyAccessedAPITypeReasons + + CA92.1 + + + + NSPrivacyCollectedDataTypes + + NSPrivacyTracking + + +${EOL}`; + + fs.writeFileSync(extensionsPrivacyPath, content); + + // TODO: can add control (lock screen custom control icon handler) in future + // ${[1, 2].includes(type) ? capitalName + "LockScreenControl()" : ""} + + content = `import WidgetKit +import SwiftUI + +@main +struct ${capitalName}Bundle: WidgetBundle { + var body: some Widget { + ${[1, 2].includes(type) ? capitalName + "HomeScreenWidget()" : ""} + ${[0, 1].includes(type) ? capitalName + "LiveActivity()" : ""} + } +}${EOL}`; + + fs.writeFileSync(widgetBundlePath, content); + + content = `import WidgetKit +import AppIntents + +struct ConfigurationAppIntent: WidgetConfigurationIntent { + static var title: LocalizedStringResource { "Pizza Delivery" } + static var description: IntentDescription { "Get up to date delivery details" } + + // An example configurable parameter. + @Parameter(title: "Favorite Pizza", default: "🍕") + var favoritePizza: String + + @Parameter(title: "Random", default: "Hello") + var random: String +}${EOL}`; + + fs.writeFileSync(appIntentPath, content); + + if ([0, 1].includes(type)) { + content = `import ActivityKit +import SwiftUI +import WidgetKit +import Foundation +import SharedWidget + +struct ${capitalName}LiveActivity: Widget { + var body: some WidgetConfiguration { + ActivityConfiguration(for: ${capitalName}Model.self) { context in + // Lock screen/banner UI goes here + ContentView(driver: context.state.driverName) + .activityBackgroundTint(Color.black) + .activitySystemActionForegroundColor(Color.white) + + } dynamicIsland: { context in + DynamicIsland { + // Expanded UI goes here. Compose the expanded UI through + // various regions, like leading/trailing/center/bottom + DynamicIslandExpandedRegion(.leading) { + if let timeLeft = timeLeft(range1: Date(), range2: context.state.estimatedDeliveryTime) { + Image(systemName: "car") + .resizable() + .scaledToFit() + .frame(width: 50, height: 50) // Adjust size + .foregroundColor(timeLeft <= 1000 ? Color.green : Color.orange) + } + } + DynamicIslandExpandedRegion(.trailing) { + if let timeLeft = timeLeft(range1: Date(), range2: context.state.estimatedDeliveryTime) { + ProgressView(value: timeLeft, total: 3600) + .progressViewStyle(.circular) + .tint(timeLeft <= 1000 ? Color.green : Color.orange) + } + } + DynamicIslandExpandedRegion(.bottom) { + if let timeLeft = timeLeft(range1: Date(), range2: context.state.estimatedDeliveryTime) { + Text("\\(context.state.driverName) \\(timeLeft <= 0 ? "has arrived!" : String(format: "is %.1f min away", timeLeft / 60))") + } + } + } compactLeading: { + if let timeLeft = timeLeft(range1: Date(), range2: context.state.estimatedDeliveryTime) { + Image(systemName: "car") + .resizable() + .scaledToFit() + .frame(width: 20, height: 20) + .foregroundColor(timeLeft <= 0 ? .green : .orange) + } + } compactTrailing: { + if let timeLeft = timeLeft(range1: Date(), range2: context.state.estimatedDeliveryTime) { + Image(systemName: "timer.circle.fill") + .resizable() + .scaledToFit() + .frame(width: 20, height: 20) + .foregroundColor(timeLeft <= 0 ? .green : .orange) + } + } minimal: { + Text(context.state.driverName).font(.system(size: 12)) + } + .widgetURL(URL(string: "http://www.apple.com")) + .keylineTint(Color.red) + } + } + + func timeLeft(range1: Date, range2: ClosedRange) -> TimeInterval? { + let end = min(range1, range2.upperBound) + + if end > range1 { + let remaining = end.timeIntervalSince(range1) + print("Time left: \\(remaining)") + return remaining + } else { + return 0 + } + } +} + +struct ContentView: View { + @State var driver = "" + + var body: some View { + HStack { + Spacer() + Image(uiImage: UIImage(named: "pizza-live") ?? UIImage()) + Spacer() + Text("\\(driver) is on \\(driver == "Sally" ? "her" : "his") way!") + Spacer() + }.frame(maxWidth: .infinity, maxHeight: .infinity) + } +}${EOL}`; + + fs.writeFileSync(widgetLiveActivityPath, content); + } + + if ([1, 2].includes(type)) { + content = `import SwiftUI +import WidgetKit + +struct Provider: AppIntentTimelineProvider { + func placeholder(in context: Context) -> SimpleEntry { + SimpleEntry(date: Date(), random: "Starting", configuration: ConfigurationAppIntent()) + } + + func snapshot(for configuration: ConfigurationAppIntent, in context: Context) async -> SimpleEntry + { + SimpleEntry(date: Date(), random: configuration.random, configuration: configuration) + } + + func timeline(for configuration: ConfigurationAppIntent, in context: Context) async -> Timeline< + SimpleEntry + > { + var entries: [SimpleEntry] = [] + + // Generate a timeline consisting of five entries a second apart, starting from the current time. + let currentDate = Date() + for secondOffset in 0..<5 { + let entryDate = Calendar.current.date( + byAdding: .second, value: secondOffset, to: currentDate)! + var config = configuration + switch (secondOffset) { + case 1: + config = .pepperoni + case 2: + config = .supreme + case 3: + config = .cowboy + case 4: + config = .pineswine + default: + break; + } + let entry = SimpleEntry(date: entryDate, random: config.random, configuration: config) + entries.append(entry) + } + + return Timeline(entries: entries, policy: .atEnd) + } +} + +struct SimpleEntry: TimelineEntry { + let date: Date + let random: String + let configuration: ConfigurationAppIntent +} + +struct WidgetView: View { + var entry: Provider.Entry + + var body: some View { + ZStack { + Image(uiImage: UIImage(named: "pizza") ?? UIImage()).frame( + maxWidth: .infinity, maxHeight: .infinity) + VStack { + Text("Time:") + .foregroundStyle(.white) + Text(entry.date, style: .time) + .foregroundStyle(.white) + Text("Random City:") + .foregroundStyle(.white) + Text(entry.configuration.random) + .foregroundStyle(.white) + Text("Favorite Pizza:") + .foregroundStyle(.white) + Text(entry.configuration.favoritePizza) + .foregroundStyle(.white) + } + }.frame(maxWidth: .infinity, maxHeight: .infinity) + } +} + +@available(iOSApplicationExtension 17.0, *) +struct ${capitalName}HomeScreenWidget: Widget { + let kind: String = "widget" + + var body: some WidgetConfiguration { + AppIntentConfiguration(kind: kind, intent: ConfigurationAppIntent.self, provider: Provider()) { + entry in + WidgetView(entry: entry) + .containerBackground(.fill.tertiary, for: .widget) + } + } +} + +extension ConfigurationAppIntent { + fileprivate static var pepperoni: ConfigurationAppIntent { + let intent = ConfigurationAppIntent() + intent.favoritePizza = "Pepperoni" + intent.random = "Georgia" + return intent + } + fileprivate static var supreme: ConfigurationAppIntent { + let intent = ConfigurationAppIntent() + intent.favoritePizza = "Supreme" + intent.random = "Kansas City" + return intent + } + + fileprivate static var cowboy: ConfigurationAppIntent { + let intent = ConfigurationAppIntent() + intent.favoritePizza = "Cowboy" + intent.random = "Nashville" + return intent + } + + fileprivate static var pineswine: ConfigurationAppIntent { + let intent = ConfigurationAppIntent() + intent.favoritePizza = "Pine & Swine" + intent.random = "Portland" + return intent + } +} + +#Preview(as: .systemSmall) { + ${capitalName}HomeScreenWidget() +} timeline: { + SimpleEntry(date: .now, random: "Atlanta", configuration: .pepperoni) + SimpleEntry(date: .now, random: "Austin", configuration: .supreme) +}${EOL}`; + fs.writeFileSync(widgetHomeScreenPath, content); + } + + const bundleId = this.$projectConfigService.getValue(`id`, ""); + content = `{ + "${bundleId}.${name}": "{set-your-provision-profile-id}" +}`; + + fs.writeFileSync(extensionProvisionPath, content); + + content = ` + + + + com.apple.security.application-groups + + group.${bundleId} + + +${EOL}`; + + fs.writeFileSync(entitlementsPath, content); + + if (fs.existsSync(appInfoPlistPath)) { + const appSupportLiveActivity = "NSSupportsLiveActivities"; + const appInfoPlist = plist.parse( + fs.readFileSync(appInfoPlistPath, { + encoding: "utf-8", + }) + ) as plist.PlistObject; + + if (!appInfoPlist[appSupportLiveActivity]) { + // @ts-ignore + appInfoPlist[appSupportLiveActivity] = true; + const appPlist = plist.build(appInfoPlist); + fs.writeFileSync(appInfoPlistPath, appPlist); + } + } + + const appGroupKey = "com.apple.security.application-groups"; + if (fs.existsSync(appEntitlementsPath)) { + const appEntitlementsPlist = plist.parse( + fs.readFileSync(appEntitlementsPath, { + encoding: "utf-8", + }) + ) as plist.PlistObject; + + if (!appEntitlementsPlist[appGroupKey]) { + // @ts-ignore + appEntitlementsPlist[appGroupKey] = [`group.${bundleId}`]; + const appEntitlements = plist.build(appEntitlementsPlist); + console.log("appentitlement:", appEntitlements); + fs.writeFileSync(appEntitlementsPath, appEntitlements); + } + } else { + content = ` + + + + com.apple.security.application-groups + + group.${bundleId} + + +${EOL}`; + fs.writeFileSync(appEntitlementsPath, content); + } + + content = `{ + "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 © All rights reserved.\\"", + "IPHONEOS_DEPLOYMENT_TARGET": 18.0, + "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", + "CODE_SIGN_ENTITLEMENTS": "../../App_Resources/iOS/extensions/${name}/${name}.entitlements" + }, + "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" + } + } +}${EOL}`; + + fs.writeFileSync(extensionsConfigPath, content); + + console.log( + `🚀 Your widget is now ready to develop: App_Resources/iOS/extensions/${name}.\n` + ); + console.log( + `Followup steps:\n +- Update App_Resources/iOS/extensions/provisioning.json with your profile id. +- Customize App_Resources/iOS/extensions/${name}/${capitalizeFirstLetter( + name + )}LiveActivity.swift for your display. +- Customize Shared_Resources/iOS/SharedWidget/Sources/SharedWidget/${capitalizeFirstLetter( + name + )}Model.swift for your data. +` + ); + } + + // if (fs.existsSync(filePath)) { + // this.$errors.failWithHelp(`Error: File '${filePath}' already exists.`); + // return; + // } + } + + private generateAppleUtility(projectDir: string, name: string): void { + const appResourcePath = this.$projectData.appResourcesDirectoryPath; + const appResourceSrcPath = path.join(appResourcePath, "iOS", "src"); + const appleUtilityPath = path.join( + appResourceSrcPath, + `AppleWidgetUtils.swift` + ); + const referenceTypesPath = path.join(projectDir, "references.d.ts"); + + if (!fs.existsSync(appleUtilityPath)) { + fs.mkdirSync(appResourceSrcPath, { recursive: true }); + } + if (!fs.existsSync(appleUtilityPath)) { + } + + let content = `import Foundation +import UIKit +import ActivityKit +import WidgetKit +import SharedWidget + +@objcMembers +public class AppleWidgetUtils: NSObject { + + // Live Activity Handling + public static func startActivity(_ data: NSDictionary) { + if ActivityAuthorizationInfo().areActivitiesEnabled { + let numberOfPizzas = data.object(forKey: "numberOfPizzas") as! Int + let totalAmount = data.object(forKey: "totalAmount") as! String + let attrs = ${capitalizeFirstLetter( + name + )}Model(numberOfPizzas: numberOfPizzas, totalAmount: totalAmount) + + let driverName = data.object(forKey: "driverName") as! String + let deliveryTime = data.object(forKey: "deliveryTime") as! CGFloat + let initialStatus = ${capitalizeFirstLetter( + name + )}Model.DeliveryStatus( + driverName: driverName, estimatedDeliveryTime: Date()...Date().addingTimeInterval(deliveryTime * 60)) + let content = ActivityContent(state: initialStatus, staleDate: nil) + + do { + let activity = try Activity<${capitalizeFirstLetter( + name + )}Model>.request( + attributes: attrs, + content: content, + pushType: nil) + print("Requested a Live Activity \\(activity.id)") + } catch (let error) { + print("Error requesting Live Activity \\(error.localizedDescription)") + } + } + } + public static func updateActivity(_ data: NSDictionary) { + if ActivityAuthorizationInfo().areActivitiesEnabled { + Task { + let driverName = data.object(forKey: "driverName") as! String + let deliveryTime = data.object(forKey: "deliveryTime") as! CGFloat + let status = ${capitalizeFirstLetter(name)}Model.DeliveryStatus( + driverName: driverName, estimatedDeliveryTime: Date()...Date().addingTimeInterval(deliveryTime * 60)) + let content = ActivityContent(state: status, staleDate: nil) + + for activity in Activity<${capitalizeFirstLetter( + name + )}Model>.activities { + await activity.update(content) + } + } + } + } + public static func cancelActivity(_ data: NSDictionary) { + if ActivityAuthorizationInfo().areActivitiesEnabled { + Task { + let driverName = data.object(forKey: "driverName") as! String + let status = ${capitalizeFirstLetter(name)}Model.DeliveryStatus( + driverName: driverName, estimatedDeliveryTime: Date()...Date()) + let content = ActivityContent(state: status, staleDate: nil) + + for activity in Activity<${capitalizeFirstLetter( + name + )}Model>.activities { + await activity.end(content, dismissalPolicy: .immediate) + } + } + } + } + public static func showAllActivities() { + if ActivityAuthorizationInfo().areActivitiesEnabled { + Task { + for activity in Activity<${capitalizeFirstLetter( + name + )}Model>.activities { + print("Activity Details: \\(activity.id) -> \\(activity.attributes)") + } + } + } + } + + // Home Screen Widget Handling + public static func updateWidget() { + if #available(iOS 14.0, *) { + Task.detached(priority: .userInitiated) { + WidgetCenter.shared.reloadAllTimelines() + } + } + } +}${EOL}`; + + fs.writeFileSync(appleUtilityPath, content); + + content = `/** + * Customize for your own Apple Widget Data + */ +declare interface AppleWidgetModelData { + numberOfPizzas: number; + totalAmount: string; + driverName: string; + deliveryTime: number; +} +declare class AppleWidgetUtils extends NSObject { + static startActivity(data: AppleWidgetModelData): void; + static updateActivity( + data: Pick + ): void; + static cancelActivity(data: Pick): void; + static showAllActivities(): void; + static updateWidget(): void; +}${EOL}`; + + if (!fs.existsSync(referenceTypesPath)) { + const references = `/// +/// ${EOL}${content}`; + fs.writeFileSync(referenceTypesPath, references); + } else { + const references = fs.readFileSync(referenceTypesPath, { + encoding: "utf-8", + }); + if (references?.indexOf("AppleWidgetUtils") === -1) { + content = `${references.toString()}${EOL}${content}`; + fs.writeFileSync(referenceTypesPath, content); + } + } + } +} + +injector.registerCommand(["widget"], WidgetCommand); +injector.registerCommand(["widget|ios"], WidgetIOSCommand); diff --git a/lib/definitions/project.d.ts b/lib/definitions/project.d.ts index 7589ae9c09..9e13e752f1 100644 --- a/lib/definitions/project.d.ts +++ b/lib/definitions/project.d.ts @@ -101,8 +101,34 @@ interface INsConfigPlaform { id?: string; } +interface IOSSPMPackageBase { + name: string; + libs: string[]; + /** + * Optional: If you have more targets (like widgets for example) + * you can list their names here to include the Swift Package with them + */ + targets?: string[]; +} + +export interface IOSRemoteSPMPackage extends IOSSPMPackageBase { + repositoryURL: string; + version: string; +} + +export interface IOSLocalSPMPackage extends IOSSPMPackageBase { + path: string; +} + +export type IOSSPMPackage = IOSRemoteSPMPackage | IOSLocalSPMPackage; + interface INsConfigIOS extends INsConfigPlaform { discardUncaughtJsExceptions?: boolean; + /** + * Swift Package Manager + * List packages to be included in the iOS build. + */ + SPMPackages?: Array; } interface INSConfigVisionOS extends INsConfigIOS {} From a8ab7ac2d291a8c813208cc7df40946f72ca30cd Mon Sep 17 00:00:00 2001 From: Nathan Walker Date: Wed, 19 Feb 2025 12:47:55 -0800 Subject: [PATCH 03/11] fix: widgetbundle name collision --- lib/commands/widget.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/commands/widget.ts b/lib/commands/widget.ts index d5d8b2d4e5..3d4851ec57 100644 --- a/lib/commands/widget.ts +++ b/lib/commands/widget.ts @@ -321,7 +321,7 @@ public struct ${capitalizeFirstLetter(name)}Model: ActivityAttributes { import SwiftUI @main -struct ${capitalName}Bundle: WidgetBundle { +struct ${capitalName}Bundle: SwiftUI.WidgetBundle { var body: some Widget { ${[1, 2].includes(type) ? capitalName + "HomeScreenWidget()" : ""} ${[0, 1].includes(type) ? capitalName + "LiveActivity()" : ""} @@ -691,6 +691,7 @@ extension ConfigurationAppIntent { ); console.log( `Followup steps:\n +- Check App_Resources/iOS/build.xcconfig IPHONEOS_DEPLOYMENT_TARGET=17 or higher. - Update App_Resources/iOS/extensions/provisioning.json with your profile id. - Customize App_Resources/iOS/extensions/${name}/${capitalizeFirstLetter( name From b391fe473c7a0b7f604c76652ed0302a49608ed2 Mon Sep 17 00:00:00 2001 From: Nathan Walker Date: Wed, 19 Feb 2025 13:12:47 -0800 Subject: [PATCH 04/11] chore: cleanup --- lib/commands/widget.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/commands/widget.ts b/lib/commands/widget.ts index 3d4851ec57..bb02872250 100644 --- a/lib/commands/widget.ts +++ b/lib/commands/widget.ts @@ -691,7 +691,7 @@ extension ConfigurationAppIntent { ); console.log( `Followup steps:\n -- Check App_Resources/iOS/build.xcconfig IPHONEOS_DEPLOYMENT_TARGET=17 or higher. +- Check App_Resources/iOS/build.xcconfig uses IPHONEOS_DEPLOYMENT_TARGET=17 or higher. - Update App_Resources/iOS/extensions/provisioning.json with your profile id. - Customize App_Resources/iOS/extensions/${name}/${capitalizeFirstLetter( name From f4df29e73b2da5f0759636931f768fc7cd18e8b6 Mon Sep 17 00:00:00 2001 From: Nathan Walker Date: Wed, 19 Feb 2025 19:47:07 -0800 Subject: [PATCH 05/11] chore: update nativescript-dev-xcode to fix widget handling --- package-lock.json | 170 +++++++++++++++++++++++++++++++++++----------- package.json | 2 +- 2 files changed, 132 insertions(+), 40 deletions(-) diff --git a/package-lock.json b/package-lock.json index 577163ad1d..e616914561 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,7 +46,7 @@ "minimatch": "7.4.2", "mkdirp": "2.1.6", "mute-stream": "1.0.0", - "nativescript-dev-xcode": "0.8.0", + "nativescript-dev-xcode": "0.8.1", "open": "8.4.2", "ora": "5.4.1", "pacote": "15.1.1", @@ -2701,6 +2701,18 @@ "integrity": "sha512-LEyx4aLEC3x6T0UguF6YILf+ntvmOaWsVfENmIW0E9H09vKlLDGelMjjSm0jkDHALj8A8quZ/HapKNigzwge+Q==", "license": "ISC" }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/acorn": { "version": "8.14.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", @@ -3578,10 +3590,9 @@ } }, "node_modules/call-bind-apply-helpers": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", - "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", - "dev": true, + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -4924,7 +4935,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -5021,7 +5031,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -5031,7 +5040,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -5041,7 +5049,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -5050,6 +5057,21 @@ "node": ">= 0.4" } }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es6-promise": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-0.1.2.tgz", @@ -5161,6 +5183,15 @@ "node": ">=0.10.0" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/eventemitter2": { "version": "0.4.14", "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz", @@ -5175,6 +5206,15 @@ "dev": true, "license": "MIT" }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/execa": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/execa/-/execa-7.2.0.tgz", @@ -5292,9 +5332,9 @@ } }, "node_modules/exponential-backoff": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.1.tgz", - "integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.2.tgz", + "integrity": "sha512-8QxYTVXUkuy7fIIoitQkPwGonB8F3Zj8eEO8Sqg9Zv/bkI7RJAzowee4gr81Hak/dUTpA2Z7VfQgoijjPNlUZA==", "license": "Apache-2.0" }, "node_modules/extend": { @@ -5388,9 +5428,9 @@ "license": "MIT" }, "node_modules/fastq": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz", - "integrity": "sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==", + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.0.tgz", + "integrity": "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA==", "license": "ISC", "dependencies": { "reusify": "^1.0.4" @@ -5533,9 +5573,9 @@ } }, "node_modules/flatted": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", - "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", "license": "ISC" }, "node_modules/follow-redirects": { @@ -5623,13 +5663,14 @@ } }, "node_modules/form-data": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", - "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", "mime-types": "^2.1.12" }, "engines": { @@ -5791,7 +5832,6 @@ "version": "1.2.7", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -5927,7 +5967,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -6259,7 +6298,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -7220,7 +7258,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -7229,6 +7266,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-unicode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", @@ -8821,6 +8873,7 @@ "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.", "dev": true, "license": "MIT" }, @@ -9175,7 +9228,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -10153,9 +10205,9 @@ } }, "node_modules/nativescript-dev-xcode": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/nativescript-dev-xcode/-/nativescript-dev-xcode-0.8.0.tgz", - "integrity": "sha512-NenQo5L57dJfsG8UHFqxMp7wokXVc8oGPagBT8FBTpebxb0GyVzxHwDhAzKHwk2Oex5AvjQKGlp2iOwRwxn9Xw==", + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/nativescript-dev-xcode/-/nativescript-dev-xcode-0.8.1.tgz", + "integrity": "sha512-AIHoah4ZEo8CUC6xb7CX0dWTKHcsoO4DL+nYVwIESVd2XQspE1pzSRMmar9/maz4rxbPeFFTEN7eknqn69+aVg==", "license": "Apache-2.0", "dependencies": { "simple-plist": "1.3.1", @@ -10749,9 +10801,9 @@ } }, "node_modules/object-inspect": { - "version": "1.13.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", - "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==", + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "dev": true, "license": "MIT", "engines": { @@ -12725,12 +12777,12 @@ } }, "node_modules/readable-web-to-node-stream": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz", - "integrity": "sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.4.tgz", + "integrity": "sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw==", "license": "MIT", "dependencies": { - "readable-stream": "^3.6.0" + "readable-stream": "^4.7.0" }, "engines": { "node": ">=8" @@ -12740,6 +12792,46 @@ "url": "https://github.com/sponsors/Borewit" } }, + "node_modules/readable-web-to-node-stream/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/readable-web-to-node-stream/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, "node_modules/readdir-glob": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", @@ -14156,9 +14248,9 @@ } }, "node_modules/socks": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", - "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.4.tgz", + "integrity": "sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==", "license": "MIT", "dependencies": { "ip-address": "^9.0.5", diff --git a/package.json b/package.json index ea641ea7d4..dbce28080f 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,7 @@ "minimatch": "7.4.2", "mkdirp": "2.1.6", "mute-stream": "1.0.0", - "nativescript-dev-xcode": "0.8.0", + "nativescript-dev-xcode": "0.8.1", "open": "8.4.2", "ora": "5.4.1", "pacote": "15.1.1", From b2478c6997d0837c311551b0217bd182f793ce4b Mon Sep 17 00:00:00 2001 From: Nathan Walker Date: Fri, 21 Feb 2025 15:07:09 -0800 Subject: [PATCH 06/11] chore: cleanup --- lib/commands/widget.ts | 538 +++++++++++++++++++++++------------------ 1 file changed, 307 insertions(+), 231 deletions(-) diff --git a/lib/commands/widget.ts b/lib/commands/widget.ts index bb02872250..b671c96ecb 100644 --- a/lib/commands/widget.ts +++ b/lib/commands/widget.ts @@ -93,6 +93,8 @@ export class WidgetIOSCommand extends WidgetCommand { initial: 1, }); + const bundleId = this.$projectConfigService.getValue(`id`, ""); + switch (result.value) { case 0: this.$logger.info("TODO"); @@ -102,8 +104,13 @@ export class WidgetIOSCommand extends WidgetCommand { this.$projectData.projectDir, name ); - this.generateWidget(this.$projectData.projectDir, name, result.value); - this.generateAppleUtility(this.$projectData.projectDir, name); + this.generateWidget( + this.$projectData.projectDir, + name, + bundleId, + result.value + ); + this.generateAppleUtility(this.$projectData.projectDir, name, bundleId); break; case 2: this.$logger.info("TODO"); @@ -165,12 +172,12 @@ public struct ${capitalizeFirstLetter(name)}Model: ActivityAttributes { public struct ContentState: Codable, Hashable { // Dynamic stateful properties about your activity go here! - public var driverName: String - public var estimatedDeliveryTime: ClosedRange + public var message: String + public var deliveryTime: Double - public init(driverName: String, estimatedDeliveryTime: ClosedRange) { - self.driverName = driverName - self.estimatedDeliveryTime = estimatedDeliveryTime + public init(message: String, deliveryTime: Double) { + self.message = message + self.deliveryTime = deliveryTime } } @@ -236,7 +243,12 @@ public struct ${capitalizeFirstLetter(name)}Model: ActivityAttributes { } } - private generateWidget(projectDir: string, name: string, type: number): void { + private generateWidget( + projectDir: string, + name: string, + bundleId: string, + type: number + ): void { const appResourcePath = this.$projectData.appResourcesDirectoryPath; const capitalName = capitalizeFirstLetter(name); const appInfoPlistPath = path.join(appResourcePath, "iOS", "Info.plist"); @@ -262,7 +274,7 @@ public struct ${capitalizeFirstLetter(name)}Model: ActivityAttributes { widgetPath, `${capitalName}LiveActivity.swift` ); - const appIntentPath = path.join(widgetPath, `AppIntent.swift`); + // const appIntentPath = path.join(widgetPath, `AppIntent.swift`); // const widgetLockScreenControlPath = path.join( // widgetPath, // `${capitalName}LockScreenControl.swift` @@ -285,6 +297,10 @@ public struct ${capitalizeFirstLetter(name)}Model: ActivityAttributes { NSExtensionPointIdentifier com.apple.widgetkit-extension + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1.0 ${EOL}`; @@ -330,112 +346,117 @@ struct ${capitalName}Bundle: SwiftUI.WidgetBundle { fs.writeFileSync(widgetBundlePath, content); - content = `import WidgetKit -import AppIntents - -struct ConfigurationAppIntent: WidgetConfigurationIntent { - static var title: LocalizedStringResource { "Pizza Delivery" } - static var description: IntentDescription { "Get up to date delivery details" } - - // An example configurable parameter. - @Parameter(title: "Favorite Pizza", default: "🍕") - var favoritePizza: String - - @Parameter(title: "Random", default: "Hello") - var random: String -}${EOL}`; - - fs.writeFileSync(appIntentPath, content); - if ([0, 1].includes(type)) { content = `import ActivityKit import SwiftUI import WidgetKit import Foundation import SharedWidget +import os struct ${capitalName}LiveActivity: Widget { - var body: some WidgetConfiguration { - ActivityConfiguration(for: ${capitalName}Model.self) { context in - // Lock screen/banner UI goes here - ContentView(driver: context.state.driverName) - .activityBackgroundTint(Color.black) - .activitySystemActionForegroundColor(Color.white) - - } dynamicIsland: { context in - DynamicIsland { - // Expanded UI goes here. Compose the expanded UI through - // various regions, like leading/trailing/center/bottom - DynamicIslandExpandedRegion(.leading) { - if let timeLeft = timeLeft(range1: Date(), range2: context.state.estimatedDeliveryTime) { - Image(systemName: "car") - .resizable() - .scaledToFit() - .frame(width: 50, height: 50) // Adjust size - .foregroundColor(timeLeft <= 1000 ? Color.green : Color.orange) - } - } - DynamicIslandExpandedRegion(.trailing) { - if let timeLeft = timeLeft(range1: Date(), range2: context.state.estimatedDeliveryTime) { - ProgressView(value: timeLeft, total: 3600) - .progressViewStyle(.circular) - .tint(timeLeft <= 1000 ? Color.green : Color.orange) - } - } - DynamicIslandExpandedRegion(.bottom) { - if let timeLeft = timeLeft(range1: Date(), range2: context.state.estimatedDeliveryTime) { - Text("\\(context.state.driverName) \\(timeLeft <= 0 ? "has arrived!" : String(format: "is %.1f min away", timeLeft / 60))") - } - } - } compactLeading: { - if let timeLeft = timeLeft(range1: Date(), range2: context.state.estimatedDeliveryTime) { - Image(systemName: "car") - .resizable() - .scaledToFit() - .frame(width: 20, height: 20) - .foregroundColor(timeLeft <= 0 ? .green : .orange) - } - } compactTrailing: { - if let timeLeft = timeLeft(range1: Date(), range2: context.state.estimatedDeliveryTime) { - Image(systemName: "timer.circle.fill") - .resizable() - .scaledToFit() - .frame(width: 20, height: 20) - .foregroundColor(timeLeft <= 0 ? .green : .orange) - } - } minimal: { - Text(context.state.driverName).font(.system(size: 12)) - } - .widgetURL(URL(string: "http://www.apple.com")) - .keylineTint(Color.red) - } - } - func timeLeft(range1: Date, range2: ClosedRange) -> TimeInterval? { - let end = min(range1, range2.upperBound) - - if end > range1 { - let remaining = end.timeIntervalSince(range1) - print("Time left: \\(remaining)") - return remaining - } else { - return 0 - } - } + var body: some WidgetConfiguration { + ActivityConfiguration(for: ${capitalName}Model.self) { context in + + LockScreenView(message: context.state.message, deliveryTime: context.state.deliveryTime) + .activityBackgroundTint(Color.black) + .activitySystemActionForegroundColor(Color.white) + + } dynamicIsland: { context in + DynamicIsland { + DynamicIslandExpandedRegion(.leading) { + Image(systemName: context.state.deliveryTime >= 0 ? "car.side.arrowtriangle.up.fill" : "face.smiling.inverse") + .resizable() + .scaledToFit() + .frame(width: 50, height: 50) + .foregroundColor(context.state.deliveryTime >= 0 ? Color.green : Color.blue) + } + DynamicIslandExpandedRegion(.trailing) { + if (context.state.deliveryTime >= 0) { + ZStack { + ProgressView(value: context.state.deliveryTime, total: 60) + .progressViewStyle(.circular) + .tint(Color.green) + .frame(width: 75, height: 75) + Text("\(formatter.string(for: context.state.deliveryTime) ?? "") mins") + .font(.system(size: 11)) + .foregroundStyle(.white) + }.frame(width: 75, height: 75) + } else { + Image(systemName: "checkmark.circle.fill") + .resizable() + .scaledToFit() + .frame(width: 50, height: 50) + .foregroundColor(.blue) + } + } + DynamicIslandExpandedRegion(.bottom) { + Text("\(context.state.message)") + } + } compactLeading: { + Image(systemName: context.state.deliveryTime >= 0 ? "car.side.arrowtriangle.up.fill" : "face.smiling.inverse") + .resizable() + .scaledToFit() + .frame(width: 20, height: 20) + .foregroundColor(context.state.deliveryTime >= 0 ? .green : .blue) + } compactTrailing: { + Image(systemName: context.state.deliveryTime >= 0 ? "timer.circle.fill" : "checkmark.circle.fill") + .resizable() + .scaledToFit() + .frame(width: 20, height: 20) + .foregroundColor(context.state.deliveryTime >= 0 ? .green : .blue) + } minimal: { + Text(context.state.message).font(.system(size: 12)) + } + .widgetURL(URL(string: "http://www.apple.com")) + .keylineTint(Color.red) + } + } + + private let formatter: NumberFormatter = { + let formatter = NumberFormatter() + formatter.maximumFractionDigits = 0 + formatter.minimumFractionDigits = 0 + return formatter + }() } -struct ContentView: View { - @State var driver = "" - - var body: some View { - HStack { - Spacer() - Image(uiImage: UIImage(named: "pizza-live") ?? UIImage()) - Spacer() - Text("\\(driver) is on \\(driver == "Sally" ? "her" : "his") way!") - Spacer() - }.frame(maxWidth: .infinity, maxHeight: .infinity) - } +struct LockScreenView: View { + @State private var message = "" + @State private var deliveryTime: Double = 0 + // for console debugging + let logger = Logger(subsystem: "${bundleId}.${name}", category: "Widget") + + var body: some View { + ZStack { + LinearGradient( + gradient: Gradient(colors: [Color.gray.opacity(0.3), Color.black]), + startPoint: .top, + endPoint: .bottom + ) + VStack { + Spacer() + Image(systemName: deliveryTime >= 0 ? "car.side.arrowtriangle.up.fill" : "face.smiling.inverse") + .resizable() + .scaledToFit() + .frame(width: 50, height: 50) + .foregroundColor(deliveryTime >= 0 ? .green : .blue) + Spacer() + Text("\(message)") + .foregroundStyle(.white) + Spacer() + } + }.frame(maxWidth: .infinity, maxHeight: .infinity) + } + + init(message: String = "", deliveryTime: Double = 0) { + _message = State(initialValue: message) + _deliveryTime = State(initialValue: deliveryTime) + + // Logs the deliveryTime at init for debugging purposes if needed + logger.log("deliveryTime: \(deliveryTime)") + } }${EOL}`; fs.writeFileSync(widgetLiveActivityPath, content); @@ -445,130 +466,180 @@ struct ContentView: View { content = `import SwiftUI import WidgetKit -struct Provider: AppIntentTimelineProvider { +/** + * Widget data shared between the app and the widget extension. + */ +struct WidgetData: Codable { + let pizzas: [String] + let orderTime: Double + let delivered: Bool +} + +struct Provider: TimelineProvider { + func placeholder(in context: Context) -> SimpleEntry { - SimpleEntry(date: Date(), random: "Starting", configuration: ConfigurationAppIntent()) + SimpleEntry(date: Date(), pizza: "Pepperoni", delivered: false, orderTime: Date()) } - - func snapshot(for configuration: ConfigurationAppIntent, in context: Context) async -> SimpleEntry - { - SimpleEntry(date: Date(), random: configuration.random, configuration: configuration) + + func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) { + let entry = SimpleEntry(date: Date(), pizza: "Pepperoni", delivered: false, orderTime: Date()) + completion(entry) } - - func timeline(for configuration: ConfigurationAppIntent, in context: Context) async -> Timeline< - SimpleEntry - > { + + func getTimeline(in context: Context, completion: @escaping @Sendable (Timeline) -> ()) { var entries: [SimpleEntry] = [] - - // Generate a timeline consisting of five entries a second apart, starting from the current time. - let currentDate = Date() - for secondOffset in 0..<5 { - let entryDate = Calendar.current.date( - byAdding: .second, value: secondOffset, to: currentDate)! - var config = configuration - switch (secondOffset) { - case 1: - config = .pepperoni - case 2: - config = .supreme - case 3: - config = .cowboy - case 4: - config = .pineswine - default: - break; + + if let sharedDefaults = UserDefaults(suiteName: "group.${bundleId}") { + let currentDate = Date() + if let jsonString = sharedDefaults.string(forKey: "widgetData") { + if let jsonData = jsonString.data(using: .utf8) { + do { + let widgetData = try JSONDecoder().decode(WidgetData.self, from: jsonData) + let pizzas = widgetData.pizzas + let orderTime = Date(timeIntervalSince1970: widgetData.orderTime/1000) + let delivered = widgetData.delivered + + // Generate a timeline of entries 1 second apart, starting from the current date. + for secondOffset in 0.. WidgetRelevances { +// // Generate a list containing the contexts this widget is relevant in. +// } } struct SimpleEntry: TimelineEntry { let date: Date - let random: String - let configuration: ConfigurationAppIntent + let pizza: String + let delivered: Bool + let orderTime: Date? } struct WidgetView: View { + @Environment(\.widgetFamily) var widgetFamily var entry: Provider.Entry var body: some View { - ZStack { - Image(uiImage: UIImage(named: "pizza") ?? UIImage()).frame( - maxWidth: .infinity, maxHeight: .infinity) - VStack { - Text("Time:") - .foregroundStyle(.white) - Text(entry.date, style: .time) - .foregroundStyle(.white) - Text("Random City:") - .foregroundStyle(.white) - Text(entry.configuration.random) - .foregroundStyle(.white) - Text("Favorite Pizza:") - .foregroundStyle(.white) - Text(entry.configuration.favoritePizza) + VStack { + if (entry.pizza != "") { + Spacer() + Image(systemName: entry.delivered ? "face.smiling.inverse" : "car.side") + .resizable() + .scaledToFit() + .frame(width: iconSize(for: widgetFamily), height: iconSize(for: widgetFamily)) + .foregroundColor(entry.delivered ? .blue : .green) + Spacer() + if (entry.delivered) { + Text("Pizza Delivered!") + .font(.system(size: fontSize(for: widgetFamily), weight: .bold)) + .foregroundStyle(.white) + } else { + HStack(spacing: 4) { + Text("Ordered:") + .font(.system(size: fontSize(for: widgetFamily))) + .foregroundStyle(.white) + Text(entry.orderTime!, style: .time) + .font(.system(size: fontSize(for: widgetFamily), weight: .bold)) + .foregroundStyle(.white) + } + HStack(spacing: 4) { + Text("Pizza:") + .font(.system(size: fontSize(for: widgetFamily))) + .foregroundStyle(.white) + Text(entry.pizza) + .font(.system(size: fontSize(for: widgetFamily), weight: .bold)) + .foregroundStyle(.white) + } + } + Spacer() + } else { + Spacer() + Image(systemName: "car.side.rear.open") + .resizable() + .scaledToFit() + .frame(width: 55, height: 55) + .foregroundColor(.gray) + Spacer() + Text("Awaiting orders...") .foregroundStyle(.white) + Spacer() } }.frame(maxWidth: .infinity, maxHeight: .infinity) } + + private func iconSize(for family: WidgetFamily) -> CGFloat { + switch family { + case .systemSmall: + return 65 + case .systemMedium: + return 85 + case .systemLarge: + return 150 + default: + return 65 + } + } + + private func fontSize(for family: WidgetFamily) -> CGFloat { + switch family { + case .systemSmall: + return 12 + case .systemMedium: + return 14 + case .systemLarge: + return 18 + default: + return 14 + } + } } @available(iOSApplicationExtension 17.0, *) struct ${capitalName}HomeScreenWidget: Widget { let kind: String = "widget" - + var body: some WidgetConfiguration { - AppIntentConfiguration(kind: kind, intent: ConfigurationAppIntent.self, provider: Provider()) { - entry in + StaticConfiguration(kind: kind, provider: Provider()) { entry in WidgetView(entry: entry) - .containerBackground(.fill.tertiary, for: .widget) + .containerBackground(for: .widget) { + LinearGradient( + gradient: Gradient(colors: [Color.black.opacity(0.6), Color.black]), + startPoint: .top, + endPoint: .bottom + ) + } } - } -} - -extension ConfigurationAppIntent { - fileprivate static var pepperoni: ConfigurationAppIntent { - let intent = ConfigurationAppIntent() - intent.favoritePizza = "Pepperoni" - intent.random = "Georgia" - return intent - } - fileprivate static var supreme: ConfigurationAppIntent { - let intent = ConfigurationAppIntent() - intent.favoritePizza = "Supreme" - intent.random = "Kansas City" - return intent - } - - fileprivate static var cowboy: ConfigurationAppIntent { - let intent = ConfigurationAppIntent() - intent.favoritePizza = "Cowboy" - intent.random = "Nashville" - return intent - } - - fileprivate static var pineswine: ConfigurationAppIntent { - let intent = ConfigurationAppIntent() - intent.favoritePizza = "Pine & Swine" - intent.random = "Portland" - return intent + .configurationDisplayName("${capitalName} Widget") + .description("${capitalName} delivery service.") } } #Preview(as: .systemSmall) { ${capitalName}HomeScreenWidget() } timeline: { - SimpleEntry(date: .now, random: "Atlanta", configuration: .pepperoni) - SimpleEntry(date: .now, random: "Austin", configuration: .supreme) + SimpleEntry(date: .now, pizza: "Pepperoni", delivered: false, orderTime: Date()) + SimpleEntry(date: .now, pizza: "Hawaiian", delivered: false, orderTime: Date()) }${EOL}`; fs.writeFileSync(widgetHomeScreenPath, content); } - const bundleId = this.$projectConfigService.getValue(`id`, ""); content = `{ "${bundleId}.${name}": "{set-your-provision-profile-id}" }`; @@ -709,7 +780,11 @@ extension ConfigurationAppIntent { // } } - private generateAppleUtility(projectDir: string, name: string): void { + private generateAppleUtility( + projectDir: string, + name: string, + bundleId: string + ): void { const appResourcePath = this.$projectData.appResourcesDirectoryPath; const appResourceSrcPath = path.join(appResourcePath, "iOS", "src"); const appleUtilityPath = path.join( @@ -738,43 +813,35 @@ public class AppleWidgetUtils: NSObject { if ActivityAuthorizationInfo().areActivitiesEnabled { let numberOfPizzas = data.object(forKey: "numberOfPizzas") as! Int let totalAmount = data.object(forKey: "totalAmount") as! String - let attrs = ${capitalizeFirstLetter( - name - )}Model(numberOfPizzas: numberOfPizzas, totalAmount: totalAmount) + let attrs = ${capitalizeFirstLetter}Model(numberOfPizzas: numberOfPizzas, totalAmount: totalAmount) - let driverName = data.object(forKey: "driverName") as! String - let deliveryTime = data.object(forKey: "deliveryTime") as! CGFloat - let initialStatus = ${capitalizeFirstLetter( - name - )}Model.DeliveryStatus( - driverName: driverName, estimatedDeliveryTime: Date()...Date().addingTimeInterval(deliveryTime * 60)) + let message = data.object(forKey: "message") as! String + let deliveryTime = data.object(forKey: "deliveryTime") as! Double + let initialStatus = ${capitalizeFirstLetter}Model.DeliveryStatus( + message: message, deliveryTime: deliveryTime) let content = ActivityContent(state: initialStatus, staleDate: nil) do { - let activity = try Activity<${capitalizeFirstLetter( - name - )}Model>.request( + let activity = try Activity<${capitalizeFirstLetter}Model>.request( attributes: attrs, content: content, pushType: nil) - print("Requested a Live Activity \\(activity.id)") + print("Requested a Live Activity \(activity.id)") } catch (let error) { - print("Error requesting Live Activity \\(error.localizedDescription)") + print("Error requesting Live Activity \(error.localizedDescription)") } } } public static func updateActivity(_ data: NSDictionary) { if ActivityAuthorizationInfo().areActivitiesEnabled { Task { - let driverName = data.object(forKey: "driverName") as! String - let deliveryTime = data.object(forKey: "deliveryTime") as! CGFloat - let status = ${capitalizeFirstLetter(name)}Model.DeliveryStatus( - driverName: driverName, estimatedDeliveryTime: Date()...Date().addingTimeInterval(deliveryTime * 60)) + let message = data.object(forKey: "message") as! String + let deliveryTime = data.object(forKey: "deliveryTime") as! Double + let status = ${capitalizeFirstLetter}Model.DeliveryStatus( + message: message, deliveryTime: deliveryTime) let content = ActivityContent(state: status, staleDate: nil) - for activity in Activity<${capitalizeFirstLetter( - name - )}Model>.activities { + for activity in Activity<${capitalizeFirstLetter}Model>.activities { await activity.update(content) } } @@ -783,30 +850,37 @@ public class AppleWidgetUtils: NSObject { public static func cancelActivity(_ data: NSDictionary) { if ActivityAuthorizationInfo().areActivitiesEnabled { Task { - let driverName = data.object(forKey: "driverName") as! String - let status = ${capitalizeFirstLetter(name)}Model.DeliveryStatus( - driverName: driverName, estimatedDeliveryTime: Date()...Date()) + let message = data.object(forKey: "message") as! String + let status = ${capitalizeFirstLetter}Model.DeliveryStatus( + message: message, deliveryTime: 0) let content = ActivityContent(state: status, staleDate: nil) - for activity in Activity<${capitalizeFirstLetter( - name - )}Model>.activities { + for activity in Activity<${capitalizeFirstLetter}Model>.activities { await activity.end(content, dismissalPolicy: .immediate) } } } } - public static func showAllActivities() { - if ActivityAuthorizationInfo().areActivitiesEnabled { - Task { - for activity in Activity<${capitalizeFirstLetter( - name - )}Model>.activities { - print("Activity Details: \\(activity.id) -> \\(activity.attributes)") - } - } - } - } + public static func getData(key: String) -> String? { + guard let sharedDefaults = UserDefaults(suiteName: "group.${bundleId}") else { + return nil + } + return sharedDefaults.object(forKey: key) as? String + } + public static func updateData(key: String, _ data: String) { + guard let sharedDefaults = UserDefaults(suiteName: "group.${bundleId}") else { + return + } + sharedDefaults.set(data, forKey: key) + sharedDefaults.synchronize() + } + public static func removeData(key: String) { + guard let sharedDefaults = UserDefaults(suiteName: "group.${bundleId}") else { + return + } + sharedDefaults.removeObject(forKey: key) + sharedDefaults.synchronize() + } // Home Screen Widget Handling public static func updateWidget() { @@ -832,11 +906,13 @@ declare interface AppleWidgetModelData { declare class AppleWidgetUtils extends NSObject { static startActivity(data: AppleWidgetModelData): void; static updateActivity( - data: Pick + data: Pick ): void; - static cancelActivity(data: Pick): void; - static showAllActivities(): void; + static cancelActivity(data: Pick): void; static updateWidget(): void; + static updateDataWithKey(key: string, data: string): void; + static getDataWithKey(key: string): string; + static removeDataWithKey(key: string): void; }${EOL}`; if (!fs.existsSync(referenceTypesPath)) { From ab6a577a5a3fca8d3741de3a58aa0f60e75150c3 Mon Sep 17 00:00:00 2001 From: Nathan Walker Date: Fri, 21 Feb 2025 15:15:14 -0800 Subject: [PATCH 07/11] cleanup --- lib/commands/widget.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/commands/widget.ts b/lib/commands/widget.ts index b671c96ecb..966d37d03f 100644 --- a/lib/commands/widget.ts +++ b/lib/commands/widget.ts @@ -785,6 +785,7 @@ struct ${capitalName}HomeScreenWidget: Widget { name: string, bundleId: string ): void { + const capitalName = capitalizeFirstLetter(name); const appResourcePath = this.$projectData.appResourcesDirectoryPath; const appResourceSrcPath = path.join(appResourcePath, "iOS", "src"); const appleUtilityPath = path.join( @@ -813,16 +814,16 @@ public class AppleWidgetUtils: NSObject { if ActivityAuthorizationInfo().areActivitiesEnabled { let numberOfPizzas = data.object(forKey: "numberOfPizzas") as! Int let totalAmount = data.object(forKey: "totalAmount") as! String - let attrs = ${capitalizeFirstLetter}Model(numberOfPizzas: numberOfPizzas, totalAmount: totalAmount) + let attrs = ${capitalName}Model(numberOfPizzas: numberOfPizzas, totalAmount: totalAmount) let message = data.object(forKey: "message") as! String let deliveryTime = data.object(forKey: "deliveryTime") as! Double - let initialStatus = ${capitalizeFirstLetter}Model.DeliveryStatus( + let initialStatus = ${capitalName}Model.DeliveryStatus( message: message, deliveryTime: deliveryTime) let content = ActivityContent(state: initialStatus, staleDate: nil) do { - let activity = try Activity<${capitalizeFirstLetter}Model>.request( + let activity = try Activity<${capitalName}Model>.request( attributes: attrs, content: content, pushType: nil) @@ -837,11 +838,11 @@ public class AppleWidgetUtils: NSObject { Task { let message = data.object(forKey: "message") as! String let deliveryTime = data.object(forKey: "deliveryTime") as! Double - let status = ${capitalizeFirstLetter}Model.DeliveryStatus( + let status = ${capitalName}Model.DeliveryStatus( message: message, deliveryTime: deliveryTime) let content = ActivityContent(state: status, staleDate: nil) - for activity in Activity<${capitalizeFirstLetter}Model>.activities { + for activity in Activity<${capitalName}Model>.activities { await activity.update(content) } } @@ -851,11 +852,11 @@ public class AppleWidgetUtils: NSObject { if ActivityAuthorizationInfo().areActivitiesEnabled { Task { let message = data.object(forKey: "message") as! String - let status = ${capitalizeFirstLetter}Model.DeliveryStatus( + let status = ${capitalName}Model.DeliveryStatus( message: message, deliveryTime: 0) let content = ActivityContent(state: status, staleDate: nil) - for activity in Activity<${capitalizeFirstLetter}Model>.activities { + for activity in Activity<${capitalName}Model>.activities { await activity.end(content, dismissalPolicy: .immediate) } } From be062205c63a58aebceb32605707e8d880e27433 Mon Sep 17 00:00:00 2001 From: Nathan Walker Date: Fri, 21 Feb 2025 15:21:14 -0800 Subject: [PATCH 08/11] cleanup --- lib/commands/widget.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/commands/widget.ts b/lib/commands/widget.ts index 966d37d03f..4da32487cd 100644 --- a/lib/commands/widget.ts +++ b/lib/commands/widget.ts @@ -70,7 +70,7 @@ export class WidgetIOSCommand extends WidgetCommand { result = await prompts.prompt({ type: "select", name: "value", - message: `What type of widget would you like? (Request more options: https://github.com/NativeScript/nativescript-cli/issues)`, + message: `What type of widget would you like?`, choices: [ { title: "Live Activity", From cf57a7fae9831bdc0acfe1d9b4bbe90200c9ede7 Mon Sep 17 00:00:00 2001 From: Nathan Walker Date: Fri, 21 Feb 2025 15:25:18 -0800 Subject: [PATCH 09/11] cleanup --- lib/commands/widget.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/commands/widget.ts b/lib/commands/widget.ts index 4da32487cd..69fa9f71ab 100644 --- a/lib/commands/widget.ts +++ b/lib/commands/widget.ts @@ -379,7 +379,7 @@ struct ${capitalName}LiveActivity: Widget { .progressViewStyle(.circular) .tint(Color.green) .frame(width: 75, height: 75) - Text("\(formatter.string(for: context.state.deliveryTime) ?? "") mins") + Text("\\(formatter.string(for: context.state.deliveryTime) ?? "") mins") .font(.system(size: 11)) .foregroundStyle(.white) }.frame(width: 75, height: 75) @@ -392,7 +392,7 @@ struct ${capitalName}LiveActivity: Widget { } } DynamicIslandExpandedRegion(.bottom) { - Text("\(context.state.message)") + Text("\\(context.state.message)") } } compactLeading: { Image(systemName: context.state.deliveryTime >= 0 ? "car.side.arrowtriangle.up.fill" : "face.smiling.inverse") @@ -443,7 +443,7 @@ struct LockScreenView: View { .frame(width: 50, height: 50) .foregroundColor(deliveryTime >= 0 ? .green : .blue) Spacer() - Text("\(message)") + Text("\\(message)") .foregroundStyle(.white) Spacer() } From bb88d6089f154097ec40773c9aeabfdd0ed12ea2 Mon Sep 17 00:00:00 2001 From: Nathan Walker Date: Fri, 21 Feb 2025 16:17:59 -0800 Subject: [PATCH 10/11] cleanup --- lib/commands/widget.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/commands/widget.ts b/lib/commands/widget.ts index 69fa9f71ab..b7290c7c8d 100644 --- a/lib/commands/widget.ts +++ b/lib/commands/widget.ts @@ -533,7 +533,7 @@ struct SimpleEntry: TimelineEntry { } struct WidgetView: View { - @Environment(\.widgetFamily) var widgetFamily + @Environment(\\.widgetFamily) var widgetFamily var entry: Provider.Entry var body: some View { @@ -574,7 +574,7 @@ struct WidgetView: View { Image(systemName: "car.side.rear.open") .resizable() .scaledToFit() - .frame(width: 55, height: 55) + .frame(width: iconSize(for: widgetFamily), height: iconSize(for: widgetFamily)) .foregroundColor(.gray) Spacer() Text("Awaiting orders...") From a469570c39e259d4707625d248662b675f4dc0ee Mon Sep 17 00:00:00 2001 From: Nathan Walker Date: Fri, 21 Feb 2025 20:25:11 -0800 Subject: [PATCH 11/11] cleanup --- lib/commands/widget.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/commands/widget.ts b/lib/commands/widget.ts index b7290c7c8d..70e750b57a 100644 --- a/lib/commands/widget.ts +++ b/lib/commands/widget.ts @@ -455,7 +455,7 @@ struct LockScreenView: View { _deliveryTime = State(initialValue: deliveryTime) // Logs the deliveryTime at init for debugging purposes if needed - logger.log("deliveryTime: \(deliveryTime)") + logger.log("deliveryTime: \\(deliveryTime)") } }${EOL}`; @@ -827,9 +827,9 @@ public class AppleWidgetUtils: NSObject { attributes: attrs, content: content, pushType: nil) - print("Requested a Live Activity \(activity.id)") + print("Requested a Live Activity \\(activity.id)") } catch (let error) { - print("Error requesting Live Activity \(error.localizedDescription)") + print("Error requesting Live Activity \\(error.localizedDescription)") } } }