Save value on stepper SwiftUI - swift

I have a stepper:
Stepper(value: $timeBetweenMeals, in: 1...10, step: 1) {
Text("Time Between Meals: \(timeBetweenMeals, specifier: "%.0f")h")
self.settingsUserDefaults = $timeBetweenMeals
}
That gives me error because I'm trying to set: self.settingsUserDefaults = $timeBetweenMeals inside that view. How can I run that logic? I don't have a button, I just want to save that #Published value as the user changes it

Use a custom Binding, and inside your set you can store your value:
Stepper(value: Binding<Int>(get: {
self.timeBetweenMeals
}, set: {
print($0) // here you can access your value with $0, store it here
self.timeBetweenMeals = $0
}), in: 1...10, step: 1) {
Text("Time Between Meals: \(timeBetweenMeals)h")
}

You need to perform other actions when Stepper value changes in onIncrement and onDecrement method not inside of the label parameter which is meant to return the View of the Stepper. Here is an example (Note: this is just an example, you can achieve this in many easier ways too):
struct ContentView: View {
#State var timeBetweenMeals: Int
var body: some View {
Stepper(value: Binding<Int>(get: {
self.timeBetweenMeals
}, set: {
print($0)
self.timeBetweenMeals = $0
}), in: 1...10, step: 1) {
Text("Time Between Meals: \(timeBetweenMeals)h")
}
}
}
Note: I've updated to a much better approach. Credits to davidev. Also, your specifier seems to be incorrect.

Related

Provide default value to properties in SwiftUI

I've learning SwiftUI for a week, recently I found a confusing issue with it.
#State private var checkAmount = 0.0
var body: some View {
NavigationView {
Form {
Section {
TextField("Amount", value: $checkAmount, format: .currency(code: Locale.current.currency?.identifier ?? "USD"))
.keyboardType(.decimalPad)
}
Section{
Text(checkAmount, format:.currency(code: Locale.current.currency?.identifier ?? "USD"))
}
}
}
}
I use TextFile to receive user's input and alter the value of checkAmount, and make the value shown in the section below. Here is my preview in xcode.
Preview
But when I type a random number and delete it all, this happend:
Still a digit here
It seems SwiftUI didn't delete it all, and still keep the last digit I deleted.
I guess maybe I should give it an default value when user's input is empty?
Some additional information maybe necessary: I'm using a M1 MacMini and XCode 14.1
Approach
For currency(code:) the value type is Decimal, so you have to use Optional Decimal
https://developer.apple.com/documentation/foundation/parseableformatstyle/3796617-currency
When the last digit is deleted it becomes nil
Use checkAmount ?? 0 in your TextField
Code
struct ContentView: View {
#State private var checkAmount = Decimal?(0.0) //Optional decimal
var body: some View {
NavigationView {
Form {
Section {
TextField("Amount", value: $checkAmount, format: .currency(code: Locale.current.currency?.identifier ?? "USD"))
.keyboardType(.decimalPad)
}
Section{
//Use ?? operator to display 0 when nil
Text(checkAmount ?? 0, format:.currency(code: Locale.current.currency?.identifier ?? "USD"))
}
}
.onChange(of: checkAmount) { newValue in
print("checkAmount = \(String(describing: newValue))")
}
}
}
}

Adapting #State variables and UI to user actions in SwiftUI

