How to add accessibility for UIRefreshControl in Swift - ios

I am trying to add accessibility for UIRefreshControl which has been implemented for pull to refresh in UITableView as below
self.refreshControl.accessibilityLabel = "Refreshing"
But if accessibility turned on and by swiping three finger on UITableview then does not speaking as Refreshing.
I override accessibilityScroll method as below.
override func accessibilityScroll(_ direction: UIAccessibilityScrollDirection) -> Bool {
if direction == .up {
self.refreshControl.accessibilityLabel = "Refreshing"
}
return true
}
Any idea Thanks!

Firstly, refreshing is more than just scrolling up. Its scrolling up past y position 0. Otherwise this will happen whenever the user scrolls up. Secondly, use UIAccessibilityPostNotification to announce to the user that a UI element has just changed.

Related

How to fix highlighted state UIButton delay in UIPageViewController page?

I have a screen in which I have a UIPageViewController in each page I have a UIButton. The problem is that there is a delay of the pressed/highlighted state of the button for about a half second when the user presses the button. both state images are set to the button using the storyboard.
This happens in the Simulator as well as on a real device.
Now from my Google searches, I came across a few posts that describe this issue, for example:
UIButton delayed state change
and:
UIbutton only looks clicked (highlighted) on longPress?
In all posts, the solution is to use the delaysContentTouches setting and set it to false.
The problem is: I didn't found how would I apply this in my case of a UIPageViewController. most of the posts talk this issue in a UIScrollView or a UITableView.
So, the question is: how would I do that in case of a UIPageViewController? I didn't see that UIPageViewController has this setting and didn't find any other way to apply it.
Found a solution to this issue, This piece of code will fix the button highlighted click delay but will prevent the pager scroll on the button itself.
public override func viewDidAppear(_ animated: Bool) {
for view in self.view.subviews {
if view is UIScrollView {
(view as? UIScrollView)!.delaysContentTouches = false
}
}
}
The reason I didn't find this in UIPageViewController is that UIPageViewController is not a subclass of UIScrollView as I expected but it contains it as a subview.

White line below UIRefreshControl when pulled.. tableView

