// // FeelsWidget.swift // FeelsWidget // // Created by Trey Tartt on 1/7/22. // import WidgetKit import SwiftUI import Intents import CoreData class WatchTimelineView: Identifiable { let id = UUID() let image: Image let graphic: Image let date: Date let color: Color init(image: Image, date: Date, color: Color, graphic: Image) { self.image = image self.date = date self.color = color self.graphic = graphic } } struct TimeLineCreator { public func getLastTen() -> [MoodEntry] { let dateAtEnd = Calendar.current.date(bySettingHour: 23, minute: 59, second: 59, of: Date())! var tenDaysAgo = Calendar.current.date(byAdding: .day, value: -10, to: dateAtEnd)! tenDaysAgo = Calendar.current.date(bySettingHour: 0, minute: 0, second: 0, of: tenDaysAgo)! let moodEntry = PersistenceController.shared.getData(startDate: tenDaysAgo, endDate: dateAtEnd, includedDays: [1,2,3,4,5,6,7]) return moodEntry } func createTimeLineViewsfrom(entries: [MoodEntry]) -> [WatchTimelineView] { var returnViews = [WatchTimelineView]() for pastDays in 0...10 { let pastDate = Calendar.current.date(byAdding: .day, value: -pastDays, to: Date())! if let item = entries.filter({ entry in let components = Calendar.current.dateComponents([.day, .month, .year], from: pastDate) let day = components.day let month = components.month let year = components.year let entryComponents = Calendar.current.dateComponents([.day, .month, .year], from: entry.forDate!) let entryDay = entryComponents.day let entryMonth = entryComponents.month let entryYear = entryComponents.year return day == entryDay && month == entryMonth && year == entryYear }).first { let timeLineView = WatchTimelineView(image: item.mood.icon, date: pastDate, color: item.mood == Mood.missing ? Color(UIColor.label) : item.mood.color, graphic: item.mood.graphic) returnViews.append(timeLineView) } else { let timeLineView = WatchTimelineView(image: Mood.missing.icon, date: pastDate, color: Color(UIColor.label), graphic: Mood.missing.graphic) returnViews.append(timeLineView) } } returnViews = returnViews.sorted(by: { $0.date > $1.date }) return returnViews } } struct Provider: IntentTimelineProvider { let timeLineCreator = TimeLineCreator() /* placeholder for widget, no data gets redacted auto */ func placeholder(in context: Context) -> SimpleEntry { var sampleViews = [WatchTimelineView]() for pastDay in 0...10 { let pastDate = Calendar.current.date(byAdding: .day, value: -pastDay, to: Date())! let mood = Mood.allValues.randomElement()! sampleViews.append(WatchTimelineView(image: mood.icon, date: pastDate, color: mood.color, graphic: mood.graphic)) } return SimpleEntry(date: Date(), configuration: ConfigurationIntent(), timeLineViews: sampleViews) } func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (SimpleEntry) -> ()) { var timeLineViews = [WatchTimelineView]() let lastTenEntries = timeLineCreator.getLastTen() if context.isPreview { for pastDay in 0...10 { let pastDate = Calendar.current.date(byAdding: .day, value: -pastDay, to: Date())! let mood = Mood.allValues.randomElement()! timeLineViews.append( WatchTimelineView(image: mood.icon, date: pastDate, color: mood.color, graphic: mood.graphic)) } } else { timeLineViews = timeLineCreator.createTimeLineViewsfrom(entries: lastTenEntries) } let entry = SimpleEntry(date: Date(), configuration: configuration, timeLineViews: timeLineViews) completion(entry) } func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline) -> ()) { let lastTenEntries = timeLineCreator.getLastTen() let views = timeLineCreator.createTimeLineViewsfrom(entries: lastTenEntries) let entry = SimpleEntry(date: Date(), configuration: configuration, timeLineViews: views) // TODO: make this update time make more sense let timeline = Timeline(entries: [entry], policy: .after(Random.tomorrowMidnightThirty)) completion(timeline) } } struct SimpleEntry: TimelineEntry { let date: Date let configuration: ConfigurationIntent let timeLineViews: [WatchTimelineView] let showStats: Bool init(date: Date, configuration: ConfigurationIntent, timeLineViews: [WatchTimelineView], showStats: Bool = false) { self.date = date self.configuration = configuration self.timeLineViews = timeLineViews self.showStats = showStats } } /**********************************************************/ struct FeelsWidgetEntryView : View { @Environment(\.sizeCategory) var sizeCategory @Environment(\.widgetFamily) var family var entry: Provider.Entry @ViewBuilder var body: some View { ZStack { Color(UIColor.systemBackground) switch family { case .systemSmall: SmallWidgetView(entry: entry) case .systemMedium: MediumWidgetView(entry: entry) case .systemLarge: MediumWidgetView(entry: entry) case .systemExtraLarge: MediumWidgetView(entry: entry) @unknown default: fatalError() } }.onReceive(NotificationCenter.default.publisher(for: .NSPersistentStoreRemoteChange)) { _ in // make sure you don't call this too often WidgetCenter.shared.reloadAllTimelines() } } } struct SmallWidgetView: View { var entry: Provider.Entry var body: some View { ZStack { Color(UIColor.secondarySystemBackground) HStack { ForEach([entry.timeLineViews.first!]) { watchView in EntryCard(timeLineView: watchView) } } .padding() } .clipShape(RoundedRectangle(cornerRadius: 25, style: .continuous)) .frame(minHeight: 0, maxHeight: 55) .padding() } } struct MediumWidgetView: View { var entry: Provider.Entry var firstGroup: [WatchTimelineView] { Array(self.entry.timeLineViews.prefix(5)) } var body: some View { VStack { Spacer() TimeHeaderView(startDate: firstGroup.first!.date, endDate: firstGroup.last!.date) .frame(minWidth: 0, maxWidth: .infinity) .multilineTextAlignment(.leading) TimeBodyView(group: firstGroup) .clipShape(RoundedRectangle(cornerRadius: 25, style: .continuous)) .frame(minHeight: 0, maxHeight: 55) .padding() Spacer() } } } /**********************************************************/ struct FeelsGraphicWidgetEntryView : View { @Environment(\.sizeCategory) var sizeCategory @Environment(\.widgetFamily) var family var entry: Provider.Entry @ViewBuilder var body: some View { SmallGraphicWidgetView(entry: entry) .onReceive(NotificationCenter.default.publisher(for: .NSPersistentStoreRemoteChange)) { _ in // make sure you don't call this too often WidgetCenter.shared.reloadAllTimelines() } } } struct SmallGraphicWidgetView: View { var entry: Provider.Entry var body: some View { GeometryReader { geo in entry.timeLineViews.first!.graphic .resizable() .scaledToFit() } } } /**********************************************************/ struct FeelsIconWidgetEntryView : View { @Environment(\.sizeCategory) var sizeCategory @Environment(\.widgetFamily) var family var entry: Provider.Entry @ViewBuilder var body: some View { SmallIconView(entry: entry) } } struct SmallIconView: View { var entry: Provider.Entry var body: some View { GeometryReader { geo in Mood.missing.graphic .resizable() .scaledToFit() } } } /**********************************************************/ struct TimeHeaderView: View { let startDate: Date let endDate: Date var formatter: DateFormatter { let dateFormatter = DateFormatter() dateFormatter.dateStyle = .medium return dateFormatter } var body: some View { HStack { Text(startDate, formatter: formatter) .font(.system(.footnote)) Text(" - ") .font(.system(.footnote)) Text(endDate, formatter: formatter) .font(.system(.footnote)) } } } struct TimeBodyView: View { let group: [WatchTimelineView] var body: some View { ZStack { Color(UIColor.secondarySystemBackground) HStack { ForEach(group) { watchView in EntryCard(timeLineView: watchView) } } .padding() } } } //struct LargeWidgetView: View { // var entry: Provider.Entry // // var formatter: DateFormatter { // let dateFormatter = DateFormatter() // dateFormatter.dateStyle = .medium // return dateFormatter // } // // // var body: some View { // VStack { // Spacer() // // ForEach([Array(self.entry.timeLineViews.prefix(5)), Array(self.entry.timeLineViews.suffix(5))]) { group in // // TimeHeaderView(startDate: group.first!, endDate: group.last!) // .frame(minWidth: 0, maxWidth: .infinity) // .multilineTextAlignment(.leading) // // TimeBodyView(group: group) // .clipShape(RoundedRectangle(cornerRadius: 25, style: .continuous)) // .frame(minHeight: 0, maxHeight: 55) // .padding() // // Spacer() // } // } // } //} struct EntryCard: View { var timeLineView: WatchTimelineView var body: some View { timeLineView.image .resizable() .aspectRatio(contentMode: .fit) .frame(width: 50, height: 50, alignment: .center) .foregroundColor(timeLineView.color) } } @main struct FeelsBundle: WidgetBundle { var body: some Widget { FeelsWidget() FeelsGraphicWidget() FeelsIconWidget() } } struct FeelsWidget: Widget { let kind: String = "FeelsWidget" var body: some WidgetConfiguration { IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in FeelsWidgetEntryView(entry: entry) } .configurationDisplayName("Feels") .description("") .supportedFamilies([.systemSmall, .systemMedium]) } } struct FeelsIconWidget: Widget { let kind: String = "FeelsIconWidget" var body: some WidgetConfiguration { IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in FeelsIconWidgetEntryView(entry: entry) } .configurationDisplayName("Feels Icon") .description("") .supportedFamilies([.systemSmall]) } } struct FeelsGraphicWidget: Widget { let kind: String = "FeelsGraphicWidget" var body: some WidgetConfiguration { IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in FeelsGraphicWidgetEntryView(entry: entry) } .configurationDisplayName("Mood Graphic") .description("") .supportedFamilies([.systemSmall]) } } struct FeelsWidget_Previews: PreviewProvider { static var data: [WatchTimelineView] { var data = PersistenceController.shared.randomEntries(count: 10) data.remove(at: 2) let views = TimeLineCreator().createTimeLineViewsfrom(entries: data) return views } static var previews: some View { Group { FeelsWidgetEntryView(entry: SimpleEntry(date: Date(), configuration: ConfigurationIntent(), timeLineViews: FeelsWidget_Previews.data)) .previewContext(WidgetPreviewContext(family: .systemSmall)) .environment(\.sizeCategory, .small) FeelsWidgetEntryView(entry: SimpleEntry(date: Date(), configuration: ConfigurationIntent(), timeLineViews: FeelsWidget_Previews.data)) .previewContext(WidgetPreviewContext(family: .systemMedium)) .environment(\.sizeCategory, .medium) // // FeelsWidgetEntryView(entry: SimpleEntry(date: Date(), // configuration: ConfigurationIntent(), // timeLineViews: FeelsWidget_Previews.data)) // .previewContext(WidgetPreviewContext(family: .systemLarge)) // .environment(\.sizeCategory, .large) } } }