SwiftUI - Animation in view stops when scrolling the list - list

Considering the following code, why the animations in the views that are initialized without the n property stops when you scroll the list?
Tested on Xcode 11.3 (11C29) with a new default project on device and simulator.
import SwiftUI
struct ContentView: View {
var body: some View {
HStack {
List(1...50, id: \.self) { n in
HStack {
KeepRolling()
Spacer()
KeepRolling(n: n)
}
}
}
}
}
struct KeepRolling: View {
#State var isAnimating = false
var n: Int? = nil
var body: some View {
Rectangle()
.frame(width: 50, height: 50)
.rotationEffect(Angle(degrees: self.isAnimating ? 360 : 0))
.onAppear {
withAnimation(Animation.linear(duration: 2).repeatForever(autoreverses: false)) {
self.isAnimating = true
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

IMO it is due to caching/reuse in List. For List all the values of KeepRolling() is the same, so .onAppear is not always re-called.
If to make every such view unique, say using .id as below, all works (tested with Xcode 11.2)
KeepRolling().id(UUID().uuidString)

Related

LazyGridView how to detect and act on item overflowing screen?

I have a grid of items. Each item can expand height. I want to autoscroll when the item is expanded so it doesn't overflow the screen.
I was successful with the following code but I had to revert to a hack.
The idea was to detect when the item is overflowing using a Geometry reader on the item's background. Works wonders.
The issue is that when the view is expanded , the geo reader will update after the condition to check if autoscroll should execute is ran by the dispatcher. Hence my ugly hack.
Wonder what is the proper way ?
import SwiftUI
struct BlocksGridView: View {
private var gridItemLayout = [GridItem(.adaptive(minimum: 300, maximum: .infinity), spacing: 20)]
var body: some View {
ZStack{
ScrollView {
ScrollViewReader { value in
LazyVGrid(columns: gridItemLayout, spacing: 20) {
ForEach((0..<20), id: \.self) {
BlockView(cardID: $0,scrollReader: value).id($0)
}
}
}
.padding(20)
}
}
}
}
struct BlockView : View {
var cardID : Int
var scrollReader : ScrollViewProxy
#State private var isOverflowingScreen = false
#State private var expand = false
var body: some View {
ZStack{
Rectangle()
.foregroundColor(isOverflowingScreen ? Color.blue : Color.green)
.frame(height: expand ? 300 : 135)
.clipShape(Rectangle()).cornerRadius(14)
.overlay(Text(cardID.description))
.background(GeometryReader { geo -> Color in
DispatchQueue.main.async {
if geo.frame(in: .global).maxY > UIScreen.main.bounds.maxY {
isOverflowingScreen = true
} else {
isOverflowingScreen = false
}
}
return Color.clear
})
.onTapGesture {
expand.toggle()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { // <-- Hack :(
if isOverflowingScreen {
withAnimation{
scrollReader.scrollTo(cardID)
}
}
}
}
}
}
}
struct BlocksGridView_Previews: PreviewProvider {
static var previews: some View {
BlocksGridView()
}
}
Blue items are overflowing ...

SwiftUI View doesn't change when using button and #State / #Binding

Long story short this is an onboarding view after a user goes through auth. My main navigation of the app uses navigationview but I can't use that for onboarding. I've put a fullscreencover over the main screen for this onboarding stuff.
However, when trying to simply navigate between the views from different files between the onboarding screens, the button doesn't show the other view. I've tried everything under the son from #Appstorage and #Environment stuff but can't get it to work.
Also please note I cut off the bottom of the rest of the file as that had nothing to do with this logic.
import SwiftUI
struct OnboardingTestView: View {
#State var shouldShowOnboarding = true
var body: some View {
if shouldShowOnboarding {
OffsetChoicesView(shouldShowOnboarding: $shouldShowOnboarding)
}
if shouldShowOnboarding == false {
PersonalInfoView()
}
}
}
struct OnboardingTestView_Previews: PreviewProvider {
static var previews: some View {
OnboardingTestView()
}
}
//*********************************************************************
//OffsetChoicesView
struct OffsetChoicesView: View {
#Binding var shouldShowOnboarding: Bool
var body: some View {
ZStack {
Color(#colorLiteral(red: 0.9803921569, green: 0.9568627451, blue: 0.9568627451, alpha: 1)).edgesIgnoringSafeArea(/*#START_MENU_TOKEN#*/.all/*#END_MENU_TOKEN#*/)
VStack {
Progress1View()
.padding(.bottom, 40)
.padding(.top)
Spacer()
Button(action: {
shouldShowOnboarding = false
}) {
NextButtonView()
.padding(.top)
.padding(.bottom)
}
}
You can try something like this - utilizes #AppStorage
I was not quite sure what the OffsetChoiceView() parameters are supposed to be.. so I just removed them in the example. This should be whatever your onboarding view is called.
import SwiftUI
struct OnboardingTestView: View {
//#State var shouldShowOnboarding = true
#AppStorage("showOnboarding") var showOnboarding: Bool = true
var body: some View {
if (showOnboarding == true) {
OffsetChoicesView()
} else if (showOnboarding == false) {
PersonalInfoView()
}
}
}
struct OnboardingTestView_Previews: PreviewProvider {
static var previews: some View {
OnboardingTestView()
}
}
//*********************************************************************
//OffsetChoicesView
struct OffsetChoicesView: View {
//#Binding var shouldShowOnboarding: Bool
#AppStorage("showOnboarding") var showOnboarding: Bool = false
var body: some View {
ZStack {
Color(#colorLiteral(red: 0.9803921569, green: 0.9568627451, blue: 0.9568627451, alpha: 1)).edgesIgnoringSafeArea(/*#START_MENU_TOKEN#*/.all/*#END_MENU_TOKEN#*/)
VStack {
Progress1View()
.padding(.bottom, 40)
.padding(.top)
Spacer()
Button(action: {
showOnboarding = false
}) {
NextButtonView()
.padding(.top)
.padding(.bottom)
}
}

Fade-in/out animation with a boolean flag

I am trying to implement a simple "tap to toggle the visibility of the UI" in SwiftUI with fade in/out animation. The following code animates the fade-in effect of the Text element as I expected, but it immediately hides the Text element when isVisible become false.
I'd like to understand why this code does not work, and how to fix it in the most natural way.
import SwiftUI
struct ContentView: View {
#State var isVisible = true
var body: some View {
ZStack {
Rectangle()
.foregroundColor(.blue)
.gesture(TapGesture(count: 1).onEnded {
withAnimation(.easeInOut(duration: 1.0)) {
isVisible.toggle()
}
})
if isVisible {
Text("Tap me!")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I'm using Xcode 12.5 on Big Sur, and my iPhone is running iOS 14.5.1.
Thanks to Erik Philips, here is the answer.
import SwiftUI
struct ContentView: View {
#State var isVisible = true
var body: some View {
ZStack {
Rectangle()
.zIndex(1)
.foregroundColor(.blue)
.gesture(TapGesture(count: 1).onEnded {
withAnimation(.easeInOut(duration: 1.0)) {
isVisible.toggle()
}
})
if isVisible {
Text("Tap me!")
.zIndex(2)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

TabView causing preview to crash

I am currently trying to make a featured games section in my app. I am using the TabView in SwiftUI to do this, but am running into an issue where the preview crashes from it. I am not getting any errors and also am unable to run it live. Below is the code of the view causing the preview crash.
import SwiftUI
struct FeaturedGamesView: View {
var numberOfImages: Int
#ObservedObject var featuredGames: GameQuery
#State private var currentIndex: Int = 0
private let timer = Timer.publish(every: 3, on: .main, in: .common).autoconnect()
var body: some View {
VStack(alignment: .leading) {
Text("Featured")
.font(.headline)
.padding(.leading, 15)
.padding(.top, 1)
HStack(alignment: .top, spacing: 0) {
TabView() {
ForEach(featuredGames.games.results) { game in
NavigationLink(destination: NavigationLazyView(GameDetailsView(gameDetailsQuery: GameQuery(gameName: game.slug)))){
GameItem(game: game)
}
}
}
.offset(x: (CGFloat(self.currentIndex) * -185), y: 0)
.animation(.spring())
.onReceive(self.timer) { _ in
self.currentIndex = (self.currentIndex + 1) % 19
}
}
.frame(height: 200)
.tabViewStyle(PageTabViewStyle())
.indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always))
}
}
}
struct FeaturedGamesView_Previews: PreviewProvider {
static var previews: some View {
FeaturedGamesView(numberOfImages: 3, featuredGames: GameQuery(gameCategory: GameCategory.featured))
}
}
From some of my experimentation it seems to either not like the observable object I am using to get an array of data to populate the tabview. Any help would be greatly appreciated.
Update: I created a simpler example which still produces the error. In this case everything is generic except the array being used which is an observable object. Switching out the data structure used in the ForEach fixes it, but am looking to understand why my observable object does not work here.
import SwiftUI
struct Test: View {
#State private var selection = 0
#ObservedObject var featuredGames: GameQuery
var body: some View {
VStack(alignment: .leading) {
Text("Featured")
.font(.headline)
.padding(.leading, 15)
.padding(.top, 1)
HStack {
TabView() {
ForEach(featuredGames.games.results) { game in
Text("Hi")
}
}.frame(height: 170)
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .automatic))
.indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always))
}
}
}
}
struct Test_Previews: PreviewProvider {
static var previews: some View {
Test(featuredGames: GameQuery(gameCategory: GameCategory.featured))
}
}
Please see below code.
I fixed below part to simple then it works.
Maybe this has a problem.
ForEach(featuredGames.games.results) { game in
NavigationLink(destination: NavigationLazyView(GameDetailsView(gameDetailsQuery: GameQuery(gameName: game.slug)))){
GameItem(game: game)
}
}
Working code (I made my self simple GameQuery observableObject)
import SwiftUI
struct FeaturedGamesView: View {
var numberOfImages: Int
#ObservedObject var featuredGames: GameQuery
#State private var currentIndex: Int = 0
private let timer = Timer.publish(every: 3, on: .main, in: .common).autoconnect()
var body: some View {
VStack(alignment: .leading) {
Text("Featured")
.font(.headline)
.padding(.leading, 15)
.padding(.top, 1)
HStack(alignment: .top, spacing: 0) {
TabView() {
ForEach(featuredGames.results, id:\.self) { game in
NavigationLink(destination: VStack {}){
Text("Hello")
}
}
}
.offset(x: (CGFloat(self.currentIndex) * -185), y: 0)
.animation(.spring())
.onReceive(self.timer) { _ in
self.currentIndex = (self.currentIndex + 1) % 19
}
}
.frame(height: 200)
.tabViewStyle(PageTabViewStyle())
.indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always))
}
}
}
final class GameQuery: ObservableObject {
var gameCagetory: Int = 0
var results: [Int] = [1,2,3]
}
struct FeaturedGamesView_Previews: PreviewProvider {
static var previews: some View {
FeaturedGamesView(numberOfImages: 3, featuredGames: GameQuery())
}
}

SwiftUI: popover to persist (not be dismissed when tapped outside)

I created this popover:
import SwiftUI
struct Popover : View {
#State var showingPopover = false
var body: some View {
Button(action: {
self.showingPopover = true
}) {
Image(systemName: "square.stack.3d.up")
}
.popover(isPresented: $showingPopover){
Rectangle()
.frame(width: 500, height: 500)
}
}
}
struct Popover_Previews: PreviewProvider {
static var previews: some View {
Popover()
.colorScheme(.dark)
.previewDevice("iPad Pro (12.9-inch) (3rd generation)")
}
}
Default behaviour is that is dismisses, once tapped outside.
Question:
How can I set the popover to:
- Persist (not be dismissed when tapped outside)?
- Not block screen when active?
My solution to this problem doesn't involve spinning your own popover lookalike. Simply apply the .interactiveDismissDisabled() modifier to the parent content of the popover, as illustrated in the example below:
import SwiftUI
struct ContentView: View {
#State private var presentingPopover = false
#State private var count = 0
var body: some View {
VStack {
Button {
presentingPopover.toggle()
} label: {
Text("This view pops!")
}.popover(isPresented: $presentingPopover) {
Text("Surprise!")
.padding()
.interactiveDismissDisabled()
}.buttonStyle(.borderedProminent)
Text("Count: \(count)")
Button {
count += 1
} label: {
Text("Doesn't block other buttons too!")
}.buttonStyle(.borderedProminent)
}
.padding()
}
}
Tested on iPadOS 16 (Xcode 14.1), demo video included below:
Note: Although it looks like the buttons have lost focus, they are still interact-able, and might be a bug as such behaviour doesn't exist when running on macOS.
I tried to play with .popover and .sheet but didn't found even close solution. .sheet can present you modal view, but it blocks parent view. So I can offer you to use ZStack and make similar behavior (for user):
import SwiftUI
struct Popover: View {
#State var showingPopover = false
var body: some View {
ZStack {
// rectangles only for color control
Rectangle()
.foregroundColor(.gray)
Rectangle()
.foregroundColor(.white)
.opacity(showingPopover ? 0.75 : 1)
Button(action: {
withAnimation {
self.showingPopover.toggle()
}
}) {
Image(systemName: "square.stack.3d.up")
}
ModalView()
.opacity(showingPopover ? 1: 0)
.offset(y: self.showingPopover ? 0 : 3000)
}
}
}
// it can be whatever you need, but for arrow you should use Path() and draw it, for example
struct ModalView: View {
var body: some View {
VStack {
Spacer()
ZStack {
Rectangle()
.frame(width: 520, height: 520)
.foregroundColor(.white)
.cornerRadius(10)
Rectangle()
.frame(width: 500, height: 500)
.foregroundColor(.black)
}
}
}
}
struct Popover_Previews: PreviewProvider {
static var previews: some View {
Popover()
.colorScheme(.dark)
.previewDevice("iPad Pro (12.9-inch) (3rd generation)")
}
}
here ModalView pops up from below and the background makes a little darker. but you still can touch everything on your "parent" view
update: forget to show the result:
P.S.: from here you can go further. For example you can put everything into GeometryReader for counting ModalView position, add for the last .gesture(DragGesture()...) to offset the view under the bottom again and so on.
You just use .constant(showingPopover) instead of $showingPopover. When you use $ it uses binding and updates your #State variable when you press outside the popover and closes your popover. If you use .constant(), it will just read the value from you #State variable, and will not close the popover.
Your code should look like this:
struct Popover : View {
#State var showingPopover = false
var body: some View {
Button(action: {
self.showingPopover = true
}) {
Image(systemName: "square.stack.3d.up")
}
.popover(isPresented: .constant(showingPopover)) {
Rectangle()
.frame(width: 500, height: 500)
}
}
}