UIView ignoring setHidden when in animation block - ios

I have this code on an UIView subclass:
override var hidden: Bool {
willSet {
DDLogDebug("Will set hidden=\(String(newValue)) on \(item?.name))")
}
didSet {
DDLogDebug("Did set hidden=\(String(hidden)) on \(item?.name))")
}
}
For some reason, I set false but it remains true as seen in these logs:
> Will set hidden=false on Optional("74D8E4CE-5E14-4914-8483-E9F66D2A79B7"))
> Did set hidden=true on Optional("74D8E4CE-5E14-4914-8483-E9F66D2A79B7"))
The only peculiarity of this issue is that it only happens when running inside a UIView.animateWithDuration(...) block. If I remove the animation the property is set correctly.
Any thoughts on what may be going on? This is driving me crazy, heh
Edit:
Little bit more info, this UIView I want to hide is an arrangedSubview of a UIStackView. It works correctly for the first few tries but suddenly stops working without any noticeable pattern.

This is a bug in UIStackView.
Here is another question describing the exact same issue I'm having. This is an open radar for this specific issue.
The solution I found was to avoid setting hidden to the same value twice.
if (subview.hidden) {
subview.hidden = false
}

try this one, it works for me
[UIView performWithoutAnimation:^{
arrangedSubview.hidden = isHidden;
}];

Related

UITest - UICollectionView scrolling issue with horizontal direction when isPagingEnabled true

I've been trying to scroll UICollectionView with horizontal scroll, to the next page when isPagingEnabled property was set as true. I've been working on it for couple of days and I've made a lot of research, but I couldn't find any case like mine. If you already had this problem and if you already found a solution for it, it would be great sharing your solution way with me. Here is my current case;
func sampleTest() {
let collectionView = app.collectionViews[.sampleCollectionView]
collectionView.waitUntil(.exists)
let totalPageCount = collectionView.cells.count
guard totalPageCount > 0 else {
XCTFail("No pages could find in collection to take snapshot.")
return
}
for currentPage in 1...totalPageCount {
snapshot("Page\(currentPage)")
collectionView.swipeLeft()
}
}
Here, swipeLeft() method of XCUIElement is not working as expected in my case. When I call the method, it is not moving to the next page. It swipes a little bit and turn back due to isPagingEnabled = true statement.
In addition, there is another problem that collectionView.cells.count is calculated wrong. It always returns 1. I assume that the reason of the problem is about reusability. Because the other cells has not dequeued yet. Or collectionView.cells.count is not working as I guess?

tvOS: Focus not moving correctly

