UITableView:reloadSectionIndexTitles animated correct way - ios

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.

Related

Swift how to detect if keyboard is fully visible or fully hidden only (and not partially visible)

This question is related to the following questions: Detect when keyboard is fully visible and prevent keyboard appearance handling code from adding extra offset for hidden element
I was initially using notifications like NSNotification.Name.UIKeyboardDidShow, NSNotification.Name.UIKeyboardDidHide hoping they would be triggered only once, hence enabling me to set a bool that the keyboard is fully displayed or that it is hidden.
But what I have noticed is that all the events: UIKeyboardWillShow, UIKeyboardDidShow, UIKeyboardWillHide, UIKeyboardDidHide, UIKeyboardWillChangeFrame, UIKeyboardDidChangeFrame are designed to trigger multiple times as the keyboard appears or disappears.
There seems to be no way to check if a keyboard is completely visible and not partially visible. All the answers I looked at listened to these notifications and did calculations to avoid the views from being hidden by the keyboard. But I could not find any way to see if a keyboard is fully displayed (or fully hidden)
I even looked into KeyboardObserver which makes it easier to observe keyboard events, But since it's still based on the default notifications, it's KeyboardEventType.didShow and KeyboardEventType.didHide are triggered multiple times as keyboard appears and disappears.
There should be a better way to tell if a keyboard is fully visible or not visible!
Here is an example of a property to check if the keyboard is available in the screen:
extension UIApplication {
var isKeyboardPresented: Bool {
if let keyboardWindowClass = NSClassFromString("UIRemoteKeyboardWindow"),
self.windows.contains(where: { $0.isKind(of: keyboardWindowClass) }) {
return true
}
else {
return false
}
}
}
if UIApplication.shared.isKeyboardPresented {
print("Keyboard presented") }
else {
print("Keyboard is not presented")
}

iOS tableView not reloading until click, scroll, or switch tabbar items

I have this problem where (in several places) after executing an API call, the view does not refresh until a user action - like a btn click, tab bar switch, etc occurs. I have a feeling it is related to threading, but I can't seem to figure it out and I am new to iOS programming. I have tried different solutions with DispatchQueue etc, using it, and not using it. Trying to call setNeedsDisplay on the controller view. But no luck yet. The following is an example of code pulled right from one of my tab bar item view controllers:
func getEmployeeUpdates(){
self.showLoader()
APIAdaptor.shared.getEmployeeUpdates(forEmployee: Session.shared.employee, completion: {
(updates:[ScheduleUpdate]?, error:Error?) in
guard error == nil else{
DispatchQueue.main.async {
// self.resetMainScreen()
self.hideLoader();
}
return
}
DispatchQueue.main.async {
self.hideLoader();
self.ScheduleUpdates = updates!
self.tableView.reloadData();
}
})
}
func showLoader(){
NSLayoutConstraint.activate([
activityIndicator!.centerXAnchor.constraint(equalTo: self.tableView.centerXAnchor),
activityIndicator!.centerYAnchor.constraint(equalTo: self.tableView.centerYAnchor)])
activityIndicator?.startAnimating();
}
func hideLoader(){
print("Hiding");
activityIndicator?.stopAnimating()
}
I have attached two images. The first image is where the api call has finished (confirmed through testing) but the view is not refreshing. The loader is frozen. It should disappear after a call to hideLoader(). the second Image is after a click, or tab bar item switch.
I should also mention that in this example, as well as in other api calls the view will refresh eventually after completing, but only after a significant delay.
If anyone can help I would appreciate it very much!
This was a problem caused by the simulator on Xcode 10.1. If you run into this problem, try updating Xcode, or using a real device.

Is there a better way to do this scrolling animation in Swift?

