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.
Related
When I click on an a segment of my UISegmentedControl, I want one of two UIViews to be displayed. But how do I arrange it, that I only show the new view. Currently I am calling thisview.removeFromSuperview() on the old one, and then setup the new all from scratch. I also tried setting all the HeightConstants of the views subviews to zero and then set the heightConstants of the view itself to zero but I'd rather avoid that constraints-surgery..
What better approaches are there?
Agree with #rmaddy about using UIView's hidden property, a nice simple way to cause a view to not be drawn but still occupy its place in the view hierarchy and constraint system.
You can achieve a simple animation to make it a bit less jarring as follows:
UIView.animate(withDuration:0.4, animations: {
myView.alpha = 0
}) { (result: Bool) in
myView.isHidden = true
}
This will fade the alpha on the view "myView", then upon completion set it to hidden.
The same animation concept can be used also if you've views need to re-arrange themselves, animating layout changes will be a nice touch.
Based on #rmaddy and #CSmiths answer, I built the following function:
func changeView(newView: UIView, oldView: UIView) {
newView.isHidden = false
newView.alpha = 0
UIView.animate(withDuration:0.4, animations: {
oldView.alpha = 0
newView.alpha = 1
}) { (result: Bool) in
oldView.isHidden = true
}
}
I feel dumb now for all the hours I spent on that constraint-surgery. :|
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.)
My layout is currently like this:
View
-- View
-- Vertical ScrollView
------ View
--------- Horizontal Paginated ScrollView
--------- View
------------- Horizontal ScrollView -- not working properly
See this image for view hierarchy screenshot from xcode:
Using Swift.
I am adding subviews dynamically to this "Size Select Scroll View"
Two Issues:
After adding views, there is no margin between the subviews. Each subview's coord. are like this: (10.0, 0.0, 44.0, 44.0), (54.0, 0.0, 44.0, 44.0), (98.0, 0.0, 44.0, 44.0), (142.0, 0.0, 44.0, 44.0) etc.
But the appearance is like this without the 10 points gap between each subview: http://i.stack.imgur.com/mCGRW.png
Scrolling horizontally is a pain. It only works on maybe 1/4 height from top of the scrollview area and very difficult to scroll. How do i layout subviews so that this scrollview is properly scrollable?
Note: I am explicitly setting content size of size scrollview to more than required so that i can see the scrolling.
I found out the issue. According to the Apple Docs the touch events will be passed to a subview only if it lies entirely in its parent.
In my case, the scrollview was going out of bounds of its parent view ( Details View), because of which touch events were weird. I increased the parent view's size to fit the scrollview and it works fine now.
From the docs (https://developer.apple.com/library/content/qa/qa2013/qa1812.html):
The most common cause of this problem is because your view is located outside the bounds of its parent view. When your application receives a touch event, a hit-testing process is initiated to determine which view should receive the event. The process starts with the root of the view hierarchy, typically the application's window, and searches through the subviews in front to back order until it finds the frontmost view under the touch. That view becomes the hit-test view and receives the touch event. Each view involved in this process first tests if the event location is within its bounds. Only after the test is successful, does the view pass the event to the subviews for further hit-testing. So if your view is under the touch but located outside its parent view's bounds, the parent view will fail to test the event location and won't pass the touch event to your view.
This is always a challenge in iOS.
There are various solutions which unfortunately depend on the exact situation.
Here's a drop-in solution which is often the right solution.
/*
So, PASS any touch to the NEXT view, BUT ALSO if any of OUR
subviews are buttons, etc, then THOSE should ALSO work normally. :/
*/
import UIKit
class Passthrough: UIView {
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
return subviews.contains(where: {
!$0.isHidden
&& $0.isUserInteractionEnabled
&& $0.point(inside: self.convert(point, to: $0), with: event)
})
}
}
(Of course, you can also just drop the call in to some class, eg
class SomeListOrWhatever: UICollectionView, .. {
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
print("MIGHT AS WELL TRY THIS")
return subviews.contains(where: {
!$0.isHidden
&& $0.isUserInteractionEnabled
&& $0.point(inside: self.convert(point, to: $0), with: event)
})
}
Even if you "don't totally understand what the problem is", this is "one of" the solutions!
For example, this is (usually!) the precise solution to the exact issue quoted from the doco by #kishorer747
It's definitely a real nuisance in iOS.
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.
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.