Add animations to ForEach loop elements (SwiftUI) - swift

Is there any way I can add animation when elements of a ForEach loop appears or disappears?
I have tried using withAnimation{} and .animation() in many ways but they didn't seem to work
Here is some code (Xcode 11 beta 5):
import SwiftUI
struct test: View {
#State var ContentArray = ["A","B","C"]
var body: some View {
ScrollView{
VStack{
ForEach(ContentArray.indices, id: \.self){index in
ZStack{
// Object
Text(self.ContentArray[index])
.frame(width:100,height:100)
.background(Color.gray)
.cornerRadius(20)
.padding()
//Delete button
Button(action: {
self.ContentArray.remove(at: index)
}){
Text("✕")
.foregroundColor(.white)
.frame(width:40,height:40)
.background(Color.red)
.cornerRadius(100)
}.offset(x:40,y:-40)
}
}
}
}
}
}
#if DEBUG
struct test_Previews: PreviewProvider {
static var previews: some View {
test()
}
}
#endif
As can be seen below, without animations everything feels super abrupt. Any solution is really appreciated
Important notice: The layout should change in the same way List does when the number of elements changes. For example, every object automatically moves top when a top object is deleted

It looks like this problem is still up to day (Xcode 11.4), because by just copy-pasting the observed effect is the same. So, there are a couple of problems here: first, it needs to setup combination of animation and transition correctly; and, second, the ForEach container have to know which exactly item is removed, so items must be identified, instead of indices, which are anonymous.
As a result we have the following effect (transition/animation can be others):
struct TestAnimationInStack: View {
#State var ContentArray = ["A","B","C", "D", "E", "F", "G", "I", "J"]
var body: some View {
ScrollView{
VStack{
ForEach(Array(ContentArray.enumerated()), id: \.element){ (i, item) in // << 1) !
ZStack{
// Object
Text(item)
.frame(width:100,height:100)
.background(Color.gray)
.cornerRadius(20)
.padding()
//Delete button
Button(action: {
withAnimation { () -> () in // << 2) !!
self.ContentArray.remove(at: i)
}
}){
Text("✕")
.foregroundColor(.white)
.frame(width:40,height:40)
.background(Color.red)
.cornerRadius(100)
}.offset(x:40,y:-40)
}.transition(AnyTransition.scale) // << 3) !!!
}
}
}
}
}

Related

How can I track scrolling with a ScrollView linked to a Custom PageControl - SwiftUI

I want to create a Carousel with SwiftUI(without using TabView)
with a matching/linked Page Control in SwiftUI
So far I have both views and can update the pageControl view with a
#State var pagecontrolTracker updated with a DragGesture() .onChanged but it doesn't update the PageControl if I scroll fast, or sometimes doesn't update at all 😭.
If I Scroll slow tho, the Page Control does update sometimes as expected.
Is there a better way to update this faster and smoother?
I saw .updating modifier for DragGesture() but this doesn't work either
Full View:
struct ContentView: View {
#State var pagecontrolTracker: Int = 0
var body: some View {
VStack {
ScrollView(.horizontal) {
HStack {
ForEach(0...3, id: \.self) { index in
PagingRow()
.gesture(DragGesture().onChanged({ _ in
pagecontrolTracker = index
}))
}
}
}
PagingControls(pagecontrolTracker: $pagecontrolTracker)
}
.padding()
}
}
Inside Custom SwiftUI Row View
struct PagingRow: View {
var body: some View {
VStack {
HStack {
Image(systemName: "globe")
Text("Test Title")
}
.padding()
Button {
print("Test action")
} label: {
Text("Tap Me")
}
.buttonStyle(.borderedProminent)
.padding()
}
.background(Color.orange)
.frame(width: 200)
.cornerRadius(8)
}
}
Custom PageControl in SwiftUI
struct PagingControls: View {
#Binding var pagecontrolTracker: Int
var body: some View {
HStack {
ForEach(0...3, id: \.self) { pagingIndex in
Circle()
.fill(pagecontrolTracker == pagingIndex ? .orange : .black)
.frame(width: 8, height: 8)
}
}
}
}
Note: I don't want to use TabView since I want to be able to show the next upcoming card in the scrollView
A TabView would only show one card per page

What does List var in NavigationLink mean in SwiftUI