I'm trying to add a behavior to part of my app's UI whereby the user can swipe left and this will result in a set of UILabels scrolling in sync together off-screen to the left, but then immediately scrolling back in again but from the right, but with new information contained in them.
The effect is meant to give the impression that you're moving from one "set" of info to the next... like, say, choosing a car before starting a race game... but in reality it is the same views being re-used... scrolling offscreen to have their label.text info updated... then scrolling back in again.
I have the swiping all taken care of. The issue I'm having is that my (working) solution:
UIView.animate(withDuration: 0.2, animations: {
// move label off to the left
self.titleLabel.center.x -= self.view.bounds.width
}, completion: {
$0 ; print("I'm halfway done!")
// teleport view to a location off to the right
self.titleLabel.center.x += 2*(self.view.bounds.width)
// reset label's data
self.titleLabel.text = NEW_INFO
// slide label back on screen from the right
UIView.animate(withDuration: 0.2, animations: {
self.titleLabel.center.x -= self.view.bounds.width
}, completion: nil)
})
Feels trashy like wearing someone else's underwear.
The only reason that $0 is there is to make XCode stop saying:
"Cannot convert value of type '() -> ()' to expected argument type '((Bool) -> Void)?'"
And I'm sure the fact that I'm doing the second part of the animation in a completion block will cause headaches down the road.
Is there a smarter way?
PS - I would prefer not to use any pre-made classes like "ScrollView" or anything like that... these views are all individually interactive and respond to other callbacks etc.
Thanks!
A better approach to something like this would be to use a UIPageViewController.
It will take a bit of learning and set up but is much easier than trying to roll it yourself.
The approach to take with a UIPageViewController is something like this...
Create a data model... to use your analogy...
struct Car {
let image: UIImage
let name: String
}
Then create a UIViewController subclass that will display it.
class CarViewController: UIViewController {
var car: Car? {
didSet {
displayCar()
}
}
func displayCar() {
label.text = car?.name
imageView.image = car?.image
}
}
Then you create a UIPageViewController. Inside this you have an array of cars. And in the function func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? you can then create your CarViewController and pass in the correct Car from the array.
This will then do all your scrolling and displaying and everything is still interactive.
For more information about how this works you can look at tutorials like this one from Ray Wenderlich.
You can also use this to display a part of a page (rather than scrolling the entire screen.

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.

Apple TV force focus another view

I'm working on Apple TV project. The project contains tab bar view controller, normally the tab bar will be appeared when swiping up on remote and hidden when swiping down. But now I reverse that behavior and I want to force focus another view when swiping up(normally focus on tab bar). Any way to do that? Thank you.
In your UIViewController, override shouldUpdateFocusInContext. If you detect an upward navigation into the tab bar, return false to prevent focus from reaching the tab bar. Then use a combination of preferredFocusEnvironments + setNeedsFocusUpdate to redirect focus somewhere else:
override func shouldUpdateFocus(in context: UIFocusUpdateContext) -> Bool {
if let nextView: UIView = context.nextFocusedView{
if ( context.focusHeading == .up && nextView.isDescendant(of: tabBar) ){
changeFocusTo(myView)
return false
}
}
}
internal var viewToFocus: UIView?
func changeFocusTo(_ view:UIView? ){
viewToFocus = view
setNeedsFocusUpdate()
}
override var preferredFocusEnvironments: [UIFocusEnvironment]{
return viewToFocus != nil ? [viewToFocus!] : super.preferredFocusEnvironments
}
This is a generally useful technique for customizing focus updates. An alternative technique is to use UIFocusGuide. You could insert a focus guide underneath the tab bar or surround the tab bar with a focus guide to redirect focus. Though focus guides are useful for simple cases, I have generally had better results using the technique I am describing instead.
I got the same issue with focus of UITabbarController before and I found the solution in Apple Support
Because UIViewController conforms to UIFocusEnvironment, custom view
controllers in your app can override UIFocusEnvironment delegate
methods to achieve custom focus behaviors. Custom view controllers
can:
Override the preferredFocusedView to specify where focus should start
by default. Override shouldUpdateFocusInContext: to define where focus
is allowed to move. Override
didUpdateFocusInContext:withAnimationCoordinator: to respond to focus
updates when they occur and update your app’s internal state. Your
view controllers can also request that the focus engine reset focus to
the current preferredFocusedView by callingsetNeedsFocusUpdate. Note
that calling setNeedsFocusUpdate only has an effect if the view
controller contains the currently focused view.
For more detail, please check this link
https://developer.apple.com/library/content/documentation/General/Conceptual/AppleTV_PG/WorkingwiththeAppleTVRemote.html#//apple_ref/doc/uid/TP40015241-CH5-SW14

Resources