Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add generic item presentation #6

Merged
merged 1 commit into from
Jul 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .swiftformat
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
--enable isEmpty
--disable blankLinesAtEndOfScope, blankLinesAtStartOfScope, redundantNilInit, unusedArguments, redundantParens, wrapMultilineStatementBraces, trailingCommas, braces
--swiftversion 5.6
--swiftversion 5.8
--header "SafariView\n{file}\n\nMIT License\n\nCopyright (c) 2021 Varun Santhanam\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the "Software"), to deal\n\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE."
194 changes: 51 additions & 143 deletions Sources/SafariView/SafariView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -473,7 +473,7 @@ public struct SafariView: View {

// MARK: - Private

struct Modifier: ViewModifier {
struct BoolModifier: ViewModifier {

// MARK: - API

Expand Down Expand Up @@ -594,7 +594,7 @@ public struct SafariView: View {
}

private func updateSafari() {
guard let safari = safari else {
guard let safari else {
return
}
let rep = parent.build()
Expand All @@ -607,7 +607,7 @@ public struct SafariView: View {
}

private func dismissSafari() {
guard let safari = safari else {
guard let safari else {
return
}

Expand Down Expand Up @@ -761,7 +761,7 @@ public struct SafariView: View {
}

private func updateSafari(with item: Item) {
guard let safari = safari else {
guard let safari else {
return
}
let rep = parent.build(item)
Expand All @@ -774,7 +774,7 @@ public struct SafariView: View {
}

private func dismissSafari(completion: (() -> Void)? = nil) {
guard let safari = safari else {
guard let safari else {
return
}

Expand All @@ -800,161 +800,69 @@ public struct SafariView: View {
}
}

struct URLModifier: ViewModifier {
struct GenericItemModifier<Item, Identifier>: ViewModifier where Identifier: Hashable {

@Binding
var url: URL?

var build: (URL) -> SafariView
var onDismiss: () -> Void
// MARK: - Initializers

func body(content: Content) -> some View {
content
.background(
Presenter(url: $url,
build: build,
onDismiss: onDismiss)
)
init(
item: Binding<Item?>,
id: KeyPath<Item, Identifier>,
onDismiss: (() -> Void)? = nil,
@ViewBuilder safariView: @escaping (Item) -> SafariView
)
{
self.item = item
self.id = id
self.onDismiss = onDismiss
self.safariView = safariView
}

private struct Presenter: UIViewRepresentable {

@Binding
var url: URL?

var build: (URL) -> SafariView
var onDismiss: () -> Void

final class Coordinator: NSObject, SFSafariViewControllerDelegate {

// MARK: - Initializers

init(parent: Presenter) {
self.parent = parent
}

// MARK: - API

let view = UIView()

var parent: Presenter

var url: URL? {
didSet {
switch (oldValue, url) {
case (.none, .none):
break
case let (.none, .some(new)):
presentSafari(with: new)
case let (.some(old), .some(new)) where old != new:
dismissSafari() { [presentSafari] in
presentSafari(new)
}
case let (.some, .some(new)):
updateSafari(with: new)
case (.some, .none):
dismissSafari()
}
}
}

// MARK: - SFSafariViewControllerDelegate

func safariViewControllerDidFinish(_ controller: SFSafariViewController) {
parent.url = nil
parent.onDismiss()
}

func safariViewController(_ controller: SFSafariViewController, initialLoadDidRedirectTo URL: URL) {
onInitialRedirect(URL)
}

func safariViewController(_ controller: SFSafariViewController, didCompleteInitialLoad didLoadSuccessfully: Bool) {
onInitialLoad(didLoadSuccessfully)
}

func safariViewController(_ controller: SFSafariViewController, activityItemsFor URL: URL, title: String?) -> [UIActivity] {
withActivityItems(URL, title)
}

func safariViewController(_ controller: SFSafariViewController, excludedActivityTypesFor URL: URL, title: String?) -> [UIActivity.ActivityType] {
withoutActivityItems(URL, title)
}

func safariViewControllerWillOpenInBrowser(_ controller: SFSafariViewController) {
willOpenInBrowser()
}

// MARK: - Private

private weak var safari: SFSafariViewController?

private var onInitialLoad: (Bool) -> Void = { _ in }
private var onInitialRedirect: (URL) -> Void = { _ in }
private var withActivityItems: (URL, String?) -> [UIActivity] = { _, _ in [] }
private var withoutActivityItems: (URL, String?) -> [UIActivity.ActivityType] = { _, _ in [] }
private var willOpenInBrowser: () -> Void = {}

private func presentSafari(with url: URL) {
let rep = parent.build(url)
onInitialLoad = rep.onInitialLoad
onInitialRedirect = rep.onInitialRedirect
withActivityItems = rep.withActivityItems
withoutActivityItems = rep.withoutActivityItems
willOpenInBrowser = rep.willOpenInBrowser
let vc = SFSafariViewController(url: rep.url, configuration: rep.configuration)
vc.delegate = self
rep.apply(to: vc)
guard let presenting = view.controller else {
parent.url = url
return
}

presenting.present(vc, animated: true)
// MARK: - ViewModifier

safari = vc
@ViewBuilder
func body(content: Content) -> some View {
content
.safari(item: binding, onDismiss: onDismiss) { wrappedItem in
safariView(wrappedItem.item)
}
}

private func updateSafari(with url: URL) {
guard let safari = safari else {
return
}
let rep = parent.build(url)
onInitialLoad = rep.onInitialLoad
onInitialRedirect = rep.onInitialRedirect
withActivityItems = rep.withActivityItems
withoutActivityItems = rep.withoutActivityItems
willOpenInBrowser = rep.willOpenInBrowser
rep.apply(to: safari)
}
// MARK: - Private

private func dismissSafari(_ completion: (() -> Void)? = nil) {
guard let safari = safari else {
return
}
private let item: Binding<Item?>
private let id: KeyPath<Item, Identifier>
private let onDismiss: (() -> Void)?
private let safariView: (Item) -> SafariView

safari.dismiss(animated: true) {
self.parent.onDismiss()
completion?()
}
private var binding: Binding<WrappedItem?> {
Binding<WrappedItem?> {
guard let item = item.wrappedValue else {
return nil
}
return WrappedItem(item, id)
} set: { newValue in
item.wrappedValue = newValue?.item
}
}

func makeCoordinator() -> Coordinator {
Coordinator(parent: self)
private struct WrappedItem: Identifiable {
init(
_ item: Item,
_ keyPath: KeyPath<Item, Identifier>
) {
self.item = item
self.keyPath = keyPath
}

func makeUIView(context: Context) -> UIView {
context.coordinator.view
}
let item: Item
private let keyPath: KeyPath<Item, Identifier>

func updateUIView(_ uiView: UIView, context: Context) {
context.coordinator.parent = self
context.coordinator.url = url
}
typealias ID = Identifier

var id: ID {
item[keyPath: keyPath]
}
}

}

private struct Safari: UIViewControllerRepresentable {
Expand Down
103 changes: 87 additions & 16 deletions Sources/SafariView/ViewExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,14 @@ public extension View {
/// - onDismiss: The closure to execute when dismissing the ``SafariView``
/// - safariView: A closure that returns the ``SafariView`` to present
/// - Returns: The modified view
func safari(isPresented: Binding<Bool>,
onDismiss: (() -> Void)? = nil,
safariView: @escaping () -> SafariView) -> some View {
let modifier = SafariView.Modifier(isPresented: isPresented,
build: safariView,
onDismiss: onDismiss ?? {})
func safari(
isPresented: Binding<Bool>,
onDismiss: (() -> Void)? = nil,
@ViewBuilder safariView: @escaping () -> SafariView
) -> some View {
let modifier = SafariView.BoolModifier(isPresented: isPresented,
build: safariView,
onDismiss: onDismiss ?? {})
return ModifiedContent(content: self, modifier: modifier)
}

Expand Down Expand Up @@ -127,15 +129,80 @@ public extension View {
/// - onDismiss: The closure to execute when dismissing the ``SafariView``
/// - safariView: A closure that returns the ``SafariView`` to present
/// - Returns: The modified view
func safari<Item>(item: Binding<Item?>,
onDismiss: (() -> Void)? = nil,
safariView: @escaping (Item) -> SafariView) -> some View where Item: Identifiable {
func safari<Item>(
item: Binding<Item?>,
onDismiss: (() -> Void)? = nil,
@ViewBuilder safariView: @escaping (Item) -> SafariView
) -> some View where Item: Identifiable {
let modifier = SafariView.ItemModitifer(item: item,
build: safariView,
onDismiss: onDismiss ?? {})
return ModifiedContent(content: self, modifier: modifier)
}

/// Presents a ``SafariView`` using the given item as a data source for the ``SafariView``'s content
///
/// Use this method when you need to present a ``SafariView`` with content from a custom data source. The example below shows a custom data source `InventoryItem` that the closure uses to populate the ``SafariView`` before it is shown to the user:
///
/// ```swift
/// import Foundation
/// import SafariView
/// import SwiftUI
///
/// struct InventoryItem {
/// let id: Int
/// let title: String
/// let url: URL
/// }
///
/// struct InventoryList: View {
///
/// init(inventory: [InventoryItem]) {
/// self.inventory = inventory
/// }
///
/// var inventory: [InventoryItem]
///
/// @State private var selectedItem: InventoryItem?
///
/// var body: some View {
/// List(inventory) { inventoryItem in
/// Button(action: {
/// self.selectedItem = inventoryItem
/// }) {
/// Text(inventoryItem.title)
/// }
/// }
/// .safari(item: $selectedItem,
/// id: \.id,
/// onDismiss: dismissAction) { item in
/// SafariView(url: item.url)
/// }
/// }
///
/// func didDismiss() {
/// // Handle the dismissing action.
/// }
///
/// }
/// ```
///
/// - Parameters:
/// - item: A binding to an optional source of truth for the ``SafariView``. When item is non-nil, the system passes the item’s content to the modifier’s closure. You display this content in a ``SafariView`` that you create that the system displays to the user. If item changes, the system dismisses the ``SafariView`` and replaces it with a new one using the same process.
/// - id: A keypath used to generate stable identifier for instances of `Item`.
/// - onDismiss: The closure to execute when dismissing the ``SafariView``
/// - safariView: A closure that returns the ``SafariView`` to present
/// - Returns: The modified view
func safari<Item>(
item: Binding<Item?>,
id: KeyPath<Item, some Hashable>,
onDismiss: (() -> Void)? = nil,
@ViewBuilder safariView: @escaping (Item) -> SafariView
) -> some View {
let modifier = SafariView.GenericItemModifier(item: item, id: id, onDismiss: onDismiss, safariView: safariView)
return ModifiedContent(content: self, modifier: modifier)
}

/// Presents a ``SafariView`` using the given URL as a data source for the ``SafariView``'s content
///
/// Use this method when you need to present a ``SafariView`` with content from a custom data source. The example below shows a custom data source `InventoryItem` that the closure uses to populate the ``SafariView`` before it is shown to the user:
Expand Down Expand Up @@ -186,13 +253,17 @@ public extension View {
/// - onDismiss: The closure to execute when dismissing the ``SafariView``
/// - safariView: A closure that returns the ``SafariView`` to present
/// - Returns: The modified view
func safari(url: Binding<URL?>,
onDismiss: (() -> Void)? = nil,
safariView: @escaping (URL) -> SafariView) -> some View {
let modifier = SafariView.URLModifier(url: url,
build: safariView,
onDismiss: onDismiss ?? {})
return ModifiedContent(content: self, modifier: modifier)
func safari(
url: Binding<URL?>,
onDismiss: (() -> Void)? = nil,
@ViewBuilder safariView: @escaping (URL) -> SafariView
) -> some View {
safari(
item: url,
id: \.hashValue,
onDismiss: onDismiss,
safariView: safariView
)
}

}