UIViewPropertyAnimator does not update the view when expected - swift

Here is a Swift playground code, which I also tested in the iOS Simulator just to be sure that it's not an issue of Playground.
Setting the property fractionComplete result in change of state from inactive to active. However as shown below the view is not updated for the first two calls. Only the third call magically updates the view.
This behavior is confusing and I seek for explanation.
let liveView = UIView(frame: CGRect(x: 0, y: 0, width: 400, height: 50))
liveView.backgroundColor = .white
PlaygroundPage.current.needsIndefiniteExecution = true
PlaygroundPage.current.liveView = liveView
let square = UIView(frame: CGRect(x: 0, y: 0, width: 50, height: 50))
square.backgroundColor = .red
liveView.addSubview(square)
let animator = UIViewPropertyAnimator.init(duration: 5, curve: .easeIn)
animator.addAnimations {
square.frame.origin.x = 350
}
// State: inactive
// Does nothing, position is at 0.0
DispatchQueue.main.async {
animator.fractionComplete = 0.5
}
// State: active
// Stil does nothing, position is at 0.0
DispatchQueue.main.async {
animator.fractionComplete = 0.5
}
// Updates from 0.0 to 0.75 - Why now?
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
animator.fractionComplete = 0.75
}
Update: Putting this two lines before the first call seems to be a workaround for the issue.
animator.startAnimation()
animator.pauseAnimation()
I assume it's an internal bug. rdar://30856746

Related

How to redraw a subviews in landscape

Good afternoon Community,
I am trying to add two subviews to a viewcontroller by means of an extension, but when rotating the device it does not respect the layout, any option on how to make it look correct?
I attach my code below and a couple of example photos, the first photo is how I would like it to look, and the rest of the ui is lost.
This is my code:
func presentCustomAlertController(on ViewController:UIViewController,toPresentCustomView customView:UIView,withBackgroundView backGroundView:UIView){
customView.translatesAutoresizingMaskIntoConstraints = false
guard let targetView = ViewController.view else {return}
backGroundView.alpha = 0
backGroundView.backgroundColor = .black
backGroundView.frame = targetView.bounds
targetView.addSubview(backGroundView)
targetView.addSubview(customView)
customView.setNeedsLayout()
customView.setNeedsDisplay()
customView.layoutIfNeeded()
customView.layoutSubviews()
targetView.setNeedsLayout()
targetView.setNeedsDisplay()
targetView.layoutIfNeeded()
targetView.layoutSubviews()
backGroundView.setNeedsDisplay()
backGroundView.setNeedsLayout()
backGroundView.layoutIfNeeded()
customView.frame = CGRect(x: targetView.center.x, y: targetView.center.y, width: targetView.frame.size.width-80, height: 300)
UIView.animate(withDuration: 0.25, animations: {
backGroundView.alpha = 0.6
},completion: { done in
UIView.animate(withDuration: 0.25, animations: {
customView.center = targetView.center
})
})
}
}

animation not moving upwards in chaining animation in swift

I want my swift code to follow the gif I created. What is going is after the first animation there is not any change of the position. Like the gif I created it should first move down then move up again. I tried multiplying by the negative -0.15 thinking it would go in the north direction. Right now it is not having a effect.
UIView.animate(withDuration: 1, animations: {
self.block1.frame = CGRect(x: 0, y: self.view.frame.height * 0.15, width: self.view.frame.width, height: self.view.frame.height * 0.1)
self.block1.center = self.view.center
}) { done in
/// first animation finished, start second
if done {
UIView.animate(withDuration: 1, animations: {
self.block1.frame = CGRect(x: 0, y: self.view.frame.height * (-0.15), width: self.view.frame.width, height: self.view.frame.height * 0.1)
self.block1.center = self.view.center
})
}
}
Start by getting rid of self.block1.center = self.view.center as you're making the centre point of the block the same as the view in both cycles.
self.view.frame.height * (-0.15) is setting an absolute position out side of the view, not sure if that's intentional, but you might want to be aware of it. Instead, if you only wanted to move a given distance, say the height of the view, you should be using a relative position, such as block1.frame.origin.y -= block.frame.size.height (probably a much easier way to say that, but you get the idea)
So I created a really quick Playground, made up some values and got a "sort of" bouncy, returny thing going as an example
import UIKit
import XCPlayground
let view = UIView(frame: CGRect(x: 0.0, y: 0.0, width: 300.0, height: 600.0))
// XCPShowView("container", view: container) // -> deprecated
let block1 = UIView(frame: CGRect(x: 0.0, y: 0.0, width: (view.frame.width) / 2, height: 150))
block1.backgroundColor = UIColor.green
block1.center.x = view.center.x
view.addSubview(block1)
UIView.animate(withDuration: 1, animations: {
let y = view.frame.height * 0.15
print(y)
block1.center.y = view.center.y
}) { done in
print("Done = \(done)")
/// first animation finished, start second
if done {
UIView.animate(withDuration: 1, animations: {
block1.frame.origin.y = 0
})
}
}
XCPlaygroundPage.currentPage.liveView = view
Caveat: There's probably a really neat and easy way to do "auto reversal" style animations using CALayer and I'd be keen to see other examples presenting the same concept

