Make 2x2 grid based on screen width - swiftui

Sorry if this is a simple question but I'm just starting out with SwiftUI and I'm trying to figure out how to make a 2x2 grid of views based on the width of the screen. Meaning that each square has a width and height of have the screen width and they are arranged in a 2x2 grid with no padding.
I've been trying with two HStacks with two views in each placed on top of each other but the view size inside the HStack seems to dictate the HStack's height.
Code for Views I'm trying to arrange into the 2x2 grid:
var body: some View {
VStack {
TextField("0", text: $value)
.multilineTextAlignment(.center)
.textFieldStyle(PlainTextFieldStyle())
.font(.system(size: 40, weight: .semibold, design: .rounded))
.border(Color.black)
.padding()
Text(title)
.padding([.leading, .bottom, .trailing])
.font(.system(size: 14, weight: .regular, design: .rounded))
}
.background(Color.green)
.cornerRadius(10)
.overlay(RoundedRectangle(cornerRadius: 10).stroke(Color.black, lineWidth: 5))
}

I find that using A combination of HStacks, VStacks and aspectRatio(_ aspectRatio: CGFloat? = nil, contentMode: ContentMode) works best for forcing certain proportions on views:
struct ContentView: View {
var body: some View {
VStack(spacing: 0) {
HStack(spacing: 0) {
Rectangle().fill(Color.red)
.aspectRatio(1.0, contentMode: .fit)
Rectangle().fill(Color.green)
.aspectRatio(1.0, contentMode: .fill)
}
HStack(spacing: 0) {
Rectangle().fill(Color.blue)
.aspectRatio(1.0, contentMode: .fit)
Rectangle().fill(Color.yellow)
.aspectRatio(1.0, contentMode: .fill)
}
}.aspectRatio(contentMode: .fit)
}
}
Results in a layout like this:

This can be done using a GeometryReader:
struct ContentView: View
{
var body: some View
{
GeometryReader
{ geometry in
self.useProxy(geometry)
}
}
func useProxy(_ geometry: GeometryProxy) -> some View
{
let dimension = min(geometry.size.width, geometry.size.height)
return VStack
{
HStack(spacing: 0)
{
Text("Top Left")
.frame(width: dimension / 2, height: dimension / 2)
.border(Color.black)
Text("Top Right")
.frame(width: dimension / 2, height: dimension / 2)
.border(Color.black)
}
HStack(spacing: 0)
{
Text("Bottom Left")
.frame(width: dimension / 2, height: dimension / 2)
.border(Color.black)
Text("Bottom Right")
.frame(width: dimension / 2, height: dimension / 2)
.border(Color.black)
}
}
}
}
Watch (the first part of) this WWDC video to hear about the layout system of SwiftUI.

Related

How can I edit alignment and spacing in SwiftUI for images and text?

