Reputation: 9703
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:
Upvotes: 12
Views: 2126
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
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).
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
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)
}
}
}
}
}
Upvotes: 1
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) {
}
}
Upvotes: 12