14

I am trying to display rich links in a SwiftUI List and no matter what I try, I can't seem to be able to change the size of the link view (UIViewRepresentable) on screen.

Is there a minimum size for a particular link? And how can I get it. Adding .aspectRatio and clipped() will respect size but the link is heavily clipped. Not sure why the link will not adjust aspectRatio to fit view.

Some of the following code is sourced from the following tutorial: https://www.appcoda.com/linkpresentation-framework/

I am using the following UIViewRepresentable for the LinkView:

import SwiftUI
import LinkPresentation

struct LinkViewRepresentable: UIViewRepresentable {
 
    typealias UIViewType = LPLinkView
    
    var metadata: LPLinkMetadata?
 
    func makeUIView(context: Context) -> LPLinkView {
        guard let metadata = metadata else { return LPLinkView() }
        let linkView = LPLinkView(metadata: metadata)
        return linkView
    }
 
    func updateUIView(_ uiView: LPLinkView, context: Context) {

    }
}

And my view with List is:

import SwiftUI
import LinkPresentation

struct ContentView: View {
    
    @ObservedObject var linksViewModel = LinksViewModel()
    
    var links: [(String, String)] = [("https://www.apple.com", "1"), ("https://www.stackoverflow.com", "2")]
    
    var body: some View {
        ScrollView(.vertical) {
            LazyVStack {
                ForEach(links, id: \.self.1) { link in
                    VStack {
                        Text(link.0)
                            .onAppear {
                                linksViewModel.getLinkMetadata(link: link)
                            }
                        if let richLink = linksViewModel.links.first(where: { $0.id == link.1 }) {
                            if let metadata = richLink.metadata {
                                if metadata.url != nil {
                                    LinkViewRepresentable(metadata: metadata)
                                        .frame(width: 200)  // setting frame dimensions here has no effect
                                }
                            }
                        }
                    }
                }
            }
            .padding()
        }
    }
}

Setting the frame of the view or contentMode(.fit) or padding or anything else I've tried does not change the size of the frame of the LinkViewRepresentable. I have tried sizeToFit in the representable on update and no luck. Is it possible to control the size of the representable view here?

Here are additional Files:

import Foundation
import LinkPresentation

class LinksViewModel: ObservableObject {
    
    @Published var links = [Link]()
    
    init() {
        loadLinks()
    }
    
    func createLink(with metadata: LPLinkMetadata, id: String) {
        let link = Link()
        link.id = id
        link.metadata = metadata
        links.append(link)
        saveLinks()
    }
    
    
    fileprivate func saveLinks() {
        do {
            let data = try NSKeyedArchiver.archivedData(withRootObject: links, requiringSecureCoding: true)
            guard let docDirURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return }
            try data.write(to: docDirURL.appendingPathComponent("links"))
            print(docDirURL.appendingPathComponent("links"))
        } catch {
            print(error.localizedDescription)
        }
    }
    
    
    fileprivate func loadLinks() {
        guard let docDirURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return }
        let linksURL = docDirURL.appendingPathComponent("links")
        
        if FileManager.default.fileExists(atPath: linksURL.path) {
            do {
                let data = try Data(contentsOf: linksURL)
                guard let unarchived = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? [Link] else { return }
                links = unarchived
            } catch {
                print(error.localizedDescription)
            }
        }
    }
    
    func fetchMetadata(for link: String, completion: @escaping (Result<LPLinkMetadata, Error>) -> Void) {

        guard let uRL = URL(string: link) else { return }
        let metadataProvider = LPMetadataProvider()
        metadataProvider.startFetchingMetadata(for: uRL) { (metadata, error) in
            if let error = error {
                print(error)
                completion(.failure(error))
                return
            }
            if let metadata = metadata {
                completion(.success(metadata))
            }
        }
    }
    
    func getLinkMetadata(link: (String, String)) {
        for storedLink in self.links {
            if storedLink.id != link.1 {
                return
            }
        }
        do {
            let detector = try NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)
            let matches = detector.matches(in: link.0, options: [], range: NSRange(location: 0, length: link.0.utf16.count))
            if let match = matches.first {
                guard let range = Range(match.range, in: link.0) else { return }
                let uRLString = link.0[range]
                
                self.fetchMetadata(for: String(uRLString)) { result in
                    self.handleLinkFetchResult(result, link: link)
                }
            }
        } catch {
            print(error)
        }
    }
    
    private func handleLinkFetchResult(_ result: Result<LPLinkMetadata, Error>, link: (String, String)) {
        DispatchQueue.main.async {
            switch result {
                case .success(let metadata):
                self.createLink(with: metadata, id: link.1)
                case .failure(let error):
                print(error.localizedDescription)
            }
        }
    }

}

