Keep track of UIView position during translation animation - ios

I know this has been asked before but all the answers were provided in Objective C and I'm looking for a Swift solution. If I've missed an existing Swift solution, please let me know and I'll close this question.
This is how I'm animating view:
UIView.animate(withDuration: 10.0, animations: { () in
let translateTransform = CGAffineTransform.init(translationX: 0.0, y: Constants.screenHeight)
self.icon.transform = translateTransform
})
What I'd like to do is keep track of current frame position throughout the animation. Do I need to take a different approach to achieve that?

You may want to try this to get a view's frame during an animation:
let currentFrame = myView.layer.presentation()!.frame
That will get you the frame at the time the code runs so if you wanted a record of the frames throughout the animation you may want to use a Timer (previously NSTimer).
In this example the optional is force unwrapped so if you're not sure if it's nil or not you may want to use an if-let statement.
Hope this helps and let me know if you have any other problems.

UIView.frame is observable. Try to use KVO for the frame keyPath:
view.addObserver(self, forKeyPath:"frame", options:.new, context:nil)
override func observeValue(forKeyPath keyPath: String, ofObject object: Any, change: [String: id], context: UnsafeMutableRawPointer) {
print("value of \(keyPath) changed: \(change[NSKeyValueChangeNewKey])")
}
KVO doesn't work for classes that don't extend NSObject.
In order to use key-value observing with a Swift class you should inherit from NSObject.
Add the dynamic modifier to any property you want to observe.
More on dynamic: https://developer.apple.com/library/content/documentation/Swift/Conceptual/BuildingCocoaApps/InteractingWithObjective-CAPIs.html#//apple_ref/doc/uid/TP40014216-CH4-ID57
class YourClass: NSObject {
dynamic var view: UIView = ...
}
Full article on KVO:
https://developer.apple.com/library/content/documentation/Swift/Conceptual/BuildingCocoaApps/AdoptingCocoaDesignPatterns.html#//apple_ref/doc/uid/TP40014216-CH7-XID_8

Related

How to optionally animate property of a CALayer with custom animation

I have a CALayer with a custom animation on it, working via an #NSManaged property, and overriding:
class func defaultValue(forKey key: String) -> Any?
class func needsDisplay(forKey key: String) -> Bool
func action(forKey key: String) -> CAAction?
func display()
However, I sometimes want to bypass the animation and have the property immediately step to the new value. In my CALayer sub-class I tried this:
#NSManaged private var radius: CGFloat
func animate(to radius: CGFloat) {
self.radius = radius
}
func step(to radius: CGFloat) {
// Inspired by https://stackoverflow.com/a/34941743
CATransaction.begin()
CATransaction.setDisableActions(true) // Prevents animation occurring
self.radius = radius
CATransaction.commit()
setNeedsDisplay() // Has no effect
}
When animate(to:) is called, display() is called repeatedly and my custom drawing code can do it's thing. When step(to:) is called, the CATransaction code does prevent an animation from occurring, but no drawing is ever performed at all.
I can get it to behave as desired, but it feels quite hacky:
func step(to radius: CGFloat) {
// func action(forKey key: String) -> CAAction? uses .animationDuration
// when constructing a CABasicAnimation
let duration = animationDuration
defer { animationDuration = duration }
animationDuration = 0
self.radius = radius
}
What is the correct method to give the caller the ability to choose whether the property animates from one value to the next, or steps immediately? A subsequent change to radius should respect the previous value, whether it was stepped or animated to.
You say you have implemented action(forKey:) (quite rightly). So on those occasions when you don't want this property to be animated, return nil from that method. The drawing will still take place, but without animation.
Alternatively, you could return super.action(forKey:key). That might be a little more sane, but the outcome is the same.
You may ask (and I hope you do): How can I throw some kind of switch that action(forKey:) can consult in order to know which kind of occasion this is? One possibility is to set a property of the layer using key-value coding.
CALayer has a wonderful feature that you are allowed to call setValue(_:forKey:) or value(forKey:) for any key; it doesn't have to be a "real" key that already exists.
So you could call setValue(false, forKey:"shouldAnimate") on the layer before setting the property. And your action(forKey:) can then consult value(forKey:"shouldAnimate"), and see whether it is false (as opposed to true or nil) — and if it is, it returns nil to prevent the animation.

