why cant i call a mutatable function in a button - swift

I keep getting an error when I try to call my function on the button press. I am fairly new to swift so there might be an obvious mistake. The code here is what i have that should involve the error. I have searched for other answers to this but could not find anything. I dont know what i need to do to call a mutable function.
import SwiftUI
struct ContentView2: View {
#State public var weightLifted:Int
#State public var repsPerformed:Int
#State private var percentOfReps:Double = 0.0
#State private var oneRepMax:Double = 0.0
var weightAtEachPercent:Array = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
mutating func calcMax() {
switch repsPerformed {
case 1: percentOfReps = 1.0
case 2: percentOfReps = 0.975
case 3: percentOfReps = 0.95
case 4: percentOfReps = 0.925
case 5: percentOfReps = 0.9
case 6: percentOfReps = 0.875
case 7: percentOfReps = 0.85
case 8: percentOfReps = 0.825
case 9: percentOfReps = 0.8
case 10: percentOfReps = 0.75
case 11: percentOfReps = 0.725
case 12: percentOfReps = 0.7125
case 13: percentOfReps = 0.7
case 14: percentOfReps = 0.685
case 15: percentOfReps = 0.675
default:
percentOfReps = 0.5
}
oneRepMax = Double(weightLifted) / percentOfReps
weightAtEachPercent[0] = oneRepMax
weightAtEachPercent[1] = oneRepMax * 0.975
weightAtEachPercent[2] = oneRepMax * 0.95
weightAtEachPercent[3] = oneRepMax * 0.9
weightAtEachPercent[4] = oneRepMax * 0.875
weightAtEachPercent[5] = oneRepMax * 0.825
weightAtEachPercent[6] = oneRepMax * 0.75
weightAtEachPercent[7] = oneRepMax * 0.7125
weightAtEachPercent[8] = oneRepMax * 0.675
}
var body: some View {
Button(action: {
//Place of error
//I tried using self.calcMax() as well but same error
//Error "Cannot use mutating member on immutable value: 'self' is immutable
calcMax()
}) {
Text("Calculate")
}
}
}

Have you tried adding #State to the array?
and remember that #State is always considered to be private, because when you use #State, you're essentially saying SwiftUI owns that view.
A better solution is to use an #Binding or an #ObservableObject/#ObservedObject instead.
A better quick read on it can be found here:

Views are immutable. They can store information in #State variables that will be copied over to new immutable instances, but the View itself can't change. So View functions can't be mutating.
Each piece of information should have a single source of truth. If this information is entirely internal to the view, and nothing outside the view needs to see it, then mark it #State. If things outside the view need it, but the view needs to be able to signal that it's changed, make it a #Binding. If it's a model that the view needs to react to, make it an #ObservedObject.
In your case, the updated values are already #State, and changing state doesn't actually change a view (it triggers the creation of a new view). So you can just remove the mutating modifier.

Related

Custom Set of Steps in Slider

