In this example, the blue rectangle should be initially visible on devices with .regular
size class and hidden on devices with .compact
size class.
I'm using an ObservableObject
called Settings
and the @Published
variable isVisible
to manage visibilty of the rectangle. My problem is that I don't know how I can init Settings
with the correct horizontalSizeClass
from my ContentView
. Right now I am using .onAppear
to change the value of isVisible
but this triggers .onReceive
. On compact devices this causes the rectangle to be visible and fading out when the view is presented instead of being invisible right away.
How can I init Settings
based on Environment values like horizontalSizeClass
so that isVisible
is correct from the start?
struct ContentView: View {
@Environment(\.horizontalSizeClass) var horizontalSizeClass
@StateObject var settings = Settings()
@State var opacity: CGFloat = 1
var body: some View {
VStack {
Button("Toggle Visibility") {
settings.isVisible.toggle()
}
.onReceive(settings.$isVisible) { _ in
withAnimation(.linear(duration: 2.0)) {
opacity = settings.isVisible ? 1 : 0
}
}
Rectangle()
.frame(width: 100, height: 100)
.foregroundColor(.blue)
.opacity(opacity)
}
.onAppear {
settings.isVisible = horizontalSizeClass == .regular // too late
}
}
}
class Settings: ObservableObject {
@Published var isVisible: Bool = true // can't get size class here
}
The rectangle should not be visible on start:
CodePudding user response:
We need just perform dependency injection (environment is known is parent so easily can be injected into child), and it is simple to do with internal view, like
struct ContentView: View {
@Environment(\.horizontalSizeClass) var horizontalSizeClass
struct MainView: View {
// later knonw injection
@EnvironmentObject var settings: Settings
var body: some View {
VStack {
Button("Toggle Visibility") {
settings.isVisible.toggle()
}
Rectangle()
.frame(width: 100, height: 100)
.foregroundColor(.blue)
.opacity(settings.isVisible ? 1 : 0) // << direct dependency !!
}
.animation(.linear(duration: 2.0), value: settings.isVisible) // << explicit animation
}
}
var body: some View {
MainView() // << internal view
.environmentObject(
Settings(isVisible: horizontalSizeClass == .regular) // << initial injecttion !!
)
}
}
Tested with Xcode 13.4 / iOS 15.5
CodePudding user response:
withAnimation
should be done on the Button action changing the state, e.g.
import SwiftUI
struct RectangleTestView: View {
@Environment(\.horizontalSizeClass) var horizontalSizeClass
@State var settings = Settings()
var body: some View {
VStack {
Button("Toggle Visibility") {
withAnimation(.linear(duration: 2.0)) {
settings.isVisible.toggle()
}
}
Rectangle()
.frame(width: 100, height: 100)
.foregroundColor(.blue)
.opacity(settings.opacity)
}
.onAppear {
settings.isVisible = horizontalSizeClass == .regular
}
}
}
struct Settings {
var isVisible: Bool = true
var opacity: CGFloat {
isVisible ? 1 : 0
}
}
FYI we don't really use onReceive
anymore since they added onChange
. Also its best to keep view data in structs not move it to expensive objects. "Views are very cheap, we encourage you to make them your primary encapsulation mechanism" Data Essentials in SwiftUI WWDC 2020 at 20:50.