From 1c5b66f7e715a31c167289d468981d25899160a8 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Fri, 2 Dec 2022 20:35:11 -0500 Subject: [PATCH] Embed a web view for viewing content inline --- .../CoreData 5.xcdatamodel/contents | 3 +- .../CoreDataStack/Entity/Mastodon/Card.swift | 8 ++- .../Persistence/Persistence+Card.swift | 3 +- .../Sources/MastodonExtension/UIView.swift | 18 ++++-- .../View/Content/StatusCardControl.swift | 64 ++++++++++++++++++- .../View/Content/StatusView+ViewModel.swift | 6 ++ 6 files changed, 91 insertions(+), 11 deletions(-) diff --git a/MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/CoreData 5.xcdatamodel/contents b/MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/CoreData 5.xcdatamodel/contents index c18d7492b..4d44775e1 100644 --- a/MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/CoreData 5.xcdatamodel/contents +++ b/MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/CoreData 5.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -14,6 +14,7 @@ + diff --git a/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Card.swift b/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Card.swift index d656191f9..64f5e2120 100644 --- a/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Card.swift +++ b/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Card.swift @@ -49,6 +49,8 @@ public final class Card: NSManagedObject { @NSManaged public private(set) var embedURLRaw: String? // sourcery: autoGenerateProperty @NSManaged public private(set) var blurhash: String? + // sourcery: autoGenerateProperty + @NSManaged public private(set) var html: String? // sourcery: autoGenerateRelationship @NSManaged public private(set) var status: Status @@ -96,6 +98,7 @@ extension Card: AutoGenerateProperty { public let image: String? public let embedURLRaw: String? public let blurhash: String? + public let html: String? public init( urlRaw: String, @@ -110,7 +113,8 @@ extension Card: AutoGenerateProperty { height: Int64, image: String?, embedURLRaw: String?, - blurhash: String? + blurhash: String?, + html: String? ) { self.urlRaw = urlRaw self.title = title @@ -125,6 +129,7 @@ extension Card: AutoGenerateProperty { self.image = image self.embedURLRaw = embedURLRaw self.blurhash = blurhash + self.html = html } } @@ -142,6 +147,7 @@ extension Card: AutoGenerateProperty { self.image = property.image self.embedURLRaw = property.embedURLRaw self.blurhash = property.blurhash + self.html = property.html } public func update(property: Property) { diff --git a/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+Card.swift b/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+Card.swift index 838d5e128..9ab8a817c 100644 --- a/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+Card.swift +++ b/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+Card.swift @@ -77,7 +77,8 @@ extension Persistence.Card { height: Int64(context.entity.height ?? 0), image: context.entity.image, embedURLRaw: context.entity.embedURL, - blurhash: context.entity.blurhash + blurhash: context.entity.blurhash, + html: context.entity.html.flatMap { $0.isEmpty ? nil : $0 } ) let card = Card.insert( diff --git a/MastodonSDK/Sources/MastodonExtension/UIView.swift b/MastodonSDK/Sources/MastodonExtension/UIView.swift index fa62be6c0..bfa253680 100644 --- a/MastodonSDK/Sources/MastodonExtension/UIView.swift +++ b/MastodonSDK/Sources/MastodonExtension/UIView.swift @@ -48,18 +48,22 @@ extension UIView { } public extension UIView { - - func pinToParent() { + + @discardableResult + func pinToParent() -> [NSLayoutConstraint] { pinTo(to: self.superview) } - - func pinTo(to view: UIView?) { - guard let pinToView = view else { return } - NSLayoutConstraint.activate([ + + @discardableResult + func pinTo(to view: UIView?) -> [NSLayoutConstraint] { + guard let pinToView = view else { return [] } + let constraints = [ topAnchor.constraint(equalTo: pinToView.topAnchor), leadingAnchor.constraint(equalTo: pinToView.leadingAnchor), trailingAnchor.constraint(equalTo: pinToView.trailingAnchor), bottomAnchor.constraint(equalTo: pinToView.bottomAnchor), - ]) + ] + NSLayoutConstraint.activate(constraints) + return constraints } } diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift index 6bc8d6789..7a63441e9 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift @@ -11,8 +11,11 @@ import MastodonAsset import MastodonCore import CoreDataStack import UIKit +import WebKit public final class StatusCardControl: UIControl { + public var urlToOpen = PassthroughSubject() + private var disposeBag = Set() private let containerStackView = UIStackView() @@ -23,6 +26,9 @@ public final class StatusCardControl: UIControl { private let titleLabel = UILabel() private let linkLabel = UILabel() + private static let cardContentPool = WKProcessPool() + private var webView: WKWebView? + private var layout: Layout? private var layoutConstraints: [NSLayoutConstraint] = [] @@ -115,6 +121,12 @@ public final class StatusCardControl: UIControl { self?.containerStackView.layoutIfNeeded() } + if let html = card.html, !html.isEmpty { + let webView = setupWebView() + webView.loadHTMLString("" + html, baseURL: nil) + addSubview(webView) + } + updateConstraints(for: card.layout) } @@ -123,6 +135,9 @@ public final class StatusCardControl: UIControl { if let window = window { layer.borderWidth = 1 / window.screen.scale + } else { + webView?.removeFromSuperview() + webView = nil } } @@ -159,6 +174,10 @@ public final class StatusCardControl: UIControl { ] } + if let webView { + layoutConstraints += webView.pinTo(to: imageView) + } + NSLayoutConstraint.activate(layoutConstraints) } @@ -178,6 +197,46 @@ public final class StatusCardControl: UIControl { } } +extension StatusCardControl: WKNavigationDelegate, WKUIDelegate { + fileprivate func setupWebView() -> WKWebView { + let config = WKWebViewConfiguration() + config.processPool = Self.cardContentPool + config.websiteDataStore = .nonPersistent() // private/incognito mode + config.suppressesIncrementalRendering = true + config.allowsInlineMediaPlayback = true + let webView = WKWebView(frame: .zero, configuration: config) + webView.uiDelegate = self + webView.navigationDelegate = self + webView.translatesAutoresizingMaskIntoConstraints = false + self.webView = webView + return webView + } + + public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction) async -> WKNavigationActionPolicy { + let isTopLevelNavigation: Bool + if let frame = navigationAction.targetFrame { + isTopLevelNavigation = frame.isMainFrame + } else { + isTopLevelNavigation = true + } + + if isTopLevelNavigation, + // ignore form submits and such + navigationAction.navigationType == .linkActivated || navigationAction.navigationType == .other, + let url = navigationAction.request.url, + url.absoluteString != "about:blank" { + urlToOpen.send(url) + return .cancel + } + return .allow + } + + public func webViewDidClose(_ webView: WKWebView) { + webView.removeFromSuperview() + self.webView = nil + } +} + private extension StatusCardControl { enum Layout: Equatable { case compact @@ -187,7 +246,10 @@ private extension StatusCardControl { private extension Card { var layout: StatusCardControl.Layout { - let aspectRatio = CGFloat(width) / CGFloat(height) + var aspectRatio = CGFloat(width) / CGFloat(height) + if !aspectRatio.isFinite { + aspectRatio = 1 + } return abs(aspectRatio - 1) < 0.05 || image == nil ? .compact : .large(aspectRatio: aspectRatio) diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift index 77de106b1..634473a65 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift @@ -494,6 +494,12 @@ extension StatusView.ViewModel { statusView.setStatusCardControlDisplay() } .store(in: &disposeBag) + + statusView.statusCardControl.urlToOpen + .sink { url in + statusView.delegate?.statusView(statusView, didTapCardWithURL: url) + } + .store(in: &disposeBag) } private func bindToolbar(statusView: StatusView) {