And Link Class:

import Foundation
import LinkPresentation

class Link: NSObject, NSSecureCoding, Identifiable {
    
    var id: String?
    var metadata: LPLinkMetadata?
    
    override init() {
        super.init()
    }
    
    // MARK: - NSSecureCoding Requirements
    
    static var supportsSecureCoding = true

    func encode(with coder: NSCoder) {
        guard let id = id, let metadata = metadata else { return }
        coder.encode(id, forKey: "id")
        coder.encode(metadata as NSObject, forKey: "metadata")
    }
 
    required init?(coder: NSCoder) {
        id = coder.decodeObject(forKey: "id") as? String
        metadata = coder.decodeObject(of: LPLinkMetadata.self, forKey: "metadata")
    }
}

This is what I get:

enter image description here

17
  • where u used to get the makeUIView Commented Dec 3, 2021 at 9:58
  • What do you mean? Commented Dec 3, 2021 at 9:59
  • Your code works for me (with minor mods for my simple tests, since all info is not available). Changing .frame(width: 100) to, for example, .frame(width: 300) works. On macos 12.1-beta, using xcode 13.2-beta. Tested on real devices, iOS-15, Catalyst-12.1. Commented Dec 3, 2021 at 11:51
  • 1
    The only fix I found was setting an explicit height and width which doesn’t really work for me because I want to only set width only But if that works for you I did that by subclassing the linkview and setting override intrinsicContenySize and defining your size there. Commented Dec 29, 2021 at 10:12
  • 1
    sure. I've included that as an answer below although it doesn't really work for me as I need a dynamic height. Commented Dec 29, 2021 at 10:19

5 Answers 5

13
+200

The solution that worked for me was subclassing the linkView overriding the intrinsic content size. Thanks to user1046037's comment, using super.intrinsicContentSize.height will enable it to work dynamically.

import SwiftUI
import LinkPresentation

class CustomLinkView: LPLinkView {
    override var intrinsicContentSize: CGSize { CGSize(width: 0, height: super.intrinsicContentSize.height) }
}

struct LinkViewRepresentable: UIViewRepresentable {
 
    typealias UIViewType = CustomLinkView
    
    var metadata: LPLinkMetadata?
 
    func makeUIView(context: Context) -> CustomLinkView {
        guard let metadata = metadata else { return CustomLinkView() }
        let linkView = CustomLinkView(metadata: metadata)
        return linkView
    }
 
    func updateUIView(_ uiView: CustomLinkView, context: Context) {
    }
}

enter image description here

Sign up to request clarification or add additional context in comments.

8 Comments

Nice one! could you make 2 small changes, your code crashes, I had to initialise using URL (to prevent crash) and also you could return CGSize(width: 0, height: super.intrinsicContentSize.height) without hard coding the sizes. That would make it dynamic. Nice work!
Thanks for your comment. you really helped solve the dynamic height issue I was struggling so much with and it works very well now. I wasn't sure about the crash you mentioned initializing with URL. Mine is not crashing. Did you mean you are initializing the CustomLinkView with URL instead of Metadata? Mine is working fine initializing with Metadata
oh ok, may be the way I am using it is slightly different, anyway, it would take 24 hours to grant you the bounty, will do in 24 hours. Nice work once again.
and thank you also for the help polishing it off and making it work properly.
I just experienced the crash you mentioned with respect to metadata. that occurs when the metadata is not nil but the url property of metadata is nil. For now I just added a conditional nil check for the metadata.url to solve that.
|
1