I am implementing a very basic Refresh control...
var refreshControl = UIRefreshControl()
refreshControl.addTarget(self, action: Selector(("refresh:")), for: UIControlEvents.valueChanged)
refreshControl.backgroundColor = UIColor.red
self.tableView.addSubview(refreshControl)
for some reason though whenever I pull down to refresh it's like the refresh control cannot keep up with the table view and there is a white gap between the two.
here is the problem on the simulator... it is worse on the iPhone I believe
http://gph.is/2ijyH26
While I also suggest you should do what Artem posted, it didn't directly remove that gap for me. Turns out I was running into 2 different issues related to UIRefreshControl and found a few 'hacks' to get around them. The easy fix is to set the tableView's background colour to the same colour as your refresh control, but that has the side effect of seeing the same colour at the bottom of your table view.
My set up is a UINavigationController hosting a UIViewController with a UITableView subview and a UISearchBar set as the tableView's tableHeaderView. My goal is to match the colour of the navigation bar, refresh control, and the search bar.
Issue 1
I think this the same issue that you're seeing. It seems like as soon as you drag to start the pull to refresh action, there's a gap that appears during the first few pts of the gesture. After some threshold, the refresh control becomes the correct colour. On the way back to the table view's resting scroll state though, we see that same gap again just before it reaches a content offset of 0. Here's what that looks like:
Solution 1
If you subclass UIRefreshControl and override the frame and isHidden properties and just print out their values when they are set, you'll notice that the refresh control doesn't actually get unhidden until the distance from the top of the table view is 4pt. Similarly, when you scroll back down you'll also see that it is set to hidden around the same spot, which is before you can't visibly see the refresh control anymore and why we see the gap peeking to the tableView background.
In our subclass, we can prevent this 'early' hiding and 'late' unhiding by overriding the setter and getter of isHidden and the didSet of frame to only hide when the refresh control's offset is actually 0.
class RefreshControl: UIRefreshControl {
override var isHidden: Bool {
get {
return super.isHidden
}
set(hiding) {
if hiding {
guard frame.origin.y >= 0 else { return }
super.isHidden = hiding
} else {
guard frame.origin.y < 0 else { return }
super.isHidden = hiding
}
}
}
override var frame: CGRect {
didSet {
if frame.origin.y < 0 {
isHidden = false
} else {
isHidden = true
}
}
}
}
I call this a hack because I'm not a huge fan of modifying the existing behaviour of UIRefreshControl, but I haven't found a better way to get around this yet.
Issue 2
When pulling to refresh past that gap threshold in issue 1, it seems like the frame of the UIRefreshControl can't keep up with the search bar. This gives us another kind of gap that follows the search bar around. This is what it looks like:
Solution 2
This lagging gap looks like the frames aren't being updated as fast as our scrolling maybe even animated. Turns out that if we set the frame to itself in somewhere like layoutSubviews, we get the following behaviour:
override func layoutSubviews() {
super.layoutSubviews()
var originalFrame = frame
frame = originalFrame
}
Again, this is pretty hacky, but I haven't found a different way to go about this either.
Result
Why do you add refreshControl as a subview? You must do that:
refreshControl.addTarget(self, action: #selector(refresh), for: .valueChanged)
tableView.refreshControl = refreshControl

scrollViewDidScroll is not getting called

I have a custom tab bar with a horizontal scroll view underneath that.
Tab bar contains 5 buttons each of them calls one of the five custom keyboards (they are placed on a scroll view). You can switch between these keyboards scrolling the scroll view or pressing the buttons.
Question: how do I change the states of the buttons (default <-> selected) when I switch between keyboards using scroll view?
Visual:
I downloaded your project and you have some issues there. You're implementing the UIScrollViewDelegate but for some reason, the method you have in your app is wrong for the scrollViewDidScroll(_:).
The method needs to be declared like in this way:
func scrollViewDidScroll(_ scrollView: UIScrollView) {}
You cannot change anything in that method and in your case you changed the name of the argument as the name of your UIScrollView, just a minor change but it implies that you will not be notified every time the UIScrollView did scroll.
So let's go back to your problem. To achieve what do you want you to need to calculate the current page where you are inside the UIScrollView or in your case the number of the button, we can calculate that using the following formula:
let pageWidth = scrollView.frame.size.width
let page = Int(round((scrollView.contentOffset.x ) / (pageWidth)))
Afterwards what you need is update your selected button when the scroll finish of scrolling. The problem with the scrollViewDidScroll(_:) method is that this method is called several times during the scrolling process, so we need a method that is called only when the scrolling is finished, and it's the scrollViewDidEndDecelerating(_:).
So you need to add the following code in your code:
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let pageWidth = scrollView.frame.size.width
let page = Int(round((scrollView.contentOffset.x ) / (pageWidth)))
let previousIndex = selectedIndex
tabBarButtons[previousIndex].isSelected = false
selectedIndex = page
tabBarButtons[page].isSelected = true
}
I tested the behaviour in your app and works fine. If you have any trouble I have your project modified, but the only thing you need is to add the above method in your UIViewController and it should work fine
I hope this help you
UIScroll has a delegate method called scrollViewDidScroll. This gets called every time the user scrolls. The scrollView has a property called contentOffset. That property tells you exactly how much as been scrolled. Use this method and property to branch your logic.

Handling UIPanGestureRecognizer gestures for multiple Views (one covers the other)

