SwiftUI - Reload/Force the view to update/stop the animation - swift

I have a view that make the text slide from left to right when the text is too long for its context.
import SwiftUI
struct SlidingText: View {
let geometryProxy: GeometryProxy
#Binding var text: String
let font: Font
#State private var animateSliding: Bool = false
private let slideDelay: Double = 3
private let slideDuration: Double = 6
private var isTextLargerThanView: Bool {
if text.size(forWidth: geometryProxy.size.width, andFont: font).width < geometryProxy.size.width {
return false
}
return true
}
var body: some View {
ZStack(alignment: .leading, content: {
VStack(content: {
Text(text)
.font(self.font)
.foregroundColor(.white)
})
.id("SlidingText-Animation")
.fixedSize()
.animation(isTextLargerThanView ? Animation.linear(duration: slideDuration).delay(slideDelay).repeatForever(autoreverses: true) : nil)
.frame(width: geometryProxy.size.width,
alignment: isTextLargerThanView ? (animateSliding ? .trailing : .leading) : .center)
.onAppear(perform: {
self.animateSliding.toggle()
})
})
.clipped()
}
}
The problem here is, if I have a text that was too long, it will do the correct animation, but then when I change the text during the animation, it doesn't take in consideration the new size and the isTextLargerThanView is ignored. I also tried a didSet for my #Binding var text: String, it doesn't work either.
I am not really sure how to handle this case here, I tried to use custom Binders but didn't work. I know in iOS 14 there will be a onUpdate(on: _) function but I need it to work on iOS 13 too.
Do you have an idea of what I could do to update my text and be able to update the isTextLargerThanView?
Thank you for any future help!

I can't run your code. Maybe you can try to make it reactive like this:
#Binding var text: String {
didSet {
isTextLargerThanView = (text.size(forWidth: geometryProxy.size.width, andFont: font).width > geometryProxy.size.width)
}
}
...
#State private var isTextLargerThanView = false

Related

Animate view every time an Observed property changes in SwiftUI

In the following code, an Image is animated when you tap on it by changing the #State property value. What I would like to be able to do is animate the image every time a #Published property value changes, this property is located inside an ObservableObject and is dynamically changing.
Using Local #State property wrappers, works fine.
struct ContentView: View {
#State private var scaleValue = 1.0
#State private var isAnimating: Bool = true
var body: some View {
Image(systemName: "circle.fill")
.scaleEffect(scaleValue)
.font(.title)
.foregroundColor(.green)
.animation(.easeOut(duration: 0.3), value: isAnimating)
.onTapGesture {
self.isAnimating.toggle()
self.scaleValue = 1.5
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.3){
self.scaleValue = 1.0
}
}
}
}
Using Observed #Published property wrappers, not working. Not sure where to place the animation.
struct ContentView: View {
#State private var scaleValue = 1.0
#StateObject private var contentVM = ContentViewModel()
var body: some View {
Image(systemName: "circle.fill")
.scaleEffect(scaleValue)
.font(.title)
.foregroundColor(.green)
.animation(.easeOut(duration: 0.3), value: contentVM.isAnimating)
// not sure where to add the animation
}
}
EDIT: Here is the Working Solution:
Thanks to #Fogmeister and #burnsi
class ContentViewModel: ObservableObject{
#Published var isAnimatingImage = false
}
struct ContentView2: View {
#StateObject private var contentVM = ContentViewModel()
var body: some View {
#State private var scaleValue = 1.0
Image(systemName: "circle.fill")
.scaleEffect(scaleValue)
.font(.title)
.foregroundColor(.green)
.animation(.easeOut(duration: 0.3), value: scaleValue)
.onChange(of: contentVM.isAnimatingImage) {newValue in
animateImage()
}
}
func animateImage(){
scaleValue = 1.5
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.3){
scaleValue = 1.0
}
}
}
I think the issue here is the wrong usage of the .animation modifier.
You don´t need to trigger the animation with a different boolean value. If the animation is "driven" by changing a value, in this case scaleValue, use that value in the modifier.
Take a look at your animation in your first example. It doesn´t complete. It scales to the desired size and then shrinks. But it jumps in the middle of the animation while shrinking back.
This would be a proper implementation in your first example:
struct ContentView: View {
#State private var scaleValue = 1.0
var body: some View {
Image(systemName: "circle.fill")
.scaleEffect(scaleValue)
.font(.title)
.foregroundColor(.green)
.animation(.easeOut(duration: 0.3), value: scaleValue)
.onTapGesture {
self.scaleValue = 1.5
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.3){
self.scaleValue = 1.0
}
}
}
}
and in your second example:
class ContentViewModel: ObservableObject{
#Published var scaleValue = 1.0
}
struct ContentView2: View {
#StateObject private var contentVM = ContentViewModel()
var body: some View {
Image(systemName: "circle.fill")
.scaleEffect(contentVM.scaleValue)
.font(.title)
.foregroundColor(.green)
.animation(.easeOut(duration: 0.3), value: contentVM.scaleValue)
.onTapGesture {
contentVM.scaleValue = 1.5
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.3){
contentVM.scaleValue = 1.0
}
}
}
}
This is why we shouldn't be using view model objects in SwiftUI and this is not what #StateObject is designed for - it's designed for when we need a reference type in an #State which is not the case here. If you want to group your view data vars together you can put them in a struct, e.g.
struct ContentViewConfig {
var isAnimating = false
var scaleValue = 1.0
// mutating func someLogic() {}
}
// then in the View struct:
#State var config = ContentViewConfig()

