// // SearchWidget.swift // SearchWidget // // Created by Johann Villegas on 9/04/25. // import WidgetKit import SwiftUI import ActivityKit // Shared FlutterLiveActivities definition for use in both app and widget extension struct FlutterLiveActivities: ActivityAttributes, Identifiable { public typealias ContentState = FlutterLiveActivitiesContent public struct FlutterLiveActivitiesContent: Codable, Hashable { var title: String var query: String var resultsCount: String var timestamp: String } var id = UUID() } // Define the lock screen/banner state of Live Activities @available(iOS 16.2, *) struct FlutterLiveActivitiesLiveActivity: Widget { var body: some WidgetConfiguration { ActivityConfiguration(for: FlutterLiveActivities.self) { context in // Lock screen/banner UI goes here LockScreenLiveActivityView(context: context) } dynamicIsland: { context in // Dynamic Island UI goes here DynamicIslandLiveActivityView(context: context) } } } @available(iOS 16.2, *) struct LockScreenLiveActivityView: View { let context: ActivityViewContext var body: some View { VStack { Text(context.state.title) .font(.headline) HStack { Image(systemName: "magnifyingglass") .foregroundColor(.green) Text(context.state.query) .font(.subheadline) } HStack { Image(systemName: "doc.text") .foregroundColor(.blue) Text("Results: \(context.state.resultsCount)") .font(.subheadline) } Text("Updated: \(formattedDate(from: context.state.timestamp))") .font(.caption) .foregroundColor(.secondary) } .padding() } func formattedDate(from isoString: String) -> String { let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ" if let date = dateFormatter.date(from: isoString) { dateFormatter.dateFormat = "HH:mm:ss" return dateFormatter.string(from: date) } return isoString } } @available(iOS 16.2, *) struct DynamicIslandLiveActivityView: View { let context: ActivityViewContext var body: some View { DynamicIsland { // Expanded state DynamicIslandExpandedRegion(.leading) { HStack { Image(systemName: "magnifyingglass") .foregroundColor(.green) Text(context.state.query) .font(.headline) .lineLimit(1) } } DynamicIslandExpandedRegion(.trailing) { HStack { Image(systemName: "doc.text") .foregroundColor(.blue) Text(context.state.resultsCount) .font(.headline) } } DynamicIslandExpandedRegion(.bottom) { VStack(alignment: .leading) { Text(context.state.title) .font(.caption) Text("Updated: \(formattedDate(from: context.state.timestamp))") .font(.caption) .foregroundColor(.secondary) } } } compactLeading: { Image(systemName: "magnifyingglass") .foregroundColor(.green) } compactTrailing: { Text(context.state.resultsCount) .font(.headline) } minimal: { Image(systemName: "magnifyingglass") .foregroundColor(.green) } } func formattedDate(from isoString: String) -> String { let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ" if let date = dateFormatter.date(from: isoString) { dateFormatter.dateFormat = "HH:mm:ss" return dateFormatter.string(from: date) } return isoString } } struct SearchWidget: Widget { let kind: String = "SearchWidget" var body: some WidgetConfiguration { StaticConfiguration(kind: kind, provider: EmptyTimelineProvider()) { _ in EmptyWidgetView() } .configurationDisplayName("Estudios Bíblicos") .description("Search Widget for Bible Studies App") .supportedFamilies([.systemSmall, .systemMedium]) } } // Empty widget for devices that don't support Live Activities struct EmptyWidgetView: View { var body: some View { ZStack { Color(red: 0.42, green: 0.55, blue: 0.14) // #6b8e23 color VStack { Image(systemName: "magnifyingglass") .resizable() .scaledToFit() .frame(width: 40, height: 40) .foregroundColor(.white) Text("Estudios Bíblicos") .font(.headline) .foregroundColor(.white) } .padding() } } } // Empty provider for devices that don't support Live Activities struct EmptyTimelineProvider: TimelineProvider { func placeholder(in context: Context) -> EmptyTimelineEntry { EmptyTimelineEntry() } func getSnapshot(in context: Context, completion: @escaping (EmptyTimelineEntry) -> ()) { let entry = EmptyTimelineEntry() completion(entry) } func getTimeline(in context: Context, completion: @escaping (Timeline) -> ()) { let entry = EmptyTimelineEntry() let timeline = Timeline(entries: [entry], policy: .never) completion(timeline) } } struct EmptyTimelineEntry: TimelineEntry { let date: Date = Date() } struct SearchWidget_Previews: PreviewProvider { static var previews: some View { SearchWidget() .previewContext(WidgetPreviewContext(family: .systemSmall)) } } // Preview for Live Activities @available(iOS 16.2, *) struct FlutterLiveActivitiesLiveActivity_Previews: PreviewProvider { static let attributes = FlutterLiveActivities() static let contentState = FlutterLiveActivities.ContentState( title: "Searching Bible Studies", query: "resurrection", resultsCount: "42", timestamp: ISO8601DateFormatter().string(from: Date()) ) static var previews: some View { attributes .previewContext(contentState, viewKind: .dynamicIsland(.compact)) .previewDisplayName("Island Compact") attributes .previewContext(contentState, viewKind: .dynamicIsland(.expanded)) .previewDisplayName("Island Expanded") attributes .previewContext(contentState, viewKind: .dynamicIsland(.minimal)) .previewDisplayName("Minimal") attributes .previewContext(contentState, viewKind: .content) .previewDisplayName("Notification") } }