From 76d7d23379c09a795415a39207a528a03b8ced07 Mon Sep 17 00:00:00 2001 From: David Walter Date: Sun, 8 Jan 2023 19:56:16 +0100 Subject: [PATCH] Support In-App Safari (#44) * Support In-App Safari * Fix "View in Browser" * Force external Safari on Account Creation * Fix SafariRouteur issues Attach to NavigationStack Find top-most ViewController * Make Preferred Browser a Picker choice --- IceCubesApp.xcodeproj/project.pbxproj | 4 + IceCubesApp/App/SafariRouteur.swift | 85 +++++++++++++++++++ IceCubesApp/App/Tabs/ExploreTab.swift | 1 + IceCubesApp/App/Tabs/MessagesTab.swift | 1 + IceCubesApp/App/Tabs/NotificationTab.swift | 1 + .../App/Tabs/Settings/AddAccountsView.swift | 3 +- .../App/Tabs/Settings/SettingsTab.swift | 22 ++++- .../App/Tabs/Timeline/TimelineTab.swift | 1 + .../Env/Sources/Env/PreferredBrowser.swift | 6 ++ Packages/Env/Sources/Env/Routeur.swift | 9 +- .../Env/Sources/Env/UserPreferences.swift | 1 + .../Status/Row/StatusRowContextMenu.swift | 5 +- 12 files changed, 128 insertions(+), 11 deletions(-) create mode 100644 IceCubesApp/App/SafariRouteur.swift create mode 100644 Packages/Env/Sources/Env/PreferredBrowser.swift diff --git a/IceCubesApp.xcodeproj/project.pbxproj b/IceCubesApp.xcodeproj/project.pbxproj index 4a127f52..cd5c1814 100644 --- a/IceCubesApp.xcodeproj/project.pbxproj +++ b/IceCubesApp.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 639CDF9C296AC82F00C35E58 /* SafariRouteur.swift in Sources */ = {isa = PBXBuildFile; fileRef = 639CDF9B296AC82F00C35E58 /* SafariRouteur.swift */; }; 9F24EEB829360C330042359D /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9F24EEB729360C330042359D /* Preview Assets.xcassets */; }; 9F295540292B6C3400E0E81B /* Timeline in Frameworks */ = {isa = PBXBuildFile; productRef = 9F29553F292B6C3400E0E81B /* Timeline */; }; 9F2A540729699698009B2D7C /* SupportAppView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F2A540629699698009B2D7C /* SupportAppView.swift */; }; @@ -80,6 +81,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 639CDF9B296AC82F00C35E58 /* SafariRouteur.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariRouteur.swift; sourceTree = ""; }; 9F24EEB729360C330042359D /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 9F29553D292B67B600E0E81B /* Network */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Network; path = Packages/Network; sourceTree = ""; }; 9F29553E292B6AF600E0E81B /* Timeline */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Timeline; path = Packages/Timeline; sourceTree = ""; }; @@ -184,6 +186,7 @@ 9FAE4AC9293783A200772766 /* Tabs */, 9FBFE63C292A715500C250E9 /* IceCubesApp.swift */, 9F398AA52935FE8A00A889F2 /* AppRouteur.swift */, + 639CDF9B296AC82F00C35E58 /* SafariRouteur.swift */, ); path = App; sourceTree = ""; @@ -448,6 +451,7 @@ 9F2B9301295EB8A100DE16D0 /* AppAccountViewModel.swift in Sources */, 9FBFE63D292A715500C250E9 /* IceCubesApp.swift in Sources */, 9F2B92FA295DA7D700DE16D0 /* AddAccountsView.swift in Sources */, + 639CDF9C296AC82F00C35E58 /* SafariRouteur.swift in Sources */, 9F35DB4729506F6600B3281A /* NotificationTab.swift in Sources */, 9F7335F22967608F00AFF0BA /* AddRemoteTimelineVIew.swift in Sources */, 9F7335F72968274500AFF0BA /* AppAccountsSelectorView.swift in Sources */, diff --git a/IceCubesApp/App/SafariRouteur.swift b/IceCubesApp/App/SafariRouteur.swift new file mode 100644 index 00000000..b47e21ad --- /dev/null +++ b/IceCubesApp/App/SafariRouteur.swift @@ -0,0 +1,85 @@ +import SwiftUI +import SafariServices +import Env +import DesignSystem + +extension View { + func withSafariRouteur() -> some View { + modifier(SafariRouteur()) + } +} + +private struct SafariRouteur: ViewModifier { + @EnvironmentObject private var theme: Theme + @EnvironmentObject private var preferences: UserPreferences + @EnvironmentObject private var routeurPath: RouterPath + + @State private var safari: SFSafariViewController? + + func body(content: Content) -> some View { + content + .environment(\.openURL, OpenURLAction { url in + routeurPath.handle(url: url) + }) + .onAppear { + routeurPath.urlHandler = { url in + guard preferences.preferredBrowser == .inAppSafari else { return .systemAction } + // SFSafariViewController only supports initial URLs with http:// or https:// schemes. + guard let scheme = url.scheme, ["https", "http"].contains(scheme.lowercased()) else { + return .systemAction + } + + let safari = SFSafariViewController(url: url) + safari.preferredBarTintColor = UIColor(theme.primaryBackgroundColor) + safari.preferredControlTintColor = UIColor(theme.tintColor) + + self.safari = safari + return .handled + } + } + .background { + SafariPresenter(safari: safari) + } + } + + struct SafariPresenter: UIViewRepresentable { + var safari: SFSafariViewController? + + func makeUIView(context: Context) -> UIView { + let view = UIView(frame: .zero) + view.isHidden = true + view.isUserInteractionEnabled = false + return view + } + + func updateUIView(_ uiView: UIView, context: Context) { + guard let safari = safari, let viewController = uiView.findTopViewController() else { return } + viewController.present(safari, animated: true) + } + } +} + +private extension UIView { + func findTopViewController() -> UIViewController? { + if let nextResponder = self.next as? UIViewController { + return nextResponder.topViewController() + } else if let nextResponder = self.next as? UIView { + return nextResponder.findTopViewController() + } else { + return nil + } + } +} + +private extension UIViewController { + func topViewController() -> UIViewController? { + if let nvc = self as? UINavigationController { + return nvc.visibleViewController?.topViewController() + } else if let tbc = self as? UITabBarController, let selected = tbc.selectedViewController { + return selected.topViewController() + } else if let presented = self.presentedViewController { + return presented.topViewController() + } + return self + } +} diff --git a/IceCubesApp/App/Tabs/ExploreTab.swift b/IceCubesApp/App/Tabs/ExploreTab.swift index 54fb1804..0fe5ae49 100644 --- a/IceCubesApp/App/Tabs/ExploreTab.swift +++ b/IceCubesApp/App/Tabs/ExploreTab.swift @@ -24,6 +24,7 @@ struct ExploreTab: View { } } } + .withSafariRouteur() .environmentObject(routeurPath) .onChange(of: $popToRootTab.wrappedValue) { popToRootTab in if popToRootTab == .explore { diff --git a/IceCubesApp/App/Tabs/MessagesTab.swift b/IceCubesApp/App/Tabs/MessagesTab.swift index 7d16277d..a51a3e03 100644 --- a/IceCubesApp/App/Tabs/MessagesTab.swift +++ b/IceCubesApp/App/Tabs/MessagesTab.swift @@ -38,6 +38,7 @@ struct MessagesTab: View { routeurPath.client = client watcher.unreadMessagesCount = 0 } + .withSafariRouteur() .environmentObject(routeurPath) } } diff --git a/IceCubesApp/App/Tabs/NotificationTab.swift b/IceCubesApp/App/Tabs/NotificationTab.swift index edf58e2a..50eebd56 100644 --- a/IceCubesApp/App/Tabs/NotificationTab.swift +++ b/IceCubesApp/App/Tabs/NotificationTab.swift @@ -28,6 +28,7 @@ struct NotificationsTab: View { routeurPath.client = client watcher.unreadNotificationsCount = 0 } + .withSafariRouteur() .environmentObject(routeurPath) .onChange(of: $popToRootTab.wrappedValue) { popToRootTab in if popToRootTab == .notifications { diff --git a/IceCubesApp/App/Tabs/Settings/AddAccountsView.swift b/IceCubesApp/App/Tabs/Settings/AddAccountsView.swift index 09fe5f2a..9614618f 100644 --- a/IceCubesApp/App/Tabs/Settings/AddAccountsView.swift +++ b/IceCubesApp/App/Tabs/Settings/AddAccountsView.swift @@ -7,7 +7,6 @@ import NukeUI import Shimmer struct AddAccountView: View { - @Environment(\.openURL) private var openURL @Environment(\.dismiss) private var dismiss @EnvironmentObject private var appAccountsManager: AppAccountsManager @@ -128,7 +127,7 @@ struct AddAccountView: View { do { signInClient = .init(server: instanceName) if let oauthURL = try await signInClient?.oauthURL() { - openURL(oauthURL) + await UIApplication.shared.open(oauthURL) } else { isSigninIn = false } diff --git a/IceCubesApp/App/Tabs/Settings/SettingsTab.swift b/IceCubesApp/App/Tabs/Settings/SettingsTab.swift index 1e2920d1..a62e44e4 100644 --- a/IceCubesApp/App/Tabs/Settings/SettingsTab.swift +++ b/IceCubesApp/App/Tabs/Settings/SettingsTab.swift @@ -41,6 +41,8 @@ struct SettingsTabs: View { await currentInstance.fetchCurrentInstance() } } + .withSafariRouteur() + .environmentObject(routeurPath) } private var accountsSection: some View { @@ -88,6 +90,18 @@ struct SettingsTabs: View { NavigationLink(destination: remoteLocalTimelinesView) { Label("Remote Local Timelines", systemImage: "dot.radiowaves.right") } + Picker(selection: $preferences.preferredBrowser) { + ForEach(PreferredBrowser.allCases, id: \.rawValue) { browser in + switch browser { + case .inAppSafari: + Text("In-App Safari").tag(browser) + case .safari: + Text("System Safari").tag(browser) + } + } + } label: { + Label("Browser", systemImage: "network") + } } .listRowBackground(theme.primaryBackgroundColor) } @@ -107,10 +121,10 @@ struct SettingsTabs: View { } } - Label("Source (Github link)", systemImage: "link") - .onTapGesture { - UIApplication.shared.open(URL(string: "https://github.com/Dimillian/IceCubesApp")!) - } + Link(destination: URL(string: "https://github.com/Dimillian/IceCubesApp")!) { + Label("Source (Github link)", systemImage: "link") + } + .tint(theme.labelColor) NavigationLink(destination: SupportAppView()) { Label("Support the app", systemImage: "wand.and.stars") diff --git a/IceCubesApp/App/Tabs/Timeline/TimelineTab.swift b/IceCubesApp/App/Tabs/Timeline/TimelineTab.swift index b288eec9..07684204 100644 --- a/IceCubesApp/App/Tabs/Timeline/TimelineTab.swift +++ b/IceCubesApp/App/Tabs/Timeline/TimelineTab.swift @@ -56,6 +56,7 @@ struct TimelineTab: View { .onChange(of: currentAccount.account?.id) { _ in routeurPath.path = [] } + .withSafariRouteur() .environmentObject(routeurPath) } diff --git a/Packages/Env/Sources/Env/PreferredBrowser.swift b/Packages/Env/Sources/Env/PreferredBrowser.swift new file mode 100644 index 00000000..87bba644 --- /dev/null +++ b/Packages/Env/Sources/Env/PreferredBrowser.swift @@ -0,0 +1,6 @@ +import Foundation + +public enum PreferredBrowser: Int, CaseIterable { + case inAppSafari + case safari +} diff --git a/Packages/Env/Sources/Env/Routeur.swift b/Packages/Env/Sources/Env/Routeur.swift index 96e35d87..3e09578f 100644 --- a/Packages/Env/Sources/Env/Routeur.swift +++ b/Packages/Env/Sources/Env/Routeur.swift @@ -46,6 +46,7 @@ public enum SheetDestinations: Identifiable { @MainActor public class RouterPath: ObservableObject { public var client: Client? + public var urlHandler: ((URL) -> OpenURLAction.Result)? @Published public var path: [RouteurDestinations] = [] @Published public var presentedSheet: SheetDestinations? @@ -73,7 +74,7 @@ public class RouterPath: ObservableObject { } return .handled } - return .systemAction + return urlHandler?(url) ?? .systemAction } public func handle(url: URL) -> OpenURLAction.Result { @@ -88,14 +89,14 @@ public class RouterPath: ObservableObject { } return .handled } - return .systemAction + return urlHandler?(url) ?? .systemAction } public func navigateToAccountFrom(acct: String, url: URL) async { guard let client else { return } Task { let results: SearchResults? = try? await client.get(endpoint: Search.search(query: acct, - type: "accounts", + type: "accounts", offset: nil, following: nil), forceVersion: .v2) @@ -111,7 +112,7 @@ public class RouterPath: ObservableObject { guard let client else { return } Task { let results: SearchResults? = try? await client.get(endpoint: Search.search(query: url.absoluteString, - type: "accounts", + type: "accounts", offset: nil, following: nil), forceVersion: .v2) diff --git a/Packages/Env/Sources/Env/UserPreferences.swift b/Packages/Env/Sources/Env/UserPreferences.swift index 84416f18..502db807 100644 --- a/Packages/Env/Sources/Env/UserPreferences.swift +++ b/Packages/Env/Sources/Env/UserPreferences.swift @@ -3,6 +3,7 @@ import Foundation public class UserPreferences: ObservableObject { @AppStorage("remote_local_timeline") public var remoteLocalTimelines: [String] = [] + @AppStorage("preferred_browser") public var preferredBrowser: PreferredBrowser = .inAppSafari public init() { } } diff --git a/Packages/Status/Sources/Status/Row/StatusRowContextMenu.swift b/Packages/Status/Sources/Status/Row/StatusRowContextMenu.swift index 0f255fe1..0b1f4713 100644 --- a/Packages/Status/Sources/Status/Row/StatusRowContextMenu.swift +++ b/Packages/Status/Sources/Status/Row/StatusRowContextMenu.swift @@ -5,6 +5,9 @@ import Env struct StatusRowContextMenu: View { @EnvironmentObject private var account: CurrentAccount @EnvironmentObject private var routeurPath: RouterPath + + @Environment(\.openURL) var openURL + @ObservedObject var viewModel: StatusRowViewModel var body: some View { @@ -47,7 +50,7 @@ struct StatusRowContextMenu: View { } if let url = viewModel.status.reblog?.url ?? viewModel.status.url { - Button { UIApplication.shared.open(url) } label: { + Button { openURL(url) } label: { Label("View in Browser", systemImage: "safari") } }