diff --git a/Sources/TsuzuKit/CapsuleButtonStyle.swift b/Sources/TsuzuKit/ButtonStyle/CapsuleButtonStyle.swift similarity index 100% rename from Sources/TsuzuKit/CapsuleButtonStyle.swift rename to Sources/TsuzuKit/ButtonStyle/CapsuleButtonStyle.swift diff --git a/Sources/TsuzuKit/PressButtonStyle.swift b/Sources/TsuzuKit/ButtonStyle/PressButtonStyle.swift similarity index 100% rename from Sources/TsuzuKit/PressButtonStyle.swift rename to Sources/TsuzuKit/ButtonStyle/PressButtonStyle.swift diff --git a/Sources/TsuzuKit/Representable/LinkPresentationView.swift b/Sources/TsuzuKit/Representable/LinkPresentationView.swift new file mode 100644 index 0000000..7dab2bf --- /dev/null +++ b/Sources/TsuzuKit/Representable/LinkPresentationView.swift @@ -0,0 +1,92 @@ +import SwiftUI +import LinkPresentation + +public struct LinkPresentationView: UIViewRepresentable { + public typealias UIViewType = LPLinkView + + let url: URL + @Binding var redraw: Bool + + public func makeUIView(context: UIViewRepresentableContext) -> UIViewType { + let view = LPLinkView(url: url) + view.isHidden = true + if let cachedData = MetaCache.shared.metadata(for: url) { + update(view: view, with: cachedData) + } else { + self.fetchMetadata(for: url) { result in + switch result { + case .success(let metadata): + MetaCache.shared.store(metadata) + self.update(view: view, with: metadata) + case .failure: + self.redraw = true + } + } + } + return view + } + + public func updateUIView(_ uiView: UIViewType, context: UIViewRepresentableContext) { + } + + private func fetchMetadata(for url: URL, completion: @escaping (Result) -> Void) { + let provider = LPMetadataProvider() + + provider.startFetchingMetadata(for: url) { metadata, error in + if let error = error { + completion(.failure(error)) + } else if let metadata = metadata { + completion(.success(metadata)) + } else { + completion(.failure(LPError(.unknown))) + } + } + } + + private func update(view: UIViewType, with metadata: LPLinkMetadata) { + DispatchQueue.main.async { + view.metadata = metadata + view.sizeToFit() + self.redraw = true + view.isHidden = false + } + } +} + +public final class MetaCache { + public static let shared = MetaCache() + private init(){} + + private let storage = UserDefaults.standard + + private let key = "Metadata" + + func store(_ metadata: LPLinkMetadata) { + do { + let data = try NSKeyedArchiver.archivedData(withRootObject: metadata, requiringSecureCoding: true) + var metadatas: [String: Data] = storage.dictionary(forKey: key) as? [String: Data] ?? [:] + metadatas[metadata.originalURL!.absoluteString] = data + storage.set(metadatas, forKey: key) + } + catch { + print("Failed storing metadata with error \(error as NSError)") + } + } + + func metadata(for url: URL) -> LPLinkMetadata? { + guard let metadatas = storage.dictionary(forKey: key) as? [String: Data] else { + return nil + } + + guard let data = metadatas[url.absoluteString] else { + return nil + } + + do { + return try NSKeyedUnarchiver.unarchivedObject(ofClass: LPLinkMetadata.self, from: data) + } catch { + print("Failed to unarchive metadata with error \(error)") + return nil + } + } +}