SwiftUI:仅在超过屏幕高度时才使 ScrollView 可滚动
SwiftUI: Make ScrollView scrollable only if it exceeds the height of the screen
目前我有一个看起来像这样的视图。
struct StatsView: View {
var body: some View {
ScrollView {
Text("Test1")
Text("Test2")
Text("Test3")
}
}
}
这会在滚动视图中呈现一个包含 3 个文本的视图,每当我在屏幕中拖动这些文本中的任何一个时,视图都会移动导致其可滚动,即使这 3 个文本适合屏幕并且还有剩余 space。我想要实现的是仅当 ScrollView 的内容超过屏幕高度时才使其可滚动,否则,我希望视图是静态的并且不移动。我试过使用 GeometryReader 并将滚动视图框架设置为屏幕宽度和高度,内容也相同,但我仍然有相同的行为,我也试过设置 minHeight、maxHeight,但没有任何运气。
我怎样才能做到这一点?
如果滚动视图的内容不需要用户交互(如 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: [=10=].frame(in: .local).size.height) })
}
.onPreferenceChange(ViewHeightKey.self) {
self.fitInScreen = [=10=] < gp.size.height // << here !!
}
.disabled(self.fitInScreen)
}
}
}
注意: ViewHeightKey
偏好键取自
以下解决方案允许您在内部使用 Button:
Based on @Asperi solution
SpecialScrollView:
/// 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: [=10=].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: [=10=].frame(in: .local).size.height)})
}
.onPreferenceChange(SpecialViewHeightKey.self) {
self.fitInScreen = [=10=] < gp.size.height
}
}
}
}
}
struct SpecialViewHeightKey: PreferenceKey {
static var defaultValue: CGFloat { 0 }
static func reduce(value: inout Value, nextValue: () -> Value) {
value = value + nextValue()
}
}
USE:
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
}
}
}
我为这个问题制作了一个更全面的组件,适用于所有类型的轴组:
代码
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 [=10=].height <= geometry.size.height {
axes.remove(.vertical)
}
if [=10=].width <= geometry.size.width {
axes.remove(.horizontal)
}
}
}
}
}
}
private struct ContentSizeReader: View {
var body: some View {
GeometryReader {
Color.clear
.preference(
key: ContentSizeKey.self,
value: [=10=].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()
关注
此代码在这些情况下无法工作:
- 内容大小动态变化
- ScrollView 大小动态变化
- 设备方向改变
基于 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>)
}
}
}
出于某种原因,我无法完成上述任何一项工作,但它确实激励我找到了适合我的情况的解决方案。它不像其他的那样灵活,但可以很容易地进行调整以支持滚动的两个轴。
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()
我不能发表评论,因为我没有足够的声誉,但我想在 答案中添加评论。该扩展非常适合我,对于布尔值是否显示 scrollView,我使用此代码来了解设备的高度:
///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)
}
}
我的解决方案不会禁用内容交互性
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")
}
}
}
根据!回答,我创建了一个涵盖报告问题的自定义组件
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: [=10=].frame(in: .local).size.height) })
}
.onPreferenceChange(ViewHeightKey.self) {
self.fitInScreen = [=10=] < gp.size.height // << here !!
}
}
}
}
用法:
var body: some View {
SmartScrollView {
Content()
}
}
目前我有一个看起来像这样的视图。
struct StatsView: View {
var body: some View {
ScrollView {
Text("Test1")
Text("Test2")
Text("Test3")
}
}
}
这会在滚动视图中呈现一个包含 3 个文本的视图,每当我在屏幕中拖动这些文本中的任何一个时,视图都会移动导致其可滚动,即使这 3 个文本适合屏幕并且还有剩余 space。我想要实现的是仅当 ScrollView 的内容超过屏幕高度时才使其可滚动,否则,我希望视图是静态的并且不移动。我试过使用 GeometryReader 并将滚动视图框架设置为屏幕宽度和高度,内容也相同,但我仍然有相同的行为,我也试过设置 minHeight、maxHeight,但没有任何运气。
我怎样才能做到这一点?
如果滚动视图的内容不需要用户交互(如 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: [=10=].frame(in: .local).size.height) })
}
.onPreferenceChange(ViewHeightKey.self) {
self.fitInScreen = [=10=] < gp.size.height // << here !!
}
.disabled(self.fitInScreen)
}
}
}
注意: ViewHeightKey
偏好键取自
以下解决方案允许您在内部使用 Button:
Based on @Asperi solution
SpecialScrollView:
/// 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: [=10=].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: [=10=].frame(in: .local).size.height)})
}
.onPreferenceChange(SpecialViewHeightKey.self) {
self.fitInScreen = [=10=] < gp.size.height
}
}
}
}
}
struct SpecialViewHeightKey: PreferenceKey {
static var defaultValue: CGFloat { 0 }
static func reduce(value: inout Value, nextValue: () -> Value) {
value = value + nextValue()
}
}
USE:
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
}
}
}
我为这个问题制作了一个更全面的组件,适用于所有类型的轴组:
代码
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 [=10=].height <= geometry.size.height {
axes.remove(.vertical)
}
if [=10=].width <= geometry.size.width {
axes.remove(.horizontal)
}
}
}
}
}
}
private struct ContentSizeReader: View {
var body: some View {
GeometryReader {
Color.clear
.preference(
key: ContentSizeKey.self,
value: [=10=].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()
关注
此代码在这些情况下无法工作:
- 内容大小动态变化
- ScrollView 大小动态变化
- 设备方向改变
基于 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>)
}
}
}
出于某种原因,我无法完成上述任何一项工作,但它确实激励我找到了适合我的情况的解决方案。它不像其他的那样灵活,但可以很容易地进行调整以支持滚动的两个轴。
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()
我不能发表评论,因为我没有足够的声誉,但我想在
///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)
}
}
我的解决方案不会禁用内容交互性
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")
}
}
}
根据
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: [=10=].frame(in: .local).size.height) })
}
.onPreferenceChange(ViewHeightKey.self) {
self.fitInScreen = [=10=] < gp.size.height // << here !!
}
}
}
}
用法:
var body: some View {
SmartScrollView {
Content()
}
}