I am a novice at programming and exploring SwiftUI. I've been tackling a challenge for too long, and hoping that someone can guide me to the right direction!
I want a list of interlinked sliders (as in Interlinked Multiple Sliders in SwiftUI), but with the number of sliders that change dynamically, depending on actions taken by a user.
For example, a user can choose various items, and later on adjust the percentage variable with sliders (and where these percentages are interdependent as in the linked example).
class Items: ObservableObject {
#Published var components = [ItemComponent]()
func add(component: itemComponent){
components.append(component)
}
}
struct ItemComponent: Hashable, Equatable, Identifiable {
var id = UUID().uuidString
var name: String = ""
var percentage: Double
}
Conceptually, it seems I need to do two things to adapt the linked code:
generate an array of Binding with the number of elements equal to Items.Component.EndIndex and
assign each Binding to the percentage of each ItemComponent.
I am fumbling on both. For 1., I can easily manually create any number of variables, e.g.
#State var value1 = 100
#State var value2 = 100
#State var value3 = 100
let allBindings = [$value1, $value2, $value3]
but how do I generate them automatically?
For 2., I can use ForEach() to call the components, or Index, but not both together:
ForEach(Items.components){ component in
Text("\(component.name)")
Text("\(component.percentage)")
}
ForEach(Items.components.indices){ i in
synchronizedSlider(from: allBindings, index: i+1)
}
In broken code, what I want is something like:
ForEach(Items.component){component in
HStack{
Text("component.name")
Spacer()
synchronizedSlider(from: allBindings[$component.percentage], index: component.indexPosition)
}
where allBindings[$component.percentage] is a binding array comprised of each itemComponent's percentage, and the index is an itemComponent's index.
I am happy to share more code if relevant. Any help would be greatly appreciated!
To adapt the existing code you linked, if you're going to have a dynamic number of sliders, you'll definitely want your #State to be an array, rather than individual #State variables, which would have to be hard coded.
Once you have that, there are some minor syntax issues changing the synchronizedBinding functions to accept Binding<[ItemComponent]> rather than [Binding<Double>], but they are pretty minor. Luckily, the existing code is pretty robust outside of the initial hard-coded states, so there isn't any additional math to do with the calculations.
I'm using ItemComponent rather than just Double because your sample code included it and having a model with a unique id makes the ForEach code I'm using for the sliders easier to deal with, since it expects uniquely-identifiable items.
struct ItemComponent: Hashable, Equatable, Identifiable {
var id = UUID().uuidString
var name: String = ""
var percentage: Double
}
struct Sliders: View {
#State var values : [ItemComponent] = [.init(name: "First", percentage: 100.0),.init(name: "Second", percentage: 0.0),.init(name: "Third", percentage: 0.0),.init(name:"Fourth", percentage: 0.0),]
var body: some View {
VStack {
Button(action: {
// Manually setting the values does not change the values such
// that they sum to 100. Use separate algorithm for this
self.values[0].percentage = 40
self.values[1].percentage = 60
}) {
Text("Test")
}
Button(action: {
self.values.append(ItemComponent(percentage: 0.0))
}) {
Text("Add slider")
}
Divider()
ScrollView {
ForEach(Array(values.enumerated()),id: \.1.id) { (index,value) in
Text(value.name)
Text("\(value.percentage)")
synchronizedSlider(from: $values, index: index)
}
}
}.padding()
}
func synchronizedSlider(from bindings: Binding<[ItemComponent]>, index: Int) -> some View {
return Slider(value: synchronizedBinding(from: bindings, index: index),
in: 0...100)
}
func synchronizedBinding(from bindings: Binding<[ItemComponent]>, index: Int) -> Binding<Double> {
return Binding(get: {
return bindings[index].wrappedValue.percentage
}, set: { newValue in
let sum = bindings.wrappedValue.indices.lazy.filter{ $0 != index }.map{ bindings[$0].wrappedValue.percentage }.reduce(0.0, +)
// Use the 'sum' below if you initially provide values which sum to 100
// and if you do not set the state in code (e.g. click the button)
//let sum = 100.0 - bindings[index].wrappedValue
let remaining = 100.0 - newValue
if sum != 0.0 {
for i in bindings.wrappedValue.indices {
if i != index {
bindings.wrappedValue[i].percentage = bindings.wrappedValue[i].percentage * remaining / sum
}
}
} else {
// handle 0 sum
let newOtherValue = remaining / Double(bindings.wrappedValue.count - 1)
for i in bindings.wrappedValue.indices {
if i != index {
bindings[i].wrappedValue.percentage = newOtherValue
}
}
}
bindings[index].wrappedValue.percentage = newValue
})
}
}

SwiftUI Picker Help -- Populating picker based on the selection of another picker

I am trying to populate a picker based on the selection of another picker. I am new to Swift and have been beating my head on this for way too long. I am sure its not as difficult as I am making it but I would appreciate any assistance.
I think my biggest issue is passing the selection of the first picker to the array name of the second. I have used switch case, tried to pass the selection raw value...etc. Below is a sample of what I would like it to look like without the binding of the pickers. Thanks
import SwiftUI
struct veggie: View {
let veggies = ["Beans", "Corn", "Potatoes"]
let beanList = ["Pole", "String", "Black"]
let cornList = ["Peaches & Cream", "Sweet"]
let potatoList = ["Yukon Gold", "Idaho"]
#State private var selectedVeggie = "Bean"
#State private var selectedBean = "Pole"
#State private var selectedType = ""
var body: some View {
NavigationView{
VStack{
Form{
Picker("Please choose a veggie", selection: $selectedVeggie)
{
ForEach(veggies, id: \.self) {
Text($0)
}
}
Text("You selected \(selectedVeggie)")
Picker("Type", selection: $selectedBean)
{
ForEach(beanList, id: \.self) {
Text($0)
}
}
}
} .navigationTitle("Veggie Picker")
}
}
}
I'm imagining that you want this to be somewhat dynamic and not hardcoded with if statements. In order to accomplish that, I setup an enum with the veggie types and then a dictionary that maps the veggie types to their subtypes. Those look like:
enum VeggieType : String, CaseIterable {
case beans, corn, potatoes
}
let subTypes : [VeggieType: [String]] = [.beans: ["Pole", "String", "Black"],
.corn: ["Peaches & Cream", "Sweet"],
.potatoes: ["Yukon Gold", "Idaho"]]
Then, in the view, I did this:
struct ContentView: View {
#State private var selectedVeggie : VeggieType = .beans
#State private var selectedSubtype : String?
var subtypeList : [String] {
subTypes[selectedVeggie] ?? []
}
var body: some View {
NavigationView{
VStack{
Form{
Picker("Please choose a veggie", selection: $selectedVeggie)
{
ForEach(VeggieType.allCases, id: \.self) {
Text($0.rawValue.capitalized)
}
}
Text("You selected \(selectedVeggie.rawValue.capitalized)")
Picker("Type", selection: $selectedSubtype)
{
ForEach(subtypeList, id: \.self) {
Text($0).tag($0 as String?)
}
}
}
} .navigationTitle("Veggie Picker")
}
}
}
The selectedVeggie is now typed with VeggieType. The selectedSubtype is an optional String that doesn't have an initial value (although you could set one if you want).
The first picker goes through all of the cases of VeggieType. The second one dynamically changes based on the computed property subtypeList which grabs the item from the subTypes dictionary I had made earlier.
The tag item is important -- I had to make the tag as String? because SwiftUI wants the selection parameter and the tag() to match exactly.
Note: you say you're new to Swift, so I'll mention that you had named your view `veggie` -- in Swift, normally types have capital names. I named mine `ContentView`, but you could rename it `VeggieView` or something like that.

How to build NumberField in SwiftUI?

I'm working on MacOS and I try to make NumberField — something like TextField, but for numbers. In rather big tree of views at the top I had:
...
VStack {
ForEach(instances.indices, id:\.self) {index in
TextField("",
text: Binding(
get: {"\(String(format: "%.1f", instances[index].values[valueIndex]))"},
set: {setValueForInstance(index, valueIndex, $0)})
)
}
}
...
And it worked well, but not nice:
✔︎ when I changed value, all View structure was redrawn – good
✔︎ values was updated if they were changed by another part of Views structure – good
✖︎ it was updated after each keypresses, which was annoying, when I tried to input 1.2, just after pressing 1 view was updated to 1.0. Possible to input every number but inconvenient – bad
So, I tried to build NumberField.
var format = "%.1f"
struct NumberField : View {
#Binding var number: Double {
didSet {
stringNumber = String(format: format, number)
}
}
#State var stringNumber: String
var body: some View {
TextField("" , text: $stringNumber, onCommit: {
print ("Commiting")
if let v = Double(stringNumber) {
number = v
} else {
stringNumber = String(format:format, number)
}
print ("\(stringNumber) , \(number)" )
})
}
init (number: Binding<Double>) {
self._number = number
self._stringNumber = State(wrappedValue: String(format:format, number.wrappedValue))
}
}
And It's called from the same place as before:
...
VStack {
ForEach(instances.indices, id:\.self) {index in
NumberField($instances[index].values[valueIndex])
}
}
...
But in this case it never updates NumberField View if values was changed by another part of View. Whats's wrong? Where is a trick?