I'm trying to make a card that slides while on swipe in swift

I'm having a hard time with animations and PanGestureRecognizers, I want a view that slides by dragging up or dawn with your finger. It has 4 parts
1 - 2 the height, width and bottom constraints need to be changed
2 - 3 the height is the maximum height and you can slide it in or out
3 - 4 the card is at its maximum height and you can swipe it down
here is an image for a better understanding
thanks!
this is how I would do it.
var theView = UIView()
override func viewDidLoad() {
super.viewDidLoad()
// First create your view.
theView = UIView(frame: CGRect(x: 40, y: self.view.bounds.height - 120, width: self.view.frame.width - 80, height: 100))
// set its background color
theView.backgroundColor = UIColor.black
// Create the round corners
theView.layer.cornerRadius = 20
// And add it to the view
self.view.addSubview(theView)
// Create the UIPanGestureRecognizer and assign the function to the selector
let gS = UIPanGestureRecognizer(target: self, action: #selector(growShink(_:)))
// Enable user interactions on the view
theView.isUserInteractionEnabled = true
// Add the gesture recognizer to the view
theView.addGestureRecognizer(gS)
}
// The control value is used to determine wether the user swiped up or down
var controlValue:CGFloat = 0
var animating = false
// The function called when the user swipes
#objc func growShink(_ sender:UIPanGestureRecognizer){
let translation = sender.location(in: theView)
// Check the control value to see if it has been set
if controlValue != 0 && !animating{
// if it has been set check that the control value is greater than the new value recieved by the pan gesture
if translation.x < controlValue{
animating = true
// If it is, change the values of the frame using animate
UIView.animate(withDuration: 0.5, animations: {
self.theView.frame = CGRect(x: 0, y: self.view.bounds.height - 300, width: self.view.frame.width, height: 300)
}, completion: {finished in
UIView.animate(withDuration: 1.0, animations: {
self.theView.frame = CGRect(x: 0, y: 0, width: self.view.frame.width, height: self.view.frame.height)
}, completion: {finished in self.animating = false; self.controlValue = 0})})
}else if translation.x > controlValue{
animating = true
// else, change the values of the frame back.
UIView.animate(withDuration: 0.5, animations: {
self.theView.frame = CGRect(x: 0, y: self.view.bounds.height - 300, width: self.view.frame.width, height: 300)
}, completion: {finished in
UIView.animate(withDuration: 1.0, animations: {
self.theView.frame = CGRect(x: 40, y: self.view.bounds.height - 120, width: self.view.frame.width - 80, height: 100)
}, completion: {finished in self.animating = false; self.controlValue = 0})})
}
}
// set the control value to the value received from the pan gesture
controlValue = translation.x
}
}
You can adjust the width and height values however you want.

How to make this UIButton's action operate correctly?

