SwiftUI animated view weird transition when appears inside ZStack - swift

I have a problem regarding behavior of an animated loading view. The loading view shows up while the network call. I have an isLoading #Published var inside my viewModel and the ActivityIndicator is shown inside a ZStack in my view. The Activityindicator is a custom view where I animate a trimmed circle - rotate it. Whenever the activity indicator is shown inside my mainView it has a weird transition when appearing- it is transitioned from the top left corner to the center of the view. Does anyone know why is this happening? I attach the structs with the code and 3 pictures with the behavior.
ActivityIndicator:
struct OrangeActivityIndicator: View {
var style = StrokeStyle(lineWidth: 6, lineCap: .round)
#State var animate = false
let orangeColor = Color.orOrangeColor
let orangeColorOpaque = Color.orOrangeColor.opacity(0.5)
init(lineWidth: CGFloat = 6) {
style.lineWidth = lineWidth
}
var body: some View {
ZStack {
CircleView(animate: $animate, firstGradientColor: orangeColor, secondGradientColor: orangeColorOpaque, style: style)
}.onAppear() {
self.animate.toggle()
}
}
}
struct CircleView: View {
#Binding var animate: Bool
var firstGradientColor: Color
var secondGradientColor: Color
var style: StrokeStyle
var body: some View {
Circle()
.trim(from: 0, to: 0.7)
.stroke(
AngularGradient(gradient: .init(colors: [firstGradientColor, secondGradientColor]), center: .center), style: style
)
.rotationEffect(Angle(degrees: animate ? 360 : 0))
.transition(.opacity)
.animation(Animation.linear(duration: 0.7) .repeatForever(autoreverses: false), value: animate)
}
}
The view is use it in :
struct UserProfileView: View {
#ObservedObject var viewModel: UserProfileViewModel
#Binding var lightMode: ColorScheme
var body: some View {
NavigationView {
ZStack {
VStack(alignment: .center, spacing: 12) {
HStack {
Text(userProfileEmail)
.font(.headline)
.foregroundColor(Color(UIColor.label))
Spacer()
}.padding(.bottom, 16)
SettingsView(userProfile: $viewModel.userProfile, isDarkMode: $viewModel.isDarkMode, lightMode: $lightMode, location: viewModel.locationManager.address, viewModel: viewModel)
ButtonsView( userProfile: $viewModel.userProfile)
Spacer()
}.padding([.leading, .trailing], 12)
if viewModel.isLoading {
OrangeActivityIndicator()
.frame(width: 40, height: 40)
// .animation(nil)
}
}
}
}
}
I also tried with animation nil but it doesn't seem to work.
Here are the pictures:

Here is a possible solution - put it over NavigationView. Tested with Xcode 12.4 / iOS 14.4
NavigationView {
// .. your content here
}
.overlay( // << here !!
VStack {
if isLoading {
OrangeActivityIndicator()
.frame(width: 40, height: 40)
}
}
)

Related

ScrollView stops components from expanding