I would like the Image "Astronaut Meditation (Traced)" and the text "Levitate" to be aligned as in the prototype image, which is in the dimensions of an iPhone 13 pro max. How I want it to look
Here is my ContentView code:
import SwiftUI
struct ContentView: View {
var body: some View {
ZStack
{
//Background
Image("Background")
.edgesIgnoringSafeArea([.top])
**//Meditating Astronaut
Image("Astronaut Meditaton (Traced)")
.position(x: 102, y: 106)
//Levitate
Text("Levitate")
.font(.system(size: 34, weight: .semibold, design: .rounded))
.foregroundColor(Color.white)
.position(x: 110, y: 386)**
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I have included an image of how it is previewed on Xcode:
How it looks in XCode
Do not use static value alignment like position or offset because it’s not stable for all devices, instead just wrap your content inside VStack{} then you will achieve your desired design.
ZStack {
//Background
Image("Background")
.edgesIgnoringSafeArea([.top])
VStack { //start wrapping after background color image
//Meditating Astronaut
Image("Astronaut Meditaton (Traced)")
//Levitate
Text("Levitate")
.font(.system(size: 34, weight: .semibold, design: .rounded))
.foregroundColor(Color.white)
}
}
Like #tail said, you should avoid absolute position & offset, since it will vary from device to device. But if you do want to use them for some reason, you can add a alignment parameter to the ZStack and then use offset to get the desired outcome.
import SwiftUI
struct ContentView: View {
var body: some View {
ZStack(alignment: .center) {
//Background
Image("Background")
.ignoresSafeArea(edges: [.top])
//Meditating Astronaut
Image(systemName: "gear")
.resizable()
.frame(width: 150, height: 150)
.offset(y: -150)
//Levitate
Text("Levitate")
.font(.system(size: 34, weight: .semibold, design: .rounded))
.offset(y: -50)
}
}
}
Even better way would be to use GeometryReader and work with relative values to the screen size:
import SwiftUI
struct ContentView: View {
var body: some View {
GeometryReader { geometry in
ZStack(alignment: .center) {
//Background
Image("Background")
.ignoresSafeArea(edges: [.top])
//Meditating Astronaut
Image(systemName: "gear")
.resizable()
.frame(width: 150, height: 150)
.position(x: geometry.size.width / 2, y: geometry.size.height / 5)
//Levitate
Text("Levitate")
.font(.system(size: 34, weight: .semibold, design: .rounded))
// Position with offsetting by the size of image.
// Image frame 150 -> half is 75 + 25 for padding.
.position(x: geometry.size.width / 2, y: geometry.size.height / 5 + 100)
}
.frame(width: geometry.size.width, height: geometry.size.height)
}
}
}
With GeometryReader I would use absolute position and calculate the position of image with the relative values and then use the image size in addition to position the text
Use below code to achieve your actual desire design. Wrap your content in VStack and also add frame to background image for bind in your whole device with device width and height.
And also add Spacer() in VStack to occupy remains bottom space(For move view to up side).
ZStack {
//Background
Image("Background")
.resizable()
.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height) //Here add frame to background image
.edgesIgnoringSafeArea(.all)
VStack { //Wrap your view in VStack
//Meditating Astronaut
Image("Astronaut Meditaton (Traced)")
.resizable()
.frame(width: 100, height: 100)
.padding(.top,20)
//Levitate
Text("Levitate")
.font(.system(size: 34, weight: .semibold, design: .rounded))
.foregroundColor(Color.white)
Spacer() //Add Spacer to move view to upper side.
}.padding(.top,UIApplication.shared.windows.first?.safeAreaInsets.top ?? 0 + 150) //Add padding with safe area to top side for upper space(suitable for all devices).
}

HStack in VStack How can I force background color + get rid of dividing lines?

I don't want the behavior I'm getting with this SwiftUI thing (first time messing with it). I've been putting .background() on everything and there's some kind of padding happening and some sort of dividing line, whether I enable the Button code or not (pic below is with Button code commented out).
What do I need to do to fix it?
var body: some View {
ZStack() {
Color.black
.ignoresSafeArea(.all)
VStack(alignment: .leading, spacing: 0) {
List(eventFields) { eventField in
HStack() {
Spacer(minLength: 10)
if let iconName = eventField.iconName {
Button(action: {
print("edit \(eventField.name)")
}, label: {
Image(uiImage: UIImage(named: iconName)!.colorizeMask(eventField.iconColor!))
.frame(width: 27, height: 27)
self.background(.black)
}).background(.black)
} else {
Text("")
.frame(width: 27)
}
Text(eventField.iconName == nil ? "" : eventField.name)
.font(.system(size: eventField.labelFontSize))
.foregroundColor(eventField.labelFontColor)
.frame(width: 50, alignment: .trailing)
Spacer(minLength: 3)
Text(eventField.stringValue)
.font(.system(size: eventField.fontSize))
.foregroundColor(eventField.fontColor)
.frame(width: 200, alignment: .leading)
Spacer(minLength: 10)
}.background(.black)
}.background(.black)
}
}
}
}
instead of putting .background on the HStack, use
.listRowBackground(Color.black)
and for separator use
.listRowSeparator(.hidden)
Keep in mind, this is on the HStack not the List
Full Code:
var body: some View {
ZStack() {
Color.black
.ignoresSafeArea(.all)
VStack(alignment: .leading, spacing: 0) {
List(eventFields) { eventField in
HStack() {
Spacer(minLength: 10)
if let iconName = eventField.iconName {
Button(action: {
print("edit \(eventField.name)")
}, label: {
Image(uiImage: UIImage(named: iconName)!.colorizeMask(eventField.iconColor!))
.frame(width: 27, height: 27)
self.background(.black)
}).background(.black)
} else {
Text("")
.frame(width: 27)
}
Text(eventField.iconName == nil ? "" : eventField.name)
.font(.system(size: eventField.labelFontSize))
.foregroundColor(eventField.labelFontColor)
.frame(width: 50, alignment: .trailing)
Spacer(minLength: 3)
Text(eventField.stringValue)
.font(.system(size: eventField.fontSize))
.foregroundColor(eventField.fontColor)
.frame(width: 200, alignment: .leading)
Spacer(minLength: 10)
}
.listRowBackground(Color.black)
.listRowSeparator(.hidden)
}
}
}
}
}
I believe the color specification for your hstack and frames is supposed to be "(Color.black)" instead of just "(.black)".
Which type of color you use isn't consistent across all Swift objects. Some objects, such as UITableView use "UI colors" which are in the form ".black", while others, like frames, vstacks, hstacks and other objects, use SwiftUI colors in the form "Color.black".
I recommend this very informative page for a very accessible explanation of using color in a view and a stack.

