Skip to content

Commit 2efb01a

Browse files
committed
Bump to v2.1.0
2 parents daeac97 + a6c27e4 commit 2efb01a

File tree

10 files changed

+522
-509
lines changed

10 files changed

+522
-509
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## [v2.1.0](https://github.com/stleamist/BetterSafariView/releases/tag/v2.1.0) (2020-08-24)
4+
### Changed
5+
- Coordinators are now in charge of view controller presentations, following the structure of [VisualEffects](https://github.com/twostraws/VisualEffects).
6+
37
## [v2.0.1](https://github.com/stleamist/BetterSafariView/releases/tag/v2.0.1) (2020-08-22)
48
### Fixed
59
- Fixed typos on markup

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,14 @@ A better way to present a SFSafariViewController or start a ASWebAuthenticationS
4747

4848
SwiftUI is a strong, intuitive way to build user interfaces, but was released with some part of existing elements missing. One example of those missing elements is the [`SFSafariViewController`](https://developer.apple.com/documentation/safariservices/sfsafariviewcontroller).
4949

50-
Fortunately, Apple provides a way to wrap UIKit elements into SwiftUI views. A common approach to place the `SFSafariViewController` inside SwiftUI is to create [a simple view representing a `SFSafariViewController`](/Demo/BetterSafariViewDemo/NaiveSafariView.swift), then present it with a [`sheet(isPresented:onDismiss:content:)`](https://developer.apple.com/documentation/swiftui/view/3352791-sheet) modifier or a [`NavigationLink`](https://developer.apple.com/documentation/swiftui/navigationlink) button (See [`ContentView.swift`](/Demo/BetterSafariViewDemo/ContentView.swift) in the demo project).
50+
Fortunately, Apple provides a way to wrap UIKit elements into SwiftUI views. A common approach to place the `SFSafariViewController` inside SwiftUI is to create [a simple view representing a `SFSafariViewController`](/Demo/BetterSafariViewDemo/NaiveSafariView.swift), then present it with a [`sheet(isPresented:onDismiss:content:)`](https://developer.apple.com/documentation/swiftui/view/3352791-sheet) modifier or a [`NavigationLink`](https://developer.apple.com/documentation/swiftui/navigationlink) button (See [`RootView.swift`](/Demo/BetterSafariViewDemo/RootView.swift) in the demo project).
5151

5252
However, there’s a problem in this approach: it can’t present the `SFSafariViewController` with its default presentation style — a push transition covers full screen. A sheet modifier can present the view only in a modal sheet, and a navigation link shows the two navigation bars at the top so we have to deal with them. This comes down to the conclusion that there’s no option to present it the right way except for using [`present(_:animated:completion:)`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621380-present) method of a [`UIViewController`](https://developer.apple.com/documentation/uikit/uiviewcontroller) instance, but it is prohibited and not a good design to access the [`UIHostingController`](https://developer.apple.com/documentation/swiftui/uihostingcontroller) directly from the SwiftUI view.
5353

5454
`BetterSafariView` clearly achieves this goal by hosting a simple `UIViewController` to present a `SFSafariViewController` as a view’s background. In this way, a [`ASWebAuthenticationSession`](https://developer.apple.com/documentation/authenticationservices/aswebauthenticationsession) is also able to be started without any issue in SwiftUI.
5555

5656
## Usage
57-
You can use it easily with the following modifiers in a similar way to presenting a sheet.
57+
With the following modifiers, you can use it in a similar way to present a sheet.
5858

5959
### SafariView
6060
#### Modifiers
@@ -147,7 +147,7 @@ struct ContentView: View {
147147
Add the following line to the `dependencies` in your [`Package.swift`](https://developer.apple.com/documentation/swift_packages/package) file:
148148

149149
```swift
150-
.package(url: "https://github.com/stleamist/BetterSafariView.git", .upToNextMajor(from: "2.0.1"))
150+
.package(url: "https://github.com/stleamist/BetterSafariView.git", .upToNextMajor(from: "2.1.0"))
151151
```
152152

153153
Next, add `BetterSafariView` as a dependency for your targets:
@@ -166,7 +166,7 @@ import PackageDescription
166166
let package = Package(
167167
name: "MyPackage",
168168
dependencies: [
169-
.package(url: "https://github.com/stleamist/BetterSafariView.git", .upToNextMajor(from: "2.0.1"))
169+
.package(url: "https://github.com/stleamist/BetterSafariView.git", .upToNextMajor(from: "2.1.0"))
170170
],
171171
targets: [
172172
.target(name: "MyTarget", dependencies: ["BetterSafariView"])

Sources/BetterSafariView/SafariView.swift renamed to Sources/BetterSafariView/SafariView/SafariView.swift

Lines changed: 0 additions & 211 deletions
Original file line numberDiff line numberDiff line change
@@ -162,214 +162,3 @@ public extension SafariView.Configuration {
162162
self.barCollapsingEnabled = barCollapsingEnabled
163163
}
164164
}
165-
166-
struct SafariViewHosting<Item: Identifiable>: UIViewControllerRepresentable {
167-
168-
// MARK: Representation
169-
170-
@Binding var item: Item?
171-
var onDismiss: (() -> Void)? = nil
172-
var representationBuilder: (Item) -> SafariView
173-
174-
// MARK: UIViewControllerRepresentable
175-
176-
func makeUIViewController(context: Context) -> UIViewController {
177-
return UIViewController()
178-
}
179-
180-
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
181-
// Ensure the following statements are executed once only after the `item` is changed
182-
// by comparing current item to old one during frequent view updates.
183-
let itemUpdateChange = context.coordinator.itemStorage.updateItem(item)
184-
185-
switch itemUpdateChange { // (oldItem, newItem)
186-
case (.none, .none):
187-
()
188-
case let (.none, .some(newItem)):
189-
presentSafariViewController(from: uiViewController, in: context, using: newItem)
190-
case let (.some(oldItem), .some(newItem)) where oldItem.id != newItem.id:
191-
dismissSafariViewController(from: uiViewController) {
192-
self.presentSafariViewController(from: uiViewController, in: context, using: newItem)
193-
}
194-
case let (.some, .some(newItem)):
195-
updateSafariViewController(presentedBy: uiViewController, using: newItem)
196-
case (.some, .none):
197-
dismissSafariViewController(from: uiViewController)
198-
}
199-
}
200-
201-
// MARK: Update Handlers
202-
203-
private func presentSafariViewController(from uiViewController: UIViewController, in context: Context, using item: Item) {
204-
let representation = representationBuilder(item)
205-
let safariViewController = SFSafariViewController(url: representation.url, configuration: representation.configuration)
206-
safariViewController.delegate = context.coordinator.safariViewControllerFinishDelegate
207-
representation.applyModification(to: safariViewController)
208-
209-
// 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`.
210-
// As a workaround, use a `rootViewController` of the `window` for presenting.
211-
// (Unlike the other view controllers, a view controller hosted by a cell doesn't have a parent, but has the same window.)
212-
let presentingViewController = uiViewController.view.window?.rootViewController ?? uiViewController
213-
presentingViewController.present(safariViewController, animated: true)
214-
}
215-
216-
private func updateSafariViewController(presentedBy uiViewController: UIViewController, using item: Item) {
217-
guard let safariViewController = uiViewController.presentedViewController as? SFSafariViewController else {
218-
return
219-
}
220-
let representation = representationBuilder(item)
221-
representation.applyModification(to: safariViewController)
222-
}
223-
224-
private func dismissSafariViewController(from uiViewController: UIViewController, completion: (() -> Void)? = nil) {
225-
226-
// Check if the `uiViewController` is a instance of the `SFSafariViewController`
227-
// to prevent other controllers presented by the container view from being dismissed unintentionally.
228-
guard uiViewController.presentedViewController is SFSafariViewController else {
229-
return
230-
}
231-
uiViewController.dismiss(animated: true) {
232-
self.handleDismissalWithoutResettingItemBinding()
233-
completion?()
234-
}
235-
}
236-
237-
// MARK: Dismissal Handlers
238-
239-
// Used when the Safari view controller is finished by an item change during view update.
240-
private func handleDismissalWithoutResettingItemBinding() {
241-
self.onDismiss?()
242-
}
243-
244-
// Used when the Safari view controller is finished by a user interaction.
245-
private func resetItemBindingAndHandleDismissal() {
246-
self.item = nil
247-
self.onDismiss?()
248-
}
249-
250-
// MARK: Coordinator
251-
252-
func makeCoordinator() -> Coordinator {
253-
return Coordinator(onFinished: resetItemBindingAndHandleDismissal)
254-
}
255-
256-
class Coordinator {
257-
258-
var itemStorage: ItemStorage<Item>
259-
let safariViewControllerFinishDelegate: SafariViewControllerFinishDelegate
260-
261-
init(onFinished: @escaping () -> Void) {
262-
self.itemStorage = ItemStorage()
263-
self.safariViewControllerFinishDelegate = SafariViewControllerFinishDelegate(onFinished: onFinished)
264-
}
265-
}
266-
267-
class SafariViewControllerFinishDelegate: NSObject, SFSafariViewControllerDelegate {
268-
269-
private let onFinished: () -> Void
270-
271-
init(onFinished: @escaping () -> Void) {
272-
self.onFinished = onFinished
273-
}
274-
275-
func safariViewControllerDidFinish(_ controller: SFSafariViewController) {
276-
onFinished()
277-
}
278-
}
279-
}
280-
281-
struct SafariViewPresentationModifier: ViewModifier {
282-
283-
@Binding var isPresented: Bool
284-
var onDismiss: (() -> Void)? = nil
285-
var representationBuilder: () -> SafariView
286-
287-
private var item: Binding<Bool?> {
288-
.init(
289-
get: { self.isPresented ? true : nil },
290-
set: { self.isPresented = ($0 != nil) }
291-
)
292-
}
293-
294-
// Converts `() -> Void` closure to `(Bool) -> Void`
295-
private func itemRepresentationBuilder(bool: Bool) -> SafariView {
296-
return representationBuilder()
297-
}
298-
299-
func body(content: Content) -> some View {
300-
content.background(
301-
SafariViewHosting(
302-
item: item,
303-
onDismiss: onDismiss,
304-
representationBuilder: itemRepresentationBuilder
305-
)
306-
)
307-
}
308-
}
309-
310-
struct ItemSafariViewPresentationModifier<Item: Identifiable>: ViewModifier {
311-
312-
@Binding var item: Item?
313-
var onDismiss: (() -> Void)? = nil
314-
var representationBuilder: (Item) -> SafariView
315-
316-
func body(content: Content) -> some View {
317-
content.background(
318-
SafariViewHosting(
319-
item: $item,
320-
onDismiss: onDismiss,
321-
representationBuilder: representationBuilder
322-
)
323-
)
324-
}
325-
}
326-
327-
public extension View {
328-
329-
/// Presents a Safari view when a given condition is true.
330-
///
331-
/// - Parameters:
332-
/// - isPresented: A binding to whether the Safari view is presented.
333-
/// - onDismiss: A closure executed when the Safari view dismisses.
334-
/// - content: A closure returning the `SafariView` to present.
335-
///
336-
func safariView(
337-
isPresented: Binding<Bool>,
338-
onDismiss: (() -> Void)? = nil,
339-
content representationBuilder: @escaping () -> SafariView
340-
) -> some View {
341-
self.modifier(
342-
SafariViewPresentationModifier(
343-
isPresented: isPresented,
344-
onDismiss: onDismiss,
345-
representationBuilder: representationBuilder
346-
)
347-
)
348-
}
349-
350-
/// Presents a Safari view using the given item as a data source
351-
/// for the `SafariView` to present.
352-
///
353-
/// - Parameters:
354-
/// - item: A binding to an optional source of truth for the Safari view.
355-
/// When representing a non-`nil` item, the system uses `content` to
356-
/// create a `SafariView` of the item.
357-
/// If the identity changes, the system dismisses a
358-
/// currently-presented Safari view and replace it by a new Safari view.
359-
/// - onDismiss: A closure executed when the Safari view dismisses.
360-
/// - content: A closure returning the `SafariView` to present.
361-
///
362-
func safariView<Item: Identifiable>(
363-
item: Binding<Item?>,
364-
onDismiss: (() -> Void)? = nil,
365-
content representationBuilder: @escaping (Item) -> SafariView
366-
) -> some View {
367-
self.modifier(
368-
ItemSafariViewPresentationModifier(
369-
item: item,
370-
onDismiss: onDismiss,
371-
representationBuilder: representationBuilder
372-
)
373-
)
374-
}
375-
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import SwiftUI
2+
3+
struct SafariViewPresentationModifier: ViewModifier {
4+
5+
@Binding var isPresented: Bool
6+
var onDismiss: (() -> Void)? = nil
7+
var representationBuilder: () -> SafariView
8+
9+
private var item: Binding<Bool?> {
10+
.init(
11+
get: { self.isPresented ? true : nil },
12+
set: { self.isPresented = ($0 != nil) }
13+
)
14+
}
15+
16+
// Converts `() -> Void` closure to `(Bool) -> Void`
17+
private func itemRepresentationBuilder(bool: Bool) -> SafariView {
18+
return representationBuilder()
19+
}
20+
21+
func body(content: Content) -> some View {
22+
content.background(
23+
SafariViewPresenter(
24+
item: item,
25+
onDismiss: onDismiss,
26+
representationBuilder: itemRepresentationBuilder
27+
)
28+
)
29+
}
30+
}
31+
32+
struct ItemSafariViewPresentationModifier<Item: Identifiable>: ViewModifier {
33+
34+
@Binding var item: Item?
35+
var onDismiss: (() -> Void)? = nil
36+
var representationBuilder: (Item) -> SafariView
37+
38+
func body(content: Content) -> some View {
39+
content.background(
40+
SafariViewPresenter(
41+
item: $item,
42+
onDismiss: onDismiss,
43+
representationBuilder: representationBuilder
44+
)
45+
)
46+
}
47+
}
48+
49+
public extension View {
50+
51+
/// Presents a Safari view when a given condition is true.
52+
///
53+
/// - Parameters:
54+
/// - isPresented: A binding to whether the Safari view is presented.
55+
/// - onDismiss: A closure executed when the Safari view dismisses.
56+
/// - content: A closure returning the `SafariView` to present.
57+
///
58+
func safariView(
59+
isPresented: Binding<Bool>,
60+
onDismiss: (() -> Void)? = nil,
61+
content representationBuilder: @escaping () -> SafariView
62+
) -> some View {
63+
self.modifier(
64+
SafariViewPresentationModifier(
65+
isPresented: isPresented,
66+
onDismiss: onDismiss,
67+
representationBuilder: representationBuilder
68+
)
69+
)
70+
}
71+
72+
/// Presents a Safari view using the given item as a data source
73+
/// for the `SafariView` to present.
74+
///
75+
/// - Parameters:
76+
/// - item: A binding to an optional source of truth for the Safari view.
77+
/// When representing a non-`nil` item, the system uses `content` to
78+
/// create a `SafariView` of the item.
79+
/// If the identity changes, the system dismisses a
80+
/// currently-presented Safari view and replace it by a new Safari view.
81+
/// - onDismiss: A closure executed when the Safari view dismisses.
82+
/// - content: A closure returning the `SafariView` to present.
83+
///
84+
func safariView<Item: Identifiable>(
85+
item: Binding<Item?>,
86+
onDismiss: (() -> Void)? = nil,
87+
content representationBuilder: @escaping (Item) -> SafariView
88+
) -> some View {
89+
self.modifier(
90+
ItemSafariViewPresentationModifier(
91+
item: item,
92+
onDismiss: onDismiss,
93+
representationBuilder: representationBuilder
94+
)
95+
)
96+
}
97+
}

0 commit comments

Comments
 (0)