alionthego
alionthego

Reputation: 9703

Can't change frame size of LPLinkView in a SwiftUI List

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

Upvotes: 12

Views: 2126

Answers (4)

Jason Armstrong
Jason Armstrong

Reputation: 1240

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))

Upvotes: 0

Arsienij
Arsienij

Reputation: 147

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: {}
            )
        }
    }

Upvotes: 0

PAULMAX
PAULMAX

Reputation: 312

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

Upvotes: 1

alionthego
alionthego

Reputation: 9703

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

Upvotes: 12

Related Questions