I would like to have my cards expandable and fill the while area of the screen while they are doing the change form height 50 to the whole screen (and don't display the other components)
Here is my code:
import SwiftUI
struct DisciplineView: View {
var body: some View {
ScrollView(showsIndicators: false) {
LazyVStack {
Card(cardTitle: "Notes")
Card(cardTitle: "Planner")
Card(cardTitle: "Homeworks / Exams")
}
.ignoresSafeArea()
}
}
}
struct DisciplineV_Previews: PreviewProvider {
static var previews: some View {
DisciplineView()
}
}
import SwiftUI
struct Card: View {
#State var cardTitle = ""
#State private var isTapped = false
var body: some View {
RoundedRectangle(cornerRadius: 30, style: .continuous)
.stroke(style: StrokeStyle(lineWidth: 5, lineCap: .round, lineJoin: .round))
.foregroundColor(.gray.opacity(0.2))
.frame(width: .infinity, height: isTapped ? .infinity : 50)
.background(
VStack {
cardInfo
if(isTapped) { Spacer() }
}
.padding(isTapped ? 10 : 0)
)
}
var cardInfo: some View {
HStack {
Text(cardTitle)
.font(.title).bold()
.foregroundColor(isTapped ? .white : .black)
.padding(.leading, 10)
Spacer()
Image(systemName: isTapped ? "arrowtriangle.up.square.fill" : "arrowtriangle.down.square.fill")
.padding(.trailing, 10)
.onTapGesture {
withAnimation {
isTapped.toggle()
}
}
}
}
}
struct Card_Previews: PreviewProvider {
static var previews: some View {
Card()
}
}
here is almost the same as I would like to have, but I would like the first one to be on the whole screen and stop the ScrollView while appearing.
Thank you!
Described above:
I would like to have my cards expandable and fill the while area of the screen while they are doing the change form height 50 to the whole screen (and don't display the other components)
I think this is pretty much what you are trying to achieve.
Basically, you have to scroll to the position of the recently presented view and disable the scroll. The scroll have to be disabled enough time to avoid continuing to the next item but at the same time, it have to be enabled soon enough to give the user the feeling that it is scrolling one item at once.
struct ContentView: View {
#State private var canScroll = true
#State private var itemInScreen = -1
var body: some View {
GeometryReader { geo in
ScrollViewReader { proxy in
ScrollView {
LazyVStack {
ForEach(0...10, id: \.self) { item in
Text("\(item)")
.onAppear {
withAnimation {
proxy.scrollTo(item)
canScroll = false
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
canScroll = true
}
}
}
}
.frame(width: geo.size.width, height: geo.size.height)
.background(Color.blue)
}
}
}
.disabled(!canScroll)
}
.ignoresSafeArea()
}
}

swiftUI geometryEffect animation

I'm practicing swiftui geometryEffect by applying it to a transition from view to another. The first view has three circles with different colors, the user selects a color by tapping the desired color, and clicks "next" to go to second view which contains a circle with the selected color.
GeometryEffect works fine when transitioning from FirstView to SecondView in which the selected circle's positions animates smoothly, but going back is the problem.
GeometryEffect does not animate the circle position smoothly when going back from SecondView to FirstView. Instead, it seems like it moves the circle to the left and right before positioning the circle to its original position.
Im sharing a GIF on my google drive to show what I mean:
I'd like to achieve something like this: desired result
(file size is too large to be uploaded directly)
Thank you!
FirstView:
struct FirstView: View {
#Namespace var namespace
#State var whichStep: Int = 1
#State var selection: Int = 0
var colors: [Color] = [.red, .green, .blue]
#State var selectedColor = Color.black
var transitionNext: AnyTransition = .asymmetric(
insertion: .move(edge: .trailing),
removal:.move(edge: .leading))
var transitionBack: AnyTransition = .asymmetric(
insertion: .move(edge: .leading),
removal:.move(edge: .trailing))
#State var transition: AnyTransition = .asymmetric(
insertion: .move(edge: .trailing),
removal:.move(edge: .leading))
var body: some View {
VStack {
HStack { // back and next step buttons
Button { // back button
print("go back")
withAnimation(.spring()) {
whichStep = 1
transition = transitionBack
}
} label: {
Image(systemName: "arrow.backward")
.font(.system(size: 20))
}
.padding()
Spacer()
Button { // next button
withAnimation(.spring()) {
whichStep = 2
transition = transitionNext
}
} label: {
Text("next step")
}
}
Spacer()
if whichStep == 1 {
ScrollView {
ForEach(0..<colors.count, id:\.self) { color in
withAnimation(.none) {
Circle()
.fill(colors[color])
.frame(width: 100, height: 100)
.matchedGeometryEffect(id: color, in: namespace)
.onTapGesture {
selectedColor = colors[color]
selection = color
}
}
}
}
Spacer()
} else if whichStep == 2 {
// withAnimation(.spring()) {
ThirdView(showScreen: $whichStep, namespace: namespace, selection: $selection, color: selectedColor)
.transition(transition)
// }
Spacer()
}
}
.padding()
}
}
SecondView:
struct ThirdView: View {
var namespace: Namespace.ID
#Binding var selection: Int
#State var color: Color
var body: some View {
VStack {
GeometryReader { geo in
Circle()
.fill(color)
.frame(width: 100, height: 100)
.matchedGeometryEffect(id: selection, in: namespace)
}
}
.ignoresSafeArea()
}
}

SwiftUI Custom View repeat forever animation show as unexpected

