How to pass reference to underlying array struct to a view? - swiftui

In the example below I want to modify the struct in an array that is used to generate a view from that view. It results in an obvious error (in ForEach the reference to the currently processed item is a let constant). How else I can modify the original array row upon interaction with the view at runtime? In other words, how do I store/pass a reference to the corresponding array item?
I also tried passing the array index to inside the view and accessing it there directly by array[index].someproperty. It worked great... until I tried deleting rows. In such case the dynamic views no longer contain correct indexes and it generally leads to out of range errors.
In the example below I want to save drag location to the original array rather than to the view itself so that I can conduct logic on it later (can't iterate through views, but can easily iterate over their underlaying array). Could someone point me to the right approach? Thanks!
EDIT: Clarification: The goal here is for the drag gesture to write values to the ballStorage object's array and have generated views react accordingly. But it needs to know which row to modify!
import SwiftUI
let screenWidth = UIScreen.main.bounds.size.width
let screenHeight = UIScreen.main.bounds.size.height
struct SingleBall: Identifiable {
var id = UUID()
var position = CGPoint.zero
}
class BallStorage: ObservableObject {
#Published var balls: [SingleBall] = [
SingleBall(position: CGPoint(x: 110, y: 220)),
SingleBall(position: CGPoint(x: 150, y: 120)),
SingleBall(position: CGPoint(x: 200, y:160)),
SingleBall(position: CGPoint(x: 200, y: 200))
]
}
struct StartScreen: View {
#StateObject var ballStorage = BallStorage()
var body: some View {
ZStack { // stack of balls
ForEach(ballStorage.balls, id: \.id) {
ball in
littleBall(id: ball.id).position(ball.position).gesture(DragGesture()
.onChanged {
gesture in
ball.position = gesture.location //ERROR! can't modify ball (it's a let constant)
}
)
}
}.frame(width:screenWidth, height:screenHeight) //end stack balls
}
}
struct test_Previews: PreviewProvider {
static var previews: some View {
StartScreen()
}
}
EDIT: Another thing I tried was passing 'ball' (see the code) to inside the view. I can modify it there, and the view reacts to the changes, but in this case what gets passed is a copy of the array item, not a reference to the actual struct in the original array. So modifying the array has no effect on views.

You'd need to modify the original array instead, so you'd need an index, which you can get by doing ForEach over indices:
ForEach(ballStorage.balls.indices, id: \.self) { index in
littleBall(id: ballStorage.balls[index].id)
.position(ballStorage.balls[index].position)
//...
.onChanged { gesture in
// modify ballStorage.balls[index]
ballStorage.balls[index].position = gesture.location
}
}

Related

How to use Binding<Color> in SwiftUI?

How to change a color dynamically in SwiftUI, like in this example
#Binding var randomColor: Color
public var body: some View {
ZStack {
Circle()
.stroke(lineWidth: 20)
.opacity(0.5)
.foregroundColor($randomColor)
}
}
This does'nt work because .foregroundColor only takes Color. Is there any way of using #Binding with colors or is this just not the way of doing it in SwiftUI?
You only need to use the $variableName form of a binding variable in subviews that need to change, as well as read, the variable’s value.
If all your subview is doing is reading the currently set value, you would access variableName directly. So your body in the example you provided would be
public var body: some View {
ZStack {
Circle()
.stroke(lineWidth: 20)
.opacity(0.5)
.foregroundColor(randomColor)
}
}
Furthermore, using #Binding says that your view is going to need to edit the value of a state variable that is owned by the parent’s view. If you’re not making any changes to it, there is no point declaring it as a binding at all.
On the other hand, if you are going to be changing the value of the variable, but this view “owns" the variable, you’d use #State instead.
For example:
struct ContentView: View {
// owned by this view (and with a starting value defined)
#State var myColor: Color = .accessColor
var body: some View {
VStack {
// This subview is allowed to change the value, so it takes a binding
ColorPicker("Color", selection: $myColor)
// This subview is only going to display the color, so it takes a read-only property
ExampleColorView(color: myColor)
}
}
}
struct ExampleColorView: View {
var color: Color
var body: some View {
ZStack {
Circle()
.stroke(lineWidth: 20)
.opacity(0.5)
.foregroundColor(color)
}
}
}
Whenever the parent view’s myColor variable changes, the SwiftUI system creates a new ExampleColorView struct with the new color defined – and it spots the difference between the old view and the new one, and redraws the screen.

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)

Can dynamic views modify array rows on the basis of which they have been created?

I've been trying to wrap my head about some key concepts of SwiftUI. There is one thing I still don't really understand.
The situation:
Have an array of structs that contain various values.
Dynamically create views (e.g. Circles) by iterating through this array with ForEach.
Question: Are the newly created views somehow connected to their corresponding array rows? If something changes about a Circle, can this Circle save that change back to the Struct in the Array from which it was created?
I made a chart to illustrate this:
The use case/issue I'm referring to is: I create a number of Circles this way. The circles then animate, but I need to track their position... Can I make them update it back at the Array as they go?
Thank you!
EDIT: Example code:
import SwiftUI
struct blackCircle: Identifiable {
var id = UUID()
var position: CGPoint!
}
class CircleStorage: ObservableObject {
#Published var circles = [
blackCircle(position: CGPoint(x: 10, y: 20)),
blackCircle(position: CGPoint(x: 50, y: 100)),
blackCircle(position: CGPoint(x: 100, y: 50))
]
}
struct ContentView: View {
#StateObject var circleStorage = CircleStorage()
var body: some View {
ZStack {
ForEach(circleStorage.circles, id: \.id) {
circle in
Circle()
.position(circle.position)
.frame(width:44, height:44)
}
}
}
}
This creates three circles. I need to iterate through the circles, check their position and e.g. remove ones that have x over a certain value. I can do that by iterating through the Array - it contains positions where Circles are created. But what if their position changes at runtime by means of animation or dragging etc.? Can I make sure corresponding rows contain an up-to-date position even if it changes?

