SwiftUI-シートにはスクロールできない固定の続行ボタンがあります

Aug 22 2020

シートを引き下げようとしているのに、続行ボタンが下がらないことがわかります。シートをそのように動作させるにはどうすればよいですか?私のアプリでは、続行ボタンが画面外に移動します。シートを少し引き下げたときのアプリの外観は次のとおりです。

以下にコードを添付しました。横向きと縦向きの両方で見栄えがします。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()
    }
}

回答

3 Asperi Aug 29 2020 at 20:38

これが可能なアプローチのデモです(チューニングとエフェクトは範囲外です-デモコードを短くしてみてください)。アイデアは、シートの上にUIViewボタンが付いたホルダーを注入して、シートを下にドラッグしている間も持続するようにすることです(調査結果が示すように、動的オフセットはいくつかの醜い望ましくない振動効果を与えるため)。

Xcode 12 / iOS14でテスト済み

            // ... 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()
        }
    }
}
1 LorisFoe Aug 28 2020 at 20:07

単にそれを追加します:

.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?()
       }
   }
}

シートを下にドラッグして、シートを閉じることができなくなります。ボタンでシートを閉じたい場合は、presentationModeもう使用しないでください。のバインディングを渡し、self.$visibleSheet内部からfalseに変更します。