Skip to content
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?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.security.application-groups</key>
<array>
<string>group.com.exptech.dpip.dpip.widgets</string>
</array>
</dict>
</plist>
22 changes: 22 additions & 0 deletions ios/DPIPWidgetIntentsExtension/Info.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?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>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
<dict>
<key>IntentsRestrictedWhileLocked</key>
<array/>
<key>IntentsSupported</key>
<array>
<string>WeatherWidgetConfigurationIntent</string>
</array>
</dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.intents-service</string>
<key>NSExtensionPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).IntentHandler</string>
</dict>
</dict>
</plist>
39 changes: 39 additions & 0 deletions ios/DPIPWidgetIntentsExtension/IntentHandler.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import Intents

final class IntentHandler: INExtension,
WeatherWidgetConfigurationIntentHandling
{
override func handler(for intent: INIntent) -> Any {
return self
}

func provideLocationOptionsCollection(
for intent: WeatherWidgetConfigurationIntent,
with completion: @escaping (
INObjectCollection<WidgetLocation>?,
Error?
) -> Void
) {
let catalog = WidgetLocationCatalogStore().load()
let currentLocationDisplayString = String(
localized: "intent.current_location",
bundle: .main,
comment: "Current-location option in weather widget configuration."
)

let locations = makeWidgetLocationOptions(
from: catalog,
currentLocationDisplayString: currentLocationDisplayString
).map {
WidgetLocation(
identifier: $0.identifier,
display: $0.displayString
)
}

completion(
INObjectCollection(items: locations),
nil
)
}
}
23 changes: 23 additions & 0 deletions ios/DPIPWidgetIntentsExtension/Localizable.xcstrings
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"sourceLanguage" : "en",
"strings" : {
"intent.current_location" : {
"comment" : "Current-location option in weather widget configuration.",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Current Location"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "所在地"
}
}
}
}
},
"version" : "1.0"
}
54 changes: 54 additions & 0 deletions ios/DPIPWidgetIntentsExtension/WidgetLocationCatalog.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import Foundation

struct WidgetLocationCatalog: Decodable {
let schemaVersion: Int
let locations: [WidgetLocationCatalogLocation]
}

struct WidgetLocationCatalogLocation: Decodable {
let regionCode: String
let displayName: String
let administrativeAreaName: String
let latitude: Double
let longitude: Double
}

struct WidgetLocationCatalogStore {
private let appGroupIdentifier =
"group.com.exptech.dpip.dpip.widgets"

func load() -> WidgetLocationCatalog? {
guard let containerURL =
FileManager.default.containerURL(
forSecurityApplicationGroupIdentifier: appGroupIdentifier
)
else {
return nil
}

let url = containerURL
.appendingPathComponent("WidgetSnapshots")
.appendingPathComponent("location-catalog.json")

guard let data = try? Data(contentsOf: url) else {
return nil
}

return Self.decode(data)
}

static func decode(_ data: Data) -> WidgetLocationCatalog? {
guard let catalog = try? JSONDecoder().decode(
WidgetLocationCatalog.self,
from: data
) else {
return nil
}

guard catalog.schemaVersion == 1 else {
return nil
}

return catalog
}
}
35 changes: 35 additions & 0 deletions ios/DPIPWidgetIntentsExtension/WidgetLocationOptions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import Foundation

struct WidgetLocationOption: Equatable {
let identifier: String
let displayString: String
}

