Question about .gesture(drag gesture) in swift ui - ios

I am trying to detect when this text view has been swiped on. The code compiles fine, but I am not able to trigger the swipe on my actual device. When I swipe, nothing happens. Tap seems to work just fine. Can anyone let me know what I'm doing wrong in my code?
In case this matters, I'm developing a watch OS app in swift 5.3 with the latest Xcode.
var body: some View {
Text(tempstring).onTapGesture { checkStateRoll() }
.frame(maxWidth: .infinity, maxHeight: .infinity)
.gesture(DragGesture(minimumDistance: 10, coordinateSpace: .global)
.onEnded { value in
let horizontalAmount = value.translation.width as CGFloat
let verticalAmount = value.translation.height as CGFloat
if abs(horizontalAmount) > abs(verticalAmount) {
horizontalAmount < 0 ? leftswipe() : rightswipe()
} else {
verticalAmount < 0 ? upswipe() : downswipe()
}
tempstring = String(numdice) + "d" + String(typesofdice[typedice])
speaknumber()
} )
.background(progstate == 2 ? Color.blue : Color.red)
}
}
Thanks a lot for any help. This has been stumping me for weeks.

A couple of things:
With respect to layout, setting a frame will no change the text size because the text sizes itself to its content. Use a greedy view like a ZStack if you want to take up all of the space.
The sequence of the modifiers does matter. You read the order in which they are applied from bottom to top, so in this case they need to go on the stack.
Here is an example playground:
import SwiftUI
import PlaygroundSupport
struct V: View {
var body: some View {
ZStack {
Color.red
Text("Test")
}
.onTapGesture { print("tap") }
.gesture(DragGesture(minimumDistance: 10, coordinateSpace: .global).onEnded { print($0)})
}
}
let host = UIHostingController(rootView: V().frame(width: 500.0, height: 500.0))
host.preferredContentSize = CGSize(width: 300, height: 300)
PlaygroundPage.current.liveView = host

Related

SwiftUI iOS15 Image being animated from top left corner instead of cetner

I ma having an unusual issue.I have a loading screen where the center image is suppose to give a heart beat animation. I am accomplishing this by using the scaleEffect. This works fine in iOS 16. However, in iOS 15 (and even 15.4) The animation is wrong.
What is happening is that the animation's reference point is in the top-left corner instead of the picture's center. Additionally, it appears that the entire view is animating rather then the picture.
Coud I get some feedback on my code? I believe that I am setting something incorrectly or maybe there is a better way of accomplishing what I want.
The issue is happening in the LoadingView struct
struct LoadingView: View{
#State private var animatingLogo = false
var body: some View{
// GeometryReader{geoReader in
VStack(alignment: .center, spacing: 6) {
Image("JHLogo")
.frame(width: 200, height: 200, alignment: .center)
.aspectRatio(contentMode: .fill)
.scaleEffect(animatingLogo ? 0.15 : 0.1, anchor: .center)
.onAppear{
withAnimation(.easeInOut(duration: 1).repeatForever()){
animatingLogo.toggle()
}
}
}
.navigationBarHidden(true)
}
}
And here is the top level view in case this is the one that has the issue:
// Initial view that is loaded on app startup
struct MainView: View{
#State private var isLoading = false
var body : some View {
GeometryReader{geoReader in
NavigationView {
if (isLoading) {
LoadingView()
}
else {
CoverPageView()
}
}
.frame(width: geoReader.size.width, height:geoReader.size.height)
.fixedSize()
.aspectRatio(contentMode: .fill)
.onAppear {
startLoadingScreen()
}
}
}
func startLoadingScreen() {
isLoading = true;
DispatchQueue.main.asyncAfter(deadline: .now() + 3){
isLoading = false
}
}
}

Swift UI - UltraThinMaterial Glitch on Moving Views

