Let's say I have a ZStack with an indeterminate/flexible width. There are four views within this ZStack. How would I distribute the views evenly... as if I were fanning out a deck of cards (with each card overlapping the next), like this:
I can accomplish this effect with .offset(x: ?) if I know the width of the container:
ZStack {
Text("One")
.frame(width: 99, height: 150, alignment: .topLeading)
.background(Color.blue)
.offset(x: -99)
Text("Two")
.frame(width: 99, height: 150, alignment: .topLeading)
.background(Color.red)
.offset(x: -33)
Text("Three")
.frame(width: 99, height: 150, alignment: .topLeading)
.background(Color.orange)
.offset(x: 33)
Text("Four")
.frame(width: 99, height: 150, alignment: .topLeading)
.background(Color.green)
.offset(x: 99)
}
But what if I don't know the width of the container? Or what if it changes? Or what if the number of views within the container changes?
Basically, I just need to distribute views within the ZStack evenly... no matter what the width is. How?
Since you have that many cards, you should use a ForEach. This will make adding/removing cards easier and saves some code.
But what if I don't know the width of the container?
This is where GeometryReader comes in! You can use it to get the width of the container.
I just need to distribute views within the ZStack evenly
Once you get the width from the GeometryReader, you can just divide by the number of elements in the cards array to calculate each card's width.
struct Card {
var name: String
var color: Color
}
struct ContentView: View {
let cards = [
Card(name: "One", color: .blue),
Card(name: "Two", color: .red),
Card(name: "Three", color: .orange),
Card(name: "Four", color: .green)
]
var body: some View {
ZStack {
GeometryReader { proxy in
ForEach(cards.indices, id: \.self) { index in
let card = cards[index]
Text(card.name) /// add a bit of overlap
.frame(width: proxy.size.width / CGFloat(cards.count) + 10, height: 150, alignment: .topLeading)
.background(
card.color
.shadow(color: Color.black, radius: 5) /// make the overlap a bit more visible
)
.offset(x: proxy.size.width / CGFloat(cards.count) * CGFloat(index))
}
.frame(maxHeight: .infinity) /// center the cards vertically
}
}
}
}
Result:
try this:
create a function that returns x spacers (x for the number of text boxes that you want - 1) and inserts a Text view where you want it.
set the width to whatever you want it to be
have the text align to the left
replace all of the Text views in your code with this function
This should get the result you are looking for.
Related
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).
}
i trying to position plus sign in the middle of circle, but it doesnt work properly and goes a little bit lower
.navigationBarItems(trailing: Button(action: {print("add")}, label: {
Circle()
.accentColor(.green)
.frame(width: 30, height: 30, alignment: .center)
.overlay(Text("+").accentColor(.white), alignment: .center)
}))
use Image(systemName: "plus").foregroundColor(.white) instead of Text("+")
In text "+" symbol doesn't have to be in the middle of the view because of text layout.
SFSymbols are more convenient in this regard. Also you can specify size with .font(.system(size: 10))
Try this button. Here size is the same as yours, but the tappable area is larger.
struct YourReusableButton: View {
var action: (() -> Void)? = nil
var body: some View {
Button(action: buttonAction) {
ZStack {
Circle()
.frame(width: 30, height: 30)
.foregroundColor(.green)
Image(systemName: "plus")
.foregroundColor(.white)
.imageScale(.small)
.frame(width: 44, height: 44)
}
}.padding()
}
func buttonAction() { action?() }
}
you can use offset to adjust the text position, like this:
Text("+").offset(x: -1, y: -2)
You can use Text's baselineOffset modifier to adjust the text vertical offset. But that's not a good idea.
You should use an image object such as SFSymbol image or PNG image.
I wanted to build a Playground which stores my notes for my next examination using either Xcode playgrounds or just Swift Playgrounds. However, I attempted to build a list with an arrow at the side for navigation in Playgrounds, but with the lack of information after searching lots of tutorials, there's no way I could create a navigation list on
Swift Playgrounds. So I switched to Xcode playgrounds. But I could not seem to load the LiveView.
Note: no errors shown. Xcode playgrounds displays "Successful" but nothing is shown.
import SwiftUI
import PlaygroundSupport
//--------------------------------------------------------------//
// Weighted Asesstment III Notes
// Subject: Science
// Written in: English & SwiftUI
struct IntroView: View {
// arrays cus I wanna put this to a list
let expTech = ["Chromatography","Evaporation", "Distillation", "Dissolving","Magnetic Attraction", "Filtration"]
let expUse = ["Chromatography",
"Method: Paper",
"Used for: ",
"Evaporation",
"Method: Heating",
"Used for: Separating salt & water",
"Distillation",
"Method: Heating of liquid mixture with different boiling points",
"eg. Used for: Obtaining pure water from alcohol",
"Dissolving",
"Method",
"Used for",
"Magnetic Attraction",
//I need complete silence lol
//Iron nickel cobalt steel
"Method: Magnets (I,N,C,S)",
"Used for: Separating magnetic objects from non magnets",
"Filtration",
"Method: Filtering/Use of filter paper",
"Used for: Separating mixture of insoluble solid from liquid"]
var body: some View {
// ZStack
ZStack {
// background color
Color.white
// VStack
VStack {
LinearGradient(gradient: Gradient(colors: [.white, .gray, .black]), startPoint: .leading, endPoint: .trailing)
.mask(Text("WA 3 Sci Notes: M5C7")
.font(.system(size: 30, weight: .bold, design: .monospaced)))
.foregroundColor(Color.black)
.offset(x: 10, y: -325)
.padding()
Text("Types of experimental techniques")
.foregroundColor(Color.black)
.offset(x: -100, y: -710)
.font(.system(size: 25, weight: .semibold))
}
ZStack {
Rectangle()
.frame(width: 340, height: 240)
.cornerRadius(20)
.scaledToFill()
.shadow(color: Color.black, radius: 10)
.offset(x: -100, y: -120)
// list
List(expTech, id: \.self) { expTech in Text(expTech) }
.frame(width: 350, height: 250, alignment: .center)
.cornerRadius(25)
.offset(x: -100, y: -120)
}
Text("Uses of Experimental Techniques")
.foregroundColor(Color.blue)
.offset(x: 80, y: 40)
.font(.system(size: 25, weight: .semibold))
ZStack {
VStack {
Rectangle()
.frame(width: 600, height: 250)
.cornerRadius(20)
.scaledToFill()
.shadow(color: Color.black, radius: 5)
.offset(x: 5, y: 530)
}
}
}
}
}
PlaygroundPage.current.setLiveView(IntroView())
The issue could be that your View doesn't render properly. I put your code into an Xcode project (not a storyboard) and it looks like this:
You can even see it rendering weirdly in the playground:
It seems there is a potential bug in SwiftUI. I am trying to put a rectangle with opacity 0.5 on top of an image.
When I try to fix the transparent rectangle on top, from 100px width, it goes down instead of sticking to the top.
Here is the code:
ZStack {
VStack {
Image("movistar")
.resizable(capInsets: EdgeInsets(), resizingMode: .stretch)
.scaledToFit()
.cornerRadius(8)
.padding(15)
.frame(minWidth: Global.SCREEN_WIDTH)
}
VStack {
HStack {
Rectangle()
.fill(Color(red: 0, green: 0, blue: 0, opacity: 0.5))
.frame(width: 110, height: Global.SCREEN_WIDTH / 4)
}
Spacer()
}
.scaledToFit()
.cornerRadius(8)
.padding(15)
.frame(width: Global.SCREEN_WIDTH, height: Global.SCREEN_WIDTH)
There is no bug here. If you add a .background to all of your layers, you will see that because of the way you set up the view (ie. Spacer, scaledToFit, etc.) the actual frames of the views are not necessarily the edges of the image. You also have not set the alignment of any of the Stacks or Frames.
There are many ways to do what you are trying to do, but I believe this is the simplest:
var body: some View {
Image("movistar")
.resizable(capInsets: EdgeInsets(), resizingMode: .stretch)
.scaledToFit()
.cornerRadius(8)
.frame(minWidth: UIScreen.main.bounds.width)
.overlay(
Rectangle()
.fill(Color(red: 0, green: 0, blue: 0, opacity: 0.5))
.frame(width: 110, height: UIScreen.main.bounds.width / 4)
, alignment: .top
)
}
Finally got into a solution: .scaleToFit() was messing with the VStack(). After deleting, it worked perfectly. I also got rid of the HStack().
I cannot figure out what compositingGroup() is. At first, I thought it is something like Merging layers in Photoshop. But it was not. Because .shadow() effects to the overlay and background views respectively even if I use .compositingGroup().
So far, I've found 2 differences when I use .compositingGroup()
Text doesn't have shadows.
The shadow size of the overlay view is slightly smaller than the above one.
What is the purpose of compositingGroup?
struct ContentView: View {
var body: some View {
VStack(spacing: 50) {
Text("Without\ncompositing")
.font(.largeTitle)
.bold()
.padding()
.foregroundColor(Color.white)
.background(RoundedRectangle(cornerRadius: 30).fill(Color.red))
.padding()
.padding()
.overlay(RoundedRectangle(cornerRadius: 30).stroke(lineWidth: 10))
.shadow(color: .blue, radius: 5)
Text("With\ncompositing")
.font(.largeTitle)
.bold()
.padding()
.foregroundColor(Color.white)
.background(RoundedRectangle(cornerRadius: 30).fill(Color.red))
.padding()
.padding()
.overlay(RoundedRectangle(cornerRadius: 30).stroke(lineWidth: 10))
.compositingGroup() // <--- I added .compositingGroup() here.
.shadow(color: .blue, radius: 5)
}
}
}
This modifier makes the following modifiers be applied to the view as a whole and not to each particular subview separately
Here's an example to better illustrate this:
struct ContentView: View {
let circles: some View = ZStack {
Circle()
.frame(width: 100, height: 100)
.foregroundColor(.red)
.offset(y: -25)
Circle()
.frame(width: 100, height: 100)
.foregroundColor(.blue)
.offset(x: -25, y: 25)
Circle()
.frame(width: 100, height: 100)
.foregroundColor(.green)
.offset(x: 25, y: 25)
}
var body: some View {
VStack(spacing: 100) {
circles
circles
.opacity(0.5)
circles
.compositingGroup()
.opacity(0.5)
}
}
}
So in your case the shadow is applied to the whole view rather than separately to the Text and overlaying RoundedRectangle
Use it when wanting to apply effects like opacity or shadow to a group of views and not each contained element by itself.
It seems like that .shadow() modifier will add both inner and outer shadow. It means that if the view is not "solid", for example, it has a "hole", .shadow() will add shadow like this:
RoundedRectangle(cornerRadius: 30)
.stroke(lineWidth: 10)
.frame(width: 300)
.shadow(color: .blue, radius: 5)
Click to see the image
So, if you do not want the inner shadow, you need to make your view be "solid", like this:
RoundedRectangle(cornerRadius: 30)
.stroke(lineWidth: 10)
.frame(width: 300)
.background(RoundedRectangle(cornerRadius: 30).fill(.white))
.shadow(color: .blue, radius: 5)
Click to see the image
However, something goes wrong again, the inner shadow doesn't disappear.
That's because I forgot to apply the .compositingGroup() modifier.
As #ramzesenok mentioned, .compositingGroup() makes the following modifiers be applied to the view as a whole and not to each particular subview separately.
So, change the code a little bit:
RoundedRectangle(cornerRadius: 30)
.stroke(lineWidth: 10)
.frame(width: 300)
.background(RoundedRectangle(cornerRadius: 30).fill(.white))
.compositingGroup()
.shadow(color: .blue, radius: 5)
Click to see the image
There is only outer shadow now.