I have a custom titleView with 2 custom UIButtons with arrow images that allow navigation to the next view controller in the paging structure. They work perfectly fine until a button is tapped within the WKWebView. Then they don't work anymore and the selector is not called. Note that other buttons in the nav bar still work (UIBarButtonItems). The buttons work properly again after the user swipes over to the next view controller.
After looking into it some, it looks like a WKCompositingView becomes first responder and if I override becomeFirstResponder() in a WKWebView subclass, the issue goes away. I'm still a little baffled though, and would like to understand the root of the problem.
class NonFirstRespondableWebView: WKWebView {
override func becomeFirstResponder() -> Bool {
return false
}
}
Does anyone have any insight into why this is happening?
Most UI elements in swift have a UIResponder. Unhandled events are passed up the responder chain to enclosing views. My guess is that the WKWebView is absorbing all touch events once the window has become active. You can learn more about the responder chain here
Regarding a first responder. From the docs:
The first responder is usually the first object in a responder chain to receive an event or action message. In most cases, the first responder is a view object that the user selects or activates with the mouse or keyboard.
Assuming you want to keep interactivity with the WKWebView fully functional (e.g. you need to bring up a keyboard or something), you can use
webView.resignFirstResponder()
To resign the responder at any time.
Otherwise, an extension that would give you the same functionality might look something like this:
extension WKWebView {
open override func becomeFirstResponder() -> Bool {
if self.superview?.superview is UIWebView {
return false
} else {
return super.becomeFirstResponder()
}
}
}
Related
Currently i have an issue in editingDidEnd method of swift textfield.
I have a module inside editingDidEnd func to check some validation in B ViewController. If the validation is wrong then it shows popup / alert.
In this case, while user is typing and still focus on the textField, users tap back on navigation bar. it makes editingDidEnd function is also called. So the page is showing A ViewController and also showing pop up.
Is there any workaround to handle this issue? I don't want the alert is showing when i tap back in navigation bar. My expectation is if user press back on navigation. it's not call editingDidEnd function
Thanks Before
e.g.
B View Controller
extension bviewcontroller: textfielddelegate {
func editingDidEnd(_ value: String, textField: SearchTextField) {
//showingalert
}
}
You can add a guard statement to your editingDidEnd method like below.
func editingDidEnd(_ value: String, textField: SearchTextField) {
guard navigationController?.topViewController is bviewcontroller else {
print("DON'T SHOW ALERT")
return
}
print("SHOW ALERT")
}
I have tested the above code with a simple view controller containing a UITextField. However, your delegate method appears to be something different than the standard UITextFieldDelegate's textFieldDidEndEditing(_:) method.
Otherwise I would have suggested that you could also try experimenting with the textFieldDidEndEditing(_:reason:) method.
I have added a UI Button inside of a stack view which is inside of a table view in my storyboard. When I click on my button the correct output is printed in my debugger console but there is no indication in the app that the button has been clicked (no default animation). I have tried looking at my view hierarchy and changing all of the parent views to clip to bounds. Any idea why the button is functioning but not being animated to the user?
The quick fix to your problem is to set delaysContentTouches = false for your table view.
According to the Apple Docs,
If the value of this property is true, the scroll view delays handling the touch-down gesture until it can determine if scrolling is the intent. If the value is false, the scroll view immediately calls touchesShouldBegin(_:with:in:). The default value is true.
See the class description for a fuller discussion.
Alternatively, if you have subclassed the UIScrollView, you can get the same thing done by overriding the following function,
class MyScrollView: UIScrollView {
override func touchesShouldCancel(in view: UIView) -> Bool {
return type(of: view) == UIButton.self
}
}
I'm trying to use a UISplitViewController where the secondary controller should expose a "close" function (via a button or button bar item) whenever the UISplitViewController is in side-by-side mode, but should hide the function at other times. I tried putting this in the secondary view controller:
override func viewWillAppear(_ animated: Bool) {
if splitViewController!.primaryHidden {
// hide the "close" UI artifact
} else {
// show the "close" UI artifact
}
}
This correctly sets the visibility of the "close" function when the secondary view is first displayed, but if the UISplitViewController switches between expanded and collapsed (say, by rotating an iPhone 6s Plus), then this function is not called again (which makes sense, as the secondary controller remains visible). Consequently, the "close" function remains in its initial state--hidden or shown--even as the UISplitViewController changes mode.
How can I get the "close" function to hide or show in reaction to changes in the mode of the UISplitViewController?
There is the UIViewControllerShowDetailTargetDidChangeNotification notification for that:
// Sometimes view controllers that are using showViewController:sender and
// showDetailViewController:sender: will need to know when the split view
// controller environment above it has changed. This notification will be
// posted when that happens (for example, when a split view controller is
// collapsing or expanding). The NSNotification's object will be the view
// controller that caused the change.
UIKIT_EXTERN NSNotificationName const UIViewControllerShowDetailTargetDidChangeNotification NS_AVAILABLE_IOS(8_0);
Use as follows
- (void)viewDidLoad{
[super viewDidLoad];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(showDetailTargetDidChange:) name:UIViewControllerShowDetailTargetDidChangeNotification object:self.splitViewController];
}
- (void)showDetailTargetDidChange:(NSNotification *)notification{
// changed from collapsed to expanded or vice versa
}
This Apple sample demonstrates how the table cell accessory changes from a disclosure indicator in portrait (denoting that a push will happen) to it being removed when changing to landscape split view:
https://developer.apple.com/library/archive/samplecode/AdaptivePhotos/Introduction/Intro.html
Note on iOS 13 beta, use addObserver with object nil because there currently is a bug they send the notification using the wrong object. They use their new UISplitViewControllerPanelImpl from the internal class cluster instead of the UISplitViewController object.
http://www.openradar.appspot.com/radar?id=4956722791710720
For future reference:
What about using the UISplitViewControllerDelegate??
It has a method called
splitViewController:willChangeToDisplayMode:
that should do exactly what you where looking for.
Documentation here
Okay, I found a simple solution. I was making a novice mistake. The trick is to override viewWillLayoutSubviews() instead of viewWillAppear(animated:). Then everything works as I want. It seems that viewWillLayoutSubviews() is called (sometimes more than once) every time the containing UISplitViewController changes its display mode, and that's exactly what I need to respond to. The only gotcha is that splitViewController might be nil on some of those calls, so it needs to be implemented like this:
override func viewWillAppear(_ animated: Bool) {
if let svc = splitViewController {
if svc.primaryHidden {
// hide the "close" UI artifact
} else {
// show the "close" UI artifact
}
}
}
As part of my stumbling around to find a solution, I tried overriding traitCollectionDidChange(previousTraitCollection:). (I tried this because I wanted to react to device rotations.) At first I thought I was onto something, because this function also get called whenever the device rotates. Interestingly (and, frustratingly) I found that my view's splitViewController property was nil when this function is called. It seems odd that this should be so, since neither viewDidDisappear(animated:) nor viewWillAppear(animated:) is called when the UISplitViewController reconfigures itself. But why it should be nil is, I suppose, a question for another day.
I switched my apps one screen from UISearchBar to UISearchController. It's a tableview controller. As per design I should not keep the search bar on UI initially unless it is activated, (Normally it's a common practice to keep search bar as the 'tableHeaderView'). The problem was, I have a search button, when tapped 'search bar' should be activated and become first responder.
When tapped on cancel button, it should be removed from UI. However when I'm tapping on the 'Search Bar Button' on navigation bar, the UISearchController gets activated, providing a dim background but the keyboard doesn't appear. I need to tap one more time on search bar to bring the keyboard upon UI.
Here's my search bar button action:
#IBAction func onTapSearch(_ sender: AnyObject) {
self.view.addSubview(searchController.searchBar)
searchController.isActive = true
searchController.becomeFirstResponder()
isSearchActive = true
self.navigationController?.setToolbarHidden(true, animated: false)
}
I'm configuring the UISearchController in my viewDidLoad method. Let me know if that part code is any of you want to see, however it's usual code. And I verified I'm not calling anywhere resignFirstResponder() method anywhere.
try this,
Just replace this line,
searchController.becomeFirstResponder()
With this below,
searchController.searchBar.becomeFirstResponder()
Edit,
func didPresentSearchController(_ searchController1: UISearchController) {
searchController1.searchBar.becomeFirstResponder()
}
Implement this delegate method and try.
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