Skip to content

Commit 817ab35

Browse files
authored
Add support for iOS Live Activities and Home Screen widgets (#18)
1 parent 94a638f commit 817ab35

36 files changed

Lines changed: 1530 additions & 197 deletions
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"colors" : [
3+
{
4+
"color" : {
5+
"color-space" : "srgb",
6+
"components" : {
7+
"alpha" : "1.000",
8+
"blue" : "0.961",
9+
"green" : "0.424",
10+
"red" : "0.345"
11+
}
12+
},
13+
"idiom" : "universal"
14+
}
15+
],
16+
"info" : {
17+
"author" : "xcode",
18+
"version" : 1
19+
}
20+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"images" : [
3+
{
4+
"filename" : "cuckoo_small_banner.svg",
5+
"idiom" : "universal",
6+
"scale" : "1x"
7+
},
8+
{
9+
"idiom" : "universal",
10+
"scale" : "2x"
11+
},
12+
{
13+
"idiom" : "universal",
14+
"scale" : "3x"
15+
}
16+
],
17+
"info" : {
18+
"author" : "xcode",
19+
"version" : 1
20+
}
21+
}
Lines changed: 13 additions & 0 deletions
Loading
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"images" : [
3+
{
4+
"idiom" : "universal",
5+
"platform" : "ios",
6+
"size" : "1024x1024"
7+
}
8+
],
9+
"info" : {
10+
"author" : "xcode",
11+
"version" : 1
12+
}
13+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"info" : {
3+
"author" : "xcode",
4+
"version" : 1
5+
}
6+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"images" : [
3+
{
4+
"filename" : "cuckoo 2.svg",
5+
"idiom" : "universal",
6+
"scale" : "1x"
7+
},
8+
{
9+
"idiom" : "universal",
10+
"scale" : "2x"
11+
},
12+
{
13+
"idiom" : "universal",
14+
"scale" : "3x"
15+
}
16+
],
17+
"info" : {
18+
"author" : "xcode",
19+
"version" : 1
20+
}
21+
}
Lines changed: 12 additions & 0 deletions
Loading
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"colors" : [
3+
{
4+
"idiom" : "universal"
5+
}
6+
],
7+
"info" : {
8+
"author" : "xcode",
9+
"version" : 1
10+
}
11+
}

ios/EventWidget/CommonUtils.swift

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
//
2+
// CommonUtils.swift
3+
// Runner
4+
//
5+
// Created by Ruijie Li on 24/7/2024.
6+
//
7+
8+
import Foundation
9+
import SwiftUI
10+
11+
@available(iOS 13.0, *)
12+
func hexStringToColor(hex: String) -> Color {
13+
var cString: String = hex.trimmingCharacters(in: .whitespacesAndNewlines).uppercased()
14+
15+
if (cString.hasPrefix("#")) {
16+
cString.remove(at: cString.startIndex)
17+
}
18+
19+
if ((cString.count) != 6) {
20+
return Color.gray
21+
}
22+
23+
var rgbValue: UInt64 = 0
24+
Scanner(string: cString).scanHexInt64(&rgbValue)
25+
26+
return Color(
27+
red: CGFloat((rgbValue & 0xFF0000) >> 16) / 255.0,
28+
green: CGFloat((rgbValue & 0x00FF00) >> 8) / 255.0,
29+
blue: CGFloat(rgbValue & 0x0000FF) / 255.0
30+
)
31+
}