Dynamic TextFields based on user input using SwiftUI

I am trying to create a dynamic set of TextFields which are added after the user presses the add button. Each press will add another set of those fields. I am new to this so please bear with me. I am getting a fatal error: index out of range. Here is a simple example of what I am trying to achieve.
struct ContentView: View {
#State var name: [String] = []
#State var counter = 0
var body: some View {
Form {
Section {
ForEach(0..<counter, id: \.self) { index in
TextField("Name", text: self.$name[index])
}
Button(action:{
self.counter += 1
}) {
Text("Add more")
}
}
}
}
}
You're increasing the counter without adding new items. If you add a new item to your array it will work without errors:
Button(action:{
self.name.append("")
self.counter += 1
}) {
Text("Add more")
}
But preferably don't use the counter variable at all. If you add a new item to the names array it's count will automatically increase. You can use it in the ForEach loop like this:
ForEach(0..<names.count, id: \.self) { index in
TextField("Name", text: self.$names[index])
}
Button(action:{
self.names.append("")
}) {
Text("Add more")
}
Note: For arrays it's better to use plural names: names instead of name. It indicates it's a collection of items.
SWIFTUI:
Here's the code to show multiple dynamic Text() element:
#State var textsArray = ["a","b","c","d"]
HStack{ ForEach(textsArray, id: \.self)
{ text in
Text("\(text)")
}
}
You can add more texts into "textsArray" or you can change the values in "textsArray" and it'll be automatically changing on UI.