How to set a Limit for the the views inside a HStack?

Good day, beginner SwiftUI & iOS developer here.
I'm not quite sure how else I could've worded this question, but I'll try my best to explain what I would like to achieve.
Right now, I have a VStack that contains a WebImage and Text view, and this VStack is nested inside a HStack. The views inside the VStack are inside a ForEach loop and are generated dynamically with the data I fetch.
When I display these on a screen, all of these views appear in a single line, as shown below.
However I would like for there to only be max two views per "line", not all four of them stacked into a single line. Is there a way to achieve this?
Here is the code:
HStack(spacing: 20) {
ForEach(attViewModel.students, id: \.self) { student in
VStack {
WebImage(url: URL(string: student.photo))
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 40, height: 40)
.clipShape(Circle())
.overlay(Circle().stroke(Color("DarkGreen"), lineWidth: 3))
.compositingGroup()
Text("\(student.name)")
.bold()
.compositingGroup()
CustomRadioButton()
}
.padding()
.overlay(RoundedRectangle(cornerRadius: 10)
.stroke(Color.orange, lineWidth: 2))
.shadow(radius: 7)
}
}
.frame(maxWidth: .infinity)
Here a possible approach for you:
struct ContentView: View {
let arrayOfStudents: [String] = ["jessy", "joy", "joly", "jack"]
var body: some View {
GeometryReader { proxy in
ScrollView(.horizontal) {
HStack(spacing: .zero) {
ForEach(arrayOfStudents, id: \.self) { student in
VStack {
Image(systemName: "person")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 40, height: 40)
.padding()
.clipShape(Circle())
.overlay(Circle().stroke(Color.green, lineWidth: 3))
Text(student)
.bold()
Circle()
.strokeBorder(style: .init(lineWidth: 2))
.frame(width: 10, height: 10)
}
.compositingGroup()
.padding()
.overlay(RoundedRectangle(cornerRadius: 10).stroke(Color.orange, lineWidth: 2))
.shadow(radius: 7)
.padding()
.frame(width: proxy.size.width/2.0)
}
}
}
.position(x: proxy.size.width/2.0, y: proxy.size.height/2.0)
}
}
}

Variable Rectangle() dimension based on cell size to draw a timeline

