Animate UIImageView appearing on screen line by line - ios

I'd like to display a UIImageView with some animation and i'm thinking i'd like for it to appear on the screen pixel by pixel moving left to right, line by line. A bit like a printer would print an image.
I haven't got a clue where to start with this.
I was thinking maybe overlay the UIImageView with another view that can use animation to become transparent, but how can I make it happen?

Well one idea is, we have one view on top of your image, covering it entirely. Let's call this view V. Move that view down by 1 point, so a line of your image is exposed. Then have another view on top of your image, covering it entirely again. Let's call this view H. Then move that view right by 1 point. Now one "pixel" (or rather, a 1x1 point grid) of your image is exposed.
We'll animate H to the right. When it reaches the end, we'll put it back where it started, move V and H down by 1 point, and repeat the process.
Here's something to get you started.
extension UIView {
/**
- Parameter seconds: the time for one line to reveal
*/
func scanReveal(seconds: Double) {
let colour = self.superview?.backgroundColor ?? UIColor.black
let v = UIView(frame: self.bounds)
v.backgroundColor = colour
self.addSubview(v)
let h = UIView(frame: self.bounds)
h.backgroundColor = colour
self.addSubview(h)
v.frame.origin.y += 1
// Animate h to the end.
// When it reaches the end, bring it back, move v and h down by 1 and repeat.
func move() {
UIView.animate(withDuration: seconds, animations: {
h.frame.origin.x = h.bounds.height
}) { _ in
h.frame.origin.x = 0
h.frame.origin.y += 1
v.frame.origin.y += 1
if v.frame.origin.y > self.bounds.height {
return
}
move()
}
}
move()
}
}

Related

Why does CGAffineTransform(scaleX:y:) cause my view to rotate as if I'd called CGAffineTransform(rotationAngle:)?

