I have an inputAccessoryView for text input in a chat app. I implemented the inputAccessoryView using the following:
override var inputAccessoryView: UIView? {
get {
setupInputToolbar()
return inputToolbar
}
}
override var canBecomeFirstResponder: Bool {
return true
}
This occurs in a viewController which is a childViewController. I use a segmented control to display this chat viewController with the inputAccessoryView.
The problem I'm having is the inputAccessoryView will only display if I place self.becomeFirstResponder() in the viewDidAppear() of the chat viewController (child).
If I omit self.becomeFirstResponder() or place it in viewDidLoad() or viewWillAppear(), the inputAccessoryView does not display.
The problem with having it in viewDidAppear() is that it displays with an animation after the view is already on screen which is not what I want.
I accomplished this by first adding a viewHasPerformedSubviewLayoutAtLeastOnce property to the ViewController.
private var viewHasPerformedSubviewLayoutAtLeastOnce = false
Making canBecomeFirstResponder dependent on the above property.
override var canBecomeFirstResponder: Bool {
return viewHasPerformedSubviewLayoutAtLeastOnce
}
Then updating that property in viewDidLayoutSubviews() and becoming first responder inside a UIView.performWithoutAnimation block.
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if viewHasPerformedSubviewLayoutAtLeastOnce == false {
viewHasPerformedSubviewLayoutAtLeastOnce = true
UIView.performWithoutAnimation { becomeFirstResponder() }
}
}
It all feels a little hacky, but it results in the view appearing without any animations.
Related
I have added a collectionView on top of a UITabBar but its touch is not working.The screeshot for the tabBar and collectionView
The code is attached below, I want the collectionView to be touchable. Here quickAccessView is the UIView that contains the collectionView. For constraints I'm using snapKit
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
self.tabBar.bringSubviewToFront(quickAccessView)
}
private func setupQuickAccessView(){
print("this is tabBar's height", self.tabBar.frame.size.height)
self.tabBar.frame.size.height = 150
print("this is new tabBar's height", self.tabBar.frame.size.height)
self.tabBar.addSubview(quickAccessView)
quickAccessView.clipsToBounds = true
}
private func addQuickAccessViewConstraints(){
quickAccessView.snp.makeConstraints { make in
make.right.left.equalTo(self.tabBar.safeAreaLayoutGuide)
make.height.equalTo(76)
make.bottom.equalTo(self.tabBar.snp.bottom).offset(-80)
}
}
this is after modification that Aman told
The UITabBarController
final class MainTabBarController: UITabBarController {
private lazy var quickAccessView: QuickAccessView = .fromNib()
var quickAccessSupportedTabBar: QuickAccessSupportedTabBar {
self.tabBar as! QuickAccessSupportedTabBar // Even code is crashing here
}
// Even code is crashing here
override func viewDidLoad() {
super.viewDidLoad()
self.tabBar.backgroundColor = .white
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.view.frame = self.quickAccessView.bounds
setupUI()
}
}
extension MainTabBarController{
private func setupUI(){
setupQuickAcessView()
addQuickAcessViewConstraints()
}
}
// MARK: - Setting Up Quick Access view
extension MainTabBarController {
private func setupQuickAcessView(){
self.quickAccessSupportedTabBar.addSubview(quickAccessView)
}
private func addQuickAcessViewConstraints(){
quickAccessView.snp.makeConstraints { make in
make.left.right.equalTo(self.quickAccessSupportedTabBar.safeAreaLayoutGuide)
make.height.equalTo(66)
make.bottom.equalTo(self.quickAccessSupportedTabBar.snp.top)
}
}
}
the UItabBar and here it is throwing error and I too am confuse that how to access it and convert it to points
class QuickAccessSupportedTabBar: UITabBar {
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
// if `quickAccessView` is visible, then convert `point` to its coordinate-system
// and check if it is within its bounds; if it is, then ask `quickAccessView`
// to perform the hit-test; you may skip the `isHidden` check, in-case this view
// is always present in your app; I'm assuming based on your screenshot that
// the user can dismiss / hide the `quickAccessView` using the cross icon
if !quickAccessView.isHidden {
// Convert the point to the target view's coordinate system.
// The target view isn't necessarily the immediate subview
let targetPoint = quickAccessView.convert(point, from: self)
if quickAccessView.bounds.contains(targetPoint) {
// The target view may have its view hierarchy, so call its
// hitTest method to return the right hit-test view
return quickAccessView.hitTest(targetPoint, with: event)
}
}
// else execute tabbar's default implementation
return super.hitTest(point, with: event)
}
}
I think what may be happening here is that since you've added quickAccessView as tab bar's subview, it is not accepting touches. This would be so because the tabbar's hist test will fail in this scenario.
To get around this, instead of using a UITabBar, subclass UITabBar, let us call it ToastyTabBar for reference. See the code below:
class ToastyTabBar: UITabBar {
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
// if `quickAccessView` is visible, then convert `point` to its coordinate-system
// and check if it is within its bounds; if it is, then ask `quickAccessView`
// to perform the hit-test; you may skip the `isHidden` check, in-case this view
// is always present in your app; I'm assuming based on your screenshot that
// the user can dismiss / hide the `quickAccessView` using the cross icon
if !quickAccessView.isHidden {
// Convert the point to the target view's coordinate system.
// The target view isn't necessarily the immediate subview
let targetPoint = quickAccessView.convert(point, from: self)
if quickAccessView.bounds.contains(targetPoint) {
// The target view may have its view hierarchy, so call its
// hitTest method to return the right hit-test view
return quickAccessView.hitTest(targetPoint, with: event)
}
}
// else execute tabbar's default implementation
return super.hitTest(point, with: event)
}
}
Set this as the class of your tabBar (both in the storyboard and the swift file), and then see if that solves it for you.
You'll have to figure out a way to make quickAccessView accessible to the tabbar for the hit test check. I haven't advised on that above because I'm not familiar with your class hierarchy, and how and where you set stuff up, but this should be trivial.
If this solves it for you, please consider marking this as the answer, and if it does not then please share a little more info here about where you're having the problem.
Edit (for someone using a UITabBarController):
In response to your comment about "how to access UITabBar class from UITabBarController" here's how I would go about it.
I'm assuming you have a storyboard with the UITabBarController.
The first step (ignore this step if you already have a UITabBarController custom subclass) is that you need to subclass UITabBarController. Let us call this class ToastyTabBarController for reference. Set this class on the UITabBarController in your storyboard using the identity inspector pane in xcode.
The second step is to set the class of the UITabBar in your storyboard as ToastyTabBar (feel free to name it something more 'professional' 😊).
This is to be done in the same storyboard, in your UITabBarController scene itself. It will show the tabBar under your UITabBarController, and you can set the custom class on it using the identity inspector pane just like earlier.
The next step is to expose a computed property on your custom UITabBarController class, as shown below.
var toastyTabBar: ToastyTabBar {
self.tabBar as! ToastyTabBar
}
And that's it. Now you have a property on your UITabBarController subclass which is of ToastyTabBar type and you can use this new property, toastyTabBar, however you require.
Hope this helps.
when launching the iOS share extension the textView will by default already be selected / entered. (the keyboard will be visible and the textView will be in edit mode)
I don't want this to happen, how do I programatically exit the textView
override func viewDidLoad() {
self.textView.exit() // obviously doesn't work
}
I see tons of posts about how to exit when user press enter on the keyboard, I do not want to do it "when something delegate" I just want the textview to not be in edit mode when the extension is launched (on viewDidLoad).
I have also tried (as suggested in other post)
self.textView.endEditing(true)
which did not hide the keyboard or exit the textView
You can call textView.resignFirstResponder() in presentationAnimationDidFinish
class ShareViewController: SLComposeServiceViewController {
var textViewTintColor: UIColor?
override func viewDidLoad() {
super.viewDidLoad()
// hide cursor which appears during presentation animation
self.textViewTintColor = self.textView.tintColor
self.textView.tintColor = .clear
}
override func presentationAnimationDidFinish() {
super.presentationAnimationDidFinish()
guard let tintColor = self.textViewTintColor else { return }
self.textView.resignFirstResponder()
// reset cursor
self.textView.tintColor = tintColor
self.textViewTintColor = nil
}
}
I have a view controller that takes up the whole screen from top to bottom. I would like to hide the home bar indicator on the bottom of the screen on iPhone X devices.
How can I do this in iOS 11?
You should override prefersHomeIndicatorAutoHidden in your view controller to achieve that:
override var prefersHomeIndicatorAutoHidden: Bool {
return true
}
There is another alternative. If you are looking for the behavior where the indicator dims, then when the user swipes up it activates, and when they swipe up again the home action is invoked (I.E., two swipes are needed to invoke), then the answer is here: iPhone X home indicator behavior. The short of it is to override on your UIViewController:
override var preferredScreenEdgesDeferringSystemGestures: UIRectEdge {
return UIRectEdge.bottom
}
prefersHomeIndicatorAutoHidden only hides the indicator, but will not suppress the gesture.
And you will get what you want (If I understand your comments correctly - your question and the selected answer seem to imply the other answer).
If your window?.rootViewController is a UITabBarController or UINavigationController, just inherit it and add two function as follows,
override var prefersHomeIndicatorAutoHidden: Bool {
return true
}
//#available(iOS 11, *)
override var childViewControllerForHomeIndicatorAutoHidden: UIViewController? {
return nil
}
Implement -(BOOL)prefersHomeIndicatorAutoHidden in your UIViewController and return YES.
Read more https://developer.apple.com/documentation/uikit/uiviewcontroller/2887510-prefershomeindicatorautohidden.
I tried to set it and return true only when I am in full-screen :
override var prefersHomeIndicatorAutoHidden: Bool { isNavigationBarAndTabBarHidden }
but it doesn't seem to work... isNavigationBarAndTabBarHidden is a custom variable tied to my fullscreen extension.
Edit: We need to call setNeedsUpdateOfHomeIndicatorAutoHidden every time we update prefersHomeIndicatorAutoHidden's value.
var isNavigationBarAndTabBarHidden = false {
didSet {
setNeedsUpdateOfHomeIndicatorAutoHidden()
}
}
override func prefersHomeIndicatorAutoHidden() -> Bool {
return true
}
I suppose you can add this method in your AppDelegate for hide home indicator on all of your ViewControllers.
I am trying to implement a textView that floats about the keyboard (like the iMessage app) I want to use the inputAccessoryViewController so that I can have view controller, and not just a view by overriding inputAccessoryView. I have written the code below but it has a problem in iOS 9 where the textView disappears when you tap and the textView on the keyboard pops up. It works fine in iOS 10.
class ViewController: UIViewController {
var textEntryController = TextEntryController.fromNIB()
override var inputAccessoryViewController: UIInputViewController? {
return self.textEntryController
}
override var canBecomeFirstResponder: Bool {
return true
}
}
I was trying to figure out what went wrong so I did override both inputAccessoryViewController and inputAccessoryView and put break points in each method.
override var inputAccessoryViewController: UIInputViewController? {
return self.textEntryController //break point
}
override var inputAccessoryView: UIView? {
return self.textEntryController.view //break point
}
This fixes the problem of the textview disappearing on iOS 9, but I don't think that it is right. This probably breaks the view controller hierarchy.
Observations:
On both iOS 9 and iOS 10 both methods get called when when I start the app
On iOS 9 ONLY theinputAccessoryView gets called when I tap on the textfield.
On iOS 10 BOTH the inputAccessoryView and inputAccessoryViewController get called when I tap on the textview.
What is the proper way to override inputAccessoryViewController? I don't think I should have to override both inputAccessoryView and inputAccessoryViewController.
I was facing a similar issue and in my case the inputAccessoryViewController and canBecomeFirstResponder override methods were not being called. It was because the ViewController was not a subclass of UIViewController.
I have a view which I set up as input accessory view for view controller the following way:
#IBOutlet private weak var bottomPane: UIView!
override func canBecomeFirstResponder() -> Bool {
return true
}
override var inputAccessoryView: UIView {
return bottomPane
}
Everything works just fine until I try to view YouTube video in fullscreen mode (video is loaded in UIWebView). When video enters fullscreen mode, keyboard and my input accessory view disappear (which is normal, I guess), but when I exit fullscreen mode, they do not appear. If I keep the reference to bottomPane weak, it becomes nil and application crashes, if I change it to strong, input accessory view remains hidden until the keyboard appears next time.
Can anybody explain what's going on and how to fix this?
Here's what's going on.
When user interacts with UIWebView, it becomes first responder and inputAccessoryView provided by view controller disappears (no idea why behavior in this case is different from, say, UITextField). Subclassing UIWebView and overriding inputAccessoryView property does not work (never gets called). So I block interaction with UIWebView until user loads video.
private func displayVideo(URL: String) {
if let video = Video(videoURL: URL) {
// load video in webView
webView.userInteractionEnabled = true
} else {
webView.userInteractionEnabled = false
}
}
When user loads video, the only way to detect that user has entered/exited fullscreen mode is to listen to UIWindowDidBecomeKeyNotification and UIWindowDidResignKeyNotification and detect when our window loses/gains key status:
//in view controller:
private func windowDidBecomeKey(notification: NSNotification!) {
let isCurrentWindow = (notification.object as! UIWindow) == view.window
if isCurrentWindow {
// this restores our inputAccessoryView
becomeFirstResponder()
}
}
private func windowDidResignKey(notification: NSNotification!) {
let isCurrentWindow = (notification.object as! UIWindow) == view.window
if isCurrentWindow {
// this hides our inputAccessoryView so that it does not obscure video
resignFirstResponder()
}
}
And, of course, since inputAccessoryView can be removed at some point, we should recreate it if needed:
//in view controller:
override var inputAccessoryView: UIView {
if view == nil {
// load view here
}
return view
}