md-app/ios/SearchWidget/SearchWidget.swift

227 lines
7.2 KiB
Swift

//
// 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<FlutterLiveActivities>
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<FlutterLiveActivities>
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<EmptyTimelineEntry>) -> ()) {
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")
}
}