Reputation: 4430
I have to put a background image from the internet in a widget, but it gives me the following problem.
You can tell me where I'm wrong?
Note.swift(model)
import Foundation
struct Note: Identifiable, Codable {
let title: String
let message: String
var id = UUID()
}
SmallWidget.swift
import SwiftUI
struct SmallWidget: View {
var entry: Note
@Environment(\.colorScheme) var colorScheme
var body: some View {
VStack(alignment: .center){
Text(entry.title)
.font(.title)
.bold()
.minimumScaleFactor(0.5)
.foregroundColor(.white)
.shadow(
color: Color.black,
radius: 1.0,
x: CGFloat(4),
y: CGFloat(4))
Text(entry.message)
.foregroundColor(Color.gray)
.shadow(
color: Color.black,
radius: 1.0,
x: CGFloat(4),
y: CGFloat(4))
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.edgesIgnoringSafeArea(.all)
.background(NetworkImage(url: URL(string: "https://a.wattpad.com/useravatar/climaxmite.256.718018.jpg")))
}
}
struct SmallWidget_Previews: PreviewProvider {
static var previews: some View {
let note = Note(title: "Title", message: "Mex")
Group {
SmallWidget(entry: note)
SmallWidget(entry: note)
.preferredColorScheme(.dark)
}
}
}
note.swift
import WidgetKit
import SwiftUI
struct Provider: TimelineProvider {
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(note: Note(title: "Title", message: "placeholder"))
}
func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
let entry = SimpleEntry(note: Note(title: "Title", message: "getSnapshot"))
completion(entry)
}
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
var entries: [SimpleEntry] = []
// Generate a timeline consisting of five entries an hour apart, starting from the current date.
let currentDate = Date()
for hourOffset in 0 ..< 5 {
let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
let entry = SimpleEntry(note: Note(title: "Title", message: "getTimeline"))
entries.append(entry)
}
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
}
struct SimpleEntry: TimelineEntry {
public let note: Note
public let date: Date = Date()
}
struct noteEntryView : View {
/*var entry: Provider.Entry
var body: some View {
Text(entry.date, style: .time)
}*/
var entry: Provider.Entry
@Environment(\.widgetFamily) private var widgetFamily
var body: some View {
switch widgetFamily {
case .systemSmall:
SmallWidget(entry: entry.note)
default:
SmallWidget(entry: entry.note)
}
}
}
@main
struct note: Widget {
let kind: String = "note"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider()) { entry in
noteEntryView(entry: entry)
}
.configurationDisplayName("My Widget")
.description("This is an example widget.")
.supportedFamilies([.systemSmall, .systemMedium])
}
}
struct note_Previews: PreviewProvider {
static var previews: some View {
noteEntryView(entry: SimpleEntry(note: Note(title: "Title", message: "Mex")))
.previewContext(WidgetPreviewContext(family: .systemSmall))
}
}
NetworkImage.swift
import Foundation
import Combine
import SwiftUI
extension NetworkImage {
class ViewModel: ObservableObject {
@Published var imageData: Data?
@Published var isLoading = false
private var cancellables = Set<AnyCancellable>()
func loadImage(from url: URL?) {
isLoading = true
guard let url = url else {
isLoading = false
return
}
URLSession.shared.dataTaskPublisher(for: url)
.map { $0.data }
.replaceError(with: nil)
.receive(on: DispatchQueue.main)
.sink { [weak self] in
self?.imageData = $0
self?.isLoading = false
}
.store(in: &cancellables)
}
}
}
// Download image from URL
struct NetworkImage: View {
@StateObject private var viewModel = ViewModel()
let url: URL?
var body: some View {
Group {
if let data = viewModel.imageData, let uiImage = UIImage(data: data) {
Image(uiImage: uiImage)
.resizable()
.aspectRatio(contentMode: .fit)
} else if viewModel.isLoading {
ProgressView()
} else {
Image(systemName: "photo")
.resizable()
.aspectRatio(contentMode: .fit)
.redacted(reason: /*@START_MENU_TOKEN@*/.placeholder/*@END_MENU_TOKEN@*/)
}
}
.onAppear {
viewModel.loadImage(from: url)
}
}
}
Upvotes: 1
Views: 2521
Reputation: 53830
Unfortunately, no async image loader is going to work in a widget since widgets are static. They're declarative, but not dynamic. They're not allowed to change over time.
You can download the image in getSnapshot()
and getTimeline()
before calling the completion handler, then pass the image along with data in the entry.
Here's some pseudo code:
struct SimpleEntry: TimelineEntry {
public let note: Note
public let date: Date = Date()
}
func placeholder(in context: Context) -> SimpleEntry {
// Some placeholder note + image
SimpleEntry(note: note, date: Date())
}
func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
// fetch note and image
...
note.image = image
let entry = SimpleEntry(note: note, date: Date())
completion(entry)
}
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
var entries: [SimpleEntry] = []
// fetch note and image
...
note.image = image
let entry = SimpleEntry(note: note, date: Date())
entries.append(entry)
let timeline = ...
completion(timeline)
}
struct noteEntryView : View {
var entry: Provider.Entry
@Environment(\.widgetFamily) private var widgetFamily
@ViewBuilder
var body: some View {
switch widgetFamily {
case .systemSmall:
SmallWidget(note: entry.note)
default:
SmallWidget(note: entry.note)
}
}
}
struct SmallWidget: View {
let note: Note
var body: some View {
Image(uiImage: note.image)
...
}
}
Upvotes: 1