I had the same issue and I don't know if this is a "limitation" or actually SwiftUI "telling" us to use view composition.
Personally, to test the design and layout of views that use @AppStorage
variables, I just extract child views that take the original variable(s) marked with @AppStorage
as plain, stateless variable(s) and generate as many previews as needed for those children. That's usually enough for me. However, if you want to easily preview the "assembled" layout, you could, for example, extract a new parent view that is identical to the one you already had with @AppStorage
but using @Binding
instead. To add persistence, just wrap this new parent in a "container" view that adds persistence with @AppStorage
:
enum AnimalSize: Int {
case small, large
func toggle() -> AnimalSize {
self == .small ? .large : .small
}
}
// A child that can be easily previewed
struct CatView: View {
var size: AnimalSize
var body: some View {
Text("🐈")
.font(.system(size: size == .small ? 25 : 75))
}
}
// Another child
struct DogView: View {
var size: AnimalSize
var body: some View {
Text("🐕")
.font(.system(size: size == .small ? 25 : 75))
}
}
// The new parent view we can preview for all state combinations
struct AnimalView: View {
@Binding var catSize: AnimalSize
@Binding var dogSize: AnimalSize
var body: some View {
VStack(spacing: 25) {
CatView(size: catSize)
.onTapGesture { catSize = catSize.toggle() }
DogView(size: dogSize)
.onTapGesture { dogSize = dogSize.toggle() }
}
}
}
// The wrapper that adds persistence and you don't really need to preview
struct AnimalViewPersistent: View {
@AppStorage("cat.size") private var catSize: AnimalSize = .small
@AppStorage("dog.size") private var dogSize: AnimalSize = .small
var body: some View {
AnimalView(catSize: $catSize, dogSize: $dogSize)
}
}
// All parameter combinations we might want to preview
struct AnimalView_Previews: PreviewProvider {
static var previews: some View {
Group {
AnimalView(catSize: .constant(.small), dogSize: .constant(.small))
AnimalView(catSize: .constant(.large), dogSize: .constant(.small))
AnimalView(catSize: .constant(.small), dogSize: .constant(.large))
AnimalView(catSize: .constant(.large), dogSize: .constant(.large))
}
.previewLayout(.sizeThatFits)
}
}
As a nice bonus your views now become smaller, more testable and reusable.
P.S. N1: Alternatively, I guess you could pass the Binding
down all the way to the children and place the logic that changes state from the parent right into the applicable child. As a matter of personal preference, I like to have the children as dumb as possible and keep the state changes identical to what I had in the original parent view using @AppStorage
.
P.S. N2: You might still want to live preview the container view that adds persistence to check that the state changing logic is wroking as expected (e.g. animations). However, you already get all possible state combinations in the static previews that use constant bindings without having to manually change state in a live preview because @AppStorage
just persisted the state from the last live preview. This way, if you change one child, you can immediately see those changes reflected on the final layout of the parent that uses @Binding
.