I am just a hobbyist trying to learn swiftui so I may not be even asking the right questions but here is the view I created to build a basic square grid that could be used for checkers. It works exactly as I would like but now I'm trying to add gestures.
Issue 1: On the main VStack under the .onTapGesture the "if x != nil { print(x!) } " correctly prints out the square that was tapped on. How do I get that info back to the view that called it or get it to change data in my Model to register the selection and/or trigger the event to highlight the square? This struct is in its own file. Do I have to put this entire struct inside my main struct for this to work? I feel like I'm missing something conceptually. I can trigger the highlight from subviews that are part of the bigger view with a button but can't figure out what the approach is for "generic views" outside the main struct.
Issue 2: it seems to lump all of the imageItems together for the .gesture(dragGesture). I can only drag by clicking the last item in the grid and as I move it around, all imageItems move as one. I feel like I read a stack overflow that addressed this before but can't find it now.
PS if you just feel like teaching a little, lol, any changes you would recommend?
import SwiftUI
struct FixedSquareGrid<Item: Identifiable, ItemView: View>: View {
let lightSquare: String
let darkSquare: String
@State private var translation = CGSize.zero
@State private var lastTranslation = CGSize.zero
var items: [Item]
@ViewBuilder var content: (Item) -> ItemView
init(_ items: [Item], lightSquare: String, darkSquare: String, @ViewBuilder content: @escaping (Item) -> ItemView) {
self.items = items
self.content = content
self.lightSquare = lightSquare
self.darkSquare = darkSquare
}
var body: some View {
GeometryReader { geo in
let numberOfRows: Int = Int(sqrt(Double(items.count)))
let gridItemSize = gridItemWidthThatFits(
count: numberOfRows,
size: geo.size)
let boardSize = maxBoardSize(
boardSquaresSize: gridItemSize * CGFloat(numberOfRows),
size: geo.size)
VStack(spacing: 0) {
ForEach (0..<numberOfRows, id: \.self) { row in
HStack(spacing: 0) {
ForEach (0..<numberOfRows, id: \.self) { col in
let itemImage = content(items[((row * numberOfRows) + col)])
ZStack {
Image(((row % 2) + col) % 2 == 0 ? lightSquare : darkSquare)
.padding(0)
.frame(width: gridItemSize , height: gridItemSize)
.clipped()
itemImage
.padding(.bottom, 1)
.frame(width: gridItemSize, height: gridItemSize)
.clipped()
.gesture(dragGesture)
.offset(
x: lastTranslation.width + translation.width,
y: lastTranslation.height + translation.height
)
}
.border(Color.black, width: 1)
}
}
}
}
.frame(width: boardSize + 10, height: boardSize + 10 )
.background(Color.black)
.onTapGesture( perform: { location in
let x = squareTapped(clickedAt: location, itemSize: gridItemSize, rowCount: numberOfRows)
if x != nil { print(x!) }
})
}.aspectRatio(1, contentMode: .fit)
}
func maxBoardSize(boardSquaresSize: CGFloat, size: CGSize) -> CGFloat {
let maxSquareSpace = (size.width <= size.height ? size.width : size.height)
return (maxSquareSpace <= boardSquaresSize ? maxSquareSpace : boardSquaresSize)
}
func gridItemWidthThatFits(
count: Int,
size: CGSize
) -> CGFloat {
let rowCount = CGFloat(count)
let width = (size.width / rowCount)
let height = (size.height / rowCount)
return (width <= height ? width : height).rounded(.down)
}
var dragGesture: some Gesture {
DragGesture()
.onChanged { value in
translation = value.translation
}
.onEnded { value in
lastTranslation.width += value.translation.width
lastTranslation.height += value.translation.height
translation = .zero
}
}
func squareTapped(clickedAt: CGPoint, itemSize: CGFloat, rowCount: Int) -> Int? {
let col = Int((clickedAt.x / itemSize).rounded(.down))
let row = Int((clickedAt.y / itemSize).rounded(.down))
let sqr = col + (rowCount * row)
let inBoundsSquare: Int? = (sqr > 0 && sqr <= (rowCount * rowCount) - 1 ) ? sqr : nil
return inBoundsSquare
}
}
The FixedSquareGrid
can just take an extra (Item) -> Void
function so that super views can detect that an item is tapped.
let items: [Item]
@ViewBuilder var content: (Item) -> ItemView
let itemTapped: (Item) -> Void // extra property here
init(
_ items: [Item],
lightSquare: String,
darkSquare: String,
@ViewBuilder content: @escaping (Item) -> ItemView,
itemTapped: @escaping (Item) -> Void // extra argument here
) {
self.items = items
self.content = content
self.itemTapped = itemTapped
self.lightSquare = lightSquare
self.darkSquare = darkSquare
}
Then you can call this in onTapGesture
:
.onTapGesture() { location in
if let index = squareTapped(clickedAt: location, itemSize: gridItemSize, rowCount: numberOfRows) {
itemTapped(items[index])
}
}
Usage would look something like this:
struct ContentView: View {
var body: some View {
FixedSquareGrid(
[ /* insert your items here */ ],
lightSquare: "...", darkSquare: "..."
) { item in
// ...
} itemTapped: { item in
// handle tap here...
}
}
}
Also consider putting the tap gesture on each of the ZStack
s, so that you don't have to calculate which square is tapped using the squareTapped
method.
As for the drag gesture, you only declared one set of states (translation
and lastTranslation
) for all the grid items, so of course they all move together.
Each draggable thing should have its own pair of translation
and lastTranslation
states. You can make a new View
or ViewModifier
that contains these states.
// the way you implemented the drag gesture is kind of weird
// I changed it to an implementation similar to the one here:
// https://sarunw.com/posts/move-view-around-with-drag-gesture-in-swiftui/
struct DraggableModifier: ViewModifier {
@State private var offset = CGSize.zero
@GestureState private var startOffset: CGSize? = nil
var drag: some Gesture {
DragGesture()
.onChanged { value in
var newOffset = startOffset ?? offset
newOffset.width += value.translation.width
newOffset.height += value.translation.height
self.offset = newOffset
}.updating($startOffset) { (value, startOffset, transaction) in
startOffset = startOffset ?? offset
}
}
func body(content: Content) -> some View {
content
.offset(offset)
.gesture(drag)
}
}
itemImage
.padding(.bottom, 1)
.frame(width: gridItemSize, height: gridItemSize)
.clipped()
.modifier(DraggableModifier())