I created a custom LoadingView as a Indicator for loading objects from internet. When add it to NavigationView, it shows like this
enter image description here
I only want it showing in the middle of screen rather than move from top left corner
Here is my Code
struct LoadingView: View {
#State private var isLoading = false
var body: some View {
Circle()
.trim(from: 0, to: 0.8)
.stroke(Color.primaryDota, lineWidth: 5)
.frame(width: 30, height: 30)
.rotationEffect(Angle(degrees: isLoading ? 360 : 0))
.onAppear {
withAnimation(.linear(duration: 1).repeatForever(autoreverses: false)) {
self.isLoading.toggle()
}
}
}
}
and my content view
struct ContentView: View {
var body: some View {
NavigationView {
LoadingView()
.frame(width: 30, height: 30)
}
}
}
This looks like a bug of NavigationView: without it animation works totally fine. And it wan't fixed in iOS15.
Working solution is waiting one layout cycle using DispatchQueue.main.async before string animation:
struct LoadingView: View {
#State private var isLoading = false
var body: some View {
Circle()
.trim(from: 0, to: 0.8)
.stroke(Color.red, lineWidth: 5)
.frame(width: 30, height: 30)
.rotationEffect(Angle(degrees: isLoading ? 360 : 0))
.onAppear {
DispatchQueue.main.async {
withAnimation(.linear(duration: 1).repeatForever(autoreverses: false)) {
self.isLoading.toggle()
}
}
}
}
}
This is a bug from NavigationView, I tried to kill all possible animation but NavigationView ignored all my try, NavigationView add an internal animation to children! here all we can do right now!
struct ContentView: View {
var body: some View {
NavigationView {
LoadingView()
}
}
}
struct LoadingView: View {
#State private var isLoading: Bool = Bool()
var body: some View {
Circle()
.trim(from: 0, to: 0.8)
.stroke(Color.blue, lineWidth: 5.0)
.frame(width: 30, height: 30)
.rotationEffect(Angle(degrees: isLoading ? 360 : 0))
.animation(Animation.linear(duration: 1).repeatForever(autoreverses: false), value: isLoading)
.onAppear { DispatchQueue.main.async { isLoading.toggle() } }
}
}

SwiftUI: Understanding .sheet / .fullScreenCover lifecycle when using constant vs #Binding initializers

