Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

50 medium widget #334

Open
wants to merge 21 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions Basic-Car-Maintenance-Widget/AppIntent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,9 @@ import WidgetKit
import AppIntents

struct ConfigurationAppIntent: WidgetConfigurationIntent {
static var title: LocalizedStringResource = "Configuration"
static var description = IntentDescription("This is an example widget.")
static var title: LocalizedStringResource = "Current Vehicle"
static var description = IntentDescription("See the current vehicle's maintenance events")

// An example configurable parameter.
@Parameter(title: "Favorite Emoji", default: "😃")
var favoriteEmoji: String
@Parameter(title: "Vehichle")
var selectedVehicle: VehicleAppEntity?
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.applesignin</key>
<array>
<string></string>
</array>
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)com.mikaelacaron.Shared</string>
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what does this entitlement do? also should this be .Shared? as opposed to matching the bundle ID with the app name

and should there be a . between $(AppIdentifierPrefix) and com.mikaelacaron.Shared

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The . is included in $(AppIdentifierPrefix).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can change the Shared portion. It was a standard that I saw other devs using online.

</array>
</dict>
</plist>
100 changes: 53 additions & 47 deletions Basic-Car-Maintenance-Widget/BasicCarMaintenanceWidget.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,55 +6,76 @@
// See LICENSE for license information.
//

import Firebase
import FirebaseAuth
import WidgetKit
import SwiftUI

