Last active
March 18, 2023 12:14
-
-
Save kaishin/a383348034caa987a78f572ca8ff52de to your computer and use it in GitHub Desktop.
StickySheet – A sticky sheet implementation for SwiftUI
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
enum PresentedSheet: String, Identifiable { | |
case sticky, standard | |
var id: String { | |
return self.rawValue | |
} | |
} | |
extension String: Identifiable { | |
public var id: String { | |
return self | |
} | |
} | |
struct StickySheetScreen: View { | |
@State var sheet: PresentedSheet? = nil | |
@State var showDemo: String? = nil | |
var body: some View { | |
VStack { | |
Text("Sticky Sheet").font(.title) | |
Button("Present Sticky") { | |
self.sheet = .sticky | |
}.padding(20) | |
Button("Present Standard") { | |
self.sheet = .standard | |
} | |
}.stickySheet(item: self.$sheet, destination: { item in | |
if item == .sticky { | |
ChildView() | |
} else { | |
Text("This view doesn't care about being dismissed.").shouldDismiss(true) | |
} | |
}) | |
} | |
} | |
struct ChildView: View { | |
@Environment(\.presentationMode) var presentationMode | |
@EnvironmentObject var sheetState: StickySheetState | |
@State var disableDismiss = true | |
@State var showAlert = true | |
@State var dismissAttempted = true | |
var body: some View { | |
NavigationView { | |
Form { | |
Toggle(isOn: $disableDismiss) { | |
Text("Prevent Dismissing") | |
} | |
Toggle(isOn: $showAlert) { | |
Text("Show Alert") | |
} | |
} | |
.alert(isPresented: self.$dismissAttempted) { | |
Alert(title: Text("Required Action"), | |
message: Text("You want to dismiss this sheet, but we can't let you now."), | |
primaryButton: .destructive(Text("Dismiss Anyway"), action: { | |
self.presentationMode.wrappedValue.dismiss() | |
}), | |
secondaryButton: .cancel(Text("Continue Editing"))) | |
} | |
.navigationBarTitle("Sticky Sheet", displayMode: .inline) | |
.navigationBarItems(trailing: | |
Button("core_cmd_done") { | |
self.presentationMode.wrappedValue.dismiss() | |
} | |
) | |
} | |
.environment(\.horizontalSizeClass, .compact) | |
.shouldDismiss(!disableDismiss) | |
.onReceive(self.sheetState.$dismissAttempted) { value in | |
if self.showAlert { | |
self.dismissAttempted = value | |
} | |
} | |
} | |
} |
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
import SwiftUI | |
public final class StickySheetState: ObservableObject { | |
@Published public var dismissAttempted: Bool = false | |
} | |
struct StickySheetShouldDismissPreferenceKey: PreferenceKey { | |
typealias Value = Bool | |
static var defaultValue: Value = false | |
static func reduce(value: inout Value, nextValue: () -> Value) { | |
value = nextValue() | |
} | |
} | |
struct StickySheetWrapper<Content: View>: UIViewControllerRepresentable { | |
@Binding var shouldDismiss: Bool | |
@Binding var attempted: Bool | |
var contentView: Content | |
typealias UIViewControllerType = StickySheetHostingController<Content> | |
init(shouldDismiss: Binding<Bool>, attempted: Binding<Bool>, @ViewBuilder content: @escaping () -> Content) { | |
self.contentView = content() | |
self._shouldDismiss = shouldDismiss | |
self._attempted = attempted | |
} | |
func makeUIViewController(context: UIViewControllerRepresentableContext<StickySheetWrapper>) -> UIViewControllerType { | |
return StickySheetHostingController(rootView: contentView, shouldDimiss: shouldDismiss) | |
} | |
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) { | |
uiViewController.rootView = contentView | |
uiViewController.shouldDismiss = shouldDismiss | |
uiViewController.stickySheetDelegate = context.coordinator | |
} | |
func makeCoordinator() -> StickySheetWrapper<Content>.Coordinator { | |
return Coordinator(attempted: $attempted) | |
} | |
class Coordinator: NSObject, StickySheetDelegate { | |
@Binding var attempted: Bool | |
init(attempted: Binding<Bool>) { | |
self._attempted = attempted | |
} | |
func updateAttempted(flag: Bool) { | |
self.attempted = flag | |
} | |
} | |
} | |
protocol StickySheetDelegate { | |
func updateAttempted(flag: Bool) | |
} | |
class StickySheetHostingController<Content>: UIHostingController<Content>, UIAdaptivePresentationControllerDelegate where Content: View { | |
var shouldDismiss: Bool | |
var stickySheetDelegate: StickySheetDelegate? | |
init(rootView: Content, shouldDimiss: Bool) { | |
self.shouldDismiss = shouldDimiss | |
super.init(rootView: rootView) | |
} | |
@objc required dynamic init?(coder aDecoder: NSCoder) { | |
fatalError("init(coder:) has not been implemented") | |
} | |
override func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) { | |
viewControllerToPresent.presentationController?.delegate = self | |
self.stickySheetDelegate?.updateAttempted(flag: false) | |
super.present(viewControllerToPresent, animated: flag, completion: completion) | |
} | |
func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) { | |
stickySheetDelegate?.updateAttempted(flag: true) | |
} | |
func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool { | |
return shouldDismiss | |
} | |
} | |
struct StickySheetModifier<Item, DestinationView>: ViewModifier where Item: Identifiable, DestinationView: View { | |
@State var shouldDismiss = false | |
@State var sheetState = StickySheetState() | |
@Binding var item: Item? | |
var destination: (Item) -> DestinationView | |
var onDismiss: (() -> Void)? | |
init(item: Binding<Item?>, | |
onDismiss: (() -> Void)? = nil, | |
@ViewBuilder destination: @escaping (Item) -> DestinationView) { | |
self._item = item | |
self.destination = destination | |
self.onDismiss = onDismiss | |
} | |
func body(content: Content) -> some View { | |
StickySheetWrapper(shouldDismiss: $shouldDismiss, | |
attempted: $sheetState.dismissAttempted) { | |
content | |
.sheet(item: self.$item, onDismiss: self.onDismiss) { | |
self.destination($0) | |
.onPreferenceChange(StickySheetShouldDismissPreferenceKey.self) { value in | |
self.$shouldDismiss.wrappedValue = value | |
} | |
.environmentObject(self.sheetState) | |
} | |
} | |
} | |
} | |
public extension View { | |
func shouldDismiss(_ value: Bool) -> some View { | |
return preference(key: StickySheetShouldDismissPreferenceKey.self, value: value) | |
} | |
func stickySheet<Item: Identifiable, DestinationView: View>(item: Binding<Item?>, | |
onDismiss: (() -> Void)? = nil, | |
@ViewBuilder destination: @escaping (Item) -> DestinationView) -> some View { | |
modifier(StickySheetModifier(item: item, onDismiss: onDismiss, destination: destination)) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment