// // 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 let secondaryColor: Color init(image: Image, graphic: Image, date: Date, color: Color, secondaryColor: Color) { self.image = image self.date = date self.color = color self.graphic = graphic self.secondaryColor = secondaryColor } } struct TimeLineCreator { static func createViews(daysBack: Int) -> [WatchTimelineView] { var timeLineView = [WatchTimelineView]() let latestDayToShow = ShowBasedOnVoteLogics.getCurrentVotingDate(onboardingData: UserDefaultsStore.getOnboarding()) let dates = Array(0...daysBack).map({ Calendar.current.date(byAdding: .day, value: -$0, to: latestDayToShow)! }) for date in dates { let dayStart = Calendar.current.startOfDay(for: date) let dayEnd = Calendar.current.date(bySettingHour: 23, minute: 59, second: 59, of: dayStart)! let moodTint: MoodTintable.Type = UserDefaultsStore.moodTintable() let moodImages: MoodImagable.Type = UserDefaultsStore.moodMoodImagable() if let todayEntry = PersistenceController.shared.getData(startDate: dayStart, endDate: dayEnd, includedDays: []).first { timeLineView.append(WatchTimelineView(image: moodImages.icon(forMood: todayEntry.mood), graphic: moodImages.icon(forMood: todayEntry.mood), date: dayStart, color: moodTint.color(forMood: todayEntry.mood), secondaryColor: moodTint.secondary(forMood: todayEntry.mood))) } else { timeLineView.append(WatchTimelineView(image: moodImages.icon(forMood: .missing), graphic: moodImages.icon(forMood: .missing), date: Date(), color: moodTint.color(forMood: .missing), secondaryColor: moodTint.secondary(forMood: .missing))) } } timeLineView = timeLineView.sorted(by: { $0.date > $1.date }) return timeLineView } } struct Provider: IntentTimelineProvider { let timeLineCreator = TimeLineCreator() /* placeholder for widget, no data gets redacted auto */ func placeholder(in context: Context) -> SimpleEntry { return SimpleEntry(date: Date(), configuration: ConfigurationIntent(), timeLineViews: Array(TimeLineCreator.createViews(daysBack: 11).prefix(10))) } func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (SimpleEntry) -> ()) { let entry = SimpleEntry(date: Date(), configuration: ConfigurationIntent(), timeLineViews: Array(TimeLineCreator.createViews(daysBack: 11).prefix(10))) completion(entry) } func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline) -> ()) { let entry = SimpleEntry(date: Calendar.current.date(byAdding: .second, value: 15, to: Date())!, configuration: ConfigurationIntent(), timeLineViews: nil) let midNightEntry = SimpleEntry(date: Calendar.current.date(bySettingHour: 23, minute: 59, second: 59, of: Date())!, configuration: ConfigurationIntent(), timeLineViews: nil) let date = Calendar.current.date(byAdding: .second, value: 10, to: Date())! let timeline = Timeline(entries: [entry, midNightEntry], policy: .after(date)) 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: LargeWidgetView(entry: entry) case .systemExtraLarge: LargeWidgetView(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 timeLineView = [WatchTimelineView]() init(entry: Provider.Entry) { self.entry = entry timeLineView = [TimeLineCreator.createViews(daysBack: 2).first!] } var body: some View { ZStack { Color(UIColor.secondarySystemBackground) HStack { ForEach(self.timeLineView) { 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 timeLineView = [WatchTimelineView]() init(entry: Provider.Entry) { self.entry = entry timeLineView = Array(TimeLineCreator.createViews(daysBack: 6).prefix(5)) } var body: some View { VStack { Spacer() TimeHeaderView(startDate: timeLineView.first!.date, endDate: timeLineView.last!.date) .frame(minWidth: 0, maxWidth: .infinity) .multilineTextAlignment(.leading) TimeBodyView(group: timeLineView) .clipShape(RoundedRectangle(cornerRadius: 25, style: .continuous)) .frame(minHeight: 0, maxHeight: 55) .padding() Spacer() } } } struct LargeWidgetView: View { var entry: Provider.Entry var timeLineView = [WatchTimelineView]() init(entry: Provider.Entry) { self.entry = entry timeLineView = Array(TimeLineCreator.createViews(daysBack: 11).prefix(10)) } var firstGroup: ([WatchTimelineView], String) { return (Array(self.timeLineView.prefix(5)), UUID().uuidString) } var secondGroup: ([WatchTimelineView], String) { return (Array(self.timeLineView.suffix(5)), UUID().uuidString) } var body: some View { VStack { Spacer() ForEach([firstGroup, secondGroup], id: \.1) { group in VStack { Spacer() TimeHeaderView(startDate: group.0.first!.date, endDate: group.0.last!.date) .frame(minWidth: 0, maxWidth: .infinity) .multilineTextAlignment(.leading) TimeBodyView(group: group.0) .clipShape(RoundedRectangle(cornerRadius: 25, style: .continuous)) .frame(minHeight: 0, maxHeight: 55) .padding() Spacer() } } 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 timeLineView: [WatchTimelineView] init(entry: Provider.Entry) { self.entry = entry timeLineView = TimeLineCreator.createViews(daysBack: 2) } var body: some View { if let first = timeLineView.first { IconView(iconViewModel: IconViewModel(backgroundImage: first.graphic, bgColor: first.color, bgOverlayColor: first.secondaryColor, centerImage: first.graphic, innerColor: first.color)) } else { IconView(iconViewModel: IconViewModel.great) } } } /**********************************************************/ 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 if let inUseWidget = UserDefaultsStore.getCustomWidgets().first(where: { $0.inUse == true }) { CustomWidgetView(customWidgetModel: inUseWidget) .frame(width: geo.size.width, height: geo.size.height) } else { CustomWidgetView(customWidgetModel: CustomWidgetModel.randomWidget) .frame(width: geo.size.width, height: geo.size.height) } } } } /**********************************************************/ 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 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() FeelsVoteWidget() } } 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, .systemLarge]) } } 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 previews: some View { Group { FeelsGraphicWidgetEntryView(entry: SimpleEntry(date: Date(), configuration: ConfigurationIntent(), timeLineViews: [WatchTimelineView(image: HandEmojiMoodImages.icon(forMood: .great), graphic: HandEmojiMoodImages.icon(forMood: .great), date: Date(), color: MoodTints.Neon.color(forMood: .great), secondaryColor: .white), WatchTimelineView(image: HandEmojiMoodImages.icon(forMood: .great), graphic: HandEmojiMoodImages.icon(forMood: .great), date: Date(), color: MoodTints.Neon.color(forMood: .great), secondaryColor: .white)])) .previewContext(WidgetPreviewContext(family: .systemSmall)) FeelsGraphicWidgetEntryView(entry: SimpleEntry(date: Date(), configuration: ConfigurationIntent(), timeLineViews: [WatchTimelineView(image: HandEmojiMoodImages.icon(forMood: .horrible), graphic: HandEmojiMoodImages.icon(forMood: .horrible), date: Date(), color: MoodTints.Neon.color(forMood: .horrible), secondaryColor: .white), WatchTimelineView(image: HandEmojiMoodImages.icon(forMood: .horrible), graphic: HandEmojiMoodImages.icon(forMood: .horrible), date: Date(), color: MoodTints.Neon.color(forMood: .horrible), secondaryColor: .white)])) .previewContext(WidgetPreviewContext(family: .systemSmall)) // 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) } } }