I took Paul Hudson's (Hacking with Swift) animation tutorial (His Project 15) and tried to extend it to see the effect of multiple animations layered on one another.
There are four distinct animations: scale, translate, rotate and color. Instead of a single button that cycles through all of them, as in his tutorial, I used four buttons to allow selection of each animation individually. I also modified his "undo" animation by reversing the original animation rather than using CGAffineTransform.identity, since the identity transform would undo all the animations to that point.
My problem is that when I click my c button, it does the appropriate scaling the first click but, rather than scale the penguin back to its original size on the second click, it rotates the view. Subsequent clicks of the scale button continue to rotate the view as if I were clicking the rotate button.
There are other anomalies, such as the first click of the move button moves the penguin appropriately but following that with a click of the rotate button, both rotates the penguin and moves it back to the original position. I'm not sure why it moves back but I accept that that might be my own ignorance of the animation system.
I've added print statements and put in breakpoints to debug the scale problem. Everything in the code seems to be working exactly as coded but the animations defy logic! Any help would be appreciated.
The complete program is relatively simple:
import UIKit
class ViewController: UIViewController {
var imageView: UIImageView!
var scaled = false
var moved = false
var rotated = false
var colored = false
#IBOutlet var scaleButton: UIButton!
#IBOutlet var moveButton: UIButton!
#IBOutlet var rotateButton: UIButton!
#IBOutlet var colorButton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
imageView = UIImageView(image: UIImage(named: "penguin"))
imageView.center = CGPoint(x: 200, y: 332)
view.addSubview(imageView)
}
#IBAction func tapped(_ sender: UIButton) {
sender.isHidden = true
var theAnimation: ()->Void
switch sender {
case scaleButton:
theAnimation = scaleIt
case moveButton:
theAnimation = moveIt
case rotateButton:
theAnimation = rotateIt
case colorButton:
theAnimation = colorIt
default:
theAnimation = { self.imageView.transform = .identity }
}
print("scaled = \(scaled), moved = \(moved), rotated = \(rotated), colored = \(colored)")
UIView.animate(withDuration: 1, delay: 0, options: [], animations: theAnimation) { finished in
sender.isHidden = false
}
}
func scaleIt() {
print("scaleIt()")
let newScale: CGFloat = self.scaled ? -1.5 : 1.5
self.imageView.transform = CGAffineTransform(scaleX: newScale, y: newScale)
self.scaled.toggle()
}
func moveIt() {
print("moveIt()")
let newX: CGFloat = self.moved ? 0 : -50
let newY: CGFloat = self.moved ? 0 : -150
self.imageView.transform = CGAffineTransform(translationX: newX, y: newY)
self.moved.toggle()
}
func rotateIt() {
print("rotateIt()")
let newAngle = self.rotated ? 0.0 : CGFloat.pi
self.imageView.transform = CGAffineTransform(rotationAngle: newAngle)
self.rotated.toggle()
}
func colorIt() {
print("colorIt()")
let newAlpha: CGFloat = self.colored ? 1 : 0.1
let newColor = self.colored ? UIColor.clear : UIColor.green
self.imageView.alpha = newAlpha
self.imageView.backgroundColor = newColor
self.colored.toggle()
}
}
By any combination of clicking the buttons, I can only arrive at 10 configurations of the Penguin (five with CGAffineTransforms and the same five modified by the color button).
I'll use these images to answer some of your questions. I've tried to label these images "Configuration1A, Configuration2A, ... Configuration1B", where the B configurations are identical to the A configurations but with the color button's effect. Perhaps the labels will show up in my post (I'm very new to posting on StackOverflow).
Here are the five configurations with the first one repeated using the color button:
I first tried repeated button pressings. For each series of pressing a given button, I first returned the penguin to its original position, size, angle and color. For these repeated button pressings, the behavior I observed was as follows:
Button Observed behavior
scale Penguin scales from Configuration1A to Configuration2A (as expected).
scale Penguin rotates by pi from Configuration2A to Configuration4A (WHAT???).
scale Penguin rotates by pi from Configuration4A to Configuration2A (WHAT???).
scale ... steps 2 and 3, above, repeat indefinitely (WHAT???).
move Penguin moves (-50, -150), Configuration1A to Configuration3A (as expected).
move Penguin moves (50, 150) Configuration3A to Configuration1A (as expected).
move ... behavior above repeats indefinitely (as expected).
rotate Penguin rotates by pi, Configuration1A to Configuration5A (as expected).
rotate Penguin rotates by pi, Configuration5A to Configuration1A (as expected).
rotate ... behavior above repeats indefinitely (as expected).
color Penguin's color changes, Configuration1A to Configuration1B (as expected).
color Penguin's color changes, Configuration1B to Configuration1A (as expected).
color ... behavior above repeats indefinitely (as expected).
The opposite of scale 1.5 (50% bigger) is not -1.5, it's 0.5. (50% of it's original size)
Actually, wouldn't you want it to alternate between a scale of 1.5 (50% bigger) and 1.0 (normal size?)
Assuming you do want to alternate between 50% bigger and 50% smaller, change your scaleIt function to:
func scaleIt() {
print("scaleIt()")
let newScale: CGFloat = self.scaled ? -1.5 : 0.5
self.imageView.transform = CGAffineTransform(scaleX: newScale, y: newScale)
self.scaled.toggle()
}
When you set the X or Y scale to a negative number, it inverts the coordinates in that dimension. Inverting in both dimensions will appear like a rotation.
As somebody else mentioned, the way you've written your functions, you won't be able to combine the different transforms. You might want to rewrite your code to apply the changes to the view's existing transform:
func scaleIt() {
print("scaleIt()")
// Edited to correct the math if you are applying a scale to an existing transform
let scaleAdjustment: CGFloat = self.scaled ? -1.5 : 0.66666
self.imageView.transform = self.imageView.transform.scaledBy(x: scaleAdjustment, y: scaleAdjustment)
self.scaled.toggle()
}
Edit:
Note that changes to transforms are not "commutative", which is a fancy way of saying that the order in which you apply them matters. Applying a shift, then a rotate, will give different results than applying a rotate, then a shift, for example.
Edit #2:
Another thing:
The way you've written your functions, they will set the view to its current state, and then invert that current state for next time (you toggle scaled after deciding what to do.)
That means that for function like moveIt() nothing will happen the first time you tap the button. Rewrite that function like this:
func moveIt() {
print("I like to moveIt moveIt!")
self.moved.toggle() //Toggle the Bool first
let newX: CGFloat = self.moved ? 0 : -50
let newY: CGFloat = self.moved ? 0 : -150
self.imageView.transform = self.imageView.transform.translatedBy(x: newX, y: newY)
}

iOS: Programmatically move cursor up, down, left and right in UITextView

