SwiftUI Animation: EaseIn only then reset - swiftui

When the screen first appears, I want three separate Text views to fade in with different delayed times. After tapping a refresh button to get new String values, I want the animation to produce the same fade-in-from-nothing effect, just like it did when the screen first appears.
Here are the properties and methods:
let colorArray = ["Red", "Orange", "Yellow", "Green", "Blue", "Purple"]
#State private var text1 = " "
#State private var text2 = " "
let cityArray = ["Dallas", "Miami", "Nashville", "Chicago", "Los Angeles", "Seattle"]
#State private var text3 = " "
#State private var isAnimating = false
func resetValues() {
isAnimating = false
text1 = " "
text2 = " "
text3 = " "
}
func refreshItems() {
resetValues()
text1 = colorArray.randomElement()!
text2 = colorArray.randomElement()!
text3 = cityArray.randomElement()!
if text1 == text2 {
text2 = colorArray.randomElement()!
}
isAnimating = true
}
And here is the body:
NavigationStack {
VStack(spacing: 20) {
Text(text1)
.opacity(isAnimating ? 1 : 0)
.animation(.easeIn(duration: 1).delay(0.5), value: isAnimating)
Text(text2)
.opacity(isAnimating ? 1 : 0)
.animation(.easeIn(duration: 1).delay(1.5), value: isAnimating)
Text(text3)
.opacity(isAnimating ? 1 : 0)
.animation(.easeIn(duration: 1).delay(2.5), value: isAnimating)
}
.onAppear(perform: {
refreshItems()
})
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button {
refreshItems()
} label: {
Image(systemName: "arrow.triangle.2.circlepath")
.font(.system(size: 25, weight: .bold))
}
}
}
}
Here is what this looks like:
See when the preview first starts, I get the result I want, but when I hit the refresh button, I don't get the desired animation.
SwiftUI-Lab was helpful but didn't address the challenge that I have. Any thoughts?

Place within an animation block
func refreshItems() {
withAnimation {
resetValues()
text1 = colorArray.randomElement()!
text2 = colorArray.randomElement()!
text3 = cityArray.randomElement()!
if text1 == text2 {
text2 = colorArray.randomElement()!
}
}
isAnimating = true
}

Related

Why typing something in a TextField outputs Picker: the selection "0" is invalid

