Skip to content

Commit

Permalink
Add Configurable Widget, OpenURL inside app
Browse files Browse the repository at this point in the history
  • Loading branch information
npwitk committed Dec 26, 2024
1 parent aac9407 commit 4a4092a
Show file tree
Hide file tree
Showing 7 changed files with 228 additions and 0 deletions.
16 changes: 16 additions & 0 deletions SuperCounter/Router.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//
// Router.swift
// SuperCounter
//
// Created by Nonprawich I. on 26/12/2024.
//

import Foundation

@Observable
class Router {
var tallyName: String?
init(tallyName: String? = nil) {
self.tallyName = tallyName
}
}
9 changes: 9 additions & 0 deletions SuperCounter/SuperCounterApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,19 @@ import SwiftData

@main
struct SuperCounterApp: App {
@State private var router = Router()

var body: some Scene {
WindowGroup {
TallySelectionView()
.onOpenURL { url in
guard url.scheme == "mtls",
url.host == "tally" else { return }

router.tallyName = url.lastPathComponent
}
}
.modelContainer(for: Tally.self)
.environment(router)
}
}
8 changes: 8 additions & 0 deletions SuperCounter/TallySelectionView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import SwiftData
import WidgetKit

struct TallySelectionView: View {
@Environment(Router.self) var router
@Query(sort: \Tally.name) var tallies: [Tally]
@State private var selectedTally: Tally?
@Environment(\.modelContext) var modelContext
Expand Down Expand Up @@ -135,6 +136,11 @@ struct TallySelectionView: View {
id = UUID()
}
}
.onChange(of: router.tallyName) { oldValue, newValue in
if newValue != selectedTally?.name {
selectedTally = tallies.first(where: { $0.name == newValue })
}
}
}
}
}
Expand All @@ -144,8 +150,10 @@ struct TallySelectionView: View {

#Preview("Mock Data", traits: .mockData) {
TallySelectionView()
.environment(Router())
}

#Preview("Empty") {
TallySelectionView()
.environment(Router())
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//
// ConfigurableUpdateIntent.swift
// SuperCounterWidgetExtension
//
// Created by Nonprawich I. on 26/12/2024.
//

import AppIntents
import SwiftData
import WidgetKit

struct ConfigurableUpdateIntent: AppIntent {
static var title: LocalizedStringResource = LocalizedStringResource("Update first tally")
static var description: IntentDescription? = IntentDescription("Tap the tally once to increment")

@Parameter(title: "Tally")
var name: String

init(name: String) {
self.name = name
}

init() { }

func perform() async throws -> some IntentResult {
let update = await updateTally(name: name)
return .result(value: update)
}

@MainActor func updateTally(name: String) -> Int {
let container = try! ModelContainer(for: Tally.self)
let predicate = #Predicate<Tally> { $0.name == name }
let descriptor = FetchDescriptor<Tally>(predicate: predicate)
let foundTallies = try? container.mainContext.fetch(descriptor)

if let tally = foundTallies?.first {
tally.increase()
try? container.mainContext.save()
WidgetCenter.shared.reloadAllTimelines()
return tally.value
}
return 0
}
}
100 changes: 100 additions & 0 deletions SuperCounterWidget/ConfigurableWidget/ConfigurableWidget.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
//
// Provider.swift
// SuperCounter
//
// Created by Nonprawich I. on 26/12/2024.
//


import WidgetKit
import SwiftUI
import SwiftData

struct ConfigWidgetProvider: AppIntentTimelineProvider {

var container: ModelContainer = {
try! ModelContainer(for: Tally.self)
}()

func placeholder(in context: Context) -> ConfigurableEntry {
ConfigurableEntry(date: Date(), configuration: ConfigurationAppIntent(), selectedTally: nil)
}

func snapshot(for configuration: ConfigurationAppIntent, in context: Context) async -> ConfigurableEntry {
let selectedTally = try? await getTally(name: configuration.selectedTally?.id)
return ConfigurableEntry(date: Date(), configuration: configuration, selectedTally: selectedTally)
}

func timeline(for configuration: ConfigurationAppIntent, in context: Context) async -> Timeline<ConfigurableEntry> {
let currentDate = Date.now
let selectedTally = try? await getTally(name: configuration.selectedTally?.id)
let entry = ConfigurableEntry(date: currentDate, configuration: configuration, selectedTally: selectedTally)
return Timeline(entries: [entry], policy: .atEnd)
}

@MainActor func getTally(name: String?) throws -> Tally? {
guard let name else { return nil }
let predicate = #Predicate<Tally> { $0.name == name }
let descriptor = FetchDescriptor<Tally>(predicate: predicate)
let foundTallies = try? container.mainContext.fetch(descriptor)
return foundTallies?.first
}

}

