Reputation: 964
Here is an example code for a WidgetExtension that should show the link preview image retrieved using the LinkPresentation. I am guessing the widgets dont load url data directly but need to be somehow fetched using the geTimeline and defined there somehow? How is it done?
Using the standard widget template in Xcode just adding the Metadata
import WidgetKit
import SwiftUI
import LinkPresentation
struct SimpleEntry: TimelineEntry {
let date: Date
}
struct LinkPresentationWidgetEntryView : View {
var entry: Provider.Entry
var body: some View {
//Text(entry.date, style: .time)
VStack{
MetadataView(vm: LinkViewModel(link: "https://www.wsj.com/articles/global-stock-markets-dow-update-03-03-2021-11614761029?mod=markets_lead_pos1"))
}
}
}
class LinkViewModel : ObservableObject {
let metadataProvider = LPMetadataProvider()
@Published var metadata: LPLinkMetadata?
@Published var image: UIImage?
init(link : String) {
guard let url = URL(string: link) else {
return
}
metadataProvider.startFetchingMetadata(for: url) { (metadata, error) in
guard error == nil else {
assertionFailure("Error")
return
}
DispatchQueue.main.async {
self.metadata = metadata
}
guard let imageProvider = metadata?.imageProvider else { return }
imageProvider.loadObject(ofClass: UIImage.self) { (image, error) in
guard error == nil else {
// handle error
return
}
if let image = image as? UIImage {
// do something with image
DispatchQueue.main.async {
self.image = image
}
} else {
print("no image available")
}
}
}
}
}
struct MetadataView : View {
@StateObject var vm : LinkViewModel
var body: some View {
VStack {
if let uiImage = vm.image {
Image(uiImage: uiImage)
.resizable()
//.scaledToFill()
//.clipped()
.frame(width: 60, height: 60)
}
}
}
}
Upvotes: 0
Views: 371
Reputation: 1059
AFAIK WidgetKit does not observe View
s, but only render them once and discard them afterwards. Thus your approach of having an @StateObject
doesn't work as WidgetKit has already discarded the View when your request returns.
As you already guessed you have to do every asynchronous task inside of either getSnapshot
or getTimeline
.
Below is a minimal working example reusing some of your code. Of course you might want to adapt configuration of the url, error handling and timeline-strategy according to your needs.
Note that I don't know if any of this is considered best practice...I only know it works :)
import WidgetKit
import SwiftUI
import Intents
import LinkPresentation
struct Provider: IntentTimelineProvider {
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(date: Date(), configuration: ConfigurationIntent(), image: UIImage(systemName: "wifi.slash"))
}
func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (SimpleEntry) -> ()) {
fetchImage(for: "https://www.wsj.com/articles/global-stock-markets-dow-update-03-03-2021-11614761029?mod=markets_lead_pos1", calling: { image in
let entry = SimpleEntry(date: Date(), configuration: configuration, image: image)
completion(entry)
})
}
func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
getSnapshot(for: configuration, in: context, completion: { entry in
let timeline = Timeline(entries: [entry], policy: .after(Date().advanced(by: 3600)))
completion(timeline)
})
}
}
struct SimpleEntry: TimelineEntry {
let date: Date
let configuration: ConfigurationIntent
let image: UIImage?
}
struct WidgetEntryView : View {
var entry: Provider.Entry
var body: some View {
if let image = entry.image {
Image(uiImage: image).resizable().scaledToFill()
} else {
Text("Could not load image...")
}
}
}
func fetchImage(for url: String, calling callback: @escaping (UIImage) -> Void = { _ in }) {
let metadataProvider = LPMetadataProvider()
guard let url = URL(string: url) else {
return
}
metadataProvider.startFetchingMetadata(for: url) { (metadata, error) in
guard error == nil else {
assertionFailure("Error")
return
}
guard let imageProvider = metadata?.imageProvider else { return }
imageProvider.loadObject(ofClass: UIImage.self) { (image, error) in
guard error == nil else {
// handle error
return
}
if let image = image as? UIImage {
// do something with image
DispatchQueue.main.async {
callback(image)
}
} else {
print("no image available")
}
}
}
}
@main
struct MyWidget: Widget {
let kind: String = "Widget"
var body: some WidgetConfiguration {
IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in
WidgetEntryView(entry: entry)
}
.configurationDisplayName("My Widget")
.description("This is an example widget.")
}
}
Upvotes: 1