I have some data that will be loaded from a server and in the mean time I want to show a shimmer placeholder view.
To achieve the shimmer effect, I've created the following:
struct ShimmerEffectBox: View {
@State private var startPoint: UnitPoint = .init(x: -1.8, y: -1.2)
@State private var endPoint: UnitPoint = .init(x: 0, y: -0.2)
private var gradientColors = [
Color(uiColor: .systemGray5),
Color(uiColor: .systemGray6),
Color(uiColor: .systemGray5)
]
var body: some View {
LinearGradient(colors: gradientColors,
startPoint: startPoint,
endPoint: endPoint)
.onAppear {
withAnimation(.easeInOut(duration: 2)
.repeatForever(autoreverses: false)) {
startPoint = .init(x: 1, y: 1)
endPoint = .init(x: 2.2, y: 2.2)
}
}
}
}
The shimmer works fine and nothing needs to be looked at here.
I then create a ScrollView
with some data and my placeholder views, however, for some reason the views are overlapping, this is my code:
struct ContentView: View {
var body: some View {
ScrollView {
Text("Title")
.font(.system(size: 20))
.bold()
ForEach(1...5, id: \.self) { _ in
placeholderView
}
}
}
private var placeholderView: some View {
GeometryReader { proxy in
VStack(alignment: .leading, spacing: 4) {
ShimmerEffectBox()
.frame(width: proxy.size.width / 2, height: 18)
.cornerRadius(4)
ShimmerEffectBox()
.frame(height: 16)
.cornerRadius(4)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.bottom, 24)
}
}
}
This gives me the following result:
I'm sure that I'm doing something wrong with the geometry reader, so I added the padding to the geometry reader
private var placeholderView: some View {
GeometryReader { proxy in
VStack(alignment: .leading, spacing: 4) {
ShimmerEffectBox()
.frame(width: proxy.size.width / 2, height: 18)
.cornerRadius(4)
ShimmerEffectBox()
.frame(height: 16)
.cornerRadius(4)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
.padding(.bottom, 24)
}
This is better, however, the padding is definitely not 24.
My goal is:
Not sure if I'm using geometry reader wrong or specifying the padding / spacing at the wrong place.
This problem is happening because the GeometryReader
is inside a ScrollView
, which stops it from being as greedy as usual. Consequently, the height of the GeometryReader
is very small.
Since you know the height of the ShimmerEffectBox
(because you are setting it using .frame
), you could fix the problem by setting a fixed height on the GeometryReader
too:
GeometryReader { proxy in
// content as before
}
.frame(height: 38)
However, it looks like you only need the GeometryReader
to find the screen width, which you are dividing by two. The same result can be achieved by using an HStack
that contains two views with maxWidth: .infinity
. So you could also fix like this:
private var placeholderView: some View {
VStack(alignment: .leading, spacing: 4) {
HStack(spacing: 0) {
ShimmerEffectBox()
.frame(height: 18)
.cornerRadius(4)
.frame(maxWidth: .infinity)
Color.clear
.frame(maxWidth: .infinity)
}
ShimmerEffectBox()
.frame(height: 16)
.cornerRadius(4)
.frame(maxWidth: .infinity)
}
.padding(.bottom, 24)
}
If you want to fine-tune the spacing between the content of the ScrollView
then you might want to use a VStack
as the top-level container inside the ScrollView
. Then you can control the spacing of the VStack
:
ScrollView {
VStack(spacing: 0) { // or spacing: 24
// content as before
}
}
At the moment, the ScrollView
is adding a bit of vertical space between each of the subviews it contains, which you might not be expecting.