I have a UIView with two buttons on it. In the MyView class I have this code:
-(BOOL) canBecomeFocused {
return YES;
}
-(NSArray<id<UIFocusEnvironment>> *)preferredFocusEnvironments {
return #[_editButton, _addButton];
}
-(IBAction) editTapped:(id) sender {
BOOL editing = !tableViewController.editing;
[_editButton setTitle:editing ? #"Done" : #"Edit" forState:UIControlStateNormal];
_addButton.hidden = !editing;
[tableViewController setEditing:editing animated:YES];
}
The basic idea is that the user can move the focus to the edit button, which can then make the Add button appear.
The problem started because every time I tapped the edit button, focus would shift to the table view. I would actually like it to move to the Add button. I also want it so that when editing it deactivated, the edit button keeps the focus. but again it's shifting down to the table view.
So I tried the above code. This works in that focus can move to the view and on to the button. But once it's there, I cannot get it to move anywhere else.
Everything I've read says just override preferredFocusEnvironments but so far I've not been able to get this to work. Focus keeps going to a button then refusing to move anywhere else.
Any ideas?
If anybody is facing this issue, Just check if you are getting the following debug message printed in the console.
WARNING: Calling updateFocusIfNeeded while a focus update is in progress. This call will be ignored.
I had the following code :
// MARK: - Focus Environment
var viewToBeFocused: UIView?
func updateFocus() {
setNeedsFocusUpdate()
updateFocusIfNeeded()
}
override var preferredFocusEnvironments: [UIFocusEnvironment] {
if let viewToBeFocused = self.viewToBeFocused {
self.viewToBeFocused = nil
return [viewToBeFocused]
}
return super.preferredFocusEnvironments
}
I was calling the updateFocus() method multiple times while viewToBeFocused was either nil or some other view. Debugging the focus issues mainly between transition is really difficult. You should have patience.
Important to note: This depends on your use case, but if you want to
update the focus right after a viewcontroller transition (backward
navigation), You might have to set the following in viewDidLoad:
restoresFocusAfterTransition = false // default is true
If this is true, the view controller will have the tendancy to focus the last focused view even if we force the focus update by calling updateFocusIfNeeded(). In this case , since a focus update is already in process, you will get the warning as mentioned before at the top of this answer.
Debug focus issue
Use the following link to debug the focus issues: https://developer.apple.com/documentation/uikit/focus_interactions/debugging_focus_issues_in_your_app
Enable the focus debugger first under Edit scheme > Arguments passed on launch:
-UIFocusLoggingEnabled YES
This will log all the attempts made by the focus engine to update the focus. This is really helpful.
You can override the preferredFocusEnviromnets with the following logic:
-(NSArray<id<UIFocusEnvironment>> *)preferredFocusEnvironments {
if (condition) {
return #[_editButton];
}
else {
return #[_addButton];
}
}
After setting it, you can call
[_myView setNeedsFocusUpdate];
[_myView updateFocusIfNeeded];
The condition could be BOOL condition = tableViewController.editing; or sg like that.
If that now works, you can call it with a delay (0.1 sec or so).

Swift: Disappearing views from a stackView

I have a fairly simple set up in my main storyboard:
A stack view which includes three views
The first view has a fixed height and contains a segment controller
The other two views have no restrictions, the idea being that only one will be active at a time and thus fill the space available
I have code that will deal with the changing view active views as follows:
import Foundation
import UIKit
class ViewController : UIViewController {
#IBOutlet weak var stackView: UIStackView!
#IBOutlet weak var segmentController: UISegmentedControl!
#IBAction func SegmentClicked(_ sender: AnyObject) {
updateView(segment: sender.titleForSegment(at: sender.selectedSegmentIndex)!)
}
override func viewDidLoad() {
updateView(segment: "First")
}
func updateView(segment: String) {
UIView.animate(withDuration: 1) {
if(segment == "First") {
self.stackView.arrangedSubviews[1].isHidden = false
self.stackView.arrangedSubviews[2].isHidden = true
} else {
self.stackView.arrangedSubviews[1].isHidden = true
self.stackView.arrangedSubviews[2].isHidden = false
}
print("Updating views")
print("View 1 is \(self.stackView.arrangedSubviews[1].isHidden ? "hidden" : "visible")")
print("View 2 is \(self.stackView.arrangedSubviews[2].isHidden ? "hidden" : "visible")")
}
}
}
As you can see, when the tab called 'First' is selected, the subview at index 1 should show, whilst 2 is hidden, and when anything else is selected, the subview at index 2 should show, whilst 1 is hidden.
This appears to work at first, if I go slowly changing views, but if I go a bit quicker, the view at index 1 seems to remain permanently hidden after a few clicks, resulting in the view at index 0 covering the whole screen. I've placed an animation showing the issue and a screenshot of the storyboard below. The output shows that when the problem happens, both views remain hidden when clicking on the first segment.
Can anybody tell me why this is happening? Is this a bug, or am I not doing something I should be?
Many thanks in advance!
Update: I seem to be able to reliably reproduce the issue by going to the First > Second > Third > Second > First segments in that order.
The bug is that hiding and showing views in a stack view is cumulative. Weird Apple bug. If you hide a view in a stack view twice, you need to show it twice to get it back. If you show it three times, you need to hide it three times to actually hide it (assuming it was hidden to start).
This is independent of using animation.
So if you do something like this in your code, only hiding a view if it's visible, you'll avoid this problem:
if !myView.isHidden {
myView.isHidden = true
}
Building on the nice answer by Dave Batton, you can also add a UIView extension to make the call site a bit cleaner, IMO.
extension UIView {
var isHiddenInStackView: Bool {
get {
return isHidden
}
set {
if isHidden != newValue {
isHidden = newValue
}
}
}
}
Then you can call stackView.subviews[someIndex].isHiddenInStackView = false which is helpful if you have multiple views to manage within your stack view versus a bunch of if statements.
In the end, after trying all the suggestions here I still couldn't work out why it was behaving like this so I got in touch with Apple who asked me to file a bug report. I did however find a work around, by unhiding both views first, which solved my problem:
func updateView(segment: String) {
UIView.animate(withDuration: 1) {
self.stackView.arrangedSubviews[1].isHidden = false
self.stackView.arrangedSubviews[2].isHidden = false
if(segment == "First") {
self.stackView.arrangedSubviews[2].isHidden = true
} else {
self.stackView.arrangedSubviews[1].isHidden = true
}
}
}
Based on what I can see, this weird behavior is caused by the animation duration. As you can see, it takes one second for the animation to complete, but if you start switching the segmentControl faster than that, then I would argue that is what is causing this behavior.
What you should do is deactivate the user interactivity when the method is called, and then re-enable it once the animation is complete.
It should look something like this:
func updateView(segment: String) {
segmentControl.userInteractionEnabled = false
UIView.animateWithDuration(1.0, animations: {
if(segment == "First") {
self.stackView.arrangedSubviews[1].isHidden = false
self.stackView.arrangedSubviews[2].isHidden = true
} else {
self.stackView.arrangedSubviews[1].isHidden = true
self.stackView.arrangedSubviews[2].isHidden = false
}
print("Updating views")
print("View 1 is \(self.stackView.arrangedSubviews[1].isHidden ? "hidden" : "visible")")
print("View 2 is \(self.stackView.arrangedSubviews[2].isHidden ? "hidden" : "visible")")
}, completion: {(finished: Bool) in
segmentControl.userInteractionEnabled = true
}
}
While this will prevent from fast switching (which you may see as a downside), the only other way I am aware of that solve this is by removing the animations altogether.
Check the configuration and autolayout constraints on the stack view and the subviews, particularly the segmented control.
The segmented control complicates the setup for the stack view, so I'd take the segmented control out of the stack view and set its constraints relative to the main view.
With the segmented control out of the stack view, it's relatively straightforward to set up the stack view so that your code will work properly.
Reset the constraints on the stack view so that it is positioned below the segmented control and covers the rest of the superview. In the Attributes Inspector, set Alignment to Fill, Distribution to Fill Equally, and Content Mode to Scale to Fill.
Remove the constraints on the subviews and set their Content Mode to Scale to Fill.
Adjust the indexing on arrangedSubviews in your code and it should work automagically.

addCoordinatedAnimations animation block not running

In tvOS on Xcode 7.3.1, one of the places I use UIFocusAnimationCoordinator's addCoordinatedAnimations function is running the completion before the animation:
if (coordinator != nil) {
var tempDidAnimate: Bool = false // breakpoint 1
coordinator!.addCoordinatedAnimations({
self.myFunctionThatDoesntGetCalled() // breakpoint 2
tempDidAnimate = true
}, completion: {
() in
if tempDidAnimate == false {
print("whaaaat?!??") // breakpoint 3
self.myFunctionThatDoesntGetCalled()
}
})
}
Order of breakpoints being hit is 1, 3. Never 2.
This hacky use of if tempDidAnimate == false does solve the problem, but I don't get why the problem is happening.
Any ideas what could be wrong?
One idea: I'm already inside an addCoordinatedAnimations block in the stack... I don't think so, but the stack is complicated... can't see any way to check that via code.
tl;dr: YES I was already inside an animation block #$&^##$!
Ok, turned out that in my "cleverly" refactored code, I was forgetting that I'm calling an animation block to reset any currently active autolayout animations (by requesting a new animation with duration 0). Then in that completion, my code above is being called... Since I'm officially already inside an animation block, the OS refuses to execute the main animation block above, and skips right to the completion. Don't know if this OS behavior is documented anywhere.

UITableView:reloadSectionIndexTitles animated correct way

I am trying to hide the index bar of a UITableView while scrolling.
Therefore I am reloading the section index titles when I start scrolling and when finish. Returning an empty array hides the bar.
My code is:
var showSectionIndexTitles = true
override func scrollViewWillBeginDragging(scrollView: UIScrollView) {
showSectionIndexTitles = false
UIView.animateWithDuration(0.5, animations: { () -> Void in
self.tableView.reloadSectionIndexTitles()
})
}
override func scrollViewWillBeginDecelerating(scrollView: UIScrollView) {
showSectionIndexTitles = true
UIView.animateWithDuration(0.5, animations: { () -> Void in
self.tableView.reloadSectionIndexTitles()
})
}
override func sectionIndexTitlesForTableView(tableView: UITableView) -> [AnyObject]! {
if showSectionIndexTitles {
return uniq([UITableViewIndexSearch] + AlphabetUppercase + datamanager.categoryIndexTitles)
} else {
return nil
}
}
This works when using no animations, but I would like to use an animation.
I would prefer an animation where the whole bar moves out to the right when the bar is hidden and move in from the right when the bar is visible
I tried to use UIView:animateWithDuration to test if an animation is possible at all.
What I have noticed:
This basic animation moves/scales in from the left top corner when
visible
When hiding the bar it disappears instantly
My questions:
What is the best way of achieving an animation for indexbar visibility?
What is the best way of achieving the particular animation I mentioned earlier?
Thank you in advance!
EDIT 1:
I just remembered where I have seen this effect before: iOS 8.4 Music App
Apple does the same when you scroll so far that you can only see the title list(UITableView)
EDIT 2:
I filed a bug report to apple suggesting a function for changing visibility of the index bar with a animated parameter. I am going to inform you as soon as I get a response.
Even though #matt already suggested a possible solution in his answer, still if anybody else knows a different convenient way of solving this problem or has also faced this kind of feature in the past I would be glad to hear from you!
What you're trying to do is unsupported. Therefore there is no "best" or "correct" way - whatever you do will be an illegal hack. What I would do is snapshot the index bar, hide the real index bar as you are already doing (i.e. legally and normally), and animate the snapshot.

Resources