Enable stopwatch selection in widgets

release
Laurent 3 years ago
parent f5a1b7d15b
commit f6777f4cb4
  1. 2
      LaunchIntents/Info.plist
  2. 38
      LaunchIntents/IntentHandler.swift
  3. 18
      LaunchWidget/LaunchWidget.intentdefinition
  4. 52
      LaunchWidget/LaunchWidget.swift
  5. 46
      LaunchWidget/SingleTimerView.swift
  6. 8
      LeCountdown.xcodeproj/project.pbxproj
  7. 2
      LeCountdown/Info.plist
  8. 10
      LeCountdown/Widget/IntentDataProvider.swift

@ -12,7 +12,7 @@
<array/>
<key>IntentsSupported</key>
<array>
<string>SelectCountdownIntent</string>
<string>SelectTimerIntent</string>
</array>
</dict>
<key>NSExtensionPointIdentifier</key>

@ -7,38 +7,46 @@
import Intents
class IntentHandler: INExtension, SelectCountdownIntentHandling {
class IntentHandler: INExtension, SelectTimerIntentHandling {
func resolveCountdown(for intent: SelectCountdownIntent) async -> [CountdownPropertiesResolutionResult] {
func resolveTimer(for intent: SelectTimerIntent) async -> [TimerPropertiesResolutionResult] {
print("***resolveCountdown")
if let properties = intent.countdown?.first {
return [CountdownPropertiesResolutionResult.success(with: properties)]
if let properties = intent.timer?.first {
return [TimerPropertiesResolutionResult.success(with: properties)]
}
return [CountdownPropertiesResolutionResult.needsValue()]
return [TimerPropertiesResolutionResult.needsValue()]
}
func provideCountdownOptionsCollection(for intent: SelectCountdownIntent) async throws -> INObjectCollection<CountdownProperties> {
func provideTimerOptionsCollection(for intent: SelectTimerIntent) async throws -> INObjectCollection<TimerProperties> {
print("*** provideCountdownOptionsCollection")
do {
let countdowns = try IntentDataProvider.main.countdowns()
let timers = try IntentDataProvider.main.timers()
let properties: [CountdownProperties] = countdowns.map { countdown in
let properties: [TimerProperties] = timers.map { timer in
let displayName: String
let formattedDuration = countdown.duration.minuteSecond
if let name = countdown.activity?.name, !name.isEmpty {
displayName = "\(name) (\(formattedDuration))"
} else {
displayName = formattedDuration
switch timer {
case let countdown as Countdown:
let formattedDuration = countdown.duration.minuteSecond
if let name = timer.activity?.name, !name.isEmpty {
displayName = "\(name) (\(formattedDuration))"
} else {
displayName = formattedDuration
}
break
case let stopwatch as Stopwatch:
displayName = stopwatch.name ?? "no name"
default:
displayName = "no name"
}
let cp = CountdownProperties(identifier: countdown.objectID.uriRepresentation().absoluteString, display: displayName)
let cp = TimerProperties(identifier: timer.objectID.uriRepresentation().absoluteString, display: displayName)
return cp
}
let collection: INObjectCollection<CountdownProperties> = INObjectCollection(items: properties)
let collection: INObjectCollection<TimerProperties> = INObjectCollection(items: properties)
print("*** provide \(properties.count) countdowns")
return collection

@ -28,7 +28,7 @@
<key>INIntentLastParameterTag</key>
<integer>3</integer>
<key>INIntentName</key>
<string>SelectCountdown</string>
<string>SelectTimer</string>
<key>INIntentParameters</key>
<array>
<dict>
@ -88,7 +88,7 @@
<key>INIntentParameterCustomDisambiguation</key>
<true/>
<key>INIntentParameterDisplayName</key>
<string>Countdown</string>
<string>Timer</string>
<key>INIntentParameterDisplayNameID</key>
<string>lE8mOk</string>
<key>INIntentParameterDisplayPriority</key>
@ -96,9 +96,9 @@
<key>INIntentParameterFixedSizeArray</key>
<integer>1</integer>
<key>INIntentParameterName</key>
<string>countdown</string>
<string>timer</string>
<key>INIntentParameterObjectType</key>
<string>CountdownProperties</string>
<string>TimerProperties</string>
<key>INIntentParameterObjectTypeNamespace</key>
<string>88xZPY</string>
<key>INIntentParameterPromptDialogs</key>
@ -119,7 +119,7 @@
<key>INIntentParameterPromptDialogCustom</key>
<true/>
<key>INIntentParameterPromptDialogFormatString</key>
<string>There are ${count} options matching ‘${countdown}’.</string>
<string>There are ${count} options matching ‘${timer}’.</string>
<key>INIntentParameterPromptDialogFormatStringID</key>
<string>gtJyOP</string>
<key>INIntentParameterPromptDialogType</key>
@ -129,7 +129,7 @@
<key>INIntentParameterPromptDialogCustom</key>
<true/>
<key>INIntentParameterPromptDialogFormatString</key>
<string>Just to confirm, you wanted ‘${countdown}’?</string>
<string>Just to confirm, you wanted ‘${timer}’?</string>
<key>INIntentParameterPromptDialogFormatStringID</key>
<string>nntWsg</string>
<key>INIntentParameterPromptDialogType</key>
@ -165,7 +165,7 @@
</array>
</dict>
<key>INIntentTitle</key>
<string>Select Countdown</string>
<string>Select Timer</string>
<key>INIntentTitleID</key>
<string>Dm6sPw</string>
<key>INIntentType</key>
@ -178,13 +178,13 @@
<array>
<dict>
<key>INTypeDisplayName</key>
<string>CountdownProperties</string>
<string>TimerProperties</string>
<key>INTypeDisplayNameID</key>
<string>ZTfW1g</string>
<key>INTypeLastPropertyTag</key>
<integer>102</integer>
<key>INTypeName</key>
<string>CountdownProperties</string>
<string>TimerProperties</string>
<key>INTypeProperties</key>
<array>
<dict>

@ -11,33 +11,33 @@ import Intents
struct Provider: IntentTimelineProvider {
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(countdowns: [], date: Date(), configuration: SelectCountdownIntent())
SimpleEntry(timers: [], date: Date(), configuration: SelectTimerIntent())
}
func getSnapshot(for configuration: SelectCountdownIntent, in context: Context, completion: @escaping (SimpleEntry) -> ()) {
func getSnapshot(for configuration: SelectTimerIntent, in context: Context, completion: @escaping (SimpleEntry) -> ()) {
guard let cp = configuration.countdown
guard let cp = configuration.timer
else {
completion(placeholder(in: context))
return
}
let countdowns: [Countdown] = cp.compactMap {
let timers: [AbstractTimer] = cp.compactMap {
if let id = $0.identifier {
return IntentDataProvider.main.countdown(id: id)
return IntentDataProvider.main.timer(id: id)
} else {
return nil
}
}
let entry = SimpleEntry(countdowns: countdowns,
let entry = SimpleEntry(timers: timers,
date: Date(),
configuration: configuration)
completion(entry)
}
func getTimeline(for configuration: SelectCountdownIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
func getTimeline(for configuration: SelectTimerIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
getSnapshot(for: configuration, in: context) { entry in
let timeline = Timeline(entries: [entry], policy: .atEnd)
@ -48,28 +48,28 @@ struct Provider: IntentTimelineProvider {
}
struct SimpleEntry: TimelineEntry {
let countdowns: [Countdown]
let timers: [AbstractTimer]
let date: Date
let configuration: SelectCountdownIntent
let configuration: SelectTimerIntent
}
struct CountdownSimpleWidgetView: View {
let countdown: Countdown
let timer: AbstractTimer
var body: some View {
SingleCountdownView(countdown: countdown)
.widgetURL(countdown.url)
SingleTimerView(timer: timer)
.widgetURL(timer.url)
}
}
struct CountdownMultiWidgetView: View {
let countdowns: [Countdown]
let timers: [AbstractTimer]
var body: some View {
MultiCountdownView(countdowns: countdowns)
MultiCountdownView(timers: timers)
}
}
@ -93,23 +93,23 @@ struct LaunchWidgetEntryView : View {
var body: some View {
switch family {
case .systemSmall, .accessoryInline:
if let countdown = entry.countdowns.first {
CountdownSimpleWidgetView(countdown: countdown)
.background(Image(countdown.imageName))
if let timer = entry.timers.first {
CountdownSimpleWidgetView(timer: timer)
.background(Image(timer.imageName))
} else {
VoidView()
}
case .accessoryCircular:
if let countdown = entry.countdowns.first {
LockScreenCountdownView(countdown: countdown)
if let countdown = entry.timers.first {
LockScreenCountdownView(timer: countdown)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.black)
} else {
VoidView()
}
case .accessoryRectangular:
if let countdown = entry.countdowns.first {
LockScreenCountdownView(countdown: countdown)
if let timer = entry.timers.first {
LockScreenCountdownView(timer: timer)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.black)
.cornerRadius(16.0)
@ -117,7 +117,7 @@ struct LaunchWidgetEntryView : View {
VoidView()
}
default:
MultiCountdownView(countdowns: entry.countdowns)
MultiCountdownView(timers: entry.timers)
}
}
@ -128,7 +128,7 @@ struct LaunchWidget: Widget {
var body: some WidgetConfiguration {
IntentConfiguration(kind: kind,
intent: SelectCountdownIntent.self,
intent: SelectTimerIntent.self,
provider: Provider()) { entry in
LaunchWidgetEntryView(entry: entry)
}
@ -143,11 +143,11 @@ struct LaunchWidget_Previews: PreviewProvider {
let fake = Countdown.fake(context: PersistenceController.preview.container.viewContext)
LaunchWidgetEntryView(entry: SimpleEntry(countdowns: [fake], date: Date(), configuration: SelectCountdownIntent()))
LaunchWidgetEntryView(entry: SimpleEntry(timers: [fake], date: Date(), configuration: SelectTimerIntent()))
.previewContext(WidgetPreviewContext(family: .systemSmall))
LaunchWidgetEntryView(entry: SimpleEntry(countdowns: [fake, fake, fake, fake], date: Date(), configuration: SelectCountdownIntent()))
LaunchWidgetEntryView(entry: SimpleEntry(timers: [fake, fake, fake, fake], date: Date(), configuration: SelectTimerIntent()))
.previewContext(WidgetPreviewContext(family: .systemMedium))
LaunchWidgetEntryView(entry: SimpleEntry(countdowns: [fake], date: Date(), configuration: SelectCountdownIntent()))
LaunchWidgetEntryView(entry: SimpleEntry(timers: [fake], date: Date(), configuration: SelectTimerIntent()))
.previewContext(WidgetPreviewContext(family: .accessoryRectangular))
}

@ -9,18 +9,20 @@ import SwiftUI
import WidgetKit
import CoreData
struct SingleCountdownView: View {
struct SingleTimerView: View {
@Environment(\.widgetFamily) var family: WidgetFamily
var countdown: Countdown
var timer: AbstractTimer
var body: some View {
VStack {
HStack {
VStack(alignment: .leading) {
Text(countdown.displayName.uppercased())
Text(countdown.duration.minuteSecond)
Text(timer.displayName.uppercased())
if let countdown = timer as? Countdown {
Text(countdown.duration.minuteSecond)
}
}
Spacer()
}
@ -30,7 +32,7 @@ struct SingleCountdownView: View {
.monospaced()
.foregroundColor(Color.white)
.font(self.font)
.widgetURL(countdown.url)
.widgetURL(timer.url)
}
private var font: Font {
@ -51,17 +53,19 @@ struct LockScreenCountdownView: View {
@Environment(\.widgetFamily) var family: WidgetFamily
var countdown: Countdown
var timer: AbstractTimer
var body: some View {
VStack {
Text(countdown.displayName.uppercased())
Text(countdown.duration.minuteSecond)
Text(timer.displayName.uppercased())
if let countdown = timer as? Countdown {
Text(countdown.duration.minuteSecond)
}
}
.monospaced()
.foregroundColor(Color.white)
.font(self.font)
.widgetURL(countdown.url)
.widgetURL(timer.url)
}
private var font: Font {
@ -86,11 +90,11 @@ struct MultiCountdownView: View {
GridItem(spacing: 10.0),
]
var countdowns: [Countdown]
var timers: [AbstractTimer]
var body: some View {
if countdowns.isEmpty {
if timers.isEmpty {
VoidView()
} else {
@ -99,22 +103,24 @@ struct MultiCountdownView: View {
spacing: 10.0
) {
ForEach(countdowns) { countdown in
ForEach(timers) { timer in
Link(destination: countdown.url) {
Link(destination: timer.url) {
HStack {
VStack(alignment: .leading) {
Spacer()
Text(countdown.displayName.uppercased())
Text(countdown.duration.minuteSecond)
Text(timer.displayName.uppercased())
if let countdown = timer as? Countdown {
Text(countdown.duration.minuteSecond)
}
Spacer()
}
Spacer()
}
.padding(.horizontal)
.font(.callout)
.background(Image(countdown.imageName))
.background(Image(timer.imageName))
.foregroundColor(.white)
.monospaced()
.cornerRadius(16.0)
@ -157,10 +163,10 @@ struct MultiCountdownView: View {
struct CountdownView_Previews: PreviewProvider {
static var previews: some View {
SingleCountdownView(countdown: Countdown.fake(context: PersistenceController.preview.container.viewContext)).previewContext(WidgetPreviewContext(family: .systemSmall)).background(.black)
LockScreenCountdownView(countdown: Countdown.fake(context: PersistenceController.preview.container.viewContext)).previewContext(WidgetPreviewContext(family: .accessoryRectangular))
LockScreenCountdownView(countdown: Countdown.fake(context: PersistenceController.preview.container.viewContext)).previewContext(WidgetPreviewContext(family: .accessoryCircular))
MultiCountdownView(countdowns: self.countdowns(context: PersistenceController.preview.container.viewContext)).previewContext(WidgetPreviewContext(family: .systemMedium))
SingleTimerView(timer: Countdown.fake(context: PersistenceController.preview.container.viewContext)).previewContext(WidgetPreviewContext(family: .systemSmall)).background(.black)
LockScreenCountdownView(timer: Countdown.fake(context: PersistenceController.preview.container.viewContext)).previewContext(WidgetPreviewContext(family: .accessoryRectangular))
LockScreenCountdownView(timer: Countdown.fake(context: PersistenceController.preview.container.viewContext)).previewContext(WidgetPreviewContext(family: .accessoryCircular))
MultiCountdownView(timers: self.countdowns(context: PersistenceController.preview.container.viewContext)).previewContext(WidgetPreviewContext(family: .systemMedium))
}
static func countdowns(context: NSManagedObjectContext) -> [Countdown] {

@ -32,7 +32,7 @@
C438C7E02981216300BF3EF9 /* LaunchWidget.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = C438C7DB2981216200BF3EF9 /* LaunchWidget.intentdefinition */; };
C438C7E32981216300BF3EF9 /* LaunchWidgetExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = C438C7CE2981216200BF3EF9 /* LaunchWidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
C438C7E82981255D00BF3EF9 /* TimeInterval+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4060DF4297AE9A7003FAB80 /* TimeInterval+Extensions.swift */; };
C438C7EB2981266F00BF3EF9 /* SingleCountdownView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C438C7E92981260D00BF3EF9 /* SingleCountdownView.swift */; };
C438C7EB2981266F00BF3EF9 /* SingleTimerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C438C7E92981260D00BF3EF9 /* SingleTimerView.swift */; };
C438C7F229812BB200BF3EF9 /* Intents.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C438C7F129812BB200BF3EF9 /* Intents.framework */; };
C438C7F529812BB200BF3EF9 /* IntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = C438C7F429812BB200BF3EF9 /* IntentHandler.swift */; };
C438C7F929812BB200BF3EF9 /* LaunchIntents.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = C438C7F029812BB200BF3EF9 /* LaunchIntents.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
@ -225,7 +225,7 @@
C438C7DB2981216200BF3EF9 /* LaunchWidget.intentdefinition */ = {isa = PBXFileReference; lastKnownFileType = file.intentdefinition; path = LaunchWidget.intentdefinition; sourceTree = "<group>"; };
C438C7DC2981216300BF3EF9 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
C438C7DE2981216300BF3EF9 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
C438C7E92981260D00BF3EF9 /* SingleCountdownView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleCountdownView.swift; sourceTree = "<group>"; };
C438C7E92981260D00BF3EF9 /* SingleTimerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleTimerView.swift; sourceTree = "<group>"; };
C438C7F029812BB200BF3EF9 /* LaunchIntents.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = LaunchIntents.appex; sourceTree = BUILT_PRODUCTS_DIR; };
C438C7F129812BB200BF3EF9 /* Intents.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Intents.framework; path = System/Library/Frameworks/Intents.framework; sourceTree = SDKROOT; };
C438C7F429812BB200BF3EF9 /* IntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntentHandler.swift; sourceTree = "<group>"; };
@ -425,7 +425,7 @@
C438C7D52981216200BF3EF9 /* LaunchWidgetBundle.swift */,
C438C7D72981216200BF3EF9 /* LaunchWidgetLiveActivity.swift */,
C438C7D92981216200BF3EF9 /* LaunchWidget.swift */,
C438C7E92981260D00BF3EF9 /* SingleCountdownView.swift */,
C438C7E92981260D00BF3EF9 /* SingleTimerView.swift */,
C438C7DB2981216200BF3EF9 /* LaunchWidget.intentdefinition */,
C438C7DC2981216300BF3EF9 /* Assets.xcassets */,
C438C7DE2981216300BF3EF9 /* Info.plist */,
@ -882,7 +882,7 @@
C4F8B1AF298AC451005C86A5 /* Countdown+CoreDataProperties.swift in Sources */,
C445FA932987CF280054D761 /* Sound.swift in Sources */,
C498E5A6299152C600E90DE0 /* GreenCheckmarkView.swift in Sources */,
C438C7EB2981266F00BF3EF9 /* SingleCountdownView.swift in Sources */,
C438C7EB2981266F00BF3EF9 /* SingleTimerView.swift in Sources */,
C438C7D62981216200BF3EF9 /* LaunchWidgetBundle.swift in Sources */,
C4F8B18B298AC288005C86A5 /* Record+CoreDataClass.swift in Sources */,
C4F8B195298AC288005C86A5 /* Activity+CoreDataClass.swift in Sources */,

@ -8,7 +8,7 @@
</array>
<key>NSUserActivityTypes</key>
<array>
<string>SelectCountdownIntent</string>
<string>SelectTimerIntent</string>
</array>
<key>UIApplicationSceneManifest</key>
<dict>

@ -12,16 +12,16 @@ class IntentDataProvider {
static let main: IntentDataProvider = IntentDataProvider()
func countdowns() throws -> [Countdown] {
func timers() throws -> [AbstractTimer] {
let context = PersistenceController.shared.container.viewContext
let request: NSFetchRequest<Countdown> = Countdown.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(keyPath: (\Countdown.order), ascending: true)]
let request: NSFetchRequest<AbstractTimer> = AbstractTimer.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(keyPath: (\AbstractTimer.order), ascending: true)]
return try context.fetch(request)
}
func countdown(id: String) -> Countdown? {
func timer(id: String) -> AbstractTimer? {
let context = PersistenceController.shared.container.viewContext
return context.object(stringId: id) as? Countdown
return context.object(stringId: id) as? AbstractTimer
}
}

Loading…
Cancel
Save