I use the following code to move the cursor position to 5 characters from the beginning of a UITextField:
txtView.selectedRange = NSMakeRange(5, 0);
Now, if I have a cursor at an arbitrary position as shown in the image below, how can I move the cursor up, down, left and right?
Left and right should be more or less easy. I guess the tricky part is top and bottom. I would try the following.
You can use caretRect method to find the current "frame" of the cursor:
if let cursorPosition = answerTextView.selectedTextRange?.start {
let caretPositionRect = answerTextView.caretRect(for: cursorPosition)
}
Then to go up or down, I would use that frame to calculate estimated position in UITextView coordinates using characterRange(at:) (or maybe closestPosition(to:)), e.g. for up:
let yMiddle = caretPositionRect.origin.y + (caretPositionRect.height / 2)
let lineHeight = answerTextView.font?.lineHeight ?? caretPositionRect.height // default to caretPositionRect.height
// x does not change
let estimatedUpPoint = CGPoint(x: caretPositionRect.origin.x, y: yMiddle - lineHeight)
if let newSelection = answerTextView.characterRange(at: estimatedUpPoint) {
// we have a new Range, lets just make sure it is not a selection
newSelection.end = newSelection.start
// and set it
answerTextView.selectedTextRange = newSelection
} else {
// I guess this happens if we go outside of the textView
}
I haven't really done it before, so take this just as a general direction that should work for you.
Documentation to the methods used is here.

Why are my UI elements not resetting correctly after being animated/scaled off screen?

So I'll give as much information about this project as I can right up front. Here is an image of a section of the storyboard that is relevant to the issue:
And here is the flow of the code:
1) A user plays the game. This scrambles up the emoji that are displayed and will eventually hide all of the emoji on the right side.
2) When someone wins the game, it calls
performSegue(withIdentifier: "ShowWinScreenSegue", sender: self)
Which will perform the segue the red arrow is pointing to. This segue is a modal segue, over current content, cross dissolve.
3) Stuff goes on here, and then I try to get back to the game screen so the user can play another game. Here is my current code for that
// self.delegate is the GameController that called the segue
// it's set somewhere else in the code so I can call these reset functions
GameController.gs = GameState()
guard let d = self.delegate else {
return
}
d.resetGameToMatchState()
dismiss(animated: true, completion: {
print("Modal dismiss completed")
GameController.gs = GameState()
self.delegate?.resetGameToMatchState()
})
So here's where the issue is. You can see that I have to call delegate?.resetGameToMatchState() twice for anything to actually happen. If I remove the top one, nothing happens when I call the second one and vice-versa. What makes this so annoying is that the user will see a weird jump where all the ui goes from the old state to the new state because it's updating so late and spastically.
What I've tried so far
So this whole issue has made me really confused on how the UI system works.
My first thought was that maybe the function is trying to update the UI in a thread that's executing too early for the UI thread. So I put the whole body of resetGameToMatchState in a DispatchQueue.main.async call. This didn't do anything.
Then I thought that it was working before because when the WinScreenSegue was being dismissed before (when it was a "show" segue) it was calling GameController's ViewDidAppear. I tried manually calling this function in the dismiss callback, but that didn't work either and feels really hacky.
And now I'm stuck :( Any help would be totally appreciated. Even if it's just a little info that can clear up how the UI system works.
Here is my resetGameToMatchState():
//reset all emoji labels
func resetGameToMatchState() {
DispatchQueue.main.async {
let tier = GameController.gs.tier
var i = 0
for emoji in self.currentEmojiLabels! {
emoji.frame = self.currentEmojiLabelInitFrames[i]
emoji.isHidden = false
emoji.transform = CGAffineTransform(scaleX: 1, y: 1);
i+=1
}
i=0
for emoji in self.goalEmojiLabels! {
emoji.frame = self.goalEmojiLabelInitFrames[i]
emoji.isHidden = false
emoji.transform = CGAffineTransform(scaleX: 1, y: 1);
i+=1
}
//match state
for i in 1...4 {
if GameController.gs.currentEmojis[i] == GameController.gs.goalEmojis[i] {
self.currentEmojiLabels?.findByTag(tag: i)?.isHidden = true
}
}
//reset highlight
let f = self.highlightBarInitFrame
let currentLabel = self.goalEmojiLabels?.findByTag(tag: tier)
let newSize = CGRect(x: f.origin.x, y: (currentLabel?.frame.origin.y)!, width: f.width, height: (currentLabel?.frame.height)! )
self.highlightBarImageView.frame = newSize
//update taps
self.updateTapUI()
//update goal and current emojis to show what the current goal/current selected emoji is
self.updateGoalEmojiLabels()
self.updateCurrentEmojiLabels()
}
}
UPDATE
So I just found this out. The only thing that isn't working when I try to reset the UI is resetting the right side emoji to their original positions. What I do is at the start of the app (in viewDidLoad) I run this:
for emoji in currentEmojiLabels! {
currentEmojiLabelInitFrames.append(emoji.frame)
}
This saves their original positions to be used later. I do this because I animate them to the side of the screen before hiding them.
Now when I want to reset their positions, I do this:
var i = 0
for emoji in self.currentEmojiLabels! {
emoji.frame = self.currentEmojiLabelInitFrames[i]
emoji.isHidden = false
emoji.transform = CGAffineTransform(scaleX: 1, y: 1);
i+=1
}
this should set them all to their original frame and scale, but it doesn't set the position correctly. It DOES reset the scale though. What's weird is that I can see a tiny bit of one of the emoji off to the left of the screen and when they animate, they animate from far off on the left. I'm trying to think of why the frames are so off...
UPDATE 2
So I tried changing the frame reset code to this:
emoji.frame = CGRect(x: 25, y: 25, width: 25, height: 25)
Which I thought should reset them correctly to the top left, but it STILL shoves them off to the left. This should prove that the currentEmojiLabelInitFrames are not the issue and that it has something to do with when I'm setting them. Maybe the constraints are getting reset or messed up?
Your first screen, GameController, should receive a viewWillAppear callback from UIKit when the modal WinScreenController is being dismissed.
So its resetGameToMatchState function could simply set a property to true, then your existing resetGameToMatchState could move into viewWillAppear, checking first if the property is being set.
var resetNeeded: Bool = false
func resetGameToMatchState() {
resetNeeded = true
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// reset code here
}
TLDR; Reset an element's scale BEFORE resetting it's frame or else it will scale/position incorrectly
Finally figured this out. Here's a bit more background. When an emoji is animated off the screen, this is called:
UIView.animate(withDuration: 1.5, animations: {
let newFrame = self.profileButton.frame
prevLabel?.frame = newFrame
prevLabel?.transform = CGAffineTransform(scaleX: 0.1, y: 0.1);
}) { (finished) in
prevLabel?.isHidden = true
}
So this sets the frame to the top left of the screen and then scales it down. What I didn't realize is that when I want to reset the element, I NEED to set the scale back to normal before setting the frame. Here is the new reset code:
for emoji in self.currentEmojiLabels! {
emoji.transform = CGAffineTransform(scaleX: 1, y: 1) //this needs to be first
emoji.frame = self.currentEmojiLabelInitFrames[i] //this needs to be after the scale
emoji.isHidden = false
i+=1
}