Here is my LinkPresentation code example in SwiftUI

import SwiftUI
import LinkPresentation

Custom Class, to resize internal content

class CustomLinkView: LPLinkView {
    override var intrinsicContentSize: CGSize {
        CGSize(width: super.intrinsicContentSize.width,
               height: super.intrinsicContentSize.height)
    }
}

ViewModel Class (not the best approach for SwiftUI)

class LPMeatDataProvider_VM: ObservableObject {

    private let stringURLs: [String] = [
        "https://medium.com", "https://apple.com",
        "https://yahoo.com", "https://stackoverflow.com"
    ]

    @Published var URLs: [URL] = []

    init() { getURLs() }

    func getURLs() {
        stringURLs.forEach { string in
            guard let url = URL(string: string) else { return }
            URLs.append(url)
       }
    }
}

View & UIViewRepresentable

struct URLPreviewContainer: UIViewRepresentable {

    @Binding var togglePreview: Bool

    var previewURL: URL

    func makeUIView(context: Context) -> CustomLinkView {
        let view = CustomLinkView(url: previewURL)
        let provider = LPMetadataProvider()
        provider.startFetchingMetadata(for: previewURL) { metadata, error in
            if error == nil, let metadata = metadata {
                DispatchQueue.main.async {
                    view.metadata = metadata
                    togglePreview.toggle()
                }
            }
        }
        return view
    }

    func updateUIView(_ uiView: CustomLinkView, context: Context) {}
}

struct LPMeatDataProvider_: View {

    @State var togglePreview = false
    @StateObject var vm = LPMeatDataProvider_VM()

    var body: some View {
        ScrollView {
            LazyVStack {
                ForEach(vm.URLs, id: \.self) { _url in
                    URLPreviewContainer(togglePreview: $togglePreview, previewURL: _url)
                        .padding()
                        .padding(.horizontal)
                }
            }
        }
    }
}

enter image description here

Comments

1

If you're using iOS 16+, you can use a combination of @PaulMax's answer and the sizeThatFits() function which is available on UIViewRepresentable.

import SwiftUI
import LinkPresentation

// SOURCE: https://stackoverflow.com/a/75717026/10528457
fileprivate class CustomLinkView: LPLinkView {
    override var intrinsicContentSize: CGSize {
        CGSize(width: super.intrinsicContentSize.width, height: super.intrinsicContentSize.height)
    }
}

struct LinkPreview: UIViewRepresentable {
    var metadata: LPLinkMetadata
        
    func makeUIView(context: Context) -> LPLinkView {
        let view = CustomLinkView(metadata: metadata)
        view.sizeToFit()
        return view
    }
    
    func sizeThatFits(_ proposal: ProposedViewSize, uiView: LPLinkView, context: Context) -> CGSize? {
        // The proposed width is the containing frame's width, if one is available use that width, 
        // otherwise fallback to the custom view's intrinsic width
        let width = proposal.width ?? uiView.intrinsicContentSize.width

        // The proposed height is the containing frame's height which is going to be way to big.
        // So use the view's intrinsic height otherwise fallback to the smallest.
        let height = min(proposal.height ?? .infinity, uiView.intrinsicContentSize.height)
        return CGSize(width: width, height: height)
    }
    
    func updateUIView(_ uiView: LPLinkView, context: Context) {
    }
}

To use:

LinkPreview(metadata: metadata)
                        .id(urlString)
                        .aspectRatio(contentMode: .fit)
                        .transition(.scale(scale: 0.0, anchor: .top).combined(with: .opacity))

Comments

1

Starting from iOS 16, the UIViewRepresentable protocol has a new API sizeThatFits(_:uiView:context:) that lets us provide custom sizing logic for wrapped UIViews in SwiftUI apps.

SwiftUI calls this method at each layout pass, ensuring it always uses the latest content size.

import SwiftUI
import LinkPresentation

struct LinkPreview: UIViewRepresentable {
    let url: URL
    
