目前我有一个看起来像这样的视图。
struct StatsView: View {
var body: some View {
ScrollView {
Text("Test1")
Text("Test2")
Text("Test3")
}
}
}
这会渲染一个在滚动视图中包含 3 个文本的视图,每当我在屏幕中拖动这些文本中的任何一个时,视图都会移动,导致其可滚动,即使这 3 个文本适合屏幕并且有剩余空间。我想要实现的是仅当 ScrollView 的内容超过屏幕高度尺寸时才使其可滚动,如果不是,我希望视图是静态的并且不移动。我尝试使用 GeometryReader 并将滚动视图框架设置为屏幕宽度和高度,内容也相同,但我仍然有相同的行为,我也尝试设置 minHeight、maxHeight 但没有任何运气。
我怎样才能实现这个目标?
由于某种原因,我无法完成上述任何一项工作,但它确实激励我找到了适合我的情况的解决方案。它不像其他的那么灵活,但可以很容易地适应支持两个滚动轴。
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()
如果滚动视图的内容不需要用户交互(如 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)
}
}
}
注意:
ViewHeightKey
首选项键取自这是我的解决方案
我的解决方案不会禁用内容交互性
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.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")
}
}
}
我为这个问题制作了一个更全面的组件,适用于所有类型的轴集:
代码
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()
注意
此代码在这些情况下无法工作:
iOS 16.4 后: 您现在可以使用新的 ScrollView 修饰符之一,例如:
var body: some View {
ScrollView {
//your Content
}
.scrollBounceBehaviour(.basedOnSize, axes: .vertical)
}
iOS16 后: 我会使用 ViewThatFits 的模式匹配特性:
var body: some View {
ViewThatFits {
//your Content
ScrollView {
//same Content
}
}
}
基于 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>)
}
}
}
我无法发表评论,因为我没有足够的声誉,但我想在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)
}
}
根据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()
}
}
如果您需要监听字体大小的变化、上下文变化等,这可能会有所帮助。只需将
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..
}
以下解决方案允许您在内部使用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
}
}
}
不幸的是,这里的解决方案都不允许在打开辅助功能和动态增加字体大小时进行动态响应。希望有一个完整的解决方案,无需禁用滚动视图中的 UI。