SwiftUI + Timer + AVPlayer - When audio playing, onReceive(timer) not triggered

I am having issue to make my timer (and my animation) to work properly. My timer is not triggered if AVPlayer is currently playing. As soon as I pause it, my timer is resuming...
I have an implementation of a SlidingText view. Basically just moving a text in its box from left to right and then right to left. This animation is triggered with a Timer every 5 seconds.
I have an AVPlayer playing something at the same time with a pause/play button.
This is the implementation of the SlidingText. And the AVPlayer is just being called with a button that triggers player.play() or player.pause().
import SwiftUI
struct SlidingText: View {
let geometryProxy: GeometryProxy
#Binding var text: String
let font: Font
#State private var animateSliding: Bool = false
private let timer = Timer.publish(every: 5, on: .main, in: .common).autoconnect()
private let slideDuration: Double = 3
var body: some View {
ZStack(alignment: .leading, content: {
VStack(content: {
Text(text)
.font(self.font)
.foregroundColor(.white)
.background(Color.red)
})
.id("SlidingText-Animation")
.fixedSize(horizontal: false, vertical: true)
.frame(width: geometryProxy.size.width, alignment: animateSliding ? .trailing : .leading)
.clipped()
.animation(Animation.linear(duration: slideDuration))
.onReceive(timer, perform: { _ in
self.animateSliding.toggle()
})
})
.frame(width: self.geometryProxy.size.width, height: self.geometryProxy.size.height)
.background(Color.yellow)
}
}
Is there anything wrong it the way I use the timer here?
Thanks for any future help!
Alright, I endup not using a timer because it is buggy.
for those interested it looks like this now, I am using the Animation properties.
No more interferences with the audio player.
struct SlidingText: View {
let geometryProxy: GeometryProxy
#Binding var text: String
let font: Font
#State private var animateSliding: Bool = false
private let slideDelay: Double = 3
private let slideDuration: Double = 6
private var isTextLargerThanView: Bool {
if text.size(forWidth: geometryProxy.size.width, andFont: font).width < geometryProxy.size.width {
return false
}
return true
}
var body: some View {
ZStack(alignment: .leading, content: {
VStack(content: {
Text(text)
.font(self.font)
.foregroundColor(.white)
.background(Color.red)
})
.id("SlidingText-Animation")
.fixedSize()
.animation(Animation.linear(duration: slideDuration).delay(slideDelay).repeatForever(autoreverses: true))
.frame(width: geometryProxy.size.width,
alignment: isTextLargerThanView ? (animateSliding ? .trailing : .leading) : .center)
.onAppear(perform: {
self.animateSliding.toggle()
})
})
.clipped()
}
}

Show/Hide Password - How can I add this feature?