I'm trying to understand how and when the .sheet and .fullScreenCover initializers are called. Below is a minimal reproducible example, where the first screen has 3 colored rectangles and the SecondView (shown via .fullScreenCover) has 1 rectangle that changes color based on the selected color from the first screen.
When the app first loads, the color is set to .gray.
If I tap on the green rectangle, SecondView presents with a gray rectangle. (ie. the color DIDN'T change correctly).
If I then dismiss the SecondView and tap on the red rectangle, the SecondView presents with a red rectangle. (ie. the color DID change correctly.)
So, I'm wondering why this set up does NOT work on the initial load, but does work on the 2nd/3rd try?
Note: I understand this can be solved by changing the 'let selectedColor' to a #Binding variable, that's not what I'm asking.
Code:
import SwiftUI
struct SegueTest: View {
#State var showSheet: Bool = false
#State var color: Color = .gray
var body: some View {
HStack {
RoundedRectangle(cornerRadius: 25)
.fill(Color.red)
.frame(width: 100, height: 100)
.onTapGesture {
color = .red
showSheet.toggle()
}
RoundedRectangle(cornerRadius: 25)
.fill(Color.green)
.frame(width: 100, height: 100)
.onTapGesture {
color = .green
showSheet.toggle()
}
RoundedRectangle(cornerRadius: 25)
.fill(Color.orange)
.frame(width: 100, height: 100)
.onTapGesture {
color = .orange
showSheet.toggle()
}
}
.fullScreenCover(isPresented: $showSheet, content: {
SecondView(selectedColor: color)
})
}
}
struct SecondView: View {
#Environment(\.presentationMode) var presentationMode
let selectedColor: Color // Should change to #Binding
var body: some View {
ZStack {
Color.black.edgesIgnoringSafeArea(.all)
RoundedRectangle(cornerRadius: 25)
.fill(selectedColor)
.frame(width: 300, height: 300)
}
.onTapGesture {
presentationMode.wrappedValue.dismiss()
}
}
}
struct SegueTest_Previews: PreviewProvider {
static var previews: some View {
SegueTest()
}
}
See comments and print statements. Especially the red
import SwiftUI
struct SegueTest: View {
#State var showSheet: Bool = false{
didSet{
print("showSheet :: didSet")
}
willSet{
print("showSheet :: willSet")
}
}
#State var color: Color = .gray{
didSet{
print("color :: didSet :: \(color.description)")
}
willSet{
print("color :: willSet :: \(color.description)")
}
}
#State var refresh: Bool = false
init(){
print("SegueTest " + #function)
}
var body: some View {
print(#function)
return HStack {
//Just to see what happens when you recreate the View
//Text(refresh.description)
Text(color.description)
RoundedRectangle(cornerRadius: 25)
.fill(Color.red)
.frame(width: 100, height: 100)
.onTapGesture {
print("SegueTest :: onTapGesture :: red")
//Changing the color
color = .red
//Refreshed SegueTest reloads function
//refresh.toggle()
showSheet.toggle()
}
RoundedRectangle(cornerRadius: 25)
.fill(Color.green)
.frame(width: 100, height: 100)
.onTapGesture {
print("SegueTest :: onTapGesture :: green")
//Changing the color
color = .green
showSheet.toggle()
}
RoundedRectangle(cornerRadius: 25)
.fill(Color.orange)
.frame(width: 100, height: 100)
.onTapGesture {
print("SegueTest :: onTapGesture :: orange")
//Changing the color
color = .orange
showSheet.toggle()
}
}
//This part is likely created when SegueTest is created and since a struct is immutable it keeps the original value
.fullScreenCover(isPresented: $showSheet, content: {
SecondView(selectedColor: color)
})
}
}
struct SecondView: View {
#Environment(\.presentationMode) var presentationMode
//struct is immutable
let selectedColor: Color // Should change to #Binding
init(selectedColor: Color){
print("SecondView " + #function)
self.selectedColor = selectedColor
print("SecondView :: struct :: selectedColor = \(self.selectedColor.description)" )
print("SecondView :: parameter :: selectedColor = \(selectedColor.description)" )
}
var body: some View {
ZStack {
Color.black.edgesIgnoringSafeArea(.all)
RoundedRectangle(cornerRadius: 25)
.fill(selectedColor)
.frame(width: 300, height: 300)
}
.onTapGesture {
presentationMode.wrappedValue.dismiss()
}
}
}
struct SegueTest_Previews: PreviewProvider {
static var previews: some View {
SegueTest()
}
}
The problem is that you are not using your #State color inside SegueView, which will not reload your view on State change. Just include your #State somewhere in the code, which will force it to rerender at then update the sheet, with the correct color.
RoundedRectangle(cornerRadius: 25)
.fill(color == .red ? Color.red : Color.red) //<< just use dump color variable here
If you do not include your color state somewhere in the code, it won't re render your SegueView, hence you still pass the old color gray to your SecondView.
Or even pass your colors as Binding your your Second View..
SecondView(selectedColor: $color)
#Binding var selectedColor: Color

How to position a List using ZStack & Geometry?

I'm trying to position a ListView directly below a text field, using a ZStack and the field's geometry - its position and size. This is with a view toward creating an autocomplete picklist
Setting the offset only appears to half work.
So far it looks as follows.
Emulator and device appears as on the right:
Some useful information here:
https://swiftui-lab.com/geometryreader-to-the-rescue/
import SwiftUI
struct TestView: View {
#State private var firstname = ""
#State private var lastname = ""
#State private var townCity = ""
#State private var rect: CGRect = CGRect()
var body: some View {
ZStack (alignment: .topLeading){
VStack{
Form {
Section {
TextField("Firstname", text: $firstname)
TextField("Lastname", text: $lastname)
ZStack{
VStack (alignment: .leading, spacing: 0){
TextField("Town/City", text: self.$townCity)
.background(GeometryGetterV2(rect: $rect))
}
.border(Color.black, width: 1)
}
Button("Save") {
}
}
}
}
SelectionsPickerV2()
.offset(x: rect.origin.x, y: rect.origin.y)
//.offset(x: rect.minX, y: rect.minY)
.frame(
width: rect.size.width,
height: rect.size.height * 7)
}
.coordinateSpace(name: "myZstack")
}
}
struct SelectionsPickerV2: View {
var body: some View {
VStack (alignment: .leading) {
List{
Text("Sydney, Australia")
Text("New York, New York")
Text("London, UK")
Text("Paris, France")
}
.background(Color.blue)
}
.border(Color.red, width: 1)
}
}
struct TestView_Previews: PreviewProvider {
static var previews: some View {
TestView()
}
}
struct GeometryGetterV2: View {
#Binding var rect: CGRect
var body: some View {
return GeometryReader { geometry in
self.makeView(geometry: geometry)
}
}
func makeView(geometry: GeometryProxy) -> some View {
DispatchQueue.main.async {
self.rect = geometry.frame(in: .named("myZstack"))
//self.rect = geometry.frame(in: .local)
//self.rect = geometry.frame(in: .global)
}
return Rectangle().fill(Color.clear)
}
}
What Xcode version are you using, it looks nice with mine (Xcode 11.2, beta 11B41).
As many mentioned, your ZStack being the top level view gets geometry calculations wrong as the navigation bar gets added, etc. Probably just another SwiftUI bug.
To fix it just embed your ZStack inside a new parent view.
struct TestView: View {
// ...
var body: some View {
VStack { // New parent view
ZStack (alignment: .topLeading){
// ...
}
SelectionsPickerV2()
.offset(x: rect.origin.x, y: rect.origin.y)
.frame(
width: rect.size.width,
height: rect.size.height * 7)
}
.coordinateSpace(name: "myZstack") // Fixed coordinate space
}
}
}