Passing a nested array element result into a #Binding - swift

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.

Related

How to perform `ForEach` on a computed property in SwiftUI

Intro
Imagine we have an infinite range like the range of all possible integers and we can NOT just store them in memory. So we need to calculate them chunk by chunk like:
func numbers(around number: Int, distance: Int) -> [Int] {
((number - distance)...(number + distance))
.map { $0 } // Using `map` ONLY for making the question clear that we do NOT want to use a range
}
so now we can build our view like:
struct ContentView: View {
#State var lastVisibleNumber: Int = 0
var body: some View {
ScrollView(.horizontal) {
LazyHStack {
ForEach(numbers(around: lastVisibleNumber, distance: 10), id:\.self) { number in
Text("\(number)")
.padding()
.foregroundColor(.white)
.background(Circle())
// .onAppear { lastVisibleNumber = number }
}
}
}
}
}
Describing the issue and what tried
In order to load more numbers when needed, I have tried to update the lastVisibleNumber on the appearance of last visible Number by adding the following modifier on the Text:
.onAppear { lastVisibleNumber = number }
Although there is a safe range that is visible and should prevent the infinite loop (theoretically), adding this modifier will freeze the view for calculating all numbers!
So how can we achieve a scroll view with some kind of infinite data source?
Considerations
The range of Int is just a sample and the question can be about an infinite range of anything. (In a form of an Array!)
we don't want unexpected stops (like showing a dummy spinner at the leading or trailing of the list) in the middle of the scrolling.
For this, you should try using table view and data source. Let's assume you have an array of integers. You may create a buffer with an arbitrary number of instances. Let say 100. In that case, with a similar logic, your distance would become a 50. When you scroll, and close enough to the limits, you create another array and reload the table view data source so that you can pretend like you have an infinite array. Be aware that reusable cell and table view implementation is very consistent and optimized.
Don’t refresh view when lastVisibleNumber changes - it just gets into endless loop. You can create Observableobject, store it and update it only on-demand (I provided a button but you can obviously load it onAppear eith some criteria - e.g. last of the array was shown):
class NumberViewModel: ObservableObject {
var lastVisibleNumber = 0
#Published var lastRefreshedNumber = 0
func numbers(distance: Int) -> [Int] {
((lastRefreshedNumber - distance)...(lastRefreshedNumber + distance))
.map { $0 } // Using `map` ONLY for making the question clear that we do NOT want to use a range
}
}
struct ContentView: View {
#StateObject var viewModel = NumberViewModel()
var body: some View {
VStack {
Button("Refresh view", action: {
viewModel.lastRefreshedNumber = viewModel.lastVisibleNumber
})
ScrollView(.horizontal) {
LazyHStack {
ForEach(viewModel.numbers(distance: 10), id:\.self) { number in
Text("\(number)")
.padding()
.foregroundColor(.white)
.background(Circle())
.onAppear { viewModel.lastVisibleNumber = number }
}
}
}
}
}
}

ForEach not properly updating with dynamic content SwiftUI

