Alignment for GeometryReader and .frame - swift

I'm using GeometryReader() and .frame() to show their comparability and playing with their width.
However the object wrapped in GeometryReader() doesn't have center alignment when width is less than minimum.
Please refer to attached gifs for demo.
Could you please help me with alignment?
Ideal:
Current:
struct ExampleView: View {
#State private var width: CGFloat = 50
var body: some View {
VStack {
SubView()
.frame(width: self.width, height: 120)
.border(Color.blue, width: 2)
Text("Offered Width is \(Int(width))")
Slider(value: $width, in: 0...200, step: 5)
}
}
}
struct SubView: View {
var body: some View {
GeometryReader { geometry in
Rectangle()
.fill(Color.yellow.opacity(0.6))
.frame(width: max(geometry.size.width, 120), height: max(geometry.size.height, 120))
}
}
}

That's because GeometryReader doesn't center its children.
You have to manually position the Rectangle by adding either a .position modifier or a .offset.
.position will move the origin of the rectangle's frame relative to the parent's center.
.offset will not change the frame, but rather change the rendering (working like a CGAffineTransform with translation).
The following modifiers to your Rectangle will yield the same results visually (though different under the hood):
Rectangle()
.fill(Color.yellow.opacity(0.6))
.frame(width: max(geometry.size.width, 120), height: max(geometry.size.height, 120))
.position(x: geometry.size.width / 2, y: geometry.size.height / 2)
or
let rectWidth = max(geometry.size.width, 120)
Rectangle()
.fill(Color.yellow.opacity(0.6))
.frame(width: rectWidth, height: max(geometry.size.height, 120))
.offset(x: ((geometry.size.width - rectWidth) / 2), y: 0)
Note that your rectangle's frame exceeds the bounds of its parent. I'd suggest to avoid that because it will cause all sorts of difficulties laying out other UI elements on the screen.
You could build it the other way around (unless your practical goal is to just understand how GeometryReader works):
struct ExampleView: View {
#State private var width: CGFloat = 50
var body: some View {
VStack {
let minWidth: CGFloat = 120
let subViewWidth: CGFloat = max(minWidth, width)
SubView(desiredWidth: width)
.frame(width: subViewWidth, height: 120)
.background(.yellow.opacity(0.6))
Text("Offered Width is \(Int(width))")
Slider(value: $width, in: 0...200, step: 5)
}
}
}
struct SubView: View {
let desiredWidth: CGFloat
var body: some View {
Rectangle()
.fill(.clear)
.border(Color.blue, width: 2)
.frame(width: desiredWidth, height: nil)
}
}
In this example the SubView has a yellow fill and a minimal frame width while the inner rectangle just takes whatever width is set on the slider. It also doesn't need a GeometryReader anymore. It looks and behaves the same but none of the views exceeds its parent's bounds anymore.

Related

SwiftUI - Animate height of rectangle from 0 to height

I am attempting to animate the height of a RoundedRectangle() from 0 to its new height, I would like it to grow from its current position upwards when my device shakes. As it currently stands, I have a onShake function which I want to activate the growing of the rectangle.
struct View1: View {
var body: some View {
ZStack {
Color.white
.edgesIgnoringSafeArea(.all)
Text("Hola")
.foregroundColor(.black)
.font(.headline)
RoundedRectangle(cornerRadius: 5)
.frame(width: 100, height: 0, alignment: .center)
.foregroundColor(Color("RedColour"))
.onShake {
withAnimation {
self.frame(width: 100, height: 100)
}
}
}
}
}
As you can probably tell I am very new to SwiftUI and have very limited knowledge of how animations work. Feel free to grill me for it, but I would also love some help with attempting to grow this rectangle upwards when my device shakes.
Much love!
You could try this. But as the information you provided do not include how you want to use this, this might be incomplete.
struct View1: View {
#State private var height: CGFloat = 0 // add this to modify
// the height of the Rounded Rectangle
let targetHeight: CGFloat = 100 // targetHeight var to syncronise with the VStack
var body: some View {
ZStack {
Color.white
.edgesIgnoringSafeArea(.all)
Text("Hola")
.foregroundColor(.black)
.font(.headline)
.background{
VStack{
Spacer(minLength: 0)
RoundedRectangle(cornerRadius: 5)
.frame(width: 100, height: height)
.foregroundColor(.red)
.onShake {
withAnimation(.linear(duration: 5)) {
height = targetHeight
}
}
}.frame(height: targetHeight) // VStack with fixed height
//to animate from bottom up
}
}
}
}

How to horizontally center content of horizontal ScrollView?