I am trying to build a List that I want to look like a timeline.
Each cell will represent a milestone.
Down the left hand side of the table, I want the cells to be 'connected', by a line (the timeline).
I have tried various things to get it to display as I want but I have settled with basic geometric shapes , i.e Circle() and Rectangle().
This is sample code to highlight the problem:
import SwiftUI
struct ContentView: View {
var body: some View {
let roles: [String] = ["CEO", "CFO", "Managing Director and Chairman of the supervisory board", "Systems Analyst", "Supply Chain Expert"]
NavigationView{
VStack{
List {
ForEach(0..<5) { toto in
NavigationLink(
destination: dummyView()
) {
HStack(alignment: .top, spacing: 0) {
VStack(alignment: .center, spacing: 0){
Rectangle()
.frame(width: 1, height: 30, alignment: .center)
Circle()
.frame(width: 10, height: 10)
Rectangle()
.frame(width: 1, height: 20, alignment: .center)
Circle()
.frame(width: 30, height: 30)
.overlay(
Image(systemName: "gear")
.foregroundColor(.gray)
.font(.system(size: 30, weight: .light , design: .rounded))
.frame(width: 30, height: 30)
)
//THIS IS THE RECTANGLE OBJECT FOR WHICH I WANT THE HEIGHT TO BE VARIABLE
Rectangle()
.frame(width: 1, height: 40, alignment: .center)
.foregroundColor(.green)
}
.frame(width: 32, height: 80, alignment: .center)
.foregroundColor(.green)
VStack(alignment: .leading, spacing: 0, content: {
Text("Dummy operation text that will be in the top of the cell")
.font(.subheadline)
.multilineTextAlignment(.leading)
.lineLimit(1)
Label {
Text("March 6, 2021")
.font(.caption2)
} icon: {
Image(systemName: "calendar.badge.clock")
}
HStack{
HStack{
Image(systemName: "flag.fill")
Text("In Progress")
.font(.system(size: 12))
}
.padding(.horizontal, 4)
.padding(.vertical, 3)
.foregroundColor(.blue)
.background(Color.white)
.cornerRadius(5, antialiased: true)
HStack{
Image(systemName: "person.fill")
Text(roles[toto])
.font(.system(size: 12))
}
.padding(.horizontal, 4)
.padding(.vertical, 3)
.foregroundColor(.green)
.background(Color.white)
.cornerRadius(5, antialiased: true)
HStack{
Image(systemName: "deskclock")
Text("in 2 Months")
.font(.system(size: 12))
}
.padding(.horizontal, 4)
.padding(.vertical, 3)
.foregroundColor(.red
)
.background(
Color.white
)
.cornerRadius(5, antialiased: true)
}
})
}.listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0))
}
}
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct dummyView: View {
var body: some View {
Text("Hello, World!")
}
}
struct dummyView_Previews: PreviewProvider {
static var previews: some View {
dummyView()
}
}
but as you can see in the enclosed picture, there are unwanted gaps
So other content in the cell is making the height of the entire cell 'unpredictable' and break the line.
Is there a way to determine the height of the cell and extend the dimensions of the Rectangle, so that it extends to the full height of the cell?
Is there a better approach you recommend for trying to build such a timeline ?
PS: I have tried playing around with .frame and .infinity but that does work.
Many thanks.
Why not just draw the line based on the size of the row. See Creating custom paths with SwiftUI. Remember, everything is a view.
First, you need to decompose what you are doing into subviews. You have too many moving parts in one view to get it correct. Also, I would avoid setting specific padding amounts as that will mess you up when you change devices. You want a simple, smart view that is generic enough to handle different devices.
I would have a row view that has a geometry reader so it knows its own height. You could then draw the line so that it spanned the full height of the row, regardless of the height. Something along the lines of this:
struct ListRow: View {
var body: some View {
GeometryReader { geometry in
ZStack {
HStack {
Spacer()
Text("Hello, World!")
Spacer()
}
VerticalLine(geometry: geometry)
}
}
}
}
and
struct VerticalLine: View {
let geometry: GeometryProxy
var body: some View {
Path { path in
path.move(to: CGPoint(x: 20, y: -30))
path.addLine(to: CGPoint(x: 20, y: geometry.size.height+30))
}
.stroke(Color.green, lineWidth: 4)
}
}

how to strech image on one side in swiftUI, like a .9 file in Android?

i have an image that i want to strech only it's height to fit different content, how do i do that in swiftUI? right now it looks like this
struct ContentView: View {
var body: some View {
VStack {
HStack {
VStack(alignment: .leading, spacing: 130) {
Text("Title")
.font(.headline)
.foregroundColor(.primary)
Text("text")
.font(.subheadline)
.foregroundColor(.secondary)
Text("padding")
}
.padding(.vertical)
Spacer()
Image("rightTag")
.resizable(capInsets: .init(top: 0, leading: 0, bottom: 0, trailing: 0), resizingMode: .stretch)
.aspectRatio(contentMode: .fit)
.frame(maxWidth: 20)
}
.frame(maxWidth: screen.width - 60)
.padding(.leading)
.background(.white)
.cornerRadius(20)
}
}
}
how can i stretch its height to fit this outer frame? ragular resizable and stuff can't get it done.
any helped would be wonderful! Thanks!
sry i didn't make myself clear earllier.
Here is possible approach. However as I see now you'd rather need to stretch not the entire original image, but only middle of it, so in real project it would be needed to make your image tri-part and apply below stretching approach only to middle (square) part.
Approach uses asynchronous state update, so works in Live Preview / Simulator / RealDevice. (Tested with Xcode 11.2 / iOS 13.2)
struct ContentView: View {
#State private var textHeigh: CGFloat = .zero
var body: some View {
VStack {
HStack(alignment: .top) {
VStack(alignment: .leading, spacing: 130) {
Text("Title")
.font(.headline)
.foregroundColor(.primary)
Text("text")
.font(.subheadline)
.foregroundColor(.secondary)
Text("padding")
}
.padding(.vertical)
.alignmentGuide(.top, computeValue: { d in
DispatchQueue.main.async {
self.textHeigh = d.height // !! detectable height of left side text
}
return d[.top]
})
Spacer()
Image("rightTag")
.resizable()
.frame(maxWidth: 20, maxHeight: max(60, textHeigh)) // 60 just for default
}
.frame(maxWidth: screen.width - 60)
.padding(.leading)
.background(Color.white)
}
}
}