I have a private variable in a struct which I can only access using a setter and getter. I want to change this variable using a slider, so I am attempting to bind a different var with this var using willSet:
struct MyStruct {
private var myVar: Float? = nil
mutating func setMyVar(newVal: Float) {
if (SomeCondition) { // always true when the slider is in use
myVar = newVal
} else {
// stuff
}
}
func getMyVar() -> Float {
myVar == nil ? 0.0 : myVar!
}
}
struct MyView: View {
#State var myStructToEdit = MyStruct()
#State var tempVar: Double = 0.0 {
willSet {
myStructToEdit.setMyVar(newVal: Float(newValue))
}
}
var body: some View {
VStack {
Text(String(tempVar))
Text(String(myStructToEdit.getMyVar()))
Slider(value: $tempVar, in: 1.0...20.0, step: 1.0)
}
}
}
As the slider moves, tempVar changes but MyVar doesn't. What is the correct way to achieve this binding?
Property observers won't work with SwiftUI #State variables.
Use .onChange(of:) to act upon changes to tempVar:
struct MyView: View {
#State var myStructToEdit = MyStruct()
#State var tempVar: Double = 0.0
var body: some View {
VStack {
Text(String(tempVar))
Text(String(myStructToEdit.getMyVar()))
Slider(value: $tempVar, in: 1.0...20.0, step: 1.0)
.onChange(of: tempVar) { value in
myStructToEdit.setMyVar(newVal: Float(value))
}
}
}
}
Use Binding(get:set:) to directly set and get the value in your struct:
You don't need tempVar. You can directly set and get the value to and from your struct.
struct ContentView: View {
#State var myStructToEdit = MyStruct()
var body: some View {
VStack {
Text(String(myStructToEdit.getMyVar()))
Slider(value: Binding(get: {
myStructToEdit.getMyVar()
}, set: { value in
myStructToEdit.setMyVar(newVal: value)
}), in: 1.0...20.0, step: 1.0)
}
}
}
or assign the Binding to a let to make it cleaner:
struct ContentView: View {
#State var myStructToEdit = MyStruct()
var body: some View {
let myVarBinding = Binding(
get: { myStructToEdit.getMyVar() },
set: { value in myStructToEdit.setMyVar(newVal: value) }
)
VStack {
Text(String(myStructToEdit.getMyVar()))
Slider(value: myVarBinding, in: 1.0...20.0, step: 1.0)
}
}
}
Note: Since myVarBinding is already a binding, you do not need to use a $ to turn it into a binding.
Related
I've included stubbed code samples. I'm not sure how to get this presentation to work. My expectation is that when the sheet presentation closure is evaluated, aDependency should be non-nil. However, what is happening is that aDependency is being treated as nil, and TheNextView never gets put on screen.
How can I model this such that TheNextView is shown? What am I missing here?
struct ADependency {}
struct AModel {
func buildDependencyForNextExperience() -> ADependency? {
return ADependency()
}
}
struct ATestView_PresentationOccursButNextViewNotShown: View {
#State private var aDependency: ADependency?
#State private var isPresenting = false
#State private var wantsPresent = false {
didSet {
aDependency = model.buildDependencyForNextExperience()
isPresenting = true
}
}
private let model = AModel()
var body: some View {
Text("Tap to present")
.onTapGesture {
wantsPresent = true
}
.sheet(isPresented: $isPresenting, content: {
if let dependency = aDependency {
// Never executed
TheNextView(aDependency: dependency)
}
})
}
}
struct TheNextView: View {
let aDependency: ADependency
init(aDependency: ADependency) {
self.aDependency = aDependency
}
var body: some View {
Text("Next Screen")
}
}
This is a common problem in iOS 14. The sheet(isPresented:) gets evaluated on first render and then does not correctly update.
To get around this, you can use sheet(item:). The only catch is your item has to conform to Identifiable.
The following version of your code works:
struct ADependency : Identifiable {
var id = UUID()
}
struct AModel {
func buildDependencyForNextExperience() -> ADependency? {
return ADependency()
}
}
struct ContentView: View {
#State private var aDependency: ADependency?
private let model = AModel()
var body: some View {
Text("Tap to present")
.onTapGesture {
aDependency = model.buildDependencyForNextExperience()
}
.sheet(item: $aDependency, content: { (item) in
TheNextView(aDependency: item)
})
}
}
I am trying to increment or decrement a value using Stepper, however I cannot fathom how to take the new value and update my model.
I've also tried to use the onIncrement, onDecrement version, but here there appears to be no option to disable the + or - when the range has been reached.
Here's my example code:
import SwiftUI
struct ContentView: View {
var body: some View {
MainView()
}
}
struct MainView: View {
#ObservedObject var updateAge = UpdateAge()
#State private var showAgeEditor = false
#State var age: Int = 0
var body: some View {
VStack {
/// Show the current age.
Text("Age \(age)")
Image(systemName: "keyboard")
.onTapGesture {
self.showAgeEditor = true
}
/// Present the sheet to update the age.
.sheet(isPresented: $showAgeEditor) {
SheetView(showAgeEditor: self.$showAgeEditor)
.environmentObject(self.updateAge)
.frame(minWidth: 300, minHeight: 400)
}
}
/// Calls the viewModel (UpdateAge) to fetch the age from the model.
.onAppear(perform: {self.age = self.updateAge.withAge})
}
}
struct SheetView: View {
#EnvironmentObject var updateAge: UpdateAge
#Binding var showAgeEditor: Bool
/// Steppers state.
#State private var age: Int = 18
let maxAge = 21
let minAge = 18
var body: some View {
return VStack {
Text("Age of person = \(age)")
/// When I increment or Decrement the stepper I want the Age to increase or decrease.
Stepper<Text>(value: $age, in: minAge...maxAge, step: 1) {
/// And when it does, to store the value in the model via the viewModel.
/// However I appear to have created an infinite loop. Just uncomment the line below.
// self.updateAge.with(age: age)
/// Then display the value in the label.
return Text("\(age)")
}
}.onAppear(perform: {self.age = self.updateAge.withAge})
}
}
class UpdateAge: ObservableObject {
#Published var model = Model()
func with(age value: Int) {
print("Value passed \(value)")
self.model.setDrinkingAge(with: value)
}
var withAge: Int {
get { model.drinkingAge }
}
}
struct Model {
var drinkingAge: Int = 18
mutating func setDrinkingAge(with value: Int) {
drinkingAge = value
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
You can bind to model directly via view model projected value, like below (so local state age is redundant)
Stepper(value: $updateAge.model.drinkingAge, in: minAge...maxAge, step: 1) {
Text("\(updateAge.model.drinkingAge)")
}
In the process of learning SwiftUI, I am having this issue.
I have this view with a slider:
struct RangeSpanView: View {
var minimumValue,maximumValue:Double
init(minSlide: UInt64,
maxSlide: UInt64) {
self.minimumValue = Double(minSlide)
self.maximumValue = Double(maxSlide)
}
#State var sliderValue = 0.0
var body: some View {
VStack {
HStack {
Text("\(Int(minimumValue))")
Slider(value: $sliderValue,
in: minimumValue...maximumValue)
Text("\(Int(maximumValue))")
}.padding()
Text("\(Int(sliderValue))")
Spacer()
}
}
}
And this is the code where the view is loaded.
var body: some View {
NavigationLink(destination: RangeSpanView(minSlide: myMinVal,
maxSlide: myMaxVal),
label: {
.......... // Useful code.
})
}
It works fine using the slider. There is only a little inconsistency detail concerning the initialization of the sliderValue that I would like to fix.
The way it is in the current code is that sliderValue is initialized to 0.0, which is not exactly what I like when the min and max values are respectively 150.0 and 967.0 (for example).
I have tried various solutions to initialize sliderValue to minimumValue (for example) to make it consistent. But it always fails with some error message complaining about some "expected argument type 'Binding" in the Slider object.
So, is there a proper way to solve this problem?
You can initialize your #State variable with the lowest min value of the slider. So you won't need to do this in the willApear() method.
struct RangeSpanView: View {
var minimumValue,maximumValue:Double
//declare State here
#State var sliderValue : Double
init(minSlide: UInt64,
maxSlide: UInt64) {
self.minimumValue = Double(minSlide)
self.maximumValue = Double(maxSlide)
//Initialize the State with the lowest value
self._sliderValue = State(initialValue: self.minimumValue)
}
How about something like this:
struct TestView: View {
let myMinVal: UInt64 = UInt64.random(in: 100...200)
let myMaxVal: UInt64 = UInt64.random(in: 400...600)
#State var sliderValue: Double = 0
var body: some View {
RangeSpanView(
minimumValue: Double(myMinVal),
maximumValue: Double(myMaxVal),
sliderValue: $sliderValue
).onAppear {
self.sliderValue = Double(self.myMinVal)
}
}
}
struct RangeSpanView: View {
var minimumValue: Double
var maximumValue: Double
#Binding var sliderValue: Double
var body: some View {
VStack {
HStack {
Text("\(Int(minimumValue))")
Slider(value: $sliderValue,
in: minimumValue...maximumValue)
Text("\(Int(maximumValue))")
}.padding()
Text("\(Int(sliderValue))")
Spacer()
}
}
}
You can use a #Binding var on the SliderValue so that you can set starting value before you present it.
I used onAppear but you can set the sliderValue on wherever you are setting the myMinVal
I think Ludyem is onto somthing. On this site I found an explanation as to why setting the value in the init wont work.
But if we use the onAppear function on the slider we can ensure the default value is within range like this
.onAppear {
if(self.sliderValue < self.minimumValue) {
self.sliderValue = self.minimumValue
}
}
So the whole thing would look like this
struct RangeSpanView: View {
let minimumValue,maximumValue:Double
init(minSlide: UInt64, maxSlide: UInt64) {
self.minimumValue = Double(minSlide)
self.maximumValue = Double(maxSlide)
}
#State var sliderValue = 0.0
var body: some View {
VStack {
Text("hi guys")
HStack {
Text("\(Int(minimumValue))")
Slider(value: $sliderValue,
in: minimumValue...maximumValue)
.onAppear {
if(self.sliderValue < self.minimumValue) {
self.sliderValue = self.minimumValue
}
}
Text("\(Int(maximumValue))")
}.padding()
Text("\(Int(sliderValue))")
Spacer()
}
}
}
I want to extract String value from Observed Object
This is example code
import SwiftUI
import Combine
class SetViewModel : ObservableObject {
private static let userDefaultTextKey = "textKey"
#Published var text: String = UserDefaults.standard.string(forKey: SetViewModel.userDefaultTextKey) ?? ""
private var canc: AnyCancellable!
init() {
canc = $text.debounce(for: 0.2, scheduler: DispatchQueue.main).sink { newText in
UserDefaults.standard.set(newText, forKey: SetViewModel.userDefaultTextKey)
}
}
deinit {
canc.cancel()
}
}
struct SettingView: View {
#ObservedObject var viewModel = SettingViewModel()
var body: some View {
ZStack {
Rectangle().foregroundColor(Color.white).edgesIgnoringSafeArea(.all).background(Color.white)
VStack {
TextField("test", text: $viewModel.text).textFieldStyle(BottomLineTextFieldStyle()).foregroundColor(.red)
Text($viewModel.text) //I want to get String Value from $viewModel.text
}
}
}
}
I want to use "$viewModel.text"'s String value. How can I do this?
Here is fix
Text(viewModel.text) // << use directly, no $ needed, it is for binding
try this:
struct SettingView: View {
#ObservedObject var viewModel = SetViewModel()
var body: some View {
ZStack {
Rectangle().foregroundColor(Color.white).edgesIgnoringSafeArea(.all).background(Color.white)
VStack {
TextField("test", text: self.$viewModel.text)
.textFieldStyle(PlainTextFieldStyle())
.foregroundColor(.red)
Text(viewModel.text) //I want to get String Value from $viewModel.text
}
}
}
}
I am tearing out my hair trying to figure out how to bind the picked value in my SwiftUI view:
The picker needs to be bound to the Int returned from the tags. I need to covert this Int to the String and set the Binding. How?
struct ContentView: View {
#Binding var operatorValueString:String
var body: some View {
Picker(selection: queryType, label: Text("Query Type")) {
ForEach(0..<DM.si.operators.count) { index in
Text(DM.si.operators[index]).tag(index)
}
}.pickerStyle(SegmentedPickerStyle())
}
}
How and where can I set my operatorValueString ?
operatorValueString = DM.si.operators[queryType] //won't compile.
You can achieve the result, using your own custom binding that sets the string, whenever the picker's selection changes:
struct ContentView: View {
#State private var operatorString = ""
var body: some View {
VStack {
Subview(operatorValueString: $operatorString)
Text("Selected: \(operatorString)")
}
}
}
struct Subview: View {
#Binding var operatorValueString: String
#State private var queryType: Int = 0
let operators = ["OR", "AND", "NOT"]
var body: some View {
let binding = Binding<Int>(
get: { self.queryType },
set: {
self.queryType = $0
self.operatorValueString = self.operators[self.queryType]
})
return Picker(selection: binding, label: Text("Query Type")) {
ForEach(operators.indices) { index in
Text(self.operators[index]).tag(index)
}
}.pickerStyle(SegmentedPickerStyle())
}
}