The ObservableObject
conformance allows instances of a class to be used inside views, so that when important changes happen the View will reload.
The @Published
property wrapper tells SwiftUI that changes to a property should trigger View reloads.
class Contact: ObservableObject {
@Published var name: String
@Published var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
func haveBirthday() -> Int {
age += 1
return age
}
}
let john = Contact(name: "John Appleseed", age: 24)
cancellable = john.objectWillChange
.sink { _ in
print("\(john.age) will change")
}
print(john.haveBirthday())
// Prints "24 will change"
// Prints "25"
Note: You should only use
@ObservedObject
with views that were passed in from elsewhere. You should not use this property wrapper to create the initial instance of an observable object – that’s what@StateObject
is for.
By default an ObservableObject synthesizes an objectWillChange publisher that emits the changed value before any of its @Published
properties changes. To send a change event manually, you call objectWillChange.send()
.
class UserAuthentication: ObservableObject {
var username = "Taylor" {
willSet {
objectWillChange.send()
}
}
}
https://www.hackingwithswift.com/quick-start/swiftui/how-to-use-observedobject-to-manage-state-from-external-objects https://www.hackingwithswift.com/quick-start/swiftui/how-to-send-state-updates-manually-using-objectwillchange https://developer.apple.com/documentation/combine/observableobject
Use when a subview needs to mutate the value being passed in. Otherwise, property should always be a let
Use when a view needs to retain information between redraws
Environment is a way to pass arbitrary things to your subviews at arbitrary depth. Modifiers like .font()
and .buttonStyle()
set a value in the environment that gets read by those views.
Example:
VStack {
Text("")
HStack {
Text("")
Button(...) {}
}
}
.font(Font.system(size: 16))
.buttonStyle(FancyButtonStyle())
An Environment is similar to @ObservedObject
in that they both allow a parent View to communicate state downward to its children. The difference is that Environment is more decoupled from the View because when you define a new Environment type you need to supply a default value. That way, Views can depend on it regardless if one of its parents defines a value - which also enables arbitrary depth. With an @ObservableObject
you would need to pass the binding to each View in the hierarchy that might use it.
Example:
// ===========================================================
// Setup
// ===========================================================
public struct FormValidationErrorsKey: EnvironmentKey {
public static let defaultValue: [String: String] = [:]
}
extension EnvironmentValues {
public var formValidationErrors: FormValidationErrorsKey.Value {
get { self[FormValidationErrorsKey.self] }
set { self[FormValidationErrorsKey.self] = newValue }
}
}
// Get access to environment as a struct/class property
@Environment(\.formValidationErrors) var errors: [String: String]
// Set environment at parent level
SomeView
.environment(\.formValidationErrors, viewModel.validationErrors)
Preferences are how views can communicate things back up to their parent.
Preferences are similar to @Binding
in that they both enable a subview to communicate upward to its parent. Preference also needs a default value defined so you can rely on its presence.
Example:
public struct SizeKey: PreferenceKey {
public static let defaultValue: CGSize = .zero
public static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
value = nextValue()
}
}
public extension View {
func measureSize(_ f: @escaping (CGSize) -> ()) -> some View {
overlay(GeometryReader { g in
Color.clear.preference(key: SizeKey.self, value: g.size)
}
.onPreferenceChange(SizeKey.self, perform: f))
}
}
Read here for more information on preferences: https://swiftwithmajid.com/2020/01/15/the-magic-of-view-preferences-in-swiftui/