AndiAna
AndiAna

Reputation: 964

How to use the LinkPresentation Framework inside a Widget in SwiftUI to show an urlImage preview?

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

Answers (1)

theMomax
theMomax

Reputation: 1059

AFAIK WidgetKit does not observe Views, 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

Related Questions