From 94d50fafc4d8b757330ad0ba91d8004c7e734468 Mon Sep 17 00:00:00 2001 From: David Walter Date: Tue, 21 Feb 2023 07:23:23 +0100 Subject: [PATCH] Fix In-App Safari (#945) * Fix In-App Safari * Open SFSafariViewController in a separate window --- IceCubesApp/App/SafariRouter.swift | 129 ++++++++++++++++++++++------- 1 file changed, 101 insertions(+), 28 deletions(-) diff --git a/IceCubesApp/App/SafariRouter.swift b/IceCubesApp/App/SafariRouter.swift index 0bbe7e10..5079ba95 100644 --- a/IceCubesApp/App/SafariRouter.swift +++ b/IceCubesApp/App/SafariRouter.swift @@ -14,20 +14,20 @@ private struct SafariRouter: ViewModifier { @EnvironmentObject private var preferences: UserPreferences @EnvironmentObject private var routerPath: RouterPath - @State private var presentedURL: URL? - + @StateObject private var safariManager = InAppSafariManager() + func body(content: Content) -> some View { content .environment(\.openURL, OpenURLAction { url in // Open internal URL. routerPath.handle(url: url) }) - .onOpenURL(perform: { url in + .onOpenURL { url in // Open external URL (from icecubesapp://) let urlString = url.absoluteString.replacingOccurrences(of: "icecubesapp://", with: "https://") guard let url = URL(string: urlString), url.host != nil else { return } _ = routerPath.handle(url: url) - }) + } .onAppear { routerPath.urlHandler = { url in if url.absoluteString.contains("@twitter.com"), url.absoluteString.hasPrefix("mailto:") { @@ -45,31 +45,104 @@ private struct SafariRouter: ViewModifier { guard let scheme = url.scheme, ["https", "http"].contains(scheme.lowercased()) else { return .systemAction } - - presentedURL = url - return .handled + return safariManager.open(url) + } + } + .background { + WindowReader { window in + self.safariManager.windowScene = window.windowScene } } - .fullScreenCover(item: $presentedURL, content: { url in - SafariView(url: url, inAppBrowserReaderView: preferences.inAppBrowserReaderView) - .edgesIgnoringSafeArea(.all) - }) - } - - struct SafariView: UIViewControllerRepresentable { - let url: URL - let inAppBrowserReaderView: Bool - - func makeUIViewController(context _: UIViewControllerRepresentableContext) -> SFSafariViewController { - let configuration = SFSafariViewController.Configuration() - configuration.entersReaderIfAvailable = inAppBrowserReaderView - - let safari = SFSafariViewController(url: url, configuration: configuration) - safari.preferredBarTintColor = UIColor(Theme.shared.primaryBackgroundColor) - safari.preferredControlTintColor = UIColor(Theme.shared.tintColor) - return safari - } - - func updateUIViewController(_: SFSafariViewController, context _: UIViewControllerRepresentableContext) {} + } +} + +private class InAppSafariManager: NSObject, ObservableObject, SFSafariViewControllerDelegate { + var windowScene: UIWindowScene? + let viewController: UIViewController = UIViewController() + var window: UIWindow? + + @MainActor + func open(_ url: URL) -> OpenURLAction.Result { + guard let windowScene = windowScene else { return .systemAction } + + self.window = setupWindow(windowScene: windowScene) + + let configuration = SFSafariViewController.Configuration() + configuration.entersReaderIfAvailable = UserPreferences.shared.inAppBrowserReaderView + + let safari = SFSafariViewController(url: url, configuration: configuration) + safari.preferredBarTintColor = UIColor(Theme.shared.primaryBackgroundColor) + safari.preferredControlTintColor = UIColor(Theme.shared.tintColor) + safari.delegate = self + + DispatchQueue.main.async { [weak self] in + self?.viewController.present(safari, animated: true) + } + + return .handled + } + + func setupWindow(windowScene: UIWindowScene) -> UIWindow { + let window = self.window ?? UIWindow(windowScene: windowScene) + + window.rootViewController = viewController + window.makeKeyAndVisible() + + switch Theme.shared.selectedScheme { + case .dark: + window.overrideUserInterfaceStyle = .dark + case .light: + window.overrideUserInterfaceStyle = .light + } + + self.window = window + return window + } + + func safariViewControllerDidFinish(_ controller: SFSafariViewController) { + window?.resignKey() + window?.isHidden = false + window = nil + } +} + +private struct WindowReader: UIViewRepresentable { + var onUpdate: (UIWindow) -> Void + + func makeUIView(context: Context) -> InjectView { + InjectView(onUpdate: onUpdate) + } + + func updateUIView(_ uiView: InjectView, context: Context) { + } + + class InjectView: UIView { + var onUpdate: (UIWindow) -> Void + + init(onUpdate: @escaping (UIWindow) -> Void) { + self.onUpdate = onUpdate + super.init(frame: .zero) + isHidden = true + isUserInteractionEnabled = false + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func willMove(toWindow newWindow: UIWindow?) { + super.willMove(toWindow: newWindow) + + if let window = newWindow { + onUpdate(window) + } else { + DispatchQueue.main.async { [weak self] in + if let window = self?.window { + self?.onUpdate(window) + } + } + } + } } }