#Binding to an element of an array that must be reinitialized - swiftui

Sorry if I did something wrong but it's my first time using stackoverflow to ask something.
You will se a View used to represent a Shot variable, which is part of a Shot Array.
I will have as many Row's View as the Array number of elements, to display in a List.
After different tries, leaving the variable shot as #State, I noticed that the TextField did not change the shotName stored value of the single element of the Array, but using Binding it does.
And now comes the problem, in the view where I display all this Row's View, I have at some point to reinitialize the Shot array shots = [Shot](), to create a new one appending new Values.
As soon as I do that, I got an Array out of bound error, during execution, that comes from the reinitialization of the shot array, because I suppose, the binding value loses its variable (?)
PS. i noticed that I did not get the error after the first reinitialization but after the second one
Unfortunately I must reinitialize that array, and so I don't really know how to solve that problem.
struct ShotRowView: View {
#Binding var shot: Roll.Shot
var body: some View {
HStack {
Text("\(shot.id)")
.font(.title)
TextField(
"",
text: $shot.shotName,
prompt: Text("Shot Name")
)
.font(.title)
.autocorrectionDisabled()
Text("\(shot.date)")
.font(.body)
ShotImageView(image: shot.image)
}
}
}
Here's how I use this secondary view in my primary view:
#State var shots: Array<Roll.Shot> = [Roll.Shot]()
private func viewRoll() -> some View{
VStack {
if showPopUp {
RollPopUp(view: self)
}else{
NavigationView(){
List {
ForEach($shots){shot in
NavigationLink {
LogoView()
} label: {
ShotRowView(shot: shot)
}
}
.onDelete(perform: removeShot)
}
.padding([.top, .leading, .trailing])
.toolbar {
ToolbarItem (placement: .automatic) {
HStack {
}
}
}
}
}
}
}

Related

How to redraw a child view in SwiftUI?

I have a ContentView that has a state variable "count". When its value is changed, the number of stars in the child view should be updated.
struct ContentView: View {
#State var count: Int = 5
var body: some View {
VStack {
Stepper("Count", value: $count, in: 0...10)
StarView(count: $count)
}
.padding()
}
}
struct StarView: View {
#Binding var count: Int
var body: some View {
HStack {
ForEach(0..<count) { i in
Image(systemName: "star")
}
}
}
}
I know why the number of stars are not changed in the child view, but I don't know how to fix it because the child view is in a package that I cannot modify. How can I achieve my goal only by changing the ContentView?
You are using the incorrect ForEach initializer, because you aren't explicitly specifying an id. This initializer is only for constant data, AKA a constant range - which this data isn't since count changes.
The documentation for the initializer you are currently using:
It's important that the id of a data element doesn't change unless you
replace the data element with a new data element that has a new
identity. If the id of a data element changes, the content view
generated from that data element loses any current state and animations.
Explicitly set the id like so, by adding id: \.self:
struct StarView: View {
#Binding var count: Int
var body: some View {
HStack {
ForEach(0 ..< count, id: \.self) { _ in
Image(systemName: "star")
}
}
}
}
Similar answer here.

How can I prevent re-Initializing ForEach in SwiftUI?

