SwiftUI: How do you animate from multiple values? iOS 15 - swiftui

With iOS 15 you can no longer use .animation() without specifying the value you want to animate on. While this can be extremely useful you cannot animate on multiple values.
Example: If I want to animate the movement of two buttons, but the trigger is based on two values rather than just a simple bool. See code below.
VStack(spacing: 0) {
if self.gvm.canPlot && self.gvm.points.count > 0 {
Button {
self.gvm.points = self.gvm.points.dropLast()
self.gvm.hostFence = self.gvm.hostFence.dropLast()
} label: {
Image(systemName: "arrow.counterclockwise")
.font(.system(size: 19))
.padding(12)
}
} else if !self.gvm.canPlot {
Button {
self.gvm.satelliteMap.toggle()
} label: {
Image(systemName: self.gvm.satelliteMap ? "map.fill" : "map")
.font(.system(size: 19))
.padding(12)
}
}
Button {
self.gvm.canPlot.toggle()
} label: {
Image(systemName: self.gvm.canPlot ? "arrow.up.and.down.and.arrow.left.and.right" : "mappin.and.ellipse")
.font(.system(size: 19))
.padding(12)
}
}
.background(Color.theme.background)
.cornerRadius(10)
.shadow(color: Color.theme.background.opacity(0.5), radius: 5, x: 3, y: 3)
.animation(.spring(), value: self.gvm.canPlot)
As you can see, .canPlot and .points.count > 0 are both used to trigger UI movement. However, .animation only allows for one value. Of course I could change other code to make this work. EX. Add a secondary .animation - or even not specify a value and live with the deprecation warning until it becomes an error.
Is there any simple way to use .animation based on multiple values? The older .animation would animate everything on the view, good and bad. Please let me know how .animation is intended to be used in this case.
EDIT:
Currently I have it working with this code:
.animation(.spring(), value: self.gvm.canPlot)
.animation(.spring(), value: self.gvm.points)

Can you try something like
.animation(.spring(), value: (self.gvm.canPlot && self.gvm.points.count > 0))

Related

XCode SwiftUI - Why is my keypad toolbar doing this?

Very novice to the app development game. I am trying to put this toolbar above the .decimalPad and I cannot get this large gap to go away.
VStack {
Rectangle()
.foregroundColor(Color(UIColor.systemBackground))
.frame(height: 35)
.overlay {
HStack {
Spacer()
Button(action: {
isTextFieldFocused = false
}) { Text("Done")}
}
.offset(y: -3)
.padding(.trailing)
}
.opacity(isTextFieldFocused ? 1 : 0)
.ignoresSafeArea(.keyboard) //This makes sure the bottom tab bar stays below the keyboard.
}
I initially thought it was something in another view causing the spacing, but I managed to parse through the views in the canvas and it does it regardless.
Here is what I'd like it to look like, for reference.
What I want
To add a Button onto your keyboard, you use a .toolbar with the locations to .keyboard like this:
TextField("Enter Text", text: $text)
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Button(action: {
isTextFieldFocused = false
}) { Text("Done")}
// If you want it leading, then use a Spacer() after
Spacer()
}
}
You were overthinking it by adding the Rectangle. This is why we look for minimal reproducible examples. We can dial in the fix for your specific code.

How to get toolbar separator in SwiftUI?

