Learning to data between parent and child views in swiftui - swift

I have my parent view where I'm setting my States and I have two different views I'm trying to pass data between. When subview_1 calls subview_2 I'm not getting the binding data, the compiler fails with the error, The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions.
My parent view I've set the states
#State var businessPhone = ""
#State var businessRating = 0.0
In subview_1 I have my bindings set and the data works fine.
struct subview_1: View {
#Binding var businessRating: Double
#Binding var businessPhone: String
var body: some View {
....
}
}
When I call subview_2 from subview_1 the data isn't getting passed as expected.
struct subview_1: View {
#Binding var businessRating: Double
#Binding var businessPhone: String
var body: some View {
subview_2(businessRating: $businessRating, businessPhone: $businessPhone)
}
}
If I use .constant() with data that works as expected.
For subview_2 I have the following bindings set
#Binding var businessRating: Double
#Binding var businessPhone: String

Related

How do i fix error: Type '()' cannot conform to 'View' and error : Type'Void' cannot conform to 'View'

struct QuePlayers: View {
#Binding var quedPlayerCount:Int
#Binding var newPlayerCount:Int
let QuedPlayersRef:DatabaseReference! = Database.database().reference().child("QuedPlayers")
let playerCountRef:DatabaseReference! = Database.database().reference().child("QuedPlayerCount")
#EnvironmentObject var viewModel: AuthViewModel
var body: some View {
var myUid = self.viewModel.userSession!.uid
newPlayerCount = quedPlayerCount + 1
playerCountRef.setValue(newPlayerCount)
print("newPlayerCount = \(newPlayerCount)")
//myUid = self.viewModel.userSession!.uid
QuedPlayersRef.child("\(quedPlayerCount + 1)").setValue(myUid)
}
}
I would like to make these modifications to variables and set database values. The code from line 64 to line 72 (refer to attached photo) ran perfectly when placed in a button but i need to be able to run those lines automatically
Let me know if you need any more information
Stated above^^^^^^^^

SwiftUI View not updating with binding to computed property on ObservableObject

