Hello I'm looking to replicate a swiftui animation that I see around specifically to animate a view (like a button) from where it is in the hierarchy to full screen and then back.
Here is a demo
https://imgur.com/a/qG5iaAB
What I've tried
I don't have a lot to go on with this. I basically tried manipulating frame dimensions with gesture inputs, but this breaks down pretty quickly once you have anything else on the screen.
the below works with a single element and nothing else 🤷🏻♀️.
struct Expand: View {
#State private var dimy = CGFloat(100)
#State private var dimx = CGFloat(100)
#State private var zz = 0.0
#State private var offset = CGFloat(0)
let w = UIScreen.main.bounds.size.width
let h = UIScreen.main.bounds.size.height
var body: some View {
VStack(spacing: 0) {
HStack(spacing: 0) {
Image(systemName: "play")
.foregroundColor(Color.red)
.frame(width: dimx, height: dimy)
.background(Color.red.opacity(0.5))
.border(Color.red)
.offset(y: offset)
.gesture(DragGesture()
.onChanged { gesture in
let y = gesture.translation.height
offset = y
}
.onEnded { _ in
offset = 0
dimx = dimx == 100 ? UIScreen.main.bounds.size.width : 100
dimy = dimy == 100 ? UIScreen.main.bounds.size.height : 100
zz = zz == 0 ? 3 : 0
}
)
.animation(.default)
.position(x: w / 2, y: h / 2)
.zIndex(zz)
}
}
.edgesIgnoringSafeArea(.vertical)
}
}
Related
In my iOS app (which is using SwiftUI) I am using a scroll view in which there is vertical scrolling. My issue is: I want to scroll only from top to bottom, and not bottom to top.
Is that possible?
Thank you.
Use LazyVGrid
and DragGesture
when changed, you only allow scroll to bottom.
struct ContentView: View {
#State private var offset: CGFloat = .zero
var columns: [GridItem] = Array(repeating: .init(.flexible()), count: 2)
var body: some View {
LazyVGrid(columns: columns) {
ForEach((0...79), id: \.self) {
let codepoint = $0 + 0x1f600
let codepointString = String(format: "%02X", codepoint)
Text("\(codepointString)")
let emoji = String(Character(UnicodeScalar(codepoint)!))
Text("\(emoji)")
}
}
.font(.largeTitle)
.offset(y: offset)
.gesture(DragGesture().onChanged {value in
let translation = value.translation.height
if translation < 1 {
offset = translation
}
})
}
}
Update
You need to track the last offset when scrolling and reset it to zero each time you stop scrolling. This fixes the issue you have addressed in your comment.
struct ContentView: View {
#State private var offset: CGFloat = .zero
#State private var lastOffset: CGFloat = .zero
var columns: [GridItem] = Array(repeating: .init(.flexible()), count: 2)
var body: some View {
LazyVGrid(columns: columns) {
ForEach((0...79), id: \.self) {
let codepoint = $0 + 0x1f600
let codepointString = String(format: "%02X", codepoint)
Text("\(codepointString)")
let emoji = String(Character(UnicodeScalar(codepoint)!))
Text("\(emoji)")
}
}
.font(.largeTitle)
.offset(y: offset)
.gesture(DragGesture().onChanged {value in
let translation = value.translation.height
if translation <= lastOffset {
offset = translation
lastOffset = translation
}
}.onEnded { value in
lastOffset = .zero
})
}
}
You do not need to use LazyVGrid if you your Views are not too many.
I'm having some trouble layering iOS animations for my video recoding button. I've modeled it after other standard recording buttons and I'd like for it to begin as a solid circle and then on press, transform to a rounded rectangle where its opacity pulses.
The issue is that I've ordered the animations in a way such that their all applied at the same time.
https://imgur.com/a/xi7PxtH
I've tried reordering these animations but can't figure out which ordering with achieve the scope I want.
import SwiftUI
struct ContentView: View {
#State var recordComplete = false
#State private var rCorner: CGFloat = 100
#State private var rWidth: CGFloat = 70
#State private var rHeight: CGFloat = 70
#State private var opacityVal = 1.0
var body: some View {
HStack{
Button(action: {
self.rCorner = self.rCorner == 100 ? 12 : 100
self.rWidth = self.rWidth == 70 ? 45 : 70
self.rHeight = self.rHeight == 70 ? 45 : 70
self.recordComplete.toggle()
}) {
ZStack{
Circle()
.stroke(Color.red, lineWidth: 6)
.frame(width:85, height: 85)
RoundedRectangle(cornerRadius: rCorner)
.fill(Color.red)
.frame(width: rWidth, height: rHeight)
.opacity(opacityVal)
.animation(Animation.easeInOut(duration: 1).repeatForever(autoreverses: true)).onAppear{ self.opacityVal = 0.3 }
}
.padding(.vertical, 25)
}
}.animation(.spring(response: 0.5, dampingFraction: 2, blendDuration: 0.5))
}
}
Thank you as always for the help with this issue!
Here is a possible solution - the main idea is to separate animations by value and make them serial. Tested with Xcode 12 / iOS 14
struct ContentView: View {
#State var recordComplete = false
#State private var rCorner: CGFloat = 100
#State private var rWidth: CGFloat = 70
#State private var rHeight: CGFloat = 70
#State private var opacityVal = 1.0
var body: some View {
HStack{
Button(action: {
self.rCorner = self.rCorner == 100 ? 12 : 100
self.rWidth = self.rWidth == 70 ? 45 : 70
self.rHeight = self.rHeight == 70 ? 45 : 70
self.recordComplete.toggle()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.opacityVal = self.recordComplete ? 0.3 : 1.0
}
}) {
ZStack{
Circle()
.stroke(Color.red, lineWidth: 6)
.frame(width:85, height: 85)
RoundedRectangle(cornerRadius: rCorner)
.fill(Color.red)
.frame(width: rWidth, height: rHeight)
.opacity(opacityVal)
.animation(
self.recordComplete ?
Animation.easeInOut(duration: 1).repeatForever(autoreverses: true) :
Animation.default,
value: opacityVal)
}
.padding(.vertical, 25)
}
}.animation(.spring(response: 0.5, dampingFraction: 2, blendDuration: 0.5), value: rCorner)
}
}
I want to create a custom scroll that snaps views to middle
But I can't figure out how to set the offset correctly.
This is the sample of the scroll:
struct contentView: View {
#State var startOffset: CGFloat = 0
#State var translationWidth: CGFloat = 0
#State var offset: CGFloat = 0
#State var index: Int = 0
var body: some View {
GeometryReader { geo in
HStack {
ForEach(0..<5, id: \.self) { num in
Button(action: {
// some action
}) {
Circle()
}
.frame(width: geo.size.width)
.offset(x: -self.offset*CGFloat(self.index) + self.translationWidth)
}
}.frame(width: geo.size.width, height: 200)
.offset(x: self.startOffset )
.highPriorityGesture (
DragGesture(minimumDistance: 0.1, coordinateSpace: .global)
.onChanged({ (value) in
self.translationWidth = value.translation.width
})
.onEnded({ (value) in
if self.translationWidth > 40 {
self.index -= 1
} else if self.translationWidth < -40 {
self.index += 1
}
self.translationWidth = 0
})
)
.onAppear {
withAnimation(.none) {
let itemsWidth: CGFloat = CGFloat(geo.size.width*5) - (geo.size.width)
self.offset = geo.size.width
self.startOffset = itemsWidth/2
}
}
}
}
}
It works but it is slightly off the middle, it doesn't scroll exactly in the middle and I don't understand why.
The problem occurs with the onAppear closure:
.onAppear {
withAnimation(.none) {
let itemsWidth: CGFloat = CGFloat(geo.size.width*5) - (geo.size.width)
self.offset = geo.size.width
self.startOffset = itemsWidth/2
}
}
I might be missing some small pixels in my calculations.
UPDATE
So Apparently the HStack has a default value for spacing between views.
so to fix it you should remove it like so:
HStack(spacing: 0) {
....
}
So after a bit of experimenting, I realized the HStack is using a default spacing which is not equal to zero.
It means that in my calculations I didn't include the spacing between items
so to fix this all you need to add is:
HStack(spacing: 0) {
...
}
I have 25 buttons laid out in a grid for a game like this.
I'm generating these buttons like this:
VStack {
ForEach(1..<6) {_ in
HStack {
ForEach(1..<6) {_ in
Button(action: {
// Button clicked
doButton()
}) {
Rectangle()
.frame(width: 50, height: 50)
.border(Color.black, lineWidth: 1)
}
}
}
}
}
How can I make it so if the user drags their finger across multiple buttons, each one of the buttons are clicked, and doButton() is executed?
I tried using a DragGesture() but couldn't get it to work...
Swift 5.1, iOS 13.
I don't have a 100% solution for you, but I do have something to get you off the ground hopefully. These are not buttons, but simply squares. As I click/drag across them they^ll signal I passed by.
It isn't perfect, but should give you some thing more to work with.
I read a tutorial on medium on doing something similar with drag and drop. This one.
Can help but think you'll need to do something with drag here and the Geometry reader ultimately, detecting where they are and responding to that.
import SwiftUI
struct ContentView: View {
var body: some View {
return VStack {
ForEach(1..<6) {colY in
HStack {
ForEach(1..<6) {rowX in
boxView(row: rowX, col: colY)
}
}
}
}
}
}
struct boxView: View {
#State var row: Int
#State var col: Int
var body: some View {
let dragGesture = DragGesture(minimumDistance: 0, coordinateSpace: CoordinateSpace.global)
.onChanged { (value) in
print("trigger ",self.row,self.col)
}
return Rectangle()
.stroke(Color.black)
.frame(width: 50, height: 50)
.gesture(dragGesture)
}
}
import SwiftUI
class Container : ObservableObject {
#Published var location = CGPoint()
public func updateLocation(_ location: CGPoint) {
self.location = location
}
}
struct SliderGestureButtonsView: View {
#ObservedObject var container = Container()
var body: some View {
VStack(alignment: .center, spacing: 0) {
ForEach(1..<6) { colY in
HStack {
ForEach(1..<6) { rowX in
GeometryReader { gp in
let x = gp.frame(in:.named("hive")).midX
let y = gp.frame(in:.named("hive")).midY
let w = gp.frame(in:.named("hive")).size.width
let h = gp.frame(in:.named("hive")).size.height
let rect = CGRect(x: x, y: y, width: w, height: h)
boxView(row: rowX, col: colY, rect: rect, container: container)
}
}
}.frame(height: 60)
}
}.coordinateSpace(name: "hive").frame(width: 300)
.gesture(
DragGesture(minimumDistance: 0, coordinateSpace: CoordinateSpace.named("hive"))
.onChanged { (value) in
container.updateLocation(value.location)
}
)
}
}
struct boxView: View {
var row: Int
var col: Int
var rect: CGRect
#ObservedObject var container: Container
var body: some View {
let x = container.location.x
let y = container.location.y
let hw = rect.size.width / 2
let ox = rect.origin.x
let withinX = x > ox - hw && x < ox + hw
let hh = rect.size.height / 2
let oy = rect.origin.y
let withinY = y > oy - hh && y < oy + hh
var pressed = false
if(withinX && withinY) {
pressed = true
print("trigger ", self.row, self.col)
}
return Rectangle()
.stroke(Color.white)
.frame(width: 60, height: 60)
.background(pressed ? Color.green : Color.black)
}
}
I have a ZStack with an Image and a small circle, and some gestures for panning and zooming in/out on the Image. I want the small circle to change location based on where the user taps. Everything is working fine until I start zooming in with scaleEffect. How can I account for the current scale value when placing the circle? (See TODO: Fix tap point when scaled for where the issue is.)
struct PodMapTest: View {
#State var circlePosition = CGPoint.zero
#State var position = CGSize.zero
#State var scale: CGFloat = 1.0
#State var lastScaleValue: CGFloat = 1.0
#State var tapPoint: CGPoint = CGPoint.zero
#State var dragSize: CGSize = CGSize.zero
#State var lastDrag: CGSize = CGSize.zero
#GestureState private var dragOffset = CGSize.zero
var body: some View {
VStack {
Spacer()
ZStack {
Image("floorPlan")
.resizable()
self.circle
}
.scaledToFit()
.animation(.linear)
.scaleEffect(self.scale)
.offset(self.dragSize)
.gesture(MagnificationGesture().onChanged { val in
let delta = val / self.lastScaleValue
self.lastScaleValue = val
self.scale = self.scale * delta
}.onEnded { val in
// without this the next gesture will be broken
self.lastScaleValue = 1.0
}
)
.simultaneousGesture(DragGesture(minimumDistance: 1, coordinateSpace: .local).onChanged({ val in
print("dragging map")
self.tapPoint = val.startLocation
self.dragSize = CGSize(width: val.translation.width + self.lastDrag.width, height: val.translation.height + self.lastDrag.height)
})
.onEnded({ (val) in
self.dragSize = CGSize(width: val.translation.width + self.lastDrag.width, height: val.translation.height + self.lastDrag.height)
self.lastDrag = self.dragSize
}))
.simultaneousGesture(DragGesture(minimumDistance: 0, coordinateSpace: .local).onChanged({ (val) in
// TODO: Fix tap point when scaled
let tapPoint = CGPoint(x: val.startLocation.x - self.dragSize.width, y: val.startLocation.y - self.dragSize.height)
self.circlePosition = tapPoint
}))
Spacer()
}
}
var circle: some View {
Circle()
.frame(width: 20, height: 20, alignment: .center)
.position(circlePosition)
}
}