iosswiftswiftuipreview

Swift intrinsicContentSize uses max frame to scale LPLinkPreview


I am creating a link preview and in order to set a custom frame to the preview I have to override the intrinsic content size and place my own sizing. I pass frame.width and frame.height which is supposed to reflect .frame(maxWidth: 300, maxHeight: 400) in the view.

If the preview is originally smaller that 300*400 I want it to remain its current size and if it's larger, then it should scale down to 300x400. The problem I'm facing is passing in frame.width and frame.height to the override var intrinsicContentSize causes the preview to be scaled to 300x400, which is supposed to only be the max frame, not the default frame.

Thus smaller previews such as this SO thread are stretched to the size of 300x400. How can I modify this to not stretch previews while keeping the max frame?

I tried to pass in the code below but that didn't work. I also played around with .scaledToFill, .scaledToFit, .aspectRation(... fit/fill) but none did the trick.

return CGSize(width: super.intrinsicContentSize.width, height: super.intrinsicContentSize.height)
import SwiftUI

struct SwiftUIView: View {
    @State var text = "https://stackoverflow.com/questions/77101513/resize-swift-ui-real-link-to-fit-frame/77102190?noredirect=1#comment135923413_77102190"
    var body: some View {
        if let url = checkForFirstUrl(text: text){
            LinkPreviewView(url: url)
                .frame(maxWidth: 300, maxHeight: 400)
                .aspectRatio(contentMode: .fit)
        }
    }
}

struct LinkPreviewView: UIViewRepresentable {
    let url: URL
    
    func makeUIView(context: Context) -> UIView {
        let linkView = CustomLinkView()
        return linkView
    }

    func updateUIView(_ uiView: UIView, context: Context) {
        let provider = LPMetadataProvider()
        provider.startFetchingMetadata(for: url) { metaData, error in
            guard let data = metaData, error == nil else { return }
            DispatchQueue.main.async {
                if let linkPreview = uiView as? CustomLinkView {
                    linkPreview.metadata = data
                }
            }
        }
    }
}

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

func checkForFirstUrl(text: String) -> URL? {
    let types: NSTextCheckingResult.CheckingType = .link

    do {
        let detector = try NSDataDetector(types: types.rawValue)
        let matches = detector.matches(in: text, options: .reportCompletion, range: NSMakeRange(0, text.count))
        if let firstMatch = matches.first {
            return firstMatch.url
        }
    } catch {
        print("")
    }

    return nil
}

Solution

  • I found a fix by dynamically picking the size using linkView.systemLayoutSizeFitting(UIView.layoutFittingExpandedSize) here is the code:

    import SwiftUI
    
    struct SwiftUIView: View {
        @State var text = "https://wp.titan.email/mail/"
        @State var size: CGFloat = .zero
        @State var size2: CGFloat = .zero
        var body: some View {
            VStack {
                if let url = checkForFirstUrl(text: text){
                    LinkPreviewView(url: url, width: $size, height: $size2, message: true)
                        .frame(width: size, height: size2, alignment: .leading)
                        .aspectRatio(contentMode: .fill)
                        .cornerRadius(15)
                }
            }
        }
    }
    
    import LinkPresentation
    import UIKit
    import SwiftUI
    
    struct LinkPreviewView: UIViewRepresentable {
        let url: URL
        @Binding var width: CGFloat
        @Binding var height: CGFloat
        
        func makeUIView(context: Context) -> UIView {
            let linkView = CustomLinkView()
            
            let provider = LPMetadataProvider()
            provider.startFetchingMetadata(for: url) { metaData, error in
                guard let data = metaData, error == nil else { return }
                DispatchQueue.main.async {
                    linkView.metadata = data
                    let linksize = linkView.systemLayoutSizeFitting(UIView.layoutFittingExpandedSize)
                    let width = linksize.width
                    let height = linksize.height
                    
                    let goal = widthOrHeight(width: true) * 0.8
                    if width > goal {
                        self.width = goal
                    } else {
                        self.width = width
                    }
                    if height > 400 {
                       self.height = 400
                    } else {
                       self.height = height
                    }
                }
            }
            return linkView
        }
    
            func updateUIView(_ uiView: UIView, context: Context) { }
        }
        
        class CustomLinkView: LPLinkView {
            
            init() {
                super.init(frame: .zero)
            }
                
            override var intrinsicContentSize: CGSize {
                return CGSize(width: frame.width, height: frame.height)
            }
        }
        
        func checkForFirstUrl(text: String) -> URL? {
            let types: NSTextCheckingResult.CheckingType = .link
        
            do {
                let detector = try NSDataDetector(types: types.rawValue)
                let matches = detector.matches(in: text, options: .reportCompletion, range: NSMakeRange(0, text.count))
                if let firstMatch = matches.first {
                    return firstMatch.url
                }
            } catch {
                print("")
            }
        
            return nil
        }