How to initialize derived variables in body methods in SwiftUI (or alternate approach) - binding

I'm trying to figure out the right way to initialized derived variables in the body method for a SwiftUI view. An example would the string value for an editable integer which would then be edited in a TextField. The integer could for example be part of an #ObservedObject. I cannot figure out any remotely clean way to do this.
I've looked into using custom initializers but this does not seem like the right thing to do. I'm not even sure this code would be run at the appropriate time.
I've also tried using the .onAppear method for TextField, but this method does not appear to be re-executed when the view is rebuilt.
simplified example:
final class Values : ObservableObject {
#Published var count: Int = 0;
}
var sharedValues = Values()
struct ContentView : View {
#ObservedObject var values = sharedValues
var body: some View {
VStack {
Button(
action: { self.add() },
label: { Text("Plus")}
)
InnerView()
}
}
func add() { values.count += 1 }
}
struct InnerView : View {
#ObservedObject var values = sharedValues
#State private var text = ""
var body: some View {
// text = String(value.count) - what I want to do
TextField("", text: $text, onEditingChanged: updateCount)
.textFieldStyle(RoundedBorderTextFieldStyle())
}
func updateCount(updated: Bool) { /* this isn't important in this context */}
}
I would hope to be able to update sharedValues externally and see the update in MyView. In this example, I would want pressing the button to update the text field with the updated text value. But I can't figure a way to have the string representation of the count value computed at the appropriate point in the execution of the code.
I've tried multiple approaches to achieving this type of result, but have come up short.

I'm not sure if I'm understanding your question correctly, but if you are just trying to be able to change a number with a button, have the number be displayed in a text field, and then be able to edit it there, you don't need an ObserverableObject or multiple views.
Here is an example of how you can do it:
struct ContentView: View {
#State var count = 0
#State var countStr = ""
var body: some View {
VStack {
Button(action: {
self.count += 1
self.countStr = "\(self.count)"
}) {
Text("Plus")
}
TextField("", text: $countStr, onEditingChanged: updateCount)
}
}
func updateCount(updated: Bool) { /* this isn't important in this context */ }
}

Use value init method of TextField. This take the value as 2 way Binding. So it automatically update count from both text field and buttons.
import SwiftUI
import Combine
final class Values : ObservableObject {
#Published var count: Int = 0;
}
var sharedValues = Values()
struct AndrewVoelkel : View {
#ObservedObject var values = sharedValues
var body: some View {
HStack {
InnerView()
VStack{
Button(
action: { self.add() },
label: { Text("+")}
)
Button(
action: { self.sub() },
label: { Text("-")}
)
}.font(.headline)
}.padding()
}
func add() { values.count += 1 }
func sub() { values.count -= 1 }
}
struct InnerView : View {
#ObservedObject var values = sharedValues
var body: some View {
TextField("", value: $values.count, formatter: NumberFormatter())
.textFieldStyle(RoundedBorderTextFieldStyle())
}
}

Related

Not able to use the Toggle Switch with if else conditions in swift ui

I am not able to access use the switch. I want if the switch is on, it should come up with a text field and if it is off the value of the variable should be zero. Can anyone help me with this. I have tried to use two different methods. One by using .Onchange and one without using .Onchange. When I use .Onchange, it comes up with a waning that the result of text field is unused. And when I don't use .onAppear it doesn't accept (userSettings.load = 0) but the text field works fine then. I don't understand what I am doing wrong here.The variables are defined as :
struct TwoView: View {
#EnvironmentObject var userSettings: UserSettings
#State var load: Bool = false
var body: some View {
NavigationView {
VStack {
Form {
Toggle("Casual loading", isOn: $load)
.onChange(of: load) { value in
if load == false
{
userSettings.loadrate = 0
}
else
{
TextField("Casual Loading", value: $userSettings.loadrate, format: .number)
}
}
}
}
}
}
}
class UserSettings: ObservableObject
{
#Published var loadrate = Float()
}
Right now, you're using TextField outside of the view hierarchy, and just inside the onChange. In fact, as you mentioned, Xcode is giving you a warning about the fact that it is unused.
To solve this, you can use an if clause inside the hierarchy itself:
struct ContentView : View {
var body: some View {
TwoView().environmentObject(UserSettings())
}
}
struct TwoView: View {
#EnvironmentObject var userSettings: UserSettings
#State var load: Bool = false
var body: some View {
NavigationView {
VStack {
Form {
Toggle("Casual loading", isOn: $load)
.onChange(of: load) { value in
//only imperative, non-View hierarchy code should go in this block
if load == false
{
userSettings.loadrate = 0
}
}
if load { //<-- Here
TextField("Casual Loading", value: $userSettings.loadrate, format: .number)
}
}
}
}
}
}
class UserSettings: ObservableObject
{
#Published var loadrate = Float()
}
TextField is a view element and it shouldn't be inside a closure. It should be a child of a view.
Similarly, assignment is not a view element, so it is not accepted to be in a view.
So, what you need to do is put userSettings.loadrate = 0 into .onChange, and put TextField outside .onChange.
I'm not sure what exactly is your expected result, but here is an example.
NavigationView {
VStack {
Form {
Toggle("Casual loading", isOn: $load)
.onChange(of: load) { value in
// Assignment should be inside a closure
if load == false {
userSettings.loadrate = 0
}
}
if load == true {
// View element should be a child of a view.
TextField("Casual Loading", value: $userSettings.loadrate, format: .number)
}
}
}
}