I have a very simple codes and I want keep it as much as possible simple, I am using a ForEach to render some simple Text, for understanding what is happening undercover I made a TextView to get notified each time this View get called by SwiftUI, unfortunately each time I add new element to my array, SwiftUI is going to render all array elements from begging to end, which I want and expecting it call TextView just for new element, So there is a way to defining an array of View/Text which would solve the issue, but that is over kill for such a simple work, I mean me and you would defiantly use ForEach in our projects, and we could use a simple Text inside ForEach or any other custom View, how we could solve this issue to stop SwiftUI initializing same thing again and again, whith this in mind that I want just use a simple String array and not going to crazy and defining a View array.
My Goal is using an simple array of String to this work without being worry to re-initializing issue.
Maybe it is time to re-think about using ForEach in your App!
SwiftUI would fall to re-rendering trap even with updating an element of the array! which is funny. so make yourself ready if you got 50 or 100 or 1000 rows and you are just updating 1 single row, swiftUI would re render the all entire your array, it does not matter you are using simple Text or your CustomView. So I would wish SwiftUI would be smart to not rendering all array again, and just making necessary render in case.
import SwiftUI
struct ContentView: View {
#State private var arrayOfString: [String] = [String]()
var body: some View {
ForEach(arrayOfString.indices, id:\.self) { index in
TextView(stringOfText: arrayOfString[index])
}
Spacer()
Button("append new element") {
arrayOfString.append(Int.random(in: 1...1000).description)
}
.padding(.bottom)
Button("update first element") {
if arrayOfString.count > 0 {
arrayOfString[0] = "updated!"
}
}
.padding(.bottom)
}
}
struct TextView: View {
let stringOfText: String
init(stringOfText: String) {
self.stringOfText = stringOfText
print("initializing TextView for:", stringOfText)
}
var body: some View {
Text(stringOfText)
}
}
Initializing and rendering are not the same thing. The views get initialized, but not necessarily re-rendered.
Try this with your original ContentView:
struct TextView: View {
let stringOfText: String
init(stringOfText: String) {
self.stringOfText = stringOfText
print("initializing TextView for:", stringOfText)
}
var body: some View {
print("rendering TextView for:", stringOfText)
return Text(stringOfText)
}
}
You'll see that although the views get initialized, they do not in fact get re-rendered.
If you go back to your ContentView, and add dynamic IDs to each element:
TextView(stringOfText: arrayOfString[index]).id(UUID())
You'll see that in this case, they actually do get re-rendered.
You are always iterating from index 0, so that’s an expected outcome. If you want forEach should only execute for newly added item, you need to specify correct range. Check code below-:
import SwiftUI
struct ContentViewsss: View {
#State private var arrayOfString: [String] = [String]()
var body: some View {
if arrayOfString.count > 0 {
ForEach(arrayOfString.count...arrayOfString.count, id:\.self) { index in
TextView(stringOfText: arrayOfString[index - 1])
}
}
Spacer()
Button("append new element") {
arrayOfString.append(Int.random(in: 1...1000).description)
}
}
}
struct TextView: View {
let stringOfText: String
init(stringOfText: String) {
self.stringOfText = stringOfText
print("initializing TextView for:", stringOfText)
}
var body: some View {
Text(stringOfText)
}
}
You need to use LazyVStack here
LazyVStack {
ForEach(arrayOfString.indices, id:\.self) { index in
TextView(stringOfText: arrayOfString[index])
}
}
so it reuse view that goes out of visibility area.
Also note that SwiftUI creates view here and there very often because they are just value type and we just should not put anything heavy into custom view init.
The important thing is not to re-render view when it is not visible or not changed and exactly this thing is what you should think about. First is solved by Lazy* container, second is by Equatable protocol (see next for details https://stackoverflow.com/a/60483313/12299030)

SwiftUI: Dismissed view causes Index out of Range error

This one has been tearing my hair out, I'm hoping someone can show me where I'm going wrong.
The goal is I need to be able to manage a dynamic list of items (i.e. the list could grow or shrink), where each item's properties can be adjusted (edited), and the items are related to a given parent.
In the abridged code below I've called the item Cell, which belongs to a Row. A Row can have many Cells, and each Cell has an amount which can be changed by the user. This is an abridged version of my code, which I hope makes the relationship labelling a little clearer but, crucially, it gives me the same error which is:
If the number of Cells is less than the original amount (i.e. 3 or less in this example) then the application crashes with an 'Index out of Range' error whenever the view is dismissed. Up to that point cells can be added or removed without any issue whatsoever and I don't get this error when changing the amount of cell items. I've looked all over SO and various blogs and can't find anyone who has encountered this particular issue - it seems most of the Index out of Range posts occur when actually modifying the list, my error only happens once the list is shrunk and then dismissed.
I've attached some sample code below - you should be able to cut/paste and try it out.
PS. I know calculateTotals() is sketch; please don't add letters or punctuation to your totals - it's just to test the bindings are bubbling correctly :)
Cell
struct Cell: Identifiable {
var amount: String
var id = UUID()
init(_ amount: String = "0.00"){
self.amount = amount
}
}
Row
class Row: ObservableObject {
#Published var cells: [Cell]
init(){
self.cells = [
Cell("10"),
Cell("15"),
Cell("20"),
Cell("25")]
}
}
ContentView
struct ContentView: View {
#State private var displayAmounts = false
var body: some View {
NavigationView {
Button(action: {
self.displayAmounts.toggle()
}) {
Text("View amounts")
}
}
.sheet(isPresented: self.$displayAmounts) {
CellSheet()
}
}
}
CellSheet
struct CellSheet: View {
#ObservedObject var row: Row = Row()
private func deleteCell(at offsets: IndexSet) {
self.row.cells.remove(atOffsets: offsets)
}
private func calculateTotals() -> String {
var total = Double("0.00")!
for cell in self.row.cells {
if( "" != cell.amount ) {
total += Double(cell.amount)!
}
}
return String("\(total)")
}
var body: some View {
VStack{
List {
ForEach(row.cells.indices, id: \.self){ i in
CellItem(amount: self.$row.cells[i].amount)
}.onDelete(perform: deleteCell)
}
Button(action: {
self.row.cells.append(Cell())
}) {
Text("Add new cell")
}
Text(calculateTotals())
}
}
}
CellItem - some oddness here: if I wrap the TextField with an HStack then the app crashes immediately if remove a cell -- even if the total number of cells is greater than the original amount (4).
struct CellItem: View {
#Binding var amount: String
var body: some View {
// Uncomment HStack and deleting rows immediately causes index out of range.
// HStack {
TextField("Amount: ", text: $amount)
// }
}
}
I'm really at a loss as to why/how this is happening. Clearly Swift is trying to access an index that doesn't exist (if I remove the binding and just output the value there's no problem), but I don't understand why that would cause issues when the view is dismissed. My guess is that Swift is caching some things in memory? The HStack wrapping issue is also peculiar.
Anyway, I'm relatively new to Swift so it's possible I'm overlooking something obvious. For additional context, I'm running XCode 11.4.1 and targeting iOS 13.4.
You should be able to lift all of this code straight into a new project and it will compile. Any help will be gratefully received :)
Ok, literally minutes after creating this post (and I've been on this for a couple of days), I think I have a solution. I've modified my List() in ContentView as follows:
List {
ForEach(Array(row.cells.enumerated()), id:\.1.id) { (i, cell) in
CellItem(amount: self.$row.cells[i].amount)
}.onDelete(perform: deleteCell)
}
This was based on this answer as I think (?) enumerated() is better suited when the array is of a variable length. The HStack crashing is also resolved with the above implementation. Maybe someone can add some more context to this.

SwiftUI List is not showing any items

I want to use NavigationView together with the ScrollView, but I am not seeing List items.
struct ContentView: View {
var body: some View {
NavigationView {
ScrollView{
VStack {
Text("Some stuff 1")
List{
Text("one").padding()
Text("two").padding()
Text("three").padding()
}
Text("Some stuff 2")
}
}
}
}
}
All I see is the text. If I remove ScrollView I see it all, but the text is being pushed to the very bottom. I simply want to be able to add List and Views in a nice scrollable page.
The ScrollView expects dimension from content, but List expects dimension from container - as you see there is conflict, so size for list is undefined, and a result rendering engine just drop it to avoid disambiguty.
The solution is to define some size to List, depending of your needs, so ScrollView would now how to lay out it, so scroll view could scroll entire content and list could scroll internal content.
Eg.
struct ContentView: View {
#Environment(\.defaultMinListRowHeight) var minRowHeight
var body: some View {
NavigationView {
ScrollView{
VStack {
Text("Some stuff 1")
List {
Text("one").padding()
Text("two").padding()
Text("three").padding()
}.frame(minHeight: minRowHeight * 3).border(Color.red)
Text("Some stuff 2")
}
}
}
}
}
Just wanted to throw out an answer that fixed what I was seeing very similar to the original problem - I had put a Label() item ahead of my List{ ... } section, and when I deleted that Label() { } I was able to see my List content again. Possibly List is buggy with other items surrounding it (Xcode 13 Beta 5).

How to allow Text to grow enough to display entire contents without truncation?

I am trying to create a SwiftUI view that represents a chat conversation in a series of vertically arranged bubbles, like in Messages.
I'm having trouble figuring out how to display very long messages. I would like to display such messages simply as very large bubbles that display all of the text. For example, like Messages does this:
The problem I run into is that the Text view, on which my bubbles are based, pretty much does its own thing. This can lead to the entire text being displayed correctly for small messages, long messages being broken into several lines but still displayed in full, other long messages being reduced to a single line with an ellipse.
Consider the following code to create a sequence of messages:
import SwiftUI
struct MessageView: View {
var body: some View {
Text("This is a very long message. Can you imagine it will ever be displayed in full on the screen? Because I can‘t. I can tell you, the one other time I wrote a message this long was when we went to the picnic and uncle Bob whipped out his cigars and I had to vent on the family WhatsApp group.")
}
}
struct ContentView: View {
var body: some View {
VStack {
ForEach(0..<100, id: \.self) { i in
MessageView()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
It results in this undesirable layout:
I have experimented with fixedSize and frame after reading several SwiftUI guides; this makes it possible to make bubbles large, but then they have a fixed size and/or won't grow when the text to display is even longer than expected.
How can I tell the MessageViews – or rather the Text views inside them – that they're free to take up as much space as they need vertically to render their text contents in full?
I found one interesting decision, using List and removing dividers:
struct ContentView: View {
init() {
UITableView.appearance().tableFooterView = UIView()
UITableView.appearance().separatorStyle = .none
}
var body: some View {
List {
ForEach(0..<50, id: \.self) { i in
VStack(alignment: .trailing, spacing: 20) {
if i%2 == 0 {
MessageView().lineLimit(nil)
} else {
MessageViewLeft()
}
}
}
}
}
}
and I made 2 structs, for demonstration:
struct MessageView: View {
var body: some View {
HStack {
Text("This is a very long message. Can you imagine it will ever be displayed in full on the screen? Because I can‘t. I can tell you, the one other time I wrote a message this long was when we went to the picnic and uncle Bob whipped out his cigars and I had to vent on the family WhatsApp group.")
Spacer(minLength: 20)
}
}
}
struct MessageViewLeft: View {
var body: some View {
HStack {
Spacer(minLength: 20)
Text("This is a very long message. Can you imagine it will ever be displayed in full on the screen? Because I can‘t. I can tell you, the one other time I wrote a message this long was when we went to the picnic and uncle Bob whipped out his cigars and I had to vent on the family WhatsApp group.")
}
}
}
the result is:
P.S. the answer should be .lineLimit(nil), but there is some bug with this in TextField. Maybe with Text this bug continues too
P.P.S. I wad hastened with the answer =(. You can set your VStack List into ScrollView:
var body: some View {
VStack {
ScrollView {
ForEach(0..<50, id: \.self) { i in
VStack(alignment: .trailing, spacing: 20) {
MessageView().padding()
}
}
}
}
}
and the result is:
Try to set your MessageView in a ScrollView.