I have a horizontal ScrollView, and in it an HStack, like so:
ScrollView(.horizontal) {
HStack {
Circle()
.frame(width: 60, height: 60)
...
}
With this code, the contents of the HStack are aligned to the left of the screen, but I would like to get them in the middle of the ScrollView. How can I accomplish this?
all you need to do is to define the size of ScrollView and the HStack so when you define the size of ScrollView and define the size of HStack the view and the important thing is to make the alignment HStack to the center so that the View like a circle inside the Hstack center in it
struct ContentView: View {
var body: some View {
ScrollView(.horizontal) {
HStack() {
Circle()
.frame(width: 60, height: 60)
// the alignment is center here
}.frame(width: 500, height: 60,alignment: .center)
}.frame(width: 500, height: 500)
}
}
here the alignment is leading so the circle go to left
struct ContentView: View {
var body: some View {
ScrollView(.horizontal) {
HStack() {
Circle()
.frame(width: 60, height: 60)
// the alignment is leading here
}.frame(width: 500, height: 60,alignment: .leading)
}.frame(width: 500, height: 500)
}
}
here the alignment is trailing so the circle go to right
struct ContentView: View {
var body: some View {
ScrollView(.horizontal) {
HStack() {
Circle()
.frame(width: 60, height: 60)
// the alignment is trailing here
}.frame(width: 500, height: 60,alignment: .trailing)
}.frame(width: 500, height: 500)
}
}
Assuming you want to center ScrollView's content when there is free space (because otherwise scrolling behaviour would be rather broken) here is a possible approach (w/o hardcode) - to use preference key to read scroll view's width on screen and apply it as a minimum width of content's container, the center alignment is applied by default.
Tested with Xcode 13.2 / iOS 15.2
struct DemoView: View {
#State private var scrollWidth = CGFloat.zero
var body: some View {
ScrollView(.horizontal) {
HStack {
Circle()
.frame(width: 60, height: 60)
}
.frame(minWidth: scrollWidth) // << here (cetner by default) !!
}
.border(Color.green) // << for demo
.background(GeometryReader {
// read width of ScrollView on screen
Color.clear
.preference(key: ViewWidthKey.self, value: $0.frame(in: .local).size.width)
})
.onPreferenceChange(ViewWidthKey.self) {
self.scrollWidth = $0 // << store width
}
}
}

How to scale a SwiftUI Image including its overlay?

Given this view with an image that has an overlay:
struct TestView: View {
var body: some View {
Image("image")
.overlay {
Image("logo")
.position(x: 20, y: 130) // I want to freely position the overlay image
}
}
}
The size of the images shouldn’t matter. I my example they are 150×150 pixels (“image”) and 20×20 pixels (logo).
How can I scale this up to its bounds (like superviews frame), including the overlay?
Some must-have conditions:
I need to freely position the overlay image, so I can not use the alignment parameter of overlay in conjunction with a padding on the logo.
I also need to position the overlay image absolutely, not relatively. So I can’t use a GeometryReader (or alignment).
I need the view to be reusable in different scenarios (different devices, different view hierarchies). That means I don’t know the scale factor, so I can’t use scaleEffect.
I tried:
A) .aspectRatio(contentMode: .fit)
struct TestView: View {
var body: some View {
Image("image")
.resizable()
.overlay {
Image("logo")
.position(x: 20, y: 130)
}
.aspectRatio(contentMode: .fit)
}
}
B) .scaledToFit()
struct TestView: View {
var body: some View {
Image("image")
.resizable()
.overlay {
Image("logo")
.position(x: 20, y: 130)
}
.scaledToFit()
}
}
C) Making logo resizable
struct TestView: View {
var body: some View {
Image("image")
.resizable()
.overlay {
Image("logo")
.resizable()
.position(x: 20, y: 130)
}
.scaledToFit() // or .aspectRatio(contentMode: .fit)
}
}
A and B looking like this:
C looking like this:
Both of which gave me a relative position of the logo that was different from the original (see linked screenshot). The relative size differs as well (it is now too small).
Wrapping in a stack and scale this stack instead also didn’t help.
[UPDATE] Accepted answer
Apparently not having a real view hierarchy in SwiftUI vs UIKit comes at a price. Scaling a hierarchy shouldn’t be that complicated imho. Instead I still would expect .scaledToFit (if added at the end) to scale everything before it (see A), B), and C)).
Adapted accepted answer (minus contentSize and alignment: .topLeading):
struct ContentView: View {
var body: some View {
Image("image")
.resizable()
.scaledToFit()
.overlay {
GeometryReader { geometry in
let imageSize = CGSize(width: 150, height: 150)
let logoSize = CGSize(width: 20, height: 20)
let logoPosition = CGPoint(x: 20, y: 130)
Image("logo")
.resizable()
.position(
x: logoPosition.x / imageSize.width * geometry.size.width,
y: logoPosition.y / imageSize.height * geometry.size.height
)
.frame(
width: logoSize.width / imageSize.width * geometry.size.width,
height: logoSize.height / imageSize.height * geometry.size.height
)
}
}
}
}
Then like this with GeometryReader:
struct ContentView: View {
let imageSize = CGSize(width: 150, height: 150) // Size of orig. image
let logoPos = CGSize(width: 10, height: 120) // Position of Logo in relation to orig. image
let logoSize = CGSize(width: 20, height: 20) // Size of logo in relation to orig. image
#State private var contentSize = CGSize.zero
var body: some View {
Image("dog")
.resizable()
.scaledToFit()
.overlay(
GeometryReader { geo in
Color.clear.onAppear {
contentSize = geo.size
}
}
)
.overlay(
Image(systemName: "circle.hexagongrid.circle.fill")
.resizable()
.foregroundColor(.pink)
.offset(x: logoPos.width / imageSize.width * contentSize.width,
y: logoPos.height / imageSize.height * contentSize.height)
.frame(width: logoSize.width / imageSize.width * contentSize.width,
height: logoSize.height / imageSize.height * contentSize.height)
, alignment: .topLeading)
}
}
The problem is the absolute positioning of the logo. I see two ways:
scale the whole resulting image with .scaleEffect.
Be aware this is a render scaling of the underlying image, it will not be redrawn in the new size, so can become blurry.
Image("dog")
.overlay {
Image(systemName: "circle.hexagongrid.circle.fill")
.foregroundColor(.pink)
.position(x: 20, y: 130)
}
.scaleEffect(2.0)
or – and my preference
2. Position the Logo relative to the lower left corner
ZStack(alignment: .bottomLeading) {
Image("dog")
.resizable()
.scaledToFit()
Image(systemName: "circle.hexagongrid.circle.fill")
.resizable()
.foregroundColor(.pink)
.frame(width: 70, height: 70)
.padding()
}