Missing parameter but need parameter to be changed based on user input

This file is for the main structure of the application. This is where the error is coming from which is "Missing argument for parameter 'numberOfDoors' in call". This is because it wants me to add
ContentView(numberOfDoors: <#Int#>)
but im having trouble finding out how I can get what the user chooses to be the int instead of me putting a number in there statically.
import SwiftUI
#main
struct NumOfDoorsApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
This is my project file.
import SwiftUI
struct ContentView: View {
#State var numberOfDoors: Int
#State var multiOptions: Array<String>
init(numberOfDoors: Int) {
self.numberOfDoors = numberOfDoors
self.multiOptions = [String](repeating: "", count: numberOfDoors)
}
var body: some View {
NavigationView {
Form{
Section {
Picker("Number of doors", selection: $numberOfDoors) {
ForEach(1 ..< 64) {
Text("\($0) doors")
}
}
ForEach(multiOptions.indices, id: \.self) { index in
TextField("Enter your option...", text: $multiOptions[index])
.padding()
.textFieldStyle(RoundedBorderTextFieldStyle())
}
}
Section {
Text("\(numberOfDoors + 1)")
}
}
}
}
}
One of the most important parts of SwiftUI programming is creating an appropriate model for your views.
#State properties are OK for local, often independent properties, but they typically aren't a good solution for your main data model or where a model needs to be manipulated by the user.
In your case you want the size of the array to change based on the selected number of doors, so you need somewhere for that procedural code to live; The model is that place.
Here is a simple model object you can use
class DoorModel: ObservableObject {
#Published var numberOfDoors: Int {
didSet {
self.adjustArray(newSize: numberOfDoors)
}
}
#Published var doors:[String]
init(numberOfDoors: Int) {
self.numberOfDoors = numberOfDoors
self.doors = [String](repeating: "", count: numberOfDoors)
}
private func adjustArray(newSize: Int) {
let delta = newSize - doors.count
print("new size = \(newSize) Delta = \(delta)")
if delta > 0 {
doors.append(contentsOf:[String](repeating: "", count: delta))
} else if delta < 0 {
doors.removeLast(-delta)
}
}
}
Note that you still need to supply a starting number of doors via the initialiser for your model. Whenever that value changes, the didSet property observer calls a function to add or remove elements from the end of the array.
You can use this model in your view with an #StateObject decorator. This ensures that a single instance is created and reused as your view is redrawn
struct ContentView: View {
#StateObject var model = DoorModel(numberOfDoors: 1)
var body: some View {
NavigationView {
Form{
Section {
Picker("Number of doors", selection: $model.numberOfDoors) {
ForEach(1 ..< 64) { index in
Text("\(index) doors").tag(index)
}
}
ForEach($model.doors.indices, id: \.self) { index in
TextField("Enter your option...", text: $model.doors[index])
.padding()
.textFieldStyle(RoundedBorderTextFieldStyle())
}
}
}
}
}
}
I added the .tag modifier to ensure that the picker works correctly with your 1-based list; By default the tag will be 0 based.
There are, to my opinion, two issues with your code.
Don't initialize ContentView with a parameter. Especially, since you want to start with a value of 0 doors and let the user choose how many doors.
You can't initialize an #State value directly like: self.numberOfDoors = numberOfDoors.
I suggest the following code, it worked for me.
import SwiftUI
struct ContentView: View {
#State private var numberOfDoors: Int
#State private var multiOptions: Array<String>
init() {
_numberOfDoors = State(wrappedValue: 0)
_multiOptions = State(wrappedValue: [String](repeating: "", count: 1))
}
var body: some View {
NavigationView {
Form{
Section {
Picker("Number of doors", selection: $numberOfDoors) {
ForEach(1 ..< 64) {
Text("\($0) doors")
}
}
ForEach(multiOptions.indices, id: \.self) { index in
TextField("Enter your option...", text: $multiOptions[index])
.padding()
.textFieldStyle(RoundedBorderTextFieldStyle())
}
}
Section {
Text("\(numberOfDoors + 1)")
}
}
}
}
}
The issue is, that when you initialize ContentView, the #State or #StateObject variables don't yet exist. You first need to initialize the View, before you can assign an #State variable.
An #State variable is wrapped in a property wrapper, where the original variable at initialization is _variable. Therefore, you need to initialize an #State variable like this:
_myVariable = State(wrappedValue: myContent)
If you don't so it is like this, you get the error as you mentioned.