making a UIimage move every 1 second

I have a UIimage which I want to move according to X and Y given by certain equations ( physics projectile time equations) and I want it to move every 1 second so that it would appear to the user as if it's actually moving not just disappearing and reappearing at the last position, plus the positions given off are wrong I think. I would appreciate any help for either or both problems.
So far I tried this:
The movement function:
func moveCannonBall(toX:Int , toY:Int ){
var frm: CGRect = ballimageview.frame
frm.origin.x = frm.origin.x + CGFloat(toX)
frm.origin.y = frm.origin.y - CGFloat(toY)
ballimageview.frame = frm
}
On button click it's supposed to take the user's inputs (gravity, angle, and the initial speed)
#IBAction func getAlpha( sender:AnyObject){
gravity = Float(g[pickerView.selectedRow(inComponent: 0)])
xMax = ((V0*V0)*sin(2.0*angle))/gravity
It's supposed to stop every 1 second but only the calculations pause every 1 second and the UIimage just disappears and reappears just
while Int(toX) < Int(xMax) {
sleep(1)
t = t + 1
toX = V0*cos(angle)*t
toY = -0.5*gravity*t*t + V0*sin(angle)*t
moveCannonBall(toX: Int(toX), toY: Int(toY))
}
}
Any help is appreciated.
As Losiowaty said UIView.animationWithDuration is the way to go. Check out this answer to see how the syntax is: https://stackoverflow.com/a/30992214/5257729

CALayers: A) Can I draw directly on them and B) How to make a width adjustment on a superlayer affect a sublayer