Why isn't the draw method of the custom CALayer called earlier?

Issue
I trigger random animations on a selection of UIViews using a Timer. All animations work as intended but one where the draw method of a CALayer is called after the exit of the timer selector.
Detailed Description
For the sake of clarity and simplification, let me schematise the actions performed.
All the animations I have created so far work as intended: they are a combination of CABasicAnimations on existing subviews/sublayers or new ones added to the selected view hierarchy. They are coded as an UIView extension, so that they can be called on any views, irrespectively of the view or the view controller the view is in. Well, all work except one.
I have indeed created a custom CALayer class which consists in drawing patterns on a CALayer. In an extension of this custom class, there is a method to animate those patterns (see hereafter the code). So all in all, when I reach the step/method animate selected view and run this particular animation, here is what should happen:
a method named animatePattern is called
this method adds the custom CALayer class to the selected view and then calls the animation extension of this layer
The issue: if with all the other animations, all the drawings are performed prior to the exit of the animate selected view step/method, in that particular case, the custom CALayer class draw method is called after the exit of the performAnimation method, which in turn results in the crash of the animation.
I should add that I have tried the custom CALayer class animation in a separate and simplified playground and it works well (I add the custom layer to a UIView in the UIViewController's viewDidLoad method and then I call the layer animation in the UIViewController's viewDidAppear method.)
The code
the method called by animate selected view step/method:
func animatePattern(for duration: TimeInterval = 1) {
let patternLayer = PatternLayer(effectType: .dark)
patternLayer.frame = self.bounds
self.layer.addSublayer(patternLayer)
patternLayer.play(for: duration)
}
(note that this method is in a UIView extension, therefore self here represents the UIView on which the animation has been called)
the simplified custom CALayer Class:
override var bounds: CGRect {
didSet {
self.setNeedsDisplay()
}
// Initializers
override init() {
super.init()
self.setNeedsDisplay()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
init(effectType: EffectType){
super.init()
// sets various properties
self.setNeedsDisplay()
}
// Drawing
override func draw(in ctx: CGContext) {
super.draw(in: ctx)
guard self.frame != CGRect.zero else { return }
self.masksToBounds = true
// do the drawings
}
the animation extension of the custom CALayer class:
func play(for duration: Double, removeAfterCompletion: RemoveAfterCompletion = .no) {
guard self.bounds != CGRect.zero else {
print("Cannot animate nil layer")
return }
CATransaction.begin()
CATransaction.setCompletionBlock {
if removeAfterCompletion.removeStatus {
if case RemoveAfterCompletion.yes = removeAfterCompletion { self.fadeOutDuration = 0.0 }
self.fadeToDisappear(duration: &self.fadeOutDuration)
}
}
// perform animations
CATransaction.commit()
}
Attempts so far
I have tried to force draw the layer by inserting setNeedsDisplay / setNeedsLayout at various places in the code but it does not work: debugging the custom CALayer class's draw method is constantly reached after the exit of the performAnimation method, whilst it should be called when the layer's frame is modified in the animatePattern method. I must miss something quite obvious but I am currently running in circles and I'd appreciate a fresh pair of eyes on it.
Thank you in advance for taking the time to consider this issue!
Best,
You override the draw(_ context) as the UIView can be the delegate of CALayer.
UIView: {
var patternLayer : PatternLayer
func animatePattern(for duration: TimeInterval = 1) {
patternLayer = PatternLayer(effectType: .dark)
patternLayer.frame = self.bounds
self.layer.addSublayer(patternLayer)
}
func draw(_ layer: CALayer, in ctx: CGContext){
layer.draw(ctx)
if NECESSARY { patternLayer.play(for: duration)}
}
}
As it is frequently the case, when you take the time to write down your issue, new ideas start to pop up. In fact, I remembered that self.setNeedsDisplay() informs the system that the layer needs to be (re)drawn but it does not (re)draw it at once: it will be done during the next refresh. It seems then that the refresh occurs after the end of the cycle for that specific animation. In order to overcome this is issue, I first added a call for the display method right after the patternLayer bounds are set, and it worked, but given #Matt comment, I changed the solution by calling displayIfNeeded method instead. It works as well.
func animatePattern(for duration: TimeInterval = 1) {
let patternLayer = PatternLayer(effectType: .dark)
patternLayer.frame = self.bounds
self.layer.addSublayer(patternLayer)
// asks the system to draw the layer now
patternLayer.displayIfNeeded()
patternLayer.play(for: duration)
}
Again, should anyone come up with another more elegant solution/explanation, please do not refrain to share!

How to respond to programmatic changes of a TextView text

The TextView delegate is set:
textView.delegate = self //self being a UITextViewDelegate
but the delegate method doesn't get called when the text is set programmatically
func textViewDidChange(_ textView: UITextView) {
print(textView.text)
}
How to respond to text changes without going reactive?
This is how UITextField (and most of UIKit controls) behave- doesn't trigger event when set programatically. It makes sense- lets you avoid recurring, infinite calls.
If you really want to be notify when text is changed programatically, you have to subclass UITextField and override text property (probably attributedText also). Then in didSet block call delegate method.
Don't forget that UITextField inherits from UIControl- I would also call sendActions(for:) to make target-action mechanism fire.
I just tried KVO on UITextView,
self.textView1.text = "You are working, but I will change you in 5 seconds"
Add your observer
self.textView1.addObserver(self, forKeyPath: "text", options: NSKeyValueObservingOptions(rawValue: 0), context: nil)
Trigger text change programmatically, just an example do it the way you want.
DispatchQueue.main.asyncAfter(deadline:.now() + 5) {
self.textView1.text = "Changed after some time"
}
Override the KVO method.
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if object == self.textView1{
//do your thing here...
}
}
FYI from Apple docs below
Note: Although the classes of the UIKit framework generally do not
support KVO, you can still implement it in the custom objects of your
application, including custom views.
https://developer.apple.com/library/content/documentation/General/Conceptual/DevPedia-CocoaCore/KVO.html
declare your text view variable with #objc dynamic
declare and hold a variable with type NSKeyValueObservation
use function observe(_:changeHandler:) bind your text view's text property, hold the return value with variable declared in step 2
observe changes in changeHandler
example:
#objc dynamic private var textView: UITextView!
private var observation: NSKeyValueObservation?
func bind() {
observation = observe(\.textView.text, options: [.old, .new]) { object, change in
print(object, change)
}
}
You have misspelled the delegate with textView with _textView
func textViewDidChange(textView: UITextView) { //Handle the text changes here
print(textView.text); //the textView parameter is the textView where text was changed
}
Put this in viewDidLoad
textView!.delegate = self

Exclude UIView from animating on autorotation

TL;DR
Need to keep autorotation, but exclude one UIView from autorotating on orientation change, how?
Back story
I need to keep a UIView stationary during the animation accompanied by autorotation (which happens on orientation change). Similar to how the iOS camera app handles the rotation (i.e controls rotate in their place).
Things I've tried
Returning false from shouldAutorotate(), subscribing to UIDeviceOrientationDidChangeNotification, and trying to manually handle the rotation event for each view separately.
Works well if you don't need to change any of your UIViews' places, otherwise it's a pain figuring out where it should end up and how to get it there
Placing a non rotating UIWindow under the main UIWindow, and setting the main UIWindow background colour to clear.
This works well if it's only one item, but I don't want to manage a bunch of UIWindows
Inverse rotation I.e rotating the UIView in the opposite direction to the rotation. Not reliable, and looks weird, it's also vertigo inducing
Overriding the animation in the viewWillTransitionToSize method. Failed
And a bunch of other things that would be difficult to list here, but they all failed.
Question
Can this be done? if so, how?
I'm supporting iOS8+
Update This is how the views should layout/orient given #Casey's example:
I have faced with same problem and found example from Apple, which helps to prevent UIView from rotation: https://developer.apple.com/library/ios/qa/qa1890/_index.html
However, if UIView is not placed in the center of the screen, you should handle new position manually.
i think part of the reason this is so hard to answer is because in practice it doesn't really make sense.
say i make a view that uses autolayout to look like this in portrait and landscape:
if you wanted to prevent c from rotating like you are asking, what would you expect the final view to look like? would it be one of these 3 options?
without graphics of the portrait/landscape view you are trying to achieve and a description of the animation you are hoping for it'll be very hard to answer your question.
are you using NSLayoutConstraint, storyboard or frame based math to layout your views? any code you can provide would be great too
If you're wanting to have the same effect as the camera app, use size classes (see here and here).
If not, what is wrong with creating a UIWindow containing a view controller that doesn't rotate? The following code seems to work for me (where the UILabel represents the view you don't want to rotate).
class ViewController: UIViewController {
var staticWindow: UIWindow!
override func viewDidLoad() {
super.viewDidLoad()
showWindow()
}
func showWindow() {
let frame = CGRect(x: 10, y: 10, width: 100, height: 100)
let vc = MyViewController()
let label = UILabel(frame: frame)
label.text = "Hi there"
vc.view.addSubview(label)
staticWindow = UIWindow(frame: frame)
staticWindow.rootViewController = MyViewController()
staticWindow.windowLevel = UIWindowLevelAlert + 1;
staticWindow.makeKeyAndVisible()
staticWindow.rootViewController?.presentViewController(vc, animated: false, completion: nil)
}
}
class MyViewController: UIViewController {
override func shouldAutorotate() -> Bool {
return false
}
override func shouldAutomaticallyForwardRotationMethods() -> Bool {
return false
}
override func shouldAutomaticallyForwardAppearanceMethods() -> Bool {
return false
}
override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
return UIInterfaceOrientationMask.Portrait
}
}