SwiftUI: Reset TextField value from CoreData on change in value of another state variable

I have a ContentView and a NumericView. The selectedIndex is a #State in ContentView. What I want is that if selectedIndex changes, it should reset the answer field in NumericView from CoreData. Below is the code. "av" contains the value of the answer field from CoreData.
struct NumericEntry: View {
#EnvironmentObject var questionAnswerStore: QuestionAnswerStore
#State var answer: String
var questionIndex: Int
init(questionIndex: Int, av: String) {
self.questionIndex = questionIndex
_answer = State(initialValue: av)
}
var body: some View {
VStack {
TextField("Answer", text: $answer)
.textFieldStyle(CustomTextFieldStyle())
.onChange(of: answer) { newValue in
self.questionAnswerStore.getUserAttemptData(selectedIndex: questionIndex).answer = newValue
PersistenceController.shared.saveContext()
}
.keyboardType(.decimalPad)
.padding()
}
}
private struct CustomTextFieldStyle : TextFieldStyle {
public func _body(configuration: TextField<Self._Label>) -> some View {
configuration
.padding(10)
.background(
RoundedRectangle(cornerRadius: 5)
.strokeBorder(Color.secondary.opacity(0.5), lineWidth: 1))
}
}
}
struct ContentView: View {
#State var selectedIndex: Int = 0
#ObservedObject private var questionAnswerStore: QuestionAnswerStore = QuestionAnswerStore.sharedInstance
var body: some View {
NumericEntry(questionIndex: selectedIndex, av: self.questionAnswerStore.getUserAttemptData(selectedIndex: selectedIndex).answer ?? "")
// some code that keeps changing the value of selectedIndex
}
}
I read somewhere that _stateVariable = State(initialValue: "abcd") should set the state of #State stateVariable. In the above code the code
_answer = State(initialValue: av)
executes fine but when it reaches
TextField("Answer", text: $answer)
$answer is still "".
I would prefer a solution where I don't even have to send "av" from the parent component, just selectedIndex should check QuestionAnswerStore for the value of "answer". This could be solved using .onAppear but in my case, the NumericView appears only once and then its parent just keeps changing selectedIndex value, so onAppear doesn't get called again.
Of course, if that is not possible then what's the way out using "av" as above?

How to update ParentView after updating SubView #ObservedObject SwiftUI