Sorry for such a long question, but felt I should convey what I have tried.
I've got a view viewA within a navigation controller. I am then adding a subview viewB (that contains a UITableView) to viewA and offsetting its origin height so that it covers only half the screen (with the other half overflowing off out the bottom of the screen). I want to be able to then drag this viewB upwards but it get stopped when it hits the bottom of the navigation bar and similarly get stopped when dragged back down when it hits the origin offset point. This I have achieved successfully.
However, I want the UITableView interaction to only be enabled when viewB is in its upper position and thus not respond to gestures in any other position. Essentially, dragging viewB up so that it completely covers viewA should enable interaction with the UITableView.
The tricky part here is that I want it to do the following:
If viewB is in its upper position so that it is covering the screen, the UITableView content offset is 0 (i.e. we are at the top of the table) and the user makes a pan gesture downwards, the gesture should not interact with the UITableView but should move viewB downwards.
Any other pan gesture in the above condition should be an interaction with the UITableView.
If viewB is in its upper position so that it is covering the screen, the UITableView content offset is NOT at 0 (i.e. we are NOT at the top of the table) and the user makes a pan gesture downwards, the gesture should interact with the UITableView.
I've been very close to achieving this but I can't get it quite right.
Attempts So Far
I'm using a UIPanGestureRecognizer to handle the dragging of the view. I have tried adding this to:
viewB with the UITableView user interaction initially disabled. This allows me to drag viewB up and down without interfering with the UITableView. Once viewB is in its upper position I enable UITableView user interaction which then correctly allows me to interact with the UITableView without moving viewB.
However, by enabling UITableView user interaction, this means touches never reach the UIPanGestureRecognizer, meaning I can never detect for the scenario described in point (1.) above and thus can't re-disable UITableView user interaction to make viewB movable again.
Maybe it is possible to do it this way by overriding the gesture recognition methods used by the UITableView? If this is possible can anyone point me in the right direction?
a new view added in front of the UITableView. I thought maybe I could forward the touch gestures to the UITableView behind it when necessary but I still haven't found a way to do this.
All I have been able to do is disable the gesture recognizer which allows me to interact with the UITableView, but then I have the same issue as above. I can't detect when to re-enable it.
the UITableView within viewB. This seemed to be the most promising way so far. By setting the return values of the following methods I can enable and disable recognition of either viewB and the UITableView.
func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldReceiveTouch touch: UITouch) -> Bool {
if pulloverVC.view.frame.origin.y == bottomNavbarY &&
pulloverVC.tableView?.contentOffset.y == 0 { // need to add gesture direction check to this condition
viewBisAtTop = true
return false // disable pullover control
}
return true // enable pullover control
}
func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWithGestureRecognizer otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if (gestureRecognizer as! UIPanGestureRecognizer).velocityInView(view).y < 0 && viewBisAtTop { // gesture direction check not wanted here
return true // enable tableview control
}
viewBisAtTop = false
return false // disable tableview control
}
The top method is called first when a gesture is made (I have checked with print statements) followed by the bottom method. By making different combinations of true/false for the 2 methods I can alternate interaction between viewB and the UITableView.
To detect whether the user is swiping downwards I am calling velocityInView() on the recognizer (as shown in the bottom method). I was intending on making this check in the top methods if statement and I think this would work, however, although velocityInView() works fine in the bottom method, it does not in the top one (velocity is always 0).
I have scoured SO for some solution and find many similar queries about gesture handling for views that cover each other, but these all seem to be regarding one gesture type, e.g. pinch, on one view, and another type, e.g. pan, on the other. In my case the gesture type is the same for both.
Maybe someone has a clever idea? Or maybe this is actually very simple to do and I have made this incredibly complicated? xD
Managed to get this working.
Of the methods described in my question above I removed the top one keeping just this (it has a few changes):
func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWithGestureRecognizer otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if ((gestureRecognizer as! UIPanGestureRecognizer).velocityInView(view).y < 0
|| pulloverVC.tableView.contentOffset.y > 0)
&& pulloverVC.view.frame.origin.y == bottomNavbarY {
return true // enable tableview control
}
return false
}
The if statement checks that the covering UITableView is in its upper position AND that either the user is is not dragging downwards or the table content is offset (we are not at the top of the table). If this is true, then we return true to enable the tableview.
After this method is called, the standard method implemented to handle my pan gesture is called. In here I have an if statement that sort of checks the opposite to above, and if that's true, it prevents control over the covering viewB from moving:
func handlePanGesture(recognizer: UIPanGestureRecognizer) {
let gestureIsDraggingFromTopToBottom = (recognizer.velocityInView(view).y > 0)
if pulloverVC.view.frame.origin.y != bottomNavbarY || (pulloverVC.view.frame.origin.y == bottomNavbarY && gestureIsDraggingFromTopToBottom && pulloverVC.tableView.contentOffset.y == 0) {
...
This now keeps the UITableView interaction off unless its parent view viewB is in the correct position, and when it is, disables the movement of viewB so that only interaction with the UITableView works.
Then when, we are at the top of the table, and drag downwards, interaction with the UITableView is re-disabled and interaction with its parent view viewB is re-enabled.
A wordy post and answer, but if someone can make sense of what I'm saying, hopefully it will help you.

Can I disable / edit the automatic jump-to-top scroll when tapping status bar?

I'm using an app with a tableView that auto-scrolls downward, so tapping the status bar, which would normally jump to the top of the table, causes problems (it begins scrolling but if the auto-scroll ticks, it stops and leaves it somewhere in the middle).
I'd either like to disable it, or at least have shove some code in when it's tapped to temporarily pause the timer and then resume it when it reaches the top.
Are there any means of achieving either of these things?
UIScrollView (and UITableView as a subclass of it) has scrollsToTop property which defaults to YES, making it scroll to top when status bar is tapped. All you have to do is set it to NO.
However, please keep in mind that iOS users might expect this behavior, so turning it off may not be the best idea from user experience standpoint. You can also leave it as YES and return NO from -scrollViewShouldScrollToTop: delegate method if you only need it disabled at specific times.
Actually, handling it via delegate might be a perfect fit for your case:
- (BOOL)scrollViewShouldScrollToTop:(UIScrollView *)scrollView
{
// disable timer
return YES;
}
- (void)scrollViewDidScrollToTop:(UIScrollView *)scrollView
{
// re-enable timer
}
You can try:
[myView setScrollsToTop:NO];
For swift 5
func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool {
return false
}

Resources