// // Recents.swift // Reader // // Created by Shadowfacts on 6/19/22. // import WidgetKit import SwiftUI import Persistence struct RecentsProvider: IntentTimelineProvider { func placeholder(in context: Context) -> RecentsEntry { RecentsEntry(items: [], configuration: ConfigurationIntent()) } func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (RecentsEntry) -> ()) { let entry = RecentsEntry(items: getItems(), configuration: configuration) completion(entry) } func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline) -> ()) { // TODO: get account from configuration intent let entry = RecentsEntry(items: getItems(), configuration: configuration) let refreshDate = Calendar.current.date(byAdding: .hour, value: 1, to: Date())! let timeline = Timeline(entries: [entry], policy: .after(refreshDate)) completion(timeline) } private func getItems() -> [WidgetData.Item] { guard let account = LocalData.mostRecentAccount() else { return [] } return WidgetData.load(account: account).recentItems } } struct RecentsEntry: TimelineEntry { let date = Date() let items: [WidgetData.Item] let configuration: ConfigurationIntent } struct RecentsEntryView: View { let entry: RecentsEntry @Environment(\.widgetFamily) var family @Environment(\.colorScheme) var colorScheme var body: some View { if #available(iOS 17.0, *) { content // the system padding is too much :S .padding(-4) .containerBackground(for: .widget) { colorScheme == .dark ? Color.black : .white } } else { content .padding() } } @ViewBuilder private var content: some View { if entry.items.isEmpty { } else { switch family { case .systemSmall: SquareItemView(item: entry.items[0]) case .systemMedium, .systemLarge: VStack(spacing: 0) { ForEach(Array(entry.items.prefix(family.maxItemCount).enumerated()), id: \.element.id) { (index, item) in if index != 0 { Divider() Spacer(minLength: 4) } ItemListEntryView(item: item) Spacer(minLength: 4) } } case .systemExtraLarge: if #available(iOS 16.0, *) { VStack { ForEach(Array(stride(from: 0, to: min(entry.items.count, family.maxItemCount), by: 2)), id: \.self) { idx in HStack { ItemListEntryView(item: entry.items[idx]) if idx + 1 < entry.items.count { Divider() ItemListEntryView(item: entry.items[idx + 1]) } } if idx + 2 < entry.items.count { Spacer() Divider() } } } } else { Text("Requires iOS 16") } default: fatalError("unreachable") } } } } private extension WidgetFamily { var maxItemCount: Int { switch self { case .systemSmall: return 1 case .systemMedium: return 2 case .systemLarge: return 4 case .systemExtraLarge: return 8 default: return 0 } } } private var feedFont = Font.subheadline.weight(.medium).italic() private var titleFont = Font.headline.width(.compressed) struct SquareItemView: View { let item: WidgetData.Item var body: some View { VStack(alignment: .leading) { HStack { Text(verbatim: item.feedTitle ?? "") .font(feedFont) .foregroundColor(.red) // force the vstack to be as wide as possible Spacer(minLength: 0) } Text(verbatim: item.title ?? "") .font(titleFont) if let published = item.published { Text(published, format: .relative(presentation: .numeric, unitsStyle: .narrow)) .font(.caption) } Spacer(minLength: 0) } .widgetURL(item.launchURL) } } struct ItemListEntryView: View { let item: WidgetData.Item var body: some View { Link(destination: item.launchURL) { VStack(alignment: .leading) { HStack { Text(verbatim: item.feedTitle ?? "") .font(feedFont) .foregroundColor(.red) Spacer() if let published = item.published { Text(published, style: .relative) .font(.caption.width(.condensed)) .multilineTextAlignment(.trailing) .layoutPriority(-1) } } Text(verbatim: item.title ?? "") .font(titleFont) .lineLimit(3) Spacer(minLength: 0) } } } } struct Recents_Previews: PreviewProvider { static let item1 = WidgetData.Item( id: "1", feedTitle: "Daring Fireball", title: "There's a Privacy Angle on Apple's Decision to Finance Apple Pay Layer On Its Own", published: Calendar.current.date(byAdding: .hour, value: -1, to: Date())! ) static let item2 = WidgetData.Item( id: "2", feedTitle: "Ars Technica", title: "Senate bill would ban data brokers from selling location and health data", published: Calendar.current.date(byAdding: .hour, value: -2, to: Date())! ) static var previews: some View { // RecentsEntryView(entry: RecentsEntry(items: [item1], configuration: ConfigurationIntent())) // .previewContext(WidgetPreviewContext(family: .systemSmall)) RecentsEntryView(entry: RecentsEntry(items: [item1, item2], configuration: ConfigurationIntent())) .previewContext(WidgetPreviewContext(family: .systemMedium)) } }