This is a simple example for my case.
I have a #ObservedObject viewModel (Object1), pass a property (Object2) to another view (View2) . Change value in View 2, and when i go back to View 1, i wish the value is updated too. What is the best solution?
In this Example, when i press the blue number, i wish black number is also updated.
Actually I don't know why do the black number is updated after pressing button "Show".
I would really appreciate if you could help me. Thanks.
import SwiftUI
import Combine
struct ContentView: View {
#ObservedObject var object1: Object1 = Object1(ob: Object2(n: 0))
#State var isShow = false
var body: some View {
NavigationView {
VStack {
Text("\(object1.object2.n)")
//NavigationLink(destination: View2(object2: object1.object2)) {
// Text("Go to view 2")
//}
View2(object2: object1.object2)
Button {
isShow = true
} label: {
Text("Show")
}.alert(isPresented: $isShow, content: {
Alert(title: Text("\(object1.object2.n)"))
})
}
}
}
}
struct View2: View {
#ObservedObject var object2: Object2
var body: some View {
Button {
object2.n += 1
} label: {
Text("\(object2.n)")
}
}
}
class Object1: ObservableObject {
#Published var object2: Object2
init(ob: Object2) {
self.object2 = ob
}
}
class Object2: ObservableObject {
#Published var n: Int = 0
init(n: Int) {
self.n = n
}
}
Here is possible solution:
var body: some View {
NavigationView {
VStack {
Text("\(object1.object2.n)")
.onChange(of: object1.object2.n) { _ in
object1.objectWillChange.send()
}
// .. other code
Alternate is to move every object2 dependent part into separated subview observed object2 explicitly.

SwiftUI View not updating based on #ObservedObject

In the following code, an observed object is updated but the View that observes it is not. Any idea why?
The code presents on the screen 10 numbers (0..<10) and a button. Whenever the button is pressed, it randomly picks one of the 10 numbers and flips its visibility (visible→hidden or vice versa).
The print statement shows that the button is updating the numbers, but the View does not update accordingly. I know that updating a value in an array does not change the array value itself, so I use a manual objectWillChange.send() call. I would have thought that should trigger the update, but the screen never changes.
Any idea? I'd be interested in a solution using NumberLine as a class, or as a struct, or using no NumberLine type at all and instead rather just using an array variable within the ContentView struct.
Here's the code:
import SwiftUI
struct ContentView: View {
#ObservedObject var numberLine = NumberLine()
var body: some View {
VStack {
HStack {
ForEach(0 ..< numberLine.visible.count) { number in
if self.numberLine.visible[number] {
Text(String(number)).font(.title).padding(5)
}
}
}.padding()
Button(action: {
let index = Int.random(in: 0 ..< self.numberLine.visible.count)
self.numberLine.objectWillChange.send()
self.numberLine.visible[index].toggle()
print("\(index) now \(self.numberLine.visible[index] ? "shown" : "hidden")")
}) {
Text("Change")
}.padding()
}
}
}
class NumberLine: ObservableObject {
var visible: [Bool] = Array(repeatElement(true, count: 10))
}
With #ObservedObject everything's fine... let's analyse...
Iteration 1:
Take your code without changes and add just the following line (shows as text current state of visible array)
VStack { // << right below this
Text("\(numberLine.visible.reduce(into: "") { $0 += $1 ? "Y" : "N"} )")
and run, and you see that Text is updated so observable object works
Iteration 2:
Remove self.numberLine.objectWillChange.send() and use instead default #Published pattern in view model
class NumberLinex: ObservableObject {
#Published var visible: [Bool] = Array(repeatElement(true, count: 10))
}
run and you see that update works the same as on 1st demo above.
*But... main numbers in ForEach still not updated... yes, because problem in ForEach - you used constructor with Range that generates constant view's group by-design (that documented!).
!! That is the reason - you need dynamic ForEach, but for that model needs to be changed.
Iteration 3 - Final:
Dynamic ForEach constructor requires that iterating data elements be identifiable, so we need struct as model and updated view model.
Here is final solution & demo (tested with Xcode 11.4 / iOS 13.4)
struct ContentView: View {
#ObservedObject var numberLine = NumberLine()
var body: some View {
VStack {
HStack {
ForEach(numberLine.visible, id: \.id) { number in
Group {
if number.visible {
Text(String(number.id)).font(.title).padding(5)
}
}
}
}.padding()
Button("Change") {
let index = Int.random(in: 0 ..< self.numberLine.visible.count)
self.numberLine.visible[index].visible.toggle()
}.padding()
}
}
}
class NumberLine: ObservableObject {
#Published var visible: [NumberItem] = (0..<10).map { NumberItem(id: $0) }
}
struct NumberItem {
let id: Int
var visible = true
}
I faced the same issue.
For me, replacing #ObservedObject with #StateObject worked.
Using your insight, #Asperi, that the problem is with the ForEach and not with the #ObservableObject functionality, here's a small modification to the original that does the trick:
import SwiftUI
struct ContentView: View {
#ObservedObject var numberLine = NumberLine()
var body: some View {
VStack {
HStack {
ForEach(Array(0..<10).filter {numberLine.visible[$0]}, id: \.self) { number in
Text(String(number)).font(.title).padding(5)
}
}.padding()
Button(action: {
let index = Int.random(in: 0 ..< self.numberLine.visible.count)
self.numberLine.visible[index].toggle()
}) {
Text("Change")
}.padding()
}
}
}
class NumberLine: ObservableObject {
#Published var visible: [Bool] = Array(repeatElement(true, count: 10))
}
There is nothing Wrong with observed object, you should use #Published in use of observed object, but my code works without it as well. And also I updated your logic in your code.
import SwiftUI
struct ContentView: View {
#ObservedObject var model = NumberLineModel()
#State private var lastIndex: Int?
var body: some View {
VStack(spacing: 30.0) {
HStack {
ForEach(0..<model.array.count) { number in
if model.array[number] {
Text(String(number)).padding(5)
}
}
}
.font(.title).statusBar(hidden: true)
Group {
if let unwrappedValue: Int = lastIndex { Text("Now the number " + unwrappedValue.description + " is hidden!") }
else { Text("All numbers are visible!") }
}
.foregroundColor(Color.red)
.font(Font.headline)
Button(action: {
if let unwrappedIndex: Int = lastIndex { model.array[unwrappedIndex] = true }
let newIndex: Int = Int.random(in: 0...9)
model.array[newIndex] = false
lastIndex = newIndex
}) { Text("shuffle") }
}
}
}
class NumberLineModel: ObservableObject {
var array: [Bool] = Array(repeatElement(true, count: 10))
}
The problem is with the function, do not forget to add id: \.self in your ForEach function, and make your Model Hashable, Identifiable.

Resources