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
This commit is contained in:
David Walter 2023-01-08 19:56:16 +01:00 committed by GitHub
parent c304b3eefe
commit 76d7d23379
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 128 additions and 11 deletions

View File

@ -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 = "<group>"; };
9F24EEB729360C330042359D /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
9F29553D292B67B600E0E81B /* Network */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Network; path = Packages/Network; sourceTree = "<group>"; };
9F29553E292B6AF600E0E81B /* Timeline */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Timeline; path = Packages/Timeline; sourceTree = "<group>"; };
@ -184,6 +186,7 @@
9FAE4AC9293783A200772766 /* Tabs */,
9FBFE63C292A715500C250E9 /* IceCubesApp.swift */,
9F398AA52935FE8A00A889F2 /* AppRouteur.swift */,
639CDF9B296AC82F00C35E58 /* SafariRouteur.swift */,
);
path = App;
sourceTree = "<group>";
@ -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 */,

View File

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

View File

@ -24,6 +24,7 @@ struct ExploreTab: View {
}
}
}
.withSafariRouteur()
.environmentObject(routeurPath)
.onChange(of: $popToRootTab.wrappedValue) { popToRootTab in
if popToRootTab == .explore {

View File

@ -38,6 +38,7 @@ struct MessagesTab: View {
routeurPath.client = client
watcher.unreadMessagesCount = 0
}
.withSafariRouteur()
.environmentObject(routeurPath)
}
}

View File

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

View File

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

View File

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

View File

@ -56,6 +56,7 @@ struct TimelineTab: View {
.onChange(of: currentAccount.account?.id) { _ in
routeurPath.path = []
}
.withSafariRouteur()
.environmentObject(routeurPath)
}

View File

@ -0,0 +1,6 @@
import Foundation
public enum PreferredBrowser: Int, CaseIterable {
case inAppSafari
case safari
}

View File

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

View File

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

View File

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