Increase/Decrease the size of a view horizontally by dragging the edges of it

I've seen a few similar examples of this such as How to correctly do up an adjustable split view in SwiftUI? and How to resize UIView by dragging from its edges? but I can't find exactly what I'm looking for that correlates correctly across to SwiftUI
I have a view that I want the user to be able to adjust the width of via a 'grab bar' on the right of the view. When the user drags this bar left (decreases view width) and to the right (increases the view width). How can I go about doing this?
In the example, RedRectangle is my view that i'm trying to adjust which comprises of a Rectangle and the Resizer which is manipulated to adjust the size. What am I doing wrong here?
Additionally, there isn't a gradual animation/transition of the frame being increased/decreased and it just seems to jump. How can I achieve this?
Reproducible example linked here:
import SwiftUI
struct ContentView: View {
#State var resizedWidth: CGFloat?
var body: some View {
HStack(alignment: .center) {
Spacer()
RedRectangle(width: 175, resizedWidth: resizedWidth)
Resizer()
.gesture(
DragGesture()
.onChanged({ value in
resizedWidth = max(80, resizedWidth ?? 0 + value.translation.width)
})
)
Spacer()
}
}
}
struct RedRectangle: View {
let width: CGFloat
var resizedWidth: CGFloat?
var body: some View {
Rectangle()
.fill(Color.red)
.frame(width: resizedWidth != nil ? resizedWidth : width, height: 300)
.frame(minWidth: 80, maxWidth: 400)
}
}
struct Resizer: View {
var body: some View {
Rectangle()
.fill(Color.blue)
.frame(width: 8, height: 75)
.cornerRadius(10)
}
}
import SwiftUI
struct ContentView: View {
let minWidth: CGFloat = 100
#State var width: CGFloat?
var body: some View {
HStack(alignment: .center) {
Spacer()
RedRectangle(width: width ?? minWidth)
Resizer()
.gesture(
DragGesture()
.onChanged { value in
width = max(minWidth, width! + value.translation.width)
}
)
Spacer()
}
.onAppear {
width = minWidth
}
}
}
struct RedRectangle: View {
let width: CGFloat
var body: some View {
Rectangle()
.fill(Color.red)
.frame(width: width, height: 100)
}
}
struct Resizer: View {
var body: some View {
Rectangle()
.fill(Color.blue)
.frame(width: 8, height: 75)
.cornerRadius(10)
}
}

SwiftUI actual Image size of a scaled image

I am new to SwiftUI and trying to test an idea with it. I am sure what I want to do is simple but I am missing it. I want to get the size of the actual image as the scale changes. Everything I read tells me to use GeometryReader. But that seams to give me the size of the view it will be displayed in. As the scale changes, it does not change.
Below is my code, but it does not produce what I need. My end goal is so I know the size of the image and I can restrict the user so they can not slide the image out of the view independent of the scale.
With following line of code, I would like to be able to position the image anywhere from the left side (0.0 offset) to the right side (1.0 x ?) independent of the scale of the image.
.position(x: self.sharedImageOffset * geometry.size.width, y: 200 )
Thanks for any help.
import SwiftUI
struct ContentView: View {
#State var sharedImageScale: CGFloat = 1.0
#State var sharedImageOffset: CGFloat = 0.0
var body: some View {
VStack{
GeometryReader { geometry in
Image("IMG_1650")
.resizable()
.aspectRatio(contentMode: .fill)
.scaleEffect(self.sharedImageScale)
.position(x: self.sharedImageOffset * geometry.size.width, y: 0 )
.background(Color.blue)
.gesture(
TapGesture()
.onEnded { _ in
print("SIZE \(geometry.size)")
}
)
}.border(Color.green, width: 2)
.clipped()
HStack{
Text("Scale")
Slider(value: self.$sharedImageScale, in: 0.5...10 ).frame(minWidth: 10, idealWidth: 100, maxWidth: 200)
Text("Offset")
Slider(value: self.$sharedImageOffset, in: -1.0...1 ).frame(minWidth: 10, idealWidth: 100, maxWidth: 200)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}