Every time I run this swift playground (on Xcode), I get an error on the last line saying:
'PlaygroundView' cannot be constructed because it has no accessible initialisers.
I also have another error on line 4 saying:
class 'PlaygroundView' has no initializers.
import UIKit
import PlaygroundSupport
class PlaygroundView:UIViewController {
var introImage:UIImageView
var introButton:UIButton
override func loadView() {
print("workspace started running.")
//Main Constant and Variable Declarations/General Setup
let mainView = UIView()
//Main View Modifications & styling
mainView.backgroundColor = UIColor.init(colorLiteralRed: 250, green: 250, blue: 250, alpha: 1)
func addItem(item: UIView) {
mainView.addSubview(item)
}
//Sub-Element Constant and Variable Declarations
introImage = UIImageView(frame: CGRect(x: 87.5, y: -300, width: 200, height: 200))
introImage.layer.borderColor = UIColor.black.cgColor
introImage.layer.borderWidth = 4
introImage.alpha = 0
introImage.transform = CGAffineTransform(rotationAngle: -90.0 * 3.14/180.0)
introImage.image = UIImage(named: "profile_pic.png")
introImage.image?.accessibilityFrame = introImage.frame
introImage.layer.cornerRadius = 100
addItem(item: introImage)
introButton = UIButton(frame: CGRect(x: 87.5, y: 900, width: 200, height: 30))
introButton.alpha = 0
introButton.setTitle("Tap to meet me", for: .normal)
introButton.titleLabel?.font = UIFont(name: "Avenir Book", size: 20)
introButton.backgroundColor = UIColor.black
introButton.layer.cornerRadius = 15
introButton.layer.borderWidth = 5
addItem(item: introButton)
introButton.addTarget(self, action: #selector(self.introButtonAction), for: .touchDown)
UIView.animate(withDuration: 1.5, delay: 1, options: .curveEaseInOut, animations: {
self.introImage.frame = CGRect(x: 87.5, y: 175, width: 200, height: 200)
self.introButton.frame = CGRect(x: 87.5, y: 400, width: 200, height: 30)
self.introImage.transform = CGAffineTransform(rotationAngle: 0.0 * 3.14/180.0)
self.introImage.alpha = 1
self.introButton.alpha = 1
}) { (mainAnimationComped) in
if mainAnimationComped == true {
print("introduction animation comped.")
}
}
}
//User Interface Actions
func introButtonAction() {
print("user interacted with user interface")
}
}
PlaygroundPage.current.liveView = PlaygroundView()
How would I fix this problem?
That's because in Swift, you need to initialize variables before use them in the code. On the other hand, since in this case you set them explicitly in your function, you might declare them as implicitly unwrapped optionals
var introImage:UIImageView!
var introButton:UIButton!
and this should work without changing anything else in your code
Add ? after introImage and introButton
var introImage:UIImageView?
var introButton:UIButton?

Animate the fractionComplete of UIViewPropertyAnimator for blurring the background

So I'm using the new UIViewPropertyAnimator and UIVisualEffectView to achieve the same thing as the Spotlight search when you scrolling down on the home screen and it blurs the background.
I'm using the fractionComplete property to set the procent of how much to blur when panning a UIView.
animator = UIViewPropertyAnimator(duration: 1, curve: .linear) {
self.blurEffectView.effect = nil
}
And the amount of blurriness is changed with a value between 0.0 - 1.0.
animator?.fractionComplete = blurValue
But when I cancel the pan gesture I want the blur to animate back from where it is to no blur (e.g ~ -> 1.0) with a duration of something like 0.4 milliseconds.
Right now I just set the fractionComplete to 1.0 when the pan gesture is cancelled. Instead I want to animate it.
I have tried the UIView.animate(withDuration.. but it doesn't affect the UIViewPropertyAnimators fractionComplete, and thats the only way to blur an UIVisualEffectView.
Any ideas?
It seems that fractionComplete has a bug (my question on Stackoverflow: UIViewPropertyAnimator does not update the view when expected), rdar://30856746. The property only sets the state from inactive to active, but does not update the view, because (I assume) there is another internal state that does not trigger.
To workaround the problem you can do this:
animator.startAnimation() // This will change the `state` from inactive to active
animator.pauseAnimation() // This will change `isRunning` back to false, but the `state` will remain as active
// Now any call of `fractionComplete` should update your view correctly!
animator.fractionComplete = /* your value here */
Here is a playground snippet to play around:
let liveView = UIView(frame: CGRect(x: 0, y: 0, width: 400, height: 50))
liveView.backgroundColor = .white
PlaygroundPage.current.needsIndefiniteExecution = true
PlaygroundPage.current.liveView = liveView
let square = UIView(frame: CGRect(x: 0, y: 0, width: 50, height: 50))
square.backgroundColor = .red
liveView.addSubview(square)
let animator = UIViewPropertyAnimator.init(duration: 5, curve: .linear)
animator.addAnimations {
square.frame.origin.x = 350
}
let blurView = UIVisualEffectView(effect: UIBlurEffect(style: .dark))
blurView.frame = liveView.bounds
liveView.addSubview(blurView)
animator.addAnimations {
blurView.effect = nil
}
// If you want to restore the blur after it was animated, you have to
// safe a reference to the effect which is manipulated
let effect = blurView.effect
animator.addCompletion {
// In case you want to restore the blur effect
if $0 == .start { blurView.effect = effect }
}
animator.startAnimation()
animator.pauseAnimation()
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
animator.fractionComplete = 0.5
}
DispatchQueue.main.asyncAfter(deadline: .now() + 4) {
// decide the direction you want your animation to go.
// animator.isReversed = true
animator.startAnimation()
}
If you're still looking for a way to actually animate fractionComplete without the use of a slider or a gesture, I was quite happy with my results using a CADisplayLink. You can see my results here: https://gist.github.com/vegather/07993d15c83ffcd5182c8c27f1aa600b
I've used a delayed loop to decrease "fractionComplete"
func resetAnimator(){
let duration = 1.0 / 60.0
if self.animator.fractionComplete > 0 {
self.animator.fractionComplete -= 0.06
delay(duration, closure: {
self.resetAnimator()
})
}
}
func delay(_ delay:Double, closure:#escaping ()->()) {
DispatchQueue.main.asyncAfter(
deadline: DispatchTime.now() + Double(Int64(delay * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC), execute: closure)
}