【问题标题】:SwiftUI: Make ScrollView scrollable only if it exceeds the height of the screenSwiftUI:仅在超出屏幕高度时才使 ScrollView 可滚动
【发布时间】:2020-10-09 06:53:52
【问题描述】:

目前我有一个看起来像这样的视图。

struct StatsView: View {
    var body: some View {
        ScrollView {
            Text("Test1")
            Text("Test2")
            Text("Test3")
        }
    }
}

这会在滚动视图中呈现一个包含 3 个文本的视图,每当我在屏幕中拖动这些文本中的任何一个时,视图都会移动导致其可滚动,即使这 3 个文本适合屏幕并且还有剩余空间。我想要实现的是仅在其内容超过屏幕高度大小时才使 ScrollView 可滚动,如果不是,我希望视图是静态的并且不要移动。我尝试使用 GeometryReader 并将滚动视图框架设置为屏幕宽度和高度,内容也相同,但我仍然有相同的行为,我也尝试过设置 minHeight、maxHeight,但没有任何运气。

我怎样才能做到这一点?

【问题讨论】:

  • 如果默认情况下它的内容不超过屏幕高度,我认为滚动视图不会滚动。根据您的描述,我认为它正在弹跳(如果它在离开触摸后回到初始位置)。尝试设置 scrollView.alwaysBounceHorizo​​ntal = false & scrollView.bounces = false 并检查它是否有效
  • @MuhammadAli,这是关于 SwiftUI 的,ScrollView 不像 UIKit 中的 UIScrollView 那样有 alwaysBounceHorizontalbounces,所以下次评论之前要注意。
  • @Asperi 至少 MuhammedAli 指出这是弹跳行为。因此,这听起来有点像 how to disable scrollview bounce in swiftui 的重复。那里接受的答案有一些缺陷,所以你可能也想在那里添加你的答案。

标签: ios swift swiftui scrollview


【解决方案1】:

根据Asperi!回答,我创建了一个涵盖报告问题的自定义组件

private struct ViewHeightKey: PreferenceKey {
    static var defaultValue: CGFloat { 0 }
    static func reduce(value: inout Value, nextValue: () -> Value) {
        value = value + nextValue()
    }
}

struct SmartScrollView<Content: View>: View {
    @State private var fitInScreen = false
    @State var axes = Axis.Set.vertical
    
    let content: () -> Content
    
    var body: some View {
        GeometryReader { gp in
            ScrollView(axes) {
                content()
                    .onAppear {
                        axes = fitInScreen ? [] : .vertical
                    }
                    
                .background(GeometryReader {
                    // calculate height by consumed background and store in
                    // view preference
                    Color.clear.preference(key: ViewHeightKey.self,
                        value: $0.frame(in: .local).size.height) })
                
            }
            .onPreferenceChange(ViewHeightKey.self) {
                 self.fitInScreen = $0 < gp.size.height    // << here !!
            }
            
           
        }
        
    }
    
}

用法:

var body: some View {
    SmartScrollView {
        Content()
    }
}