I've looked through the forums but I'm seeing mixed answers especially ones from an old Xcode version.
I only decided to add this after already typing up the code I have in this:
How could I go about doing that? I was wanting the 'Eyeball' toggle implemented on the password field.
You can simply use this view instead of SecureField. It has the eye icon inside, so for most cases you don't need to care about anything.
struct SecureInputView: View {
#Binding private var text: String
#State private var isSecured: Bool = true
private var title: String
init(_ title: String, text: Binding<String>) {
self.title = title
self._text = text
}
var body: some View {
ZStack(alignment: .trailing) {
Group {
if isSecured {
SecureField(title, text: $text)
} else {
TextField(title, text: $text)
}
}.padding(.trailing, 32)
Button(action: {
isSecured.toggle()
}) {
Image(systemName: self.isSecured ? "eye.slash" : "eye")
.accentColor(.gray)
}
}
}
}
Copy paste this view into your app, and instead of SecureField just use SecureInputView.
Example: SecureInputView("Password", text: $viewModel.password)
The possible approach is to show either TextField or SecureField joined to one storage, like in below demo:
Updated: Xcode 13.4 / iOS 15.5
with FocusState, now it is possible to change fields without having the keyboard disappear
Main part:
if showPassword {
TextField("Placeholer", text: $password)
.focused($inFocus, equals: .plain)
} else {
SecureField("Placeholder", text: $password)
.focused($inFocus, equals: .secure)
}
Button("toggle") {
self.showPassword.toggle()
inFocus = showPassword ? .plain : .secure
}
Test module in project is here
Old:
struct DemoShowPassword: View {
#State private var showPassword: Bool = false
#State private var password = "demo"
var body: some View {
VStack {
if showPassword {
TextField("Placeholer", text: $password)
} else {
SecureField("Placeholder", text: $password)
}
Button("toggle") {
self.showPassword.toggle()
}
}
}
}
For those still looking for a simple solution to this issue (requires iOS 15 for swiftUI 3):
With the new #FocusState introduced in swiftUI 3, it's possible to keep focus and keyboard open while changing State.
By using the opacity modifier instead of conditionally changing between SecureField and TextField, the focus can jump between the two without issues with the keyboard.
This allows you to toggle between revealing and hiding the password with the the eye button included in the ZStack.
import SwiftUI
struct SecureTextFieldWithReveal: View {
#FocusState var focus1: Bool
#FocusState var focus2: Bool
#State var showPassword: Bool = false
#State var text: String = ""
var body: some View {
HStack {
ZStack(alignment: .trailing) {
TextField("Password", text: $text)
.modifier(LoginModifier())
.textContentType(.password)
.focused($focus1)
.opacity(showPassword ? 1 : 0)
SecureField("Password", text: $text)
.modifier(LoginModifier())
.textContentType(.password)
.focused($focus2)
.opacity(showPassword ? 0 : 1)
Button(action: {
showPassword.toggle()
if showPassword { focus1 = true } else { focus2 = true }
}, label: {
Image(systemName: self.showPassword ? "eye.slash.fill" : "eye.fill").font(.system(size: 16, weight: .regular))
.padding()
})
}
}
}
}
Password field hidden
Password field revealed
This is the code in LoginModifier:
import SwiftUI
struct LoginModifier: ViewModifier {
var borderColor: Color = Color.gray
func body(content: Content) -> some View {
content
.disableAutocorrection(true)
.autocapitalization(.none)
.padding()
.overlay(RoundedRectangle(cornerRadius: 10).stroke(borderColor, lineWidth: 1))
}
}
The only issue I've had with this method is that on regaining focus SecureField will automatically clear any text already entered if you start typing. This seems to be a design choice by Apple.
I am using this approach for now in my current application. I would like to say that it works flawlessly.
#ViewBuilder
func secureField() -> some View {
if self.showPassword {
TextField("Password", text: $passwordText)
.font(.system(size: 15, weight: .regular, design: .default))
.keyboardType(.default)
.autocapitalization(.none)
.disableAutocorrection(true)
.frame(maxWidth: UIScreen.main.bounds.width, maxHeight: 60, alignment: .center)
} else {
SecureField("Password", text: $passwordText)
.font(.system(size: 15, weight: .regular, design: .default))
.keyboardType(.default)
.autocapitalization(.none)
.disableAutocorrection(true)
.frame(maxWidth: UIScreen.main.bounds.width, maxHeight: 60, alignment: .center)
}
}
Use:
HStack{
Image(systemName: "lock.fill")
.foregroundColor(passwordText.isEmpty ? .secondary : .primary)
.font(.system(size: 18, weight: .medium, design: .default))
.frame(width: 18, height: 18, alignment: .center)
secureField()
if !passwordText.isEmpty {
Button(action: {
self.showPassword.toggle()
}, label: {
ZStack(alignment: .trailing){
Color.clear
.frame(maxWidth: 29, maxHeight: 60, alignment: .center)
Image(systemName: self.showPassword ? "eye.slash.fill" : "eye.fill")
.font(.system(size: 18, weight: .medium))
.foregroundColor(Color.init(red: 160.0/255.0, green: 160.0/255.0, blue: 160.0/255.0))
}
})
}
}
.padding(.horizontal, 15)
.background(Color.primary.opacity(0.05).cornerRadius(10))
.padding(.horizontal, 15)
I am afraid most answers here fail to mention that switching from SecureField to TextField reduces security. SecureField is essentially, per Apple documentation, simply a TextField where user input is masked [1]. However, SecureField also does one other job - it prevents using third-party keyboards (keyboard extensions) and thus protects user's security and privacy.
Ideal solution would be to have input field that is both "secure" and has mask()/unmask() methods. Unfortunately, the only advice I found is when you want to implement unmasking as other answers suggested, at least block third-party keyboards from your application entirely [2]:
class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, shouldAllowExtensionPointIdentifier extensionPointIdentifier: UIApplication.ExtensionPointIdentifier) -> Bool {
return extensionPointIdentifier != UIApplication.ExtensionPointIdentifier.keyboard
}
}
#main
struct MyApplication: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Should also mention that UIApplicationDelegate is part of UIKit, not SwiftUI. There is no "native" SwiftUI for the same purpose as for now, although the above works fine for now.
https://developer.apple.com/documentation/swiftui/securefield
https://www.securing.pl/en/third-party-iphone-keyboards-vs-your-ios-application-security/
For those that do not want the keyboard disappearing while typing:
struct CustomSecureField: View {
#State var password: String = ""
#State var isShowingPassword: Bool = false
var body: some View {
VStack{
ZStack{
HStack{
SecureField(
isShowingPassword ? "" : "Password",
text: $password) {
}.opacity(isShowingPassword ? 0 : 1)
// show only one of these is not empty.
if(!password.isEmpty){
Image(systemName: isShowingPassword ? "eye.slash" : "eye")
.foregroundColor(.white)
.frame(width: 20, height: 20, alignment: .center)
.modifier(TouchDownUpEventModifier(changeState: { (buttonState) in
if buttonState == .pressed {
isShowingPassword = true
} else {
isShowingPassword = false
}
}))
}
}
if(isShowingPassword){
HStack{
Text(password)
.foregroundColor(.white)
.allowsHitTesting(false)
Spacer()
}
}
}
}.padding(10)
.background(Color.gray)
}
}
and the on tap and release modifier:
public enum ButtonState {
case pressed
case notPressed
}
/// ViewModifier allows us to get a view, then modify it and return it
public struct TouchDownUpEventModifier: ViewModifier {
/// Properties marked with `#GestureState` automatically resets when the gesture ends/is cancelled
/// for example, once the finger lifts up, this will reset to false
/// this functionality is handled inside the `.updating` modifier
#GestureState private var isPressed = false
/// this is the closure that will get passed around.
/// we will update the ButtonState every time your finger touches down or up.
let changeState: (ButtonState) -> Void
/// a required function for ViewModifier.
/// content is the body content of the caller view
public func body(content: Content) -> some View {
/// declare the drag gesture
let drag = DragGesture(minimumDistance: 0)
/// this is called whenever the gesture is happening
/// because we do this on a `DragGesture`, this is called when the finger is down
.updating($isPressed) { (value, gestureState, transaction) in
/// setting the gestureState will automatically set `$isPressed`
gestureState = true
}
return content
.gesture(drag) /// add the gesture
.onChange(of: isPressed, perform: { (pressed) in /// call `changeState` whenever the state changes
/// `onChange` is available in iOS 14 and higher.
if pressed {
self.changeState(.pressed)
} else {
self.changeState(.notPressed)
}
})
}
/// if you're on iPad Swift Playgrounds and you put all of this code in a seperate file,
/// you need to add a public init so that the compiler detects it.
public init(changeState: #escaping (ButtonState) -> Void) {
self.changeState = changeState
}
}
From what I have seen there is no easy way to keep the text showing unless you want to lose focus on your text.
Cheers!
#Derwrecked's answer really gave me some good inspirations: instead using two TextField, change SecureField opacity and show/hide a Text can avoid keyboard dismissing problem, but in his answer that long TouchDownUpEventModifier seems unnecessarily complicated, you can easily achieve the same effect using a Button with label.
So below is my approach, and the previews look like this
import SwiftUI
struct SecureInput: View {
let placeholder: String
#State private var showText: Bool = false
#State var text: String
var onCommit: (()->Void)?
var body: some View {
HStack {
ZStack {
SecureField(placeholder, text: $text, onCommit: {
onCommit?()
})
.opacity(showText ? 0 : 1)
if showText {
HStack {
Text(text)
.lineLimit(1)
Spacer()
}
}
}
Button(action: {
showText.toggle()
}, label: {
Image(systemName: showText ? "eye.slash.fill" : "eye.fill")
})
.accentColor(.secondary)
}
.padding()
.overlay(RoundedRectangle(cornerRadius: 12)
.stroke(Color.secondary, lineWidth: 1)
.foregroundColor(.clear))
}
}
struct SecureInput_Previews: PreviewProvider {
static var previews: some View {
Group {
SecureInput(placeholder: "Any placeholder", text: "")
.padding()
.previewLayout(.fixed(width: 400, height: 100))
SecureInput(placeholder: "Any placeholder", text: "")
.padding()
.preferredColorScheme(.dark)
.previewLayout(.fixed(width: 400, height: 100))
}
}
}
An known issue for this approach: since when password is shown, SecureField has 0.0 opacity, so input cursor is not visible. But users can still keep typing without losing keyboard focus, so I find it acceptable, if anyone has a solution for this, please comment and share.
I've been looking for a nice solution for my use-case. I had to have an indicator which field is in focus. Successfully done that with onEditingChanged from TextField, but SecureField doesn't provide that closure. I tried stacking them both and disabling the SecureField so it only shows 'hidden' characters. That resulted in cursor sticking to the TextField text while SecureField text had different text width which made it seem buggy. Imagine a password with a lot of I's in it. The idea is to have a main binding with two side bindings that update the main one and sync each other.
struct CustomSecureField : View {
var label : String
#Binding var text : String
#State var isEditing = false
#State var isHidden = true
var body : some View {
let showPasswordBinding = Binding<String> {
self.text
} set: {
self.text = $0
}
let hidePasswordBinding = Binding<String> {
String.init(repeating: "●", count: self.text.count)
} set: { newValue in
if(newValue.count < self.text.count) {
self.text = ""
} else {
self.text.append(contentsOf: newValue.suffix(newValue.count - self.text.count) )
}
}
return ZStack(alignment: .trailing) {
TextField(
label,
text: isHidden ? hidePasswordBinding : showPasswordBinding,
onEditingChanged: { editingChanged in
isEditing = editingChanged
}
)
Image("eye").frame(width: 50, height: 50).onTapGesture {
isHidden.toggle()
}
}
}
}
}
Crazy (AKA don't use in production) and very breakable solution here (but working at the time of writing):
extension TextField {
public func secure(_ secure: Bool = true) -> TextField {
if secure {
var secureField = self
withUnsafeMutablePointer(to: &secureField) { pointer in
let offset = 32
let valuePointer = UnsafeMutableRawPointer(mutating: pointer)
.assumingMemoryBound(to: Bool.self)
.advanced(by: offset)
valuePointer.pointee = true
}
return secureField
} else {
return self
}
}
}
Usage
#State securing = true
...
TextField(...)
.secure(securing)
#Vahagn Gevorgyan's answer was almost correct but some people were struggling with maintaining state... this is because the field is using a binding which should ideally be held in a parent view. Therefore just update the bindings to state variables like this
struct SecureInputView: View {
let placeholder: String
#State var text: String
#State var isSecure: Bool = true
var body: some View {
ZStack(alignment: .trailing) {
Group {
if isSecure {
SecureField(placeholder, text: $text)
} else {
TextField(placeholder, text: $text)
}
}.padding(.trailing, 32)
Button {
isSecure.toggle()
} label: {
Image(systemName: isSecure ? "lock.fill" : "lock.open")
}
}
}
}
#State private var isPasswordVisible = false
ZStack {
TextField("", text: $password)
.opacity(isPasswordVisible ? 1 : 0)
SecureField("", text: $password)
.opacity(isPasswordVisible ? 0 : 1)
}
It doesn't need #Focus from iOS 15
Keyboard will not disappear/appear on changing isPasswordVisible
Password will not cleared on changing from visible to invisible then typing
Good Luck
I made a custom text field that combine SecureField and TextField.
This is an example where I used my custom field for both email and pwd.
This is my solution:
struct CustomTextField: View {
let imageName: String
let placeholderText: String
var isSecureInput: Bool = false ///< define if this text field is secured and require eye button
#State private var isSecured: Bool
#Binding var text: String
init(image: String,
placeholder: String,
text: Binding<String>,
isSecureInput: Bool) {
imageName = image
placeholderText = placeholder
self._text = text
self.isSecureInput = isSecureInput
isSecured = isSecureInput
}
var body: some View {
VStack {
HStack {
Image(systemName: imageName)
.resizable()
.scaledToFit()
.frame(width: 25, height: 25)
.foregroundColor(Color(.darkGray))
if isSecureInput {
Group {
if isSecured {
SecureField(placeholderText, text: $text)
}
else {
TextField(text, text: $text)
}
}
.disableAutocorrection(true)
.autocapitalization(.none)
.textContentType(.password)
Button(action: {
isSecured.toggle()
}) {
Image(systemName: self.isSecured ? "eye.slash" : "eye")
.resizable()
.scaledToFit()
.frame(width: 25, height: 25)
.foregroundColor(Color(.darkGray))
}
}
else {
TextField(placeholderText, text: $text)
}
}
Divider()
}
}
}

SwiftUI: Stop an Animation that Repeats Forever

I would like to have a 'badge' of sorts on the screen and when conditions are met, it will bounce from normal size to bigger and back to normal repeatedly until the conditions are no longer met. I cannot seem to get the badge to stop 'bouncing', though. Once it starts, it's unstoppable.
What I've tried:
I have tried using a few animations, but they can be classified as animations that use 'repeatForever' to achieve the desired effect and those that do not. For example:
Animation.default.repeatForever(autoreverses: true)
and
Animation.spring(response: 1, dampingFraction: 0, blendDuration: 1)(Setting damping to 0 makes it go forever)
followed by swapping it out with .animation(nil). Doesn't seem to work. Does anyone have any ideas? Thank you so very much ahead of time! Here is the code to reproduce it:
struct theProblem: View {
#State var active: Bool = false
var body: some View {
Circle()
.scaleEffect( active ? 1.08: 1)
.animation( active ? Animation.default.repeatForever(autoreverses: true): nil )
.frame(width: 100, height: 100)
.onTapGesture {
self.active = !self.active
}
}
}
I figured it out!
An animation using .repeatForever() will not stop if you replace the animation with nil. It WILL stop if you replace it with the same animation but without .repeatForever(). ( Or alternatively with any other animation that comes to a stop, so you could use a linear animation with a duration of 0 to get a IMMEDIATE stop)
In other words, this will NOT work: .animation(active ? Animation.default.repeatForever() : nil)
But this DOES work: .animation(active ? Animation.default.repeatForever() : Animation.default)
In order to make this more readable and easy to use, I put it into an extension that you can use like this: .animation(Animation.default.repeat(while: active))
Here is an interactive example using my extension you can use with live previews to test it out:
import SwiftUI
extension Animation {
func `repeat`(while expression: Bool, autoreverses: Bool = true) -> Animation {
if expression {
return self.repeatForever(autoreverses: autoreverses)
} else {
return self
}
}
}
struct TheSolution: View {
#State var active: Bool = false
var body: some View {
Circle()
.scaleEffect( active ? 1.08: 1)
.animation(Animation.default.repeat(while: active))
.frame(width: 100, height: 100)
.onTapGesture {
self.active.toggle()
}
}
}
struct TheSolution_Previews: PreviewProvider {
static var previews: some View {
TheSolution()
}
}
As far as I have been able to tell, once you assign the animation, it will not ever go away until your View comes to a complete stop. So if you have a .default animation that is set to repeat forever and auto reverse and then you assign a linear animation with a duration of 4, you will notice that the default repeating animation is still going, but it's movements are getting slower until it stops completely at the end of our 4 seconds. So we are animating our default animation to a stop through a linear animation.
How about using a Transaction
In the code below, I turn off or turn on the animation depending on the state of the active
Warning: Be sure to use withAnimation otherwise nothing will work
#State var active: Bool = false
var body: some View {
Circle()
.scaleEffect(active ? 1.08: 1)
.animation(Animation.default.repeatForever(autoreverses: true), value: active)
.frame(width: 100, height: 100)
.onTapGesture {
useTransaction()
}
}
func useTransaction() {
var transaction = Transaction()
transaction.disablesAnimations = active ? true : false
withTransaction(transaction) {
withAnimation {
active.toggle()
}
}
}
After going through many things, I found out something that works for me. At the least for the time being and till I have time to figure out a better way.
struct WiggleAnimation<Content: View>: View {
var content: Content
#Binding var animate: Bool
#State private var wave = true
var body: some View {
ZStack {
content
if animate {
Image(systemName: "minus.circle.fill")
.foregroundColor(Color(.systemGray))
.offset(x: -25, y: -25)
}
}
.id(animate) //THIS IS THE MAGIC
.onChange(of: animate) { newValue in
if newValue {
let baseAnimation = Animation.linear(duration: 0.15)
withAnimation(baseAnimation.repeatForever(autoreverses: true)) {
wave.toggle()
}
}
}
.rotationEffect(.degrees(animate ? (wave ? 2.5 : -2.5) : 0.0),
anchor: .center)
}
init(animate: Binding<Bool>,
#ViewBuilder content: #escaping () -> Content) {
self.content = content()
self._animate = animate
}
}
Use
#State private var editMode = false
WiggleAnimation(animate: $editMode) {
VStack {
Image(systemName: image)
.resizable()
.frame(width: UIScreen.screenWidth * 0.1,
height: UIScreen.screenWidth * 0.1)
.padding()
.foregroundColor(.white)
.background(.gray)
Text(text)
.multilineTextAlignment(.center)
.font(KMFont.tiny)
.foregroundColor(.black)
}
}
How does it work?
.id(animate) modifier here does not refresh the view but just replaces it with a new one, so it is back to its original state.
Again this might not be the best solution but it works for my case.
There is nothing wrong in your code, so I assume it is Apple's defect. It seems there are many with implicit animations (at least with Xcode 11.2). Anyway...
I recommend to consider alternate approach provided below that gives expected behaviour.
struct TestAnimationDeactivate: View {
#State var active: Bool = false
var body: some View {
VStack {
if active {
BlinkBadge()
} else {
Badge()
}
}
.frame(width: 100, height: 100)
.onTapGesture {
self.active.toggle()
}
}
}
struct Badge: View {
var body: some View {
Circle()
}
}
struct BlinkBadge: View {
#State private var animating = false
var body: some View {
Circle()
.scaleEffect(animating ? 1.08: 1)
.animation(Animation.default.repeatForever(autoreverses: true))
.onAppear {
self.animating = true
}
}
}
struct TestAnimationDeactivate_Previews: PreviewProvider {
static var previews: some View {
TestAnimationDeactivate()
}
}
Aspid comments on the accepted solution that an Xcode update broke it. I was struggling with a similar problem while playing around with an example from Hacking with Swift, and
.animation(active ? Animation.default.repeatForever() : Animation.default)
was not working for me either on Xcode 13.2.1. The solution I found was to encapsulate the animation in a custom ViewModifier. The code below illustrates this; the big button toggles between active and inactive animations.
`
struct ContentView: View {
#State private var animationAmount = 1.0
#State private var animationEnabled = false
var body: some View {
VStack {
Button("Tap Me") {
// We would like to stop the animation
animationEnabled.toggle()
animationAmount = animationEnabled ? 2 : 1
}
.onAppear {
animationAmount = 2
animationEnabled = true
}
.padding(50)
.background(.red)
.foregroundColor(.white)
.clipShape(Circle())
.overlay(
Circle()
.stroke(.red)
.scaleEffect(animationAmount)
.opacity(2 - animationAmount)
)
.modifier(AnimatedCircle(animationAmount: $animationAmount, animationEnabled: $animationEnabled))
}
}
}
struct AnimatedCircle: ViewModifier {
#Binding var animationAmount: Double
#Binding var animationEnabled: Bool
func body(content: Content) -> some View {
if animationEnabled {
return content.animation(.easeInOut(duration: 2).repeatForever(autoreverses: false),value: animationAmount)
}
else {
return content.animation(.easeInOut(duration: 0),value: animationAmount)
}
}
}
`
It may not be the best conceivable solution, but it works. I hope it helps somebody.

SwiftUI: Generic parameter 'Subject' could not be inferred

I built a LoadingView with SwiftUI for showing some loading stuff in my app while I'm fetching remote data from an API. I am on Xcode Version 11.0 beta 5.
This is the LoadingView:
struct LoadingView<Content>: View where Content: View {
#Binding var isShowing: Bool
var content: () -> Content
var body: some View {
GeometryReader { geometry in
ZStack(alignment: .center) {
self.content()
.disabled(self.isShowing)
.blur(radius: self.isShowing ? 3 : 0)
VStack {
Text("Loading...")
ActivityIndicator(isAnimating: .constant(true), style: .large)
}
.frame(width: geometry.size.width / 2,
height: geometry.size.height / 5)
.background(Color.white)
.foregroundColor(Color.primary)
.cornerRadius(5)
.opacity(self.isShowing ? 1 : 0)
}
}
}
}
This is my DataStore. It is declared as ObservableObject and has more than one #Published property. Also it does some remote fetching from an API:
class CharacterStore: ObservableObject {
#Published private(set) var isLoading = false
// Fetches some stuff from a remote api
func fetch() {
self.isLoading = true
myService.getCharacters { (result) in
DispatchQueue.main.async {
self.isLoading = false
}
}
}
}
And finally this is the View I want to show my LoadingView with the content of ContentView in it. Of course I am setting the #EnvironmentObject before showing this view.
struct ContentView: View {
#EnvironmentObject var charStore: CharacterStore
var body: some View {
LoadingView(isShowing: self.$charStore.isLoading) { // Here I get the error
// Show some Content here
Text("")
}
}
}
The problem is that I want to bind self.$charStore.isLoading to LoadingView. In this line i get the following error:
Generic parameter 'Subject' could not be inferred
I tried in several ways but none of these things work. Btw: If I use a #State property in ContentView it just works fine like this:
struct ContentView: View {
#EnvironmentObject var charStore: CharacterStore
#State var loads: Bool = false
var body: some View {
LoadingView(isShowing: self.$loads) { // Here I get no error
// Show some Content here
Text("")
}
}
}
Am I missing a thing? If you need further informations let me know i can provide more content if needed.
Thanks for the help!
Since your LoadingView is not going to modify .isLoading, you do not need to pass it as a binding:
LoadingView(isShowing: self.$charStore.isLoading)
Instead, remove the #Binding in LoadingView:
struct LoadingView<Content>: View where Content: View {
var isShowing: Bool
...
and create it like this (remove the dollar sign):
LoadingView(isShowing: self.charStore.isLoading) { ... }
On the contrary, if you insist on passing a binding, then you need to remove the private(set) from:
#Published private(set) var isLoading = false
Couldn't you do the following:
Replacing the #Binding by the same #EnvironmentObject as the ContentView uses.
struct LoadingView<Content>: View where Content: View {
#EnvirontmentObject var charStore: CharacterStore // added
//#Binding var isShowing: Bool // removed
var content: () -> Content
var body: some View {
GeometryReader { geometry in
ZStack(alignment: .center) {
self.content()
.disabled(self.$charStore.isLoading) // Changed
.blur(radius: self.$charStore.isLoading ? 3 : 0) // Changed
VStack {
Text("Loading...")
ActivityIndicator(isAnimating: .constant(true), style: .large)
}
.frame(width: geometry.size.width / 2,
height: geometry.size.height / 5)
.background(Color.white)
.foregroundColor(Color.primary)
.cornerRadius(5)
.opacity(self.$charStore.isLoading ? 1 : 0) // Changed
}
}
}
}
Of course, you also have to remove the isShowing parameter from the LoadingView() initializer in the ContentView.
Please correct me if I am wrong!