When I move a view with ultra thin material background, it turns black. Is it a bug or am I doing something wrong?
Is there a workaround to achieve this view on a moving view?
I noticed only happens when there is angular motion. If I delete rotation effect the problem goes away.
Testable code:
struct Test: View {
#State var offset: CGFloat = 0
#GestureState var isDragging: Bool = false
var body: some View {
GeometryReader { reader in
ZStack {
Image(systemName: "circle.fill")
.font(.largeTitle)
.frame(width: 300, height: 300)
.background(.red)
.overlay(alignment: .bottom) {
Rectangle()
.frame(height: 75)
.background(.ultraThinMaterial)
}
.clipShape(
RoundedRectangle(cornerRadius: 15, style: .continuous)
)
.compositingGroup()
.offset(x: offset)
.rotationEffect(.degrees(getRotation(angle: 8)))
.compositingGroup()
.gesture(
DragGesture()
.updating($isDragging) { _, state, _ in
state = true
}
.onChanged { value in
let translation = value.translation.width
offset = (isDragging ? translation : .zero)
}
.onEnded { value in
let width = getRect().width
let translation = value.translation.width
let checkingStatus = translation > 0 ? translation : -translation
withAnimation {
if checkingStatus > (width / 2) {
offset = (translation > 0 ? width : -width) * 2
} else {
offset = 0
}
}
}
)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
private func getRotation(angle: Double) -> Double {
let rotation = (offset / getRect().width) * angle
return rotation
}
}
Code is not testable so hard to say definitely, but
try to move all that image with modifiers into separated standalone view
try composite it
.clipShape(
RoundedRectangle(cornerRadius: 15, style: .continuous)
)
.compositingGroup() // << here !!
.offset(y: -topOffset)
For me, just adding .compositingGroup() modifier changed nothing, I still had a glitch
In my case, I also use blur, shadows and other modifiers as well as rotationEffect. Sure, the glitch is connected with rotation
I managed to solve the problem via changing the order or view modifiers. Make sure that rotationEffect(_:axes:) is used before others

Animation conflict issue with another animation

I have this LoaderView which grow with given value, I want add some highlight effect to it as well, So basically there is 2 deferent and separate animation happening, one for activeValue and one for the highlight. i did not combine them together because they are 2 deferent things, but my output of codes combine these 2 values together and destroys highlight effect. I am looking a solid answer for my issue to having this 2 animation works side by side.
The issue happens when i add value.
I want this view be usable in macOS as well, so there is a possibility user changes the Window size, then my view would be changed as well.
struct ContentView: View {
#State private var value: CGFloat = 50
var body: some View {
LoaderView(activeValue: value, totalValue: 100)
.padding()
Button("add") { value += 10 }
.padding()
}
}
struct LoaderView: View {
let activeValue: CGFloat
let totalValue: CGFloat
let activeColor: Color
let totalColor: Color
init(activeValue: CGFloat, totalValue: CGFloat, activeColor: Color = Color.green, totalColor: Color = Color.secondary) {
if activeValue >= totalValue { self.activeValue = totalValue }
else if activeValue < 0 { self.activeValue = 0 }
else { self.activeValue = activeValue }
self.totalValue = totalValue
self.activeColor = activeColor
self.totalColor = totalColor
}
#State private var startAnimation: Bool = .init()
private let heightOfCapsule: CGFloat = 5.0
var body: some View {
let linearGradient = LinearGradient(colors: [Color.yellow.opacity(0.1), Color.yellow], startPoint: .leading, endPoint: .trailing)
return GeometryReader { proxy in
totalColor
.overlay(
Capsule()
.fill(activeColor)
.frame(width: (activeValue / totalValue) * proxy.size.width)
.overlay(
Capsule()
.fill(linearGradient)
.frame(width: 100)
.offset(x: startAnimation ? 100 : -100),
alignment: startAnimation ? Alignment.trailing : Alignment.leading)
.clipShape(Capsule()),
alignment: Alignment.leading)
.clipShape(Capsule())
}
.frame(height: heightOfCapsule)
.onAppear(perform: { startAnimation.toggle() })
.animation(Animation.default, value: activeValue)
.animation(Animation.linear(duration: 3.0).repeatForever(autoreverses: false), value: startAnimation)
}
}
The issue isn't the .animations it is that .onAppear is only called once, so your animation never restarts. startAnimation needs to be toggled when activeValue changes. The way your animation works, toggling startAnimation reverses the animation direction. Therefore, I set autoreverses: true. It is a compromise either way.
struct LoaderView: View {
//Nothing changed in this portion
.onAppear{
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
startAnimation.toggle()
}
}
// The issue is that .onAppear() is only called once.
.onChange(of: activeValue, perform: { _ in
startAnimation.toggle()
})
.animation(Animation.default, value: activeValue)
// As a side effect, changing startAnimation to opposite values reverses the animation
// you have made, so setting autoreverses: true hides this.
.animation(Animation.linear(duration: 3.0).repeatForever(autoreverses: true), value: startAnimation)
}
}
Edit:
I apologize for not getting back to this sooner. The issue with the rubber band animation is because of the transition when the app firsts launches. To prevent this, you need to wrap your variable change in .onAppear() in a DispatchQueue.main.asyncAfter(deadline:) with enough time for the view to load. Then when you start the animation, it works correctly.

SwiftUI Animation similar to that of the Apple Podcasts app expanding player

So I'm trying to mimic the Apple Podcasts app expanding audio player.
So far I've added an overlay to my TabView and it works like a charm, se below:
TabView {
...
}.overlay(
PlayerView()
)
Now I want to achieve the expanding view/sheet similar to the gif above for my PlayerView(), how would I go about doing that in SwiftUI?
So I solved this issue after some time researching, was a bit of hassle getting the animation working smoothly but I think I got it in the end. See code below:
App.swift
TabView {
...
}.overlay(
FloatingPlayer()
.edgesIgnoringSafeArea(.all)
)
FloatingPlayer.swift
struct FloatingPlayer: View {
#State var viewState = CGSize.zero
#State var playerExpanded = false
var body: some View {
GeometryReader { geometry in
VStack {
if self.playerExpanded {
Spacer()
}
ZStack {
VisualEffectView(effect: UIBlurEffect(style: self.settings.playerExpanded ? .systemThickMaterialDark : .dark))
.frame(
width: geometry.size.width,
height: self.playerExpanded ? geometry.size.height + 10 : 60
)
}
.offset(y: self.viewState.height)
.gesture(DragGesture()
.onChanged { value in
if (self.playerExpanded) {
self.viewState = value.translation
if (value.translation.height > 200) {
self.playerExpanded = false
self.simpleSuccess()
}
}
}
.onEnded { value in
self.viewState = CGSize.zero
})
.onTapGesture {
if !self.playerExpanded {
self.playerExpanded.toggle()
}
}
}
.animation(.interactiveSpring(response: 0.5, dampingFraction: 0.9, blendDuration: 0.3))
.statusBar(hidden: true)
}
}
}
You would also need to play with some paddings in order for it to work perfectly.

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