The following is my code for my simple contentView
struct ContentView: View {
#State private var selection = 1;
#State private var addFood = false;
var listItems = [
Food(name: "List Item One"),
Food(name: "List Item Two")
]
var body: some View {
TabView(selection: $selection) {
NavigationView {
List(listItems){
food in NavigationLink(destination: FoodView(selec: selection)) {
Text(food.name)
}
}.navigationBarTitle(Text("Fridge Items"), displayMode: .inline)
.navigationBarItems(trailing:
NavigationLink(destination: FoodView(selec: selection)) {
Image(systemName: "plus.circle").resizable().frame(width: 22, height: 22)
} )
}
.tabItem {
Image(systemName: "house.fill")
Text("Home")
}
.tag(1)
Text("random tab")
.font(.system(size: 30, weight: .bold, design: .rounded))
.tabItem {
Image(systemName: "bookmark.circle.fill")
Text("profile")
}
.tag(0)
}
}
}
struct FoodView: View{
var selec: Int?
var body: some View{
NavigationView{
Text("food destination view \(selec!)");
}
}
}
I follow some tutorials to get to this point. However, I'm confused about the syntax
List(listItems){
food in NavigationLink(destination: FoodView(selec: selection)) {
Text(food.name)
}
What does the above line mean? My guess is that we list out the items we have, and then for each food we have, we add a navigation link for them. However, that's my guess and I want to know the true syntax behind this. I've already read the documentation about List and also go through the official documentation about swift syntax. I didn't find anything useful. I thought it was a closure first, but I found there is still a difference.
Could anyone help, please.
List is a struct which is a View, and it takes a closure as a parameter when initialising itself.
You can image this closure as a function , so that this function is kind of like this.
func imagineFunc(item: YourItem) -> YourRowContent {
//some code goes here
return YourRowContent
}
Note: above function is only for explanation. there are no such types.
so , when ListView wants to create one of its rows , then List call this given closure(imagineFunc) and get made a RowContent/row view.
According to your code when List view call the given closure(for each row) you have return a NavigationLink(You can imagine this as a button which can navigate to another view when it is inside navigation view). So that each row becomes a NavigationLink in your List.

is it possible get List array to load horizontally in swiftUI?

Do I need to dump using List and just load content into a Scrollview/HStack or is there a horizontal equivalent to stack? I would like to avoid having to set it up differently, but am willing todo so if there is no alternative... it just means recoding multiple other views.
current code for perspective:
import SwiftUI
import Combine
struct VideoList: View {
#Environment(\.presentationMode) private var presentationMode
#ObservedObject private(set) var viewModel: ViewModel
#State private var isRefreshing = false
var btnBack : some View { Button(action: {
self.presentationMode.wrappedValue.dismiss()
}) {
HStack {
Image("Home") // set image here
.aspectRatio(contentMode: .fit)
.foregroundColor(.white)
}
}
}
var body: some View {
NavigationView {
List(viewModel.videos.sorted { $0.id > $1.id}, id: \.id) { video in
NavigationLink(
destination: VideoDetails(viewModel: VideoDetails.ViewModel(video: video))) {
VideoRow(video: video)
}
}
.onPullToRefresh(isRefreshing: $isRefreshing, perform: {
self.viewModel.fetchVideos()
})
.onReceive(viewModel.$videos, perform: { _ in
self.isRefreshing = false
})
}
.onAppear(perform: viewModel.fetchVideos)
.navigationViewStyle(StackNavigationViewStyle())
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: btnBack)
}
}
In general, List is List and it by design is vertical-only. For all horizontal case we should use ScrollView+HStack or ScrollView+LazyHStack (SwiftUI 2.0).
Anyway here is a simple demo of possible way that can be applicable in some particular cases. Prepared & tested with Xcode 12 / iOS 14.
Note: all tuning and alignments fixes are out of scope - only possibility demo.
struct TestHorizontalList: View {
let data = Array(1...20)
var body: some View {
GeometryReader { gp in
List {
ForEach(data, id: \.self) {
RowDataView(item: $0)
.rotationEffect(.init(degrees: 90)) // << rotate content back
}
}
.frame(height: gp.size.width) // initial fit in screen
.rotationEffect(.init(degrees: -90)) // << rotate List
}
}
}
struct RowDataView: View {
let item: Int
var body: some View {
RoundedRectangle(cornerRadius: 25.0).fill(Color.blue)
.frame(width: 80, height: 80)
.overlay(
Text("\(item)")
)
}
}

ForEach onDelete Mac