So my goal is to make a sort of sliding door animation in response to a swipe gesture. You can see a GIF of my current animation here (ignore the fact that the gesture behaves opposite to what you'd expect).
Here's how I'm currently accomplishing this: I have a subclass of UIView I'm calling DoorView. DoorView has three CALayers: the base superlayer that comes with every UIView; a sublayer called doorLayer which is the white rectangle that slides; and another sublayer called frameLayer which is the "doorframe" (the black border around doorLayer). The doorLayer and the frameLayer have their own separate animations that are triggered in sequence.
Here's what I need to add to DoorView: a simple rectangle that represents a door handle. At the moment I don't plan to give the door handle its own animation. Instead, I want it to simply be "attached" to the doorLayer so that it animates along with any animations applied to doorLayer.
This is where my first question comes in: I know that I can add another layer (let's call it handleLayer) and add it as a sublayer to doorLayer. But is there a way to simply "draw" a small rectangle on doorLayer without needing an extra layer? And if so, is this preferable for any reason?
Now for my second question: so at the moment I am in fact using a separate layer called handleLayer which is added as a sublayer to doorLayer. You can see a GIF of the animation with the handleLayer here.
And here is the animation being applied to doorLayer:
UIView.animateWithDuration(1.0, animations: { () -> Void in
self.doorLayer.frame.origin.x = self.doorLayer.frame.maxX
self.doorLayer.frame.size.width = 0
}
This animation shifts the origin of doorLayer's frame to the door's right border while decrementing its width, resulting in the the appearance of a door sliding to the right and disappearing as it does so.
As you can see in the above GIF, the origin shift of doorLayer is applied to its handleLayer sublayer, as desired. But the width adjustment does not carry over to the handleLayer. And this is good, because I don't want the handle to be getting narrower at the same rate as the doorLayer.
Instead what is desired is that the handleLayer moves with the doorLayer, but retains its size. But when the doorLayer disappears into the right side of the doorframe, the handle disappears with it (as it would look with a normal door). Any clue what the best way to accomplish this is?
Currently in my doorLayer's animation, I added this line:
if self.doorLayer.frame.size.width <= self.handleLayer.frame.size.width {
self.handleLayer.frame.size.width = 0
}
But that results in this, which isn't quite right.
Thanks for any help!
From a high level, you would need to
Make your sliding layer a child of your outline layer
Make your outline layer masks its bounds
Animate your sliding layer's transform using a x translation
On completion of the animation, animate your outline layer's transform using a scale translation
Reverse the animations to close the door again
Your doorknob layer is fine as is and no need to animate it separately.
I took a shot at it for fun and here's what I came up with. I didn't use a swipe gesture, but it could just as easily by added. I trigger the animation with a tap on the view. Tap again to toggle back.
func didTapView(gesture:UITapGestureRecognizer) {
// Create a couple of closures to perform the animations. Each
// closure takes a completion block as a parameter. This will
// be used as the completion block for the Core Animation transaction's
// completion block.
let slideAnimation = {
(completion:(() -> ())?) in
CATransaction.begin()
CATransaction.setCompletionBlock(completion)
CATransaction.setAnimationDuration(1.0)
if CATransform3DIsIdentity(self.slideLayer.transform) {
self.slideLayer.transform = CATransform3DMakeTranslation(220.0, 0.0, 0.0)
} else {
self.slideLayer.transform = CATransform3DIdentity
}
CATransaction.commit()
}
let scaleAnimation = {
(completion:(() -> ())?) in
CATransaction.begin()
CATransaction.setCompletionBlock(completion)
CATransaction.setAnimationDuration(1.0)
if CATransform3DIsIdentity(self.baseLayer.transform) {
self.baseLayer.transform = CATransform3DMakeScale(2.0, 2.0, 2.0)
} else {
self.baseLayer.transform = CATransform3DIdentity
}
CATransaction.commit()
}
// Check to see if the slide layer's transform is the identity transform
// which would mean that the door is currently closed.
if CATransform3DIsIdentity(self.slideLayer.transform) {
// If the door is closed, perform the slide animation first
slideAnimation( {
// And when it completes, perform the scale animation
scaleAnimation(nil) // Pass nil here since we're done animating
} )
} else {
// Otherwise the door is open, so perform the scale (down)
// animation first
scaleAnimation( {
// And when it completes, perform the slide animation
slideAnimation(nil) // Pass nil here since we're done animating
})
}
}
Here's how the layers are setup initially:
func addLayers() {
baseLayer = CALayer()
baseLayer.borderWidth = 10.0
baseLayer.bounds = CGRect(x: 0.0, y: 0.0, width: 220, height: 500.0)
baseLayer.masksToBounds = true
baseLayer.position = self.view.center
slideLayer = CALayer()
slideLayer.bounds = baseLayer.bounds
slideLayer.backgroundColor = UIColor.whiteColor().CGColor
slideLayer.position = CGPoint(x: baseLayer.bounds.size.width / 2.0, y: baseLayer.bounds.size.height / 2.0)
let knobLayer = CALayer()
knobLayer.bounds = CGRect(x: 0.0, y: 0.0, width: 20.0, height: 20.0)
knobLayer.cornerRadius = 10.0 // Corner radius with half the size of the width and height make it round
knobLayer.backgroundColor = UIColor.blueColor().CGColor
knobLayer.position = CGPoint(x: 30.0, y: slideLayer.bounds.size.height / 2.0)
slideLayer.addSublayer(knobLayer)
baseLayer.addSublayer(slideLayer)
self.view.layer.addSublayer(baseLayer)
}
And here's what the animation looks like:
You can see a full Xcode project here: https://github.com/perlmunger/Door

Resources