Sorry to make this post so long, but in hindsight I should have shown you the simpler instance of the issue so you could better understand what the problem is. I am assuming the same issue with ForEach is at the root cause of both of these bugs, but I could be wrong. The second instance is still included to give you context, but the first intance should be all you need to fully understand the issue.
First Instance:
Here is a video of the issue: https://imgur.com/a/EIg9TSm. As you can see, there are 4 Time Codes, 2 of which are favorite and 2 are not favorites (shown by the yellow star). Additionally, there is text at the top that represents the array of Time Codes being displayed just as a list of favorite (F) or not favorite (N). I click on the last Time Code (Changing to favorite) and press the toggle to unfavorite it. When I hit save, the array of Time Codes is updated, yet as you see, this is not represented in the List. However, you see that the Text of the reduced array immediately updates to FNFF, showing that it is properly updated as a favorite by the ObservedObject.
When I click back on the navigation and back to the page, the UI is properly updated and there are 3 yellow stars. This makes me assume that the problem is with ForEach, as the Text() shows the array is updated but the ForEach does not. Presumably, clicking out of the page reloads the ForEach, which is why it updates after exiting the page. EditCodeView() handles the saving of the TimeCodeVieModel in CoreData, and I am 99% certain that it works properly through my own testing and the fact that the ObservedObject updates as expected. I am pretty sure I am using the dynamic version of ForEach (since TimeCodeViewModel is Identifiable), so I don't know how to make the behavior update immediately after saving. Any help would be appreciated.
Here is the code for the view:
struct ListTimeCodeView: View {
#ObservedObject var timeCodeListVM: TimeCodeListViewModel
#State var presentEditTimeCode: Bool = false
#State var timeCodeEdit: TimeCodeViewModel?
init() {
self.timeCodeListVM = TimeCodeListViewModel()
}
var body: some View {
VStack {
HStack {
Text("TimeCodes Reduced by Favorite:")
Text("\(self.timeCodeListVM.timeCodes.reduce(into: "") {$0 += $1.isFavorite ? "F" : "N"})")
}
List {
ForEach(self.timeCodeListVM.timeCodes) { timeCode in
TimeCodeDetailsCell(fullName: timeCode.fullName, abbreviation: timeCode.abbreviation, color: timeCode.color, isFavorite: timeCode.isFavorite, presentEditTimeCode: $presentEditTimeCode)
.contentShape(Rectangle())
.onTapGesture {
timeCodeEdit = timeCode
}
.sheet(item: $timeCodeEdit, onDismiss: didDismiss) { detail in
EditCodeView(timeCodeEdit: detail)
}
}
}
}
}
}
Here is the code for the View Models (shouldn't be relevant to the problem, but included for understanding):
class TimeCodeListViewModel: ObservableObject {
#Published var timeCodes = [TimeCodeViewModel]()
init() {
fetchAllTimeCodes()
}
func fetchAllTimeCodes() {
self.timeCodes = CoreDataManager.shared.getAllTimeCodes().map(TimeCodeViewModel.init)
}
}
class TimeCodeViewModel: Identifiable {
var id: String = ""
var fullName = ""
var abbreviation = ""
var color = ""
var isFavorite = false
var tags = ""
init(timeCode: TimeCode) {
self.id = timeCode.id!.uuidString
self.fullName = timeCode.fullName!
self.abbreviation = timeCode.abbreviation!
self.color = timeCode.color!
self.isFavorite = timeCode.isFavorite
self.tags = timeCode.tags!
}
}
Second Instance:
EDIT: I realize it may be difficult to understand what the code is doing, so I have included a gif demoing the problem (unfortunately I am not high enough reputation for it to be shown automatically). As you can see, I select the cells I want to change, then press the button to assign that TimeCode to it. The array of TimeCodeCellViewModels changes in the background, but you don't actually see that change until I press the home button and then reopen the app, which triggers a refresh of ForEach. Gif of issue. There is also this video if the GIF is too fast: https://imgur.com/a/Y5xtLJ3
I am trying to display a grid view using a VStack of HStacks, and am running into an issue where the ForEach I am using to display the content is not refreshing when the array being passed in changes. I know the array itself is changing because if I reduce it to a string and display the contents with Text(), it properly updates as soon as a change is made. But, the ForEach loop only updates if I close and reopen the app, forcing the ForEach to reload. I know that there is a special version of ForEach that is specifically designed for dynamic content, but I am pretty sure I am using this version since I pass in '''id: .self'''. Here is the main code snippet:
var hoursTimeCode: [[TimeCodeCellViewModel]] = []
// initialize hoursTimeCode
VStack(spacing: 3) {
ForEach(self.hoursTimeCode, id: \.self) {row in
HStack(spacing: 3){
HourTimeCodeCell(date: row[0].date) // cell view for hour
.frame(minWidth: 50)
ForEach(row.indices, id: \.self) {cell in
// TimeCodeBlockCell displays minutes normally. If it is selected, and a button is pressed, it is assigned a TimeCode which it will then display
TimeCodeBlockCell(timeCodeCellVM: row[cell], selectedArray: $selectedTimeCodeCells)
.frame(maxWidth: .infinity)
.aspectRatio(1.0, contentMode: .fill)
}
}
}
}
I'm pretty sure it doesn't change anything, but I did have to define a custom hash function for the TimeCodeCellViewModel, which might change the behavior of the ForEach (the attributes being changed are included in the hash function). However, I have noticed the same ForEach behavior in another part of my project that uses a different view model, so I highly doubt this is the issue.
class TimeCodeCellViewModel:Identifiable, Hashable {
static func == (lhs: TimeCodeCellViewModel, rhs: TimeCodeCellViewModel) -> Bool {
if lhs.id == rhs.id {
return true
}
else {
return false
}
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
hasher.combine(isSet)
hasher.combine(timeCode)
hasher.combine(date)
}
var id: String = ""
var date = Date()
var isSet = false
var timeCode: TimeCode
var frame: CGRect = .zero
init(timeCodeCell: TimeCodeCell) {
self.id = timeCodeCell.id!.uuidString
self.date = timeCodeCell.date!
self.isSet = timeCodeCell.isSet
self.timeCode = timeCodeCell.toTimeCode!
}
}
Here is a snippet of what you need to make the code work.
See the comments for some basics of why
struct EditCodeView:View{
#EnvironmentObject var timeCodeListVM: TimeCodeListViewModel
//This will observe changes to the view model
#ObservedObject var timeCodeViewModel: TimeCodeViewModel
var body: some View{
EditTimeCodeView(timeCode: timeCodeViewModel.timeCode)
.onDisappear(perform: {
//*********TO SEE CHANGES WHEN YOU EDIT
//uncomment this line***********
//_ = timeCodeListVM.update(timeCodeVM: timeCodeViewModel)
})
}
}
struct EditTimeCodeView: View{
//This will observe changes to the core data entity
#ObservedObject var timeCode: TimeCode
var body: some View{
Form{
TextField("name", text: $timeCode.fullName.bound)
TextField("appreviation", text: $timeCode.abbreviation.bound)
Toggle("favorite", isOn: $timeCode.isFavorite)
}
}
}
class TimeCodeListViewModel: ObservableObject {
//Replacing this whole thing with a #FetchRequest would be way more efficient than these extra view models
//IF you dont want to use #FetchRequest the only other way to observe the persistent store for changes is with NSFetchedResultsController
//https://stackoverflow.com/questions/67526427/swift-fetchrequest-custom-sorting-function/67527134#67527134
//This array will not see changes to the variables of the ObservableObjects
#Published var timeCodeVMs = [TimeCodeViewModel]()
private var persistenceManager = TimeCodePersistenceManager()
init() {
fetchAllTimeCodes()
}
func fetchAllTimeCodes() {
//This method does not observe for new and or deleted timecodes. It is a one time thing
self.timeCodeVMs = persistenceManager.retrieveObjects(sortDescriptors: nil, predicate: nil).map({
//Pass the whole object there isnt a point to just passing the variables
//But the way you had it broke the connection
TimeCodeViewModel(timeCode: $0)
})
}
func addNew() -> TimeCodeViewModel{
let item = TimeCodeViewModel(timeCode: persistenceManager.addSample())
timeCodeVMs.append(item)
//will refresh view because there is a change in count
return item
}
///Call this to save changes
func update(timeCodeVM: TimeCodeViewModel) -> Bool{
let result = persistenceManager.updateObject(object: timeCodeVM.timeCode)
//You have to call this to see changes at the list level
objectWillChange.send()
return result
}
}
//DO you have special code that you aren't including? If not what is the point of this view model?
class TimeCodeViewModel: Identifiable, ObservableObject {
//Simplify this
//This is a CoreData object therefore an ObservableObject it needs an #ObservedObject in a View so changes can be seem
#Published var timeCode: TimeCode
init(timeCode: TimeCode) {
self.timeCode = timeCode
}
}
Your first ForEach probably cannot check if the identity of Array<TimeCodeCellViewModel> has changed.
Perhaps you want to use a separate struct which holds internally an array of TimeCodeCellViewModel and conforms to Identifiable, effectively implementing such protocol.
stuct TCCViewModels: Identifiable {
let models: Array<TimeCodeCellViewModel>
var id: Int {
models.hashValue
}
}
You might as well make this generic too, so it can be reused for different view models in your app:
struct ViewModelsContainer<V: Identifiable> where V.ID: Hashable {
let viewModels: Array<V>
let id: Int
init(viewModels: Array<V>) {
self.viewModels = viewModels
var hasher = Hasher()
hasher.combine(viewModels.count)
viewModels.forEach { hasher.combine($0.id) }
self.id = hasher.finalize
}
}

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 Add to ForEach array without causing the entire view to reload

if I have something like this:
struct ContentView: View {
var results = [Result(score: 8), Result(score: 5), Result(score: 10)]
var body: some View {
VStack {
ForEach(results, id: \.id) { result in
Text("Result: \(result.score)")
}
}
}
}
And then I have a button that appends sometihng to the results array, the entire ForEach loop will reload. This makes sense, but I'm wondering if there is some way to prevent this. The reason I'm asking is because I have a ForEach loop with a few items, each of which plays an animation. If another item is appended to the array, however, the new item appears at the top of the ForEach, but, since the entire view is reload, the other animations playing in the items stop.
Is there any way to prevent this? Like to add an item to a ForEach array, and have it appear, but not reload the entire ForEach loop?
I assume not, but I would wonder how to get around such an issue.
Create separate view for iterating ForEach content, then SwiftUI rendering engine will update container (by adding new item), but not refresh existed rows
ForEach(results, id: \.id) {
ResultCellView(result: $0)
}
and
struct ResultCellView: View {
let result: Result
var body: some View {
Text("Result: \(result.score)")
}
}
Note: I don't see your model, so there might be needed to confirm it to Hashable, Equatable.
In general not providing an id makes it impossible for the ForEach to know what changed (as it has no track of the items) and therefore does not re-render the view.
E.g.
struct ContentView: View {
#State var myData: Array<String> = ["first", "second"]
var body: some View {
VStack() {
ForEach(0..<self.myData.count) { item in
Text(self.myData[item])
}
Button(action: {
self.myData.append("third")
}){
Text("Add third")
}
}
}
}
This throws an console output (that you can ignore) where it tells you about what I just wrote above:
ForEach<Range<Int>, Int, Text> count (3) != its initial count (2).
`ForEach(_:content:)` should only be used for *constant* data.
Instead conform data to `Identifiable` or use `ForEach(_:id:content:)`
and provide an explicit `id`!
For your code try this:
Tested on iOS 13.5.
struct ContentView: View {
#State var results = [Result(score: 8), Result(score: 5), Result(score: 10)]
var body: some View {
VStack {
ForEach(0..<self.results.count) { item in
// please use some index test on production
Text("Result: \(self.results[item].score)")
}
Button(action: {
self.results.append(Result(score: 11))
}) {
Text("Add 11")
}
}
}
}
class Result {
var score: Int
init(score: Int) {
self.score = score
}
}
Please note that this is a "hacky" solution and ForEach was not intended to be used for such cases. (See the console output)

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
.