SwiftUI Screen safe area

Im trying to calculate the screen safe area size in a SwiftUI app launch so I can derive component sizes from the safe area rectangle for iOS devices of different screen sizes.
UIScreen.main.bounds - I can use this at the start but it gives me the total screen and not the safe area
GeometryReader - using this I can get the CGSize of the safe area but I cant find a way to send this anywhere - tried using Notifications and simple functions both of which caused errors
Finally I tried using the .onPreferenceSet event in the initial view then within that closure set a CGSize variable in a reference file, but doing that, for some reason makes the first view initialise twice. Does anyone know a good way to get the edge insets or the safe area size at app startup?
More simpler solution :
UIApplication.shared.windows.first { $0.isKeyWindow }?.safeAreaInsets.bottom
or shorter:
UIApplication.shared.windows.first?.safeAreaInsets.top
Have you tried this?
You can use EnvironmentObject to send the safe area insets anywhere in your code after initializing it in your initial View.
This works for me.
class GlobalModel: ObservableObject {
//Safe Area size
#Published var safeArea: (top: CGFloat, bottom: CGFloat)
init() {
self.safeArea = (0, 0)
}
}
Inside SceneDelegate.
let globalModel = GlobalModel()
let contentView = ContentView().environmentObject(globalModel)
Inside your initial view.
struct ContentView: View {
#EnvironmentObject var globalModel: GlobalModel
var body: some View {
ZStack {
GeometryReader { geo in
Color.clear
.edgesIgnoringSafeArea(.all)
.onAppear {
self.globalModel.safeArea = (geo.safeAreaInsets.top, geo.safeAreaInsets.bottom)
}
}
SomeView()
}
}
}

View is not rerendered in Nested ForEach loop

I have the following component that renders a grid of semi transparent characters:
var body: some View {
VStack{
Text("\(self.settings.numRows) x \(self.settings.numColumns)")
ForEach(0..<self.settings.numRows){ i in
Spacer()
HStack{
ForEach(0..<self.settings.numColumns){ j in
Spacer()
// why do I get an error when I try to multiply i * j
self.getSymbol(index:j)
Spacer()
}
}
Spacer()
}
}
}
settings is an EnvironmentObject
Whenever settings is updated the Text in the outermost VStack is correctly updated. However, the rest of the view is not updated (Grid has same dimenstions as before). Why is this?
Second question:
Why is it not possible to access the i in the inner ForEach-loop and pass it as a argument to the function?
I get an error at the outer ForEach-loop:
Generic parameter 'Data' could not be inferred
TL;DR
Your ForEach needs id: \.self added after your range.
Explanation
ForEach has several initializers. You are using
init(_ data: Range<Int>, #ViewBuilder content: #escaping (Int) -> Content)
where data must be a constant.
If your range may change (e.g. you are adding or removing items from an array, which will change the upper bound), then you need to use
init(_ data: Data, id: KeyPath<Data.Element, ID>, content: #escaping (Data.Element) -> Content)
You supply a keypath to the id parameter, which uniquely identifies each element that ForEach loops over. In the case of a Range<Int>, the element you are looping over is an Int specifying the array index, which is unique. Therefore you can simply use the \.self keypath to have the ForEach identify each index element by its own value.
Here is what it looks like in practice:
struct ContentView: View {
#State var array = [1, 2, 3]
var body: some View {
VStack {
Button("Add") {
self.array.append(self.array.last! + 1)
}
// this is the key part v--------v
ForEach(0..<array.count, id: \.self) { index in
Text("\(index): \(self.array[index])")
//Note: If you want more than one views here, you need a VStack or some container, or will throw errors
}
}
}
}
If you run that, you'll see that as you press the button to add items to the array, they will appear in the VStack automatically. If you remove "id: \.self", you'll see your original error:
`ForEach(_:content:)` should only be used for *constant* data.
Instead conform data to `Identifiable` or use `ForEach(_:id:content:)`
and provide an explicit `id`!"
ForEach should only be used for constant data. So it is only evaluated once by definition. Try wrapping it in a List and you will see errors being logged like:
ForEach, Int, TupleView<(Spacer, HStack, Int, TupleView<(Spacer, Text, Spacer)>>>, Spacer)>> count (7) != its initial count (0). ForEach(_:content:) should only be used for constant data. Instead conform data to Identifiable or use ForEach(_:id:content:) and provide an explicit id!
I was surprised by this as well, and unable to find any official documentation about this limitation.
As for why it is not possible for you to access the i in the inner ForEach-loop, I think you probably have a misleading compiler error on your hands, related to something else in the code that is missing in your snippets. It did compile for me after completing the missing parts with a best guess (Xcode 11.1, Mac OS 10.14.4).
Here is what I came up with to make your ForEach go over something Identifiable:
struct SettingsElement: Identifiable {
var id: Int { value }
let value: Int
init(_ i: Int) { value = i }
}
class Settings: ObservableObject {
#Published var rows = [SettingsElement(0),SettingsElement(1),SettingsElement(2)]
#Published var columns = [SettingsElement(0),SettingsElement(1),SettingsElement(2)]
}
struct ContentView: View {
#EnvironmentObject var settings: Settings
func getSymbol(index: Int) -> Text { Text("\(index)") }
var body: some View {
VStack{
Text("\(self.settings.rows.count) x \(self.settings.columns.count)")
ForEach(self.settings.rows) { i in
VStack {
HStack {
ForEach(self.settings.columns) { j in
Text("\(i.value) \(j.value)")
}
}
}
}
}
}
}