1

I'm using code I found from a great article here that demonstrates how to use the LinkPresentation framework in SwiftUI.

However I'm having a small problem that I can't find solution to - the link previews loads their metadata but don't refresh the view once fully loaded unless I do something which forces the view to refresh, like rotating the phone.

They load as much as this:

Before Rotating

Then look like this after rotating:

After Rotating

I'd like the views to fully refresh once the metadata is loaded. I feel like I probably need to add some binding in somewhere but I don't know where. Can anyone help at all?

Here's the UIViewRepresentable

import SwiftUI
import LinkPresentation

struct URLPreview : UIViewRepresentable {
    var previewURL:URL

    func makeUIView(context: Context) -> LPLinkView {
        LPLinkView(url: previewURL)
    }

    func updateUIView(_ view: LPLinkView, context: Context) {
        // New instance for each update

        let provider = LPMetadataProvider()

        provider.startFetchingMetadata(for: previewURL) { (metadata, error) in
            if let md = metadata {
                DispatchQueue.main.async {
                    view.metadata = md
                    view.sizeToFit()

                }
            }
        }
    }
}

and here's how it's called:

struct Content: View {
    var body: some View {
        URLPreview(previewURL: URL(string: "www.apple.com")!)
    }
}
mralexhay
  • 1,164
  • 1
  • 10
  • 16
  • How could you load in table list without looping through array? I need to achieve the same whatever you have mentioned in screenshot i.e. multiple URLs from array to table list, but it just scatter – NSPratik Mar 28 '20 at 18:41
  • @NSPratik TableViews aren't ideal to be used with `LPLinkView`s. Consider using a `VStack`. I posted a detailed answer below. – Isuru Mar 28 '21 at 08:17

2 Answers2

2

Triggering a redraw is what you need. Not a fan of this, but you can try Binding a State CGSize and set frame to width/height.

struct URLPreview : UIViewRepresentable {
    var previewURL:URL
    //Add binding
    @Binding var metaSize: CGSize

    func makeUIView(context: Context) -> LPLinkView {
        LPLinkView(url: previewURL)
    }

    func updateUIView(_ view: LPLinkView, context: Context) {
        // New instance for each update

        let provider = LPMetadataProvider()

        provider.startFetchingMetadata(for: previewURL) { (metadata, error) in
            if let md = metadata {
                DispatchQueue.main.async {
                    view.metadata = md
                    view.sizeToFit()
                    //Set binding after resize
                    self.metaSize = view.frame.size
                }
            }
        }
    }
}
struct ContentView: View {
    //can default original state
    @State var metaSize: CGSize = CGSize()
    
    var body: some View {
        URLPreview(previewURL: URL(string: "www.apple.com")!, metaSize: $metaSize)
            .frame(width: metaSize.width, height: metaSize.height)
    }
}

UPDATE

NSPratik is right, the solution is not really viable for Lists. So an amended solution is actually just to use a simple Bool State to toggle the Views generated by a list:

struct ContentView: View {
    //can default original state
    @State var togglePreview = false
    let urls: [String] = ["https://medium.com","https://apple.com","https://yahoo.com","https://stackoverflow.com"]
    
    var body: some View {
        List(urls, id: \.self) { url in
            URLPreview(previewURL: URL(string: url)!, togglePreview: self.$togglePreview)
                .aspectRatio(contentMode: .fit)
                .padding()
        }
    }
}

struct URLPreview : UIViewRepresentable {
    var previewURL:URL
    //Add binding
    @Binding var togglePreview: Bool

    func makeUIView(context: Context) -> LPLinkView {
        let view = LPLinkView(url: previewURL)
        
        let provider = LPMetadataProvider()

        provider.startFetchingMetadata(for: previewURL) { (metadata, error) in
            if let md = metadata {
                DispatchQueue.main.async {
                    view.metadata = md
                    view.sizeToFit()
                    self.togglePreview.toggle()
                }
            }
        }
        
        return view
    }
    
    func updateUIView(_ uiView: LPLinkView, context: UIViewRepresentableContext<URLPreview>) {
    }
}

We simply use togglePreview as our trigger, pass it to a Binding var in the UIView, and then setup our List. Even if this triggers all the Views in the List, there won't be any animation to reflect the resize of fully loaded LinkViews.

Community
  • 1
  • 1
Danny B
  • 93
  • 5
  • This is working...but I need to load multiple URLs in list view. – NSPratik Mar 28 '20 at 17:29
  • @NSPratik good point, I've added a new solution that could help – Danny B Mar 31 '20 at 20:33
  • Thanks @DannyB I will check and get back – NSPratik Mar 31 '20 at 20:53
  • “Even if this triggers all the views...” If first few links are loading and meanwhile, I scroll down to 20th cell. Suddenly then link of 1st cell loaded, will it refresh first cell properly or will there be any reusability issue? – NSPratik Mar 31 '20 at 21:03
  • I have tried but performance is not smoother and there is a cell re-usability issues. Can you help me? – NSPratik Apr 01 '20 at 17:06
  • @NSPratik SwiftUI relies on dependencies to refresh it's views, if the dependencies (`@ObservedObject`, `@EnvironmentObject`, `@StateObject`, `@State`) changes, then it would reload it's views. So make sure you define the dependancies (example: `DataStore` class which could contain a `@Published` property containing the `[NSLPLinkMetadata]`, now have `DataStore` as your `ObservedObject`, so when ever the metadata array changes, the SwiftUI would be refreshed. – user1046037 Nov 26 '21 at 15:26
1

Using LPLinkViews in a List causes huge memory leaks. Your best bet is to use a VStack embedded inside a ScrollView.

ScrollView {
    VStack {
        ForEach(links, id: \.self) { link in
            if let url = URL(string: link) {
                LinkRow(url: url)
            }
        }
    }
    .padding()
}

This will make the LPLinkViews resize themselves as they load.

I have done this in an app and it has significant improvement over using a List. However a little caveat, if the user stars scrolling up and down as soon as the view comes on screen while the previews are still loading, it might causes crashes at random. Unfortunately I haven't been able to find a solution for that yet. I think all these crashes happen because the LPMetadataProvider requires you to be called on the main thread and obviously that doesn't play well with smooth scrolling.

Isuru
  • 30,617
  • 60
  • 187
  • 303