func makeWidgetLocationOptions(
from catalog: WidgetLocationCatalog?,
currentLocationDisplayString: String
) -> [WidgetLocationOption] {
var options = [
WidgetLocationOption(
identifier: "current-location",
displayString: currentLocationDisplayString
)
]

guard let catalog else {
return options
}

options.append(
contentsOf: catalog.locations.map { location in
WidgetLocationOption(
identifier: "region:\(location.regionCode)",
displayString:
"\(location.displayName) — "
+ location.administrativeAreaName
)
}
)

return options
}
23 changes: 22 additions & 1 deletion ios/DPIPWidgets/CurrentWeatherWidgetSnapshot.swift
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ enum CurrentWeatherWidgetCondition: String, Decodable {

struct CurrentWeatherWidgetSnapshot: Decodable {
let schemaVersion: Int
let sourceIdentifier: String?

let regionCode: String
let regionName: String
Expand All @@ -96,6 +97,7 @@ struct CurrentWeatherWidgetSnapshot: Decodable {

private enum CodingKeys: String, CodingKey {
case schemaVersion
case sourceIdentifier
case regionCode
case regionName
case observationTime
Expand All @@ -113,6 +115,7 @@ struct CurrentWeatherWidgetSnapshot: Decodable {

init(
schemaVersion: Int,
sourceIdentifier: String? = nil,
regionCode: String,
regionName: String,
observationTime: Int,
Expand All @@ -128,6 +131,7 @@ struct CurrentWeatherWidgetSnapshot: Decodable {
rain: Double?
) {
self.schemaVersion = schemaVersion
self.sourceIdentifier = sourceIdentifier
self.regionCode = regionCode
self.regionName = regionName
self.observationTime = observationTime
Expand All @@ -143,7 +147,6 @@ struct CurrentWeatherWidgetSnapshot: Decodable {
self.humidity = humidity
self.rain = rain
}

init(from decoder: Decoder) throws {
let container = try decoder.container(
keyedBy: CodingKeys.self
Expand All @@ -154,6 +157,24 @@ struct CurrentWeatherWidgetSnapshot: Decodable {
forKey: .schemaVersion
)

guard (1...5).contains(schemaVersion) else {
throw DecodingError.dataCorruptedError(
forKey: .schemaVersion,
in: container,
debugDescription:
"Unsupported current-weather snapshot schema version."
)
}

if schemaVersion >= 5 {
sourceIdentifier = try container.decode(
String.self,
forKey: .sourceIdentifier
)
} else {
sourceIdentifier = nil
}

regionCode = try container.decode(
String.self,
forKey: .regionCode
Expand Down
37 changes: 31 additions & 6 deletions ios/DPIPWidgets/DPIPWidgets.swift
Original file line number Diff line number Diff line change
@@ -1,10 +1,24 @@
import WidgetKit
import SwiftUI
import Intents

struct DPIPWidgetProvider: TimelineProvider {
struct DPIPWidgetProvider: IntentTimelineProvider {
typealias Intent = WeatherWidgetConfigurationIntent
private let staleAfter: TimeInterval = 30 * 60
private let snapshotStore = WidgetSnapshotStore()

private func snapshot(
for configuration: WeatherWidgetConfigurationIntent
) -> CurrentWeatherWidgetSnapshot? {
let target = WidgetLocationTarget(
identifier: configuration.location?.identifier
)

return snapshotStore.loadCurrentWeatherSnapshot(
for: target
)
}

func placeholder(in context: Context) -> DPIPWidgetEntry {
DPIPWidgetEntry(
date: .now,
Expand All @@ -15,11 +29,13 @@ struct DPIPWidgetProvider: TimelineProvider {
}

func getSnapshot(
for configuration: WeatherWidgetConfigurationIntent,
in context: Context,
completion: @escaping (DPIPWidgetEntry) -> Void
) {
let snapshot = snapshotStore.loadCurrentWeatherSnapshot()
let snapshot = snapshot(for: configuration)
let deviceNow = Date.now

let state = CurrentWeatherWidgetTimeline.state(
snapshot: snapshot,
at: deviceNow,
Expand All @@ -37,11 +53,13 @@ struct DPIPWidgetProvider: TimelineProvider {
}

func getTimeline(
for configuration: WeatherWidgetConfigurationIntent,
in context: Context,
completion: @escaping (Timeline<DPIPWidgetEntry>) -> Void
) {
let deviceNow = Date()
let snapshot = snapshotStore.loadCurrentWeatherSnapshot()
let snapshot = snapshot(for: configuration)

let entries = CurrentWeatherWidgetTimeline.states(
snapshot: snapshot,
deviceNow: deviceNow,
Expand Down Expand Up @@ -177,7 +195,11 @@ struct DPIPWidgets: Widget {
let kind: String = "DPIPWidgets"

var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: DPIPWidgetProvider()) { entry in
IntentConfiguration(
kind: kind,
intent: WeatherWidgetConfigurationIntent.self,
provider: DPIPWidgetProvider()
) { entry in
if #available(iOS 17.0, *) {
DPIPWidgetsEntryView(entry: entry)
.widgetURL(URL(string: "dpip:///home"))
Expand All @@ -199,7 +221,8 @@ struct DPIPWidgets_Previews: PreviewProvider {
private static let previewEntry = DPIPWidgetEntry(
date: .now,
snapshot: CurrentWeatherWidgetSnapshot(
schemaVersion: 4,
schemaVersion: 5,
sourceIdentifier: "current-location",
regionCode: "660",
regionName: "西屯區",
observationTime: 0,
Expand All @@ -213,7 +236,9 @@ struct DPIPWidgets_Previews: PreviewProvider {
temperature: 28.4,
humidity: 76,
rain: 0
),isStale: true, isNight: true
),
isStale: true,
isNight: true
)

static var previews: some View {
Expand Down
Loading
Loading