From cfa7f031eadb6698eef45a4c745bf1771dfeabda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Rutkowski?= Date: Thu, 15 Jun 2023 13:27:11 +0200 Subject: [PATCH 1/8] [iOS 17] Fix navigation to safari not working when keyboard is visible iOS 17 adds SwiftUI.UIKitKeyPressResponder to responder chain between inserted UIView and first UIViewController which is neither an UIView or UIViewController. This causes the search for UIViewController to fail. --- Demo/iOS/Views/RootView.swift | 8 ++++++-- .../BetterSafariView/Shared/UIView+viewController.swift | 6 +++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/Demo/iOS/Views/RootView.swift b/Demo/iOS/Views/RootView.swift index 7650652..baba374 100644 --- a/Demo/iOS/Views/RootView.swift +++ b/Demo/iOS/Views/RootView.swift @@ -16,7 +16,9 @@ struct RootView: View { @State private var showingWebAuthenticationSessionOptionsForm = false @State private var webAuthenticationSessionCallbackURL: URL? = nil - + + @State private var text = "" + var body: some View { NavigationView { List { @@ -73,7 +75,9 @@ struct RootView: View { WebAuthenticationSessionOptionsForm(options: $webAuthenticationSessionOptions) } } - + + TextField("Text field", text: $text) + Section(header: Text("NaiveSafariView" + "\n" + "(Just for comparison. Do not use in practice.)").textCase(nil)) { Button(action: { showingNaiveSafariViewSheet = true }) { HStack { diff --git a/Sources/BetterSafariView/Shared/UIView+viewController.swift b/Sources/BetterSafariView/Shared/UIView+viewController.swift index 80aa67c..620f185 100644 --- a/Sources/BetterSafariView/Shared/UIView+viewController.swift +++ b/Sources/BetterSafariView/Shared/UIView+viewController.swift @@ -2,15 +2,15 @@ import UIKit -extension UIView { - +extension UIResponder { + /// The receiver’s view controller, or `nil` if it has none. /// /// This property is `nil` if the view has not yet been added to a view controller. var viewController: UIViewController? { if let nextResponder = self.next as? UIViewController { return nextResponder - } else if let nextResponder = self.next as? UIView { + } else if let nextResponder = self.next { return nextResponder.viewController } else { return nil From 0ff5cb35f6a20836f791192159429d3dc914043f Mon Sep 17 00:00:00 2001 From: Songjiyeon Date: Tue, 24 Oct 2023 17:49:00 +0900 Subject: [PATCH 2/8] =?UTF-8?q?=F0=9F=90=9B=20Present=20view=20on=20keyWin?= =?UTF-8?q?dow=20over=20iOS=2017?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SafariView/SafariViewPresenter.swift | 35 ++++++++++++------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/Sources/BetterSafariView/SafariView/SafariViewPresenter.swift b/Sources/BetterSafariView/SafariView/SafariViewPresenter.swift index ec98958..255529c 100644 --- a/Sources/BetterSafariView/SafariView/SafariViewPresenter.swift +++ b/Sources/BetterSafariView/SafariView/SafariViewPresenter.swift @@ -79,20 +79,31 @@ extension SafariViewPresenter { let safariViewController = SFSafariViewController(url: representation.url, configuration: representation.configuration) safariViewController.delegate = self representation.applyModification(to: safariViewController) - - // Present a Safari view controller from the `viewController` of `UIViewRepresentable`, instead of `UIViewControllerRepresentable`. - // This fixes an issue where the Safari view controller is not presented properly - // when the `UIViewControllerRepresentable` is detached from the root view controller (e.g. `UIViewController` contained in `UITableViewCell`) - // while allowing it to be presented even on the modal sheets. - // Thanks to: Bohdan Hernandez Navia (@boherna) - guard let presentingViewController = uiView.viewController else { - self.resetItemBinding() + + if #available(iOS 17.0, *) { + if var topController = UIApplication.shared.windows.filter(\.isKeyWindow).first?.rootViewController { + while let presentedViewController = topController.presentedViewController { + topController = presentedViewController + } + topController.present(safariViewController, animated: true, completion: nil) + } + self.safariViewController = safariViewController return + } else { + // Present a Safari view controller from the `viewController` of `UIViewRepresentable`, instead of `UIViewControllerRepresentable`. + // This fixes an issue where the Safari view controller is not presented properly + // when the `UIViewControllerRepresentable` is detached from the root view controller (e.g. `UIViewController` contained in `UITableViewCell`) + // while allowing it to be presented even on the modal sheets. + // Thanks to: Bohdan Hernandez Navia (@boherna) + guard let presentingViewController = uiView.viewController else { + self.resetItemBinding() + return + } + + presentingViewController.present(safariViewController, animated: true) + + self.safariViewController = safariViewController } - - presentingViewController.present(safariViewController, animated: true) - - self.safariViewController = safariViewController } private func updateSafariViewController(with item: Item) { From 10357f5ad70d7c94a42dca6e49aa7d8071ae7627 Mon Sep 17 00:00:00 2001 From: Dongkyu Kim Date: Mon, 30 Oct 2023 00:42:15 +0900 Subject: [PATCH 3/8] Add `UIWindow+farthestPresentedViewController.swift` --- .../UIWindow+farthestPresentedViewController.swift | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 Sources/BetterSafariView/Shared/UIWindow+farthestPresentedViewController.swift diff --git a/Sources/BetterSafariView/Shared/UIWindow+farthestPresentedViewController.swift b/Sources/BetterSafariView/Shared/UIWindow+farthestPresentedViewController.swift new file mode 100644 index 0000000..3b179ae --- /dev/null +++ b/Sources/BetterSafariView/Shared/UIWindow+farthestPresentedViewController.swift @@ -0,0 +1,14 @@ +#if os(iOS) + +import UIKit + +extension UIWindow { + + /// The view controller that was presented modally on top of the window. + var farthestPresentedViewController: UIViewController? { + guard let rootViewController = rootViewController else { return nil } + return Array(sequence(first: rootViewController, next: \.presentedViewController)).last + } +} + +#endif From 6ff7a13bbbf05eec91bfbbcae2717b2620d87641 Mon Sep 17 00:00:00 2001 From: Dongkyu Kim Date: Mon, 30 Oct 2023 00:43:05 +0900 Subject: [PATCH 4/8] Use `window.farthestPresentedViewController` on presentation --- .../SafariView/SafariViewPresenter.swift | 38 ++++++++----------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/Sources/BetterSafariView/SafariView/SafariViewPresenter.swift b/Sources/BetterSafariView/SafariView/SafariViewPresenter.swift index 255529c..bc61799 100644 --- a/Sources/BetterSafariView/SafariView/SafariViewPresenter.swift +++ b/Sources/BetterSafariView/SafariView/SafariViewPresenter.swift @@ -3,6 +3,10 @@ import SwiftUI import SafariServices +// `SafariViewPresenter` conforms `UIViewRepresentable` instead of `UIViewControllerRepresentable`. +// This fixes an issue where the Safari view controller is not presented properly +// when the `UIViewControllerRepresentable` is detached from the root view controller +// (e.g. `UIViewController` contained in `UITableViewCell`). struct SafariViewPresenter: UIViewRepresentable { // MARK: Representation @@ -80,30 +84,20 @@ extension SafariViewPresenter { safariViewController.delegate = self representation.applyModification(to: safariViewController) - if #available(iOS 17.0, *) { - if var topController = UIApplication.shared.windows.filter(\.isKeyWindow).first?.rootViewController { - while let presentedViewController = topController.presentedViewController { - topController = presentedViewController - } - topController.present(safariViewController, animated: true, completion: nil) - } - self.safariViewController = safariViewController + // Presents a Safari view controller from the farthest `presentedViewController` of `UIWindow`. + // (same approach when presenting `UIAlertController`) + guard let presentingViewController = uiView.window?.farthestPresentedViewController else { + assertionFailure( + "Cannot find the view controller to present from." + + " This happens when a 'SafariViewPresenter' is detached from the window, or the window doesn't have 'rootViewController.'" + ) + self.resetItemBinding() return - } else { - // Present a Safari view controller from the `viewController` of `UIViewRepresentable`, instead of `UIViewControllerRepresentable`. - // This fixes an issue where the Safari view controller is not presented properly - // when the `UIViewControllerRepresentable` is detached from the root view controller (e.g. `UIViewController` contained in `UITableViewCell`) - // while allowing it to be presented even on the modal sheets. - // Thanks to: Bohdan Hernandez Navia (@boherna) - guard let presentingViewController = uiView.viewController else { - self.resetItemBinding() - return - } - - presentingViewController.present(safariViewController, animated: true) - - self.safariViewController = safariViewController } + + presentingViewController.present(safariViewController, animated: true) + + self.safariViewController = safariViewController } private func updateSafariViewController(with item: Item) { From 5a6bb73820387edc33794eb9262975063c0c5c1c Mon Sep 17 00:00:00 2001 From: Dongkyu Kim Date: Mon, 30 Oct 2023 01:06:45 +0900 Subject: [PATCH 5/8] Remove `UIView+viewController.swift` --- .../Shared/UIView+viewController.swift | 21 ------------------- .../WebAuthenticationPresenter.swift | 2 +- 2 files changed, 1 insertion(+), 22 deletions(-) delete mode 100644 Sources/BetterSafariView/Shared/UIView+viewController.swift diff --git a/Sources/BetterSafariView/Shared/UIView+viewController.swift b/Sources/BetterSafariView/Shared/UIView+viewController.swift deleted file mode 100644 index 620f185..0000000 --- a/Sources/BetterSafariView/Shared/UIView+viewController.swift +++ /dev/null @@ -1,21 +0,0 @@ -#if os(iOS) - -import UIKit - -extension UIResponder { - - /// The receiver’s view controller, or `nil` if it has none. - /// - /// This property is `nil` if the view has not yet been added to a view controller. - var viewController: UIViewController? { - if let nextResponder = self.next as? UIViewController { - return nextResponder - } else if let nextResponder = self.next { - return nextResponder.viewController - } else { - return nil - } - } -} - -#endif diff --git a/Sources/BetterSafariView/WebAuthenticationSession/WebAuthenticationPresenter.swift b/Sources/BetterSafariView/WebAuthenticationSession/WebAuthenticationPresenter.swift index bbe7552..90f87bd 100644 --- a/Sources/BetterSafariView/WebAuthenticationSession/WebAuthenticationPresenter.swift +++ b/Sources/BetterSafariView/WebAuthenticationSession/WebAuthenticationPresenter.swift @@ -199,7 +199,7 @@ extension WebAuthenticationPresenter { @available(iOS, introduced: 13.0, deprecated: 14.0) func setInteractiveDismissalDelegateIfPossible() { - guard let safariViewController = view.viewController?.presentedViewController as? SFSafariViewController else { + guard let safariViewController = view.window?.farthestPresentedViewController as? SFSafariViewController else { return } safariViewController.presentationController?.delegate = interactiveDismissalDelegate From 4520bd11286d0ef6bdcf891661b4c6b313efa8b5 Mon Sep 17 00:00:00 2001 From: Dongkyu Kim Date: Mon, 30 Oct 2023 00:46:43 +0900 Subject: [PATCH 6/8] Remove `TextField` from `RootView` --- Demo/iOS/Views/RootView.swift | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Demo/iOS/Views/RootView.swift b/Demo/iOS/Views/RootView.swift index baba374..a687ce1 100644 --- a/Demo/iOS/Views/RootView.swift +++ b/Demo/iOS/Views/RootView.swift @@ -16,9 +16,7 @@ struct RootView: View { @State private var showingWebAuthenticationSessionOptionsForm = false @State private var webAuthenticationSessionCallbackURL: URL? = nil - - @State private var text = "" - + var body: some View { NavigationView { List { @@ -76,8 +74,6 @@ struct RootView: View { } } - TextField("Text field", text: $text) - Section(header: Text("NaiveSafariView" + "\n" + "(Just for comparison. Do not use in practice.)").textCase(nil)) { Button(action: { showingNaiveSafariViewSheet = true }) { HStack { From d4d33ef1a5b0e0e23d9d3711929b8d2e4064ca24 Mon Sep 17 00:00:00 2001 From: Dongkyu Kim Date: Mon, 30 Oct 2023 02:05:34 +0900 Subject: [PATCH 7/8] Update CHANGELOG.md --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ee5e1a8..ead4d9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## [v2.4.2](https://github.com/stleamist/BetterSafariView/releases/tag/v2.4.2) (2023-10-30) +### Fixed +- Fixed an issue where the `SafariViewPresenter` fails to find a view controller to presented from (#41 & #46). Thanks, @Tunous and @SongJiyeon! + ## [v2.4.1](https://github.com/stleamist/BetterSafariView/releases/tag/v2.4.1) (2023-01-15) ### Fixed - Fixed an issue where the `WebAuthenticationPresenter` fails to find its presentation anchor (#22). Thanks, @kevvdevv, @exentrich, and @ldstreet! From 84a63e760f21ea42a875b24ead2d50de77436039 Mon Sep 17 00:00:00 2001 From: Dongkyu Kim Date: Mon, 30 Oct 2023 00:51:11 +0900 Subject: [PATCH 8/8] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1f1b3f1..2813d90 100644 --- a/README.md +++ b/README.md @@ -285,7 +285,7 @@ func prefersEphemeralWebBrowserSession(_ prefersEphemeralWebBrowserSession: Bool Add the following line to the `dependencies` in your [`Package.swift`](https://developer.apple.com/documentation/swift_packages/package) file: ```swift -.package(url: "https://github.com/stleamist/BetterSafariView.git", .upToNextMajor(from: "2.4.1")) +.package(url: "https://github.com/stleamist/BetterSafariView.git", .upToNextMajor(from: "2.4.2")) ``` Next, add `BetterSafariView` as a dependency for your targets: @@ -304,7 +304,7 @@ import PackageDescription let package = Package( name: "MyPackage", dependencies: [ - .package(url: "https://github.com/stleamist/BetterSafariView.git", .upToNextMajor(from: "2.4.1")) + .package(url: "https://github.com/stleamist/BetterSafariView.git", .upToNextMajor(from: "2.4.2")) ], targets: [ .target(name: "MyTarget", dependencies: ["BetterSafariView"])