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 @Binding
s 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
How to Make Embedded View Controller Part of the Responder Chain
Swiftui Widget iOS 14 Gradient Issue
Swift Protocol for String Interpolation
Icloud Drive Issue: "[Documentmanager] Failed to Associate Thumbnails for Picked Url"
Swiftui 2 Pop to Root View with No Scene Delegate
How to Override Private Method and Call Super in Swift
Can You Evaluate a String in Swift
Programmatically Create an Nsviewcontroller Without an Xib in Swift 3
How to Make Swiftui Uiviewrepresentable View Hug Its Content
Why Do I Get "Static Member '...' Cannot Be Used on Instance of Type '...'" Error
Alamofire 5 Upload Encodingcompletion
How to Add Custom Init for String Extension
Need Detailed Explanation for Memoize Implementation in Swift (Wwdc 14, Session 404)
Having Trouble with Nstimer (Swift)
How to I Turn Off the Ambient Light in Scene Kit (With Swift)
Swiftui - Scale, Zoom and Crop Image