ios/EventWidget/EventWidget.swift

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
//
2+
// EventWidget.swift
3+
// EventWidget
4+
//
5+
// Created by Ruijie Li on 23/7/2024.
6+
//
7+
8+
import WidgetKit
9+
import SwiftUI
10+
11+
private let widgetGroupId = "group.jerry.li.Cuckoo"
12+
13+
struct Provider: TimelineProvider {
14+
func placeholder(in context: Context) -> DateEntry {
15+
DateEntry(date: Date())
16+
}
17+
18+
func getSnapshot(in context: Context, completion: @escaping (DateEntry) -> ()) {
19+
let entry = DateEntry(date: Date())
20+
completion(entry)
21+
}
22+
23+
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
24+
let date = Date()
25+
let entry = DateEntry(date: date)
26+
27+
let nextUpdateDate = Calendar.current.date(byAdding: .minute, value: 30, to: date)!
28+
29+
let timeline = Timeline(
30+
entries:[entry],
31+
policy: .after(nextUpdateDate)
32+
)
33+
34+
completion(timeline)
35+
}
36+
}
37+
38+
struct DateEntry: TimelineEntry {
39+
let date: Date
40+
}
41+
42+
struct EventWidgetEntryView : View {
43+
var entry: Provider.Entry
44+
let data = UserDefaults.init(suiteName: widgetGroupId)
45+
@Environment(\.widgetFamily) var family
46+
47+
var hasEvent = false
48+
var remainingDay: Int?
49+
var remainingHour: Int?
50+
var dueDateStr: String?
51+
var courseCode: String?
52+
var eventTitle: String?
53+
var color: Color?
54+
55+
init(entry: Provider.Entry) {
56+
self.entry = entry
57+
58+
// Check data availability first
59+
hasEvent = data?.bool(forKey: "hasEvent") ?? false
60+
if (hasEvent) {
61+
courseCode = data!.string(forKey: "courseCode")
62+
eventTitle = data!.string(forKey: "eventTitle")
63+
64+
color = hexStringToColor(hex: data!.string(forKey: "courseColorHex")!)
65+
if (courseCode?.count == 0) {
66+
color = .gray
67+
}
68+
69+
let dueDate = Date(timeIntervalSince1970: data!.double(forKey: "eventDueDate"))
70+
let dueFormatter = DateFormatter()
71+
dueFormatter.dateFormat = "HH:mm MMM d"
72+
dueDateStr = dueFormatter.string(from: dueDate)
73+
74+
let remainingSecs = max(Int(dueDate.timeIntervalSince1970 - entry.date.timeIntervalSince1970), 0)
75+
remainingDay = remainingSecs / 86400
76+
remainingHour = (remainingSecs - remainingDay! * 86400) / 3600
77+
}
78+
}
79+
80+
var body: some View {
81+
if (hasEvent) {
82+
if (family == .systemSmall) {
83+
HStack {
84+
VStack (alignment: .leading) {
85+
Text("\(Text(String(remainingDay!)).font(.custom("Montserrat-Bold", size: 31.0)))d \(Text(String(remainingHour!)).font(.custom("Montserrat-Bold", size: 31.0)))hr")
86+
.font(.custom("Montserrat-Medium", size: 15.0))
87+
Spacer()
88+
(Text(courseCode!)
89+
.bold()
90+
.foregroundColor(color!)
91+
.font(.caption) +
92+
Text("\n") +
93+
Text(eventTitle!))
94+
.font(.subheadline)
95+
.lineLimit(3)
96+
Spacer()
97+
.frame(height: 5)
98+
Text(dueDateStr!)
99+
.bold()
100+
.font(.caption2)
101+
.foregroundColor(color!)
102+
}
103+
Spacer()
104+
}
105+
} else if (family == .systemMedium) {
106+
HStack {
107+
VStack(alignment: .leading) {
108+
Image("AppBanner")
109+
Spacer()
110+
(Text(courseCode!)
111+
.bold()
112+
.foregroundColor(color!)
113+
.font(.caption) +
114+
Text("\n") +
115+
Text(eventTitle!))
116+
.font(.subheadline)
117+
.lineLimit(4)
118+
Spacer()
119+
.frame(height: 5)
120+
Text(dueDateStr!)
121+
.bold()
122+
.font(.caption2)
123+
.foregroundColor(color!)
124+
}
125+
Spacer(minLength: 30.0)
126+
VStack(alignment: .trailing) {
127+
Spacer()
128+
Text("\(Text(String(remainingDay!)).font(.custom("Montserrat-Bold", size: 42.0)))d")
129+
.font(.custom("Montserrat-Medium", size: 20.0))
130+
.frame(height: -3.0)
131+
Text("\(Text(String(remainingHour!)).font(.custom("Montserrat-Bold", size: 42.0)))h")
132+
.font(.custom("Montserrat-Medium", size: 20.0))
133+
Text("Time\nRemaining")
134+
.foregroundColor(Color(.secondaryLabel))
135+
.font(.caption2)
136+
.multilineTextAlignment(.trailing)
137+
}
138+
}
139+
} else if (family == .accessoryInline) {
140+
Text("\(remainingDay! > 0 ? "\(remainingDay!)d " : "")\(remainingHour!)h Left")
141+
.padding(.leading, 2.0)
142+
} else if (family == .accessoryRectangular) {
143+
HStack(spacing: 8.0) {
144+
RoundedRectangle(cornerRadius: 3.0)
145+
.fill()
146+
.frame(width: 6.0, height: .infinity)
147+
.padding(.vertical, 2.0)
148+
VStack(alignment: .leading) {
149+
(Text(courseCode!)
150+
.bold()
151+
.font(.caption) +
152+
Text(courseCode!.count > 0 ? "\n" : "") +
153+
Text(eventTitle!)
154+
.fontWeight(.medium))
155+
.font(.subheadline)
156+
.lineLimit(2)
157+
Spacer()
158+
.frame(height: 5)
159+
Text(dueDateStr!)
160+
.bold()
161+
.font(.caption2)
162+
}
163+
Spacer()
164+
}
165+
}
166+
} else {
167+
Text("No Events")
168+
.foregroundColor(Color(.secondaryLabel))
169+
}
170+
}
171+
}
172+
173+
struct CuckooUpcomingEventWidget: Widget {
174+
let kind: String = "CuckooUpcomingEventWidget"
175+
176+
var body: some WidgetConfiguration {
177+
StaticConfiguration(kind: kind, provider: Provider()) { entry in
178+
if #available(iOS 17.0, *) {
179+
EventWidgetEntryView(entry: entry)
180+
.containerBackground(.fill.tertiary, for: .widget)
181+
} else {
182+
EventWidgetEntryView(entry: entry)
183+
.padding()
184+
.background()
185+
}
186+
}
187+
.supportedFamilies([.systemSmall, .systemMedium, .accessoryInline, .accessoryRectangular])
188+
.configurationDisplayName("Upcoming Event")
189+
.description("Display the next upcoming event in the events list.")
190+
}
191+
}

0 commit comments

Comments
 (0)