【发布时间】:2021-08-20 14:57:07
【问题描述】:
基于这个问题:SwiftUI - Drawing (curved) paths between views
我使用GeometryReader 绘制所需的视图和路径。问题是,现在屏幕显示太长了(有 15 个这样的视图,但您在这里只能看到 6 个):
所以...我在整个内容中添加了ScrollView。在GeometryReader 之外,否则所有视图都放错了位置。代码如下:
var body: some View {
if (challengeViewModel.challengeAmount != 0) {
ScrollView {
GeometryReader { geometry in
let points = calculatePoints(geometry: geometry)
drawPaths(geometry: geometry, points: points)
.stroke(lineWidth: 3).foregroundColor(.yellow)
drawCircles(geometry: geometry, points: points)
}
}
.background(Color.black)
.navigationBarTitle("Journey")
} else {
Text("Loading...")
}
}
(内容基本上就是1.根据屏幕大小计算点数,2.沿点绘制路径,3.在点上放置圆)
问题:
现在我明白GeometryReader 需要从父母那里得到它的高度,ScrollView 从它的孩子那里得到它的高度,这变成了一个“鸡鸡蛋问题”。所以我想手动计算GeometryReader的高度。
我的尝试
第一次尝试
最简单的方法是取一些固定值并计算出一个非常粗略的值。我只是将其添加到GeometryReader:
GeometryReader { geometry in
let points = calculatePoints(geometry: geometry)
drawPaths(geometry: geometry, points: points)
.stroke(lineWidth: 3).foregroundColor(.yellow)
drawCircles(geometry: geometry, points: points)
}
.frame(height: JourneyView.SPACING_Y_AXIS * CGFloat(1.2) * CGFloat(challengeViewModel.challengeAmount))
这行得通。但是现在我有一个很长的屏幕,底部有不必要的空间。根据手机型号,它可能看起来完全不同。
第二次尝试
一个潜在的解决方案似乎是在.body 中使用DispatchQueue.main.async,并将@State 变量设置为其中计算出的孩子的高度。如此处所述:https://stackoverflow.com/a/61315678/1972372
所以:
@State private var totalHeight: CGFloat = 100
var body: some View {
if (challengeViewModel.challengeAmount != 0) {
ScrollView {
GeometryReader { geometry in
let points = calculatePoints(geometry: geometry)
drawPaths(geometry: geometry, points: points)
.stroke(lineWidth: 3).foregroundColor(.yellow)
drawCircles(geometry: geometry, points: points)
}
.background(GeometryReader { gp -> Color in
DispatchQueue.main.async {
// update on next cycle with calculated height of ZStack !!!
print("totalheight read from Geo = \(totalHeight)")
self.totalHeight = gp.size.height
}
return Color.clear
})
}
.background(Color.black)
.frame(height: $totalHeight.wrappedValue)
.navigationBarTitle("Journey")
} else {
Text("Loading...")
}
}
这根本不起作用。 ScrollView 出于某种原因接收固定值 100 或 10,并且永远不会更改。我尝试将 .background(...) 块放在任何孩子身上,以尝试将每个孩子的值相加,但这只会造成无限循环。但它似乎有可能以某种方式解决。
第三次尝试
使用PreferenceKeys 类似于此tutorial 或此答案:https://stackoverflow.com/a/64293734/1972372。
这看起来确实应该有效。我在GeometryReader 中放置了一个ZStack,以便从中获取height,并添加了一个类似的.background(...):
.background(GeometryReader { geo in
Color.clear
.preference(key: ViewHeightKey.self,
value: geo.size.height
})
连同.onPreferenceChange(...) 一起发给它。
但不幸的是,这在所有尝试中效果最少,因为它根本没有改变,并且只在开始时调用一次,基值为 10。我尝试将它放在所有子视图上,但结果很奇怪,所以我已经删除了代码。
需要帮助
我现在觉得只是采用愚蠢但有效的第一次尝试解决方案,只是为了让我免于其他两个的头痛。
有人能看出我在尝试 2 或 3 中做错了什么,以及为什么我没有得到想要的结果吗?因为看起来它们应该可以工作。
完整代码
根据要求,这是完整的课程(使用第一次尝试技术):
struct JourneyView: View {
// MARK: Variables
@ObservedObject var challengeViewModel: ChallengeViewModel
@State private var selectedChlgID: Int = 0
@State private var showChlgDetailVIew = false
// Starting points
private static let START_COORD_RELATIVE_X_LEFT: CGFloat = 0.2
private static let START_COORD_RELATIVE_X_RIGHT: CGFloat = 0.8
private static let START_COORD_RELATIVE_Y: CGFloat = 0.05
// Y axis spacing of chlg views
private static let SPACING_Y_AXIS: CGFloat = 120
// MARK: UI
var body: some View {
if (challengeViewModel.challengeAmount != 0) {
ScrollView {
GeometryReader { geometry in
let points = calculatePoints(geometry: geometry)
drawPaths(geometry: geometry, points: points)
.stroke(lineWidth: 3).foregroundColor(.yellow)
drawCircles(geometry: geometry, points: points)
}
.frame(height: JourneyView.SPACING_Y_AXIS * CGFloat(1.2) * CGFloat(challengeViewModel.challengeAmount))
NavigationLink("", destination: ChallengeView(challengeID: selectedChlgID), isActive: $showChlgDetailVIew)
.opacity(0)
}
.background(Color.black)
.navigationBarTitle("Journey")
} else {
Text("Loading...")
}
}
// MARK: Functions
init() {
challengeViewModel = ChallengeViewModel()
}
func calculatePoints(geometry: GeometryProxy) -> [CGPoint] {
// Calculated constants
let normalizedXLeft = JourneyView.START_COORD_RELATIVE_X_LEFT * geometry.size.width
let normalizedXRight = JourneyView.START_COORD_RELATIVE_X_RIGHT * geometry.size.width
let normalizedY = JourneyView.START_COORD_RELATIVE_Y * geometry.size.height
let startPoint = CGPoint(x: geometry.size.width / 2, y: normalizedY)
var points = [CGPoint]()
points.append(startPoint)
(1...challengeViewModel.challengeAmount).forEach { i in
let isLeftAligned: Bool = i % 2 == 0 ? false : true
let nextPoint = CGPoint(x: isLeftAligned ? normalizedXRight : normalizedXLeft,
y: normalizedY + JourneyView.SPACING_Y_AXIS * CGFloat(i))
points.append(nextPoint)
}
return points
}
func drawPaths(geometry: GeometryProxy, points: [CGPoint]) -> some Shape {
// Connection paths
Path { path in
path.move(to: points[0])
points.forEach { point in
path.addLine(to: point)
}
}
}
func drawCircles(geometry: GeometryProxy, points: [CGPoint]) -> some View {
let circleRadius = geometry.size.width / 5
return ForEach(0...points.count - 1, id: \.self) { i in
JourneyChlgCircleView(chlgName: "Sleep" + String(i), color: Color(red: 120, green: 255, blue: 160))
.frame(width: circleRadius, height: circleRadius)
.position(x: points[i].x, y: points[i].y)
.onTapGesture {
selectedChlgID = i
showChlgDetailVIew = true
}
}
}
}
}
【问题讨论】:
-
我认为您在代码设计中遗漏了一些要点!如果你打算使用 GeometryReader 来阅读所有可用空间来绘制你想要的东西!一定是不用ScrollView来滚动的!因为我们只是使用了所有可用空间来绘制!为什么需要更多空间,然后为什么需要 ScrollView!?
-
@swiftPunk 因为在这种情况下,“所有可用空间”仅表示您在屏幕上看到的当前空间。但是我绘制的布局比适合屏幕的布局要长。现在呢?
-
啊哈!这是问题!您只想使用可用空间以防万一,而您不需要额外的空间!或者您不能说当前可用空间是否适合绘图,如果不适合则使用更多空间。哪一个?
-
@swiftPunk 是的,第二个。我必须绘制大约 15 个这样的圆圈,并且彼此之间有足够的边距,所以屏幕必须变得可滚动,否则其中一半会消失在下方。这也取决于手机型号。在小 iPhone 上你看到的比在大 iPhone 上要少。
-
我可以很容易地给出这个问题的答案,但如果我能运行你的代码!你给定的鳕鱼没有建立!您可以编辑您的代码以最小化重新创建问题,然后我可以处理它!我无法想象你的项目,然后开始回答想象中的问题。
标签: ios swiftui geometryreader swiftui-scrollview