How to get UIView's frame update while applying UIKit Dynamics' gravity to it?

I'm experiencing UIKit Dynamics right now and here is what I would like to do :
"Drop" multiple UIView instances from the top of the screen to the bottom using gravity => OK
Trigger an event when each view's distance from the bottom is like 100px. => NOT OK :(
Here is what I tried :
Add on observer on the "frame" property for each view => I don't know why but observeValueForKeyPath is never called (see code below)...
Add a transparent collision boundary to each view and listen to UICollisionBehaviorDelegate => This kinda works but it is not the behavior I want. Because of course the view is "stopped" by the boundary and I want it to go down
This is my test code :
var animator: UIDynamicAnimator!
var gravity: UIGravityBehavior!
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
self.animator = UIDynamicAnimator(referenceView: view)
self.gravity = UIGravityBehavior()
self.animator.addBehavior(gravity)
let timer = NSTimer.scheduledTimerWithTimeInterval(2.0, target: self, selector: Selector("createView"), userInfo: nil, repeats: true)
}
func createView() {
var newView = UIView(frame: CGRectMake((UIScreen.mainScreen().bounds.size.width/2) - 20, -40, 40, 40))
self.view.addSubview(newView)
self.gravity.addItem(newView)
newView.addObserver(self, forKeyPath: "frame", options: NSKeyValueObservingOptions.New, context: nil)
}
override func observeValueForKeyPath(keyPath: String, ofObject object: AnyObject, change: [NSObject : AnyObject], context: UnsafeMutablePointer<Void>) {
if keyPath == "frame" {
if let newView = object as? UIView {
println(newView.frame.origin.y)
if (newView.frame.origin.y > UIScreen.mainScreen().bounds.size.height - 100) {
println("Trigger event !")
}
}
}
}
With this code, the views will get down correctly but no event will be triggered when it the views are 100px far from the bottom
I would greatly appreciate any help
A way to do it is by adding an UICollisionBehavior, creating a view position 100px above the bottom and define an action which would be called when your items collide with this.
I can't create a sample for you now, but http://www.raywenderlich.com/50197/uikit-dynamics-tutorial will be a great help, look for a section called "Invisible boundaries and collisions". It's objective-C, though, but that shouldn't be a problem as this is pretty easy.
I've been trying to make this work too, really there should be a delegate callback from UIDynamicAnimator after it updates frames.
I've managed to solve it like this:
class FrameReportingView: UIView {
override var center: CGPoint {
didSet{
print("center: \(center)")
}
}
}
I'm using a UIPushBehavior and the center is set each frame. You can implement a delegate on FrameReportingView to create a callback after the center is changed.

Resources