使ScrollView仅在超过窗口高度时才可滚动

问题描述 投票:0回答:12

目前我的看法是这样的:

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

这会渲染一个在滚动视图中包含 3 个文本的视图。每当我将这些文本中的任何一个拖动到窗口内时,视图都会移动,因为它是可滚动的,即使这 3 个文本适合窗口并且有剩余空间。
我想要实现的是仅当

ScrollView
的内容超出窗口高度时才使其可滚动。如果没有,我希望视图是静态的而不是移动的。

我尝试使用

GeometryReader
并将滚动视图的框架设置为窗口宽度和高度,内容也相同,但我仍然有相同的行为。我也尝试过设置
minHeight
maxHeight
但没有任何运气。

swift swiftui scrollview
12个回答
35
投票

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

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()

25
投票

iOS 16.4 后: 您现在可以使用 scrollBounceBehavior(_:axes:) 修饰符:

var body: some View {
   ScrollView {
      // your content
   }
   .scrollBounceBehavior(.basedOnSize, axes: [.vertical])
}

iOS16 后: 我会使用 ViewThatFits 的模式匹配特性:

var body: some View {
   ViewThatFits {
      // your content
      ScrollView {
          // same content
      }
   }
}

20
投票

如果滚动视图的内容不需要用户交互(如 PO 问题中所示),这是一种可能的方法:

使用 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)              // for iOS <16
            //.scrollDisabled(self.fitInScreen)      // for iOS 16+
        }
    }
}

注意:

ViewHeightKey
首选项键取自这是我的解决方案


13
投票

我的解决方案不会禁用内容交互性

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 in
                Color.clear.onAppear() {
                    DispatchQueue.main.async {
                         size = geometry.size
                    }
                }
            }
        )
    }
}

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")
        }
    }
}

8
投票

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

代码

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. 设备方向改变

5
投票

基于 Asperi 的答案,当我们知道内容即将溢出时,我们可以有条件地用

ScrollView
包裹视图。这是您可以创建的视图扩展:

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>)
        }
    }
}

3
投票

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

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

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

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

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

2
投票

根据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()
    }
}

1
投票

如果您需要监听字体大小的变化、上下文变化等,这可能会有所帮助。只需将

viewIndex
更改为您需要的更改标识符即可。

此视图将通知您是否滚动,以及原始内容是否适合滚动视图或是否可滚动。

希望它对某人有帮助:)

import Combine
import SwiftUI

struct FeedbackScrollView<Content: View>: View {
    
    /// Used to inform the FeedbackScrollView if the view changes (mainly used in 'flows')
    var viewIndex: Double
    /// Notifies if the scrollview is scrolled
    @Binding var scrollViewIsScrolled: Bool
    /// Notifies if the scrollview has overflow in it's content, to indicate if it can scroll or now
    @Binding var scrollViewCanScroll: Bool
    /// The content you want to put into the scrollview.
    @ViewBuilder private let content: () -> Content
    
    public init(
        viewIndex: Double = 0,
        scrollViewIsScrolled: Binding<Bool> = .constant(false),
        scrollViewCanScroll: Binding<Bool>,
        @ViewBuilder content: @escaping () -> Content
    ) {
        self.viewIndex = viewIndex
        self._scrollViewIsScrolled = scrollViewIsScrolled
        self._scrollViewCanScroll = scrollViewCanScroll
        self.content = content
    }
    
    var body: some View {
        GeometryReader { geometry in
            ScrollView {
                offsetReader
                content()
                    .frame(
                        minHeight: geometry.size.height,
                        alignment: .topLeading
                    )
                    .background(
                        GeometryReader { contentGeometry in
                            Color.clear
                                .onAppear {
                                    scrollViewCanScroll = contentGeometry.size.height > geometry.size.height
                                }
                                .onChange(of: viewIndex) { _ in
                                    scrollViewCanScroll = contentGeometry.size.height > geometry.size.height
                                }
                        }
                    )
            }
            .dismissKeyboardOnDrag()
            .coordinateSpace(name: "scrollSpace")
            .onPreferenceChange(OffsetPreferenceKey.self, perform: offsetChanged(offset:))
        }
    }
    
    var offsetReader: some View {
        GeometryReader { proxy in
            Color.clear
                .preference(
                    key: OffsetPreferenceKey.self,
                    value: proxy.frame(in: .named("scrollSpace")).minY
                )
        }
        .frame(height: 0)
    }
    
    private func offsetChanged(offset: CGFloat) {
        withAnimation {
            scrollViewIsScrolled = offset < 0
        }
    }
}

private struct OffsetPreferenceKey: PreferenceKey {
    static var defaultValue: CGFloat = .zero
    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {}
}

struct FeedbackScrollView_Previews: PreviewProvider {
    static var previews: some View {
        FeedbackScrollView(
            viewIndex: 0,
            scrollViewIsScrolled: .constant(false),
            scrollViewCanScroll: .constant(true)
        ) { }
    }
}

像这样使用它:

...

@State var scrollViewIsScrolled: Bool
@State var scrollViewCanScroll: Bool

FeedbackScrollView(
   viewIndex: numberOfCompletedSteps,
   scrollViewIsScrolled: $scrollViewIsScrolled,
   scrollViewCanScroll: $scrollViewCanScroll
) {
    // Your (scrollable) content goes here..
}

0
投票

以下解决方案允许您在内部使用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
        }
    }
}

0
投票

不幸的是,这里的解决方案都不允许在打开辅助功能和动态增加字体大小时进行动态响应。希望有一个完整的解决方案,无需禁用滚动视图中的 UI。


-1
投票

如果根据某些条件需要禁用滚动,这可能会有所帮助。从 iOS 16 开始就有可能:

var body: some View {
    ScrollView {
        // Content
    }
    .scrollDisabled(yourFlag)
}
© www.soinside.com 2019 - 2024. All rights reserved.