I was playing with a SwiftUI animation that required the view size. In my case, it is some list implemented using HStack. Since it consists of many items, I used the ForEach
struct.
It shocked me when I saw its size (0.0, 0.0). After digging, I found out that changing ForEach
to an explicit items declaration made the size correct.
And now, there's a question – why is that? How do you suggest overcoming this issue?
For me, it looks like a SwiftUI bug. Since ForEach
conforms to the View
protocol, it should behave like any other view IMO:D
Below you can find snippets of the code related to the issue. Both ForEach
implementations and size modifier-related.
struct ContentView: View {
@State private var contentSize: CGSize = .zero
var body: some View {
HStack {
HStack {
// Version 1: - With ForEach inside HStack's size is (0.0, 0.0)
ForEach([0, 1], id: \.self) { item in
Text("Item \(item)")
}
// Version 2: - Without ForEach inside HStack's size is (101.66666666666666, 20.333333333333332)
Text("Item 0")
Text("Item 1")
}
.fixedSize()
.size($contentSize)
}
}
}
extension View {
public func size(_ size: Binding<CGSize>) -> some View {
modifier(SizeBindingViewModifier(size: size))
}
}
struct SizePreferenceKey: PreferenceKey {
typealias Value = CGSize
static var defaultValue: Value = .zero
static func reduce(value: inout Value, nextValue: () -> Value) {
value = nextValue()
}
}
struct SizeBindingViewModifier: ViewModifier {
@Binding private var size: CGSize
init(size: Binding<CGSize>) {
_size = size
}
func body(content: Content) -> some View {
content
.background(
GeometryReader { proxy in
Color
.clear
.preference(
key: SizePreferenceKey.self,
value: proxy.size
)
}
)
.onPreferenceChange(SizePreferenceKey.self) { size in
print("size: \(size)")
self.size = size
}
}
}
Interestingly, if you check value
and nextValue()
in reduce
, value
is initially set correctly.
In fact, in the example above, reduce
is only called (with nextValue() =.zero
) when the ForEach
is used. Presumably this is due size not being calculable initially due to the "dynamic" nature of the ForEach
.
Assuming your views will never have zero size, a simple workaround could be:
static func reduce(value: inout Value, nextValue: () -> Value) {
guard nextValue() != .zero else { return }
value = nextValue()
}
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.