Using State Variables as Inputs to a Func in Swiftui

Using state variables as inputs to a func in SwiftUI

The problem is that the assignment to milesPerGallon does not work well with the “function builder syntax” used to build the arguments of the view hierarchy. It becomes a bit more obvious if we replace @State var milesPerGallon by a local variable (which is is, it does not carry state on which the view depends, only a intermediate value):

if showMPGInfo {
Spacer()
let milesPerGallon = CalcMPG(start: startingMileage,
end: endingMileage, fuel: fuelAdded)
Text("Fuel effiency: \(milesPerGallon) MPG")
.font(.largeTitle)
}

Now the compiler error is


Closure containing a declaration cannot be used with function builder 'ViewBuilder'

For more information about the function builder syntax see What enables SwiftUI's DSL? (which has also links to the documentation).

The simplest solution would be to avoid a local variable and interpolate the text directly:

if showMPGInfo {
Spacer()
Text("Fuel effiency: \(CalcMPG(start: startingMileage, end: endingMileage, fuel: fuelAdded)) MPG")
.font(.largeTitle)
}

Other solutions are to calculate the text field in an “immediately evaluated closure”:

if showMPGInfo {
Spacer();
{ () -> Text in
let milesPerGallon = CalcMPG(start: startingMileage,
end: endingMileage,
fuel: fuelAdded)
return Text("Fuel effiency: \(milesPerGallon) MPG")
}()
.font(.largeTitle)
}

or to define an auxiliary function

func resultField(start: String, end: String, fuel: Double) -> Text {
let milesPerGallon = CalcMPG(start: startingMileage, end: endingMileage, fuel: fuelAdded)
return Text("Fuel effiency: \(milesPerGallon) MPG")
}

and use it as

if showMPGInfo {
Spacer()
resultField(start: startingMileage, end: endingMileage,
fuel: fuelAdded)
.font(.largeTitle)
}

There may be other workarounds, but that is what I came up with so far.

How Do You Pass A View Input Argument To @State Variable?

property initializers run before 'self' is available

By calling

FetchPrice(symbolName: symbolNameV)

you're accessing self. The code above is actually:

FetchPrice(symbolName: self.symbolNameV)

To solve this you can create a custom init:

struct SymbolRow2: View {
private var symbolNameV: String
@ObservedObject private var fetchPrice: FetchPrice

init(symbolNameV: String) {
self.symbolNameV = symbolNameV
self.fetchPrice = FetchPrice(symbolName: symbolNameV)
}

...
}

@State var not updating SwiftUI View

As other fellows suggested, you have to be very careful about using global variables, you should expose them only to the scope needed.

The problem is that you are treating @State var i = myStng thinking this would create a reactive connection between i and myStng, but that is not true. This line is creating a reactive connection between i and a memory address that SwiftUI manages for you, with its first value being what myStng is at that exact moment.

Anyway, I am posting an example of how can you achieve your goal using Environment Object with your provided code.

import SwiftUI

class GlobalVariables: ObservableObject{
@Published var myStng = "Hello\n"
@Published var myArray = ["Hello\n"]
}

func myTest(variables: GlobalVariables){
variables.myStng.append("Hello\n")
variables.myArray.append("Hello\n")
}

@main
struct myTestxApp: App {
@StateObject var globalEnvironment = GlobalVariables()
var body: some Scene {
WindowGroup {
ContentView().environmentObject(globalEnvironment)
}
}
}

//
// ContentView.swift
// myTestx
//import SwiftUI
struct ContentView: View {
@EnvironmentObject var global: GlobalVariables

var body: some View {
VStack{
Button {
myTest(variables: global)
} label: {
Text("Update")
}

List{ Text(global.myStng).padding() }
List{ ForEach(global.myArray, id: \.self)
{ i in Text(i).padding()} }
} //end VStack
} //end View
} //end ContentView

State Variable From A Textfield With Input Is Returning Null

Here's a very simplified version of your code showing state owned by the parent. Validity just tests whether the text is empty or not (just for demo purposes).

func checkErrors(input: String) -> Bool {
return !input.isEmpty
}

