When I change the height of my CATextLayer, a new text comes in from above (or below) as is depicted in the image below. How can I prevent this?
#IBAction func Tap(sender: UIButton) {
counter += 1
CATransaction.begin()
CATransaction.setAnimationDuration(8.0)
txtLay!.frame = frameFromCounter()
CATransaction.commit()
}
CATextLayer draws itself via drawInContext: method, so any change into the rendered representation (e.g. changing string property) will also modify the contents of the layer. In your case you're resizing the layer causing the backing store resize which changes contents which adds an implicit animation to that property.
If you don't want the animation to happen you can use the actions dictionary to disable the implicit contents animation:
txtLay!.actions = ["contents" : NSNull()]
However, disabling contents animation will cause a jump in that case, so you'll probably better of not changing the bounds of the CATextLayer and just embed it into a superlayer to provide any additional styling/layout you want.
Related
I have a UIView containing a number of UILabel positioned with AutoLayout.
On device rotation I switch the layout constraints to animate the labels to new locations (the layout constraints are derived from a Layout object which is assigned to the view):
override func viewWillTransition(
to size: CGSize,
with coordinator: UIViewControllerTransitionCoordinator
) {
super.viewWillTransition(to: size, with: coordinator)
let isLandscape = size.width > size.height
let layout = isLandscape ? layoutLandscape : layoutPortrait
coordinator.animate(
alongsideTransition: { _ in self.layoutView.setLayout(layout) },
completion: nil
)
}
This effect works well and provides a smooth transition between different content layouts optimised for landscape and portrait.
However, during the transition, the size of the labels also changes due to the layout constraints. It is noticeable that while the transition of labels' positions is gradual, the labels' shapes change immediately at the start of the transition, and then they animate to their final position.
I would like to be able to cross-disolve the labels between their initial and final shapes and have this occur alongside the position animation.
Ideally, I'd like to do this in the same highly decoupled manner that position animation is achieved, with the layout update code being oblivious of whether the change will be animated or not. I'm expecting at least a bit of compromise may be required, though!
Thank you.
I'm fairly close to a working solution on this. It's not answer material yet, so I'm going to put notes here as "Stuff I've Tried" – if it becomes an answer I'll turn it in to one.
A fantastic article at objc.io shows how you can create a UILabel subclass that will tell its backing CALayer to animate changes to its content property using a CATransition. It's purest unadulterated sorcery, but marvellously, it actually all makes sense and you suspect that perhaps you too can don the wizard's hat.
So, you end up with something a bit like this:
class ContentAnimatedLabel: UILabel {
override func action(for layer: CALayer, forKey event: String) -> CAAction? {
guard event == "contents" else {
return super.action(for: layer, forKey: event)
}
let transition = CATransition()
// Oh noes! A hard coded made up value!
// And where do we get the timing curve?! Curses.
transition.duration = 0.3
return transition
}
}
And as far as it goes, it works! The content of the labels fades nicely, and their position interpolates. And it looks fairly grand.
The problem is that you this code doesn't know if it is being called from within an animation block. Really, it should only return the transition when being called from within an animation block, and it should copy animation properties such as duration and timing from those given by the animation block.
The article suggests a solution to this…
The UIView animation mechanism implementation is private, so there's no simple flag we can check to see if the view is currently animating, however we do know one thing: When animating, UIView's actionForLayer:forKey: will return valid CAActions for its animatable property keys, and when not animating it will return [NSNull null] for them. If we simply pick a suitable key we can interrogate UIView to see if it's currently supplying an action for that key, and use that to determine our response for our custom key. We'll use the key "backgroundColor" since that's a property of UIView that normally supports animation, and that we know returns a CABasicAnimation as its action (as of iOS 8, some properties, such as "bounds" return a private class instead, so watch out):
… but this doesn't seem to work, at least for the iOS 11 on which I'm testing.
When UIKit asks for the action for the content key, its as if the animation block has finished. Asking super for the action in use by another keys always returns nothing. So you're left with nothing to grab appropriate transition parameters from.
class ApplyCorners: UIButton {
override func didMoveToWindow() {
self.layer.cornerRadius = self.frame.height / 2
}
}
I apply this class to the buttons in my application and it is working great, but when I apply it to a button used in every cell in a table view the button corners are not round upon entering the view, but if I click one of the buttons I get segued to another view. If I then segue back the corners are "fixed" / round.
The green is the button when returning and the red is upon first entering the view.
Anyone know how to fix this?
I'd suggest layoutSubviews, which captures whenever the frame of the button changes:
class ApplyCorners: UIButton {
override func layoutSubviews() {
super.layoutSubviews()
layer.cornerRadius = frame.height / 2
}
}
This takes care of both the original appearance and any subsequent appearance. It also avoids all sorts of problems related to not only whether the frame was known when the view appeared, but also if you do anything that might change the size of the button (e.g. anything related to constraints, rotation events, etc.).
This sort of thing is likely to be a timing problem. Consider the phrase self.frame.height. At the time didMoveToWindow is called, we may not yet know our frame. If you are going to call a method that depends upon layout, do so when layout has actually occurred.
Gonna propose another alternative: listen to any bounds changes. This avoids the problem of wondering "is my frame set yet when this is called?"
class ApplyCorners: UIButton {
override var bounds: CGRect {
didSet {
layer.cornerRadius = bounds.height / 2
}
}
}
Edited frame to bounds because as #Rob points out, listening for frame changes will cause you to miss the initial load sometimes.
Putting your code in didMoveToWindow() does not make sense to me. I'd suggest implementing layoutSubviews() instead. That method gets called any time a view object's layout changes, so it should update if you resize your view.
(Changed my suggestion based on comments from TNguyen and and Rob.)
I'm using the following code to have a label slide onto the screen when a button is pressed, but it's having no effect.
override func viewDidLayoutSubviews() {
summaryLabel.alpha = 0
}
#IBAction func searchButton(sender: AnyObject) {
UIView.animateWithDuration(2) { () -> Void in
self.summaryLabel.center = CGPointMake(self.summaryLabel.center.x - 400, self.summaryLabel.center.y)
self.summaryLabel.alpha = 1
self.summaryLabel.center = CGPointMake(self.summaryLabel.center.x + 400, self.summaryLabel.center.y)
}
//button code continues...
I've tested what's going on by fixing the alpha at 1, but the label just stays where it is and does not move when the button is pressed. What am I missing?
A couple of things:
First of all, your two changes to the view's center cancel each other out. The animation applies the full set of changes that are inside the animation block all in one animation. If the end result is no change, then no change is applied. As Ramy says in his comment, you either need 2 animations, timed so the second one takes place after the first one has completed, or you nee to apply the first change before the animation begins. I would suggest starting with a single change, and a single animation, and get that working first.
Second problem: View controllers use auto layout by default. With auto layout, you can't animate the position of a view directly. It doesn't work reliably. Instead, you have to put a constraint on the view, connect it to an outlet, and the animate a change to the constraint's constant value by changing the constant and calling layoutIfNeeded() inside the animation block. The call to layoutIfNeeded() inside the animation block causes the view's position to be changed, and since it's inside the animation block, the change is applied with animation.
I am building a menu based upon buttons. I am building them purely in code.
let menu00 = UIButton()
menu00.frame=menuHome
let imageSubLayer = CALayer()
imageSubLayer.contents = UIImage(named: "menuIcon.png")!.CGImage
menu00.layer.insertSublayer(drawHexFill(menu00.frame, fillColor: UIColor.lightGrayColor()), atIndex: 2)
menu00.layer.insertSublayer(drawHexBorder(menu00.frame, fillColor: UIColor.clearColor()), atIndex: 1)
menu00.layer.insertSublayer(imageSubLayer, atIndex: 0)
menu00.titleLabel!.text="MENU"
menu00.titleLabel!.textColor=UIColor.whiteColor()
menu00.tag=100
self.view.addSubview(menu00)
drawHexFill and drawHexBorder are two UIBezierPath methods to draw the graphical layers, which I do as the button may have different colour border and fill depending on the situation.
The problem I have is that the text and the image are not visible. If I add the image as the button image, it is visible, however behind the hex border and hex fill layers.
I have tried different positions "atIndex" for all objects but simply cannot get what I want. I would like hexFill in the background, hexBorder next and image or text on top.
Any suggestions?
The problem is possibly that your subLayers have different values of their .zPosition property. From
Adds a new sublayer object to the current layer. The sublayer is added
to the end of the layer’s list of sublayers. This causes the sublayer
to appear on top of any siblings with the same value in their
zPosition property.
From here. You could double-check that the .zPosition property has the same value for all your three sublayers.
If this is not the issue, try using insertSubLayer:above instead of insertSubLayer:atIndex (see documentation).
I have a UIButton that has a title.
Consider it as Thu 20.
There is an image which is set as the background for the button,this image is width resized and content mode is aspect fit.So it appears to the full button.
My problem:
I need the image only for the "20" part and not the entire "Thu 20".
if you see the iPad calendar you will get the point.
How can i do it?
Thanks
Unless something like an attributed string has a functionality of putting a rectangle around some text I suggest you to create a custom view that handles this:
You would need a view that excepts 2 strings, a normal one and the one in the rectangle. Then insert a normal label, set the text, call sizeToFit. Then add the second label and do the same as first but in the end place it right next to the first label. Then add the image view onto the label respecting the labels dimensions. Then resize the whole view to fit all the views on it. Now you can add this custom label to a button or anywhere you want it...
I would rather suggest you place an Image VIew containing your Image and Then place a Label on top of Image with your "20" over it and then Place a custom UIButton with No backGround Color, and No Images on the top of both
With a bit of work you could override one of these UIButton methods to place the image at the correct location. Using a method within UILabel to determine the coordinates of "20" would give the the location for the image.
class UIButton {
// these return the rectangle for the background (assumes bounds), the content (image + title) and for the image and title separately. the content rect is calculated based
// on the title and image size and padding and then adjusted based on the control content alignment. there are no draw methods since the contents
// are rendered in separate subviews (UIImageView, UILabel)
open func backgroundRect(forBounds bounds: CGRect) -> CGRect
open func contentRect(forBounds bounds: CGRect) -> CGRect
open func titleRect(forContentRect contentRect: CGRect) -> CGRect
open func imageRect(forContentRect contentRect: CGRect) -> CGRect
}
See Apple's Documentation under "Getting Dimensions" for more information about these methods.