    func makeUIView(context: Context) -> LPLinkView {
        let preview = LPLinkView(url: url)
        // start fetching metadata
        LPMetadataProvider()
            .startFetchingMetadata(for: url) { metadata, error in
                guard let metadata, error == nil else { return }
                DispatchQueue.main.async {
                    preview.metadata = metadata
                    preview.sizeToFit()
                }
            }
        return preview
    }
    
    // Not needed
    func updateUIView(_ uiView: LPLinkView, context: Context) {}
    
    // iOS 16+ only
    func sizeThatFits(_ proposal: ProposedViewSize, uiView: LPLinkView, context: Context) -> CGSize? {
        // Use the proposed width (or fallback to its default width)
        let width = proposal.width ?? uiView.intrinsicContentSize.width
        // Ask the link view how tall it needs to be for that width
        let bestFit = uiView.sizeThatFits(
            CGSize(width: width, height: .greatestFiniteMagnitude) // For available frame size
        )
        // Return new size
        return CGSize(width: width, height: bestFit.height)
    }
}

Comments

0

So I share with you my option. I'm just created customizable, custom preview for URL for SwiftUI with help of LPMetadataProvider.

It is only for preview purposes, I'm not adding an option to open it in Safari (otherwise it easy to achieve).

enter image description here

I created Observable object, i.e view model for my view. Inside it I pass url and then retrieve metadata for needed pieces (image, title, host).

final class PreviewViewModel: ObservableObject {

@Published var image: UIImage?
@Published var title: String?
@Published var url: String?

let previewURL: URL?

init(_ url: String) {
    self.previewURL = URL(string: url)
    
    initMetada()
}

private func initMetada() {
    guard let previewURL else { return }
    let provider = LPMetadataProvider()
    
    Task {
        let metadata = try await provider.startFetchingMetadata(for: previewURL)
        
        image = try await convertToImage(metadata.imageProvider)
        title = metadata.title
        
        if #available(iOS 16, *) {
            url = metadata.url?.host()
        } else {
            url = metadata.url?.host
        }
    }
}

private func convertToImage(_ imageProvider: NSItemProvider?) async throws -> UIImage? {
    var image: UIImage?
    
    if let imageProvider {
        let type = String(describing: UTType.image)
        
        if imageProvider.hasItemConformingToTypeIdentifier(type) {
            let item = try await imageProvider.loadItem(forTypeIdentifier: type)
            
            if item is UIImage {
                image = item as? UIImage
            }
            
            if item is URL {
                guard let url = item as? URL,
                      let data = try? Data(contentsOf: url) else { return nil }
                
                image = UIImage(data: data)
            }
            
            if item is Data {
                guard let data = item as? Data else { return nil }
                
                image = UIImage(data: data)
            }
        }
    }
    
    return image
}
}

In view just adopted my view model, set up for my preference image, title, etc.

    struct FileLinkView: View {
    
    @ObservedObject var viewModel: PreviewViewModel
    
    let action: () -> ()
    
    var body: some View {
        ZStack(alignment: .topTrailing) {
            HStack(spacing: 15) {
                if let image = viewModel.image {
                    Image(uiImage: image)
                        .resizable()
                        .aspectRatio(contentMode: .fill)
                        .frame(maxWidth: 107, maxHeight: 107)
                        .clipped()
                        .cornerRadius(16)
                }
                
                VStack(alignment: .leading, spacing: 1, content: {
                    if let title = viewModel.title {
                        Text(title)
                            .font(.CreatePost.previewTitle)
                            .foregroundColor(.codGray)
                    }
                    
                    if let url = viewModel.url {
                        Text(url)
                            .font(.smallFont)
                            .foregroundColor(.santasGray)
                    }
                })
                .padding(.top, 16)
                .padding(.bottom, 9)
                .padding(.trailing, 40)
            }
            
            Button(action: action, label: {
                Image.CreatePost.deleteIcon
            })
            .padding([.top, .trailing], 8)
        }
    }
}

    struct FileLinkView_Previews: PreviewProvider {
        static var previews: some View {
            FileLinkView(
                viewModel: PreviewViewModel("https://www.f1news.ru/news/f1-168537.html"),
                action: {}
            )
        }
    }

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.