-
-
Notifications
You must be signed in to change notification settings - Fork 514
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add SwiftUI proof of concept in Maps-SwiftUI example (#481)
Partially solves #281. This adds a new example app which mimics the Maps.app, written in SwiftUI. The code works for iOS 13+, however: * the project has been created with Xcode 13 * the project uses the SwiftUI lifecycle (iOS 14+) The source code in Examples/Maps-SwiftUI/Maps/FloatingPanel is ready to move into the library, but there is an issue on SwiftUI’s environment propagation into FloatingPanel. SwiftUI’s environment is propagated to all subviews. However FloatingPanel is not a subview, but a new view controller in the screen (and not a child view controller). It’s possible to lead behaviors unexpected by SwiftUI users so that this is merged as a sample code until it will be resolved.
- Loading branch information
Showing
19 changed files
with
1,229 additions
and
1 deletion.
There are no files selected for viewing
426 changes: 426 additions & 0 deletions
426
Examples/Maps-SwiftUI/Maps-SwiftUI.xcodeproj/project.pbxproj
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
// Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license. | ||
|
||
import SwiftUI | ||
import MapKit | ||
|
||
struct ContentView: View { | ||
@State private var region = MKCoordinateRegion( | ||
center: CLLocationCoordinate2D(latitude: 37.623198015869235, longitude: -122.43066818432008), | ||
span: MKCoordinateSpan(latitudeDelta: 0.4425100023575723, longitudeDelta: 0.28543697435880233) | ||
) | ||
|
||
var body: some View { | ||
ZStack { | ||
Map(coordinateRegion: $region) | ||
.ignoresSafeArea() | ||
statusBarBlur | ||
} | ||
} | ||
|
||
private var statusBarBlur: some View { | ||
GeometryReader { geometry in | ||
VisualEffectBlur() | ||
.frame(height: geometry.safeAreaInsets.top) | ||
.ignoresSafeArea() | ||
} | ||
} | ||
} | ||
|
||
struct ContentView_Previews: PreviewProvider { | ||
static var previews: some View { | ||
ContentView() | ||
} | ||
} |
121 changes: 121 additions & 0 deletions
121
Examples/Maps-SwiftUI/Maps/FloatingPanel/FloatingPanelView.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
// Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license. | ||
|
||
import FloatingPanel | ||
import SwiftUI | ||
|
||
/// A proxy for exposing the methods of the floating panel controller. | ||
public struct FloatingPanelProxy { | ||
/// The associated floating panel controller. | ||
public weak var fpc: FloatingPanelController? | ||
|
||
/// Tracks the specified scroll view to correspond with the scroll. | ||
/// | ||
/// - Parameter scrollView: Specify a scroll view to continuously and | ||
/// seamlessly work in concert with interactions of the surface view. | ||
public func track(scrollView: UIScrollView) { | ||
fpc?.track(scrollView: scrollView) | ||
} | ||
|
||
/// Moves the floating panel to the specified position. | ||
/// | ||
/// - Parameters: | ||
/// - floatingPanelState: The state to move to. | ||
/// - animated: `true` to animate the transition to the new state; `false` | ||
/// otherwise. | ||
public func move( | ||
to floatingPanelState: FloatingPanelState, | ||
animated: Bool, | ||
completion: (() -> Void)? = nil | ||
) { | ||
fpc?.move(to: floatingPanelState, animated: animated, completion: completion) | ||
} | ||
} | ||
|
||
/// A view with an associated floating panel. | ||
struct FloatingPanelView<Content: View, FloatingPanelContent: View>: UIViewControllerRepresentable { | ||
/// A type that conforms to the `FloatingPanelControllerDelegate` protocol. | ||
var delegate: FloatingPanelControllerDelegate? | ||
|
||
/// The behavior for determining the adjusted content offsets. | ||
@Environment(\.contentInsetAdjustmentBehavior) var contentInsetAdjustmentBehavior | ||
|
||
/// Constants that define how a panel content fills in the surface. | ||
@Environment(\.contentMode) var contentMode | ||
|
||
/// The floating panel grabber handle offset. | ||
@Environment(\.grabberHandlePadding) var grabberHandlePadding | ||
|
||
/// The floating panel `surfaceView` appearance. | ||
@Environment(\.surfaceAppearance) var surfaceAppearance | ||
|
||
/// The view builder that creates the floating panel parent view content. | ||
@ViewBuilder var content: Content | ||
|
||
/// The view builder that creates the floating panel content. | ||
@ViewBuilder var floatingPanelContent: (FloatingPanelProxy) -> FloatingPanelContent | ||
|
||
public func makeUIViewController(context: Context) -> UIHostingController<Content> { | ||
let hostingController = UIHostingController(rootView: content) | ||
hostingController.view.backgroundColor = nil | ||
// We need to wait for the current runloop cycle to complete before our | ||
// view is actually added (into the view hierarchy), otherwise the | ||
// environment is not ready yet. | ||
DispatchQueue.main.async { | ||
context.coordinator.setupFloatingPanel(hostingController) | ||
} | ||
return hostingController | ||
} | ||
|
||
public func updateUIViewController( | ||
_ uiViewController: UIHostingController<Content>, | ||
context: Context | ||
) { | ||
context.coordinator.updateIfNeeded() | ||
} | ||
|
||
public func makeCoordinator() -> Coordinator { | ||
Coordinator(parent: self) | ||
} | ||
|
||
/// `FloatingPanelView` coordinator. | ||
/// | ||
/// Responsible to setup the view hierarchy and floating panel. | ||
final class Coordinator { | ||
private let parent: FloatingPanelView<Content, FloatingPanelContent> | ||
private lazy var fpc = FloatingPanelController() | ||
|
||
init(parent: FloatingPanelView<Content, FloatingPanelContent>) { | ||
self.parent = parent | ||
} | ||
|
||
func setupFloatingPanel(_ parentViewController: UIViewController) { | ||
updateIfNeeded() | ||
let panelContent = parent.floatingPanelContent(FloatingPanelProxy(fpc: fpc)) | ||
let hostingViewController = UIHostingController( | ||
rootView: panelContent, | ||
ignoresKeyboard: true | ||
) | ||
hostingViewController.view.backgroundColor = nil | ||
fpc.set(contentViewController: hostingViewController) | ||
fpc.addPanel(toParent: parentViewController, at: 1, animated: false) | ||
} | ||
|
||
func updateIfNeeded() { | ||
if fpc.contentInsetAdjustmentBehavior != parent.contentInsetAdjustmentBehavior { | ||
fpc.contentInsetAdjustmentBehavior = parent.contentInsetAdjustmentBehavior | ||
} | ||
if fpc.contentMode != parent.contentMode { | ||
fpc.contentMode = parent.contentMode | ||
} | ||
if fpc.delegate !== parent.delegate { | ||
fpc.delegate = parent.delegate | ||
} | ||
if fpc.surfaceView.grabberHandlePadding != parent.grabberHandlePadding { | ||
fpc.surfaceView.grabberHandlePadding = parent.grabberHandlePadding | ||
} | ||
if fpc.surfaceView.appearance != parent.surfaceAppearance { | ||
fpc.surfaceView.appearance = parent.surfaceAppearance | ||
} | ||
} | ||
} | ||
} |
44 changes: 44 additions & 0 deletions
44
Examples/Maps-SwiftUI/Maps/FloatingPanel/UIHostingController+ignoreKeyboard.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
// Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license. | ||
|
||
import SwiftUI | ||
|
||
/// This extension makes sure SwiftUI views are not affected by iOS keyboard. | ||
/// | ||
/// Credits to https://steipete.me/posts/disabling-keyboard-avoidance-in-swiftui-uihostingcontroller/ | ||
extension UIHostingController { | ||
public convenience init(rootView: Content, ignoresKeyboard: Bool) { | ||
self.init(rootView: rootView) | ||
|
||
if ignoresKeyboard { | ||
guard let viewClass = object_getClass(view) else { return } | ||
|
||
let viewSubclassName = String( | ||
cString: class_getName(viewClass) | ||
).appending("_IgnoresKeyboard") | ||
|
||
if let viewSubclass = NSClassFromString(viewSubclassName) { | ||
object_setClass(view, viewSubclass) | ||
} else { | ||
guard | ||
let viewClassNameUtf8 = (viewSubclassName as NSString).utf8String, | ||
let viewSubclass = objc_allocateClassPair(viewClass, viewClassNameUtf8, 0) | ||
else { return } | ||
|
||
if let method = class_getInstanceMethod( | ||
viewClass, | ||
NSSelectorFromString("keyboardWillShowWithNotification:") | ||
) { | ||
let keyboardWillShow: @convention(block) (AnyObject, AnyObject) -> Void = { _, _ in } | ||
class_addMethod( | ||
viewSubclass, | ||
NSSelectorFromString("keyboardWillShowWithNotification:"), | ||
imp_implementationWithBlock(keyboardWillShow), | ||
method_getTypeEncoding(method) | ||
) | ||
} | ||
objc_registerClassPair(viewSubclass) | ||
object_setClass(view, viewSubclass) | ||
} | ||
} | ||
} | ||
} |
31 changes: 31 additions & 0 deletions
31
Examples/Maps-SwiftUI/Maps/FloatingPanel/View+floatingPanel.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
// Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license. | ||
|
||
import FloatingPanel | ||
import SwiftUI | ||
|
||
extension View { | ||
/// Presents a floating panel using the given closure as its content. | ||
/// | ||
/// The modifier's content view builder receives a `FloatingPanelProxy` | ||
/// instance; you use the proxy's methods to interact with the associated | ||
/// `FloatingPanelController`. | ||
/// | ||
/// - Parameters: | ||
/// - delegate: A type that conforms to the | ||
/// `FloatingPanelControllerDelegate` protocol. You have comprehensive | ||
/// control over the floating panel behavior when you use a delegate. | ||
/// - floatingPanelContent: The floating panel content. This view builder | ||
/// receives a `FloatingPanelProxy` instance that you use to interact | ||
/// with the `FloatingPanelController`. | ||
public func floatingPanel<FloatingPanelContent: View>( | ||
delegate: FloatingPanelControllerDelegate? = nil, | ||
@ViewBuilder _ floatingPanelContent: @escaping (_: FloatingPanelProxy) -> FloatingPanelContent | ||
) -> some View { | ||
FloatingPanelView( | ||
delegate: delegate, | ||
content: { self }, | ||
floatingPanelContent: floatingPanelContent | ||
) | ||
.ignoresSafeArea() | ||
} | ||
} |
38 changes: 38 additions & 0 deletions
38
...es/Maps-SwiftUI/Maps/FloatingPanel/View+floatingPanelContentInsetAdjustmentBehavior.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
// Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license. | ||
|
||
import FloatingPanel | ||
import SwiftUI | ||
|
||
struct ContentInsetKey: EnvironmentKey { | ||
static var defaultValue: FloatingPanelController.ContentInsetAdjustmentBehavior = .always | ||
} | ||
|
||
extension EnvironmentValues { | ||
/// The behavior for determining the adjusted content offsets. | ||
var contentInsetAdjustmentBehavior: FloatingPanelController.ContentInsetAdjustmentBehavior { | ||
get { self[ContentInsetKey.self] } | ||
set { self[ContentInsetKey.self] = newValue } | ||
} | ||
} | ||
|
||
extension View { | ||
/// Sets the content inset adjustment behavior for floating panels within | ||
/// this view. | ||
/// | ||
/// Use this modifier to set a specific content inset adjustment behavior | ||
/// for floating panel instances within a view: | ||
/// | ||
/// MainView() | ||
/// .floatingPanel { _ in | ||
/// FloatingPanelContent() | ||
/// } | ||
/// .floatingPanelContentInsetAdjustmentBehavior(.never) | ||
/// | ||
/// - Parameter contentInsetAdjustmentBehavior: The content inset adjustment | ||
/// behavior to set. | ||
public func floatingPanelContentInsetAdjustmentBehavior( | ||
_ contentInsetAdjustmentBehavior: FloatingPanelController.ContentInsetAdjustmentBehavior | ||
) -> some View { | ||
environment(\.contentInsetAdjustmentBehavior, contentInsetAdjustmentBehavior) | ||
} | ||
} |
37 changes: 37 additions & 0 deletions
37
Examples/Maps-SwiftUI/Maps/FloatingPanel/View+floatingPanelContentMode.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
// Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license. | ||
|
||
import FloatingPanel | ||
import SwiftUI | ||
|
||
struct ContentModeKey: EnvironmentKey { | ||
static var defaultValue: FloatingPanelController.ContentMode = .static | ||
} | ||
|
||
extension EnvironmentValues { | ||
/// Used to determine how the floating panel controller lays out the content | ||
/// view when the surface position changes. | ||
var contentMode: FloatingPanelController.ContentMode { | ||
get { self[ContentModeKey.self] } | ||
set { self[ContentModeKey.self] = newValue } | ||
} | ||
} | ||
|
||
extension View { | ||
/// Sets the content mode for floating panels within this view. | ||
/// | ||
/// Use this modifier to set a specific content mode for floating panel | ||
/// instances within a view: | ||
/// | ||
/// MainView() | ||
/// .floatingPanel { _ in | ||
/// FloatingPanelContent() | ||
/// } | ||
/// .floatingPanelContentMode(.static) | ||
/// | ||
/// - Parameter contentMode: The content mode to set. | ||
public func floatingPanelContentMode( | ||
_ contentMode: FloatingPanelController.ContentMode | ||
) -> some View { | ||
environment(\.contentMode, contentMode) | ||
} | ||
} |
36 changes: 36 additions & 0 deletions
36
Examples/Maps-SwiftUI/Maps/FloatingPanel/View+floatingPanelGrabberHandlePadding.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
// Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license. | ||
|
||
import FloatingPanel | ||
import SwiftUI | ||
|
||
struct GrabberHandlePaddingKey: EnvironmentKey { | ||
static var defaultValue: CGFloat = 6.0 | ||
} | ||
|
||
extension EnvironmentValues { | ||
/// The offset of the grabber handle from the interactive edge. | ||
var grabberHandlePadding: CGFloat { | ||
get { self[GrabberHandlePaddingKey.self] } | ||
set { self[GrabberHandlePaddingKey.self] = newValue } | ||
} | ||
} | ||
|
||
extension View { | ||
/// Sets the grabber handle padding for floating panels within this view. | ||
/// | ||
/// Use this modifier to set a specific padding to floating panel instances | ||
/// within a view: | ||
/// | ||
/// MainView() | ||
/// .floatingPanel { _ in | ||
/// FloatingPanelContent() | ||
/// } | ||
/// .floatingPanelGrabberHandlePadding(16) | ||
/// | ||
/// - Parameter padding: The grabber handle padding to set. | ||
public func floatingPanelGrabberHandlePadding( | ||
_ padding: CGFloat | ||
) -> some View { | ||
environment(\.grabberHandlePadding, padding) | ||
} | ||
} |
44 changes: 44 additions & 0 deletions
44
Examples/Maps-SwiftUI/Maps/FloatingPanel/View+floatingPanelSurfaceAppearance.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
// Copyright 2021 the FloatingPanel authors. All rights reserved. MIT license. | ||
|
||
import FloatingPanel | ||
import SwiftUI | ||
|
||
struct SurfaceAppearanceKey: EnvironmentKey { | ||
static var defaultValue = SurfaceAppearance() | ||
} | ||
|
||
extension EnvironmentValues { | ||
/// The appearance of a surface view. | ||
var surfaceAppearance: SurfaceAppearance { | ||
get { self[SurfaceAppearanceKey.self] } | ||
set { self[SurfaceAppearanceKey.self] = newValue } | ||
} | ||
} | ||
|
||
extension View { | ||
/// Sets the surface appearance for floating panels within this view. | ||
/// | ||
/// Use this modifier to set a specific surface appearance for floating | ||
/// panel instances within a view: | ||
/// | ||
/// MainView() | ||
/// .floatingPanel { _ in | ||
/// FloatingPanelContent() | ||
/// } | ||
/// .floatingPanelSurfaceAppearance(.transparent) | ||
/// | ||
/// extension SurfaceAppearance { | ||
/// static var transparent: SurfaceAppearance { | ||
/// let appearance = SurfaceAppearance() | ||
/// appearance.backgroundColor = .clear | ||
/// return appearance | ||
/// } | ||
/// } | ||
/// | ||
/// - Parameter surfaceAppearance: The surface appearance to set. | ||
public func floatingPanelSurfaceAppearance( | ||
_ surfaceAppearance: SurfaceAppearance | ||
) -> some View { | ||
environment(\.surfaceAppearance, surfaceAppearance) | ||
} | ||
} |
Oops, something went wrong.