Skip to content

Instantly share code, notes, and snippets.

@kaishin
Last active March 18, 2023 12:14
Show Gist options
  • Save kaishin/a383348034caa987a78f572ca8ff52de to your computer and use it in GitHub Desktop.
Save kaishin/a383348034caa987a78f572ca8ff52de to your computer and use it in GitHub Desktop.
StickySheet – A sticky sheet implementation for SwiftUI
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
}
}
}
}
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