1

enter image description here

As you can see even though I am trying to pull the sheet down, the continue button does not move down. How can I make my sheet to behave like that? In my app the continue button moves offscreen. This is how my app looks when the sheet is pulled down slightly:

enter image description here

I have also attached my code below, it looks aesthetic on both landscape and portrait orientation. Is there a way to pull this off without ruining how it looks on landscape on smaller devices such as the iPhone 7?

import SwiftUI

struct IntroView: View {
    @State private var animationAmount: CGFloat = 1
    @Environment(\.presentationMode) var presentationMode
    @Environment(\.verticalSizeClass) var sizeClass
    
    var body: some View {
        VStack {
            VStack {
                Spacer()
                if sizeClass == .compact {
                    HStack {
                        Text("Welcome to Demo").fontWeight(.heavy)
                        Text("App").foregroundColor(.orange).fontWeight(.heavy)
                    }
                    .padding(.bottom, 10)
                }
                
                else {
                    Text("Welcome to").fontWeight(.heavy)
                    HStack {
                        Text("Demo").fontWeight(.heavy)
                        Text("App").foregroundColor(.orange).fontWeight(.heavy)
                    }
                    .padding(.bottom, 30)
                }
            }//Intro VStack close
            .font(.largeTitle)
            .frame(maxWidth: .infinity, maxHeight: 180)
            
            VStack (spacing: 30) {
                HStack (spacing: 20) {
                    Image(systemName: "sparkle")
                        .foregroundColor(.yellow)
                        .font(.title2)
                        .scaleEffect(animationAmount)
                        .onAppear {
                            let baseAnimation = Animation.easeInOut(duration: 1)
                            let repeated = baseAnimation.repeatForever(autoreverses: true)
                            return withAnimation(repeated) {
                                self.animationAmount = 1.5
                            }
                        }
                    VStack (alignment: .leading) {
                        Text("All new design").fontWeight(.semibold)
                        Text("Easily view all your essentials here.")
                            .foregroundColor(.gray)
                    }
                    Spacer()
                }//HStack 1
                .padding([.leading, .trailing], 10)
                
                HStack (spacing: 20) {
                    Image(systemName: "pin")
                        .foregroundColor(.red)
                        .font(.title2)
                        .padding(.trailing, 5)
                        .scaleEffect(animationAmount)
                        .onAppear {
                            let baseAnimation = Animation.easeInOut(duration: 1)
                            let repeated = baseAnimation.repeatForever(autoreverses: true)
                            return withAnimation(repeated) {
                                self.animationAmount = 1.5
                            }
                        }
                    VStack (alignment: .leading) {
                        Text("Pin favourites").fontWeight(.semibold)
                        Text("You can pin your favourite content on all devices")
                            .foregroundColor(.gray)
                    }
                    Spacer()
                }//HStack 2
                .padding([.leading, .trailing], 10)
                
                .frame(maxWidth: .infinity, maxHeight: 100)
                
                HStack (spacing: 20) {
                    Image(systemName: "moon.stars.fill")
                        .foregroundColor(.blue)
                        .font(.title2)
                        .scaleEffect(animationAmount)
                        .onAppear {
                            let baseAnimation = Animation.easeInOut(duration: 1)
                            let repeated = baseAnimation.repeatForever(autoreverses: true)
                            return withAnimation(repeated) {
                                self.animationAmount = 1.5
                            }
                        }
                    VStack (alignment: .leading) {
                        Text("Flexible").fontWeight(.semibold)
                        Text("Supports dark mode")
                            .foregroundColor(.gray)
                    }
                    Spacer()
                }//HStack 3
                .padding([.leading, .trailing], 10)
                
            }//VStack for 3 criterias
            .padding([.leading, .trailing], 20)
            
                Spacer()
            
            Button {
                presentationMode.wrappedValue.dismiss()
                UserDefaults.standard.set(true, forKey: "LaunchedBefore")
            } label: {
                Text("Continue")
                    .fontWeight(.medium)
                    .padding([.top, .bottom], 15)
                    .padding([.leading, .trailing], 90)
                    .background(Color.blue)
                    .foregroundColor(.white)
                    .cornerRadius(15)
            }
            .frame(maxWidth: .infinity, maxHeight: 100)

        }//Main VStack
    }
}
struct IntroView_Previews: PreviewProvider {
    static var previews: some View {
        IntroView()
    }
}
ItsZiaW
  • 180
  • 2
  • 14

2 Answers2

4

Here is a demo of possible approach (tuning & effects are out of scope - try to make demo code short). The idea is to inject UIView holder with button above sheet so it persist during sheet drag down (because as findings shown any dynamic offsets gives some ugly undesired shaking effects).

Tested with Xcode 12 / iOS 14