struct Provider: AppIntentTimelineProvider {
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(date: Date(), configuration: ConfigurationAppIntent())
func placeholder(in context: Context) -> MaintenanceEntry {
MaintenanceEntry(
date: Date(),
configuration: .demo,
maintenanceEvents: .demo
)
}

func snapshot(for configuration: ConfigurationAppIntent, in context: Context) async -> SimpleEntry {
SimpleEntry(date: Date(), configuration: configuration)

func snapshot(for configuration: ConfigurationAppIntent, in context: Context) async -> MaintenanceEntry {
MaintenanceEntry(
date: Date(),
configuration: .demo,
maintenanceEvents: .demo
)
}

func timeline(for configuration: ConfigurationAppIntent,
in context: Context) async -> Timeline<SimpleEntry> {
var entries: [SimpleEntry] = []

// Generate a timeline consisting of five entries an hour apart, starting from the current date.
func timeline(for configuration: ConfigurationAppIntent, in context: Context) async -> Timeline<MaintenanceEntry> {

Check warning on line 31 in Basic-Car-Maintenance-Widget/BasicCarMaintenanceWidget.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Line should be 110 characters or less; currently it has 119 characters (line_length)
let currentDate = Date()
for hourOffset in 0 ..< 5 {
let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
let entry = SimpleEntry(date: entryDate, configuration: configuration)
entries.append(entry)
let nextUpdate = Calendar.current.date(byAdding: .hour, value: 1, to: currentDate)!

let result = await DataService.fetchMaintenanceEvents(for: configuration.selectedVehicle?.id)
let entry = switch result {
case .success(let events):
MaintenanceEntry(date: currentDate, configuration: configuration, maintenanceEvents: events)
case .failure(let error):
// Returns an empty list of options
MaintenanceEntry(date: currentDate, configuration: configuration, maintenanceEvents: [], error: error.localizedDescription)

Check warning on line 41 in Basic-Car-Maintenance-Widget/BasicCarMaintenanceWidget.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Line should be 110 characters or less; currently it has 135 characters (line_length)
}

return Timeline(entries: entries, policy: .atEnd)
return Timeline(entries: [entry], policy: .after(nextUpdate))
}
}

struct SimpleEntry: TimelineEntry {
struct MaintenanceEntry: TimelineEntry {
let date: Date
let configuration: ConfigurationAppIntent
}

struct BasicCarMaintenanceWidgetEntryView: View {
var entry: Provider.Entry

var body: some View {
VStack {
Text("Time:")
Text(entry.date, style: .time)

Text("Favorite Emoji:")
Text(entry.configuration.favoriteEmoji)
}
let maintenanceEvents: [MaintenanceEvent]
let error: String?

init(date: Date, configuration: ConfigurationAppIntent, maintenanceEvents: [MaintenanceEvent], error: String? = nil) {

Check warning on line 54 in Basic-Car-Maintenance-Widget/BasicCarMaintenanceWidget.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Line should be 110 characters or less; currently it has 122 characters (line_length)
self.date = date
self.configuration = configuration
self.maintenanceEvents = maintenanceEvents
self.error = error
}
}

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

init() {
// Since this widget access Firebase, the same configuration as the main application is needed.
FirebaseApp.configure()

try? Auth.auth().useUserAccessGroup(Bundle.main.keychainAccessGroup)
let useEmulator = true // Process arguments aren't respected here from the main app
if useEmulator {
let settings = Firestore.firestore().settings
settings.host = "localhost:8080"
settings.cacheSettings = MemoryCacheSettings()
settings.isSSLEnabled = false
Firestore.firestore().settings = settings
}
}

var body: some WidgetConfiguration {
AppIntentConfiguration(kind: kind,
Expand All @@ -66,23 +87,8 @@
}
}

extension ConfigurationAppIntent {
fileprivate static var smiley: ConfigurationAppIntent {
let intent = ConfigurationAppIntent()
intent.favoriteEmoji = "😀"
return intent
}

fileprivate static var starEyes: ConfigurationAppIntent {
let intent = ConfigurationAppIntent()
intent.favoriteEmoji = "🤩"
return intent
}
}

#Preview(as: .systemSmall) {
#Preview(as: .systemMedium) {
BasicCarMaintenanceWidget()
} timeline: {
SimpleEntry(date: .now, configuration: .smiley)
SimpleEntry(date: .now, configuration: .starEyes)
MaintenanceEntry(date: .now, configuration: .demo, maintenanceEvents: .demo)
}
26 changes: 26 additions & 0 deletions Basic-Car-Maintenance-Widget/Extensions/Array+Demo.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//
// Array+Demo.swift
// Basic-Car-Maintenance
//
// https://github.com/mikaelacaron/Basic-Car-Maintenance
// See LICENSE for license information.
//

extension Array where Element == MaintenanceEvent {
static var demo: [MaintenanceEvent] {
[
MaintenanceEvent(
vehicleID: "",
title: "Tire Rotation",
date: .now,
notes: "New shiny tires were installed."
),
MaintenanceEvent(
vehicleID: "",
title: "Oil Change",
date: .now.advanced(by: -36000),
notes: "The cabin air filter was also replaced."
)
]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//
// ConfiguartionAppIntent+Demo.swift
// Basic-Car-Maintenance
//
// https://github.com/mikaelacaron/Basic-Car-Maintenance
// See LICENSE for license information.
//

extension ConfigurationAppIntent {
static var demo: ConfigurationAppIntent {
let intent = ConfigurationAppIntent()
intent.selectedVehicle = VehicleAppEntity(
id: "",
displayString: "Hot Wheels",
data: .init(name: "Kia Soul", make: "Kia", model: "Soul", year: "2015")
)
return intent
}
}
2 changes: 2 additions & 0 deletions Basic-Car-Maintenance-Widget/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>KeychainAccessGroup</key>
<string>$(AppIdentifierPrefix)com.mikaelacaron.Shared</string>
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question herea bout the format (as the widget extension question

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The . is included in $(AppIdentifierPrefix).

<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
Expand Down
101 changes: 101 additions & 0 deletions Basic-Car-Maintenance-Widget/Service/DataService.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
//
// DataService.swift
// Basic-Car-Maintenance
//
// https://github.com/mikaelacaron/Basic-Car-Maintenance
// See LICENSE for license information.
//

import Firebase
import FirebaseAuth

enum DataService {
/// Fetches maintenance events for the selected vehicle from Firestore.
/// - Parameter vehichleID: The ID of the selected vehicle.
/// - Returns: A list of maintenance events or an error if the fetch fails.
///
/// Example usage:
/// ```swift
/// Task {
/// let result = await DataService.fetchMaintenanceEvents(for: "vehicle123")
///
/// switch result {
/// case .success(let events):
/// print("Fetched \(events.count) maintenance events.")
/// case .failure(let error):
/// print("Failed to fetch maintenance events with error: \(error.localizedDescription)")
/// }
/// }
/// ```
static func fetchMaintenanceEvents(for vehichleID: String?) async -> Result<[MaintenanceEvent], Error> {
guard let vehichleID else {
return .failure(FetchError.noVehicleSelected)
}

do {
let docRef = Firestore
.firestore()
.collection("\(FirestoreCollection.vehicles)/\(vehichleID)/\(FirestoreCollection.maintenanceEvents)")

Check warning on line 38 in Basic-Car-Maintenance-Widget/Service/DataService.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Line should be 110 characters or less; currently it has 129 characters (line_length)
let snapshot = try await docRef.getDocuments()
let events = snapshot.documents.compactMap {
try? $0.data(as: MaintenanceEvent.self)
}.sorted { $0.date > $1.date }

return .success(events)
} catch {
return .failure(error)
}
}

/// Fetches vehicles for the current user from Firestore.
/// - Returns: A list of vehicles or an error if the fetch fails.
///
/// Example usage:
/// ```swift
/// Task {
/// let result = await DataService.fetchVehicles()
///
/// switch result {
/// case .success(let vehicles):
/// print("Fetched \(vehicles.count) vehicles.")
/// case .failure(let error):
/// print("Failed to fetch vehicles with error: \(error.localizedDescription)")
/// }
/// }
/// ```
static func fetchVehicles() async -> Result<[Vehicle], Error> {
guard let userID = Auth.auth().currentUser?.uid else {
return .failure(FetchError.unauthenticated)
}

let docRef = Firestore
.firestore()
.collection(FirestoreCollection.vehicles)
.whereField(FirestoreField.userID, isEqualTo: userID)

do {
let snapshot = try await docRef.getDocuments()
let vehicles = snapshot.documents.compactMap {
try? $0.data(as: Vehicle.self)
}
return .success(vehicles)
} catch {
return .failure(error)
}
}
}

/// Errors that can occur when fetching maintenance events.
enum FetchError: LocalizedError {
case unauthenticated
case noVehicleSelected

var errorDescription: String {
switch self {
case .unauthenticated:
"You are not logged in. Please log in to continue."
case .noVehicleSelected:
"No vehicle selected. Please select a vehicle to continue."
}
}
}
50 changes: 50 additions & 0 deletions Basic-Car-Maintenance-Widget/VehicleAppEntity.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
//
// VehicleAppEntity.swift
// Basic-Car-Maintenance
//
// https://github.com/mikaelacaron/Basic-Car-Maintenance
// See LICENSE for license information.
//

import Foundation
import AppIntents

struct VehicleAppEntity: AppEntity {
var id: String
var displayString: String
var data: Vehicle

var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(title: "\(displayString)")
}

static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Maintenance Vehicle")
static var defaultQuery = VehicleAppEntityQuery()

init(id: String, displayString: String, data: Vehicle) {
self.id = id
self.displayString = displayString
self.data = data
}
}

struct VehicleAppEntityQuery: EntityQuery {
func entities(
for identifiers: [VehicleAppEntity.ID]
) async throws -> [VehicleAppEntity] {
let result = await DataService.fetchVehicles()

let vehicles = try result.get()
return vehicles.map {
VehicleAppEntity(
id: $0.id ?? UUID().uuidString,
displayString: $0.name,
data: $0
)
}
}

func suggestedEntities() async throws -> [VehicleAppEntity] {
return try await entities(for: [])
}
}
Loading
Loading