create new webview coordinator to manage webview

This commit is contained in:
Satindar Dhillon 2022-03-20 09:28:32 -07:00
parent 083ca05b3e
commit 7667dc27cd
5 changed files with 406 additions and 10 deletions

View file

@ -7,6 +7,11 @@ import Utils
import Views
import WebKit
struct SafariWebLink: Identifiable {
let id: UUID
let url: URL
}
final class WebReaderViewModel: ObservableObject {
@Published var isLoading = false
@Published var htmlContent: String?
@ -33,13 +38,140 @@ final class WebReaderViewModel: ObservableObject {
struct WebReaderContainerView: View {
let item: FeedItem
@State private var showFontSizePopover = false
@State var showHighlightAnnotationModal = false
@State var safariWebLink: SafariWebLink?
@State private var navBarVisibilityRatio = 1.0
@State private var showDeleteConfirmation = false
@State var increaseFontActionID: UUID?
@State var decreaseFontActionID: UUID?
@EnvironmentObject var dataService: DataService
@EnvironmentObject var authenticator: Authenticator
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
@StateObject var viewModel = WebReaderViewModel()
var fontAdjustmentPopoverView: some View {
FontSizeAdjustmentPopoverView(
increaseFontAction: { increaseFontActionID = UUID() },
decreaseFontAction: { decreaseFontActionID = UUID() }
)
}
var navBariOS14: some View {
HStack(alignment: .center) {
Button(
action: { self.presentationMode.wrappedValue.dismiss() },
label: {
Image(systemName: "chevron.backward")
.font(.appTitleTwo)
.foregroundColor(.appGrayTextContrast)
.padding(.horizontal)
}
)
.scaleEffect(navBarVisibilityRatio)
Spacer()
Button(
action: { showFontSizePopover.toggle() },
label: {
Image(systemName: "textformat.size")
.font(.appTitleTwo)
}
)
.padding(.horizontal)
.scaleEffect(navBarVisibilityRatio)
}
.frame(height: readerViewNavBarHeight * navBarVisibilityRatio)
.opacity(navBarVisibilityRatio)
.background(Color.systemBackground)
.onTapGesture {
showFontSizePopover = false
}
}
@available(macOS 12.0, *)
@available(iOS 15.0, *)
var navBar: some View {
HStack(alignment: .center) {
Button(
action: { self.presentationMode.wrappedValue.dismiss() },
label: {
Image(systemName: "chevron.backward")
.font(.appTitleTwo)
.foregroundColor(.appGrayTextContrast)
.padding(.horizontal)
}
)
.scaleEffect(navBarVisibilityRatio)
Spacer()
Button(
action: { showFontSizePopover.toggle() },
label: {
Image(systemName: "textformat.size")
.font(.appTitleTwo)
}
)
.padding(.horizontal)
.scaleEffect(navBarVisibilityRatio)
Menu(
content: {
Group {
Button(
action: {}, // ,viewModel.handleArchiveAction(dataService: dataService) },
label: {
Label(
item.isArchived ? "Unarchive" : "Archive",
systemImage: item.isArchived ? "tray.and.arrow.down.fill" : "archivebox"
)
}
)
Button(
action: { showDeleteConfirmation = true },
label: { Label("Delete", systemImage: "trash") }
)
}
},
label: {
Image.profile
.padding(.horizontal)
.scaleEffect(navBarVisibilityRatio)
}
)
}
.frame(height: readerViewNavBarHeight * navBarVisibilityRatio)
.opacity(navBarVisibilityRatio)
.background(Color.systemBackground)
.onTapGesture {
showFontSizePopover = false
}
.alert("Are you sure?", isPresented: $showDeleteConfirmation) {
Button("Remove Link", role: .destructive) {
// viewModel.handleDeleteAction(dataService: dataService)
}
Button("Cancel", role: .cancel, action: {})
}
}
var body: some View {
Group {
ZStack {
if let htmlContent = viewModel.htmlContent {
WebReader(htmlContent: htmlContent, item: item)
WebReader(
htmlContent: htmlContent,
item: item,
openLinkAction: { url in print(url) },
webViewActionHandler: { _ in },
navBarVisibilityRatioUpdater: {
if $0 < 1 {
showFontSizePopover = false
}
navBarVisibilityRatio = $0
},
authToken: authenticator.authToken ?? "",
increaseFontActionID: $increaseFontActionID,
decreaseFontActionID: $decreaseFontActionID,
annotationSaveTransactionID: nil
)
} else {
Color.clear
.contentShape(Rectangle())
@ -49,26 +181,137 @@ struct WebReaderContainerView: View {
}
}
}
if showFontSizePopover {
VStack {
Color.clear
.contentShape(Rectangle())
.frame(height: LinkItemDetailView.navBarHeight)
HStack {
Spacer()
fontAdjustmentPopoverView
.background(Color.appButtonBackground)
.cornerRadius(8)
.padding(.trailing, 44)
}
Spacer()
}
.background(
Color.clear
.contentShape(Rectangle())
.onTapGesture {
showFontSizePopover = false
}
)
}
if #available(iOS 15.0, *) {
VStack(spacing: 0) {
navBar
Spacer()
}
.navigationBarHidden(true)
} else {
VStack(spacing: 0) {
navBariOS14
Spacer()
}
.navigationBarHidden(true)
}
}.onDisappear {
// Clear the shared webview content when exiting
WebViewManager.shared().loadHTMLString("<html></html>", baseURL: nil)
}
.navigationBarHidden(true)
}
}
// TODO: implement things WebAppWrapperView does
struct WebReader: UIViewRepresentable {
let htmlContent: String
let item: FeedItem
let openLinkAction: (URL) -> Void
let webViewActionHandler: (WKScriptMessage) -> Void
let navBarVisibilityRatioUpdater: (Double) -> Void
let authToken: String
func makeUIView(context _: Context) -> WKWebView {
@Binding var increaseFontActionID: UUID?
@Binding var decreaseFontActionID: UUID?
@State var annotationSaveTransactionID: UUID?
@State private var annotation = String()
func makeCoordinator() -> WebReaderCoordinator {
WebReaderCoordinator()
}
func fontSize() -> Int {
let storedSize = UserDefaults.standard.integer(forKey: UserDefaultKey.preferredWebFontSize.rawValue)
return storedSize <= 1 ? UITraitCollection.current.preferredWebFontSize : storedSize
}
func makeUIView(context: Context) -> WKWebView {
let webView = WebViewManager.create()
let contentController = WKUserContentController()
webView.loadHTMLString(
WebReaderContent(htmlContent: htmlContent, item: item).styledContent,
WebReaderContent(
htmlContent: htmlContent,
item: item,
authToken: authToken,
isDark: UITraitCollection.current.userInterfaceStyle == .dark,
fontSize: "\(fontSize())px",
margin: "0"
)
.styledContent,
baseURL: UtilsPackage.bundleURL
)
webView.navigationDelegate = context.coordinator
webView.isOpaque = false
webView.backgroundColor = .clear
webView.configuration.userContentController = contentController
webView.scrollView.delegate = context.coordinator
webView.scrollView.contentInset.top = readerViewNavBarHeight
webView.scrollView.verticalScrollIndicatorInsets.top = readerViewNavBarHeight
for action in WebViewAction.allCases {
webView.configuration.userContentController.add(context.coordinator, name: action.rawValue)
}
webView.configuration.userContentController.add(webView, name: "viewerAction")
// webView.configureForOmnivoreAppEmbed(
// config: WebViewConfig(
// url: dataService.appEnvironment.webAppBaseURL,
// themeId: UITraitCollection.current.userInterfaceStyle == .dark ? "Gray" : "LightGray",
// margin: 0,
// fontSize: fontSize(),
// fontFamily: "inter",
// rawAuthCookie: rawAuthCookie
// )
// )
context.coordinator.linkHandler = openLinkAction
context.coordinator.webViewActionHandler = webViewActionHandler
context.coordinator.updateNavBarVisibilityRatio = navBarVisibilityRatioUpdater
return webView
}
func updateUIView(_: WKWebView, context _: UIViewRepresentableContext<WebReader>) {}
func updateUIView(_ webView: WKWebView, context: Context) {
if annotationSaveTransactionID != context.coordinator.lastSavedAnnotationID {
context.coordinator.lastSavedAnnotationID = annotationSaveTransactionID
(webView as? WebView)?.saveAnnotation(annotation: annotation)
}
if increaseFontActionID != context.coordinator.previousIncreaseFontActionID {
context.coordinator.previousIncreaseFontActionID = increaseFontActionID
(webView as? WebView)?.increaseFontSize()
}
if decreaseFontActionID != context.coordinator.previousDecreaseFontActionID {
context.coordinator.previousDecreaseFontActionID = decreaseFontActionID
(webView as? WebView)?.decreaseFontSize()
}
}
}

View file

@ -11,10 +11,13 @@ struct WebReaderContent {
let margin: String
let content: String
let item: FeedItem
let themeKey: String
let authToken: String
init(
htmlContent: String,
item: FeedItem,
authToken: String,
isDark: Bool = false,
fontSize: String = "16px",
margin: String = "24px"
@ -27,6 +30,8 @@ struct WebReaderContent {
self.margin = margin
self.content = htmlContent
self.item = item
self.themeKey = isDark ? "Gray" : "LightGray"
self.authToken = authToken
}
var styleString: String {
@ -34,6 +39,8 @@ struct WebReaderContent {
"--text-font-size:\(textFontSize);--font-color:\(fontColor);--font-color-transparent\(fontColorTransparent);--table-header-color:\(tableHeaderColor);--headers-color:\(headerColor);--app-margin:\(margin);"
}
// TODO: pass in fontSize and theme
var styledContent: String {
"""
<!DOCTYPE html>
@ -64,6 +71,7 @@ struct WebReaderContent {
}
loadArticle()
window.localStorage.setItem("authToken", "\(authToken)")
</script>
</div>
<script src="bundle.js"></script>

View file

@ -0,0 +1,144 @@
import Combine
import Models
import Services
import SwiftUI
import UIKit
import Utils
import Views
import WebKit
final class WebReaderCoordinator: NSObject {
var webViewActionHandler: (WKScriptMessage) -> Void = { _ in }
var linkHandler: (URL) -> Void = { _ in }
var needsReload = true
var lastSavedAnnotationID: UUID?
var previousIncreaseFontActionID: UUID?
var previousDecreaseFontActionID: UUID?
var updateNavBarVisibilityRatio: (Double) -> Void = { _ in }
private var yOffsetAtStartOfDrag: Double?
private var lastYOffset: Double = 0
private var hasDragged = false
private var isNavBarHidden = false
override init() {
super.init()
}
var navBarVisibilityRatio: Double = 1.0 {
didSet {
isNavBarHidden = navBarVisibilityRatio == 0
updateNavBarVisibilityRatio(navBarVisibilityRatio)
}
}
}
extension WebReaderCoordinator: WKScriptMessageHandler {
func userContentController(_: WKUserContentController, didReceive message: WKScriptMessage) {
webViewActionHandler(message)
}
}
extension WebReaderCoordinator: WKNavigationDelegate {
// swiftlint:disable:next line_length
func webView(_: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
if navigationAction.navigationType == .linkActivated {
if let linkURL = navigationAction.request.url {
linkHandler(linkURL)
}
decisionHandler(.cancel)
} else {
decisionHandler(.allow)
}
}
func webView(_ webView: WKWebView, didFinish _: WKNavigation!) {
#if os(iOS)
webView.isOpaque = true
webView.backgroundColor = .systemBackground
#endif
}
}
#if os(iOS)
extension WebReaderCoordinator: UIScrollViewDelegate {
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
hasDragged = true
yOffsetAtStartOfDrag = scrollView.contentOffset.y + scrollView.contentInset.top
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard hasDragged else { return }
let yOffset = scrollView.contentOffset.y
if yOffset == 0 {
scrollView.contentInset.top = readerViewNavBarHeight
navBarVisibilityRatio = 1
return
}
if yOffset < 0 {
navBarVisibilityRatio = 1
scrollView.contentInset.top = readerViewNavBarHeight
return
}
if yOffset < readerViewNavBarHeight {
let isScrollingUp = yOffsetAtStartOfDrag ?? 0 > yOffset
navBarVisibilityRatio = isScrollingUp || yOffset < 0 ? 1 : min(1, 1 - (yOffset / readerViewNavBarHeight))
scrollView.contentInset.top = navBarVisibilityRatio * readerViewNavBarHeight
return
}
guard let yOffsetAtStartOfDrag = yOffsetAtStartOfDrag else { return }
if yOffset > yOffsetAtStartOfDrag, !isNavBarHidden {
let translation = yOffset - yOffsetAtStartOfDrag
let ratio = translation < readerViewNavBarHeight ? 1 - (translation / readerViewNavBarHeight) : 0
navBarVisibilityRatio = min(ratio, 1)
scrollView.contentInset.top = navBarVisibilityRatio * readerViewNavBarHeight
}
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if decelerate, scrollView.contentOffset.y + scrollView.contentInset.top < (yOffsetAtStartOfDrag ?? 0) {
scrollView.contentInset.top = readerViewNavBarHeight
navBarVisibilityRatio = 1
}
}
func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool {
scrollView.contentInset.top = readerViewNavBarHeight
navBarVisibilityRatio = 1
return false
}
}
#endif
struct WebViewConfig {
let url: URL
let themeId: String
let margin: Int
let fontSize: Int
let fontFamily: String
let rawAuthCookie: String?
}
extension WKWebView {
func configureForOmnivoreAppEmbed(config: WebViewConfig) {
// Set cookies to pass article preferences to web view
injectCookie(cookieString: "theme=\(config.themeId); Max-Age=31536000;", url: config.url)
injectCookie(cookieString: "margin=\(config.margin); Max-Age=31536000;", url: config.url)
injectCookie(cookieString: "fontSize=\(config.fontSize); Max-Age=31536000;", url: config.url)
injectCookie(cookieString: "fontFamily=\(config.fontFamily); Max-Age=31536000;", url: config.url)
injectCookie(cookieString: config.rawAuthCookie, url: config.url)
}
func injectCookie(cookieString: String?, url: URL) {
if let cookieString = cookieString {
for cookie in HTTPCookie.cookies(withResponseHeaderFields: ["Set-Cookie": cookieString], for: url) {
configuration.websiteDataStore.httpCookieStore.setCookie(cookie) {}
}
}
}
}

View file

@ -15,5 +15,5 @@ public enum FeatureFlag {
public static let enableShareButton = false
public static let enableSnooze = false
public static let showFeedItemTags = false
public static let useLocalWebView = false
public static let useLocalWebView = true
}

View file

@ -2,7 +2,7 @@ import WebKit
/// Describes actions that can be sent from the WebView back to native views.
/// The names on the javascript side must match for an action to be handled.
enum WebViewAction: String, CaseIterable {
public enum WebViewAction: String, CaseIterable {
case highlightAction
case readingProgressUpdate
}
@ -26,11 +26,11 @@ public final class WebView: WKWebView {
fatalError("init(coder:) has not been implemented")
}
func increaseFontSize() {
public func increaseFontSize() {
dispatchEvent("increaseFontSize")
}
func decreaseFontSize() {
public func decreaseFontSize() {
dispatchEvent("decreaseFontSize")
}
@ -123,6 +123,7 @@ public final class WebView: WKWebView {
setDefaultMenu()
}
// swiftlint:disable:next line_length
public func gestureRecognizer(_: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith _: UIGestureRecognizer) -> Bool {
true
}
@ -209,7 +210,7 @@ public final class WebView: WKWebView {
UIMenuController.shared.showMenu(from: self, rect: rect)
}
func saveAnnotation(annotation: String) {
public func saveAnnotation(annotation: String) {
// swiftlint:disable:next line_length
let dispatch = "var event = new Event('saveAnnotation');event.annotation = '\(annotation)';document.dispatchEvent(event);"
evaluateJavaScript(dispatch) { obj, err in