In iOS15 the following code was working fine, now in iOS16 I get a warning next to ForEach(1 ..< maxMonths) that reads as follow...
Non-constant range: argument must be an integer literal
I fixed this warning by adding adding id: \.self to the forEach, ForEach(1 ..< maxMonths, id: \.self).
But now every time I type something on the inputTaskName field, I get the following output message in the `Debug Console.
Picker: the selection "0" is invalid and does not have an associated tag, this will give undefined results.
Any idea why typing something in the TextField outputs that message?
Original Code, which was running fine in iOS15.
struct PickerTest: View {
#State private var monthsPickerValue = 0
#State private var inputTaskName:String = ""
let maxMonths = 20
var body: some View {
VStack{
TextField("Task Name", text:$inputTaskName)
Picker("", selection: $monthsPickerValue) {
ForEach(1 ..< maxMonths) {
if $0 == 1{
Text("Repeats every " + String($0) + " month ")
}else{
Text("Repeats every " + String($0) + " months ")
}
}
}
.pickerStyle(.menu)
}
}
}
Thanks
Changing monthsPickerValue to something other than 0 fixed the issue, in my case, I changed it to 1 #State private var monthsPickerValue = 1.
struct PickerTest: View {
#State private var monthsPickerValue = 1
#State private var inputTaskName:String = ""
let maxMonths = 20
var body: some View {
VStack{
TextField("Task Name", text:$inputTaskName)
Picker("", selection: $monthsPickerValue) {
Text("No Option").tag(Optional<String>(nil))
ForEach(1 ..< maxMonths, id: \.self) {
if $0 == 1{
Text("Repeats every " + String($0) + " month ")
}else{
Text("Repeats every " + String($0) + " months ")
}
}
}
.pickerStyle(.menu)
}
}
}

Capture previous value from textfield in SwiftUI

I am creating a basic manual counter to keep track of visitors, however, I am struggling to find out how I can capture and present the previous value entered by the user alongside the current number.
When I tap on the counter (0) (aka editednumber), a box appears and the user is asked to enter a number, I want to be able to save the number entered by the user, so when the user taps the counter again to enter a new number, the screen will show the previous number entered as well as the current number entered.
The previous number will of course be overwritten, every time a new number is entered, but regardless, I would like the previous number and new number to appear.
Example:
User enters the number 10, this will show as current_guests/editednumber which is fine, but if I tap to enter a new number 12, only the last entered number (10) is showing.
I want the view to show both the old (10) (stored into the previous_editednumber variable) and current (12) number (editednumber).
My code is as following:
// testView.swift
import SwiftUI
struct testView: View {
#State var current_guests:Int = 0
#State var denied_guests:Int = 0
#State var total_guests:Int = 0
#State var editednumber:Int = 0
#State var previous_editednumber:Int = 0
#State private var presentAlert = false
var body: some View {
VStack {
VStack {
Text("total_guests: \(total_guests)")
Text("current_guests: \(current_guests)")
Text("editednumber: \(editednumber)")
Text("previous_editednumber:\(previous_editednumber)")
Button("\(current_guests)") {
presentAlert = true
}
.alert("", isPresented: $presentAlert, actions: {
TextField("Number", value: $editednumber, formatter: NumberFormatter()).font(.system(size: 18)).foregroundColor(.black).multilineTextAlignment(.center).keyboardType(.numberPad)
Button("OK", action: {
// perform calculations based on input
if (editednumber >= total_guests) {
current_guests = editednumber
total_guests = editednumber + total_guests
}
if (editednumber < total_guests) {
current_guests = editednumber
total_guests = total_guests - current_guests
}
})
Button("Cancel", role: .cancel, action: {})
}, message: {
Text("Enter number of guests inside")
}).font(.system(size: 58, weight: .heavy)).keyboardType(.decimalPad) .frame(maxWidth: .infinity, alignment: .center).padding(.bottom,70).ignoresSafeArea(.keyboard)
}
// main buttons
HStack {
Button {
current_guests += 1
total_guests += 1
}label: {
Image(systemName: "plus")}.foregroundColor(.white).background(Color .green).frame(width: 80, height: 80).background(Color.green).font(.system(size: 50)).cornerRadius(40).padding()
Button {
denied_guests += 1
}label: {
Image(systemName: "nosign")}.foregroundColor(.white).background(Color .orange).frame(width: 80, height: 80).background(Color.orange).font(.system(size: 50)).cornerRadius(40).padding()
Button {
current_guests -= 1
if (current_guests <= 0) {
current_guests = 0
} }label: {
Image(systemName: "minus")}.foregroundColor(.white).background(Color .red).frame(width: 80, height: 80).background(Color.red).font(.system(size: 50)).cornerRadius(40).padding()
}
}
}
}
struct testView_Previews: PreviewProvider {
static var previews: some View {
testView()
}
}
You can create an in-between #State that takes its initial value from editednumber then returns the newValue when the user clicks "Ok".
struct AlertBody: View{
#State var newValue: Int
let onAccept: (Int) -> Void
let onCancel: () -> Void
init(initialValue: Int, onAccept: #escaping (Int) -> Void, onCancel: #escaping () -> Void){
self._newValue = State(initialValue: initialValue)
self.onAccept = onAccept
self.onCancel = onCancel
}
var body: some View{
TextField("Number", value: $newValue, formatter: NumberFormatter()).font(.system(size: 18)).foregroundColor(.black).multilineTextAlignment(.center).keyboardType(.numberPad)
Button("OK", action: {
onAccept(newValue)
})
Button("Cancel", role: .cancel, action: onCancel)
}
}
Then you can replace the content in the alert
struct HistoryView: View {
#State private var currentGuests:Int = 0
#State private var deniedGuests:Int = 0
#State private var totalGuests:Int = 0
#State private var editedNumber:Int = 0
#State private var previousEditedNumber:Int = 0
#State private var presentAlert = false
var body: some View {
VStack {
VStack {
Text("total_guests: \(totalGuests)")
Text("current_guests: \(currentGuests)")
Text("editednumber: \(editedNumber)")
Text("previous_editednumber:\(previousEditedNumber)")
Button("\(currentGuests)") {
presentAlert = true
}
.alert("", isPresented: $presentAlert, actions: {
AlertBody(initialValue: editedNumber) { newValue in
//Assign the previous number
previousEditedNumber = editedNumber
//Assign the newValue
editedNumber = newValue
//Your previous logic
if (editedNumber >= totalGuests) {
currentGuests = editedNumber
totalGuests = editedNumber + totalGuests
}
if (editedNumber < totalGuests) {
currentGuests = editedNumber
totalGuests = totalGuests - currentGuests
}
} onCancel: {
print("cancelled alert")
}
}, message: {
Text("Enter number of guests inside")
}).font(.system(size: 58, weight: .heavy)).keyboardType(.decimalPad) .frame(maxWidth: .infinity, alignment: .center).padding(.bottom,70).ignoresSafeArea(.keyboard)
}
// main buttons
HStack {
Button {
currentGuests += 1
totalGuests += 1
}label: {
Image(systemName: "plus")}.foregroundColor(.white).background(Color .green).frame(width: 80, height: 80).background(Color.green).font(.system(size: 50)).cornerRadius(40).padding()
Button {
deniedGuests += 1
}label: {
Image(systemName: "nosign")}.foregroundColor(.white).background(Color .orange).frame(width: 80, height: 80).background(Color.orange).font(.system(size: 50)).cornerRadius(40).padding()
Button {
currentGuests -= 1
if (currentGuests <= 0) {
currentGuests = 0
} }label: {
Image(systemName: "minus")}.foregroundColor(.white).background(Color .red).frame(width: 80, height: 80).background(Color.red).font(.system(size: 50)).cornerRadius(40).padding()
}
}
}
}

SwiftUI: Fonts not shown in Picker?

I noticed a strange effect when using fonts in a Picker: While a Text used outside a Picker shows a font correctly, this is not the case if used inside:
#Binding var text: AttributedString
#State private var bold: Bool = false
#State private var fontName: String = ""
#State private var fontSize: CGFloat = 18
#State private var id: Int = 0
init(text: Binding<AttributedString>) {
self._text = text
}
var body: some View {
VStack {
HStack {
Text(text)
.font(Font.custom(fontName, size: 18))
Divider()
Text("\(id)")
}
Picker(
selection: $fontName,
label: Label("Font", systemImage: "")
) {
ForEach(UIFont.familyNames.sorted(), id: \.self) { family in
let names = UIFont.fontNames(forFamilyName: family)
ForEach(names, id: \.self) { name in
Text(name)
.font(Font.custom(name, size: 20))
}
}
}
.onChange(of: fontName, perform: { _ in
update()
})
}
.id(id)
}
private func update() {
var container = AttributeContainer()
.font(.custom(self.fontName, size: self.fontSize))
self.text = self.text.mergingAttributes(
container,
mergePolicy: .keepNew
)
self.id += 1
}
Is this an intended behaviour or a bug? How can I get the picker to show not only the name of the font, but display the text in that font? Actually, after researching, the Text(...).font(...) should work and be sufficient.
Xcode version 14.0 beta 2.

SwiftUI - How to make text appear using the button and then using the timer

I would like text from the dictinary to appear when the button is pressed.
"Text1" is there at the beginning. If the button is pressed, a label with Text2 should appear under the start label. Then automatically a label with Text3 after 3 seconds. Originally I tried a list, but unfortunately I didn't get it.
It would be great if someone has an idea.
struct ContentView: View {
var chats = [
"Text1" : "Hello?",
"Text2" : "Here!",
"Text3" : "And here!",
]
var body: some View {
NavigationView {
VStack {
Button(action: {}) {
Text("Button")
}
HStack {
Image (systemName: "faceid")
Text("\(chats["Text1"] ?? "Failure")")
}
}
}
}
}
Here is your solution. Please try this.
struct ContentView: View {
var chats: [String: String] = [
"Text1" : "Hello?",
"Text2" : "Here!",
"Text3" : "And here!",
]
#State var currentIndex: Int = 1
var body: some View {
NavigationView {
VStack {
Button(action: {
self.currentIndex = self.currentIndex + 1
if self.currentIndex == 2 {
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.currentIndex = self.currentIndex + 1
}
}
print("Hello, World! Tapped")
}) {
Text("Hello, World!")
}
HStack {
Image (systemName: "faceid")
Text("\(chats["Text\(currentIndex)"] ?? "Failure")")
}
}
}
}
}

SwiftUI page control implementation

I need to implement something like an animated page control. And I don't want to use integration with UIKit if possible. I have pages array containing 4 views I need to switch between. I create the animation itself by changing the value of progress variable using timer. And I have the following code right now
#State var pages: [PageView]
#State var currentIndex = 0
#State var nextIndex = 1
#State var progress: Double = 0
var body: some View {
ZStack {
Button(action: {
self.isAnimating = true
}) { shape.onReceive(timer) { _ in
if !self.isAnimating {
return
}
self.refreshAnimatingViews()
}
}.offset(y: 300)
pages[currentIndex]
.offset(x: -CGFloat(pow(2, self.progress)))
pages[nextIndex]
.offset(x: CGFloat(pow(2, (limit - progress))))
}
}
It is animating great - current page is moved to the left until it disappears, and the next page is revealed from the right taking its place. At the end of animation I add 1 to both indices and reset progress to 0. But once the animation (well not exactly an animation - I just change the value of progress using timer, and generate every state manually) is over, the page with index 1 is swapped back to page with index 0. If I check with debugger, currentIndex and nextIndex values are correct - 1 and 2, but the page displayed after animation is always the one I started with (with index 0). Does anybody know why this is happening?
The whole code follows
struct ContentView : View {
let limit: Double = 15
let step: Double = 0.3
let timer = Timer.publish(every: 0.01, on: .current, in: .common).autoconnect()
#State private var shape = AnyView(Circle().foregroundColor(.blue).frame(width: 60.0, height: 60.0, alignment: .center))
#State var pages: [PageView]
#State var currentIndex = 0
#State var nextIndex = 1
#State var progress: Double = 0
#State var isAnimating = false
var body: some View {
ZStack {
Button(action: {
self.isAnimating = true
}) { shape.onReceive(timer) { _ in
if !self.isAnimating {
return
}
self.refreshAnimatingViews()
}
}.offset(y: 300)
pages[currentIndex]
.offset(x: -CGFloat(pow(2, self.progress)))
pages[nextIndex]
.offset(x: CGFloat(pow(2, (limit - progress))))
}.edgesIgnoringSafeArea(.vertical)
}
func refreshAnimatingViews() {
progress += step
if progress > 2*limit {
isAnimating = false
progress = 0
currentIndex = nextIndex
if nextIndex + 1 < pages.count {
nextIndex += 1
} else {
nextIndex = 0
}
}
}
}
struct PageView: View {
#State var title: String
#State var imageName: String
#State var content: String
let imageWidth: Length = 150
var body: some View {
VStack(alignment: .center, spacing: 15) {
Text(title).font(Font.system(size: 40)).fontWeight(.bold).lineLimit(nil)
Image(imageName)
.resizable()
.frame(width: imageWidth, height: imageWidth)
.cornerRadius(imageWidth/2)
.clipped()
Text(content).font(.body).lineLimit(nil)
}.padding(60)
}
}
struct MockData {
static let title = "Eating grapes 101"
static let contentStrings = [
"Step 1. Break off a branch holding a few grapes and lay it on your plate.",
"Step 2. Put a grape in your mouth whole.",
"Step 3. Deposit the seeds into your thumb and first two fingers.",
"Step 4. Place the seeds on your plate."
]
static let imageNames = [
"screen 1",
"screen 2",
"screen 3",
"screen 4"
]
}
in SceneDelegate:
if let windowScene = scene as? UIWindowScene {
let pages = (0...3).map { i in
PageView(title: MockData.title, imageName: MockData.imageNames[i], content: MockData.contentStrings[i])
}
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: ContentView(pages:
pages))
self.window = window
window.makeKeyAndVisible()
}
The following solution works. I think the problem was switching out views while SwiftUI tries to diff and update them is not something SwiftUI is good at.
So just use the same two PageView views and swap out their content based on the current index.
import Foundation
import SwiftUI
import Combine
struct PagesView : View {
let limit: Double = 15
let step: Double = 0.3
#State var pages: [Page] = (0...3).map { i in
Page(title: MockData.title, imageName: MockData.imageNames[i], content: MockData.contentStrings[i])
}
#State var currentIndex = 0
#State var nextIndex = 1
#State var progress: Double = 0
#State var isAnimating = false
static let timerSpeed: Double = 0.01
#State var timer = Timer.publish(every: timerSpeed, on: .current, in: .common).autoconnect()
#State private var shape = AnyView(Circle().foregroundColor(.blue).frame(width: 60.0, height: 60.0, alignment: .center))
var body: some View {
ZStack {
Button(action: {
self.isAnimating.toggle()
self.timer = Timer.publish(every: Self.timerSpeed, on: .current, in: .common).autoconnect()
}) { self.shape
}.offset(y: 300)
PageView(page: pages[currentIndex])
.offset(x: -CGFloat(pow(2, self.progress)))
PageView(page: pages[nextIndex])
.offset(x: CGFloat(pow(2, (self.limit - self.progress))))
}.edgesIgnoringSafeArea(.vertical)
.onReceive(self.timer) { _ in
if !self.isAnimating {
return
}
self.refreshAnimatingViews()
}
}
func refreshAnimatingViews() {
progress += step
if progress > 2*limit {
isAnimating = false
progress = 0
currentIndex = nextIndex
if nextIndex + 1 < pages.count {
nextIndex += 1
} else {
nextIndex = 0
}
}
}
}
struct Page {
var title: String
var imageName: String
var content: String
let imageWidth: CGFloat = 150
}
struct PageView: View {
var page: Page
var body: some View {
VStack(alignment: .center, spacing: 15) {
Text(page.title).font(Font.system(size: 40)).fontWeight(.bold).lineLimit(nil)
Image(page.imageName)
.resizable()
.frame(width: page.imageWidth, height: page.imageWidth)
.cornerRadius(page.imageWidth/2)
.clipped()
Text(page.content).font(.body).lineLimit(nil)
}.padding(60)
}
}
struct MockData {
static let title = "Eating grapes 101"
static let contentStrings = [
"Step 1. Break off a branch holding a few grapes and lay it on your plate.",
"Step 2. Put a grape in your mouth whole.",
"Step 3. Deposit the seeds into your thumb and first two fingers.",
"Step 4. Place the seeds on your plate."
]
static let imageNames = [
"screen 1",
"screen 2",
"screen 3",
"screen 4"
]
}