【讨论】:

    【解决方案2】:

    我的解决方案没有禁用内容交互

    struct ScrollViewIfNeeded<Content: View>: View {
        @ViewBuilder let content: () -> Content
    
        @State private var scrollViewSize: CGSize = .zero
        @State private var contentSize: CGSize = .zero
    
        var body: some View {
            ScrollView(shouldScroll ? [.vertical] : []) {
                content().readSize($contentSize)
            }
            .readSize($scrollViewSize)
        }
    
        private var shouldScroll: Bool {
            scrollViewSize.height <= contentSize.height
        }
    }
    
    struct SizeReaderModifier: ViewModifier  {
        @Binding var size: CGSize
        
        func body(content: Content) -> some View {
            content.background(
                GeometryReader { geometry -> Color in
                    DispatchQueue.main.async {
                        size = geometry.size
                    }
                    return Color.clear
                }
            )
        }
    }
    
    extension View {
        func readSize(_ size: Binding<CGSize>) -> some View {
            self.modifier(SizeReaderModifier(size: size))
        }
    }
    

    用法:

    struct StatsView: View {
        var body: some View {
            ScrollViewIfNeeded {
                Text("Test1")
                Text("Test2")
                Text("Test3")
            }
        }
    }
    

    【讨论】:

      【解决方案3】:

      我无法发表评论,因为我没有足够的声誉,但我想在happymacaron 答案中添加评论。该扩展对我来说非常有效,对于布尔值是否显示滚动视图,我使用此代码来了解设备的高度:

      ///Device screen
      var screenDontFitInDevice: Bool {
          UIScreen.main.bounds.size.height < 700 ? true : false
      }
      

      所以,通过这个 var,我可以判断设备高度是否小于 700,如果它是真的,我想让视图可滚动,以便内容可以毫无问题地显示。

      所以文应用扩展我只是这样做:

      struct ForgotPasswordView: View {
          var body: some View {
              VStack {
                  Text("Scrollable == \(viewModel.screenDontFitInDevice)")
              }
              .useScrollView(when: viewModel.screenDontFitInDevice, showsIndicators: false)
          
          }
      }
      

      【讨论】:

        【解决方案4】:

        由于某种原因,我无法完成上述任何一项工作,但它确实激励我找到了适合我的解决方案。它不像其他的那样灵活,但可以很容易地适应支持两个滚动轴。

        import SwiftUI
        
        struct OverflowContentViewModifier: ViewModifier {
            @State private var contentOverflow: Bool = false
            
            func body(content: Content) -> some View {
                GeometryReader { geometry in
                    content
                    .background(
                        GeometryReader { contentGeometry in
                            Color.clear.onAppear {
                                contentOverflow = contentGeometry.size.height > geometry.size.height
                            }
                        }
                    )
                    .wrappedInScrollView(when: contentOverflow)
                }
            }
        }
        
        extension View {
            @ViewBuilder
            func wrappedInScrollView(when condition: Bool) -> some View {
                if condition {
                    ScrollView {
                        self
                    }
                } else {
                    self
                }
            }
        }
        
        extension View {
            func scrollOnOverflow() -> some View {
                modifier(OverflowContentViewModifier())
            }
        }
        

        用法

        VStack {
           // Your content
        }
        .scrollOnOverflow()
        

        【讨论】:

          【解决方案5】:

          基于 Asperi 的回答,当我们知道内容将溢出时,我们可以有条件地使用 ScrollView 包装视图。这是您可以创建的 View 扩展:

          extension View {
            func useScrollView(
              when condition: Bool,
              showsIndicators: Bool = true
            ) -> AnyView {
              if condition {
                return AnyView(
                  ScrollView(showsIndicators: showsIndicators) {
                    self
                  }
                )
              } else {
                return AnyView(self)
              }
            }
          }
          

          在主视图中,只需使用您的逻辑检查视图是否太长,可能使用GeometryReader 和背景颜色技巧:

          struct StatsView: View {
              var body: some View {
                      VStack {
                          Text("Test1")
                          Text("Test2")
                          Text("Test3")
                      }
                      .useScrollView(when: <an expression you write to decide if the view fits, maybe using GeometryReader>)
                  }
              }
          }
          

          【讨论】:

            【解决方案6】:

            我为这个问题制作了一个更全面的组件,它适用于所有类型的轴组:

            代码

            struct OverflowScrollView<Content>: View where Content : View {
                
                @State private var axes: Axis.Set
                
                private let showsIndicator: Bool
                
                private let content: Content
                
                init(_ axes: Axis.Set = .vertical, showsIndicators: Bool = true, @ViewBuilder content: @escaping () -> Content) {
                    self._axes = .init(wrappedValue: axes)
                    self.showsIndicator = showsIndicators
                    self.content = content()
                }
            
                fileprivate init(scrollView: ScrollView<Content>) {
                    self._axes = .init(wrappedValue: scrollView.axes)
                    self.showsIndicator = scrollView.showsIndicators
                    self.content = scrollView.content
                }
            
                public var body: some View {
                    GeometryReader { geometry in
                        ScrollView(axes, showsIndicators: showsIndicator) {
                            content
                                .background(ContentSizeReader())
                                .onPreferenceChange(ContentSizeKey.self) {
                                    if $0.height <= geometry.size.height {
                                        axes.remove(.vertical)
                                    }
                                    if $0.width <= geometry.size.width {
                                        axes.remove(.horizontal)
                                    }
                                }
                        }
                    }
                }
            }
            
            private struct ContentSizeReader: View {
                
                var body: some View {
                    GeometryReader {
                        Color.clear
                            .preference(
                                key: ContentSizeKey.self,
                                value: $0.frame(in: .local).size
                            )
                    }
                }
            }
            
            private struct ContentSizeKey: PreferenceKey {
                static var defaultValue: CGSize { .zero }
                static func reduce(value: inout Value, nextValue: () -> Value) {
                    value = CGSize(width: value.width+nextValue().width,
                                   height: value.height+nextValue().height)
                }
            }
            
            // MARK: - Implementation
            
            extension ScrollView {
                
                public func scrollOnlyOnOverflow() -> some View {
                    OverflowScrollView(scrollView: self)
                }
            }
            

            用法

            ScrollView([.vertical, .horizontal]) {
                Text("Ciao")
            }
            .scrollOnlyOnOverflow()
            

            注意

            此代码在这些情况下无法工作:

            1. 内容大小动态变化
            2. ScrollView 大小动态变化
            3. 设备方向更改

            【讨论】:

              【解决方案7】:

              下面的解决方案可以让你在里面使用Button:

              基于@Asperi 解决方案

              特殊滚动视图:

              /// Scrollview disabled if smaller then content view
              public struct SpecialScrollView<Content> : View where Content : View {
              
                  let content: Content
              
                  @State private var fitInScreen = false
              
                  public init(@ViewBuilder content: () -> Content) {
                      self.content = content()
                  }
                  
                  public var body: some View {
                      if fitInScreen == true {
                          ZStack (alignment: .topLeading) {
                              content
                                  .background(GeometryReader {
                                                  Color.clear.preference(key: SpecialViewHeightKey.self,
                                                                         value: $0.frame(in: .local).size.height)})
                                  .fixedSize()
                              Rectangle()
                                  .foregroundColor(.clear)
                                  .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
                          }
                      }
                      else {
                          GeometryReader { gp in
                              ScrollView {
                                  content
                                      .background(GeometryReader {
                                                      Color.clear.preference(key: SpecialViewHeightKey.self,
                                                                             value: $0.frame(in: .local).size.height)})
                              }
                              .onPreferenceChange(SpecialViewHeightKey.self) {
                                   self.fitInScreen = $0 < gp.size.height
                              }
                          }
                      }
                  }
              }
              
              struct SpecialViewHeightKey: PreferenceKey {
                  static var defaultValue: CGFloat { 0 }
                  static func reduce(value: inout Value, nextValue: () -> Value) {
                      value = value + nextValue()
                  }
              }
              

              使用:

              struct SwiftUIView6: View {
                      
              @State private var fitInScreen = false
                  var body: some View {
                      
                      VStack {
                          Text("\(fitInScreen ? "true":"false")")
                          SpecialScrollView {
                              ExtractedView()
                          }
                      }
                  }
              }
              
              
              
              struct SwiftUIView6_Previews: PreviewProvider {
                  static var previews: some View {
                      SwiftUIView6()
                  }
              }
              
              struct ExtractedView: View {
                  @State var text:String = "Text"
                  var body: some View {
                      VStack {          // container to calculate total height
                          Text(text)
                              .onTapGesture {text = text == "TextModified" ? "Text":"TextModified"}
                          Text(text)
                              .onTapGesture {text = text == "TextModified" ? "Text":"TextModified"}
                          Text(text)
                              .onTapGesture {text = text == "TextModified" ? "Text":"TextModified"}
                          Spacer()
                          //ForEach(0..<50) { _ in Text(text).onTapGesture {text = text == "TextModified" ? "Text":"TextModified"} } // uncomment for test
                      }
                  }
              }
              

              【讨论】:

                【解决方案8】:

                这是一个解决方案(使用 Xcode 11.4 / iOS 13.4 测试)

                struct StatsView: View {
                    @State private var fitInScreen = false
                    var body: some View {
                        GeometryReader { gp in
                            ScrollView {
                                VStack {          // container to calculate total height
                                    Text("Test1")
                                    Text("Test2")
                                    Text("Test3")
                                    //ForEach(0..<50) { _ in Text("Test") } // uncomment for test
                                }
                                .background(GeometryReader {
                                    // calculate height by consumed background and store in 
                                    // view preference
                                    Color.clear.preference(key: ViewHeightKey.self,
                                        value: $0.frame(in: .local).size.height) })
                            }
                            .onPreferenceChange(ViewHeightKey.self) {
                                 self.fitInScreen = $0 < gp.size.height    // << here !!
                            }
                            .disabled(self.fitInScreen)
                        }
                    }
                }
                

                注意: ViewHeightKey 偏好键取自 this my solution

                【讨论】:

                • 似乎禁用 ScrollView 也会禁用 ScrollView 中的所有其他交互元素。是否有只禁用滚动功能而不影响 ScrollView 内容的修饰符?
                • 使用 SwiftUI,我们现在是否开始在 iOS 开发中滥用 API 方法来寻找视觉解决方案?在这个“解决方案”中,应该清楚地提到disabled 禁用其中包含的所有控件。这会影响用户交互和样式(例如,如果嵌套了 Button)。使用 GeometryReader、条件视图和查看内容的解决方案将是正确的方式,imo。
                • @Ienny 请查看我的帖子
                • 让它更通用一点,并将其打包到这个 GitHub 项目中:ScrollViewIfNeeded
                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 2020-03-13
                • 2019-10-05
                • 1970-01-01
                • 2013-09-24
                • 1970-01-01
                • 2021-12-31
                • 2020-06-24
                相关资源
                最近更新 更多