struct Signup: View
{
@State private var text : String = ""
@State private var isValid : Bool = false

var body: some View
{

EnhancedTextField(hint: "Hint", text: $text)
.border(isValid ? .clear : .red)

Button(action: {
isValid = checkErrors(input: text)
}) {
Text("Test")
}
}
}

struct EnhancedTextField: View
{
@State var hint: String
@Binding var text: String
@FocusState private var isFocused: Bool
@State private var hasErrors = false

var body: some View
{

VStack (alignment: .center)
{

//First Name Input
TextField(hint, text: $text)
.frame(maxWidth: .infinity, alignment: .center)
.focused($isFocused)
.textInputAutocapitalization(.never)
.disableAutocorrection(true)
.onChange(of:isFocused)
{ newValue in
if !newValue
{
//Check Validation Errors
hasErrors = checkErrors(input: text)
}
}
}
}
}

Is there a way to pass a variable into a class like you do a function in SwiftUI?

You need to add explicit constructor with default path

class ObjectManager: ObservableObject {

@Published var current: TYPE_OF_CURRENT_HERE

init(path: String = "path") {
self.current = Parser(path: path)[0]
}
}

Passing a state variable to parent view

In general, your state should always be owned higher up the view hierarchy. Trying to access the child state from a parent is an anti-pattern.

One option is to use @Bindings to pass the values down to child views:

struct BookView: View {

@Binding var title : String
@Binding var author : String

var body: some View {
TextField("Title", text: $title)
TextField("Author", text: $author)
}
}

struct ContentView: View {
@State private var presentNewBook: Bool = false

@State private var title = ""
@State private var author = ""

var body: some View {
NavigationView {
VStack {
Text("Title: \(title)")
Text("Author: \(author)")
Button("Open") {
presentNewBook = true
}
}
}.sheet(isPresented: $presentNewBook) {
BookView(title: $title, author: $author)
}
}
}

Another possibility is using an ObservableObject:

class BookState : ObservableObject {
@Published var title = ""
@Published var author = ""
}

struct BookView: View {

@ObservedObject var bookState : BookState

var body: some View {
TextField("Title", text: $bookState.title)
TextField("Author", text: $bookState.author)
}
}

struct ContentView: View {
@State private var presentNewBook: Bool = false
@StateObject private var bookState = BookState()

var body: some View {
NavigationView {
VStack {
Text("Title: \(bookState.title)")
Text("Author: \(bookState.author)")
Button("Open") {
presentNewBook = true
}
}
}.sheet(isPresented: $presentNewBook) {
BookView(bookState: bookState)
}
}
}

I've altered your example views a bit because to me the structure was unclear, but the concept of owning the state at the parent level is the important element.

SwiftUI: Change @State variable through a function called externally?

Yes, you are thinking of it slightly wrong. @State is typically for internal state changes. Have a button that your View directly references? Use @State. @Binding should be used when you don't (or shouldn't, at least) own the state. Typically, I use this when I have a parent view who should be influencing or be influenced by a subview.

But what you are likely looking for, is @ObservedObject. This allows an external object to publish changes and your View subscribes to those changes. So if you have some midi listening object, make it an ObservableObject.

final class MidiListener: ObservableObject, AKMIDIListener {
// 66 key keyboard, for example
@Published var pressedKeys: [Bool] = Array(repeating: false, count: 66)

init() {
// set up whatever private storage/delegation you need here
}

func receivedMIDINoteOn(noteNumber: MIDINoteNumber, velocity: MIDIVelocity, channel: MIDIChannel, portID: MIDIUniqueID? = nil, offset: MIDITimeStamp = 0) {
// how you determine which key(s) should be pressed is up to you. if this is monophonic the following will suffice while if it's poly, you'll need to do more work
DispatchQueue.main.async {
self.pressedKeys[Int(noteNumber)] = true
}
}
}

Now in your view:

struct KeyboardView: View {
@ObservedObject private var viewModel = MidiListener()

var body: some View {
HStack {
ForEach(0..<viewModel.pressedKeys.count) { index in
Rectangle().fill(viewModel.pressedKeys[index] ? Color.red : Color.white)
}
}
}
}

But what would be even better is to wrap your listening in a custom Combine.Publisher that posts these events. I will leave that as a separate question, how to do that.



Related Topics



Leave a reply



Submit