Embed a web view for viewing content inline

This commit is contained in:
Jed Fox 2022-12-02 20:35:11 -05:00
parent 16a814a27c
commit 1c5b66f7e7
No known key found for this signature in database
GPG Key ID: 0B61D18EA54B47E1
6 changed files with 91 additions and 11 deletions

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21512" systemVersion="22A400" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21509" systemVersion="21G217" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="Application" representedClassName="CoreDataStack.Application" syncable="YES">
<attribute name="identifier" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
<attribute name="name" attributeType="String"/>
@ -14,6 +14,7 @@
<attribute name="desc" attributeType="String"/>
<attribute name="embedURLRaw" optional="YES" attributeType="String"/>
<attribute name="height" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="html" optional="YES" attributeType="String"/>
<attribute name="image" optional="YES" attributeType="String"/>
<attribute name="providerName" optional="YES" attributeType="String"/>
<attribute name="providerURLRaw" optional="YES" attributeType="String"/>

View File

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

View File

@ -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(

View File

@ -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
}
}

View File

@ -11,8 +11,11 @@ import MastodonAsset
import MastodonCore
import CoreDataStack
import UIKit
import WebKit
public final class StatusCardControl: UIControl {
public var urlToOpen = PassthroughSubject<URL, Never>()
private var disposeBag = Set<AnyCancellable>()
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("<meta name='viewport' content='width=device-width,user-scalable=no'><style>body { margin: 0; color-scheme: light dark; } body > :only-child { width: 100vw !important; height: 100vh !important }</style>" + 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)

View File

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