I have been experimenting with LazyVGrid to produce a table with grid lines. I'm using XCode 13.4 & iOS15. Everything seemed to be going well until I parameterized all the column sizes, line widths, etc. The content consists of an array of row data with 5 pieces of text in each row. Here is the code:
struct StatisticsView: View {
private let columnMinWidths: [CGFloat] = [120, 50, 50, 50, 50]
private let rowHeight: CGFloat = 40
private let thinGridlineSize: CGFloat = 0.5
private let thickGridlineSize: CGFloat = 2
private let lineColour = Color.accentColor
private let columns: [GridItem]
private let rowContents = [
["Player's name", "Graham", "John", "Archie", ""],
["Round 1", "2", "3", "1", ""],
["Round 2", "3", "-", "2", ""],
["Round 3", "1", "2", "4", ""],
["Round 4", "2", "1", "1", ""]
]
init() {
self.columns = [
GridItem(.fixed(thickGridlineSize), spacing: 0),
GridItem(.flexible(minimum: columnMinWidths[0]), spacing: 0),
GridItem(.fixed(thickGridlineSize), spacing: 0),
GridItem(.flexible(minimum: columnMinWidths[1]), spacing: 0),
GridItem(.fixed(thinGridlineSize), spacing: 0),
GridItem(.flexible(minimum: columnMinWidths[2]), spacing: 0),
GridItem(.fixed(thinGridlineSize), spacing: 0),
GridItem(.flexible(minimum: columnMinWidths[3]), spacing: 0),
GridItem(.fixed(thinGridlineSize), spacing: 0),
GridItem(.flexible(minimum: columnMinWidths[4]), spacing: 0),
GridItem(.fixed(thickGridlineSize), spacing: 0),
]
}
var body: some View {
VStack(spacing: 0) {
// top horizontal gridline
lineColour.frame(height: thickGridlineSize)
LazyVGrid(columns: columns, alignment: .leading, spacing: 0) {
ForEach(0..<rowContents.count, id: \.self) { row in
lineColour.frame(height: rowHeight)
ForEach(0..<rowContents.first!.count, id: \.self) { col in
// cell contents with bottom gridline
VStack(spacing: 0) {
Text(rowContents[row][col])
.font(.caption2)
.frame(height: rowHeight - thinGridlineSize)
// tried using a colour fill but same result
// Color.green
// .frame(height: rowHeight - thinGridlineSize)
lineColour.frame(height: thinGridlineSize)
}
// vertical gridline between cells
lineColour.frame(height: rowHeight)
}
}
}
// bottom horizontal gridline
lineColour.frame(height: thickGridlineSize)
}
.padding(.horizontal, 8)
}
}
and here is what is being displayed on the simulator screen:
Can anyone see where I'm going wrong here? Any help would be much appreciated. Thanks.
You have to remove id: \.self from both loops. View Id become duplicated so it's not drawing more than one line.
Related
I have an app in which the user may select a SF icon from those within a LazyHGrid. I would like to add a shadow around the selected icon and remove the shadow when deselected.
Currently, the working code below may be used to scroll the available icons and select an icon by tapping. I need help changing the view to support applying shadow to the selected element.
I tried placing the same image() and modifiers within the button action but got a Xcode warning that the ZStack initializer is unused. I also tried adding a shadow modifier to the view changing the shadow parameters with state properties set in the button action area. This applied shadow to all elements in LazyHGrid. I want the shadow applied only to the selected element.
struct ImageStore: Identifiable, Hashable {
var iconName: String
var id: Int
}
struct ContentView: View {
let rows = [
GridItem(.flexible()),
]
let colors: [Color] = [.green, .red, .yellow, .blue]
let imageName = [
ImageStore(iconName: "a.square.fill", id: 0),
ImageStore(iconName: "b.square.fill", id: 1),
ImageStore(iconName: "c.square.fill", id: 2),
ImageStore(iconName: "d.square.fill", id: 3),
ImageStore(iconName: "e.square.fill", id: 4),
ImageStore(iconName: "f.square.fill", id: 5),
ImageStore(iconName: "g.square.fill", id: 6),
]
#State private var selectedIcon: Int = 0
var body: some View {
VStack {
ScrollView (.horizontal) {
LazyHGrid( rows: rows, spacing: 20) {
ForEach(imageName, id: \.self) { image in
Button( action: {
selectedIcon = image.id
print("image name = \(image.iconName)")
print("id = \(image.id)")
print("selectedIcon = \(selectedIcon)")
}){
Image(systemName: image.iconName)
.font(.largeTitle)
.foregroundColor(.white)
.padding()
.background(colors[image.id % colors.count])
}
}
}
}
}
}
}
Perhaps I'm not understanding your question fully, but it should be as simple as using the .shadow modifier with a ternary expression, e.g.
.shadow(radius: selectedIcon == image.id ? 5 : 0)
to make sure the image doesn't have it's own shadow in addition to the background, add a .drawingGroup modifier, e.g
Button {
selectedIcon = image.id
} label: {
Image(systemName: image.iconName)
.font(.largeTitle)
.foregroundColor(.white)
.padding()
.background(colors[image.id % colors.count])
.drawingGroup()
.shadow(radius: selectedIcon == image.id ? 5 : 0)
}
I am trying to change the color programmatically of some buttons in SwiftUI.
The buttons are stored in a LazyVGrid. Each button is built via another view (ButtonCell).
I'm using a #State in the ButtonCell view to check the button state.
If I click on the single button, his own state changes correctly, just modifying the #State var of the ButtonCell view. If I try to do the same from the ContentView nothing is happening.
This is my whole ContentView (and ButtonCell) view struct:
struct ContentView: View {
private var gridItemLayout = [GridItem(.adaptive(minimum: 30))]
var body: some View {
let columns = [
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible())
]
ScrollView {
LazyVGrid(columns: columns, spacing: 0) {
ForEach(0..<10) { number in
ButtonCell(value: number + 1)
}
}
}
Button(action: {
ButtonCell(value: 0, isEnabled: true)
ButtonCell(value: 1, isEnabled: true)
ButtonCell(value: 1, isEnabled: true)
}){
Rectangle()
.frame(width: 200, height: 50)
.cornerRadius(10)
.shadow(color: .black, radius: 3, x: 1, y: 1)
.padding()
.overlay(
Text("Change isEnabled state").foregroundColor(.white)
)
}
}
struct ButtonCell: View {
var value: Int
#State var isEnabled:Bool = false
var body: some View {
Button(action: {
print (value)
print (isEnabled)
isEnabled = true
}) {
Rectangle()
.foregroundColor(isEnabled ? Color.red : Color.yellow)
.frame(width: 50, height: 50)
.cornerRadius(10)
.shadow(color: .black, radius: 3, x: 1, y: 1)
.padding()
.overlay(
Text("\(value)").foregroundColor(.white)
)
}
}
}
}
How I may change the color of a button in the LazyVGrid by clicking the "Change isEnabled state" button?
You need a different approach here. Currently you try to change the State of ButtonCell from the outside. State variables should always be private and therefore should not be changed from outside. You should swap the state and parameters of ButtonCell into a ViewModel. The ViewModels then are stored in the parent View (ContentView) and then you can change the ViewModels and the child views automatically update. Here is a example for a ViewModel:
final class ButtonCellViewModel: ObservableObject {
#Published var isEnabled: Bool = false
let value: Int
init(value: Int) {
self.value = value
}
}
Then store the ViewModels in the ContentView:
struct ContentView: View {
let buttonViewModels = [ButtonCellViewModel(value: 0), ButtonCellViewModel(value: 1), ButtonCellViewModel(value: 2)]
private var gridItemLayout = [GridItem(.adaptive(minimum: 30))]
var body: some View {
let columns = [
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible())
]
ScrollView {
LazyVGrid(columns: columns, spacing: 0) {
ForEach(0..<3) { index in
ButtonCell(viewModel: buttonViewModels[index])
}
}
}
Button(action: {
buttonViewModels[0].isEnabled.toggle()
}){
Rectangle()
.frame(width: 200, height: 50)
.cornerRadius(10)
.shadow(color: .black, radius: 3, x: 1, y: 1)
.padding()
.overlay(
Text("Change isEnabled state").foregroundColor(.white)
)
}
}
}
And implement the ObservedObject approach in ButtonCell.
struct ButtonCell: View {
#ObservedObject var viewModel: ButtonCellViewModel
var body: some View {
Button(action: {
print (viewModel.value)
print (viewModel.isEnabled)
viewModel.isEnabled = true
}) {
Rectangle()
.foregroundColor(viewModel.isEnabled ? Color.red : Color.yellow)
.frame(width: 50, height: 50)
.cornerRadius(10)
.shadow(color: .black, radius: 3, x: 1, y: 1)
.padding()
.overlay(
Text("\(viewModel.value)").foregroundColor(.white)
)
}
}
}
I've set the spacing on the LazyHGrid and the GridItems to 0, yet there is still spacing in the vertical direction. Why is that? How do I eliminate that spacing?
struct ContentView: View {
let strs: [String] = ["aa", "", "bbbb"]
let hgRows: [GridItem] = [GridItem(.fixed(60), spacing: 0), GridItem(.fixed(60), spacing: 0)]
var body: some View {
VStack(spacing: 0) {
Text("Stuff at top").padding().frame(maxWidth: .infinity).background(Color.blue.opacity(0.5))
Divider()
ScrollView(.horizontal) {
LazyHGrid(rows: hgRows, spacing: 0) {
ForEach(1..<10) { i in
Text("B\(i).\(strs[i % strs.count])")
.padding().border(Color.gray, width: 1)
}
}
.padding(0)
.background(Color.green.opacity(0.5)) // Give HGrid a green background so I can see where it is.
}.layoutPriority(1)
Divider()
Spacer().layoutPriority(2)
}
}
}
By adjusting the GridItems, it's possible to eliminate the vertical space between items:
let hgRows: [GridItem] = [GridItem(.fixed(51), spacing: 0), GridItem(.fixed(51), spacing: 0)]
It clearly isn't the best solution, but that's how I fixed it. I hope this answers your question.
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 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)
}
}
}
}