I have a #StateObject Slider. It is set to slide 1...5 step 1.
I need to have custom step [1,2,5,10,20] instead of step 1.
I have a setup where I have the slider work as an array index to my array of custom step. It is working. But It is jerky. Single slide registers slide value dozen times making my array to get picked up dozen times jerking the process. How can I have custom steps smooth.
I can adjust within the app but if Slider provides the exact number, logic of the app become much cleaner. Less translation, clear the logic.
Slider(value: $tSlider, in: 1...5, step: 1)
// How can I make above like below
Slider(value: $tSlider, in: 1...5, step: [1,2,5,10,20]
// What I have currently
#Published var tSteps:[Int] = [1,2,5,10,20]
#Published var tSlider: Double = 1
var tScale: Int { return tlSteps[Int(tlSlider)-1] }
Setting steps on Slider was a bad idea. Stepper was correct choice.
Stepper("Year Scale", onIncrement: {
switch tVM.tlScale {
case 1: self.tlScale = 2
case 2: self.tlScale = 5
case 5: self.tlScale = 10
case 10: self.tlScale = 20
default: self.tlScale = 20
},
onDecrement: {
switch tVM.tlScale {
case 20: self.tlScale = 10
case 10: self.tlScale = 5
case 5: self.tlScale = 2
case 2: self.tlScale = 1
default: self.tlScale = 1
}
})
referred to : post here and Skipjakk's answer. Strange Behavior of Stepper in SwiftUI

Unit Conversion in SwiftUI

I'm currently working on a unit conversion app, and ran into a problem that I'm still trying to figure how to solve it. The app is supposed to convert one unit to a different unit, and supposed to convert and display new value in the text view every time a unit is converted to a different unit.(meters to kilometers, miles to yards, etc.). I created a computed property called result which handles the conversion, but when I run the code in the simulator and toggle between the units, no changes are displayed to show the units converted successfully. I tried using a guard statement before the calculations since I figured Value is 0 after converting inputNumber to an integer, but that doesn't seem to work. I'm not sure where I went wrong but any help with this would be greatly appreciated. I have attached the code and simulator below.
Thanks.
PS. I have learned how to and can build this app using the measurement functionality provided by Apple, I'm trying to learn how to build it in a different way by basically using math calculations only.
struct ContentView: View {
#State private var inputNumber = ""
#State private var inputUnitValue = 2
#State private var outputUnitValue = 2
#State private var outputValue = ""
let inputUnits = ["Feet", "Meters", "Kilometers", "Yard", "Miles"]
let outputUnits = ["Feet", "Meters", "Kilometers", "Yard", "Miles"]
var result: Double {
var outputType = ""
var inputType = ""
let value = Double(inputNumber) ?? 0
var output = Double(outputValue) ?? 0
var input = Double(inputNumber) ?? 0
guard value > 0 else {
return 0
}
//Converts input value to input unit
switch inputUnits[inputUnitValue] {
case "meters":
input = value * 3.28
case "kilometers":
input = value * 3280.84
case "yards":
input = value * 3
case "miles":
input = value * 5280
case "feet":
input = value * 1
default:
input = value * 3.28
}
//Converts input unit to output unit
switch outputUnits[outputUnitValue] {
case "meters":
output = value / 3.28
case "kilometers":
output = value / 3280.84
case "yards":
output = value / 3
case "miles":
output = value / 5280
case "feet":
output = value / 1
default:
output = value / 3.28
}
return output
}
var body: some View {
NavigationView {
Form {
Section {
TextField("Value", text: $inputNumber)
.keyboardType(.numberPad)
}
Section(header: Text("Input Units")) {
Picker("Input Units", selection: $inputUnitValue) {
ForEach(0 ..< inputUnits.count) {
Text("\(self.inputUnits[$0])")
}
}
.pickerStyle(SegmentedPickerStyle())
}
Section(header: Text("Output Units")) {
Picker("Input Units", selection: $outputUnitValue) {
ForEach(0 ..< outputUnits.count) {
Text("\(self.outputUnits[$0])")
}
}
.pickerStyle(SegmentedPickerStyle())
}
Section (header: Text("output")){
Text("\(result, specifier: "%.2f")")
}
}
.navigationBarTitle("Unit Conversion")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
switch is case sensitive. Your inputUnits and outputUnits are capitalized...
let inputUnits = ["Feet", "Meters", "Kilometers", "Yard", "Miles"]
... while your switch conditions are lowercased.
switch inputUnits[inputUnitValue] {
case "meters": /// lowercased
...
}
So, your switch statement always falls through to the default.
To fix, just add .lowercased():
switch inputUnits[inputUnitValue].lowercased() {
switch outputUnits[outputUnitValue].lowercased() {
One more thing: I assume the purpose of your switches are to:
Convert value to a standard feet unit
Convert that feet to the output unit
In this case, inside the second switch, you need to perform calculations using your input variable (which is the input converted to feet) - not value.
To make things less confusing, I've renamed input to inputAsFeet.
var result: Double {
let value = Double(inputNumber) ?? 0
var inputAsFeet = Double(0)
var output = Double(0)
guard value > 0 else {
return 0
}
switch inputUnits[inputUnitValue].lowercased() {
case "meters":
inputAsFeet = value * 3.28
case "kilometers":
inputAsFeet = value * 3280.84
case "yards":
inputAsFeet = value * 3
case "miles":
inputAsFeet = value * 5280
case "feet":
inputAsFeet = value * 1
default:
inputAsFeet = value * 3.28
}
switch outputUnits[outputUnitValue].lowercased() {
case "meters":
output = inputAsFeet / 3.28
case "kilometers":
output = inputAsFeet / 3280.84
case "yards":
output = inputAsFeet / 3
case "miles":
output = inputAsFeet / 5280
case "feet":
output = inputAsFeet / 1
default:
output = inputAsFeet / 3.28
}
return output
}
Result:

Swift Combine: Alternatives too eraseToAnySubscriber?

I am trying to use Combine to update a colour when my red, green or blue variables change. The examples I have looked at use sink() and that seems appropriate for me but eraseToAnySubscriber is MIA and I can't find an alternate.
What seems to work is to use an assign() to a computed variable but that seems like a bit of a hack.
init() {
redCancellable = red.hasChanged.receive(on: RunLoop.main).assign(to: \.rgbUpdated, on: self)
}
Is there any way to save the value returned by sink()?
This sounds like a job for CombineLatest. And yes, sink is a perfectly good way to dispose of the end of the pipeline in whatever way you like.
Here's a simple example. I'll start with an object that has r, g, and b variables:
class ColorObject {
#Published var r : CGFloat = 1
#Published var g : CGFloat = 1
#Published var b : CGFloat = 1
}
Now imagine that somewhere we have an instance of that object; call it colorObject. Then we can configure a publisher:
let rpub = colorObject.$r
let gpub = colorObject.$g
let bpub = colorObject.$b
let colorpub = Publishers.CombineLatest3(rpub,gpub,bpub)
.map { UIColor(red: $0.0, green: $0.1, blue: $0.2, alpha: 1) }
The result is that every time colorObject's r or g or b changes, a UIColor comes down the pipeline. Now we can receive a notification from colorpub by subscribing to it with sink and dispose of the result however we like. Let's set some interface object's color to that color:
let sink = colorpub.sink { self.view.backgroundColor = $0 }
Alternatively, I could write it using assign, which perhaps is cleaner, though backgroundColor is an Optional so I have to interpose a map operator because keyPaths are not covariant:
let assign = colorpub.map{Optional($0)}
.assign(to: \.backgroundColor, on: self.view)
Now whenever colorObject's r, g, or b changes, our view's color changes accordingly.
This is not the only way to accomplish this goal — far from it! But it's a simple example of getting things done with Combine. A possibly useful variant would be to move the colorpub publisher up into the ColorObject; that way, the ColorObject vends the color, directly, itself:
class ColorObject {
#Published var r : CGFloat = 1
#Published var g : CGFloat = 1
#Published var b : CGFloat = 1
lazy var colorpub = Publishers.CombineLatest3($r,$g,$b)
.map { UIColor(red: $0.0, green: $0.1, blue: $0.2, alpha: 1) }
}
This changes nothing about the sink or assign:
let sink = colorObject.colorpub.sink { // ... whatever
// or
let assign = colorObject.colorpub.map{Optional($0)}
.assign(to: \.backgroundColor, on: self.view)

SpriteKit - Struct for nodes position

i want to make a struct where i declare different positions. I have many nodes which always use the same positions so instead of typing every time the same x-values and y-values i would like to just type positions.p1.
private struct Positions {
let P1 = myGameScene.frame.size.width * 0.05
let P2 = myGameScene.frame.size.width * 0.15
}
Now i get the error: Instance member frame cannot be used on type myGameScene. myGameScene is a SKScene.
If i try self. instead of myGameScene i get the error: Value of type NSObject -> () -> myGameScene has no member frame.
Need help :-(
It does work
class MyGameScene: SKScene { }
let myGameScene = MyGameScene()
struct XPositions {
let x0 = myGameScene.frame.size.width * 0.05
let x1 = myGameScene.frame.size.width * 0.15
}
print(XPositions().x0)
However let me say it's a weird way to solve the problem infact:
You are using a struct instead of an array
You need to create a value of the struct just to access the properties that are the same of every value of Positions
The properties (P1, P2) should be lowercase...
Suggestion
Why don't you just add a computed property to your scene?
class MyGameScene: SKScene {
lazy var positions: [CGFloat] = {
let factors: [CGFloat] = [0.15, 0.05]
return factors.map { $0 * self.frame.size.width }
}()
}
Use bounds instead of frame.
private struct Positions {
let P1 = myGameScene.bounds.size.width * 0.05
let P2 = myGameScene.bounds.size.width * 0.15
}

How to store many attributes at once?

I'm working with Swift, Xcode 6 and SpriteKit,
I want make a game where some sprites fall down from the top of the screen, but each sprite have a defined speed, position and activation time. I have this working code, but I really don't think that it's the most optimised way to do it:
class obstacles: SKSpriteNode
{
var isOnScreen = false
var type: Int = 0
var initTime: Int = 0
}
var obstacle = [obstacles]() // obstacle is an array of SKSpriteNode
// type : initTime : speed : position : xScale : yScale
var level1: [String] = ["0:120:3:0:1:1", "0:130:4:80:2:2","1:140:8:120:6:1","0:150:6:240:2:2"]
override func didMoveToView(view: SKView)
{
initObstacles()
}
func initObstacles()
{
for var x = 0; x < level1.count; x++ // for each obstacle
{
var index = 0
var type = String("")
var initTime = String("")
var speed = String("")
var position = String("")
var xScale = String("")
var yScale = String("")
var lastIndex = obstacle.count
for Character in level1[x] // we read each character one by one
{
if Character == ":" { index++ } // if it's a ":" we change the variable
else
{
switch index
{
case 0:
type += String(Character)
case 1:
initTime += String(Character)
case 2:
speed += String(Character)
case 3:
position += String(Character)
case 4:
xScale += String(Character)
case 5:
yScale += String(Character)
default:
break;
}
}
}
obstacle.append(obstacles(imageNamed: "Rectangle")) // we add an element to the array
obstacle[lastIndex].type = type.toInt()! // we init all the values
obstacle[lastIndex].initTime = initTime.toInt()!
obstacle[lastIndex].speed = CGFloat(speed.toInt()!)
obstacle[lastIndex].size.width = DEFAULT_OBSTACLE_SIZE * CGFloat(xScale.toInt()!)
obstacle[lastIndex].size.height = DEFAULT_OBSTACLE_SIZE * CGFloat(yScale.toInt()!)
obstacle[lastIndex].position = CGPointMake(CGFloat(position.toInt()!) - obstacle[lastIndex].size.width/2, CGRectGetMaxY(frame) + obstacle[lastIndex].size.height/2)
}
}
Do you know how could I manage to do the same thing, but more "clean" ?
I would suggest to create a class or struct that holds all necessary data for an obstacle and additionally change your type from a standard number to an enum, e.g.:
enum ObstacleType {
case Block, Tree, WhatEverObstaclesYouHave...
}
struct Obstacle {
var type: ObstacleType
var initializationTime: NSTimeInterval
var speed: Double
// and similarly for position, your scales and what you may need in future
}
and create them using, e.g.
Obstacle(type: .Block, initializationTime: 0, speed: 12.0, ...)
Advantage (obviously) is that you have no problems anymore parsing your string (there is no string anymore) and can provide all necessary information using the appropriate type directly. And you can easily use an enum for your type, which should be an enum, because your Obstacle is not a 1, but a Block, Tree or whatever.
Your level1 variable could then be defined like this:
var level1 : [Obstacle] = [
Obstacle(type: .Block, initializationTime: 120, speed: 3.0, ...),
Obstacle(type: .Block, initializationTime: 130, speed: 4.0, ...),
Obstacle(type: .Tree, initializationTime: 140, speed: 8.0, ...),
Obstacle(type: .Block, initializationTime: 150, speed: 6.0, ...)
]
To get rid of the labels in the initializer, just define your own initializer. Just add this to your struct:
init(_ type: ObstacleType, _ time: NSTimeInterval, _ speed: Double, ...) {
self.type = type
self.initializationTime = time
self.speed = speed
// other variables...
}
Then you can create every Obstacle like this:
Obstacle(.Block, 120, 3.0, ...)
But now you can not easily tell anymore which number has what meaning from reading the instantiation. I do not recommend this just to type less as autocomplete will present you with most of it.