I have a very simple SwiftUI view that only shows a TextField. The text field's text is bound to the string property of my viewModel that I instantiate as a #StateObject:
struct ContentView: View {
#StateObject private var viewModel = ViewModel()
var body: some View {
TextField("Placeholder", text: $viewModel.string)
.padding()
}
}
The one thing that's "out of the ordinary" is that my string property is not a #Published property, but a simple computed property. In its setter, it sets the displayedString property to a fixed string (for testing purposes) which is a #Published property:
class ViewModel: ObservableObject {
#Published var displayedString: String = ""
var string: String {
get { displayedString }
set { displayedString = "OVERRIDE" }
}
}
So when I type a string into the text field, I would expect the setter to trigger a view update (as it updates a #Published property) and then in the view, the text field should be updated with the displayedString. So in other words: No matter what I type, the text field should always show the string OVERRIDE.
But that's not the case!
It works for the very first letter I type, but then I can freely type anything into the text field.
For example, if I launch the app with only this view and type the string
123456789
this is what is displayed on the text field:
OVERRIDE23456789
Why is that?
And how can I get the text field to always be up-to-date to what's set on the viewModel? (I know, I can just hook the text field to a #Published property directly, but I'm doing this computed property stuff for a reason and want to understand why it doesn't work as expected.)
Additional Observation:
When I add a Text label above the TextField and just make it show the viewModel.string, it shows the correct (expected) string:
var body: some View {
Text(viewModel.string)
TextField("Placeholder", text: $viewModel.string)
.padding()
}
I’m not sure but I think a custom binding might work for you
In the ViewModel use
var string: Binding<String> { Binding(
get: { displayedString },
set: { displayedString = “OVERRIDE” } )}
Then in body use: viewModel.string instead of $viewModel.string in the TextField and use: viewModel.string.wrappedValue in the text view
Stuf

Cannot convert value of type 'Published<[StepsEntity]>.Publisher' to expected argument type 'Binding<String>'

StepsEntity is a core data entity
Receiving the following error when attempting to display a string value in a TextField: "Cannot convert value of type 'Published<[StepsEntity]>.Publisher' to expected argument type 'Binding'"
I know this is because StepsEntity in my core data model is #Published. #Published works great here as it allows all the data to be updated neatly. How can I display an #Published in a TextField?
Below is the piece where I am receiving the error:
List {
VStack(alignment: .center) {
if let recipeSteps = (vm.recipes[vm.getRecordsCount() - 1].steps?.allObjects as? [StepsEntity])?.sorted { $0.stepNumber < $1.stepNumber } {
if (textFieldCount == 1) {
//do nothing
} else if (textFieldCount > 1) {
ForEach(recipeSteps, id: \.stepNumber) { index in
HStack {
Text(String(index.stepNumber) + ".").bold()
TextField("", text: vm.$recipeSteps) //This is where the error is seen
}
}.onDelete(perform: { index in
self.vm.deleteRecipeSteps(at: index, from: vm.recipes[vm.getRecordsCount() - 1])
})
}
}
}
vm.recipeSteps refers to my CoreDataRelationshipViewModel, which is where all core data functions are handled. this is declared in the view struct as:
#StateObject var vm = CoreDataRelationshipViewModel()
Here is a snippet from the CoreDataRelationshipViewModel class:
class CoreDataRelationshipViewModel: ObservableObject {
let manager = CoreDataManager.instance
#Published var recipes: [RecipeEntity] = []
#Published var recipeSteps: [StepsEntity] = []
init() {
getRecipes()
}
func getRecipes() { ////more functions for retrieving, deleting, etc. in this section
I have tried converting the Published var to a binding but no luck:
TextField("", text: Binding(vm.$recipeSteps)!)
I have also tried referencing the recipeSteps declared in the if let statement within the list, but that does not work either.
I have been at it for a few days, and I think I have exhausted all options. Open to all ideas here. Maybe I need to rebuild my model?
Thoughts?
--Edits--
Upper portion of struct, where variables are created:
struct AddItemView: View {
#StateObject var viewModel = ViewModel()
#State var frameDimensions: CGFloat = 0
#State var imageButtonText: String = "Click To Add Image"
#State var imageToUpload: Data
#StateObject var vm = CoreDataRelationshipViewModel()
#Environment(\.presentationMode) var presentationMode
#State var stepInfo: String = ""
#State var textFieldCount: Int = 1
#State var stepNumber: [Int]
#State var recordsCount = 1
#State var errorText = ""
#State var stepErrorColor = Color.white.opacity(0)
#State var nameErrorColor = Color.white.opacity(0)
var body: some View {
ZStack {
HStack {
Your problem is that
TextField("", text: vm.$recipeSteps)
should actually be
TextField("", text: $vm.recipeSteps)
as you need to pass the view model with Binding rather than recipeSteps (which, when using $ to pass it beyond vm's dot accessor, passes a generic Publisher).
Also, this would only work if the #Published property in your view model conforms to StringProtocol (is a string). Are you sure recipeSteps is a string that can be edited via TextField?
I ended up restructuring the way my core data save works. Instead of saving item by item, I am now looping through a for in loop to save data all at the end. This allows me to display the data locally via some state variables, but then save the full array to my core data entity.

How to update global variable with TextField?

I have some variables that I need in several views and I want the user to be able to change these. These variables edit a link, so depending on for example what the longitude is the result is different. And the values I get from the link are displayed in a graph.
So I want the user to be able to enter their own value for the given variables(in the code below). I don't care if it is in the same view, or if consists of two views and the variables are passed to the view with the graphs.
The only important thing, is that the graph and the link refreshes when the user clicks a button, so that the right graph is displayed.
Another important thing, is that I need to be able to access the variables at top-level. This is my code until now, but it shows the error:
Cannot convert value of type 'string' to expected argument type 'Binding'
import SwiftUI
var month = "1"
var day = "1"
var year = "2020"
var latitude = "59.911232"
var longitude = "10.757933"
var offset = "1.0"
struct ContentView: View {
var body: some View {
GraphView()
TextField(text: latitude)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
TextField requires binding to some dynamic property source of truth. If you want to keep those values globally then wrap them into some shared instance of view model, like
final class Params: ObservableObject {
static let global = Params()
var month = "1"
var day = "1"
var year = "2020"
var latitude = "59.911232"
var longitude = "10.757933"
var offset = "1.0"
}
so now we are able to observe (and update, and bind to) those parameters from view
struct ContentView: View {
#ObservedObject var global = Params.global
var body: some View {
GraphView()
TextField(text: $global.latitude)
}
}

SwiftUI: How to implement a custom init with #Binding variables

I am working on a money input screen and I need to implement a custom init to set a state variable based on the initialized amount.
I thought the following would work:
struct AmountView : View {
#Binding var amount: Double
#State var includeDecimal = false
init(amount: Binding<Double>) {
self.amount = amount
self.includeDecimal = round(amount)-amount > 0
}
}
However, this gives me a compiler error as follows:
Cannot assign value of type 'Binding' to type 'Double'
How do I implement a custom init method which takes in a Binding struct?
Argh! You were so close. This is how you do it. You missed a dollar sign (beta 3) or underscore (beta 4), and either self in front of your amount property, or .value after the amount parameter. All these options work:
You'll see that I removed the #State in includeDecimal, check the explanation at the end.
This is using the property (put self in front of it):
struct AmountView : View {
#Binding var amount: Double
private var includeDecimal = false
init(amount: Binding<Double>) {
// self.$amount = amount // beta 3
self._amount = amount // beta 4
self.includeDecimal = round(self.amount)-self.amount > 0
}
}
or using .value after (but without self, because you are using the passed parameter, not the struct's property):
struct AmountView : View {
#Binding var amount: Double
private var includeDecimal = false
init(amount: Binding<Double>) {
// self.$amount = amount // beta 3
self._amount = amount // beta 4
self.includeDecimal = round(amount.value)-amount.value > 0
}
}
This is the same, but we use different names for the parameter (withAmount) and the property (amount), so you clearly see when you are using each.
struct AmountView : View {
#Binding var amount: Double
private var includeDecimal = false
init(withAmount: Binding<Double>) {
// self.$amount = withAmount // beta 3
self._amount = withAmount // beta 4
self.includeDecimal = round(self.amount)-self.amount > 0
}
}
struct AmountView : View {
#Binding var amount: Double
private var includeDecimal = false
init(withAmount: Binding<Double>) {
// self.$amount = withAmount // beta 3
self._amount = withAmount // beta 4
self.includeDecimal = round(withAmount.value)-withAmount.value > 0
}
}
Note that .value is not necessary with the property, thanks to the property wrapper (#Binding), which creates the accessors that makes the .value unnecessary. However, with the parameter, there is not such thing and you have to do it explicitly. If you would like to learn more about property wrappers, check the WWDC session 415 - Modern Swift API Design and jump to 23:12.
As you discovered, modifying the #State variable from the initilizer will throw the following error: Thread 1: Fatal error: Accessing State outside View.body. To avoid it, you should either remove the #State. Which makes sense because includeDecimal is not a source of truth. Its value is derived from amount. By removing #State, however, includeDecimal will not update if amount changes. To achieve that, the best option, is to define your includeDecimal as a computed property, so that its value is derived from the source of truth (amount). This way, whenever the amount changes, your includeDecimal does too. If your view depends on includeDecimal, it should update when it changes:
struct AmountView : View {
#Binding var amount: Double
private var includeDecimal: Bool {
return round(amount)-amount > 0
}
init(withAmount: Binding<Double>) {
self.$amount = withAmount
}
var body: some View { ... }
}
As indicated by rob mayoff, you can also use $$varName (beta 3), or _varName (beta4) to initialise a State variable:
// Beta 3:
$$includeDecimal = State(initialValue: (round(amount.value) - amount.value) != 0)
// Beta 4:
_includeDecimal = State(initialValue: (round(amount.value) - amount.value) != 0)
You should use underscore to access the synthesized storage for the property wrapper itself.
In your case:
init(amount: Binding<Double>) {
_amount = amount
includeDecimal = round(amount)-amount > 0
}
Here is the quote from Apple document:
The compiler synthesizes storage for the instance of the wrapper type by prefixing the name of the wrapped property with an underscore (_)—for example, the wrapper for someProperty is stored as _someProperty. The synthesized storage for the wrapper has an access control level of private.
Link: https://docs.swift.org/swift-book/ReferenceManual/Attributes.html -> propertyWrapper section
You said (in a comment) “I need to be able to change includeDecimal”. What does it mean to change includeDecimal? You apparently want to initialize it based on whether amount (at initialization time) is an integer. Okay. So what happens if includeDecimal is false and then later you change it to true? Are you going to somehow force amount to then be non-integer?
Anyway, you can't modify includeDecimal in init. But you can initialize it in init, like this:
struct ContentView : View {
#Binding var amount: Double
init(amount: Binding<Double>) {
$amount = amount
$$includeDecimal = State(initialValue: (round(amount.value) - amount.value) != 0)
}
#State private var includeDecimal: Bool
(Note that at some point the $$includeDecimal syntax will be changed to _includeDecimal.)
Since it's mid of 2020, let's recap:
As to #Binding amount
_amount is only recommended to be used during initialization. And never assign like this way self.$amount = xxx during initialization
amount.wrappedValue and amount.projectedValue are not frequently used, but you can see cases like
#Environment(\.presentationMode) var presentationMode
self.presentationMode.wrappedValue.dismiss()
A common use case of #binding is:
#Binding var showFavorited: Bool
Toggle(isOn: $showFavorited) {
Text("Change filter")
}
State:
To manages the storage of any property you declare as a state. When the state value changes, the view invalidates its appearance and recomputes the body and You should only access a state property from inside the view’s body, or from methods called.
Note: To pass a state property to another view in the view hierarchy, use the variable name with the $ prefix operator.
struct ContentView: View {
#State private var isSmile : Bool = false
var body: some View {
VStack{
Text(isSmile ? "😄" : "😭").font(.custom("Arial", size: 120))
Toggle(isOn: $isSmile, label: {
Text("State")
}).fixedSize()
}
}
}
Binding:
The parent view declares a property to hold the isSmile state, using the State property wrapper to indicate that this property is the value’s source of deferent view.
struct ContentView: View {
#State private var isSmile : Bool = false
var body: some View {
VStack{
Text(isSmile ? "😄" : "😭").font(.custom("Arial", size: 120))
SwitchView(isSmile: $isSmile)
}
}
}
Use a binding to create a two-way connection between a property that stores data, and a view that displays and changes the data.
struct SwitchView: View {
#Binding var isSmile : Bool
var body: some View {
VStack{
Toggle(isOn: $isSmile, label: {
Text("Binding")
}).fixedSize()
}
}
}
The accepted answer is one way but there is another way too
struct AmountView : View {
var amount: Binding<Double>
init(withAmount: Binding<Double>) {
self.amount = withAmount
}
var body: some View { ... }
}
You remove the #Binding and make it a var of type Binding
The tricky part is while updating this var. You need to update it's property called wrapped value. eg
amount.wrappedValue = 1.5 // or
amount.wrappedValue.toggle()
You can achieve this either with static function or with custom init.
import SwiftUI
import PlaygroundSupport
struct AmountView: View {
#Binding var amount: Double
#State var includeDecimal: Bool
var body: some View {
Text("The amount is \(amount). \n Decimals \(includeDecimal ? "included" : "excluded")")
}
}
extension AmountView {
static func create(amount: Binding<Double>) -> Self {
AmountView(amount: amount, includeDecimal: round(amount.wrappedValue) - amount.wrappedValue > 0)
}
init(amount: Binding<Double>) {
_amount = amount
includeDecimal = round(amount.wrappedValue) - amount.wrappedValue > 0
}
}
struct ContentView: View {
#State var amount1 = 5.2
#State var amount2 = 5.6
var body: some View {
AmountView.create(amount: $amount1)
AmountView(amount: $amount2)
}
}
PlaygroundPage.current.setLiveView(ContentView())
Actually you don't need custom init here at all since the logic could be easily moved to .onAppear unless you need to explicitly set initial state externally.
struct AmountView: View {
#Binding var amount: Double
#State private var includeDecimal = true
var body: some View {
Text("The amount is \(amount, specifier: includeDecimal ? "%.3f" : "%.0f")")
Toggle("Include decimal", isOn: $includeDecimal)
.onAppear {
includeDecimal = round(amount) - amount > 0
}
}
}
This way you keep your #State private and initialized internally as documentation suggests.
Don’t initialize a state property of a view at the point in the view
hierarchy where you instantiate the view, because this can conflict
with the storage management that SwiftUI provides. To avoid this,
always declare state as private, and place it in the highest view in
the view hierarchy that needs access to the value
.