demo

            // ... your above code here

            }//VStack for 3 criterias
            .padding([.leading, .trailing], 20)

                Spacer()

             // button moved from here into below background view !!

        }.background(BottomView(presentation: presentationMode) {
            Button {
                presentationMode.wrappedValue.dismiss()
                UserDefaults.standard.set(true, forKey: "LaunchedBefore")
            } label: {
                Text("Continue")
                    .fontWeight(.medium)
                    .padding([.top, .bottom], 15)
                    .padding([.leading, .trailing], 90)
                    .background(Color.blue)
                    .foregroundColor(.white)
                    .cornerRadius(15)
            }
        })
        //Main VStack
    }
}

struct BottomView<Content: View>: UIViewRepresentable {
    @Binding var presentationMode: PresentationMode
    private var content: () -> Content

    init(presentation: Binding<PresentationMode>, @ViewBuilder _ content: @escaping () -> Content) {
        _presentationMode = presentation
        self.content = content
    }

    func makeUIView(context: Context) -> UIView {
        let view = UIView()

        DispatchQueue.main.async {
            if let window = view.window {
                let holder = UIView()
                context.coordinator.holder = holder

                // simple demo background to make it visible
                holder.layer.backgroundColor = UIColor.gray.withAlphaComponent(0.5).cgColor

                holder.translatesAutoresizingMaskIntoConstraints = false

                window.addSubview(holder)
                holder.heightAnchor.constraint(equalToConstant: 140).isActive = true
                holder.bottomAnchor.constraint(equalTo: window.bottomAnchor, constant: 0).isActive = true
                holder.leadingAnchor.constraint(equalTo: window.leadingAnchor, constant: 0).isActive = true
                holder.trailingAnchor.constraint(equalTo: window.trailingAnchor, constant: 0).isActive = true

                if let contentView = UIHostingController(rootView: content()).view {
                    contentView.backgroundColor = UIColor.clear
                    contentView.translatesAutoresizingMaskIntoConstraints = false
                    holder.addSubview(contentView)

                    contentView.topAnchor.constraint(equalTo: holder.topAnchor, constant: 0).isActive = true
                    contentView.bottomAnchor.constraint(equalTo: holder.bottomAnchor, constant: 0).isActive = true
                    contentView.leadingAnchor.constraint(equalTo: holder.leadingAnchor, constant: 0).isActive = true
                    contentView.trailingAnchor.constraint(equalTo: holder.trailingAnchor, constant: 0).isActive = true
                }
            }
        }
        return view
    }

    func updateUIView(_ uiView: UIView, context: Context) {
        if !presentationMode.isPresented {
            context.coordinator.holder.removeFromSuperview()
        }
    }

    func makeCoordinator() -> Coordinator {
        Coordinator()
    }

    class Coordinator {
        var holder: UIView!

        deinit {
            holder.removeFromSuperview()
        }
    }
}
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Asperi
  • 228,894
  • 20
  • 464
  • 690
  • Does this mean it's not possible to overlay the whole screen (including sheets) with another view in *pure* SwiftUI? – pawello2222 Aug 30 '20 at 20:20
  • 2
    @pawello2222, for now - yes it does. All SwiftUI view stack is rendered into root hosting view controller UIView, but sheet is shown in separated UIView, so to have something over it we need to create explicit UIView above all. – Asperi Aug 31 '20 at 05:51
1

Simply add that :

.sheet(isPresented: self.$visibleSheet) {
    IntroView(visibleSheet: self.$visibleSheet)
        .presentation(shouldDismissOnDrag: false)
}

https://stackoverflow.com/a/61239704/7974174 :

extension View {
   func presentation(shouldDismissOnDrag: Bool, onDismissalAttempt: (()->())? = nil) -> some View {
       ModalView(view: self, shouldDismiss: shouldDismissOnDrag, onDismissalAttempt: onDismissalAttempt)
   }
}

struct ModalView<T: View>: UIViewControllerRepresentable {
   let view: T
   let shouldDismiss: Bool
   let onDismissalAttempt: (()->())?
   
   func makeUIViewController(context: Context) -> UIHostingController<T> {
       UIHostingController(rootView: view)
   }
   
   func updateUIViewController(_ uiViewController: UIHostingController<T>, context: Context) {
       uiViewController.parent?.presentationController?.delegate = context.coordinator
   }
   
   func makeCoordinator() -> Coordinator {
       Coordinator(self)
   }
   
   class Coordinator: NSObject, UIAdaptivePresentationControllerDelegate {
       let modalView: ModalView
       
       init(_ modalView: ModalView) {
           self.modalView = modalView
       }
       
       func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool {
           modalView.shouldDismiss
       }
       
       func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) {
           modalView.onDismissalAttempt?()
       }
   }
}

It disables the sheet closing by dragging the sheet down. If you want to close the sheet with the button do not use presentationMode anymore. Pass a binding of self.$visibleSheet then modify to false from inside...

LetsGoBrandon
  • 498
  • 8
  • 23