How to restart UIRefreshControl animation? - ios

I have several tableviews in a tab bar controller. On some of these I have set a UIRefreshControl.
After swiping down to start a refresh the 'spinner' animation begins. If I select a different tab and then move back to the original tab, the refresh control is still shown but the animation has stopped.
How do I restart this animation?
I've tried starting the refresh again and got no results.
I also inspected the view hierarchy, hoping to find a UIActivityIndicatorView but could only find a _UIRefreshControlModernReplicatorView that seems to be the view doing the animation. I don't know of any method I can call of this to get the animation restarted.

I had a similar problem, although not exactly the same problem as described in the original question.
The issue: When I moved from a screen with a tableView with refresh control (not a UITableViewController) to a different one, then I found when I returned to the screen, the activity indicator portion of the refresh control was displaying (not animating) as an artifact just under the top of the table view.
I tried a lot of ideas (hiding the refresh control, ensuring endRefreshing was called, etc.) until I found something that actually worked:
In viewWillAppear(animated:), add the following:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
tableView.refreshControl = nil
tableView.refreshControl = refresher
}
Just adding that code fixed the issue for me. Your mileage may vary, but perhaps somebody will find this useful.

Related

How to prevent automatic scrolling of UICollectionView by VoiceOver?

I have a horizontally scrolled UICollectionView with a title label above it and a UIPageControl below it.
UILabel
UICollectionView
UIPageControl
When I turn on the VoiceOver accessibility feature and start traversing the screen sequentially, the collection view scrolls to the beginning or end automatically. Making the page jump off suddenly. For example, if I scroll to the 2nd page using page control, and move back to the collection view, it shows and reads the last page unexpectedly. Since I'm using the page control for navigation in the accessibility mode, I'd like to prevent the automatic scrolling.
How do I prevent or counter that?
I found an issue that seems to describe the same problem, but there's no workaround suggestion: iOS 8.4: Scroll view resets contentOffset with Voice Over enabled shortly after view appear
I encountered it on iOS 13.4.1 iPhone 11 Pro
UIScrollViewDelegate.scrollViewDidScroll(_:)
A change in the accessibility focus that triggers an automatic scrolling also triggers a call to scrollViewDidScroll(_:) in your UIScrollViewDelegate. Use that to counter the automatic scrolling effect, f.i. by setting contentOffset the way you prefer it.
You may need to detect that the scrolling was actually triggered by accessibility features, and not the user dragging or pinching. UIAccessibility.isVoiceOverRunning and UIAccessibilityFocus.accessibilityElementDidBecomeFocused() are your friends here. Beware that changing contentOffset (or zoomScale or whatever is needed) may trigger another call to scrollViewDidScroll(_:), so you need to prevent an infinite recursion.
Using #pommy's suggestions, I was able to fix my similar issue. In the code I was working on, the most appropriate place to make the change ended up being CalendarCollectionView.setContentOffset(_:animated:), where CalendarCollectionView is a UICollectionView subclass. Specifically, it's a JTACMonthView subclass, but that should not be of any relevance to this answer.
From the name, you can see my use case: a calendar which shows a month at a time. It could have many months both into the future and the past, but the usual user focus is likely to start somewhere in the middle.
Like the OP, I found that swiping from an outer element to the collection view with VoiceOver enabled caused the focus to go to the first date in the calendar, in my case 1st January 1951 (Date.farPast, I believe.) An interesting aside: Switch Control navigation did not cause the same behaviour.
The underlying behaviour was that contentOffset was getting set to 0.0 in the dimension that the collection view scrolls. In my code, that direction is held in style, and changes based on configuration, but in most applications it's likely to be fixed.
My code simply blocks any offset changes to 0.0 when VoiceOver is enabled. This is pretty naïve, and won't be suitable for all apps, but gives a concrete example which I hope will help some others!
override func setContentOffset(_ contentOffset: CGPoint, animated: Bool) {
if shouldPreventAccessibilityFocusScrollback(for: contentOffset) {
return
}
super.setContentOffset(contentOffset, animated: animated)
}
func shouldPreventAccessibilityFocusScrollback(for newContentOffset: CGPoint) -> Bool {
if UIAccessibility.isVoiceOverRunning {
switch style {
case .horizontal:
return newContentOffset.x == 0
case .vertical:
return newContentOffset.y == 0
}
}
return false
}
I spent quite a long time trying to determine when UIAccessibilityFocus moved from something outside the collection view, to something inside the collection view, which is ideally the only time we want to block these automatic scrolls. I was unsuccessful, but I think that was mostly due to subclassing a third party collection view (the calendar). There's definitely more merit to that approach, if you can get it to work... but it will require some careful management of state.

