0

I am trying to recreate the native .sheet() view modifier in SwiftUI. When I look at the definition, I get below function, but I'm not sure where to go from there.

The .sheet somehow passes a view WITH bindings to a distant parent at the top of the view-tree, but I can't see how that is done. If you use PreferenceKey with an AnyView, you can't have bindings.

My usecase is that I want to define a sheet in a subview, but I want to activate it at a distant parent-view to avoid it interfering with other code.

func showSheet<Content>(isPresented: Binding<Bool>, onDismiss: (() -> Void)? = nil, @ViewBuilder content: @escaping () -> Content) -> some View where Content : View {
    // What do I put here?
}
lmunck
  • 505
  • 4
  • 12
  • https://stackoverflow.com/questions/56700752/swiftui-half-modal/67994666#67994666 – lorem ipsum Sep 18 '21 at 14:37
  • This app is only on 14 for now, but looks interesting. – lmunck Sep 18 '21 at 15:08
  • similar concept. if you remove all the `@available(iOS 15.0, *)` and slowly start removing the iOS 15 code you will have a regular sheet the only thing that is iOS 15 in that sample is the stuff related to `adaptiveSheetPresentationController`/`detents`. You will have a regular sheet. – lorem ipsum Sep 18 '21 at 15:20
  • I just did, and I see what you do, but unfortunately it doesn't help my usecase. My usecase is that I need to activate a sheet in a distant parent because the sheet interferes with other code (I get a crash because I remove the view that the sheet is activated from as part of onDismiss). – lmunck Sep 18 '21 at 18:12
  • I thought I could modify the sheet to do that, but I can see from your code that the "zoom out background as modal slides in"-animation is baked into the UIKit view itself, so that doesn't help me. – lmunck Sep 18 '21 at 18:14
  • I guess what I really need is some way to pass a view with bindings up the view-tree :( – lmunck Sep 18 '21 at 18:15
  • That is what an [EnvironmentObject](https://developer.apple.com/documentation/swiftui/environmentobject) is for. Look at [this](https://developer.apple.com/documentation/swiftui/managing-model-data-in-your-app) for more. But if you are removing the view it might not work for you. Maybe if you put it on the uppermost view. Without a [Minimal Reproducible Example](https://stackoverflow.com/help/minimal-reproducible-example) it is impossible to help – lorem ipsum Sep 18 '21 at 18:57

1 Answers1

0

So, I ended up doing my own sheet in SwiftUI using a preferenceKey for passing the view up the view-tree, and an environmentObject for passing the binding for showing/hiding the sheet back down again.

It's a bit long-winded, but here's the gist of it:

    struct HomeOverlays<Content: View>: View {
        
        @Binding var showSheet:Bool
        @State private var sheet:EquatableViewContainer = EquatableViewContainer(id: "original", view: AnyView(Text("No view")))
        
        @State private var animatedSheet:Bool = false
        @State private var dragPercentage:Double = 0 /// 1 = fully visible, 0 = fully hidden
        // Content
        let content: Content
        
        init(_ showSheet: Binding<Bool>, @ViewBuilder content: @escaping () -> Content) {
            self._showSheet = showSheet
            self.content = content()
        }
        
        var body: some View {
            GeometryReader { geometry in
                
                ZStack {
                    content
                        .blur(radius: 5 * dragPercentage)
                        .opacity(1 - dragPercentage * 0.5)
                        .disabled(showSheet)
                        .scaleEffect(1 - 0.1 * dragPercentage)
                        .frame(width: geometry.size.width, height: geometry.size.height)
                    
                    if animatedSheet {
                        sheet.view
                            .background(Color.greyB.opacity(0.5).edgesIgnoringSafeArea(.bottom))
                            .cornerRadius(5)
                            .transition(.move(edge: .bottom).combined(with: .opacity))
                            .dragToSnap(snapPercentage: 0.3, dragPercentage: $dragPercentage) { showSheet = false } /// Custom modifier for measuring how far the view is dragged down. If more than 30% it snaps showSheet to false, and otherwise it snaps it back up again
                            .edgesIgnoringSafeArea(.bottom)
                        
                    }
                }
                .onPreferenceChange(HomeOverlaySheet.self, perform: { value in  self.sheet = value } )
                .onChange(of: showSheet) { show in sheetUpdate(show) }
                
            }
        }
        
        func sheetUpdate(_ show:Bool) {
            withAnimation(.easeOut(duration: 0.2)) {
                self.animatedSheet = show
                if show { dragPercentage = 1 } else { dragPercentage = 0 }
            }
            
            // Delay onDismiss action if removing sheet, so animation can complete
            if show == false {
                DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
                    sheet.action()
                }
            }
        }
    }

    struct HomeOverlays_Previews: PreviewProvider {
        static var previews: some View {
            HomeOverlays(.constant(false)) {
                Text("Home overlays")
            }
        }
    }

    // MARK: Preference key for passing view up the tree
    struct HomeOverlaySheet: PreferenceKey {
        static var defaultValue: EquatableViewContainer = EquatableViewContainer(id: "default", view: AnyView(EmptyView()) )
        
        static func reduce(value: inout EquatableViewContainer, nextValue: () -> EquatableViewContainer) {
            if value != nextValue() && nextValue().id != "default" {
                value = nextValue()
            }
        }
    }

    // MARK: View extension for defining view somewhere in view tree
    extension View {
        
        // Change only leading view
        func homeSheet<SheetView: View>(onDismiss action: @escaping () -> Void, @ViewBuilder sheet: @escaping () -> SheetView) -> some View {
            
            let sheet = sheet()
            
            return
                self
                .preference(key: HomeOverlaySheet.self, value: EquatableViewContainer(view: AnyView( sheet ), action: action ))
        }
        
    }
lmunck
  • 505
  • 4
  • 12