// // 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 var body: some View { if entry.items.isEmpty { } else { switch family { case .systemSmall: SquareItemView(item: entry.items[0]) .padding() case .systemMedium, .systemLarge: VStack { ForEach(Array(entry.items.prefix(family.maxItemCount).enumerated()), id: \.element.id) { (index, item) in if index != 0 { Divider() } ItemListEntryView(item: item) Spacer(minLength: 4) } } .padding() 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() } } } .padding() } 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 titleUIFont: UIFont { // TODO: this should use the compressed SF Pro variant, but there's no API to get at it let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .headline).withSymbolicTraits(.traitCondensed)! return UIFont(descriptor: descriptor, size: 0) } private var titleFont = Font(titleUIFont).leading(.tight) 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, format: .relative(presentation: .numeric, unitsStyle: .narrow)) .font(.caption) } } 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)) } }