-
Notifications
You must be signed in to change notification settings - Fork 37
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
iOS 18 compatible OBA Widget Extension (#753)
Creates an iOS 18-compatible WidgetKit-based extension
- Loading branch information
Showing
22 changed files
with
1,279 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
// | ||
// RefreshButton.swift | ||
// OBAWidget | ||
// | ||
// Created by Manu on 2024-10-18. | ||
// | ||
|
||
import SwiftUI | ||
import AppIntents | ||
import WidgetKit | ||
|
||
// MARK: - RefreshWidgetIntent | ||
/// this intent serves as a way to refresh the widget and its timelines. | ||
struct RefreshWidgetIntent: AppIntent { | ||
static var title: LocalizedStringResource = "Refresh Widget" | ||
|
||
func perform() async throws -> some IntentResult { | ||
WidgetCenter.shared.reloadAllTimelines() | ||
return .result() | ||
} | ||
} | ||
|
||
|
||
/// A button to manually trigger the widget refresh. | ||
struct RefreshButton: View { | ||
var body: some View { | ||
|
||
Button(intent: RefreshWidgetIntent()) { | ||
HStack(spacing: 2){ | ||
Image(systemName: "arrow.trianglehead.clockwise") | ||
.imageScale(.small) | ||
.foregroundStyle(.white) | ||
|
||
Text("Refresh") | ||
.font(.system(size: 12)) | ||
.foregroundColor(.white) | ||
} | ||
.padding(.horizontal,6) | ||
.padding(.vertical, 4) | ||
.background(Color(.brand)) | ||
.cornerRadius(8) | ||
} | ||
.buttonStyle(.plain) | ||
|
||
} | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
// | ||
// BookmarkEntry.swift | ||
// OBAWidget | ||
// | ||
// Created by Manu on 2024-10-18. | ||
// | ||
|
||
import OBAKitCore | ||
import WidgetKit | ||
|
||
/// A struct representing a timeline entry for bookmarks in a widget. | ||
/// | ||
/// for displaying bookmarks in the widget context. | ||
struct BookmarkEntry: TimelineEntry { | ||
|
||
let date: Date | ||
|
||
/// bookmarks associated with this `BookmarkEntry`. | ||
let bookmarks: [Bookmark] | ||
|
||
/// Returns a formatted string representing the last updated time. | ||
public func lastUpdatedAt(with formatters: Formatters) -> String { | ||
bookmarks.isEmpty ? "--" : formatters.timeFormatter.string(from: date) | ||
} | ||
|
||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
// | ||
// AppIntents.swift | ||
// OBAWidget | ||
// | ||
// Created by Manu on 2024-10-12. | ||
// | ||
|
||
import WidgetKit | ||
import AppIntents | ||
import OBAKitCore | ||
|
||
struct ConfigurationAppIntent: AppIntent, WidgetConfigurationIntent { | ||
static var title: LocalizedStringResource = "Bookmarks" | ||
static var description: IntentDescription = IntentDescription("") | ||
|
||
|
||
|
||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
// | ||
// OBAWidgetBundle.swift | ||
// OBAWidget | ||
// | ||
// Created by Manu on 2024-10-12. | ||
// | ||
|
||
import Foundation | ||
import SwiftUI | ||
|
||
@main | ||
struct CBWidgetBundle: WidgetBundle { | ||
var body: some Widget { | ||
OBAWidget() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
// | ||
// OBAWidget-Bridging-Header.h | ||
// OBAWidget | ||
// | ||
// Created by Manu on 2024-10-12. | ||
// | ||
|
||
#ifndef OBAWidget_Bridging_Header_h | ||
#define OBAWidget_Bridging_Header_h | ||
|
||
|
||
#endif /* OBAWidget_Bridging_Header_h */ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
// | ||
// BookmarkProvider.swift | ||
// OBAWidget | ||
// | ||
// Created by Manu on 2024-10-14. | ||
// | ||
|
||
import Foundation | ||
import WidgetKit | ||
|
||
/// Timeline provider for generating widget updates based on bookmark data. | ||
struct BookmarkTimelineProvider: AppIntentTimelineProvider { | ||
|
||
let dataProvider: WidgetDataProvider | ||
|
||
init(dataProvider: WidgetDataProvider) { | ||
self.dataProvider = dataProvider | ||
} | ||
|
||
// MARK: Placeholder | ||
func placeholder(in context: Context) -> BookmarkEntry { | ||
BookmarkEntry(date: .now, bookmarks: []) | ||
} | ||
|
||
// MARK: Snapshot | ||
func snapshot(for configuration: ConfigurationAppIntent, in context: Context) async -> BookmarkEntry { | ||
|
||
await dataProvider.loadData() | ||
let data = dataProvider.getBookmarks() | ||
|
||
let entry = BookmarkEntry(date: .now, bookmarks: data) | ||
|
||
return entry | ||
} | ||
|
||
// MARK: Actual Timelines | ||
/// Generates timeline entries for the next 6 hours, starting from the current time. | ||
/// | ||
/// - **Current Time**: Let's say it's 12:00 PM. | ||
/// - **End Time**: 6 hours later, resulting in 6:00 PM. | ||
/// - **Entry Interval**: Creates entries every 30 minutes. | ||
/// - **Generated Entries**: | ||
/// - 12:00 PM | ||
/// - 12:30 PM | ||
/// - 1:00 PM | ||
/// - 1:30 PM | ||
/// - so on ...... | ||
func timeline(for configuration: ConfigurationAppIntent, in context: Context) async -> Timeline<BookmarkEntry> { | ||
await dataProvider.loadData() | ||
let data = dataProvider.getBookmarks() | ||
|
||
let currentDate = Date() | ||
let endDate = Calendar.current.date(byAdding: .hour, value: 6, to: currentDate)! | ||
|
||
// Generate entries for every 30 minutes within the defined time range. | ||
var entries: [BookmarkEntry] = [] | ||
var date = currentDate | ||
while date < endDate { | ||
let entry = BookmarkEntry(date: date, bookmarks: data) | ||
entries.append(entry) | ||
date = Calendar.current.date(byAdding: .minute, value: 30, to: date)! | ||
} | ||
|
||
|
||
return Timeline(entries: entries, policy: .atEnd) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
// WidgetDataProvider.swift | ||
// OBAWidget | ||
// | ||
// Created by Manu on 2024-10-15. | ||
// | ||
|
||
import Foundation | ||
import OBAKitCore | ||
import CoreLocation | ||
|
||
/// `WidgetDataProvider` is responsible for fetching and providing relevant data to the widget timeline provider. | ||
class WidgetDataProvider: NSObject, ObservableObject { | ||
|
||
public let formatters = Formatters( | ||
locale: Locale.autoupdatingCurrent, | ||
calendar: Calendar.autoupdatingCurrent, | ||
themeColors: ThemeColors.shared | ||
) | ||
|
||
static let shared = WidgetDataProvider() | ||
private let userDefaults = UserDefaults(suiteName: Bundle.main.appGroup!)! | ||
|
||
private lazy var locationManager = CLLocationManager() | ||
private lazy var locationService = LocationService( | ||
userDefaults: userDefaults, | ||
locationManager: locationManager | ||
) | ||
|
||
private lazy var app: CoreApplication = { | ||
let bundledRegions = Bundle.main.path(forResource: "regions", ofType: "json")! | ||
let config = CoreAppConfig(appBundle: Bundle.main, userDefaults: userDefaults, bundledRegionsFilePath: bundledRegions) | ||
return CoreApplication(config: config) | ||
}() | ||
|
||
private var bestAvailableBookmarks: [Bookmark] { | ||
var bookmarks = app.userDataStore.favoritedBookmarks | ||
if bookmarks.isEmpty { | ||
bookmarks = app.userDataStore.bookmarks | ||
} | ||
return bookmarks | ||
} | ||
|
||
/// Loads arrivals and departures for all favorited bookmarks for the widget. | ||
public func loadData() async { | ||
guard let apiService = app.apiService else { return } | ||
|
||
let bookmarks = getBookmarks() | ||
.filter { $0.isTripBookmark && $0.regionIdentifier == app.regionsService.currentRegion?.id } | ||
|
||
for bookmark in bookmarks { | ||
await fetchArrivalData(for: bookmark, apiService: apiService) | ||
} | ||
} | ||
|
||
/// Fetch arrival data for a specific bookmark and update the dictionary. | ||
private func fetchArrivalData(for bookmark: Bookmark, apiService: RESTAPIService) async { | ||
do { | ||
let stopArrivals = try await apiService.getArrivalsAndDeparturesForStop( | ||
id: bookmark.stopID, | ||
minutesBefore: 0, | ||
minutesAfter: 60 | ||
).entry | ||
|
||
await MainActor.run { | ||
let keysAndDeps = stopArrivals.arrivalsAndDepartures.tripKeyGroupedElements | ||
for (key, deps) in keysAndDeps { | ||
self.arrDepDic[key] = deps | ||
} | ||
} | ||
} catch { | ||
Logger | ||
.error( | ||
"Error fetching data for bookmark \(bookmark.name) with bookmark id: \(bookmark.id): \(error)" | ||
) | ||
} | ||
} | ||
|
||
/// Looks up arrival and departure data for a given trip key. | ||
public func lookupArrivalDeparture(with key: TripBookmarkKey) -> [ArrivalDeparture] { | ||
return arrDepDic[key, default: []] | ||
} | ||
|
||
/// Retrieves the best available bookmarks. | ||
public func getBookmarks() -> [Bookmark] { | ||
return bestAvailableBookmarks | ||
} | ||
|
||
/// Dictionary to store arrival and departure data grouped by trip keys. | ||
private var arrDepDic = [TripBookmarkKey: [ArrivalDeparture]]() | ||
} |
Oops, something went wrong.