UIRefreshControl is flickering and transitions are abrupt

I'd like to set up a UIRefreshControl in my apps' main table view controller to trigger a refresh function to (you probably guessed it by now) refresh it.
Adding it isn't the problem, I'm able to set it up through the storyboard (by enabling refreshing) or in the controller (by using self.refreshControl = UIRefreshControl()).
When it gets triggered by the user it starts refreshing normally, but when I trigger it through self.refreshControl.beginRefreshing() the control isn't displayed in the navigation bar, until I scroll down (like you would normally trigger it)
But that's not even the weirdest part. When it's finally visible to the user there is a constant flicker to it and after a while it's not visible at all.
For the self.refreshControl.endRefreshing() method, the animation is really abrupt any choppy and sometimes the navigation bar doesn't scroll up again (leaving an empty space at the top)
Here is a gif, that should summarise my problem
I use swift 4.2, the application was tested on different simulators (running iOS 12.1) and my iPhone X (running iOS 12.1.2) and the release target is iOS 10.0.
I've already done research on my problem, but no one seems to have the exact issue (at least the flickering) or the solutions don't work for me.
This is how my view controller is set up:
class MyViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.refreshControl = UIRefreshControl()
self.refreshControl?.addTarget(self, action: #selector(reload), for: .valueChanged)
self.refreshControl.beginRefreshing()
}
#objc func reload() {
DispatchQueue(label: "update").async {
sleep(10)
DispatchQueue.main.async {
self.refreshControl?.endRefreshing()
}
}
}
}
Thanks for reading, I really hope someone can help me.
Update
I just discovered that the issue only occurs with a non-translucent navigation bar with large title, so picking a translucent navigation bar instead of the opaque is a solution (sadly not one I would like to live with). Maybe someone has an approach to having a functional refresh control with an opaque navigation bar.
Additionally I submitted a bug report to Apple, because something like that should not happen in the first place, I'll update this post when they get back to me.

Prevent keyboard from dismissing while changing views

On apps like say the stock messages app, if you’re in a conversation with the keyboard showing and swipe to go back to your conversation list, the keyboard remains up as the view gets swiped away.
I can’t seem to figure out how to mimic this behavior but I’ve seen it in other apps so it’s gotta be possible.
Edit: Not sure I understand why this is getting downvotes. It’s definitely a valid question where I couldn’t find the answer on google?
Edit 2: Here’s a video of what I’m trying to accomplish https://arxius.io/v/a555c8db compared to this behavior in discord https://arxius.io/v/0bfda09a
On apps like say the stock messages app, if you’re in a conversation with the keyboard showing and swipe to go back to your conversation list, the keyboard remains up as the view gets swiped away.
I don't actually see that behavior, at least in the Messages app on an iPhone. That said, the keyboard should remain visible whenever the first responder can accept text. If you want to switch to a different view controller and keep the keyboard visible, then make sure that first responder in the new view graph is editable. For example, if there's a text field, you could set it to be the first responder.
So before we talk about any kind of solution, let's learn exactly why the keyboard is dismissed.
As you know, iOS UI works on a view controller based system, with a text field ultimately being managed by a view controller somewhere.
When you click the back button, the active view controller is released from the navigation stack and therefore the system deduces that the text field is no longer in use, so it resigns it as the first responder.
Unfortunately there is no built in way to alter this behaviour and whilst we can speculate how apple might do it, we do not know.
The few options we have are not particularly neat or tidy, but a method I have had success with in the past is creating a hidden text field directly on the window before you pop the view controller and setting it first responder. Then on your other view controller, the text field can take first responder from the invisible one and remove it.
This is not ideal, but it's the only technique that comes to mind.
It is correct that you had to have UITextFiled available for both views to have keyboard passed between them consistently. But nobody said that this textfield had to be on screen, or had to be part of ViewController's view.
So all you had to do is to place this textFiled somewhere out of screen, for example as subview of your window, or NavigationController's view, make it first responder, and switch to your text field on view didAppear:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.textField.becomeFirstResponder()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
let fixedView = self.view.window
let fakeTextField = fixedView?.subviews.last as? UITextField ?? UITextField()
fakeTextField.autocapitalizationType = textField.autocapitalizationType
fakeTextField.autocorrectionType = textField.autocorrectionType
fakeTextField.center = CGPoint(x: -100, y: -100)
fixedView?.addSubview(fakeTextField)
fakeTextField.becomeFirstResponder()
}
Make sure that keyboard configuration is the same, otherwise you will have it switching.

