I'm trying to implement an app where the user can swipe to the left and the right to go to a different horizontal ScrollViews. In my app I use GeometryReader to detect if the current active ScrollView should change. I didn't include that in this example.
I noticed that very strange things happen if the offset of the HStack is changing with an animation and the device gets rotated to landscape mode. Not only does the app crash, the phone becomes completely unresponsive, too!
I tried this on iOS 13.5 and with 13.6. iOS 14 doesn't even recognizes the simultaneousGesture (why's that?). I think the .offset causes the problem.
Does anybody have a solution? I couldn't find another way to implement what I described at the beginning but I'd like to know about easier ways.
Thanks!
struct ContentView: View {
#State var offset: CGFloat = UIScreen.main.bounds.width/2
var body: some View {
HStack{
ScrollView(.horizontal){
Text("Test")
}
.simultaneousGesture(DragGesture()
.onChanged{ translation in
print("triggered")
if(translation.predictedEndTranslation.width < 50){
withAnimation(.easeInOut(duration: 5)){
self.offset = -UIScreen.main.bounds.width/2
}
}
}
)
.frame(width: UIScreen.main.bounds.width)
ScrollView(.horizontal){
Text("Test2")
}
.simultaneousGesture(DragGesture()
.onChanged{ translation in
print("triggered")
if(translation.predictedEndTranslation.width > 50){
withAnimation(.easeInOut(duration: 5)){
self.offset = UIScreen.main.bounds.width/2
}
}
}
)
}
.frame(width: UIScreen.main.bounds.width * 2)
.offset(x: self.offset)
}
}
Another example with .position used instead of .offset (works on iOS 14 but not below):
struct ContentView: View {
#State var offset: CGFloat = UIScreen.main.bounds.width
var body: some View {
HStack{
ScrollView(.horizontal){
Button(action: {
withAnimation(.easeInOut(duration: 5)){
self.offset = 0
}
})
{
Circle()
.foregroundColor(.green)
.frame(width: 100, height: 100)
}
}
.frame(width: UIScreen.main.bounds.width)
ScrollView(.horizontal){
Button(action: {
withAnimation(.easeInOut(duration: 5)){
self.offset = UIScreen.main.bounds.width
}
})
{
Circle()
.frame(width: 100, height: 100)
.foregroundColor(.red)
}
} .frame(width: UIScreen.main.bounds.width)
}
.frame(width: UIScreen.main.bounds.width * 2)
.position(x: self.offset, y: 300)
}
}
Related
So, I saw many swipe to buy buttons around the web and on many apps. I thought it would be good to implement onto my app. But for some reason, the ZStack isn't functioning properly, the background is on top of the swipe arrow. I can't properly place it inside of another view. Furthermore, how would one perform an action when the swipable arrow is fully at the end?
import SwiftUI
struct ContentView: View {
#State var viewState = CGSize(width: 0, height: 50)
var body: some View {
ZStack{
/// This part is the background of the swipe to buy
RoundedRectangle(cornerRadius: 10)
.offset(x: viewState.width)
.frame(width: 800,height: 100)
HStack{
// This part is the actual swiping arrow
ZStack {
RoundedRectangle(cornerRadius: 30)
.fill(Color.blue)
.frame(width: 100, height: 100)
.offset(x: viewState.width)
.gesture(
DragGesture().onChanged { value in
viewState = value.translation
}
.onEnded { value in
withAnimation(.spring()) {
viewState = .zero
}
}
)
Image(systemName: "chevron.right")
.offset(x: viewState.width)
.gesture(
DragGesture().onChanged { value in
viewState = value.translation
}
.onEnded { value in
withAnimation(.spring()) {
viewState = .zero
}
}
)
}
Spacer()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
'''
Your ZStack is working fine, the issue is you are giving the top RoundedRectangle fix width = 800 and you use Spacer() to push the HStack to the leading side. You have to remove this width in order to work this fine, like this:
RoundedRectangle(cornerRadius: 10)
.offset(x: viewState.width)
.frame(height: 100)
But if you want fix width you can use background() instead of ZStack, like this:
HStack{
ZStack {
RoundedRectangle(cornerRadius: 30)
.fill(Color.blue)
.frame(width: 100, height: 100)
.offset(x: viewState.width)
.gesture(
DragGesture().onChanged { value in
viewState = value.translation
}
.onEnded { value in
withAnimation(.spring()) {
viewState = .zero
}
}
)
Image(systemName: "chevron.right")
.offset(x: viewState.width)
.gesture(
DragGesture().onChanged { value in
viewState = value.translation
}
.onEnded { value in
withAnimation(.spring()) {
viewState = .zero
}
}
)
}
Spacer()
}.background(
RoundedRectangle(cornerRadius: 10)
.offset(x: viewState.width)
.frame(width: 800,height: 100)
)
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
I'm trying to animate in a view and make it appear as if it's a sort of drawer opening from another view. This is all fine except if the first view is not opaque. It appears that you can see the animating view the moment it begins animating. Is there a way to clip this so it appears that the view is growing from the top of the bottom view?
Even without opacity this is an issue if where you're animating in from isn't a covered (demoed in second gif)
Sample Code:
struct ContentView: View {
#State private var showingSecondView: Bool = false
var body: some View {
VStack(spacing: 0) {
Spacer()
if showingSecondView {
ZStack {
Color.green.opacity(0.25)
Text("Second View")
}
.frame(width: 300, height: 300)
.transition(.move(edge: .bottom))
}
ZStack {
Color.black.opacity(1)
Text("First View")
}
.frame(width: 300, height: 300)
Button("Animate In / Out") {
showingSecondView.toggle()
}
.padding()
}
.animation(.easeInOut, value: showingSecondView)
}
}
It is possible to do by clipping exact container of 'drawer'. Here is a demo of possible approach.
Tested with Xcode 13.2 / iOS 15.2 (Simulator slow animation is ON for better demo)
var body: some View {
VStack(spacing: 0) {
Spacer()
VStack {
if showingSecondView {
ZStack {
Color.green.opacity(0.25)
Text("Second View")
}
.transition(.move(edge: .bottom))
} else {
Color.clear // << replacement for transition visibility
}
}
.frame(width: 300, height: 300)
.animation(.easeInOut, value: showingSecondView) // << animate drawer !!
.clipped() // << clip drawer area
ZStack {
Color.black.opacity(0.2)
Text("First View")
}
.frame(width: 300, height: 300)
Button("Animate In / Out") {
showingSecondView.toggle()
}
.padding()
}
}
Here a way for you:
struct ContentView: View {
#State private var isSecondViewPresented: Bool = false
var body: some View {
VStack(spacing: 0) {
Spacer()
ZStack {
Color.green.opacity(0.25).cornerRadius(20)
Text("Second View")
}
.frame(width: 300, height: 300)
.offset(y: isSecondViewPresented ? 0 : 300)
.clipShape(RoundedRectangle(cornerRadius: 20))
ZStack {
Color.black.opacity(0.1).cornerRadius(20)
Text("First View")
}
.frame(width: 300, height: 150)
Button("Animate In / Out") {
isSecondViewPresented.toggle()
}
.padding()
}
.animation(.easeInOut, value: isSecondViewPresented)
}
}
I am trying to implement an animation to symbolize the “swipe down”. It's basically a circle scaling and moving down. I've done it but it only works with a tap gesture to start the animation and I want to run the animation right after the content appear using onAppear.
So, this works fine (starts in the middle):
Circle()
.foregroundColor(.gray)
.frame(width: 60, height: 60)
.shadow(radius: 10)
.offset(y: self.animated ? 350 : 0)
.scaleEffect(self.animated ? 0.33 : 1)
.onTapGesture {
withAnimation(Animation.linear(duration: 1).delay(0.8).repeatForever(autoreverses: false)) {
self.animated.toggle()
}
}
This doesn't work:
Circle()
.foregroundColor(.gray)
.frame(width: 60, height: 60)
.shadow(radius: 10)
.offset(y: self.animated ? 350 : 0)
.scaleEffect(self.animated ? 0.33 : 1)
.onAppear {
withAnimation(Animation.linear(duration: 1).delay(0.8).repeatForever(autoreverses: false)) {
self.animated.toggle()
}
}
The animation start from the leading top but I thought using onAppear that every element was already in place.
Any clue why the animation doesn't start where it should start?
UPDATE
Full content view code:
struct ContentView: View {
#State var animated = false
var body: some View {
NavigationView {
ZStack {
Circle()
.foregroundColor(.gray)
.frame(width: 60, height: 60)
.shadow(radius: 10)
.offset(y: self.animated ? 350 : 0)
.scaleEffect(self.animated ? 0.33 : 1)
.onAppear {
withAnimation(Animation.linear(duration: 1).delay(0.8).repeatForever(autoreverses: false)) {
self.animated.toggle()
}
}
}
}
}
}
Just found out what was the issue. For some reason NavigationView was messing with the initial layout of the Circle. I just removed it and things started working as expected.
Final code:
struct ContentView: View {
#State var animated = false
var body: some View {
ZStack {
Circle()
.foregroundColor(.gray)
.frame(width: 60, height: 60)
.shadow(radius: 10)
.offset(y: self.animated ? 350 : 0)
.scaleEffect(self.animated ? 0.33 : 1)
.onAppear {
withAnimation(Animation.linear(duration: 1).delay(0.8).repeatForever(autoreverses: false)) {
self.animated.toggle()
}
}
}
}
}
My goal is to make sure Text in a container to scale according to its parent. It works well when the container only contains one Text view, as following:
import SwiftUI
struct FontScalingExperiment: View {
var body: some View {
Text("Hello World ~!")
.font(.system(size: 500))
.minimumScaleFactor(0.01)
.lineLimit(1)
.padding()
.background(
RoundedRectangle(cornerRadius: 20)
.fill(Color.yellow)
.scaledToFill()
)
}
}
struct FontScalingExperiment_Previews: PreviewProvider {
static var previews: some View {
Group {
FontScalingExperiment()
.previewLayout(.fixed(width: 100, height: 100))
FontScalingExperiment()
.previewLayout(.fixed(width: 200, height: 200))
FontScalingExperiment()
.previewLayout(.fixed(width: 300, height: 300))
FontScalingExperiment()
.previewLayout(.fixed(width: 400, height: 400))
}
}
}
the result:
However, when we have more complex View, we cant use same approach to automatically scale the text based on its parent size, for example:
import SwiftUI
struct IndicatorExperiment: View {
var body: some View {
VStack {
HStack {
Text("Line 1")
Spacer()
}
Spacer()
VStack {
Text("Line 2")
Text("Line 3")
}
Spacer()
Text("Line 4")
}
.padding()
.background(
RoundedRectangle(cornerRadius: 20)
.fill(Color.yellow)
)
.aspectRatio(1, contentMode: .fit)
}
}
struct IndicatorExperiment_Previews: PreviewProvider {
static var previews: some View {
Group {
IndicatorExperiment()
.previewLayout(.fixed(width: 100, height: 100))
IndicatorExperiment()
.previewLayout(.fixed(width: 200, height: 200))
IndicatorExperiment()
.previewLayout(.fixed(width: 300, height: 300))
IndicatorExperiment()
.previewLayout(.fixed(width: 400, height: 400))
}
}
}
Simply adding these 3 modifiers:
.font(.system(size: 500))
.minimumScaleFactor(0.01)
.lineLimit(1)
wont produce result like the first example; Text enlarged beyond the frame.
I did successfully, produce the result that I want by using GeometryReader then scale the font size based on geometry.size.width. Is this the only approach for achieving the desired result in SwiftUI?
You can try make all the Texts the same height. To do this you will need to set the padding and spacing explicitly, so this will scale rather than the fixed default values.
Also, the Spacer() didn't make much sense here - if the requirement was that all the Text stay the same size, the Spacer would just make all the text small. For Text to scale based on space, and where Spacer tries to use as much space as possible, it's a contradiction. Instead, I decided to just set the VStack's spacing in the initializer.
Working code:
struct IndicatorExperiment: View {
private let size: CGFloat
private let padding: CGFloat
private let primarySpacing: CGFloat
private let secondarySpacing: CGFloat
private let textHeight: CGFloat
init(size: CGFloat) {
self.size = size
padding = size / 10
primarySpacing = size / 15
secondarySpacing = size / 40
let totalHeights = size - padding * 2 - primarySpacing * 2 - secondarySpacing
textHeight = totalHeights / 4
}
var body: some View {
VStack(spacing: primarySpacing) {
HStack {
scaledText("Line 1")
Spacer()
}
.frame(height: textHeight)
VStack(spacing: secondarySpacing) {
scaledText("Line 2")
scaledText("Line 3")
}
.frame(height: textHeight * 2 + secondarySpacing)
scaledText("Line 4")
}
.padding(padding)
.background(
RoundedRectangle(cornerRadius: 20)
.fill(Color.yellow)
)
.aspectRatio(1, contentMode: .fit)
.frame(width: size, height: size)
}
private func scaledText(_ content: String) -> some View {
Text(content)
.font(.system(size: 500))
.minimumScaleFactor(0.01)
.lineLimit(1)
.frame(height: textHeight)
}
}
Code to test with:
struct ContentView: View {
var body: some View {
ScrollView {
VStack(spacing: 50) {
IndicatorExperiment(size: 100)
IndicatorExperiment(size: 200)
IndicatorExperiment(size: 300)
IndicatorExperiment(size: 400)
}
}
}
}
Result:
Using GeometryReader and a .minimumScaleFactor modifier would probably the only way to scale text in a view. To have more control on sizing, one possible way is to provde the .frame size from the parent view.
Scalable Text View
GeometryReader { geo in
Text("Foo")
.font(
.system(size: min(geo.size.height, geo.size.width) * 0.95))
.minimumScaleFactor(0.05)
.lineLimit(1)
}
Parent View that uses the Scalable Text View
GeometryReader { geo in
ScaleableText()
.frame(width: geo.size.width, height: geo.size.height)
}