I'm trying to reuse an older piece of Swift code, but getting an error 'Cannot use mutating getter on immutable value: 'self' is immutable error'. Xcode wanted to add 'mutating' before the func, and offered to do so through a 'fix'. So the error is gone there but still remains at the 'Text' statements.
import SwiftUI
struct ContentView: View {
typealias PointTuple = (day: Double, mW: Double)
let points: [PointTuple] = [(0.0, 31.98), (1.0, 31.89), (2.0, 31.77), (4.0, 31.58), (6.0, 31.46)]
lazy var meanDays = points.reduce(0) { $0 + $1.0 } / Double(points.count)
lazy var meanMW = points.reduce(0) { $0 + $1.1 } / Double(points.count)
lazy var a = points.reduce(0) { $0 + ($1.day - meanDays) * ($1.mW - meanMW) }
lazy var b = points.reduce(0) { $0 + pow($1.day - meanDays, 2) }
lazy var m = a / b
lazy var c = meanMW - m * meanDays
lazy var x : Double = bG(day: 3.0)
lazy var y : Double = bG(day: 5.0)
lazy var z : Double = bG(day: 7.0)
mutating func bG(day: Double) -> Double {
return m * day + c
}
var body: some View {
VStack {
Text("\(x)")
Text("\(y)")
Text("\(z)")
}
}
}
#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
#endif
Because when you call x inside the struct, it's not clear whether the contentView itself is mutable or not. A value type gets mutable only when it is defined as a var.
So it would help if you wrapped it with an immutable value before using it inside a builder function inside the struct.
like this:
func xValue() -> Double {
var mutatableSelf = self
return mutatableSelf.x
}
var body: some View {
VStack {
Text("\(xValue())")
}
}
š”Note: Lazy property's value will be associated on the first call and this mutates the object. So they are considered as mutating
A getter cannot mutate
This is mainly a design Swift is enforcing with its getters. The principle is:
The getters should not mutate the object. Because developers may not be expecting that. They should only expect a change when you're using the setter or calling a mutating function. A getter is neither of them.
The following example works as expected:
struct Device {
var isOn = true
}
let a = Device()
let b = a
print(a.isOn)
When we print, we get the value of a.isOn. Both a,b are identical.
Yet in the following example, the getter will have a side-effect. It won't even compile. But let's just assume that it did and see what happens.
struct Device2 {
var x = 3
var isOn: Bool {
x = 5
return true
}
}
let a = Device2()
let b = a
print(a.isOn)
When we print, we get the value of a.isOn. However this time the a,b are no longer identical. a.x will now be 5, because a copy-on-write would have happened, while b.x would still be 3.
Swift has an architecture that doesn't allow "getters to mutate an object".
Exceptions
The Swift architecture has two exceptions to this rule:
use lazy
use #State or some other property wrapper
Lazy is mutating:
struct Device2 {
lazy var x : Double = 3.0
func log() {
print(x)
}
}
Even thought print(x) will mutate x upon getting the value of x, it's fine because x a lazy property.
You might be wondering what's different between lazy var x = 5 and var x = 5 and the answer is that the latter has the value of x set upon initialization of the struct...
SwiftUI - special case
Because the body variable is a computed property, you can't mutate/set variables. There's a way around that though.
Mark the variable with a #State property wrapper.
Example. The following code won't compile:
struct ContentView: View {
var currentDate = Date()
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
Text("\(currentDate)")
.onReceive(timer) { input in
currentDate = input // ERROR: Cannot assign to property: 'self' is immutable
}
}
}
Yet the following will, just because it has #State
struct ContentView: View {
#State var currentDate = Date()
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
Text("\(currentDate)")
.onReceive(timer) { input in
currentDate = input
}
}
}
For more on that see here
Related
I'm currently trying to modify an upcoming value from a textField which is using a Binding<Double>, but haven't found any working solution yet. It's only been infinite loops (Like the example below) and other solutions which didn't work in the end anyway. So, for example, if an user inputs an amount which is too low, I would want to change the upcoming value to the minimum and vice verse if the value is higher than the maximum value.
I also want to present the modified value (if needed) for the user, so I can't just store it in another variable.
Any ideas on how to solve this?
Example
class ViewModel: ObservableObject {
#Published var amount: Double
private var subscriptions: Set<AnyCancellable> = []
private let minimum: Double = 10_000
private let maximum: Double = 100_000
init() {
$amount
.sink {
if $0 < self.minimum {
// Set minimum value
self.amount = self.minimum
} else if $0 > self.maximum {
// Set maximum value
self.amount = self.maximum
}
// If `Else` is implemented it will just be an infinite loop...
else {
self.amount = $0
}
}
.store(in: &subscriptions)
}
func prepareStuff() {
// Start preparing
let chosenAmount = amount
}
}
One way is to use a property wrapper to clamp the values.
Here is a very basic example of the issue, where we have an amount, that we can change to any value. The Stepper just makes it easy for input/testing:
struct ContentView: View {
#State private var amount = 0
var body: some View {
Form {
Stepper("Amount", value: $amount)
Text(String(amount))
}
}
}
The problem with this example is that amount isn't limited to a range. To fix this, create a Clamping property wrapper (partially from here):
#propertyWrapper
struct Clamping<Value: Comparable> {
private var value: Value
let range: ClosedRange<Value>
var wrappedValue: Value {
get { value }
set { value = min(max(range.lowerBound, newValue), range.upperBound) }
}
var clampedValue: Value {
get { wrappedValue }
set { wrappedValue = newValue }
}
init(wrappedValue value: Value, _ range: ClosedRange<Value>) {
precondition(range.contains(value))
self.value = value
self.range = range
}
}
And then we can chain property wrappers, and get a working example where amount is limited:
struct ContentView: View {
#State #Clamping(-5 ... 5) private var amount = 0
var body: some View {
Form {
Stepper("Amount", value: $amount.clampedValue)
Text(String(amount))
}
}
}
I know, this isn't the proper way to limit a Stepper's range. Instead you should use Stepper(_:value:in:). However, this is to instead demonstrate clamping a value - not how to clamp a Stepper.
What does this mean you need to do?
Well, first off change your #Published property to this:
#Published #Clamping(10_000 ... 100_000) var amount: Double
And now you can just access amount like normal to get the clamped value. Use $amount.clampedValue like I did in my solution to get your Binding<Double> binding.
If having troubles sometimes with compiling chained property wrappers (probably a bug), here is my example recreated using a Model object and #Published:
struct ContentView: View {
#StateObject private var model = Model(amount: 0)
var body: some View {
Form {
Stepper("Amount", value: $model.amount.clampedValue)
Text(String(model.amount.clampedValue))
}
}
}
class Model: ObservableObject {
#Published var amount: Clamping<Int>
init(amount: Int) {
_amount = Published(wrappedValue: Clamping(wrappedValue: amount, -5 ... 5))
}
}
I'm developing a simple SwiftUI app in Xcode 11. I want to have a form that loops through multiple user input strings and displays a form with a button. When the user presses the button it modifies the input value - specifically increment or decrement it.
However when passing an array of references like UserInput().foo where UserInput is a published observable object I cannot modify the value inside a ForEach because the ForEach is passed a copy as oppose to the original reference (at least that's my basic understanding). How do I then try to achieve it? I read about inout and everybody says to avoid it but surely this must be a relatively common issue.
I've made an simple example of what I'm trying to do but I can't quite work it out:
import SwiftUI
class UserInput: ObservableObject {
#Published var foo: String = ""
#Published var bar: String = ""
}
struct ContentView: View {
#ObservedObject var input = UserInput()
var body: some View {
LoopInputs()
}
func LoopInputs() -> AnyView?{
var userinputs = [
[UserInput().foo, "Foo"],
[UserInput().bar, "Bar"]
]
var inputs: some View{
VStack(){
ForEach(userinputs, id: \.self){userinput in
Text("\(userinput[1]): \(String(userinput[0]))")
Button(action: {
increment(input: String(userinput[0]))
}){
Text("Increase")
}
}
}
}
return AnyView(inputs)
}
func increment(input: String){
var lead = Int(input) ?? 0
lead += 1
// input = String(lead)
}
}
As I understood, when adding a value to userinputs, the ForEach values doesn't change.
Well, if that's the case, first of all, you could try creating a struct and in it, you declare foo and bar, then just declare a variable of type the struct. It'll look like this:
struct Input: Identifiable {
var id = UUID()
var foo: String
var bar: String
}
class UserInput: ObservableObject {
#Published var inputs: [Input] = [Input]()
}
//ContentView
struct ContentView: View {
#ObservedObject var input = UserInput()
var body: some View {
LoopInputs()
}
func LoopInputs() -> AnyView? {
var inputs: some View {
VStack {
ForEach(input.inputs) { userinput in
Text("\(userinput.bar): \(String(userinput.foo))")
Button(action: {
increment(input: String(userinput.foo))
}) {
Text("Increase")
}
}
}
}
return AnyView(inputs)
}
func increment(input: String) {
var lead = Int(input) ?? 0
lead += 1
// input = String(lead)
}
}
Wouldn't this be easier and more elegant?
Suppose I have a data model in my SwiftUI app that looks like the following:
class Tallies: Identifiable, ObservableObject {
let id = UUID()
#Published var count = 0
}
class GroupOfTallies: Identifiable, ObservableObject {
let id = UUID()
#Published var elements: [Tallies] = []
}
I want to add a computed property to GroupOfTallies that resembles the following:
// Returns the sum of counts of all elements in the group
var cumulativeCount: Int {
return elements.reduce(0) { $0 + $1.count }
}
However, I want SwiftUI to update views when the cumulativeCount changes. This would occur either when elements changes (the array gains or loses elements) or when the count field of any contained Tallies object changes.
I have looked into representing this as an AnyPublisher, but I don't think I have a good enough grasp on Combine to make it work properly. This was mentioned in this answer, but the AnyPublisher created from it is based on a published Double rather than a published Array. If I try to use the same approach without modification, cumulativeCount only updates when the elements array changes, but not when the count property of one of the elements changes.
There are multiple issues here to address.
First, it's important to understand that SwiftUI updates the view's body when it detects a change, either in a #State property, or from an ObservableObject (via #ObservedObject and #EnvironmentObject property wrappers).
In the latter case, this is done either via a #Published property, or manually with objectWillChange.send(). objectWillChange is an ObservableObjectPublisher publisher available on any ObservableObject.
This is a long way of saying that IF the change in a computed property is caused together with a change of any #Published property - for example, when another element is added from somewhere:
elements.append(Talies())
then there's no need to do anything else - SwiftUI will recompute the view that observes it, and will read the new value of the computed property cumulativeCount.
Of course, if the .count property of one of the Tallies objects changes, this would NOT cause a change in elements, because Tallies is a reference-type.
The best approach given your simplified example is actually to make it a value-type - a struct:
struct Tallies: Identifiable {
let id = UUID()
var count = 0
}
Now, a change in any of the Tallies objects would cause a change in elements, which will cause the view that "observes" it to get the now-new value of the computed property. Again, no extra work needed.
If you insist, however, that Tallies cannot be a value-type for whatever reason, then you'd need to listen to any changes in Tallies by subscribing to their .objectWillChange publishers:
class GroupOfTallies: Identifiable, ObservableObject {
let id = UUID()
#Published var elements: [Tallies] = [] {
didSet {
cancellables = [] // cancel the previous subscription
elements.publisher
.flatMap { $0.objectWillChange }
.sink(receiveValue: self.objectWillChange.send)
.store(in: &cancellables)
}
}
private var cancellables = Set<AnyCancellable>
var cumulativeCount: Int {
return elements.reduce(0) { $0 + $1.count } // no changes here
}
}
The above will subscribe a change in the elements array (to account for additions and removals) by:
converting the array into a Sequence publisher of each array element
then flatMap again each array element, which is a Tallies object, into its objectWillChange publisher
then for any output, call objectWillChange.send(), to notify of the view that observes it of its own changes.
This is similar to the last option of #New Devs answer, but a little shorter, essentially just passing the objectWillChange notification to the parent object:
import Combine
class Tallies: Identifiable, ObservableObject {
let id = UUID()
#Published var count = 0
func increase() {
count += 1
}
}
class GroupOfTallies: Identifiable, ObservableObject {
let id = UUID()
var sinks: [AnyCancellable] = []
#Published var elements: [Tallies] = [] {
didSet {
sinks = elements.map {
$0.objectWillChange.sink( receiveValue: objectWillChange.send)
}
}
}
var cumulativeCount: Int {
return elements.reduce(0) { $0 + $1.count }
}
}
SwiftUI Demo:
struct ContentView: View {
#ObservedObject
var group: GroupOfTallies
init() {
let group = GroupOfTallies()
group.elements.append(contentsOf: [Tallies(), Tallies()])
self.group = group
}
var body: some View {
VStack(spacing: 50) {
Text( "\(group.cumulativeCount)")
Button( action: group.elements.first!.increase) {
Text( "Increase first")
}
Button( action: group.elements.last!.increase) {
Text( "Increase last")
}
}
}
}
The simplest & fastest is to use value-type model.
Here is a simple demo. Tested & worked with Xcode 12 / iOS 14
struct TestTallies: View {
#StateObject private var group = GroupOfTallies() // SwiftUI 2.0
// #ObservedObject private var group = GroupOfTallies() // SwiftUI 1.0
var body: some View {
VStack {
Text("Cumulative: \(group.cumulativeCount)")
Divider()
Button("Add") { group.elements.append(Tallies(count: 1)) }
Button("Update") { group.elements[0].count = 5 }
}
}
}
struct Tallies: Identifiable { // << make struct !!
let id = UUID()
var count = 0
}
class GroupOfTallies: Identifiable, ObservableObject {
let id = UUID()
#Published var elements: [Tallies] = []
var cumulativeCount: Int {
return elements.reduce(0) { $0 + $1.count }
}
}
I found a lot of SwiftUI-related topics about this which didn't help (eg Why an ObservedObject array is not updated in my SwiftUI application?)
This doesn't work with Combine in Swift (specifically not using SwiftUI):
class SomeTask {
#Published var progress = Progress(totalUnitCount: 5) // Progress is a Class
[...]
}
var task = SomeTask()
let cancellable = task.$progress.sink { print($0.fractionCompleted) }
task.progress.completedUnitCount = 2
This is not SwiftUI-related so no ObservableObject inheritance to get objectWillChange, but even if I try to use ObservableObject and task.objectWillChange.send() it doesn't do anything, also trying to add extension Progress: ObservableObject {} doesn't help.
Since the publisher emits values through the var's willSet and since Progress is itself class-type nothing happens.
Looks like there is no real decent way to manually trigger it?
Only solution I found is to just re-assign itself which is quite awkward:
let pr = progress
progress = pr
(writing progress = progress is a compile-time error).
Only other way which might be working is probably by using Key-value-observing/KVO and/or writing a new #PublishedClassType property wrapper?
I was able to implement this using KVO, wrapped by a #propertyWrapper, with a CurrentValueSubject as the publisher:
#propertyWrapper
class PublishedClass<T : NSObject> {
private let subject: CurrentValueSubject<T, Never>
private var observation: NSKeyValueObservation? = nil
init<U>(wrappedValue: T, keyPath: ReferenceWritableKeyPath<T, U>) {
self.wrappedValue = wrappedValue
subject = CurrentValueSubject(wrappedValue)
observation = wrappedValue.observe(keyPath, options: [.new]) { (wrapped, change) in
self.subject.send(wrapped)
}
}
var wrappedValue: T
var projectedValue: CurrentValueSubject<T, Never> {
subject
}
deinit {
observation.invalidate()
}
}
Usage:
class Bar : NSObject {
#objc dynamic var a: Int
init(a: Int) {
self.a = a
}
}
class Foo {
#PublishedClass(keyPath: \.a)
var bar = Bar(a: 0)
}
let f = Foo()
let c = f.$bar.sink(receiveValue: { x in print(x.a) })
f.bar.a = 2
f.bar.a = 3
f.bar.a = 4
Output:
0
2
3
4
The disadvantage of using KVO is, of course, that the key path you pass in must be #objc dynamic and the root of the keypath must be an NSObject subclass. :(
I haven't tried, but it should be possible to extend this to observe on multiple key paths if you want.
You can try using CurrentValueSubject<Progress, Never>:
class SomeTask: ObservableObject {
var progress = CurrentValueSubject<Progress, Never>(Progress(totalUnitCount: 5))
func setProgress(_ value: Int) {
progress.value.completedUnitCount = value
progress.send(progress.value)
}
}
var task = SomeTask()
let cancellable = task.progress.sink { print($0.fractionCompleted) }
task.setProgress(3)
task.setProgress(1)
This way your Progress can still be a class.
Based on the ideas I did implement a #PublishedKVO property wrapper and put it up on github as a small swift package, supporting multiple key paths.
https://github.com/matis-schotte/PublishedKVO
Usable as:
class Example {
#PublishedKVO(\.completedUnitCount)
var progress = Progress(totalUnitCount: 2)
#Published
var textualRepresentation = "text"
}
let ex = Example()
// Set up the publishers
let c1 = ex.$progress.sink { print("\($0.fractionCompleted) completed") }
let c1 = ex.$textualRepresentation.sink { print("\($0)") }
// Interact with the class as usual
ex.progress.completedUnitCount += 1
// outputs "0.5 completed"
// And compare with Combines #Published (almostĀ°) same behaviour
ex.textualRepresentation = "string"
// outputs "string"
ex.$progress.emit() // Re-emits the current value
ex.$progress.send(ex.progress) // Emits given value
I try to use the set method for calling a function after the value is changed.
I did not see why the set method is not called.
The code could be directly executed in playground
//: Playground - noun: a place where people can play
import UIKit
protocol RandomItem {
var range : (Int,Int) {get set}
var result : Int {get set}
init()
mutating func createRandom()
}
extension RandomItem {
var range : (Int,Int) {
get {
return range
}
set {
range = newValue
self.createRandom()
}
}
}
struct Item: RandomItem {
var range = (0,1)
var result: Int = 0
init() {
self.createRandom()
}
mutating func createRandom() {
let low = UInt32(range.0)
let high = UInt32(range.1)
result = Int(arc4random_uniform(high - low + 1) + low)
}
}
Your struct Item declares its own range property, which overrides the default you created in the protocol extension. The range property in Item has no getters or setters defined to do what your extension version does.
Another issue:
Your protocol extension defines the range property as a computed property (no storage) whose getter and setter both call itself. This will loop infinitely.
Maybe you are looking for something more like:
protocol RandomItem {
var storedRange: (Int, Int) { get }
var range : (Int,Int) {get set}
var result : Int {get set}
init()
mutating func createRandom()
}
extension RandomItem {
var range : (Int,Int) {
get {
return storedRange
}
set {
storedRange = newValue
self.createRandom()
}
}
}
struct Item: RandomItem {
var result: Int = 0
var storedRange = (0, 1)
init() {
self.createRandom()
}
mutating func createRandom() {
let low = UInt32(range.0)
let high = UInt32(range.1)
result = Int(arc4random_uniform(high - low + 1) + low)
}
}
This requires a conforming type to define a stored property storedRange, which the default implementation of the computed property range will interact with.