How to create "Masonry like" layout with SwiftUI2 grid? - swiftui

Is SwiftUI 2 grid system is able to render "Masonry like" layouts ?
A "Masonry" layout will look like this :
The remarkable feature of this layout is that content can span on multiple columns.
This does not seems automatically possible with LazyVGrid or LazyHGrid as they rely on GridItem which seems to describe a column (either fixed, flexible or adaptive).
If you think in term of column this design cannot be achieved.
Did i missed something or is it true we cannot make this kind of grid ?

I must admit that I haven't done much research for Lazy Grids yet, but as I saw some examples, I'm afraid that it will not be possible that way. But my programming mindset is: anything is possible. So let's make our own solution! Here's mine:
struct ContentView: View {
let items = (1 ... 12).map { "Item \($0)" }
var range: Range<Int> { 0 ..< Int((Double(items.count) / 3).rounded(.up)) }
var body: some View {
GeometryReader { geometry in
ScrollView {
LazyVStack { // still some laziness
ForEach(range, id: \.self) { index in
HStack(spacing: 0) {
if index % 2 == 0 {
Text(items[index * 3])
.frame(maxHeight: .infinity)
.frame(width: geometry.size.width * 2/3)
.background(Color(#colorLiteral(red: 0.3427395821, green: 0.7238617539, blue: 0.6179549098, alpha: 1)))
VStack(spacing: 0) {
Text(items[index * 3 + 1])
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color(#colorLiteral(red: 0.1351453364, green: 0.1446713805, blue: 0.2671209574, alpha: 1)))
Text(items[index * 3 + 2])
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color(#colorLiteral(red: 0.9248386621, green: 0.3957888484, blue: 0.3508865833, alpha: 1)))
}
.frame(maxHeight: .infinity)
.frame(width: geometry.size.width * 1/3)
} else {
VStack(spacing: 0) {
Text(items[index * 3])
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color(#colorLiteral(red: 0.1351453364, green: 0.1446713805, blue: 0.2671209574, alpha: 1)))
Text(items[index * 3 + 1])
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color(#colorLiteral(red: 0.9248386621, green: 0.3957888484, blue: 0.3508865833, alpha: 1)))
}
.frame(maxHeight: .infinity)
.frame(width: geometry.size.width * 1/3)
Text(items[index * 3 + 2])
.frame(maxHeight: .infinity)
.frame(width: geometry.size.width * 2/3)
.background(Color(#colorLiteral(red: 0.226172477, green: 0.3690122366, blue: 0.3273729682, alpha: 1)))
}
}
.frame(height: geometry.size.width * 6/16)
}
}
}
}
.foregroundColor(.white)
}
}
Explanation:
The line with let items = ... is just a quick way to generate a list with strings "Item 1" through "Item 12".
Let's consider each group of three items as a "row". Then the variable range will contain the number of rows.
For each row, we create an HStack. Inside it, there will be one big item and two smaller ones in a VStack. In the even rows (index % 2 == 0), the VStack will be on the right, in the odd rows, it will be on the left.
We use .frame(maxHeight: .infinity) to make the items fill the available space.
But how do we get the right item at the right place? Well, as you can see, I worked with index * 3, index * 3 + 1 and index * 3 + 2, where index is the row number. I will illustrate that with the table below:
+-------------------------------------------+-------------------------------------------+-------------------------------------------+-------------------------------------------+
| index = 0 | index = 1 | index = 2 | index = 3 |
+-----------+---------------+---------------+-----------+---------------+---------------+-----------+---------------+---------------+-----------+---------------+---------------+
| index * 3 | index * 3 + 1 | index * 3 + 2 | index * 3 | index * 3 + 1 | index * 3 + 2 | index * 3 | index * 3 + 1 | index * 3 + 2 | index * 3 | index * 3 + 1 | index * 3 + 2 |
| = 0 | = 1 | = 2 | = 3 | = 4 | = 5 | = 6 | = 7 | = 8 | = 9 | = 10 | = 11 |
+-----------+---------------+---------------+-----------+---------------+---------------+-----------+---------------+---------------+-----------+---------------+---------------+

Related

How to make swiftui lazyvgrid multiple columns

I want to make a lazyvgrid with multiple columns as bellow. Large size items can be aligned with two smaller sizes.
My code is as bellow:
struct ContentView: View {
let paddingLeft: CGFloat = 22.0
var body: some View {
let widgetWidth: CGFloat = 72.0
let widgetHeight: CGFloat = widgetWidth
let col: Int = 4
let widgetGap: CGFloat = floor((UIScreen.main.bounds.size.width - paddingLeft * 2 - widgetWidth * CGFloat(col)) / CGFloat(col - 1))
let widgetWidthx2: CGFloat = widgetWidth * 2 + widgetGap
let colums1 = [GridItem(.adaptive(minimum: widgetWidth), spacing: widgetGap)]
LazyVGrid(columns: colums1, spacing: widgetGap){
ForEach(0 ..< 11){ idx in
RoundedRectangle(cornerRadius: 5)
.foregroundColor(Color(hue: 0.1 * Double(idx) , saturation: 1, brightness: 1))
.frame(width: idx == 4 ? widgetWidthx2 : widgetWidth, height: widgetHeight)
}
}
.padding(.horizontal, paddingLeft)
}
}
However, the result was not as expected:
Is there any way to make swiftui lazyvgrid multiple columns?
I find a solution:
struct ContentView: View {
let paddingLeft: CGFloat = 22.0
var body: some View {
let widgetWidth: CGFloat = 72.0
let widgetHeight: CGFloat = widgetWidth
let col: Int = 4
let widgetGap: CGFloat = floor((UIScreen.main.bounds.size.width - paddingLeft * 2 - widgetWidth * CGFloat(col)) / CGFloat(col - 1))
let widgetWidthx2: CGFloat = widgetWidth * 2 + widgetGap
let colums1 = [GridItem(.adaptive(minimum: widgetWidth), spacing: widgetGap, alignment: .leading)] //-- here alignment: .leading
LazyVGrid(columns: colums1, spacing: widgetGap){
ForEach(0 ..< 11){ idx in
RoundedRectangle(cornerRadius: 5)
.foregroundColor(Color(hue: 0.1 * Double(idx) , saturation: 1, brightness: 1))
.frame(width: idx == 4 ? widgetWidthx2 : widgetWidth, height: widgetHeight)
if idx == 4 {
Color.clear
}
}
}
.padding(.horizontal, paddingLeft)
}
}

Position of ScrollView not consistent in main view

I have a scrollview with an HStack inside it that is sized according to how many hexagons are to be placed in it. There are three possible values - 25, 64 and 100. The scrollview is positioned correctly for 25 and 64 hexagons, but is shifted to the right for 100. How can I make sure that is centred correctly too?
Incidentally, I have already tried manually setting the position of the scrollview - it worked the same as without.
GeometryReader
{ scrollViewGeometry in
ScrollView([.vertical, .horizontal], showsIndicators: true)
{
Spacer()
HStack(spacing:-(max(minHexagonSize, smallSide)))
{
ForEach(viewModel.hexagons){hexagon in
let hexagonFrame = getFrameFor(hexagon: hexagon, minWidth: smallSide)
let colorArray = getColorsFor(hexagon: hexagon)
ZStack()
{
let thisIndex = hexagon.id
HexagonView(strokeColor: colorArray[0]).onTapGesture(perform: {
viewModel.chooseHexagon(hexagon: hexagon)
})
.scaleEffect(hexagon.pressed ? 1.2 : 1.0)
.animation(Animation.easeInOut(duration: 0.2))
if hexagon.isSelected
{
Circle().fill(playerColorArray[hexagon.playerIndex]).frame(width: hexagonFrame.width * fontScalingFactor, height: hexagonFrame.height * fontScalingFactor, alignment: .center)
Circle().stroke(Color.black).frame(width: hexagonFrame.width * fontScalingFactor, height: hexagonFrame.height * fontScalingFactor, alignment: .center)
}
else
{
Text(verbatim: String(thisIndex+1)).font(Font.system(size: hexagonFrame.size.width * fontScalingFactor)).foregroundColor(colorArray[1])
.onTapGesture(perform: {
Sounds.playSounds(soundName: "pop")
viewModel.chooseHexagon(hexagon: hexagon)
})
.scaleEffect(hexagon.pressed ? 1.2 : 1.0)
.animation(Animation.easeIn(duration: 0.5))
}
if hexagon.isSelected && hexagon.pressed
{
Text(String(viewModel.currentScore))
.foregroundColor(Color.black)
.font(Font.system(size: hexagonFrame.size.width * fontScalingFactor)).foregroundColor(colorArray[1])
.transition(AnyTransition.asymmetric(insertion: .identity, removal: AnyTransition.move(edge: .top).combined(with: .opacity).animation(.easeIn(duration: 1.0))))
.animation(.easeIn(duration: 1.0))
.zIndex(hexagon.isSelected ? 1 : 0)
}
}
.position(x: hexagonFrame.origin.x - (scrollViewGeometry.size.width/2), y: scrollViewGeometry.size.height/2 - hexagonFrame.origin.y)
.frame(width: hexagonFrame.size.width, height: hexagonFrame.size.height)
.zIndex(hexagon.isSelected ? 1 : 0)
}
}
.frame(width: (max(minHexagonSize, smallSide)) * (sqrt(CGFloat(viewModel.hexagons.count)) * 2), height: (max(minHexagonSize, smallSide) * (sqrt(CGFloat(viewModel.hexagons.count)) + 2)))
//.position(x: scrollViewGeometry.size.width, y: scrollViewGeometry.size.height)
}
.padding()
}
OK... so it turns out that the reason it was not centred, is because I was looking at the wrong part of the view. I still don't understand why it worked fine for the smaller scrollable areas in the scrollview, but not for the larger one!

How to move View in HStack to the top of the other views

I have a set of subviews that are arranged within an HStack. I have a transition that is run whenever a view is selected successfully, but it is occluded by neighbouring views "above" it.
I found this article here, but I can't see how to integrate it into my solution:
How to bring a view on top of VStack containing ZStacks
Is there a way of getting the selected view to pop on top of the others?
HStack(spacing: -75)
{
ForEach(viewModel.hexagons){hexagon in
let hexagonFrame = getFrameFor(hexagon: hexagon, minWidth: smallSide)
let colorArray = getColorsFor(hexagon: hexagon)
ZStack()
{
let thisIndex = hexagon.id
HexagonView(strokeColor: colorArray[0]).onTapGesture(perform: {
viewModel.chooseHexagon(hexagon: hexagon)
})
.scaleEffect(hexagon.pressed ? 1.2 : 1.0)
.animation(Animation.easeInOut(duration: 0.2))
if hexagon.isSelected
{
Circle().fill(playerColorArray[hexagon.playerIndex]).frame(width: hexagonFrame.width * fontScalingFactor, height: hexagonFrame.height * fontScalingFactor, alignment: .center)
Circle().stroke(Color.black).frame(width: hexagonFrame.width * fontScalingFactor, height: hexagonFrame.height * fontScalingFactor, alignment: .center)
}
else
{
Text(verbatim: String(thisIndex+1)).font(Font.system(size: hexagonFrame.size.width * fontScalingFactor)).foregroundColor(colorArray[1])
.onTapGesture(perform: {
viewModel.chooseHexagon(hexagon: hexagon)
})
.scaleEffect(hexagon.pressed ? 1.2 : 1.0)
.animation(Animation.easeIn(duration: 0.5))
}
This is the view that animates up from the haxagon that has been selected
if hexagon.isSelected && hexagon.pressed
{
Text(String(viewModel.currentScore))
.foregroundColor(Color.gray)
.font(Font.system(size: hexagonFrame.size.width * fontScalingFactor)).foregroundColor(colorArray[1])
.transition(AnyTransition.asymmetric(insertion: .identity, removal: AnyTransition.move(edge: .top)))
.animation(.easeIn(duration: 2.0))
.zIndex(.greatestFiniteMagnitude)
}
}
.position(x: hexagonFrame.origin.x + hexagonFrame.size.width / 3 - (scrollViewGeometry.size.width/2), y: (scrollViewGeometry.size.height/2) - hexagonFrame.origin.y)
.frame(width: hexagonFrame.size.width, height: hexagonFrame.size.height, alignment:.center)
}
}.frame(width: (max(75, smallSide) * 8), height: (max(75, smallSide) * 5))

Using geometry reader to Center the Rectangle

I'm trying to use GeometryReader to make a Card View (to use in a card game).
This Card is going to have 3 Shapes on it.
I'm trying to center these 3 Shapes using GeometryReader (starting with the first shape, Rectangle), but it doesn't work.
What am I doing wrong?
Here's how I WANT it to look like: Here's how I want it to look like
Here's how it ACTUALLY looks like: Here's how it actually looks like
struct Card: View {
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 10.0).stroke(lineWidth: 3)
VStack {
GeometryReader { geometry in
Rectangle()
.size(
width: geometry.size.width * 0.75,
height: geometry.size.height * 0.75
)
.position(
x: geometry.size.width / 2,
y: geometry.size.height / 2
)
}
GeometryReader { geometry in
Circle()
.size(
width: geometry.size.width * 0.75,
height: geometry.size.height * 0.75
)
.position(
x: geometry.size.width / 2,
y: geometry.size.height / 2
)
}
GeometryReader { geometry in
Rectangle()
.size(
width: geometry.size.width * 0.75,
height: geometry.size.height * 0.75
)
.position(
x: geometry.size.width / 2,
y: geometry.size.height / 2
)
}
}
}
.foregroundColor(Color.orange)
}
}
Here is possible variant of layout. Tested with Xcode 12 / iOS 14
struct Card3: View {
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 10.0).stroke(lineWidth: 3)
GeometryReader { geometry in
VStack {
Color.clear.overlay(Rectangle()
.frame(
width: geometry.size.width * 0.75,
height: geometry.size.height / 3 * 0.75
))
Color.clear.overlay(Circle()
.frame(
width: geometry.size.width * 0.75,
height: geometry.size.height / 3 * 0.75
))
Color.clear.overlay(Rectangle()
.frame(
width: geometry.size.width * 0.75,
height: geometry.size.height / 3 * 0.75
))
}.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
.foregroundColor(Color.orange)
}
}

SwiftUI: What is the Proper Logic Statement to Prevent Views From Disappearing While Using a Custom Slider

My SwiftUI Views are disappearing when I am using a custom made slider. I believe it is because the frame values of the Views are becoming negative; however, I feel that I have coded the logic correctly to prevent this from happening (but something is wrong).
I have tried several different if statements to prevent the values from becoming negative but am having no luck.
import SwiftUI
struct ContentView: View {
#State var scale: CGFloat = 1.0
#State private var dragged = CGSize.zero
#State private var accumulated = CGSize.zero
var body: some View {
ZStack {
Rectangle()
.stroke()
.frame(width: 300, height: 5, alignment: .leading)
.position(x: 180, y: 30)
Rectangle()
.frame(width: 0 + dragged.width, height: 5, alignment: .leading)
.position(x: 30 + (0.5 * dragged.width), y: 30)
Circle()
.foregroundColor(.red)
.frame(width: 20, height: 20).border(Color.red)
.position(x: 30, y: 30)
.offset(x: self.dragged.width)
.gesture(DragGesture()
.onChanged{ value in
//My IF-Statement
if value.translation.width > 0 {
self.dragged = CGSize(
width: value.translation.width + self.accumulated.width,
height: value.translation.height + self.accumulated.height
)
}
}
.onEnded{ value in
self.dragged = CGSize(
width: value.translation.width + self.accumulated.width,
height: value.translation.height + self.accumulated.height)
self.accumulated = self.dragged
}
)
VStack{
Text("\(dragged.width)")
Text("\(30 + (0.5 * dragged.width))")
}
}
}
}
The error occurs when I slide the red circle too far to the left very quickly. I am expecting the IF statement to stop the circle before it makes the variable negative (if my assumption is correct about what is making it disappear).
Limit to the right
You don't need an if statement, but rather a maximum the width can go. One way to do it to use min(maxValue, calculated) to enforce the width to not go beyond maxValue.
Limit to the left
Your drag starts at the red dot, but dragging is not limited to your drawn scale, but you can rather drag to the left of it since the screen continues until the screen edge, which makes drags to the left go negative.
To avoid it you can check whether the result is negative and if so, just use 0 instead of the negative value, like below.
import SwiftUI
struct SliderView: View {
#State var scale: CGFloat = 1.0
#State private var dragged = CGSize.zero
#State private var accumulated = CGSize.zero
let maxWidth: CGFloat = 300
var body: some View {
ZStack {
Rectangle()
.stroke()
.frame(width: maxWidth, height: 5, alignment: .leading)
.position(x: 180, y: 30)
Rectangle()
.frame(width: 0 + dragged.width, height: 5, alignment: .leading)
.position(x: 30 + (0.5 * dragged.width), y: 30)
Circle()
.foregroundColor(.red)
.frame(width: 20, height: 20).border(Color.red)
.position(x: 30, y: 30)
.offset(x: self.dragged.width)
.gesture(DragGesture()
.onChanged{ value in
print("on changed called. width: \(value.translation.width)")
self.dragged = CGSize(
width: value.translation.width + self.accumulated.width > 0 ?
min(self.maxWidth, value.translation.width + self.accumulated.width) : 0,
height: value.translation.height + self.accumulated.height
)
}
.onEnded{ value in
self.dragged = CGSize(
width: value.translation.width + self.accumulated.width > 0 ?
min(self.maxWidth, value.translation.width + self.accumulated.width) : 0,
height: value.translation.height + self.accumulated.height)
self.accumulated = self.dragged
}
)
VStack{
Text("\(dragged.width)")
Text("\(30 + (0.5 * dragged.width))")
}
}
}
}