I am trying to figure out how to delete from an array in swiftui for Mac. All the articles that I can find show how to do it the UIKit way with the .ondelete method added to the foreach. This is does not work for the Mac because I do not have the red delete button like iOS does. So how do I delete items from an array in ForEach on Mac.
here is the code that I have tried, which gives me the error
Fatal error: Index out of range: file
import SwiftUI
struct ContentView: View {
#State var splitItems:[SplitItem] = [
SplitItem(id: 0, name: "Item#1"),
SplitItem(id: 1, name: "Item#2")
]
var body: some View {
VStack {
Text("Value: \(self.splitItems[0].name)")
ForEach(self.splitItems.indices, id:\.self) { index in
HStack {
TextField("Item# ", text: self.$splitItems[index].name)
Button(action: {self.removeItem(index: index)}) {Text("-")}
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
func removeItem(index:Int) {
self.splitItems.remove(at: index)
}
}
struct SplitItem:Identifiable {
var id:Int
var name:String
}
Your ForEach code part is completely correct.
The possible error source could be the line
Text("Value: \(self.splitItems[0].name)") - after deleting all items this will definitely crash.
The next assumption is the VStack. After Drawing a view hierarchy it will not allow to change it.
If you replace the VStack with Group and the next code will work:
var body: some View {
Group {
if !self.splitItems.isEmpty {
Text("Value: \(self.splitItems[0].name)")
} else {
Text("List is empty")
}
ForEach(self.splitItems.indices, id:\.self) { index in
HStack {
TextField("Item# ", text: self.$splitItems[index].name)
Button(action: {self.removeItem(index: index)}) {Text("-")}
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
so I was able to find the answer in this article,
instead of calling array.remove(at) by passing the index from the for loop call it using array.FirstIndex(where).

SwiftUI List disable cell press

I am using xCode 11 beta 7 with SwiftUI.
I have a simple list which each list element has several buttons. Currently when the user presses the cell(not the buttons) it is highlighting the back of the list cell(probably not the correct terminology for SwiftUI).
How do i disable this behaviour? I could not locate an obvious api to disable it.
List {
HStack {
Group {
Button(action: {}) {
Text("Read").padding(5)
}.onTapGesture {
print("1")
}
.padding()
.background(Color.blue)
.cornerRadius(5)
}
Group {
Button(action: {}) {
Text("Notify").padding(5)
}.onTapGesture {
print("2")
}
.padding()
.background(Color.purple)
.cornerRadius(5)
}
Group {
Button(action: {}) {
Text("Write").padding(5)
}.onTapGesture {
print("3")
}
.padding()
.background(Color.yellow)
.cornerRadius(5)
}
}
}
Same answer as in How to remove highlight on tap of List with SwiftUI?
I know I'm a bit late, but it's for those of you who are searching (like me 😇)
What I found
I guess you should take a look at the short article How to disable the overlay color for images inside Button and NavigationLink from #TwoStraws
Just add the .buttonStyle(PlainButtonStyle()) modifier to your item in the List and you'll have what you wanted. It also makes the Buttons work again in the List, which is another problem I encountered.
A working example for Swift 5.1 :
import Combine
import SwiftUI
struct YourItem: Identifiable {
let id = UUID()
let text: String
}
class YourDataSource: ObservableObject {
let willChange = PassthroughSubject<Void, Never>()
var items = [YourItem]()
init() {
items = [
YourItem(text: "Some text"),
YourItem(text: "Some other text")
]
}
}
struct YourItemView: View {
var item: YourItem
var body: some View {
VStack(alignment: .leading) {
Text(item.text)
HStack {
Button(action: {
print("Like")
}) {
Image(systemName: "heart.fill")
}
Button(action: {
print("Star")
}) {
Image(systemName: "star.fill")
}
}
}
.buttonStyle(PlainButtonStyle())
}
}
struct YourListView: View {
#ObservedObject var dataSource = YourDataSource()
var body: some View {
List(dataSource.items) { item in
YourItemView(item: item)
}
.navigationBarTitle("List example", displayMode: .inline)
.edgesIgnoringSafeArea(.bottom)
}
}
#if DEBUG
struct YourListView_Previews: PreviewProvider {
static var previews: some View {
YourListView()
}
}
#endif
As said in the article, it also works with NavigationLinks. I hope it helped some of you 🤞🏻
This is my simplest solution that is working for me (lo and behold, I'm building my first app in Swift and SwiftUI as well as this being my first post on SO):
Wherever you have buttons, add .buttonStyle(BorderlessButtonStyle())
Button(action:{}){
Text("Hello")
}.buttonStyle(BorderlessButtonStyle())
Then on your list, add .onTapGesture {return}
List{
Text("Hello")
}.onTapGesture {return}
Instead of using a button try it with a gesture instead
Group {
Text("Notify").padding(5).gesture(TapGesture().onEnded() {
print("action2")
})
.padding()
.background(Color.purple)
.cornerRadius(5)
}