skip to Main Content

I have a view

struct Services: View {
  @State private var isFirstSheetOpen = false
  @State private var isSecondSheetOpen = false

  var body: some View {
     Button("Open sheet") {
       isFirstSheetOpen.toggle() // turns true
     }.sheet(isPresented: $isFirstSheetOpen) {
        Button("Open second sheet") {
          isFirstSheetOpen.toggle() // turns false
          isFirstSecondOpen.toggle() // turns true
        }.sheet(isPresented: $isSecondSheetOpen) {
          Text("Second sheet")
        }
     }
  }
}

I want to achieve something like Telegram has.

When opening the second sheet the first one should close (with animation).

https://s4.gifyu.com/images/IMG_8720.gif

I have two problems with my code.

  1. If I put sheets nested (like in the example above) it closes the first one, then again opens it, even before opening the second sheet.

  2. If I put sheets like this

// cut
   Button() {
   }.shet() { /*...*/ }
    .shet() { /*...*/ }
// cut

It replaces the sheets immediately. If I wrap it inside

DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
  isSecondSheetOpen = true
}

animation takes too long (event with a small delay).

Could you help me to achieve exactly the same animation as shown in Gif?

2

Answers


  1. You can use sheet(item:) instead of isPresented, which will close a previous sheet and open a new one when the item changes:

    struct SheetContent: Identifiable, Hashable {
        var id = UUID()
        var text: String
    }
    
    struct ContentView: View {
        
        @State var content: [SheetContent] = [.init(text: "1"), .init(text: "2"), .init(text: "3")]
        @State var presented: SheetContent?
    
        var body: some View {
            Button("Open") {
                presented = content.first
            }
            .sheet(item: $presented) { item in
                Text(item.text)
                Button("New sheet") {
                    presented = content.filter { $0.id != presented?.id }.randomElement()
                }
            }
        }
    }
    
    Login or Signup to reply.
  2. You can use Combine to trigger a second boolean after whatever interval.

    
    import SwiftUI
    import Combine
    
    class ViewModel: ObservableObject {
        @Published var showFirstSheet = false
        @Published var willShowSecondSheet = false
        @Published var showSecondSheet = false
    
        private var cancellable: AnyCancellable?
        
        init() {
            cancellable = $willShowSecondSheet
                .dropFirst()
                .debounce(for: .seconds(0.2), scheduler: DispatchQueue.main)
                .sink { [weak self] newValue in
                    // Perform the desired action after the debounce interval
                    self?.showSecondSheet.toggle()
                }
        }
    }
    
    
    struct BottomDrawerView: View {
        @StateObject private var viewModel = ViewModel()
    
        var body: some View {
            VStack {
                Button("Show First Sheet") {
                    viewModel.showFirstSheet.toggle()
                }
            }
            .sheet(isPresented: $viewModel.showFirstSheet) {
                VStack {
                    Text("First Sheet")
                        .padding()
                    Button("Show Second Sheet") {
                        viewModel.showFirstSheet.toggle()
                        viewModel.willShowSecondSheet.toggle()
                    }
                }
                .presentationDetents([.medium, .large])
            }
            .sheet(isPresented: $viewModel.showSecondSheet) {
                Text("Second Sheet")
                    .presentationDetents([.medium, .large])
            }
            
        }
    }
    
    struct BottomDrawerView_Previews: PreviewProvider {
        static var previews: some View {
            BottomDrawerView()
        }
    }
    
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search