why do we wrap Action with an explicit animation instead of wrapping State change in the Reducer - swift-composable-architecture

In their series on SwiftUI animation, the PointFree guys recommend we use explicit animations that wrap actions sent to the reducer in order to trigger animations when state is updated.
My question is why we don't instead wrap the actual State change, performed in the Reducer?
If I wrap the State change with an explicit animation, nothing happens. But if I wrap the Action being sent to the Reducer, animations work.
This code below is roughly adapted from https://www.pointfree.co/episodes/ep136-swiftui-animation-the-basics around minute 16.23
//works
TextField(
"search",
text: viewStore.binding(get: \.searchText, send:
TCSearchAction.updateSearch).animation(.easeInOut)
)
If I remove the animation call in the binding above, and add it directly to the state change, animations don't seem to fire.
// doesn't fire animations
static let reducer = Reducer<TCSearchState, TCSearchAction, Void> {state, action, _ in
switch action {
case let .updateSearch(string):
//this does not trigger animations when value is changed
withAnimation(.easeInOut)
{
state.searchText = string
}

Related

Update UI rendering in React Natives 'focus' Event Listener

I am navigating from different class objects (screens) within my React Native App. Once I navigate to a given screen, I want to execute different functions including Activity Indicators to show that the function is still running.
Therefore is set up a focus Event Listener which is called every time I navigate back to my screen. My current problem is that my Activity Indicators are not updated within the focus Event but directly after, which is not my wanted behavior as I have to wait for my functions to end without any visual indicator.
class Homescreen extends Component {
constructor(props){
super(props};
this.state = {
showIndicator: false;
}
}
componentDidMount(){
this.props.navigation.addListener('focus', this._onFocus);
}
componentWillUnmount(){
this.props.navigation.removeListener('focus', this._onFocus);
}
_onFocus = async () => {
this.setState({showIndicator: true});
asyncFunc();
};
async asyncFunc(){
//calling a function from swift which calls a C function inside DispatchQeue.main.async
//inside my C function I successfully set my ActivityIndicator to false with an Event which calls
//this.setState({showIndicator: false});
}
render(){
console.log(this.state.showIndicator); // correct value is printed while I wait inside the ```focus``` Event to finish, but the UI is not updated.
return(
{this.state.showIndicator && <ActivityIndicator/>}
)
}
}
I receive the correct current status of showIndicatorbut before the focus Event is not finished, my render does not redraw.
To me it seems like the UI won't update until I am out of my focus Event. How can I reach my goal of displaying the ActivityIndicator before the focus Event or how do I call my function right after the focus Event?
I can achieve my wanted behavior by using the blurevent and setting my ActivityIndicator visible while moving to another screen and after moving back I wait until my function is finished and remove the ActivityIndicator but this seems like a bad workaround and also I can see the ActivityIndicator when moving which might be confusing.
You should replace curly braces around this statement {this.state.showIndicator && } and check.
If it doesn't work accordingly then let me know.

How to forcefully redraw a swift ui view within a button action call

So I'm doing something sort of jank, but I need to do it for a class that I am taking
I need to update the view after every step of a while loop that is inside of a button's action: callback, in order to create a really simple "animation"
is there any way to get a swift UI view to drop everything and redraw immediately?
code for my button:
Button(action: {
pass_again = true;
while(pass_again){
(right_text, pass_again) = twos.collapse(dir: Direction.right)
cur_id+=1
usleep(1000000)
// I need to redraw the UI here
}
twos.spawn()
}, label: {
Text(right_text)
}).padding(0.5)
You can bind an #State variable to any SwiftUI view i.e. Text(self.yourvar), so any time you change the #State value it will automatically update the text or any other property bonded with it.
About the animation you can use .withAnimation modifier to create a simple animation that swiftUI will perform
withAnimation {
your state var updates goes here
}

Understanding UIView.animate and how completion closures work

Is there is a way to interrupt animations in a way that doesn't "cancel" (rewind) them, but rather "fast-forwards" them forcing their completion closures to run earlier than originally planned?
Background:
In IOS, one can "animate a view with duration" and include a completion closure as well... using UIView's static method animate() like this:
class func animate(withDuration: TimeInterval, animations: () -> Void, completion: ((Bool) -> Void)? = nil)
A real-life example might look like EXHIBIT-A here:
// assume we have a UILabel named 'bigLabel'
func animationWeNeedToDo() {
UIView.animate(withDuration: 1, animations: {
self.bigLabel.alpha = 0
}, completion: {
if $0 {
UIView.animate(withDuration: 1, animations: {
self.bigLabel.center.x -= 20
}, completion: {
if $0 {
self.updateMainDisplay()
}
}) }
})
}
So we have a UILabel, bigLabel, that we are first animating to "fade," then we are chaining to yet another animation inside the completion of the first, then yet again in the completion of the second, we run the all-important function, updateMainDisplay().
But this simple example could be much more complex involving many more views. It could be imperative that updateMainDisplay() executes. ;)
The updateMainDisplay() function is important because it "resets" all the views, returning the app to a neutral state similar to when the app is originally started... sort of "re-calibrates" everything.
Anyhoo, the trouble is, if the user does something like push the home button early enough or segue to a new activity (modally, like settings... and then come back) while the animation is taking place, it never completes... and so updateMainDisplay() does not get executed! ...and things get complicated and nasty.
So, how to handle this problem?
Seems like something needs to be done in "onPause()" (I know this isn't Android)... like making sure that the animation is cancelled AND that updateMainDisplay() is executed.
But in order to do that you would have to check for all kinds of boolean states in the "onPause()" method. I would much prefer if there were a way to guarantee that the animation will complete.
So, once again, I'm thinking it would be pretty awesome if there were a way to not cancel the animations, but to "force immediate completion" of all animations.
This is pseudo-code... but is there a way to do something like this:
var myAnimation = (animation) { // EXHIBIT-A from above }
myAnimation.execute()
// if needed:
myAnimation.forceCompletionNow()
Does anyone know if that's possible?
Thanks.
The problem with your code is that you are checking the first argument of the completion closure. That indicates whether the animation finishes or not. And you only run updateMainDisplay() if that is true.
So in fact, the completion handler will be called even if the animation is not finished. It is you that told it to do nothing if the animation does not finish.
To fix this, just remove the if $0 statement.
Now Xcode will show a warning because you did not use the first argument of the closure. To silence this warning, just put _ in at the start of the closure:
{ _ in
// some code
}
Another thing that you can try is CABasicAnimation which does not actually change the view's properties. It animates the CALayers. If you update the view again in some way, the view will have gone back to its original state before the animation. You seem to want to reset everything after the animation finishes so this might be suitable for you.

How do I make a closure wait until the player pressed a button in a different view controller?

I am writing a chess GUI in Swift 3 and use nvzqz/Sage as the chess model/library. Now I face a problem with a Sage closure used for piece promotion.
Sage uses (in its game class) the execute(move: promotion:) method for promotion move execution which has a closure that returns a promotion piece kind. This allows to prompt the user for a promotion piece or perform any other operations before choosing a promotion piece kind, as follows:
try game.execute(move: move) {
...
return .queen
}
I implemented a promotion view controller ("pvc") which is called in this closure so that the player may select the new piece:
// This is in the main View Controller class
/// The piece selected in promotionViewController into which a pawn shall promote
var newPiece: Piece.Kind = ._queen // default value = Queen
try game.execute(move: move) {
boardView.isUserInteractionEnabled = false
// promotionview controller appears to select new promoted piece
let pvc = PromotionViewController(nibName: nil, bundle: nil)
pvc.delegate = self
pvc.modalPresentationStyle = .overCurrentContext
self.present(pvc, animated:true)
return newPiece
}
When the button for the new piece in the pvc is pressed, the pvc dismisses itself and the data of the selected piece (the constant selectedType) is transferred back to the main view controller via delegation:
// This is in the sending PVC class
protocol PromotionViewControllerDelegate {
func processPromotion(selectedType: Piece.Kind)
}
func buttonPressed(sender: UIButton) {
let selectedType = bla bla bla ...
delegate?.processPromotion(selectedType: selectedType)
presentingViewController!.dismiss(animated:true)
}
// This is in the receiving main View Controller class
extension GameViewController: PromotionViewControllerDelegate {
func processPromotion(selectedType: Piece.Kind) {
defer {
boardView.isUserInteractionEnabled = true
}
newPiece = selectedType
}
The problem I have is that the closure (in the game.execute method) does not wait until the player made his selection in the pvc (and immediately returns the newPiece variable which is still the default value) so that I never get another promotion piece processed other than the default value.
How do I make the closure wait until the player pressed a button in the pvc?
Of course, I tried to find a solution and read about callbacks, completion handlers or property observers. I do not know which is the best way forward, some thoughts:
Completion handler: the pvc dismisses itself upon button-press event so the completion handler is not in the receiving (main) view controller. How do I deal with this?
Property observer: I could call the try game.execute(move) method only after the promotion piece was set (with didset) but that would make the code difficult to read and not use the nice closure the game.execute method provides.
Callbacks may be related to completion handlers, but am not sure.
So your block in game.execute(move: move) will fully execute which is so designed by the Sage API. You can not pause it as easy but it is doable, still let's try to solve it the other way;
Why do you need to call the presentation of the view controller within this block? By all means try to move that away. The call try game.execute(move: move) { should only be called within processPromotion delegate method. You did not post any code but wherever this try game.execute(move: move) { code is it needs to be replaced by presenting a view controller alone.
Then on delegate you do not even need to preserve the value newPiece = selectedType but rather just call try game.execute(move: move) { return selectedType }.
So about pausing a block:
It is not possible to directly "pause" a block because it is a part of execution which means the whole operation needs to pause which in the end means you need to pause your whole thread. That means you need to move the call to a separate thread and pause that one. Still this will only work if the API supports the multithreading, if the callback is called on the same tread as its execute call... So there are many tools and ways on how to lock a thread so let me just use the most primitive one which is making the thread sleep:
var executionLocked: Bool = false
func foo() {
DispatchQueue(label: "confiramtion queue").async {
self.executionLocked = true
game.execute(move: move) {
// Assuming this is still on the "confiramtion queue" queue
DispatchQueue.main.async {
// UI code needs to be executed on main thread
let pvc = PromotionViewController(nibName: nil, bundle: nil)
pvc.delegate = self
pvc.modalPresentationStyle = .overCurrentContext
self.present(pvc, animated:true)
}
while self.executionLocked {
Thread.sleep(forTimeInterval: 1.0/5.0) // Check 5 times per second if unlocked
}
return self.newPiece // Or whatever the code is
}
}
}
Now in your delegate you need:
func processPromotion(selectedType: Piece.Kind) {
defer {
boardView.isUserInteractionEnabled = true
}
newPiece = selectedType
self.executionLocked = false
}
So what happens here is we start a new thread. Then lock the execution and start execution on game instance. In the block we now execute our code on main thread and then create an "endless" loop in which a thread sleeps a bit every time (the sleep is not really needed but it prevents the loop to take too much CPU power). Now all the stuff is happening on main thread which is that a new controller is presented and user may do stuff with it... Then once done a delegate will unlock the execution lock which will make the "endless" loop exit (on another thread) and return a value to your game instance.
I do not expect you to implement this but if you will then ensure you make all precautions to correctly release the loop if needed. Like if view controller is dismissed it should unlock it, if a delegate has a "cancel" version it should exit...

Swift: feedback when function ends

Let's say I have a ViewController A and a class B.
when I press some button inside A, it calls an IBAction that calls a function B.foo() which returns an Int
B.foo() takes 8~10 seconds to finish and while it runs I'd like to put an Loading... animation on A, and when B.foo() finishes, the animation would stop.
How can I do this? this is an pseudo-code example of what I wish:
#IBAction func buttonPressed(_ sender: UIButton){
UIView.animate(blablabla......)
DO({
self.answer = B.foo()
}, andWhenItFinishesDo: {
self.someone.layer.removeAllAnimation()
})
}
This is a very common problem. One way to solve it would be to use different queues (You can think of them as lines of work that can happen in parallel).
The the basic idea is that once your button is pressed, you show your loading indicator and "dispatch" the long work to a secondary queue, that will operate in the background and do the work. This ensures that your main queue does not block while the work happens and the user interface stays responsive.
The trick is now that you want to get notified when the long work is finished so that you can stop showing the loading indicator (and possibly do even more).
While you actually could use some kind of notification system, there are other, sometimes more appropriate ways. It would actually be even more convenient, if you could just tell the long running function to call you back specifically with code that you provide.
That would be the basic concept of a "completion handler" or "callback".
The whole thing would look something like that:
// Some made up class here
class B {
// This is only static because I do not have an instance of B around.
static func foo(completion: #escaping (Int) -> Void ) {
// The method now does all of its work on a background queue and returns immediately
DispatchQueue.global(qos: .background).async {
// In the background this may take as long as it wants
let result = longCalculation()
// VERY important. The caller of this function might have a certain
// expectation about on which queue the completion handler runs.
// Here I just use the main queue because this is relatively safe.
// You could let the caller provide a queue in the function
// parameters and use it here
DispatchQueue.main.async {
// The completion handler is a function that takes an Int.
// That is exactly what you are providing here
completion(result)
}
}
}
}
#IBAction func buttonPressed(_ sender: UIButton){
self.showLoadingIndicator()
// The foo function now takes a completion handler that gets the result in.
// You have to provide this function here and do something with the result
//
// The completion handler will only be run when the foo function calls it
// (which is after the computation as you can see in the method above.
//
// I am also telling the completion handler here that self should not be
// held on to as the view controller might already have gone away when the
// long calculation finished. The `[weak self]` thingy makes that inside
// your completion handler self is an optional and might be nil (and it
// doesn't hold a strong reference to self, but that's a whole other topic)
B.foo(completion: { [weak self] result in
// Do something with the result
// Since we are called back on the main queue we can also do UI stuff safely
self?.hideLoadingIndicator()
})
}
I hope this helps a bit.
Asynchronous programming can be quite difficult to learn but there are tons of tutorials and examples you can find on this topic.
Hey Hamish you can do this in two simple ways,
First one is using the defer statements provided for functions.
Defer statement block is executed after the functions goes out of scope.
here is a simple example to describe the same.
func print1000000() {
//start displaying the loading indicator
defer {
// hide the loading indicator and move to the next ViewController
let seVC = storyboard?.instantiateViewController(withIdentifier: "SecondVC") as! SecondVC
self.navigationController?.pushViewController(seVC, animated: true)
}
// here goes the task you want to execute such as downloading a file or the one i did here
for index in 0...1000000 {
print(index)
}
}
The above function prints numbers upto 1000000 and then pushes the control to another ViewController
=========================================================================
Second way of doing it is by using closures, as described by Thomas in his answer.

Resources