struct ConfigurableEntry: TimelineEntry {
let date: Date
let configuration: ConfigurationAppIntent
let selectedTally: Tally?
}

struct ConfigurableWidgetEntryView : View {
var entry: ConfigWidgetProvider.Entry

var body: some View {
if entry.selectedTally == nil {
ContentUnavailableView("No Tallies yet", systemImage: "plus.circle.fill")
} else {
Link(destination: URL(string: "mtls://tally/\(entry.selectedTally!.name)")!) {
ZStack {
Color.clear
VStack {
Button(intent: ConfigurableUpdateIntent(name: entry.selectedTally!.name)) {
SingleTallyView(size: 50, tally: entry.selectedTally!)
}

Text(entry.selectedTally!.name)
.font(.caption)
.fontDesign(.rounded)
.bold()
}
.buttonStyle(.plain)


}
}
}
}
}

struct ConfigurableWidget: Widget {
let kind: String = "ConfigurableWidget"

var body: some WidgetConfiguration {
AppIntentConfiguration(kind: kind, intent: ConfigurationAppIntent.self, provider: ConfigWidgetProvider()) { entry in
ConfigurableWidgetEntryView(entry: entry)
.containerBackground(.fill.tertiary, for: .widget)
}
.configurationDisplayName("Tally")
.description("Update your Tally")
.supportedFamilies([.systemSmall])
.contentMarginsDisabled()
}
}


#Preview(as: .systemSmall) {
ConfigurableWidget()
} timeline: {
ConfigurableEntry(date: .now, configuration: ConfigurationAppIntent(), selectedTally: nil)
}
50 changes: 50 additions & 0 deletions SuperCounterWidget/ConfigurableWidget/ConfigurationAppIntent.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
//
// ConfigurationAppIntent.swift
// SuperCounter
//
// Created by Nonprawich I. on 26/12/2024.
//


import WidgetKit
import AppIntents
import SwiftData

struct ConfigurationAppIntent: WidgetConfigurationIntent {
static var title: LocalizedStringResource { "Selected Tally" }
static var description: IntentDescription { "Choose your tally from the list." }

// An example configurable parameter.
@Parameter(title: "Select Tally", default: nil)
var selectedTally: TallyEntity?
}

struct TallyEntity: AppEntity {
var id: String
static var typeDisplayRepresentation: TypeDisplayRepresentation = TypeDisplayRepresentation(name: "Selected Tally")
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(title: "\(id)")
}
static var defaultQuery = TallyQuery()
}

struct TallyQuery: EntityQuery {
func entities(for identifiers: [String]) async throws -> [TallyEntity] {
try await suggestedEntities().filter({identifiers.contains($0.id)})
}

@MainActor func suggestedEntities() async throws -> [TallyEntity] {
let container = try! ModelContainer(for: Tally.self)
let sort = [SortDescriptor(\Tally.name)]
let descriptor = FetchDescriptor<Tally>(sortBy: sort)
let allTallies = try? container.mainContext.fetch(descriptor)
let allEntities = allTallies?.map({ tally in
TallyEntity(id: tally.name)
})
return allEntities ?? []
}

func defaultResult() async -> TallyEntity? {
try? await suggestedEntities().first
}
}
1 change: 1 addition & 0 deletions SuperCounterWidget/SuperCounterWidgetBundle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ import SwiftUI
struct SuperCounterWidgetBundle: WidgetBundle {
var body: some Widget {
FirstTallyWidget()
ConfigurableWidget()
}
}

0 comments on commit 4a4092a

Please sign in to comment.