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:
parent
c304b3eefe
commit
76d7d23379
|
@ -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 */,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -24,6 +24,7 @@ struct ExploreTab: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.withSafariRouteur()
|
||||
.environmentObject(routeurPath)
|
||||
.onChange(of: $popToRootTab.wrappedValue) { popToRootTab in
|
||||
if popToRootTab == .explore {
|
||||
|
|
|
@ -38,6 +38,7 @@ struct MessagesTab: View {
|
|||
routeurPath.client = client
|
||||
watcher.unreadMessagesCount = 0
|
||||
}
|
||||
.withSafariRouteur()
|
||||
.environmentObject(routeurPath)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -56,6 +56,7 @@ struct TimelineTab: View {
|
|||
.onChange(of: currentAccount.account?.id) { _ in
|
||||
routeurPath.path = []
|
||||
}
|
||||
.withSafariRouteur()
|
||||
.environmentObject(routeurPath)
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
import Foundation
|
||||
|
||||
public enum PreferredBrowser: Int, CaseIterable {
|
||||
case inAppSafari
|
||||
case safari
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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() { }
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue