SwiftUI: Aligning in regards to leading edge when using .position() - ios

I'm need to set an absolute position for my view.
A position modifier is positioning a view in regards to it center but I'd like to align it in regards to its leading edge.
Is there any way to achieve that?
I was trying to play with alignmentGuide but had no luck with that.
Here's my sample code:
var body: some View {
ZStack {
Text("Hello, this is a long text.")
.border(Color.blue, width: 1)
.position(x: 10, y: 50)
}
.frame(width: 200, height: 100)
}
Red dashed line is showing how I want the text to be positioned.

You can apply a GeometryReader to the Text's background to read out the correct width and then add half of it to the x position.
Just add some variable to safe the width:
#State private var textWidth: CGFloat?
And then apply the rectReader to the Text's background and add the Text's width to the x position:
Text("Hello, this is a long text.")
.border(Color.blue, width: 1)
.background(rectReader())
.position(x: 10 + (textWidth ?? 0) / 2, y: 50)
That's the rectReader:
private func rectReader() -> some View {
return GeometryReader { geometry -> AnyView in
DispatchQueue.main.async {
self.textWidth = geometry.size.width
}
return AnyView(Rectangle().fill(Color.clear))
}
}

Related

Adjusting the frame of a SwiftUI view with a DragGesture resizes it in all dimensions

I'm trying to implement a simple view in SwiftUI that can be resized via a drag handle placed on its lower-left corner. The resizable view's dimensions are set via two #State variables controlling the width and height. These variables are mutated within a DragGesture's onChanged handler captured by the drag handle.
It's sort of working, but not in the way I expect. When I perform a drag gesture on the drag handle, the view gets resized in all dimensions. I want the resizing to work in the same way as a desktop window; dragging shouldn't resize the view in all directions.
Below is a recording from the iOS simulator showing the incorrect behaviour:
I understand that the centre of a SwiftUI view is considered its origin, and that's probably what's causing this behaviour. I'm not sure how to work around this.
Here's my code:
import SwiftUI
struct ContentView: View {
// Initialise to a size proportional to the screen dimensions.
#State private var width = UIScreen.main.bounds.size.width / 3.5
#State private var height = UIScreen.main.bounds.size.height / 1.5
var body: some View {
// This is the view that's going to be resized.
ZStack(alignment: .bottomTrailing) {
Text("Hello, world!")
.frame(width: width, height: height)
// This is the "drag handle" positioned on the lower-left corner of this stack.
Text("")
.frame(width: 30, height: 30)
.background(.red)
.gesture(
DragGesture()
.onChanged { value in
// Enforce minimum dimensions.
width = max(100, width + value.translation.width)
height = max(100, height + value.translation.height)
}
)
}
.frame(width: width, height: height, alignment: .center)
.border(.red, width: 5)
.background(.yellow)
.padding()
}
}
Edit: I was too liberal in what I removed for the minimal example above. The following snippet adds a position constraint on the ZStack, which is used in another gesture (not shown) so the view can be dragged around.
I'm guessing this is problematic because position specifies relative coordinates from the centre of the ZStack to the parent VStack, and as the ZStack is resized, it's being "moved" to maintain the position constraint.
import SwiftUI
struct ContentView: View {
// Initialise to a size proportional to the screen dimensions.
#State private var width = UIScreen.main.bounds.size.width / 3.5
#State private var height = UIScreen.main.bounds.size.height / 1.5
var body: some View {
VStack {
// This is the view that's going to be resized.
ZStack(alignment: .bottomTrailing) {
Text("Hello, world!")
.frame(width: width, height: height)
// This is the "drag handle" positioned on the lower-left corner of this stack.
Text("")
.frame(width: 30, height: 30)
.background(.red)
.gesture(
DragGesture()
.onChanged { value in
// Enforce minimum dimensions.
width = max(100, width + value.translation.width)
height = max(100, height + value.translation.height)
}
)
}
.frame(width: width, height: height)
.border(.red, width: 5)
.background(.yellow)
.padding()
// This position constraint causes the ZStack to grow/shrink in all dimensions
// when resized.
.position(x: 400, y: 400)
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
}
}
You're right that the issue is caused by your view being center-aligned.
To fix this, you can wrap your view in a VStack with a different alignment applied, e.g. .topLeading if you want it to align to the top-left.
You also have to make sure this VStack is taking up the available space of the view. Otherwise, it will shrink to the size of your resizable box, causing the view to stay center-aligned. You can achieve this with .frame(maxWidth: .infinity, maxHeight: .infinity).
TL;DR
Place your resizable box in a VStack with a .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) modifier.
VStack { // <-- Wrapping VStack with alignment modifier
// This is the view that's going to be resized.
ZStack(alignment: .bottomTrailing) {
Text("Hello, world!")
.frame(width: width, height: height)
// This is the "drag handle" positioned on the lower-left corner of this stack.
Text("")
.frame(width: 30, height: 30)
.background(.red)
.gesture(
DragGesture()
.onChanged { value in
// Enforce minimum dimensions.
width = max(100, width + value.translation.width)
height = max(100, height + value.translation.height)
}
)
}
.frame(width: width, height: height, alignment: .topLeading)
.border(.red, width: 5)
.background(.yellow)
.padding()
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
Edit
With an additional position modifier on the ZStack, try offsetting the x and y values by half of the width and height to position relative to the top-left origin of the ZStack:
.position(x: 400 + width / 2, y: 400 + height / 2)

How to create SwiftUI HStack that adjusts to accommodate the expansion of one of its subviews?

I have a simple horizontal stack comprising three colour bars:
When the yellow bar width is magnified via a gesture, I would expect the purple bar to move to the right to accommodate the expanded yellow bar but instead the yellow bar simply expands UNDERNEATH the purple bar (which remains in into original position).
struct ContentView: View {
#GestureState var scale = CGFloat(1)
var body: some View {
HStack(spacing: 5) {
Color(.blue)
.frame(width: 100, height: 60, alignment: .leading)
Color(.yellow)
.frame(width: 100, height: 60, alignment: .leading)
.scaleEffect(x: scale, y: 1.0, anchor: .leading)
.gesture(MagnificationGesture()
.updating($scale, body: { (value, scale, trans) in
scale = value
})
)
Color(.purple)
.frame(width: 100, height: 60, alignment: .leading)
}
}
}
How can I achieve dynamic adjustment of the HStack layout whilst one of its components is expanding?
Many thanks...
Robert
you could try this code:
Color(.yellow)
.frame(width: CGFloat(100) * scale, height: 60, alignment: .leading)
.gesture(MagnificationGesture()
.updating($scale, body: { (value, scale, trans) in
scale = value
})
)

How to restrict drag gesture to particular frame only in swiftUI

I want to drag circle shape only within a frame horizontal. I try below code you can see shape work perfectly horizontal and also left side but when drag right side shape drag up to out of device frame. I check so many answer but did't work anything. also try to add .onEnded but don't work.
please help me
thanks in advance
here is a code that i tried
struct ProgressIndicator: View {
#State private var position = CGSize.zero
var body: some View {
GeometryReader { geometry in
VStack(alignment: .leading) {
ZStack {
Circle()
.foregroundColor(Color.blue)
.opacity(0.3)
.frame(width: 40, height: 40, alignment: .leading)
.padding(.bottom, -12)
}
.gesture(
DragGesture()
.onChanged({ (value) in
self.position.width += value.location.x
})
)
.offset(x: self.position.width - 20, y: 0)
}
}
}
}
here is screenshot of circle shape for drag to left and right
You can restrict the position with GeometryReader:
self.position.width = min(self.position.width + value.location.x, geometry.size.width / 2)
Here's the result from Playgrounds:
https://imgur.com/FA5xyAz

swiftUI transitions: Scale from some frame - like iOS Homescreen is doing when opening an App

How do I get the effect in a translation, that iOS home screen does when opening an App: It scales the App to fullscreen, starting from the App-icon on the home screen?
In my code, I have the icon-frame (CGRect) with position, width and height and I have the final frame. Is there a way to (probably combine some) transitions to get a scaling from the icon-frame to the final frame?
I get somewhat similar with:
view.transition(AnyTransition.scale(scale: 0, anchor: UnitPoint.trailing))
Which scales from zero to the original size, starting from trailing center position.
It's only close as:
it scales from zero instead of the size of the original icon
it starts from a fixed point (trailing, center) but I'd like to let it start from where the icon is.
Just to be sure: It must be a transition as the view is newly created and dropped. I tried with keeping the view and just change its opacity to show/hide it. With many other problems like not getting the reverse animation, when the view disappears.
Here is a demo of idea how such effect could be done with combined transitions... (positions and sizes are hardcoded for demo simplicity - they can be read with geometry reader, alignment guides or anchor preferences, and actually do not affect the transition usage idea, also animation can be configured)
Demo:
struct TestRisingView: View {
let screen = UIScreen.main.bounds
#State var showingView = false
#State var btFrame: CGRect = .zero
var body: some View {
GeometryReader { g in
ZStack {
Rectangle().fill(Color.clear)
self.activatingButton(frame: CGRect(x: 80, y: 30, width: 60, height: 40))
self.activatingButton(frame: CGRect(x: self.screen.maxX - 80, y: 30, width: 60, height: 40))
self.activatingButton(frame: CGRect(x: self.screen.maxX - 80, y: self.screen.maxY - 60, width: 60, height: 40))
self.activatingButton(frame: CGRect(x: 80, y: self.screen.maxY - 60, width: 60, height: 40))
if self.showingView {
self.topView
.zIndex(1)
.transition(
AnyTransition.scale(scale: 0.12).combined(with:
AnyTransition.offset(x: self.btFrame.origin.x - g.size.width/2.0,
y: self.btFrame.origin.y - g.size.height/2.0))
)
}
}
}
}
func activatingButton(frame: CGRect) -> some View {
Button(action: {
withAnimation {
self.btFrame = frame
self.showingView.toggle()
}
}) {
Text("Tap")
.padding()
.background(Color.yellow)
}
.position(frame.origin)
}
var topView: some View {
Rectangle()
.fill(Color.green)
.frame(width: 300, height: 400)
}
}
The above sample code should give you an idea on scaling an image
struct ImageCustomScaling: View {
// image to be scaled
var scaleImage: Image
// scale ratio
var scaleTo: Double
#State private var start = false
var body: some View {
VStack {
scaleImage
.font(.title)
.scaleEffect(self.start ? CGFloat(scaleTo) : 1)
.opacity(self.start ? 1 : 0)
.animation(Animation.interpolatingSpring(stiffness: 25, damping: 5, initialVelocity: 10).delay(0.9))
}
.foregroundColor(.blue)
.onAppear {
self.start = true
}
}
}
Above can be called by something like below
ImageCustomScaling(scaleImage: Image(systemName: "cloud.fill"), scaleTo: 5 )
Added animation modifier to give visual cue. You can change it to meet your needs.
Now in 2021 we finally got an answer from Apple. matchedGeometryEffect solves this problem elegantly and easily.
Here is a little screenshot of what it does:
And here is the code for this demo:
struct Stackoverflow: View {
#Namespace var namespace
#State private var shown: Bool = false
var body: some View {
VStack {
Button {
shown.toggle()
} label: {
Text("Tap Me!\nI'm a red square.")
.multilineTextAlignment(.center)
}
.background(
Color.red
.matchedGeometryEffect(id: "1", in: namespace, properties: .frame))
Spacer()
if shown {
Color.clear
.aspectRatio(contentMode: .fit)
.matchedGeometryEffect(id: "1", in: namespace, properties: .size)
.border(Color.gray)
}
Spacer()
}
.animation(.easeInOut(duration: 1))
}
}
struct Stackoverflow_Previews: PreviewProvider {
static var previews: some View {
Stackoverflow()
}
}
Check here on swift-lab.com, for more information and explanation on how to use matchedGeometryEffect.

Anchor point in SwiftUI

I'm playing with SwiftUI animations. I wanted to make a square bigger modifying its frame with an animation this way:
struct ContentView: View {
let squareWidth = CGFloat(100)
let squareHeight = CGFloat(100)
#State private var bigger = false
var body: some View {
VStack {
Color.green
.frame(width: bigger ? self.squareWidth * 2 : self.squareWidth, height: self.squareHeight)
.animation(.default)
Button(action: {
self.bigger.toggle()
}) {
Text("Click me!")
}
}
}
}
The animation happens from the centre of the View, that is the anchor point (0.5, 0.5). Is there a way to change this anchor point? I found out that I can use scaleEffect on the view specifying an anchor:
struct ContentView: View {
let squareWidth = CGFloat(100)
let squareHeight = CGFloat(100)
#State private var bigger = false
var body: some View {
VStack {
Color.green
.frame(width: self.squareWidth, height: self.squareHeight)
.scaleEffect(x: bigger ? 2 : 1, y: 1, anchor: .leading)
.animation(.default)
Button(action: {
self.bigger.toggle()
}) {
Text("Click me!")
}
}
}
}
But it doesn't seem exactly the same. If I needed to apply several effects I should specify the anchor point for each of them, instead of specifying the anchor point on the view just one time. On UIKit I could write:
var myView = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
myView.layer.anchorPoint = CGPoint(x: 0, y: 0.5) //that is, the .leading of SwiftUI
It is possible, but your approach must change.
There is an implied, built-in centring of views in SwiftUI, which can be confusing.
The parent view is the one placing the content in the centre, recalculating the position of its subviews each time their frames change.
Embedding the VStack in a HStack and adding borders to both clearly shows this behaviour.
Also, note the Spacer(), which pushes the view to the left.
HStack {
VStack {
Color.green
.frame(width: bigger ? self.squareWidth * 2 : self.squareWidth)
.frame(height: self.squareHeight)
.animation(.default)
Button("Click me!") { self.bigger.toggle() }
}.border(Color.black)
Spacer()
}.border(Color.red)
Using the .layer.anchorPoint in UIKit should have the same effect as using scaleEffect/offset/rotationEffect in SwiftUI, as it should affect the origin of the underlying geometry/texture which is being rendered by the GPU.
If you have a complex view (composed of multiple subviews) that needs the same scale effect, you can group them using a Group { } and use .scaleEffect on it.

Resources