SwiftUI - is it possible to get didSet to fire when changing a @Published struct?
The property observer observes the property. The trouble goes from new Swift syntax related to property wrappers. In your case you try to observe if value of Published (which is a struct defining the specialized property wrapper) did change, not the value of the wrapped property.
If you need to monitor left or right values in PaddingRect, simply observe this values directly.
import SwiftUI
struct PaddingRect {
var left: CGFloat = 20 {
didSet {
print("left padding change from:", oldValue, "to:", left)
}
}
var right: CGFloat = 20 {
didSet {
print("right padding change from:", oldValue, "to:", right)
}
}
}
final class SomeStore : ObservableObject {
@Published var someOtherValue: String = "Waiting for didSet"
@Published var paddingRect:PaddingRect = PaddingRect()
}
struct ContentView: View {
@ObservedObject var store = SomeStore()
var body: some View {
VStack {
Spacer()
Rectangle()
.fill(Color.yellow)
.padding(.leading, store.paddingRect.left)
.padding(.trailing, store.paddingRect.right)
.frame(height: 100)
Text(store.someOtherValue)
HStack {
Button(action: {
// This doesn't call didSet
self.store.paddingRect.left += 20
// This does call didSet, ie. setting the whole thing
self.store.paddingRect = PaddingRect(
left: self.store.paddingRect.left + 20,
right: self.store.paddingRect.right
)
}) {
Text("Padding left +20")
}
Button(action: {
self.store.paddingRect.right += 20
}) {
Text("Padding right +20")
}
}
Spacer()
}
}
}
struct ContentView_Preview: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Or take the advantage that Published projected value is Publisher and aply next modifier to any View
.onReceive(store.$paddingRect) { (p) in
print(p)
}
SwiftUI trigger function when model updates inside @Published property
Firstly - simplifying this code. Most of this code is unnecessary to reproduce the problem, and can't even be compiled. The code below is not the working code, but rather what we have to start and change later:
ContentView
struct ContentView: View {
var body: some View {
NavigationView {
PackagesView()
}
}
}
PackageService
class PackageService: ObservableObject {
let package: Package
init(package: Package) {
self.package = package
}
// Get placemarks from locations
func getPlacemarks() {
print("getPlacements called")
}
}
PackagesViewModel
class PackagesViewModel: ObservableObject {
@Published var results = [Package]()
}
PackagesView
struct PackagesView: View {
@StateObject var packagesViewModel = PackagesViewModel()
var body: some View {
VStack {
Button("Add new package") {
let number = packagesViewModel.results.count + 1
let new = Package(title: "title \(number)", description: "description \(number)")
packagesViewModel.results.append(new)
}
Button("Change random title") {
guard let randomIndex = packagesViewModel.results.indices.randomElement() else {
return
}
packagesViewModel.results[randomIndex].title = "new title (\(Int.random(in: 1 ... 100)))"
}
List(packagesViewModel.results, id: \.self) { package in
NavigationLink(destination: PackageView(packageService: PackageService(package: package))) {
Text(package.title)
}
}
}
}
}
PackageView
struct PackageView: View {
@ObservedObject var packageService: PackageService
var body: some View {
VStack {
Text("Title: \(packageService.package.title)")
Text("Description: \(packageService.package.description)")
}
}
}
Package
struct Package: Identifiable, Hashable {
let id = UUID()
var title: String
let description: String
}
Now, solving the problem. I fixed this issue by detecting the results
changing with onChange(of:perform:)
. However, from here there is no way to access the PackageService
s used in the view body.
To prevent this issue, the PackageService
s are actually stored in PackagesViewModel
, which logically makes more sense for the data flow. Now with PackageService
also being a struct
so the @Published
works on the array for results
, this now works.
See the code below:
PackageService (updated)
struct PackageService: Hashable {
var package: Package
init(package: Package) {
self.package = package
}
// Get placemarks from locations
mutating func getPlacemarks() {
print("getPlacements called")
// This function is mutating, feel free to set any properties in here
}
}
PackagesViewModel (updated)
class PackagesViewModel: ObservableObject {
@Published var results = [PackageService]()
}
PackagesView (updated)
struct PackagesView: View {
@StateObject var packagesViewModel = PackagesViewModel()
var body: some View {
VStack {
Button("Add new package") {
let number = packagesViewModel.results.count + 1
let new = Package(title: "title \(number)", description: "description \(number)")
packagesViewModel.results.append(PackageService(package: new))
}
Button("Change random title") {
guard let randomIndex = packagesViewModel.results.indices.randomElement() else {
return
}
let newTitle = "new title (\(Int.random(in: 1 ... 100)))"
packagesViewModel.results[randomIndex].package.title = newTitle
}
List($packagesViewModel.results, id: \.self) { $packageService in
NavigationLink(destination: PackageView(packageService: $packageService)) {
Text(packageService.package.title)
}
}
}
.onChange(of: packagesViewModel.results) { _ in
for packageService in $packagesViewModel.results {
packageService.wrappedValue.getPlacemarks()
}
}
}
}
PackageView (updated)
struct PackageView: View {
@Binding var packageService: PackageService
var body: some View {
VStack {
Text("Title: \(packageService.package.title)")
Text("Description: \(packageService.package.description)")
}
}
}
SwiftUI View Property willSet & didSet property observers not working
EDIT:
On iOS 14 property observers work the same as they did in iOS 13. But, we now have the .onChange(of:perform:)
as a replacement. Docs
Text(self.myString).onChange(of: self.myString) { newValue in print("myString changed to: \(newValue)") }
Property observers on basic var
s technically work in SwiftUI. If you do something like var view = MyTestView(...)
and then view.FSAC = "updated"
the the observers will fire (I've verified this).
However, typically with SwiftUI you construct the View
(which is a struct not a class) within body
during each layout pass. In your case var body: some View { MyTestView(FSAC: "FSAC Value") }
.
Property observers do not fire during init, and therefore they aren't usually useful in SwiftUI View
s.
If you would like to update some sort of State during init, take a look at this answer.
Is there an alternative to Combine's @Published that signals a value change after it has taken place instead of before?
You can write your own custom property wrapper:
import Combine
@propertyWrapper
class DidSet<Value> {
private var val: Value
private let subject: CurrentValueSubject<Value, Never>
init(wrappedValue value: Value) {
val = value
subject = CurrentValueSubject(value)
wrappedValue = value
}
var wrappedValue: Value {
set {
val = newValue
subject.send(val)
}
get { val }
}
public var projectedValue: CurrentValueSubject<Value, Never> {
get { subject }
}
}
Notifications not sent when changing an ObservedObject
As already suggested in the comments, you need to convert your class to a struct if you want to make it work: struct Stuff {
. A struct is a value type, which makes it work well with ObservableObject
, while a class is a reference type, which, well, doesn't work that well.
@Published
properties need to be "pure" value types, if you want to be notified when something in their contents change. By "pure" I mean value types that are made of only of other value types, which are also "pure".
Why this is needed? It's because the @Published
mechanism relies on the willSet
/didSet
property observers, and the property observers are executed only when the value they hold change.
With value types, any change in their structure is propagated upstream, resulting in the whole property being updated. This happens due to the semantics of the value types.
See this code snippet:
struct Question {
var title: String = ""
}
class Page {
var question = Question() {
didSet {
print("Question changed: \(question)")
}
}
}
let page = Page()
page.question.title = "@Published doesn't work"
Any change to the question
members results in the whole property being replaces, this triggering the willSet
/didSet
property observers, so you can nicely monitor any changes in the data structure of question
.
The above is, however, not true for classes. Classes have identity, they have a fixed memory location, unlike value types, which get copied all over the place when used. This means that changes in their internal structure are not reflected upstream, as the class instance storage doesn't change.
The only time the @Published
observer is triggered for classes, is when you replace the object itself. Try this, and you'll see the notifications are fired:
doSomething.observedObject.stuff = Stuff() // this will print `Object has changed`
In SwiftUI, how do I know when a Picker selection was changed? Why doesn't didSet work?
I started typing this out earlier, and returned to find LuLuGaGa had beaten me to the punch. :D But since I have this anyway...
Main question: From the Swift Language Guide:
"When you assign a default value to a stored property, or set its
initial value within an initializer, the value of that property is set
directly, without calling any property observers."
So the property observer will not fire when the view is constructed. But when a @State
variable changes, a new instance of the view is constructed (remember, views are structs, or value types). Thus, the didSet
property observer is, practically speaking, not useful on @State
properties.
What you want to do is create a class that conforms to ObservableObject
, and reference it from your view with the @ObservedObject
property wrapper. Because the class exists outside the struct, you can set property observers on its properties, and they will fire like you're expecting.
Question A: You can use just the enum if you make it conform to CaseIterable
(see example below)
Question B: This appears to be a SwiftUI bug, as it happens with any Picker
inside of a NavigationView
/Form
combo, as far as I can tell. I'd recommend reporting it to Apple.
Here is how I would remove the redundancy of the enum and array, and save the selection in UserDefaults:
extension ContentView {
// CaseIterable lets us use .allCases property in ForEach
enum TransmissionType: String, CaseIterable, Identifiable, CustomStringConvertible {
case automatic
case manual
// This lets us omit 'id' parameter in ForEach
var id: TransmissionType {
self
}
// This just capitalizes the first letter for prettier printing
var description: String {
rawValue.prefix(1).uppercased() + rawValue.dropFirst()
}
}
class SelectionModel: ObservableObject {
// Save selected type to UserDefaults on change
@Published var selectedTransmissionType: TransmissionType {
didSet {
UserDefaults.standard.set(selectedTransmissionType.rawValue, forKey: "TransmissionType")
}
}
// Load selected type from UserDefaults on initialization
init() {
if let rawValue = UserDefaults.standard.string(forKey: "TransmissionType") {
if let transmissionType = TransmissionType(rawValue: rawValue) {
self.selectedTransmissionType = transmissionType
return
}
}
// Couldn't load from UserDefaults
self.selectedTransmissionType = .automatic
}
}
}
Then your view just looks like
struct ContentView: View {
@ObservedObject var model = SelectionModel()
var body: some View {
NavigationView {
Form {
Section {
Picker(selection: $model.selectedTransmissionType, label: Text("Transmission Type")) {
ForEach(TransmissionType.allCases) { type in
Text(type.description)
}
}
}
}
}
}
}
Related Topics
To Change the Color of Unselected Uitabbar Icon in iOS 7
Communicating With/Opening Containing App from Share Extension
Customizing the Mkannotation Callout Bubble
External Framework File/File.H (Parse/Parse.H) File Not Found
How to Test for the Class of a Variable in Swift
How to Increase Width of Textfield According to Typed Text
How to Draw a Non-Rectangle Uitextview
Property Cannot Be Found in Forward Class Object
How to Represent Core Data Optional Scalars (Bool/Int/Double/Float) in Swift
Override a Method in Objective C via Category
"Can't Find Model for Source Store" Occurring During iPhone "Automatic Lightweight Migration"
How to Create a Bold Uifont from a Regular Uifont
How to Have a Fixed Uitableview Header While Using Sections
Uitableviewcell Selected Background Color on Multiple Selection
How to Pop View Controller to One of the Previous View Controllers in Swift