Post

Replies

Boosts

Views

Activity

Reply to Shortcuts: Invalid action metadata
Code snippet for the Shortcuts discussed above: struct HabitShortcuts: AppShortcutsProvider { static var appShortcuts: [AppShortcut] { AppShortcut( intent: HabitEntryAppIntent(), phrases: [ "Add an entry to \(\.$habitEntity) with \(.applicationName)", "Add an entry to \(\.$habitEntity) in \(.applicationName)", "Add entry \(\.$habitEntity) with \(.applicationName)", "Add entry \(\.$habitEntity) in \(.applicationName)", "Add an entry with \(.applicationName)", "Add an entry in \(.applicationName)", "Add an entry to \(.applicationName)", "Log an entry with \(.applicationName)" ], shortTitle: LocalizedStringResource("Add an Entry", table: "AppIntents"), systemImageName: "checkmark.circle.fill", parameterPresentation: ParameterPresentation( for: \.$habitEntity, summary: Summary("Add \(\.$habitEntity) entries", table: "AppIntents"), optionsCollections: { OptionsCollection(HabitEntityQuery(), title: LocalizedStringResource("Log Completed Habits", table: "AppIntents"), systemImageName: "checkmark") } ) ) AppShortcut( intent: HabitEntryCounterForTodayAppIntent(), phrases: [ "Count \(\.$habitEntity) entries logged today with \(.applicationName)", "Count \(\.$habitEntity) entries logged today in \(.applicationName)", "How many \(\.$habitEntity) entries have been logged today in \(.applicationName)?", "Count habit entries logged today with \(.applicationName)", "Count habit entries logged today in \(.applicationName)", "Count entries logged today for a habit with \(.applicationName)", "Count entries logged today for a habit in \(.applicationName)", "Count \(.applicationName) entries logged today" ], shortTitle: LocalizedStringResource("Count Entries Logged Today", table: "AppIntents"), systemImageName: "number.circle.fill", parameterPresentation: ParameterPresentation( for: \.$habitEntity, summary: Summary("The total number of \(\.$habitEntity) entries logged so far today", table: "AppIntents"), optionsCollections: { OptionsCollection(HabitEntityQuery(), title: LocalizedStringResource("Count Entries Logged Today in a Habit", table: "AppIntents"), systemImageName: "number") } ) ) } }
May ’25
Reply to Shortcuts: Invalid action metadata
Code snippets for the app intents discussed above: // This is the problematic intent that consistently fails. struct HabitEntryAppIntent: AppIntent { static let openAppWhenRun: Bool = false static let title = LocalizedStringResource("Add a Habit Entry", table: "AppIntents") static let description = IntentDescription( LocalizedStringResource("Log an entry after completing a task.", table: "AppIntents"), categoryName: LocalizedStringResource("Log Habits", table: "AppIntents"), searchKeywords: [LocalizedStringResource("log", table: "AppIntents")], resultValueName: LocalizedStringResource("Habit", table: "AppIntents") ) static var parameterSummary: some ParameterSummary { Summary("Add an entry to \(\.$habitEntity)", table: "AppIntents") } @Parameter( title: LocalizedStringResource("Habit", table: "AppIntents"), requestValueDialog: .init(LocalizedStringResource("Which habit would you like to add an entry to?", table: "AppIntents")) ) var habitEntity: HabitEntity @MainActor func perform() async throws -> some ProvidesDialog & ReturnsValue<HabitEntity> { let context = ModelContainer.shared.mainContext let logger = AppIntentHabitEntryLogger() try logger.addEntry(for: habitEntity.id, in: context) let localizedActionName = String(localized: "Logged “\(habitEntity.name)” Entry") context.undoManager?.setActionName(localizedActionName) WidgetManager.shared.reload(.all) return .result( value: habitEntity, dialog: .init(LocalizedStringResource("OK, added an entry to “\(habitEntity.name)”.", table: "AppIntents")) ) } } extension HabitEntryAppIntent { init(habit: HabitEntity) { self.habitEntity = habit } } // This is the non-problematic intent that consistently suceeds. struct HabitEntryCounterForTodayAppIntent: AppIntent, AppIntentDialogDateFormatting { static let openAppWhenRun: Bool = false static let title = LocalizedStringResource("Number of Entries Logged in a Habit Today", table: "AppIntents") static let description = IntentDescription( LocalizedStringResource("The total number of entries logged so far today for a habit.", table: "AppIntents"), categoryName: LocalizedStringResource("Habit Stats", table: "AppIntents"), searchKeywords: [LocalizedStringResource("count", table: "AppIntents")], resultValueName: LocalizedStringResource("Habit Statistics", table: "AppIntents") ) static var parameterSummary: some ParameterSummary { Summary("Get the total number of \(\.$habitEntity) entries logged so far today", table: "AppIntents") } @Parameter( title: LocalizedStringResource("Habit", table: "AppIntents"), requestValueDialog: .init(LocalizedStringResource("Which habit would you like to count entries in?", table: "AppIntents")) ) var habitEntity: HabitEntity @MainActor func perform() async throws -> some ProvidesDialog & ReturnsValue<Int> { let habitID = habitEntity.id let context = ModelContainer.shared.mainContext let date = Date.now let startOfToday = Calendar.shared.startOfDay(for: date) let startOfTomorrow = Calendar.shared.startOfNextDay(for: date) var descriptor = FetchDescriptor<HabitEntry>(predicate: #Predicate { ($0.habitID == habitID) && ($0.date >= startOfToday) && ($0.date < startOfTomorrow) && ($0.count > 0) }) descriptor.propertiesToFetch = [\HabitEntry.count] let matches = try context.fetch(descriptor) let totalCount: Int = matches.reduce(.zero) { $0 + $1.count } return .result( value: totalCount, dialog: .init(LocalizedStringResource("There are \(totalCount) entries recorded so far today for “\(habitEntity.name)”.", table: "AppIntents")) ) } } extension HabitEntryCounterForTodayAppIntent { init(habit: HabitEntity) { self.habitEntity = habit } } // This is the code that logs the habit in the problematic intent. @MainActor struct AppIntentHabitEntryLogger { func addEntry(for habitID: UUID, in context: ModelContext) throws { let currentDate = Date.now let startOfToday = Calendar.shared.startOfDay(for: currentDate) let startOfTomorrow = Calendar.shared.startOfNextDay(for: currentDate) var descriptor = FetchDescriptor<HabitEntry>(predicate: #Predicate { ($0.habitID == habitID) && ($0.date >= startOfToday) && ($0.date < startOfTomorrow) }, sortBy: [ SortDescriptor<HabitEntry>(\.date, order: .forward) // Oldest to newest. ]) descriptor.propertiesToFetch = [\HabitEntry.count] let matches = try context.fetch(descriptor) // The last match is the most recent one for the current day. if let match = matches.last { match.increaseCount() } else { let habit = try Habit.match(for: habitID, in: context) let newEntry = HabitEntry(habit: habit) context.insert(newEntry) } if context.hasChanges { try context.save() } } }
May ’25
Reply to Set 12 vs 24 hour with the new date formatting API in iOS 15
Thanks a lot for posting that code – I see exactly what you mean. I'm now thinking the best thing to do is to stick with DateFormatter after all and use df.setLocalizedDateFormatFromTemplate("jjmm") since that respects the 12/24h system setting, and get rid of my local in-app isTwentyFourHour Bool setting. Then I can use DateFormatter.dateFormat(fromTemplate: "j", options: 0, locale: self)?.contains("a") to determine the system setting wherever it's required. Unless there are any other pitfalls you might know about? I noticed that when using the new date.formatted() API that the output does not respect the 12/24h system setting at all, which is not going to work well for my app. For example, if I set the device region to United States, it always provides a 12h format even when the [Settings > General > Date & Time > 24-Hour Time] system setting is turned on.
Topic: App & System Services SubTopic: General Tags:
Mar ’22
Reply to Set 12 vs 24 hour with the new date formatting API in iOS 15
Hi – here's my previous comments rewritten: Thanks for getting back to me. So, let me try to explain what "it" is again. In a nutshell, I want to use the new date formatting API to display hours and minutes, but I want to control whether the hours are displayed in 12h format or 24h format. This can be done with DateFormatter but I'm currently trying to update some code: I want to replace all the DateFormatter usage with the new API. As an example, I have code using the new API such as date.formatted(formatStyle.hour(.defaultDigits(amPM: .abbreviated)).minute()) where formatStyle is something like in my original question and date is just a date instance. However, I see no way to control whether the hour digits are provided as 12h or 24h format – they, understandably, come as what is relevant to the user locale by default. So, for example, a user in the US will see 12h digits, and a user in France will see 24h digits. But what I really want to do is respect a local user setting: I have a boolean toggle in my app that allows the user to select their preference (12h vs 24h clocks); if the user has selected that they prefer the 24h format, I want to force the new API to always give me 24h format, and similarly if the user selects the 12h format, I want the new API to always provide the 12h format, no matter what the device locale is. But I so far cannot find a way to do this. Or, another way of putting it, let's look at the DateFormatter equivalent and see if this can be done with the new API: If we have a local user preference called isTwentyFourHourClock (a Bool) and I wanted to respect that setting I could use formatter.setLocalizedDateFormatFromTemplate(isTwentyFourHourClock ? "HHmm" : "hhmm"). Is there any way to achieve this with the date.formatted() method? To answer your other questions:  You’re displaying a time to the user? Yes.  You’re trying to display 24-hour time regardless of the user’s preferences? I'm trying to respect the user's local in-app preference for 12h vs 24h formats.  Do you absolutely require a two digit hour? Or do you want the width of any hours before 10 to vary by locale? I actually have a lot of different widgets in my app that display the current time. In some cases I display hours that are zero padded and some that are not necessarily padded. With the new API I see this can be done using .hour(.twoDigits(amPM: .abbreviated)) vs .hour(.defaultDigits(amPM: .abbreviated)). In the 12-hour case, do you want the am/pm markers? Similarly for the am/pm markers, in some cases I show them, in others they are hidden. This is easily controlled with the Date.FormatStyle.Symbol.Hour.AMPMStyle options.
Topic: App & System Services SubTopic: General Tags:
Mar ’22