I would like to get the separator in SwiftUI, but I didn't find the way. This was screenshot from mail.app.
If your view elements are in a HStack (like your mail.app suggest) using Divider() will give you a vertical "separator".
Elsewhere Divider() will give you a horizontal "separator".
You can adjust its size, like this: Divider().frame(width: 123)
You can of course do more things with Dividers, such as set its thickness or height with different color:
HStack {
Divider().frame(width: 5, height: 50).background(Color.blue)
Image(systemName: "line.3.horizontal.decrease.circle")
Divider().frame(width: 10, height: 100).background(Color.pink)
Image(systemName: "envelope")
Divider().frame(width: 15, height: 150).background(Color.green)
}
Here is the right way of doing such thing, do not use Divider, because it has lots of issues. With Divider you cannot control the thickness, also it has issue with updating color, wired Xcode complain in console in some cases, also space issue, it takes more space than it needs. In general it does not worth to use it.
struct ContentView: View {
var body: some View {
HStack {
Group {
Image(systemName: "mail")
Capsule().fill(Color.secondary).frame(width: 2.0)
Image(systemName: "trash")
}
.frame(width: 25, height: 25)
}
}
}
One alternative solution that may be more useful in some cases (e.g if you want a customisable toolbar the accepted solution won't work):
ToolbarItem (placement: .primaryAction) {
HStack {
Divider()
}
}

Is there a way in SwiftUI to fulfill scrolling animations between 2 numbers

I'm looking for a similar way https://github.com/stokatyan/ScrollCounter in SwiftUI
This is a very rudimentary build of the same thing. I'm not sure if you're wanting to do it on a per-digit basis, however this should give you a solid foundation to work off of. The way that I'm handling it is by using a geometry reader. You should be able to easily implement this view by utilizing an HStack for extra digits/decimals. The next thing I would do would be to create an extension that handles returning the views based on the string representation of your numeric value. Then that string is passed as an array and views created for each index in the array, returning a digit flipping view. You'd then have properties that are having their state observed, and change as needed. You can also attach an .opacity(...) modifier to give it that faded in/out look, then multiply the opacity * n where n is the animation duration.
In this example you can simply tie your Digit value to the previewedNumber and it should take over from there.
struct TestView: View {
#State var previewedNumber = 0;
var body: some View {
ZStack(alignment:.bottomTrailing) {
GeometryReader { reader in
VStack {
ForEach((0...9).reversed(), id: \.self) { i in
Text("\(i)")
.font(.system(size: 100))
.fontWeight(.bold)
.frame(width: reader.size.width, height: reader.size.height)
.foregroundColor(Color.white)
.offset(y: reader.size.height * CGFloat(previewedNumber))
.animation(.linear(duration: 0.2))
}
}.frame(width: reader.size.width, height: reader.size.height, alignment: .bottom)
}
.background(Color.black)
Button(action: {
withAnimation {
previewedNumber += 1
if (previewedNumber > 9) {
previewedNumber = 0
}
}
}, label: {
Text("Go To Next")
}).padding()
}
}
}

SwuifUI transitions for parameterized list

In learning SwiftUI I'm trying to do transition animations I used with UIKit. I sometimes used pages with tables that changed with a parameter by using a containerView and transitioning between children. The SwiftUI equivalent seems to be a single List View dependent on the parameter. But it seems that .transitions don't work well since no view is being removed/inserted in the hierarchy (and simple animations don't give me the transition options I want). The only way I have made it work at all is by "manually" removing the old List and inserting the new one, but that seems a kludge and didn't work perfectly. Here's a toy version of the code that illustrated the problem -- the inserted green rectangle is just for comparison -- the transition works fine for it but not for the List.
struct ContentView: View {
let listItems:[[String]] = [["A0","B0"],["A1","B1"]]
#State var pointer:Int = 0
var body: some View {
VStack{
List(listItems[pointer], id:\.self)
{item in Text(item)
.foregroundColor(self.pointer == 0 ? Color.blue : Color.purple)
}
.transition(.offset(x: -600, y: 0))
if pointer == 1 {
RoundedRectangle(cornerRadius: 10)
.frame(width: 300, height: 200)
.foregroundColor(.green)
.transition(.offset(x: 500, y: 300))
}
}
.onTapGesture {
withAnimation(.easeInOut(duration: 1)){
self.pointer = (self.pointer + 1) % 2
}
}
}
}
I've found a partial solution, but it behaves strangely. Instead of a single List with varying content, transitions seem to require separate lists which are inserted/removed. Here is a simple implementation
struct ContentView: View {
let listItems:[[String]] = [["A0","B0"],["A1","B1"],["A2","B2"]]
#State var pointer:Int = 0
var body: some View {
ZStack{ForEach(0...2, id: \.self)
{index in
ZStack{
if index == self.pointer {
List(self.listItems[index], id:\.self)
{item in HStack{Text(item);Spacer()}
.foregroundColor(self.pointer == 0 ? Color.blue : Color.purple)
.frame(height:20)
.padding(2)
.background(Color.yellow)
}
.transition(.asymmetric(insertion: .offset(x: 400, y: -370), removal: .offset(x: 0, y: -870)))
}
}
}
}
.onTapGesture {
withAnimation(.easeInOut(duration: 1)){
self.pointer = (self.pointer + 1) % 3
}
}
But it responds oddly to choice of offsets -- I don't understand the -370 y-offset needed to make insertions come in horizontally -- it was found by trial/error. And the transition 2->0 is different from the other two.

Swiftui hide disclosure arrow

I have this View
NavigationView {
GeometryReader { geometry in
List {
ForEach(self.viewModel.items) { item in
HStack(spacing: 0, content: {
ZStack {
RowItemView(data: item.FirstItem)
NavigationLink(destination: CustomView(data: item.FirstItem))
{
EmptyView()
}
}
.frame(width: geometry.size.width / 2, alignment: .center)
if (item.SecondItem != nil) {
ZStack {
RowItemView(data: item.SecondItem!)
NavigationLink(destination: CustomView(data: item.SecondItem!))
{
EmptyView()
}
}
})
}.listRowInsets(EdgeInsets())
}
}
I want to hide the disclouse arrow of the NavigationView.
I try to add .buttonStyle(PlainButtonStyle()) or add a negative trailing to the navigationLink, but it doesn't change.
I have already read this question and this one but they do not work in my case, probably because I'm creating a grid and not a plain list.
In this scenario the possible approach is to use zero frame, as following
NavigationLink(destination: CustomView(data: item.FirstItem)) {
EmptyView()
}.frame(width: 0)
Thanks to #Asperi I figured out what is the problem.
With a zero EdgeInsets in listRowInsets the arrow still show.
So I create this trick (Works in Xcode 13.3.1)
List {
ForEach(self.viewModel.items) { item in
//code for creating the row
}.listRowInsets(EdgeInsets.init(top: 8, leading: 0, bottom: 8, trailing: 0))
Put a value for both top and bottom.
I do not know if it is the correct way to get rid of that annoying arrow, but for my app it works :-)