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

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
.

Related

Swift & SwiftUI - Conditional global var

I want to make a global variable in Swift, so that its Data is accessible to any view that needs it. Eventually it will be a var so that I can mutate it, but while trying to get past this hurdle I'm just using it as let
I can do that by putting this as the top of a file (seemingly any file, Swift is weird):
let myData: [MyStruct] = load("myDataFile.json)
load() returns a JSONDecoder(). MyStruct is a :Hashable, Codable, Identifiable struct
That data is then available to any view that wants it, which is great. However, I want to be able to specify the file that is loaded based on a condition - I'm open to suggestions, but I've been using an #AppStorage variable to determine things when inside a View.
What I'd like to do, but can't, is do something like:
#AppStorage("appStorageVar") var appStorageVar: String = "Condition1"
if(appStorageVar == "Condition2") {
let myData: [MyStruct] = load("myDataFile2.json")
}
else {
let myData: [MyStruct] = load("myDataFile.json")
}
I can do this inside a View's body, but then it's only accessible to that View and then I have to repeat it constantly, which can't possibly the correct way to do it.
You could change just change the global in an onChange on the AppStorage variable. This is an answer to your question, but you have the problem that no view is going to be updating itself when the global changes.
var myData: [MyStruct] = load("myDataFile.json)
struct ContentView: View {
#AppStorage("appStorageVar") var appStorageVar: String = "Condition1"
var body: some View {
Button("Change value") {
appStorageVar = "Condition2"
}
.onChange(of: appStorageVar) { newValue in
myData = load(newValue == "Condition1" ? "myDataFile.json" : "myDataFile2.json")
}
}
}

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.

SwiftUI change on multilevel children Published object change

I have an ObservedObject AppStatus class that has multiple Published classes inside itself. If I have only level in terms of children, everything is working great.
The problem comes when I have a class RecordingTimeManager that has another variable inside (2-levels of children). The variable maxRecordingTime is changing properly when I press the button, it prints '15 fifteenSeconds' but the foregroundColor is not triggering the change. I am not sure if this is a SwiftUI bug or I should structure the relationships in another way:
// AppStatus
// Recording
#Published var recordingTimeManager: RecordingTimeManager = RecordingTimeManager()
// RecordingTimeManager
class RecordingTimeManager {
#Published var maxRecordingTime: TimeSeletedTime = .sixteenSeconds
...
// SwiftUI component that would need to change the opacity based on a maxRecordingTime change (.foregroundColor is not changing)
Button {
appStatus.recordingTimeManager.maxRecordingTime = .fifteenSeconds
print("15 \(appStatus.recordingTimeManager.maxRecordingTime)")
} label: {
Text("15")
.font(Font.custom("BwGradual-Bold", size: 15))
.foregroundColor(appStatus.recordingTimeManager.maxRecordingTime == .fifteenSeconds ? CLAPSOFFWHITE : TRIBESGREY)
}
Many Thanks,
Observable objects don't just work when you have multiple levels of classes. The #Published property wrapper notifies the view when the property has changed, but because this property is a class - a reference type - it doesn't actually change when you change one of its properties. In other words, the reference remains the same.
And the inner #Published doesn't do anything because there's nothing directly observing it (even if RecordingTimeManager conformed to an ObservableObject)
So, you either need to make RecordingTimeManager a value-type - a struct:
struct RecordingTimeManager {
var maxRecordingTime: TimeSeletedTime = .sixteenSeconds
}
Or, if it must be a class (perhaps because it has some internal state and life cycle), then you could create an inner view that directly observes it.
First, it needs to be an ObservableObject:
class RecordingTimeManager: ObservableObject {
#Published var maxRecordingTime: TimeSeletedTime = .sixteenSeconds
and then create a view (could be a private internal view) that observes it:
struct MainView: View {
#StateObject var appStatus: AppStatus = .init()
private struct InnerView: View {
#ObservedObject var recordingManager: RecordingTimeManager
var body: some View {
Button {
recordingManager.maxRecordingTime = .fifteenSeconds
} label: {
Text("15")
.font(Font.custom("BwGradual-Bold", size: 15))
.foregroundColor(
recordingManager.maxRecordingTime == .fifteenSeconds
? CLAPSOFFWHITE : TRIBESGREY)
}
}
}
var body: some View {
InnerView(recordingManager: appStatus.recordingTimeManager)
}
}

Passing a nested array element result into a #Binding

My title is probably terrible. This is one of those too hard to title questions.
extension Color {
static let availableForSelectionColors : [Color] = [.red,.blue,.green,.yellow]
}
I have a view
struct myView: View {
#Binding var bgColor : Color
var body: some View {
VStack {
...
}.background(bgColor)
}
}
That I create instances and set the background color of in a ForEach Loop.
struct myOtherView : View {
#State private var storedColors : [Int] = UserDefaults.standard.value(forKey:"key") as? [Int] ?? [0,1,3,1]
#State private var bgColor = Color.red
ForEach((0..<100), id: \.self) { index in
myView(bgColor: $bgColor)
}
}
However I want to set the color using availableForSelectionColors based on the storedColors index in the ForEach loop. I.e
...
ForEach((0..<100), id: \.self) { index in
myView(bgColor: Color.availableForSelectionColors[storedColors[index %4]])
}
}
but can't because Color.availableForSelectionColors[storedColors[index %4]] is not a #Binding
How then to make Color.availableForSelectionColors[storedColors[index %4]] #Binding so I can pass in to create myView. I have used Binding as user can change the array in UserDefaults.standard.value(forKey:"key") in a modal sheet which should be reflected back in myView
You can create a Binding yourself based on each index. It would look something like this (if I correctly understood what you're trying to do):
func colorBinding(for index: Int) -> Binding<Color> {
.init(
get: { Color.availableForSelectionColors[storedColors[index]] },
set: { storedColors[index] = Color.availableForSelectionColors
.firstIndex(of: $0)!}
)
}
This function returns a Binding<Color>, and the binding is bound to the specified element in the storedColors array, except it converts between the Color and the Int index in the Color.availableForSelectionColors array.
So, you can use the return value of the function directly as a parameter to myView:
ForEach((0..<100), id: \.self) { index in
myView(bgColor: colorBinding(for: index % 4))
}
! is used as a simplification, as I assume that the only colors that could be used are those in the Color.availableForSelectionColors array; obviously, it would crush if a different color is used.

Learning to data between parent and child views in swiftui

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