Xcode 14.1, Ventura 13.1
I have a VStack with a load of graphical dials that I want to update.
For testing they are prepopulated with data from an array as follows:
struct Stats : Identifiable {
var id : Int
var title : String
var currentData : CGFloat
var goal : CGFloat
var color : Color
var unit: String
}
var stats_Data = [
Stats(id: 0, title: "Daylight", currentData: 6.8, goal: 15, color: .yellow, unit: "Hrs"),
Stats(id: 1, title: "Sunrise/Set", currentData: 3.5, goal: 5, color: .orange, unit: "Hour"),
Stats(id: 2, title: "Cloud Cover", currentData: 585, goal: 1000, color: .gray, unit: "%"),
Stats(id: 3, title: "Inclination", currentData: 6.2, goal: 10, color: .green, unit: "°"),
Stats(id: 4, title: "Orientation", currentData: 12.5, goal: 25, color: .white, unit: "°"),
Stats(id: 5, title: "Power", currentData: 16889, goal: 20000, color: .red, unit: "kW")
]
If I want to update any of this data, say "currentData" in a particular row how do I do this?
For text I can declare it as a #State var and then update it when a button is pressed, say. I have tried to do this in a similar fashion with a #State array but the preview/compiler doesn't like arrayed data within the VStack.
The test data is currently being passed in the VStack along the lines of:
ForEach(stats_Data){stat in
VStack(spacing: 14){
...
Circle()
.trim(from: 0, to: (stat.currentData / stat.goal))
So how can I alter this to update data?
Vadian's correct syntax answered this question, e.g. stats_data[1].currentData
Many thanks.
Related
I'm trying to achieve a design which essentially has the following layout:
So I need column headings for this table or grid view. The second column does not need a heading.
This seems like a job for a LazyVGrid or LazyHGrid, however I can't seem to find a solution which neatly includes headings like this - it all seems a bit hacky.
Wondering if I am missing a better way to achieve this. I could of course try and create VStacks within HStacks, but this just seems like something which should be accomplished more intelligently than this.
You can just pass the headers into the LazyVGrid before you show the content:
struct Item: Identifiable {
let id = UUID()
var item: String
var description: String = "This is the item description"
var quantity: Int = 1
var price: Double = 0
}
struct ContentView: View {
let data = [
Item(item: "Image 1", quantity: 2, price: 1.99),
Item(item: "Image 2", quantity: 1, price: 3.99),
Item(item: "Image 3", quantity: 5, price: 9.99),
]
let columns = [
GridItem(.flexible(), alignment: .topLeading),
GridItem(.flexible(minimum: 150), alignment: .topLeading),
GridItem(.flexible(), alignment: .topLeading),
GridItem(.flexible(), alignment: .topLeading),
]
var body: some View {
LazyVGrid(columns: columns) {
// headers
Group {
Text("Item")
Text("")
Text("Qty")
Text("Price")
}
.font(.headline)
// content
ForEach(data) { item in
Text(item.item)
Text(item.description)
Text("\(item.quantity)")
Text("$\(item.price, specifier: "%.2f")")
}
}
.padding()
}
}
I have what I thought was a simple task, but it appears otherwise. As part of my code, I am simply displaying some circles and when the user clicks on one, it will navigate to a new view depending on which circle was pressed.
If I hard code the views in the NavigationLink, it works fine, but I want to use a dynamic array. In the following code, it doesn't matter which circle is pressed, it will always display the id of the last item in the array; ie it passes the last defined dot.destinationId to the Game view
struct Dot: Identifiable {
let id = UUID()
let x: CGFloat
let y: CGFloat
let color: Color
let radius: CGFloat
let destinationId: Int
}
struct ContentView: View {
var dots = [
Dot(x:10.0,y:10.0, color: Color.green, radius: 12, destinationId: 1),
Dot(x:110.0,y:10.0, color: Color.green, radius: 12, destinationId: 2),
Dot(x:110.0,y:110.0, color: Color.blue, radius: 12, destinationId: 3),
Dot(x:210.0,y:110.0, color: Color.blue, radius: 12, destinationId: 4),
Dot(x:310.0,y:110.0, color: Color.red, radius: 14, destinationId: 5),
Dot(x:210.0,y:210.0, color: Color.blue, radius: 12, destinationId: 6)]
var lineWidth: CGFloat = 1
var body: some View {
NavigationView {
ZStack
Group {
ForEach(dots){ dot in
NavigationLink(destination: Game(id: dot.destinationId))
{
Circle()
.fill(dot.color)
.frame(width: dot.radius, height: dot.radius)
.position(x:dot.x, y: dot.y )
}
}
}
.frame(width: .infinity, height: .infinity, alignment: .center )
}
.navigationBarTitle("")
.navigationBarTitleDisplayMode(.inline)
}
}
struct Game: View {
var id: Int
var body: some View {
Text("\(id)")
}
}
I tried to send the actual view as i want to show different views and used AnyView, but the same occurred It was always causing the last defined view to be navigated to.
I'm really not sure what I a doing wrong, any pointers would be greatly appreciated
While it might seem that you're clicking on different dots, the reality is that you have a ZStack with overlapping views and you're always clicking on the top-most view. You can see when you tap that the dot with ID 6 is always the one actually getting pressed (notice its opacity changes when you click).
To fix this, move your frame and position modifiers outside of the NavigationLink so that they affect the link and the circle contained in it, instead of just the inner view.
Also, I modified your last frame since there were warnings about an invalid frame size (note the different between width/maxWidth and height/maxHeight when specifying .infinite).
struct ContentView: View {
var dots = [
Dot(x:10.0,y:10.0, color: Color.green, radius: 12, destinationId: 1),
Dot(x:110.0,y:10.0, color: Color.green, radius: 12, destinationId: 2),
Dot(x:110.0,y:110.0, color: Color.blue, radius: 12, destinationId: 3),
Dot(x:210.0,y:110.0, color: Color.blue, radius: 12, destinationId: 4),
Dot(x:310.0,y:110.0, color: Color.red, radius: 14, destinationId: 5),
Dot(x:210.0,y:210.0, color: Color.blue, radius: 12, destinationId: 6)]
var lineWidth: CGFloat = 1
var body: some View {
NavigationView {
ZStack {
Group {
ForEach(dots){ dot in
NavigationLink(destination: Game(id: dot.destinationId))
{
Circle()
.fill(dot.color)
}
.frame(width: dot.radius, height: dot.radius) //<-- Here
.position(x:dot.x, y: dot.y ) //<-- Here
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center )
}
.navigationBarTitle("")
.navigationBarTitleDisplayMode(.inline)
}
}
}
I would like to show a bunch of vertical bars, and on the click of a button have the first one swap places with a randomly selected one. I want this to be animated and the gif below shows the closest I've come, but as you can see, the entire set of vertical bars flashes briefly on each button click, which I do not want.
I've tried a few different approaches. The most elegant one I think would be to have the array of values which I iterate through to show the bars be a state variable, and when the array is sorted, the list just reloads and the swapping is animated. However, this didn't result in a swapping effect. It would just shrink and grow the respective bars to match the sizes of the array elements that were swapped :( I still feel like this might be the best approach, so I'm open to completely changing the implementation.
So the closest I've come is what you see in the gif, but it's hacky and I don't like the flashing of the entire view when it reloads. I'm using matchedGeometryEffect (hero animation) and even to get that working, I had to have a state variable that would reload the view. As you will see in the code below, this forces me to have an if/else even if I am just reloading the view under either condition. So I don't truly need the if(animate){} else {}, but using a hero animation forces me to.
Here's my code:
import SwiftUI
class SortElement: Identifiable{
var id: Int
var name: String
var color: Color
var height: CGFloat
init(id: Int, name: String, color: Color, height: CGFloat){
self.id = id
self.name = name
self.color = color
self.height = height
}
}
struct SortArrayAnimationDemo: View {
#Namespace private var animationSwap
#State private var animate: Bool = false
private static var sortableElements: [SortElement] = [
SortElement(id: 6, name:"6", color: Color(UIColor.lightGray), height: 60),
SortElement(id: 2, name:"2", color: Color(UIColor.lightGray), height: 20),
SortElement(id: 5, name:"5", color: Color(UIColor.lightGray), height: 50),
SortElement(id: 1, name:"1", color: Color(UIColor.lightGray), height: 10),
SortElement(id: 3, name:"3", color: Color(UIColor.lightGray), height: 30),
SortElement(id: 9, name:"9", color: Color(UIColor.lightGray), height: 90),
SortElement(id: 4, name:"4", color: Color(UIColor.lightGray), height: 40),
SortElement(id: 8, name:"8", color: Color(UIColor.lightGray), height: 80)
]
var body: some View {
VStack{
RoundedRectangle(cornerRadius: 10.0)
.fill(Color.blue)
.frame(width: 200.0, height: 20.0)
if(animate){
fetchSwappingView()
}else {
fetchSwappingView()
}
RoundedRectangle(cornerRadius: 10.0)
.fill(Color.blue)
.frame(width: 200.0, height: 20.0)
Button(action: { doAnimate() }) { Text("Swap!") }.padding(.top, 30)
}//vstack
}
#ViewBuilder
private func fetchSwappingView() -> some View {
HStack(alignment: .bottom){
RoundedRectangle(cornerRadius: 10.0)
.fill(Color.red)
.frame(width: 30.0, height: 100.0)
ForEach(0..<SortArrayAnimationDemo.sortableElements.count) { i in
let sortableElement = SortArrayAnimationDemo.sortableElements[i]
RoundedRectangle(cornerRadius: 10.0)
.fill(Color.gray)
.frame(width: 20.0, height: sortableElement.height)
.matchedGeometryEffect(id: sortableElement.id,
in: animationSwap,
properties: .position)
}
RoundedRectangle(cornerRadius: 10.0)
.fill(Color.red)
.frame(width: 30.0, height: 100.0)
}//hstack
}
private func doAnimate() -> Void {
let randomIndex = Int.random(in: 1..<8)
let tmpLeftSortableElement: SortElement = SortArrayAnimationDemo.sortableElements[0]
SortArrayAnimationDemo.sortableElements[0] = SortArrayAnimationDemo.sortableElements[randomIndex]
SortArrayAnimationDemo.sortableElements[randomIndex] = tmpLeftSortableElement
withAnimation(.spring(dampingFraction: 0.7).speed(1.5).delay(0.05)){
animate.toggle()
}
}
}
Your code has a couple problems. First, you're working around the immutability of structs with this:
private static var sortableElements: [SortElement] = [
static isn't going to make SwiftUI refresh the view. I think you found this out too, so added another #State:
#State private var animate: Bool = false
...
if (animate) {
fetchSwappingView()
} else {
fetchSwappingView()
}
Well... this is where your flashing is coming from. The default transition applied to an if-else is a simple fade, which is what you're seeing.
But, if you think about it, this if-else doesn't make any sense. There's no need for an intermediary animate state — just let SwiftUI do the work!
You were initially on the right track.
The most elegant one I think would be to have the array of values which I iterate through to show the bars be a state variable, and when the array is sorted, the list just reloads and the swapping is animated. However, this didn't result in a swapping effect. It would just shrink and grow the respective bars to match the sizes of the array elements that were swapped :( I still feel like this might be the best approach, so I'm open to completely changing the implementation.
As you thought, the better way would be to just have a single #State array of SortElements. I'll get to the swapping effect problem in a bit — first, replace private static var with #State private var:
#State private var sortableElements: [SortElement] = [
This makes it possible to directly modify sortableElements. Now, in your doAnimate function, just set sortableElements instead of doing all sorts of weird stuff.
private func doAnimate() -> Void {
let randomIndex = Int.random(in: 1..<8)
let tmpLeftSortableElement: SortElement = sortableElements[0]
withAnimation(.spring(dampingFraction: 0.7).speed(1.5).delay(0.05)){
sortableElements[0] = sortableElements[randomIndex]
sortableElements[randomIndex] = tmpLeftSortableElement
}
}
Don't forget to also replace
if (animate) {
fetchSwappingView()
} else {
fetchSwappingView()
}
with:
fetchSwappingView()
At this point, your result should look like this:
This matches your problem description: the heights change fine, but there's no animation!
this didn't result in a swapping effect. It would just shrink and grow the respective bars to match the sizes of the array elements that were swapped :(
The problem is here:
ForEach(0..<sortableElements.count) { i in
Since you're simply looping over sortableElements.count, SwiftUI has no idea how the order changed. Instead, you should directly loop over sortableElements. The array's elements, SortElement, all conform to Identifiable — so SwiftUI can now determine how the order changed.
ForEach(sortableElements) { element in
RoundedRectangle(cornerRadius: 10.0)
.fill(Color.gray)
.frame(width: 20.0, height: element.height)
.matchedGeometryEffect(
id: element.id,
in: animationSwap,
properties: .position
)
}
Here's the final code:
struct SortArrayAnimationDemo: View {
#Namespace private var animationSwap
#State private var sortableElements: [SortElement] = [
SortElement(id: 6, name:"6", color: Color(UIColor.lightGray), height: 60),
SortElement(id: 2, name:"2", color: Color(UIColor.lightGray), height: 20),
SortElement(id: 5, name:"5", color: Color(UIColor.lightGray), height: 50),
SortElement(id: 1, name:"1", color: Color(UIColor.lightGray), height: 10),
SortElement(id: 3, name:"3", color: Color(UIColor.lightGray), height: 30),
SortElement(id: 9, name:"9", color: Color(UIColor.lightGray), height: 90),
SortElement(id: 4, name:"4", color: Color(UIColor.lightGray), height: 40),
SortElement(id: 8, name:"8", color: Color(UIColor.lightGray), height: 80)
]
var body: some View {
VStack{
RoundedRectangle(cornerRadius: 10.0)
.fill(Color.blue)
.frame(width: 200.0, height: 20.0)
fetchSwappingView() /// NO if else needed!
RoundedRectangle(cornerRadius: 10.0)
.fill(Color.blue)
.frame(width: 200.0, height: 20.0)
Button(action: { doAnimate() }) { Text("Swap!") }.padding(.top, 30)
}//vstack
}
#ViewBuilder
private func fetchSwappingView() -> some View {
HStack(alignment: .bottom){
RoundedRectangle(cornerRadius: 10.0)
.fill(Color.red)
.frame(width: 30.0, height: 100.0)
/// directly loop over `sortableElements` instead of its count
ForEach(sortableElements) { element in
RoundedRectangle(cornerRadius: 10.0)
.fill(Color.gray)
.frame(width: 20.0, height: element.height)
.matchedGeometryEffect(
id: element.id,
in: animationSwap,
properties: .position
)
}
RoundedRectangle(cornerRadius: 10.0)
.fill(Color.red)
.frame(width: 30.0, height: 100.0)
}//hstack
}
private func doAnimate() -> Void {
let randomIndex = Int.random(in: 1..<8)
let tmpLeftSortableElement: SortElement = sortableElements[0]
/// directly set `sortableElements`
withAnimation(.spring(dampingFraction: 0.7).speed(1.5).delay(0.05)){
sortableElements[0] = sortableElements[randomIndex]
sortableElements[randomIndex] = tmpLeftSortableElement
}
}
}
Result:
I have a simple horizontal list view that displays the list of colours and the problem is that I don't know how to apply border to only one selected view, for now it's adding the white border to all views in the list on didTap, please help if you know how to achieve it, thanks 🙌
Here's the code:
struct Colour: Identifiable {
var id: Int
var color: Color
}
struct ContentView: View {
#State private var didTap = false
#State private var colors: [Colour] = [
Colour(id: 1, color: .black),
Colour(id: 2, color: .yellow),
Colour(id: 3, color: .orange),
Colour(id: 4, color: .green),
Colour(id: 5, color: .red),
Colour(id: 6, color: .blue),
Colour(id: 7, color: .pink),
Colour(id: 8, color: .purple),
]
var body: some View {
ZStack {
Color.gray.opacity(0.2)
.ignoresSafeArea()
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 4) {
ForEach(colors) { color in
Rectangle()
.fill(color.color)
.frame(width: 100, height: 150)
.overlay(
Rectangle()
.stroke(didTap ? Color.white : .clear, lineWidth: 5)
.animation(.spring())
)
.padding(5)
.onTapGesture {
didTap.toggle()
print(color.id)
}
}
}
.padding(.leading, 8)
}
}
}
}
You need to store id of selected color instead of bool.
Here is simple demo (deselect on tap on same element is your excise). Tested with Xcode 12.5 / iOS 14.5
struct ContentView: View {
#State private var selected = -1 // << here !!
#State private var colors: [Colour] = [
Colour(id: 1, color: .black),
Colour(id: 2, color: .yellow),
Colour(id: 3, color: .orange),
Colour(id: 4, color: .green),
Colour(id: 5, color: .red),
Colour(id: 6, color: .blue),
Colour(id: 7, color: .pink),
Colour(id: 8, color: .purple),
]
var body: some View {
ZStack {
Color.gray.opacity(0.2)
.ignoresSafeArea()
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 4) {
ForEach(colors) { color in
Rectangle()
.fill(color.color)
.frame(width: 100, height: 150)
.overlay(
Rectangle()
.stroke(selected == color.id ? Color.white : .clear, lineWidth: 5)
.animation(.spring())
)
.padding(5)
.onTapGesture {
selected = color.id // << here !!
print(color.id)
}
}
}
.padding(.leading, 8)
}
}
}
}
I am trying to show a scrollview in which every cell has 3 picker text field.
However, only the last cell has a working picker text field that is tappable.
When I check BasketListCell preview it is just like I wanted. All the pickers are working well with placeholders. But in scrollview it does not.
I have 3 different listData in BasketList but ShoppingCartView repeats the first one 3 times.
so this is my cell struct Basket that has 3 Int for each picker text field.
struct Basket: Identifiable {
var id: Int {
return elementID
}
var elementID: Int
var image: String
var title: String
var brand: String
var price: String
var selectionImage: String
var discountedPrice: String
var sizeSelectedIndex: Int?
var colorSelectedIndex: Int?
var itemSelectedIndex : Int?
}
That is my demo BasketList with 3 elements.
struct BasketList {
static let listData: [Basket] = [
Basket(elementID: 0, image: "Tee", title: "3-Stripes Shirt", brand: "Adidas Original", price: "$79.40", selectionImage: "checkmark", discountedPrice: "$48.99", sizeSelectedIndex: 0, colorSelectedIndex: 0, itemSelectedIndex: 0),
Basket(elementID: 0, image: "Blouse", title: "Tuxedo Blouse", brand: "Lost Ink", price: "$50.00", selectionImage: "checkmark", discountedPrice: "$34.90", sizeSelectedIndex: 0, colorSelectedIndex: 0, itemSelectedIndex: 0),
Basket(elementID: 0, image: "Tee", title: "Linear Near Tee", brand: "Converse", price: "$28.50", selectionImage: "checkmark", discountedPrice: "$19.99", sizeSelectedIndex: 0, colorSelectedIndex: 0, itemSelectedIndex: 0)
]
}
That is my BasketCell
struct BasketListCell: View {
#State var isSelected: Bool = false
#State private var itemSelectedIndex : Int?
#State private var sizeSelectedIndex : Int?
#State private var colorSelectedIndex: Int?
var colors = ["Black", "Green", "Red", "Blue"]
var sizes = ["XS", "S", "M", "L", "XL", "XXL"]
var items = ["1", "2", "3", "4", "5", "6"]
var item: Basket
var body: some View {
HStack(spacing: 20) {
ZStack {
PickerTextField(data: sizes, placeHolder: "Size", lastSelectedIndex: self.$sizeSelectedIndex)
.overlay(
Image(systemName: "chevron.down")
)
}
.frame(width: UIScreen.main.bounds.width / (375/100), height: 44, alignment: .center)
ZStack{
PickerTextField(data: colors, placeHolder: "Color", lastSelectedIndex: self.$colorSelectedIndex)
.overlay(
Image(systemName: "chevron.down")
)
}
.frame(width: UIScreen.main.bounds.width / (375/100), height: 44, alignment: .center)
ZStack {
PickerTextField(data: items, placeHolder: "Item", lastSelectedIndex: self.$itemSelectedIndex)
.overlay(
Image(systemName: "chevron.down")
)
}
.frame(width: UIScreen.main.bounds.width / (375/100), height: 44, alignment: .center)
}
}
and finally, this is my ShoppingCartView
struct ShoppingCartView: View {
#State var selectedProduct = Basket(elementID: 0, image: "", title: "", brand: "", price: "", selectionImage: "", discountedPrice: "", sizeSelectedIndex: 0, colorSelectedIndex: 0, itemSelectedIndex: 0)
#State var shown: Bool = false
var body: some View {
ZStack {
Color(.white)
.edgesIgnoringSafeArea(.all)
VStack {
NavigationView {
ScrollView(.vertical, showsIndicators: false) {
VStack(spacing: 20) {
ForEach(BasketList.listData) { item in
BasketListCell(item: item)
.onTapGesture {
self.shown = true
self.selectedProduct = item
}
}
}
}
You can understand clearly my problem if you check those images better.
This is BasketListCell preview
That is my ShoppingCartView
elementID attribute of your Model class Basket needs to be unique. You currently have 3 Basket objects all with duplicate identifier, causing swiftUI to read first object every time. Changing it to some unique values should fix the problem.
Current-:
struct BasketList {
static let listData: [Basket] = [
Basket(elementID: 0, image: "Tee", title: "3-Stripes Shirt", brand: "Adidas Original", price: "$79.40", selectionImage: "checkmark", discountedPrice: "$48.99", sizeSelectedIndex: 0, colorSelectedIndex: 0, itemSelectedIndex: 0),
Basket(elementID: 0, image: "Blouse", title: "Tuxedo Blouse", brand: "Lost Ink", price: "$50.00", selectionImage: "checkmark", discountedPrice: "$34.90", sizeSelectedIndex: 0, colorSelectedIndex: 0, itemSelectedIndex: 0),
Basket(elementID: 0, image: "Tee", title: "Linear Near Tee", brand: "Converse", price: "$28.50", selectionImage: "checkmark", discountedPrice: "$19.99", sizeSelectedIndex: 0, colorSelectedIndex: 0, itemSelectedIndex: 0)
]
}
Fix-:
struct BasketList {
static let listData: [Basket] = [
Basket(elementID: 1, image: "Tee", title: "3-Stripes Shirt", brand: "Adidas Original", price: "$79.40", selectionImage: "checkmark", discountedPrice: "$48.99", sizeSelectedIndex: 0, colorSelectedIndex: 0, itemSelectedIndex: 0),
Basket(elementID: 2, image: "Blouse", title: "Tuxedo Blouse", brand: "Lost Ink", price: "$50.00", selectionImage: "checkmark", discountedPrice: "$34.90", sizeSelectedIndex: 0, colorSelectedIndex: 0, itemSelectedIndex: 0),
Basket(elementID: 3, image: "Tee", title: "Linear Near Tee", brand: "Converse", price: "$28.50", selectionImage: "checkmark", discountedPrice: "$19.99", sizeSelectedIndex: 0, colorSelectedIndex: 0, itemSelectedIndex: 0)
]
}