Embed a web view for viewing content inline
This commit is contained in:
parent
16a814a27c
commit
1c5b66f7e7
|
@ -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"/>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in New Issue