diff --git a/CHANGELOG.md b/CHANGELOG.md index 09908f3..5f3cd81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [v2.4.0](https://github.com/stleamist/BetterSafariView/releases/tag/v2.4.0) (2020-09-25) +### Changed +- `SafariViewPresenter` and `WebAuthenticationPresenter` now conforms to `UIViewRepresentable`, instead of `UIViewControllerRepresentable`. + +### Fixed +- Fixed an issue where the `SafariView` is not presented on the multi-layered modal sheets (#20). Thanks @twodayslate! + ## [v2.3.1](https://github.com/stleamist/BetterSafariView/releases/tag/v2.3.1) (2020-01-20) ### Fixed - Fixed an issue where the `SafariView` is not presented on the modal sheets (#9). Thanks @boherna! diff --git a/Demo/BetterSafariViewDemo.xcodeproj/xcshareddata/xcschemes/BetterSafariViewDemo (watchOS).xcscheme b/Demo/BetterSafariViewDemo.xcodeproj/xcshareddata/xcschemes/BetterSafariViewDemo (watchOS).xcscheme index 3023fee..475c138 100644 --- a/Demo/BetterSafariViewDemo.xcodeproj/xcshareddata/xcschemes/BetterSafariViewDemo (watchOS).xcscheme +++ b/Demo/BetterSafariViewDemo.xcodeproj/xcshareddata/xcschemes/BetterSafariViewDemo (watchOS).xcscheme @@ -40,10 +40,8 @@ debugDocumentVersioning = "YES" debugServiceExtension = "internal" allowLocationSimulation = "YES"> - + - + - + - - - - - + diff --git a/README.md b/README.md index 8bd990f..ab22bb1 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.3.1")) +.package(url: "https://github.com/stleamist/BetterSafariView.git", .upToNextMajor(from: "2.4.0")) ``` 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.3.1")) + .package(url: "https://github.com/stleamist/BetterSafariView.git", .upToNextMajor(from: "2.4.0")) ], targets: [ .target(name: "MyTarget", dependencies: ["BetterSafariView"]) diff --git a/Sources/BetterSafariView/SafariView/SafariViewPresenter.swift b/Sources/BetterSafariView/SafariView/SafariViewPresenter.swift index 8de0043..ec98958 100644 --- a/Sources/BetterSafariView/SafariView/SafariViewPresenter.swift +++ b/Sources/BetterSafariView/SafariView/SafariViewPresenter.swift @@ -3,7 +3,7 @@ import SwiftUI import SafariServices -struct SafariViewPresenter: UIViewControllerRepresentable { +struct SafariViewPresenter: UIViewRepresentable { // MARK: Representation @@ -11,18 +11,17 @@ struct SafariViewPresenter: UIViewControllerRepresentable { var onDismiss: (() -> Void)? = nil var representationBuilder: (Item) -> SafariView - // MARK: UIViewControllerRepresentable + // MARK: UIViewRepresentable func makeCoordinator() -> Coordinator { return Coordinator(parent: self) } - func makeUIViewController(context: Context) -> UIViewController { - return context.coordinator.uiViewController + func makeUIView(context: Context) -> UIView { + return context.coordinator.uiView } - func updateUIViewController(_ uiViewController: UIViewController, context: Context) { - + func updateUIView(_ uiView: UIView, context: Context) { // Keep the coordinator updated with a new presenter struct. context.coordinator.parent = self context.coordinator.item = item @@ -43,7 +42,8 @@ extension SafariViewPresenter { // MARK: View Controller Holding - let uiViewController = UIViewController() + let uiView = UIView() + private weak var safariViewController: SFSafariViewController? // MARK: Item Handling @@ -75,25 +75,28 @@ extension SafariViewPresenter { // MARK: Presentation Handlers private func presentSafariViewController(with item: Item) { - guard uiViewController.presentedViewController == nil else { - return - } - let representation = parent.representationBuilder(item) let safariViewController = SFSafariViewController(url: representation.url, configuration: representation.configuration) safariViewController.delegate = self representation.applyModification(to: safariViewController) - // There is a problem that page loading and parallel push animation are not working when a modifier is attached to the view in a `List`. - // As a workaround, use a `rootViewController` of the `window` for presenting. - // (Unlike the other view controllers, a view controller hosted by a cell doesn't have a parent, but has the same window.) - var presentingViewController = uiViewController.view.window?.rootViewController - presentingViewController = presentingViewController?.presentedViewController ?? presentingViewController ?? uiViewController - presentingViewController?.present(safariViewController, animated: true) + // 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 } private func updateSafariViewController(with item: Item) { - guard let safariViewController = uiViewController.presentedViewController as? SFSafariViewController else { + guard let safariViewController = safariViewController else { return } let representation = parent.representationBuilder(item) @@ -101,28 +104,25 @@ extension SafariViewPresenter { } private func dismissSafariViewController(completion: (() -> Void)? = nil) { - let dismissCompletion: () -> Void = { - self.handleDismissalWithoutResettingItemBinding() - completion?() - } - - guard uiViewController.presentedViewController != nil else { - dismissCompletion() + guard let safariViewController = safariViewController else { return } - // Check if the `uiViewController` is a instance of the `SFSafariViewController` - // to prevent other controllers presented by the container view from being dismissed unintentionally. - guard let safariViewController = uiViewController.presentedViewController as? SFSafariViewController else { - return + safariViewController.dismiss(animated: true) { + self.handleDismissal() + completion?() } - safariViewController.dismiss(animated: true, completion: dismissCompletion) } // MARK: Dismissal Handlers + // Used when the `viewController` of `uiView` does not exist during the preparation of presentation. + private func resetItemBinding() { + parent.item = nil + } + // Used when the Safari view controller is finished by an item change during view update. - private func handleDismissalWithoutResettingItemBinding() { + private func handleDismissal() { parent.onDismiss?() } diff --git a/Sources/BetterSafariView/Shared.swift b/Sources/BetterSafariView/Shared/Identifiables.swift similarity index 100% rename from Sources/BetterSafariView/Shared.swift rename to Sources/BetterSafariView/Shared/Identifiables.swift diff --git a/Sources/BetterSafariView/Shared/UIView+viewController.swift b/Sources/BetterSafariView/Shared/UIView+viewController.swift new file mode 100644 index 0000000..80aa67c --- /dev/null +++ b/Sources/BetterSafariView/Shared/UIView+viewController.swift @@ -0,0 +1,21 @@ +#if os(iOS) + +import UIKit + +extension UIView { + + /// 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 { + return nextResponder.viewController + } else { + return nil + } + } +} + +#endif diff --git a/Sources/BetterSafariView/WebAuthenticationSession/WebAuthenticationPresenter.swift b/Sources/BetterSafariView/WebAuthenticationSession/WebAuthenticationPresenter.swift index 1975e33..ec664c6 100644 --- a/Sources/BetterSafariView/WebAuthenticationSession/WebAuthenticationPresenter.swift +++ b/Sources/BetterSafariView/WebAuthenticationSession/WebAuthenticationPresenter.swift @@ -7,29 +7,26 @@ import SafariServices #endif #if os(iOS) -typealias ConcreteViewController = UIViewController -typealias ViewController = UIViewController -typealias ViewControllerRepresentable = UIViewControllerRepresentable +typealias ViewType = UIView +typealias ViewRepresentableType = UIViewRepresentable #elseif os(macOS) -typealias ConcreteViewController = NSTabViewController -typealias ViewController = NSViewController -typealias ViewControllerRepresentable = NSViewControllerRepresentable +typealias ViewType = NSView +typealias ViewRepresentableType = NSViewRepresentable #elseif os(watchOS) // Use `WKInterfaceInlineMovie` as a concrete interface objct type, // since there is no public initializer for `WKInterfaceObject`. -typealias ConcreteViewController = WKInterfaceInlineMovie -typealias ViewController = WKInterfaceObject -typealias ViewControllerRepresentable = WKInterfaceObjectRepresentable +typealias ViewType = WKInterfaceInlineMovie +typealias ViewRepresentableType = WKInterfaceObjectRepresentable #endif -struct WebAuthenticationPresenter: ViewControllerRepresentable { +struct WebAuthenticationPresenter: ViewRepresentableType { // MARK: Representation @Binding var item: Item? var representationBuilder: (Item) -> WebAuthenticationSession - // MARK: ViewControllerRepresentable + // MARK: ViewRepresentable func makeCoordinator() -> Coordinator { return Coordinator(parent: self) @@ -37,16 +34,16 @@ struct WebAuthenticationPresenter: ViewControllerRepresentab #if os(iOS) - func makeUIViewController(context: Context) -> ViewController { - return makeViewController(context: context) + func makeUIView(context: Context) -> ViewType { + return makeView(context: context) } - func updateUIViewController(_ uiViewController: ViewController, context: Context) { + func updateUIView(_ uiView: ViewType, context: Context) { - updateViewController(uiViewController, context: context) + updateView(uiView, context: context) // To set a delegate for the presentation controller of an `SFAuthenticationViewController` as soon as possible, - // check the view controller presented by `uiViewController` then set it as a delegate on every view updates. + // check the view controller presented by `view.viewController` then set it as a delegate on every view updates. // INFO: `SFAuthenticationViewController` is a private subclass of `SFSafariViewController`. guard #available(iOS 14.0, *) else { context.coordinator.setInteractiveDismissalDelegateIfPossible() @@ -56,31 +53,31 @@ struct WebAuthenticationPresenter: ViewControllerRepresentab #elseif os(macOS) - func makeNSViewController(context: Context) -> ViewController { - return makeViewController(context: context) + func makeNSView(context: Context) -> ViewType { + return makeView(context: context) } - func updateNSViewController(_ nsViewController: ViewController, context: Context) { - updateViewController(nsViewController, context: context) + func updateNSView(_ nsView: ViewType, context: Context) { + updateView(nsView, context: context) } #elseif os(watchOS) - func makeWKInterfaceObject(context: Context) -> ViewController { - return makeViewController(context: context) + func makeWKInterfaceObject(context: Context) -> ViewType { + return makeView(context: context) } - func updateWKInterfaceObject(_ wkInterfaceObject: ViewController, context: Context) { - updateViewController(wkInterfaceObject, context: context) + func updateWKInterfaceObject(_ wkInterfaceObject: ViewType, context: Context) { + updateView(wkInterfaceObject, context: context) } #endif - private func makeViewController(context: Context) -> ViewController { - return context.coordinator.viewController + private func makeView(context: Context) -> ViewType { + return context.coordinator.view } - private func updateViewController(_ viewController: ViewController, context: Context) { + private func updateView(_ view: ViewType, context: Context) { // Keep the coordinator updated with a new presenter struct. context.coordinator.parent = self context.coordinator.item = item @@ -101,8 +98,8 @@ extension WebAuthenticationPresenter { // MARK: View Controller Holding - let viewController = ConcreteViewController() - private var session: ASWebAuthenticationSession? + let view = ViewType() + private weak var session: ASWebAuthenticationSession? // MARK: Item Handling @@ -146,13 +143,13 @@ extension WebAuthenticationPresenter { representation.applyModification(to: session) - self.session = session session.start() + + self.session = session } private func cancelWebAuthenticationSession() { session?.cancel() - session = nil } // MARK: Dismissal Handlers @@ -173,7 +170,7 @@ extension WebAuthenticationPresenter { class PresentationContextProvider: NSObject, ASWebAuthenticationPresentationContextProviding { - weak var coordinator: WebAuthenticationPresenter.Coordinator? + unowned var coordinator: WebAuthenticationPresenter.Coordinator init(coordinator: WebAuthenticationPresenter.Coordinator) { self.coordinator = coordinator @@ -182,7 +179,7 @@ extension WebAuthenticationPresenter { // MARK: ASWebAuthenticationPresentationContextProviding func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { - return coordinator!.viewController.view.window! + return coordinator.view.window! } } @@ -202,7 +199,7 @@ extension WebAuthenticationPresenter { @available(iOS, introduced: 13.0, deprecated: 14.0) func setInteractiveDismissalDelegateIfPossible() { - guard let safariViewController = viewController.presentedViewController as? SFSafariViewController else { + guard let safariViewController = view.viewController?.presentedViewController as? SFSafariViewController else { return } safariViewController.presentationController?.delegate = interactiveDismissalDelegate