Hi there,
I’ve been trying to solve a problem of how to create two progressViews in my viewController for the past week with little to no success. Please see the diagram I have attached as an idea as to the question I am asking.
I am creating a game with a timer which elapses with each question. As the time elapses I would like a UIProgressView to cascade down the entire screen transitioning from blue to white as it goes (Please see number 1 in the diagram, this indicates the white trackTintColor).
Number 2 in the diagram (the cyan part) represents the progressTintColor. The diagram should hopefully be clear in that I am hoping to customise the progressView so that it tracks downwards. Which is one of the main issues at the moment. (I can only seem to fins walkthroughs with small customisable Progressviews which move sideways not up and down)
The hardest part of what I am trying to achieve is (number 3 in the diagram), customisizing a UIImageView, so that it slowly drains downward with the inverse colours to the background (so the dog will be white with cyan flooding downwards to coincide with the progressView elapsing behind it).
These are the options I have tried to solve this issue, thus far, to no avail.
I have tried using a progressView for the backgroundColor, but I cannot find any examples anywhere of anyone else doing this (downwards)so I’m not sure if it’s even possible?
For the image I have tried drawing a CAShape layer but it appears the dog is too difficult a shape for a novice like myself to draw effectively. And upon realising that I would not be able to set a layer of a different colour behind the dog to move downwards as the screen will also be changing color I abandoned all hope of using this option.
I have tried a UIView transition for the dog image, however, the only option I could find that was anywhere close was the transitionCrossDissolve which did not give the downward effect I was hoping for, but instead just faded from a white dog to a cyan dog which was not appropriate. Should I somehow be using progressImage? If so, is there anywhere I can find help with the syntax for that? I can’t seem to find any anywhere.
I currently have 55 images in my assets folder, each with slightly more cyan in than the last, progressively moving downwards (as it animates through an array of the images). Although this works, it is not exactly seamless and does look a little like the user is waiting for an image to load on dial up.
If anyone has any ideas or could spare the time to walk me through how I would go about doing this I would very much appreciate it. I am very much still a beginner so the more detail the better! Oh yes to make matters more difficult, so far I have managed to do the app programatically, so an answers in this form would be great.
Thanks in advance!
I hope you have done number 1 and number 2, perfectly. I have tried for number 3.
I have tried with two UIView. Its working fine. I thought, it will give some idea to achieve yours.
I have two images.
With the help of TIMER , I tried sample for this ProgressView .
Intially, cyanDogView height should be Zero. Once Timer Starts, height should increased by 2px. Once cyanDogView's height should be greater than BlackDogView, then Timer Stops.
Coding
#IBOutlet weak var blackDogView: UIView!
#IBOutlet weak var blackDogImgVw: UIImageView!
#IBOutlet weak var cyanDogView: UIView!
#IBOutlet weak var cyanDogImgVw: UIImageView!
var getHeight : CGFloat = 0.0
var progressTime = Timer()
override func viewDidAppear(_ animated: Bool) {
cyanDogView.frame.size.height = 0
getHeight = blackDogView.frame.height
}
#IBAction func startAnimateButAcn(_ sender: UIButton) {
progressTime = Timer.scheduledTimer(timeInterval: 0.2, target: self, selector: #selector(self.update), userInfo: nil, repeats: true)
}
#objc func update() {
cyanDogView.frame.size.height = cyanDogView.frame.size.height + 2
if cyanDogView.frame.size.height >= getHeight
{
progressTime.invalidate()
cyanDogView.frame.size.height = 0
}
}
Story Board
Output
I'm going to give you some Frankenstein answers here, Part Obj-C Part Swift. I hope it helps.
First, You could create a bezier path of the image mask you're using as a template:
- (UIImage *)cerateImageFromImage:(UIImage *)image
withMaskImage:(UIImage *)mask {
CGImageRef imageRef = image.CGImage;
CGImageRef maskRef = mask.CGImage;
CGImageRef imageMask = CGImageMaskCreate(CGImageGetWidth(maskRef),
CGImageGetHeight(maskRef),
CGImageGetBitsPerComponent(maskRef),
CGImageGetBitsPerPixel(maskRef),
CGImageGetBytesPerRow(maskRef),
CGImageGetDataProvider(maskRef),
NULL,
YES);
CGImageRef maskedReference = CGImageCreateWithMask(imageRef, imageMask);
CGImageRelease(imageMask);
UIImage *maskedImage = [UIImage imageWithCGImage:maskedReference];
CGImageRelease(maskedReference);
return maskedImage;
}
UIImage *image = [UIImage imageNamed:#"Photo.png"];
UIImage *mask = [UIImage imageNamed:#"Mask.png"];
self.imageView.image = [self cerateImageFromImage:image
withMaskImage:mask];
Credit to Keenle
Next you can create a custom progress view based on a path :
func drawProgressLayer(){
let bezierPath = UIBezierPath(roundedRect: viewProg.bounds, cornerRadius: viewCornerRadius)
bezierPath.closePath()
borderLayer.path = bezierPath.CGPath
borderLayer.fillColor = UIColor.blackColor().CGColor
borderLayer.strokeEnd = 0
viewProg.layer.addSublayer(borderLayer)
}
//Make sure the value that you want in the function `rectProgress` that is going to define
//the width of your progress bar must be in the range of
// 0 <--> viewProg.bounds.width - 10 , reason why to keep the layer inside the view with some border left spare.
//if you are receiving your progress values in 0.00 -- 1.00 range , just multiply your progress values to viewProg.bounds.width - 10 and send them as *incremented:* parameter in this func
func rectProgress(incremented : CGFloat){
print(incremented)
if incremented <= viewProg.bounds.width - 10{
progressLayer.removeFromSuperlayer()
let bezierPathProg = UIBezierPath(roundedRect: CGRectMake(5, 5, incremented , viewProg.bounds.height - 10) , cornerRadius: viewCornerRadius)
bezierPathProg.closePath()
progressLayer.path = bezierPathProg.CGPath
progressLayer.fillColor = UIColor.whiteColor().CGColor
borderLayer.addSublayer(progressLayer)
}
}
Credit to Dravidian
Please click the blue links and explore their answers in full to get a grasp of what is possible.
Ok so using McDonal_11's answer I have managed to get the progressView working however, I am still experiencing some problems. I cannot add anything on top of the progressView, it just blanket covers everything else underneath, and before the dog begins its animation into a cyan dog there is a brief flash of the entire cyan dog image.
Code below
private let contentView = UIView(frame: .zero)
private let backgroundImageView = UIImageView(frame: .zero)
private let progressView = ProgressView(frame: .zero)
private let clearViewOverProgress = UIView(frame: .zero)
private let blackDogView = UIView(frame: .zero)
private let blackDogViewImage = UIImageView(frame: .zero)
private let cyanDogView = UIView(frame: .zero)
private let cyanDogViewImage = UIImageView()
var timer = Timer()
var startHeight : CGFloat = 0.0
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.progressView.setProgress(10.0, animated: true)
startHeight = cyanDogViewImage.frame.height
self.cyanDogViewImage.frame.size.height = 0
self.timer = Timer.scheduledTimer(timeInterval: 0.01, target: self, selector: #selector(self.updateImage), userInfo: nil, repeats: true)
}
override func viewDidLoad() {
super.viewDidLoad()
setupViews()
}
func setupViews() {
self.view.addSubview(backgroundImageView)
self.backgroundImageView.addSubview(progressView)
self.progressView.addSubview(clearViewOverProgress)
self.clearViewOverProgress.addSubview(blackDogView)
self.blackDogView.addSubview(blackDogViewImage)
self.blackDogViewImage.addSubview(cyanDogView)
self.cyanDogView.addSubview(cyanDogViewImage)
// Setting up constraints of both labels (has been omitted for brevity)
self.blackDogViewImage.image = UIImage(named: “BlackDogImage”)
self.cyanDogViewImage.contentMode = UIViewContentMode.top
self.cyanDogViewImage.clipsToBounds = true
self.cyanDogViewImage.image = UIImage(named: “CyanDogImage”)
}
func updateImage() {
cyanDogViewImage.frame.size.height =
cyanDogViewImage.frame.size.height + 0.07
if cyanDogViewImage.frame.size.height >= blackDogViewImage.frame.size.height
{
timer.invalidate()
cyanDogViewImage.frame.size.height = blackDogViewImage.frame.size.height
}
}
func outOfTime() {
timer.invalidate()
}
Related
I'm working on a swift camera app and trying to solve the following problem.
My camera app, which allows taking a video, can change the camera focus and exposure when a user taps somewhere on the screen. Like the default iPhone camera app, I displayed the yellow square to show where the user tapped. I can see exactly where the user tapped unless triggering the timer function and updating the total video length on the screen.
Here is the image of the camera app.
As you can see there is a yellow square and that was the point I tapped on the screen.
However, when the timer counts up (in this case, 19 sec to 20sec) and updates the text of total video length, the yellow square moves back to the center of the screen. Since I put the yellow square at the center of the screen on my storyboard, I guess when the timer counts up and updates the label text, it also refreshing the yellow square, UIView, so displaying at the center of the screen (probably?).
So if I'm correct, how can I display the yellow square at the user tapped location regardless of the timer function, which updates UIlabel for every second?
Here is the code.
final class ViewController: UIViewController {
private var pointOfInterestHalfCompletedWorkItem: DispatchWorkItem?
#IBOutlet weak var pointOfInterestView: UIView!
#IBOutlet weak var time: UILabel!
var counter = 0
var timer = Timer()
override func viewDidLayoutSubviews() {
pointOfInterestView.layer.borderWidth = 1
pointOfInterestView.layer.borderColor = UIColor.systemYellow.cgColor
}
#objc func startTimer() {
timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(timerAction), userInfo: nil, repeats: true)
}
#objc func timerAction() {
counter += 1
secondsToHoursMinutesSeconds(seconds: counter)
}
func secondsToHoursMinutesSeconds (seconds: Int) {
// format seconds
time.text = "\(strHour):\(strMin):\(strSec)"
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let focusPoint = touches.first!.location(in: lfView)
showPointOfInterestViewAtPoint(point: focusPoint)
}
func showPointOfInterestViewAtPoint(point: CGPoint) {
print("point is here \(point)")
pointOfInterestHalfCompletedWorkItem = nil
pointOfInterestComplatedWorkItem = nil
pointOfInterestView.center = point
pointOfInterestView.transform = CGAffineTransform(scaleX: 1.5, y: 1.5)
let animation = UIViewPropertyAnimator(duration: 0.3, curve: .easeInOut) {
self.pointOfInterestView.transform = .identity
self.pointOfInterestView.alpha = 1
}
animation.startAnimation()
let pointOfInterestHalfCompletedWorkItem = DispatchWorkItem { [weak self] in
guard let self = self else { return }
let animation = UIViewPropertyAnimator(duration: 0.3, curve: .easeInOut) {
self.pointOfInterestView.alpha = 0.5
}
animation.startAnimation()
}
DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: pointOfInterestHalfCompletedWorkItem)
self.pointOfInterestHalfCompletedWorkItem = pointOfInterestHalfCompletedWorkItem
}
}
Since I thought it's a threading issue, I tried to change the label text & to show the yellow square in the main thread by writing DispatchQueue.main.asyncAfter, but it didn't work. Also, I was not sure if it becomes serial queue or concurrent queue if I perform both
loop the timer function and constantly updating label text
detect UI touch event and show yellow square
Since UI updates are performed in the main thread, I guess I need to figure out a way to share the main thread for the timer function and user touch event...
If someone knows a clue to solve this problem, please let me know.
It isn't a threading issue. It is an auto layout issue.
Presumably you have positioned the yellow square view in your storyboard using constraints.
You are then modifying the yellow square's frame directly by modifying the center property; this has no effect on the constraints that are applied to the view. As soon as the next auto layout pass runs (triggered by the text changing, for example) the constraints are reapplied and the yellow square jumps back to where your constraints say it should be.
You have a couple of options;
Compute the destination point offset from the center of the view and then apply those offsets to the constant property of your two centering constraints
Add the yellow view programatically and position it by setting its frame directly. You can then adjust the frame by modifying center as you do now.
So here is where I am at now. I still haven't been able to get it to work and I've tried a lot of variations. Here is my original code for a single animation.
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var imageView: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
var images = [UIImage]()
var images2 = [UIImage]()
images += [#imageLiteral(resourceName: "circle1"), #imageLiteral(resourceName: "circle2"), #imageLiteral(resourceName: "circle3"), #imageLiteral(resourceName: "circle4")]
images2 += [#imageLiteral(resourceName: "circle4"), #imageLiteral(resourceName: "circle3"), #imageLiteral(resourceName: "circle2"), #imageLiteral(resourceName: "circle1")]
imageView.animationImages = images
imageView.animationDuration = 4.0
imageView.animationRepeatCount = 2
imageView.startAnimating()
}
}
I am able to animate the first images array with the properties I've set for duration and count. However, I just do not understand how I would write this to add a second set of images to animate immediately after. I know I need to use this somehow:
UIView
.animate(withDuration: 4.0,
animations: {
//animation 1
},
completion: { _ in
UIView.animate(withDuration: 6.0,
animations: {
//animation 2
})
})
Can someone show me how I would write this given my current images arrays using the same imageView object to display both animations? I want my first animation (images) to have a duration of 4 seconds and my second animation (images2) to have a duration of 6 second immediately following the first animation. I can't figure out what I need inside the animations parameter.
i think the error is that you mix here 2 things:
UIView.animate -> animate controls and properties
UIImageView.startAnimating -> start a loop of images in an UIImageView
but they don't do the same they are very independent. but UIView animation is normally for an other use case. only one thing is that they maybe have the same duration like your UIImageView animation, but you don't set the duration of your UIImageView. maybe when you set the animation duration of your image view to the duration of UIView animation then it is done on the same time range.
myImageView.animationDuration = 2;
and for the second loop
myImageView.animationDuration = 4;
Other Solutions
the thing is you need to know when an image loop ist completed. but there is no event for this (i did not found any)
there are some solutions on StackOverflow for this:
1 performSelector afterDelay
Set a timer to fire after the duration is done. for this you need also to add an animation duration to your image view:
myImageView.animationDuration = 0.7;
solution from here: how to do imageView.startAnimating() with completion in swift?:
When the button is pressed you can call a function with a delay
self.performSelector("afterAnimation", withObject: nil, afterDelay: imageView1.animationDuration)
Then stop the animation and add the last image of imageArray to the imageView in the afterAnimation function
func afterAnimation() {
imageView1.stopAnimating()
imageView1.image = imageArray.last
}
2 Timer
this is similar to performSelector afterDelay but with NSTimer.
you find a description here UIImageView startAnimating: How do you get it to stop on the last image in the series? :
1) Set the animation images property and start the animation (as in
your code in the question)
2) Schedule an NSTimer to go off in 'animationDuration' seconds time
3) When the timer goes off, [...]
add to point 3: then start the next animation
3 CATransaction
solution from here: Cocoa animationImages finish detection (you need to convert it to swift 3 syntax but it can be a starting point
Very old question, but I needed a fix now, this works for me:
[CATransaction begin];
[CATransaction setCompletionBlock:^{
DLog(#"Animation did finish.");
}];
UIImageView *imageView = [[UIImageView alloc] initWithFrame:self.window.bounds];
imageView.animationDuration = 0.3 * 4.0;
imageView.animationImages = #[[UIImage AHZImageNamed:#"Default2"],
[UIImage AHZImageNamed:#"Default3"],
[UIImage AHZImageNamed:#"Default4"],
[UIImage AHZImageNamed:#"Default5"]];
imageView.animationRepeatCount = 1;
[self.window addSubview:imageView];
[imageView startAnimating];
[CATransaction commit];
4 Offtopic: Manual do the Image Animation
thats a little offtopic, because here they do the image animation manually. for your use case you just change the logic which image from index is visible. count forward until last image, then backwards until first image. and then stop the loop. not nice solution but in this solution is added a image transition animation:
Solution from here: Adding Image Transition Animation in Swift
class ViewController: UIViewController {
#IBOutlet weak var imageView: UIImageView!
let images = [
UIImage(named: "brooklyn-bridge.jpg")!,
UIImage(named: "grand-central-terminal.jpg")!,
UIImage(named: "new-york-city.jpg"),
UIImage(named: "one-world-trade-center.jpg")!,
UIImage(named: "rain.jpg")!,
UIImage(named: "wall-street.jpg")!]
var index = 0
let animationDuration: NSTimeInterval = 0.25
let switchingInterval: NSTimeInterval = 3
override func viewDidLoad() {
super.viewDidLoad()
imageView.image = images[index++]
animateImageView()
}
func animateImageView() {
CATransaction.begin()
CATransaction.setAnimationDuration(animationDuration)
CATransaction.setCompletionBlock {
let delay = dispatch_time(DISPATCH_TIME_NOW, Int64(self.switchingInterval * NSTimeInterval(NSEC_PER_SEC)))
dispatch_after(delay, dispatch_get_main_queue()) {
self.animateImageView()
}
}
let transition = CATransition()
transition.type = kCATransitionFade
/*
transition.type = kCATransitionPush
transition.subtype = kCATransitionFromRight
*/
imageView.layer.addAnimation(transition, forKey: kCATransition)
imageView.image = images[index]
CATransaction.commit()
index = index < images.count - 1 ? index + 1 : 0
}
}
Implement it as a custom image view would be better.
I know you said that this didn't work for you, but if you just want to execute animations right after another, I believe this really is the best option.
You should be able to do something like this:
let imageView = UIImageView()
UIView.animate(withDuration: 0.5, animations: {
//animation 1
}, completion: { (value: Bool) in
UIView.animate(withDuration: 1.0, animations: {
//animation 2
})
})
This does one animation directly after another, which a longer duration. Of course you still need to define what the animations are, but I hope this helps.
I'm not sure if this is the best way to do this, but I've found a solution to my problem. Please give feedback if there is a better way. I was able to accomplish multiple animations in a single UIImageView consecutively by using: self.perform(#selector(ViewController.afterAnimation), with: nil, afterDelay: 4.0)
This calls the afterAnimation function:
func afterAnimation() {
imageView.stopAnimating()
imageView.animationImages = images2
imageView.animationDuration = 6.0
imageView.animationRepeatCount = 1
imageView.startAnimating()
}
I needed animations to each last a specific amount of time and to be able to chain many of them together. It solves my problem.
I have a custom UIView that draws its contents using Core Graphics calls. All working well, but now I want to animate a change in value that affects the display. I have a custom property to achieve this in my custom UView:
var _anime: CGFloat = 0
var anime: CGFloat {
set {
_anime = newValue
for(gauge) in gauges {
gauge.animate(newValue)
}
setNeedsDisplay()
}
get {
return _anime
}
}
And I have started an animation from the ViewController:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.emaxView.anime = 0.5
UIView.animate(withDuration: 4) {
DDLogDebug("in animations")
self.emaxView.anime = 1.0
}
}
This doesn't work - the animated value does change from 0.5 to 1.0 but it does so instantly. There are two calls to the anime setter, once with value 0.5 then immediately a call with 1.0. If I change the property I'm animating to a standard UIView property, e.g. alpha, it works correctly.
I'm coming from an Android background, so this whole iOS animation framework looks suspiciously like black magic to me. Is there any way of animating a property other than predefined UIView properties?
Below is what the animated view is supposed to look like - it gets a new value about every 1/2 second and I want the pointer to move smoothly over that time from the previous value to the next. The code to update it is:
open func animate(_ progress: CGFloat) {
//DDLogDebug("in animate: progress \(progress)")
if(dataValid) {
currentValue = targetValue * progress + initialValue * (1 - progress)
}
}
And calling draw() after it's updated will make it redraw with the new pointer position, interpolating between initialValue and targetValue
Short answer: use CADisplayLink to get called every n frames. Sample code:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let displayLink = CADisplayLink(target: self, selector: #selector(animationDidUpdate))
displayLink.preferredFramesPerSecond = 50
displayLink.add(to: .main, forMode: .defaultRunLoopMode)
updateValues()
}
var animationComplete = false
var lastUpdateTime = CACurrentMediaTime()
func updateValues() {
self.emaxView.animate(0);
lastUpdateTime = CACurrentMediaTime()
animationComplete = false
}
func animationDidUpdate(displayLink: CADisplayLink) {
if(!animationComplete) {
let now = CACurrentMediaTime()
let interval = (CACurrentMediaTime() - lastUpdateTime)/animationDuration
self.emaxView.animate(min(CGFloat(interval), 1))
animationComplete = interval >= 1.0
}
}
}
The code could be refined and generalised but it's doing the job I needed.
You will need to call layoufIfNeeded() instead of setNeedsDisplay() if you modify any auto layout constraints in your gauge.animate(newValue) function.
https://stackoverflow.com/a/12664093/255549
If that is drawn entirely with CoreGraphics there is a pretty simple way to animate this if you want to do a little math. Fortunately you have a scale there that tells you the number of radians exactly to rotate, so the math is minimal and no trigonometry is involved. The advantage of this is you won't have to redraw the entire background, or even the pointer. It can be a bit tricky to get angles and stuff right, I can help out if the following doesn't work.
Draw the background of the view normally in draw(in rect). The pointer you should put into a CALayer. You can pretty much just move the draw code for the pointer, including the centre dark gray circle into a separate method that returns a UIImage. The layer will be sized to the frame of the view (in layout subviews), and the anchor point has to be set to (0.5, 0.5), which is actually the default so you should be ok leaving that line out. Then your animate method just changes the layer's transform to rotate according to what you need. Here's how I would do it. I'm going to change the method and variable names because anime and animate were just a bit too obscure.
Because layer properties implicitly animate with a duration of 0.25 you might be able to get away without even calling an animation method. It's been a while since I've worked with CoreAnimation, so test it out obviously.
The advantage here is that you just set the RPM of the dial to what you want, and it will rotate over to that speed. And no one will read your code and be like WTF is _anime! :) I have included the init methods to remind you to change the contents scale of the layer (or it renders in low quality), obviously you may have other things in your init.
class SpeedDial: UIView {
var pointer: CALayer!
var pointerView: UIView!
var rpm: CGFloat = 0 {
didSet {
pointer.setAffineTransform(rpm == 0 ? .identity : CGAffineTransform(rotationAngle: rpm/25 * .pi))
}
}
override init(frame: CGRect) {
super.init(frame: frame)
pointer = CALayer()
pointer.contentsScale = UIScreen.main.scale
pointerView = UIView()
addSubview(pointerView)
pointerView.layer.addSublayer(pointer)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
pointer = CALayer()
pointer.contentsScale = UIScreen.main.scale
pointerView = UIView()
addSubview(pointerView)
pointerView.layer.addSublayer(pointer)
}
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
context.saveGState()
//draw background with values
//but not the pointer or centre circle
context.restoreGState()
}
override func layoutSubviews() {
super.layoutSubviews()
pointerView.frame = bounds
pointer.frame = bounds
pointer.anchorPoint = CGPoint(x: 0.5, y: 0.5)
pointer.contents = drawPointer(in: bounds)?.cgImage
}
func drawPointer(in rect: CGRect) -> UIImage? {
UIGraphicsBeginImageContextWithOptions(rect.size, false, 0)
guard let context = UIGraphicsGetCurrentContext() else { return nil }
context.saveGState()
// draw the pointer Image. Make sure to draw it pointing at zero. ie at 8 o'clock
// I'm not sure what your drawing code looks like, but if the pointer is pointing
// vertically(at 12 o'clock), you can get it pointing at zero by rotating the actual draw context like so:
// perform this context rotation before actually drawing the pointer
context.translateBy(x: rect.width/2, y: rect.height/2)
context.rotate(by: -17.5/25 * .pi) // the angle judging by the dial - remember .pi is 180 degrees
context.translateBy(x: -rect.width/2, y: -rect.height/2)
context.restoreGState()
let pointerImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return pointerImage
}
}
The pointer's identity transform has it pointing at 0 RPM, so every time you up the RPM to what you want, it will rotate up to that value.
edit: tested it, it works. Except I made a couple errors - you don't need to change the layers position, I updated the code accordingly. Also, changing the layer's transform triggers layoutSubviews in the immediate parent. I forgot about this. The easiest way around this is to put the pointer layer into a UIView that is a subview of SpeedDial. I've updated the code. Good luck! Maybe this is overkill, but its a bit more reusable than animating the entire rendering of the view, background and all.
I have a UIImageView, where the image is set with a given url. Then, I set the content mode to Scale Aspect Fit. This works fine, but there is a ton of blank space before and after the image, when the image is supposed to be directly at the top of the screen.
What I would like to do is rescale the UIImage size (maybe frame?) to match the new size created when Aspect Fit is applied (seems to be the suggestion most people received).
The problem is, whenever I test previous solutions, I'm getting a nul error. Particularly:
import UIKit
import AVFoundation
class OneItemViewController: UIViewController {
#IBOutlet weak var itemImage: UIImageView!
#IBOutlet weak var menuButton: UIBarButtonItem!
#IBOutlet weak var titleText: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
let imageURL:NSURL? = NSURL(string: "https://upload.wikimedia.org/wikipedia/commons/d/d5/Pic_de_neige_cordier_Face_E.jpg")
if imageURL != nil {
itemImage.sd_setImageWithURL(imageURL)
itemImage.contentMode = UIViewContentMode.ScaleAspectFit
AVMakeRectWithAspectRatioInsideRect(itemImage.image!.size, itemImage.bounds)
/**
let imageSize:CGSize = onScreenPointSizeOfImageInImageView(itemImage)
var imageViewRect:CGRect = itemImage.frame
imageViewRect.size = imageSize
itemImage.frame = imageViewRect
**/
}
if self.revealViewController() != nil {
menuButton.target = self.revealViewController()
menuButton.action = "revealToggle:"
self.view.addGestureRecognizer(self.revealViewController().panGestureRecognizer())
}
self.titleText.text = "Title: " + "Earl and Countess of Derby with Edward, their Infant Son, and Chaplain"
// Do any additional setup after loading the view.
}
/**
func onScreenPointSizeOfImageInImageView(imageV: UIImageView) -> CGSize {
var scale: CGFloat
if (imageV.frame.size.width > imageV.frame.size.height) {
if (imageV.image!.size.width > imageV.image!.size.height) {
scale = imageV.image!.size.height / imageV.frame.size.height
} else {
scale = imageV.image!.size.width / imageV.frame.size.width
}
} else {
if (imageV.image!.size.width > imageV.image!.size.height) {
scale = imageV.image!.size.width / imageV.frame.size.width
} else {
scale = imageV.image!.size.height / imageV.frame.size.height
}
}
return CGSizeMake(imageV.image!.size.width / scale, imageV.image!.size.height / scale)
}
**/
}
Tried two things here to get rid of blank space.
First attempt is the call to AVMakeRectWithAspectRatioInsideRect.
Second attempt is the two chunks of code in the /** **/ comments. (onScreenPointSizeOfImageInImageView function and calls to it in viewDidLoad.)
But I can't tell if either work because itemImage.image!.size is causing an error.
So two questions:
1) Why is itemImage.image!.size giving me a nil while unwrapping?
2) Has anyone found a faster solution to removing blank spaces caused by AspectFit?
imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor, multiplier: image.size.width / image.size.height).isActive = true
This answer is answered programmatically with UIKit with Swift 5
As mentioned by #Ignelio, using NSLayoutConstraint would do the work for UIImageView.
The reasoning is that you want to keep maintain the aspect ratio - by using
// let UIImage be whatever you decide to name it
UIImage.contentMode = .scaleAspectFit
would make the UIImage inside the UIImageView fit back to its ratio size given the width. However, as mentioned in Apple's documentation, that will leave remaining area with transparent spacing. Hence, what you want to tackle is UIImageView's size/frame.
--
With this method, you're giving your UIImageView its width constraint equal to the UIImage's ratio - scaling back perfectly in regards to its parent's width constraint (whatever that may be).
// let UIImageView be whatever you name
UIImageView.widthAnchor.constraint(equalTo: UIImageView.heightAnchor, multiplier: UIImage.size.width / UIImage.size.height).isActive = true
So I declared a UILabel! named "textLabel" which has a gravity effect which goes up. Now, When it reachs -30, I want the label to reappear at (200, 444). I used a NSTimer to check if (labelText > -30) but when it reaches/passes that point, it just flashes where it's suppose to appear (around the middle) but it does not start from the middle. here is most of my code.
How do I make the Label appear at it's new position? so it can just cycle again once it reaches that -30 on the y-axis?? I've searched and searched. HELPPPPPPP
#IBOutlet weak var textLabel: UILabel!
var gravity: UIGravityBehavior!
var animator: UIDynamicAnimator!
var itemBehavior: UIDynamicItemBehavior!
var boundaryTimer = NSTimer()
Override func viewDidLoad() {
animator = UIDynamicAnimator(referenceView: view)
gravity = UIGravityBehavior(items: [textLabel])
animator.addBehavior(gravity)
boundaryTimer = NSTimer.scheduledTimerWithTimeInterval(0.5, target: self, selector: "leftBoundary", userInfo: nil, repeats: true)
itemBehavior = UIDynamicItemBehavior(items: [textLabel])
itemBehavior.elasticity = 1.2
gravity.gravityDirection = CGVectorMake(0.0 , -0.01)
animator.addBehavior(itemBehavior)
}
func leftBoundary() {
if textLabel.center.y < 40 {
self.textLabel.center = CGPointMake(200, 444)
}
}
Rather than setting the center, you can add and then remove an attachment behavior, e.g.:
func leftBoundary() {
if textLabel.center.y < 40 {
let attachment = UIAttachmentBehavior(item: textLabel, attachedToAnchor: textLabel.center)
animator.addBehavior(attachment)
attachment.anchorPoint = view.center
attachment.action = {[unowned self, attachment] in
if self.textLabel.center.y > 100 {
self.animator.removeBehavior(attachment)
}
}
}
}
Note, you might want to change the itemBehavior so that allowRotation is false (depending upon your desired UI).
Also note, this action that I use here is something you could use with your gravity behavior, too. So, rather than a timer (which might not always catch the label at the same time), use an action, which is called upon every frame of the animation.
You should first remove all label behaviors from the animator.
This has to do with how UIKit Dynamics works. It really only manipulates the layer / presentation layer of your view, so you need to reset everything if you want to set the position yourself (via the view, which also affects the layer).
Also, I think the setup with the timer is not very efficient. Rather, you should give KVO (key-value observing) a try. You can observe the key path "center.y" and react if it passes your limit.