I am looking for a way to override what happens when a user does an accessibility scroll (three finger scroll with VoiceOver enabled) to the left or right in a WKWebView on iOS. I tried subclassing WKWebView and overriding goBack(), goForward(), and accessibilityScroll(...). My subclass methods do not get called. I also tried implementing accessibilityScroll(...) in my view controller, but that method did not get called either.
Ideally I want to continue letting WKWebView handle three finger scrolls up and down, but add my own behavior when a user three finger scrolls to the left or right.
You could try capturing the accessibility scroll of the WebViews scrollView
class ViewController: UIViewController {
#IBOutlet weak var webView: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
webView.scrollView.delegate = self
}
}
extension ViewController: UIScrollViewDelegate {
override func accessibilityScroll(_ direction: UIAccessibilityScrollDirection) -> Bool {
// Your code here
}
}
Related
I am developing an app on XCode using Swift 2 and have run into an error with my UI. I have placed a UILabel, UITextView, and UITextField on the View Controller of interest.
Before inputting the UITextField, everything worked fine. I inserted the text field and assigned the proper constraints and when simulating the app, clicking on the text field will cause all of the UI elements to disappear or the alpha to go instantaneously to zero (I'm not sure which of the two is actually happening).
I'm not receiving any feedback via the console and the app itself does not crash.
Here is my code:
//Declare the following UI objects to be manipulated
#IBOutlet weak var labelOneTitle: UILabel!
#IBOutlet weak var textDescription: UITextView!
#IBOutlet weak var fieldInput: UITextField!
override func viewDidLayoutSubviews() {
//Set UI object alphas to zero prior to loading the view
labelOneTitle.alpha = 0
textDescription.alpha = 0
fieldInput.alpha = 0
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override func viewDidAppear(animated: Bool) {
UIView.animateWithDuration(2) { () -> Void in
self.labelOneTitle.alpha = 1
}
UIView.animateWithDuration(3) { () -> Void in
self.textDescription.alpha = 1
}
UIView.animateWithDuration(3) { () -> Void in
self.fieldInput.alpha = 1
}
}
It might be the UIViewController.viewDidLayoutSubviews rechecking all the constraints and reconfiguring the view as per the the class reference:
When the bounds change for a view controller's view, the view adjusts the positions of its subviews and then the system calls this method. However, this method being called does not indicate that the individual layouts of the view's subviews have been adjusted. Each subview is responsible for adjusting its own layout.
Your view controller can override this method to make changes after
the view lays out its subviews. The default implementation of this
method does nothing.
When you click on the UITextField the keyboard appears (Probably) and thus recalling the method to set the alpha of all your UI Elements to 0.
If you'd move setting the alpha's to 0 to the viewDidLoad() than the problem might go away.
I have what I believe to be a standard UITextView in a ViewController which has a substantial amount of text in it, enough that not all of it can fit on the screen. What I would like to happen is that when that view is loaded, the user can start reading at the top of the text and then scroll down the page as they progress through the text. Makes sense, right? What I want is not unrealistic.
The problem is that when the view loads, the text in the UITextView is already scrolled all the way down to the bottom. I have scoured SO and there are a number of similar posts but none of the solutions therein are resolving my problem. Here is the code in he view controller:
import UIKit
class WelcomeTextVC: UIViewController {
var textString: String = ""
#IBOutlet weak var welcomeText: UITextView!
override func viewDidLoad() {
super.viewDidLoad()
self.navigationController?.navigationBar.translucent = false
self.welcomeText.text = textString
self.welcomeText.scrollRangeToVisible(NSMakeRange(0, 0))
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(true)
self.welcomeText.scrollRangeToVisible(NSMakeRange(0, 0))
welcomeText.setContentOffset(CGPointMake(0, -self.welcomeText.contentInset.top), animated: true)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
I have tried most of the standard solutions to no avail. The one common suggestion which I have not tried is to "Uncheck the Adjust Scroll View Insets in the Attributes Inspector". The reason I have not tried it is because I cannot locate this fabled check box.
What do I need to do to make the text start out aligned to the top?
There's a couple ways I know of. Both ways are implemented programmatically through the viewDidLayoutSubviews() method in your view controller. After the call to super.viewDidLayoutSubviews(), you could add:
myTextView.scrollRangeToVisible(NSMakeRange(0, 1))
This would automatically scroll the textView to the first character in the textView. That however might add some unwanted animation when the view appears. The second way would be by adding:
myTextView.setContentOffset(CGPoint.zero, animated: false)
This scrolls the UITextView to point zero (the beginning) and gives you control over whether you want it animated or not.
A better Solution is to call the textView.setContentOffset(.zero, animated: false) inside the viewWillAppear(_ animated: Bool) lifecycle method instead of in the viewDidLayoutSubviews() method. Just override the default implementation in your custom UIViewController subclass:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
textView.setContentOffset(.zero, animated: false)
}
This will scroll to the top of the UITextView only when the view appears and not always when the layout changes (which happens more often as you might think).
If you only want that the UITextView scrolls to the top once and not every time the view appears, you can add a flag. This is really helpful if your UITextView is inside a UINavigationController and the user can push another UIViewController on top of it. After the user returns to the UITextView it keeps the scroll position of the UITextField and does not reset the position to the top:
private var didAppearOnce = false
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if !didAppearOnce {
didAppearOnce = true
textView.setContentOffset(.zero, animated: false)
}
}
I have two views; a UIView placed on top of a UITableView. I need to know when the UIView has been panned, so I’ve placed a UIPanGestureRecognizer on it. However, this creates a UI which seems buggy because you expect the UITableView behind it to move as your finger does.
So it seems I need to somehow pair up this pan gesture with making the table view behind it move, at least, until this covering view disappears.
How do I pair up a pan gesture to move a UIScrollView?
Note: If you’re wondering about the cover view, it’s actually a UIImageView which has a snapshot of the table view with a filter applied to it for UI reasons.
When this view is panned, it disappears. So from the user’s point of view, while they begin dragging the cover view, I want them to keep thinking they are dragging the table view as the cover disappears.
Assuming you have a view controller with a table view setup like the following:
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, UIGestureRecognizerDelegate
{
#IBOutlet var tableView: UITableView!
#IBOutlet var scrollView: UIScrollView!
You can add a couple of gesture recognizers:
var gestureRecognizer1: UIPanGestureRecognizer = UIPanGestureRecognizer()
var gestureRecognizer2: UIPanGestureRecognizer = UIPanGestureRecognizer()
Setup your delegates and add the gesture recognizers to the scroll views:
override func viewDidLoad()
{
super.viewDidLoad()
scrollView.contentSize = CGSizeMake( ) // <-- Set your content size.
scrollView.delegate = self
tableView.delegate = self
gestureRecognizer1.delegate = self
gestureRecognizer2.delegate = self
tableView.addGestureRecognizer(gestureRecognizer1)
scrollView.addGestureRecognizer(gestureRecognizer2)
}
Implement a delegate method for UIGestureRecognizerDelegate to recognize simultaneous gestures:
func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWithGestureRecognizer otherGestureRecognizer: UIGestureRecognizer) -> Bool
{
return true
}
Implement a delegate method for UIScrollViewDelegate:
func scrollViewDidScroll(scrollView: UIScrollView)
{
tableView.contentOffset = scrollView.contentOffset
}
}
The movement of the tableView will be synchronized to the scroll view.
In the end I’ve decided to directly change the cell’s images with a CIFilter rather than take a snapshot. This way is the way I should have done it anyway, it’s much cleaner and uses no possibly-buggy “workarounds”.
I have a UITableViewController, and I'd like to make it not flash the vertical scroll bar when I go back from a push action segue on one of it's cells (popping the view controller and going back to the UITableViewController).
It seems that, if the table has many rows (mine has around 20 with 60 points height each, so bigger than the screen), when I go back, it always flashes the vertical scroll bar once to show where it is in the table. However, I don't want that to happen, but I do want to keep the scrollbar around so it shows when the user scrolls. Therefore, disabling it completely is not an option.
Is this default behavior and can I disable it temporarily?
There is a simpler solution that doesn't require avoiding using a UITableViewController subclass.
You can override viewDidAppear: as stated by http://stackoverflow.com/users/2445863/yonosoytu, but there is no need to refrain from calling [super viewDidAppear:animated]. Simply disable the vertical scrolling indicator before doing so, and then enable it back afterwards.
- (void)viewDidAppear:(BOOL)animated {
self.tableView.showsVerticalScrollIndicator = NO;
[super viewDidAppear:animated];
self.tableView.showsVerticalScrollIndicator = YES;
}
If you're using Interface Builder, you can disable the Shows Vertical Indicator option on the tableView for your UIViewController and enable it in code as shown above.
To get Cezar's answer to work for iOS10 I had to include a (sizeable) delay before re-enabling the scroll indicator. This looks a bit strange if someone tries to scroll before the second is up, so you can re-enable the scroll indicator as soon as someone scrolls.
override func viewDidAppear(_ animated: Bool) {
tableView.showsVerticalScrollIndicator = false
super.viewDidAppear(animated)
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.tableView.showsVerticalScrollIndicator = true
}
}
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
if !tableView.showsVerticalScrollIndicator {
tableView.showsVerticalScrollIndicator = true
}
}
Actually, on thinking about it, you don't even need the delay, just do this:
override func viewDidAppear(_ animated: Bool) {
tableView.showsVerticalScrollIndicator = false
super.viewDidAppear(animated)
}
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
if !tableView.showsVerticalScrollIndicator {
tableView.showsVerticalScrollIndicator = true
}
}
Update: Please, look at Cezar’s answer below, which gives a nice workaround without any of the drawbacks of my proposals.
According to the documentation it is a behaviour of UITableViewController:
When the table view has appeared, the controller flashes the table view’s scroll indicators. The UITableViewController class implements this in the superclass method viewDidAppear:.
So I think you have two options:
You can avoid using UITableViewController and start using a naked UIViewController. Rebuilding the functionality of UITableViewController from UIViewController is not that hard (you can follow this old article as reference).
Override viewDidAppear: and don’t call [super viewDidAppear:animated]. The problem here is that you don’t know what else does UITableViewController do when viewDidAppear: is called, so you might break something.
What I'm trying to do is to create something similar to the "find on page" search function in Safari on iPad.
I'm using a UIToolbar with some items in it and attached it to the keyboard by setting it as an inputAccessoryView on the UITextField. Works like a charm, but there is one thing I can't figure out. In Safari, when you search for something, the keyboard disappears but the tool bar remains on the bottom of the screen.
Does anyone have a clue on how to accomplish this? The only solution I can think of is to respond to a keyboard dismissed event and then pull out the UIToolBar and create a custom animation that moves it to the bottom of the screen. But this is hacky. I am looking for a more elegant solution. Something that can make me decide what to do with the input accessory view when the keyboard gets dismissed.
It's done like this:
Assign your UIToolbar to a property in your view controller:
#property (strong, nonatomic) UIToolbar *inputAccessoryToolbar;
In your top view controller, add these methods:
- (BOOL)canBecomeFirstResponder{
return YES;
}
- (UIView *)inputAccessoryView{
return self.inputAccessoryToolbar;
}
And then (optionally, as it usually shouldn't be necessary), whenever the keyboard gets hidden, just call:
[self becomeFirstResponder];
That way, your inputAccessoryToolbar will be both your view controller's and your text view's input accessory view.
I've ended up with UIToolBar that is not assigned as input accessory view, and slide up and down on UIKeyboardWillShowNotification / UIKeyboardWillHideNotification
Update to Swift 4, based on prior answers. If you add toolbar via storyboards you can do this
class ViewController: UIViewController {
#IBOutlet weak var textField: UITextField!
#IBOutlet var toolbar: UIToolbar!
override var canBecomeFirstResponder: Bool {
get {
return true
}
}
override var inputAccessoryView: UIView {
get {
return self.toolbar
}
}
override func viewDidLoad() {
super.viewDidLoad()
textField.inputAccessoryView = toolbar
}
}
In this case, whenever text field resigns first responder, it defaults first responder to main view. Keep in mind, you might want to explicitly resign first responder, and set main view as first responder if there are multiple UI elements and first responder defaults to undesired view after resignation.
Adding to #arik's answer, here is the Swift version:
class ViewController: UIViewController {
#IBOutlet var textField: UITextField!
// Input Accessory View
private var inputAccessoryToolbar: UIToolBar?
override func canBecomeFirstResponder() -> Bool {
return true
}
override var inputAccessoryView: UIView? {
return inputAccessoryToolbar
}
override func viewDidLoad() {
super.viewDidLoad()
inputAccessoryToolbar = UIToolbar(frame: CGRectMake(0, 0, view.frame.size.width, 50))
textField.inputAccessoryView = inputAccessoryToolbar
}
// UITextFieldDelegate
func textFieldShouldReturn(textField: UITextField) -> Bool {
becomeFirstResponder()
return true
}
}
Thanks for the clean solution!
You may also need to work around the bug with the inputAccessoryView not respecting the safe area margins and thus not making room for home indicator thing on iPhone X: iPhone X how to handle View Controller inputAccessoryView?
I found the easiest solution when you have a UIToolbar from a xib and you are also using that UIToolbar as the inputAccessoryView of a text field is to embed the toolbar in a UIView when you return it from your overridden inputAccessoryView, and make the containing UIView taller by the safeAreaInsets.bottom. (Other solutions suggest constraining the bottom of the toolbar to the safe area in a subclass, but this leads to constraint conflicts and also means the area under the toolbar is the wrong colour.) However, you have to also bear in mind that the text field can have focus even when there is no keyboard on the screen (for instance if there is an external keyboard), so you need to change the inputAccessoryView of the text view to this toolbar-within-a-UIView in that case as well. In fact it will probably make things simpler to just always use the containing view and adjust the size of it appropriately. Anyway, here's my override of inputAccessoryView:
override var inputAccessoryView: UIView? {
if toolbarContainerView == nil {
let frame=CGRect(x: toolBar.frame.minX, y: toolBar.frame.minY, width: toolbar.frame.width, height: toolBar.frame.height+view.safeAreaInsets.bottom)
toolbarContainerView = UIView(frame: frame)
}
if (toolbar.superview != toolbarContainerView) {
//this is set to false when the toolbar is used above the keyboard without the container view
//we need to set it to true again or else the toolbar will appear at the very top of the window instead of the bottom if the keyboard has previously been shown.
toolbar.translatesAutoresizingMaskIntoConstraints=true
toolbarContainerView?.addSubview(toolbar)
}
return toolbarContainerView
}
It would probably be a good idea to override viewSafeAreaInsetsDidChange to adjust the size of toolbarContainerView in that case, too.