PageViewController scroll bug (over scrolls)

I was following the tutorial here and had to make a few changes to fit my project (code of my variation here, sorry I put it on GitHub because it's a bit lengthy to copy and paste). Sometimes when I swipe right to get to the last VC, it automatically closes on sight. Other times it works properly where if I swipe to the last VC it stays there until I swipe right again to close it. I've placed print statements in the code to help me find where the bug is. Here are the print statements when it works and when it doesn't work. Looking at the logs, since the exact same code is accessed in the same order, I'm not sure how to fix this bug. This problem doesn't occur when the transition style is PageCurl instead of Scroll.
First you need to know how pageViewControllers work.
After the first page is loaded, the moment you start to scroll it will try to load the next page. this is just for the first page. after that as soon as you reach a page it will preload the next page (depending on the direction) and have it ready.
In your case when you reach page 5 the pageViewController will try to preload the next page which leads to this part of your code
guard orderedViewControllersCount != nextIndex else {
print("last item in coachamrks going to dismiss")
self.dismissViewControllerAnimated(true, completion: nil)
return orderedViewControllers.first
}
and it will dismiss the viewController as soon as you reach page 5.
This is not a bug. It's the default behavior of pageViewController.
I think the reason that you sometimes find it behaving the way you wrongly expected is that when you scroll through the pages fast enough the pageViewController stops preloading pages for performance reasons.
The only way you could achieve what you want is a hackish way of accessing the scrollView inside of the pageViewController but it is not safe and might break your app if Apple changes the architecture of pageViewControllers in a future update.
I suggest you put a button in the last page and use it to dismiss the pageViewController.

Delay when using instantiateViewControllerWithIdentifier but not performSegueWithIdentifier?

The code below is used to push another view controller onto the navigation stack.
When using instantiateViewControllerWithIdentifier, the segue is noticeably sluggish the first time (~3 seconds) but occurs reasonably fast each subsequent time. Other SO posts suggested ensuring the segue occurs on the main thread, which the code accomplishes, but this didn't fix the problem.
However, using performSegueWithIdentifier causes no delay.
The viewDidLoad code for SendViewController is the same for the first and subsequent pushes.
Tried blanking out viewDidLoad for the destination view controller, but still the lag exists for instantiateViewControllerWithIdentifier but not for performSegueWithIdentifier.
How to fix the delay with instantiateViewControllerWithIdentifier?
No delay:
#IBAction func buttonTapped(sender: UIButton) {
performSegueWithIdentifier(SendSegue, sender: self)
}
Results in delay when showing SendViewController for first time:
#IBAction func buttonTapped(sender: UIButton) {
dispatch_async(dispatch_get_main_queue()) {
let vc = self.storyboard!.instantiateViewControllerWithIdentifier(self.SendViewControllerID) as! SendViewController
self.navigationController!.pushViewController(vc, animated: true)
}
}
This issue could occur in many different scenarios. The best way determine what is causing your specific problem is by profiling with the instruments included in Xcode.
Click and hold the Build button in your xcode window. You will see four options appear, select Profile.
Once the build runs a window with instruments will pop up. Select, Time Profiling from the options.
A new window will appear with various metrics in it. The top left corner will have a red record button. Click the red record button and this will launch the app on your phone.
Proceed to the transition giving you problems. End the recording after the transition occurs by selecting the same button you started the recording with.
Review the "Details" pane in the bottom left corner. You will see a column titled "Running time" that shows the time it took to execute every method in your code (both OS methods and user generated code)
Determine if anything is out of place or occurs that is not intended. Possibly go back and execute the transition again to compare the difference between the two. Clicking the function in the list will take you directly to the code being executed. This can be very helpful.
It is very likely that if a transition takes 3-5 seconds one particular function will be obvious when following these steps. Happy profiling!
WWDC from last year has a great segment on this as well. Def worth checking out here: (open in Safari only) WWDC Profiling Talk
The problem was isolated to the presence of a UITextField in the destination view controller, that is, removing the UITextField removes the lag.
Then it was further isolated to the presence of a custom font.
In other words, using the system font on the UITextField, rather than a custom font, removes the lag. No explanation why, but it works.
After time profiling I realized it was the call to instantiateViewController which I couldn't find anything that could help me with that.
Unfortunately, the only thing that worked was either using a separate storyboard for that view controller and instantiating it from there, or redoing the view controller programmatically.

Resources