Messenger style UITextView input - ios

I'm working on a simple chat as a part of my App.
I have a list of Chats (i.e. friends) in a Table View Controller and Chat View Controller displaying messages for each chat. Both of them are embedded in a NavigationController, which is embedded in a TabBarController.
I want to place UITextView message text input field in a ChatViewController below my UITableVeiw that shows messages of that chat. I would also like the UITextView to look like it's embedded in a tab bar.
I've run through dozens of manuals tutorials and guides and that's where I got so far.
App simulator screenshots here: nice at the top, buggy at the bottom
Main.storyboard screenshot here
I use UITextView instead of UITextField because I want it to be able to change its size depending on content size
I use UIViewController instead of UITableViewController to be able to add another UIView along with UITableView
I don't use actual UITabBar to embed my UITextView because it behaves really weird when UITextView needs to change its height. And as far as I understood Apple doesn't support embedding UI element to UITabBar
What I do is I hide TabBar in ChatViewController viewWillAppear:
override func viewWillAppear(_ animated: Bool) {
tabBarController?.tabBar.isHidden = true
}
And I show it again in parent ChatsTableViewController:
override func viewWillAppear(_ animated: Bool) {
tabBarController?.tabBar.isHidden = false
}
Once TabBar is hidden, my custom stackView embedding textView and sendButton takes its place and operates quite normally as I want it to.
To handle keyboard behaviour properly I have an outlet for my inputStackView bottom constraint (selected on screenshot), which I update on keyboardWillShow/Hide Notifications. Code for that looks like this:
#IBOutlet weak var inputBottomConstraint: NSLayoutConstraint!
var inputBottomConstraintInitialValue: CGFloat!
//MARK: - Keyboard handling
private func enableKeyboardHideOnTap(){
NotificationCenter.default.addObserver(self, selector: #selector(ChatTableViewController.keyboardWillShow(notification:)), name: NSNotification.Name.UIKeyboardWillShow, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(ChatTableViewController.keyboardWillHide(notification:)), name: NSNotification.Name.UIKeyboardWillHide, object: nil)
let tap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(ChatTableViewController.hideKeyboard))
self.view.addGestureRecognizer(tap)
}
#objc func hideKeyboard() {
textView.resignFirstResponder()
}
#objc func keyboardWillShow(notification: NSNotification) {
let info = notification.userInfo!
let keyboardFrame: CGRect = (info[UIKeyboardFrameEndUserInfoKey] as! NSValue).cgRectValue
let duration = info[UIKeyboardAnimationDurationUserInfoKey] as! Double
UIView.animate(withDuration: duration) { [weak self] () -> Void in
self?.inputBottomConstraint.constant = keyboardFrame.size.height + 8
self?.view.layoutIfNeeded()
}
}
#objc func keyboardWillHide(notification: NSNotification) {
let duration = notification.userInfo![UIKeyboardAnimationDurationUserInfoKey] as! Double
UIView.animate(withDuration: duration) { [weak self] () -> Void in
guard let `self` = self else { return }
self.inputBottomConstraint.constant = self.inputBottomConstraintInitialValue
self.view.layoutIfNeeded()
}
}
Everything seems to work fine at this point:
TabBar is hidden in a ChatViewController and is shown in ChatsTableViewController
textView and sendButton embedded in a stackView slide along will keyboard up and down, as shown in simulator screenshots above.
The BUG appears when I swipe between ChatViewController and ChatsTableViewController instead of using NavigationControllers
When I start swiping back to ChatsTableViewController, it perform its viewWillAppear method, so tabBarController's tabBar become visible. And if I don't finish that swipe to ChatsTableViewController and go back ChatViewController, ChatViewController's viewWillAppear sets it back to invisible, but the input stackView doesn't get back to it's initial position. It just hangs there above the invisible tabBar.
I've tried moving
tabBarController?.tabBar.isHidden = false
from viewWillAppear to viewDidAppear in ChatsTableViewController.
It fixes the "floating input" issue but it adds a lag of about a second, when ChatsTableViewController is showing without tabBar. Looks not too good either.
Any ideas how that could be fixed properly?
I've been struggling with that issue for about a week))

Because your ChatsTableViewController is embedded in in UINavigationController and your ChatViewController is pushed you can override this hidesBottomBarWhenPushed in ChatViewController which will determine to show your view controller or not:
public override var hidesBottomBarWhenPushed: Bool {
get { return true }
set { super.hidesBottomBarWhenPushed = newValue }
}
This is a better way to hide the tab bar to my mind, because the frame of the view controllers will be adjusted.

Why do you change constraint on keyboard show/Hide. Try to Google inputAccesoryView, many chats use it. It’s natural behaviour is to go with the keyboard. Try FaceBook Messenger for example and see there how swiping hides the keyboard and Textfield together, you can’t accomplish this kind od behaviour with regular view.

Related

How to move up collection view controller when keyboard appears?

I have a collection view I made in storyboard and a container view with text field constrained to the bottom of the view programmatically. However, when I show the keyboard the collection view stays hidden underneath and I'm not sure how to. I looked and most answers say to use self.view.frame.origin.y in the keyboard notification listener but that's hasn't worked for me.
my keyboard notification code
#objc func keyboardWillShow(notification: NSNotification) {
let keyboardFrame = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue
let duration = ((notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber)?.doubleValue)!
containerViewbottomAnchor?.constant = -keyboardFrame!.height
UIView.animate(withDuration: duration) {
self.view.layoutIfNeeded()
}
}
first image is the normal chat and second is the max view you ca see when the keyboard is shown. the container view contains the textfield and button which I added programmatically. they are within an input accessory view to update their positions when the keyboard comes up.
Have you tried this solution? https://stackoverflow.com/a/57438748/13042987
NotificationCenter.default.addObserver(self, selector: #selector(handleKeyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil)
#objc func handleKeyboardWillShow(notification: Notification) {
collectionView.scrollToItem(at: IndexPath(row: messagesList.count - 1, section: chatSection), at: .top, animated: false)
}
I managed to fix it by changing to a UICollectionview controller class

Weird bug when presenting a view controller

I have a basic app with a UITabBarController as the root view controller. When a user of the app is not signed in I'm showing a modal controller via window!.rootViewController!.present(viewController, animated: true) in my AppDelegate. This works fine on all iPhone models, however the following happens on any iPad:
The background color of the SignInController is visible during the transition. Now comes the weird thing: When I change the view in Interface Builder to an iPad the bug is gone like so:
Changing the background color back to the transparent default removes at least the white background, however the view is still animating from the left bottom which is something I don't want. And by the way, changing the view in Interface Builder breaks the animation on all iPhones. Changing it back fixes it but breaks again all iPads.
This is the code (using ReSwift for state management):
func newState(state: State) {
switch (previousState.session, state.session) {
case (.loading, .notSignedIn), (.signedIn, .loading):
(window!.rootViewController! as! UITabBarController).selectedIndex = 0
let viewController = storyboard.instantiateViewController(withIdentifier: "SignInViewController")
window!.rootViewController!.present(viewController, animated: true, completion: nil)
default:
// more stuff
break
}
}
EDIT: Added the actual code.
I fixed it! 😊
The problem was a combination of having an observer on keyboardWillShowNotification and a becomeFirstResponder in the viewWillAppear method of the presented controller.
Moving the becomeFirstResponder into viewDidAppear fixed all the problems!
Thanks man! Saved my day.. I'm presenting the keyboard from within a tableview cell - I fixed it like this:
private var canPresentKeyboard: Bool = false
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
canPresentKeyboard = true
if _currentlySelectedIdType != .image {
reload(section: .idType)
}
}
func configure(cell: NumberIdTableViewCellInput) {
cell.set(delegate: self)
if canPresentKeyboard {
cell.clearAndSetFirstResponder()
}
}
I know the code is a bit out of context, but I believe the intention is clear.

UIKeyboardWillChangeFrame not called on iOS 11 (when transitioning view controllers)

Issue
I have a number of cases on iOS 11 (that do not occur on iOS 10 and below) where .UIKeyboardWillChangeFrame notification is not being fired — specifically when transitioning between view controllers where both view controllers have a UITextfield which is set as the fristResponder.
Since I have UI that needs to animate above the keyboard in response to the keyboard showing, receiving this notification is essential.
On iOS 10 and below, I get the notification on both view controllers (on showing VC A and also when pushing VC B). However, on iOS 11, the notification does not fire when pushing VC B. It's as if the keyboard remained in place.
Does anyone know what the cause of this is?
Details
Both view controllers inherit from a base view controller with the following implementation:
class BaseViewController {
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillChangeFrame), name: .UIKeyboardWillChangeFrame, object: nil)
}
// Update layout when the keyboard is shown or hidden
#objc func keyboardWillChangeFrame(notification : Notification) {
// Check if got info
if (notification.userInfo == nil) {
return;
}
// Get resize properties
let dict = notification.userInfo!
let rect = self.view.convert((((dict[UIKeyboardFrameEndUserInfoKey as NSObject] as Any) as AnyObject).cgRectValue)!, from: nil)
let size = self.view.bounds.size.height - rect.origin.y
let duration = ((dict[UIKeyboardAnimationDurationUserInfoKey] as Any) as AnyObject).doubleValue
let curve = UIViewAnimationCurve.init(rawValue: (((dict[UIKeyboardAnimationCurveUserInfoKey] as Any) as AnyObject).intValue)!)
self.keyboardOffset = max(0, size)
// Set animation options
var options : UIViewAnimationOptions
switch (curve!) {
case .easeInOut:
options = UIViewAnimationOptions()
case .easeIn:
options = UIViewAnimationOptions.curveEaseIn
case .easeOut:
options = UIViewAnimationOptions.curveEaseOut
case .linear:
options = UIViewAnimationOptions.curveLinear
}
// Animate the change
UIView.animate(withDuration: duration!, delay: 0, options: options, animations: { () -> Void in
// Relayout
self.relayout()
}, completion: nil)
}
}
Example of a subclass:
class ViewControllerA: BaseViewController {
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
passwordField.becomeFirstResponder()
}
func relayout() {
// ... do animations to show stuff above keyboard.
}
}
Note that .keyboardWillShow and .keybaordWillHide are also not fired on the transition to VC B.
You need to add self.view.endEditing(force:Bool) in you first view controller's viewWillDisappear method.
Generally, you can always call endEditing method when you changing the screen.
I've worked around this by saving the keyboard height in a class variable in VC A and then passing that along to VC B.
It's not ideal, as I would prefer my VC's to be independent for those details.

How to implement interactive cell swiping to trigger transitioning animation like WhatsApp

I'm looking for how to implement like WhatsApp cell swiping, I already have implemented the cell swiping animation using UIPanGestureRecognizer, the only left is performing the interactive animation -adding the new UIViewController to the window and showing it based on the gesture recognizer velocity and X-axis value-.
Some additional note to be accurate on what I want to achieve:
I have a UITableViewController, which has custom UITableViewCells in it. I want to be able to drag a cell from left to right to start the interactive animations. (Note: I already have implemented the cell swiping).
The new UIViewController will be pushed from left right.
While swiping the cell, the UITableViewController's view will be moving to the right, at that point, I want to show the pushing UIViewController beside it.
Here's a GIF for more details on what I need (The GIF is swiping the cell from right to left, I need the opposite):
I suggest using SWRevealViewController. It is very easy to set up using their guide and it looks absolutely great. I have found that it even works better when you pre-load the UIViewController that you use to be what is shown underneath.
It adds a great user experience for the functionality you are looking for.
It can also be user interactive if you wish to opt-in to that functionality. I have not used the interactive feature but it is very easy to get up and running with just a few lines of code:
let storyboard = UIStoryboard(name: "Main", bundle: .main)
let mainVC = storyboard.instantiateInitialViewController()
let menuStoryboard = UIStoryboard(name: "Menu", bundle: sdkBundle)
let menuNav = menuStoryboard.instantiateInitialViewController() as! UINavigationController
let mainRevealVC = SWRevealViewController(rearViewController: menuNav, frontViewController: mainVC)
mainRevealVC?.modalTransitionStyle = .crossDissolve
present(mainRevealVC!, animated: true, completion: nil)
And to get the reveal UIViewController to get shown, you just call
// Every UIViewController will have a `self.revealViewController()` when you `import SWRevealViewController`
self.revealViewController().revealToggle(animated: true)
I agree with #DonMag, a iOS slide menu might be your best bet. Here is an example of a simple one: SimpleSideMenu
Does it necessarily have to be a new controller behind the table view? Let me try to explain my approach on the WhatsApp example. Let's assume that the app has ChatController that has the table view with the chat and a ChatDetailController that is revealed with the swipe.
When you select a conversation, instead of presenting a ChatController present instead a ChatParent, that automatically creates and adds two children. The ChatController and ChatDetailController. Next define a protocol called SwipeableCellDelegate with a function cellDidSwipe(toPosition position: CGPoint) and make the ChatParent conform to it. When the cell is swiped, the parent can make the decision whether should the chat be moved away and if so, then how much. It can then simply move the ChatController view directly through its .view property, revealing the second child, the ChatDetailController behind it.
There are two downsides to this compared to the gif you posted.
The navigation bar doesn't fade from chat to chat detail. I would, however, argue that it is better to update the navigation bar when the animation completes, at least I personally am not a fan of this fade through where you can see both sets of navigation items at times. I would think that if chat is on screen then chat items should be present and only when detail view fully appears should the items be updated.
Second thing is the animated keyboard dismissal. I have no idea how to change keyboard frame to make it disappear proportionally to how far the user scrolls, but perhaps it could be dismissed automatically as soon as a swipe is detected? This is standard practice among many apps so it should be a decent solution.
Best of luck!
There is a very simple yet Perfect for your situation Library called SWNavigationController which implements just like UINavigationController's interactivePopGestureRecognizer also interactivePushGestureRecognizer. In your case you don't want the push to be triggered from UIScreenEdgePangesturerecognizer so you're better off customizing the implementation rather than installing the pod which is what I did. Here you can find the full simple project that does just what you asked.
I've made few modifications to SWNavigationController to support replacing UIScreenEdgePangesturerecognizer with a UIPanGestureRecognizer
import UIKit
// First in AppDelegate
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
self.window = UIWindow(frame: UIScreen.main.bounds)
let firstVc = ViewController()
let initialViewController: SWNavigationController = SWNavigationController(rootViewController: firstVc)
self.window?.rootViewController = initialViewController
self.window?.makeKeyAndVisible()
return true
}
// Your chat viewController
class ViewController: UIViewController {
var backgroundColors: [IndexPath : UIColor] = [ : ]
var swNavigationController: SWNavigationController {
return navigationController as! SWNavigationController
}
/// The collectionView if you're not using UICollectionViewController
lazy var collectionView: UICollectionView = {
let cv: UICollectionView = UICollectionView(frame: self.view.bounds, collectionViewLayout: self.layout)
cv.backgroundColor = UIColor.white
cv.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "Cell")
cv.dataSource = self
return cv
}()
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.title = "chat vc"
view.addSubview(collectionView)
let panGestureRecognizer: UIPanGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(ViewController.handlePan(_:)))
panGestureRecognizer.delegate = self
collectionView.addGestureRecognizer(panGestureRecognizer)
// Replace navigation controller's interactivePushGestureRecognizer with our own pan recognizer.
// SWNavigationController uses uiscreenedgerecognizer by default which we don't need in our case.
swNavigationController.interactivePushGestureRecognizer = panGestureRecognizer
}
func handlePan(_ recognizer: UIPanGestureRecognizer) {
guard fabs(recognizer.translation(in: collectionView).x) > fabs(recognizer.translation(in: collectionView).y) else {
return
}
// create the new view controller upon .began
if recognizer.state == .began {
// disable scrolling(optional)
collectionView.isScrollEnabled = false
// pan location
let location: CGPoint = recognizer.location(in: collectionView)
// get indexPath of cell where pan is taking place
if let panCellIndexPath: IndexPath = collectionView.indexPathForItem(at: location) {
// clear previously pushed viewControllers
swNavigationController.pushableViewControllers.removeAllObjects()
// create detail view controller for pan indexPath
let dvc = DetailViewController(indexPath: panCellIndexPath, backgroundColor: backgroundColors[panCellIndexPath]!)
swNavigationController.pushableViewControllers.add(dvc)
}
} else if recognizer.state != .changed {
collectionView.isScrollEnabled = true
}
// let navigation controller handle presenting
// (you can consume the initial pan translation on x axis to drag the cell to the left until a defined threshold and call handleRightSwipe: only after that)
swNavigationController.handleRightSwipe(recognizer)
}
}
// Cell detail view controller
class DetailViewController: UIViewController {
var indexPath: IndexPath
var backgroundColor: UIColor
init(indexPath: IndexPath, backgroundColor: UIColor) {
self.indexPath = indexPath
self.backgroundColor = backgroundColor
super.init(nibName: nil, bundle: nil)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.title = "detail vc at: \(indexPath.row)"
view.backgroundColor = backgroundColor
}
}

Click rootViewController through viewController

I'm trying to add TopView in my application, it will be the same for each views. I do it like this
let vcTopMenu = storyboard?.instantiateViewControllerWithIdentifier("TopMenu")
let win:UIWindow = UIApplication.sharedApplication().delegate!.window!!
win.rootViewController = vcTopMenu
win.makeKeyAndVisible()
But when I add other viewControllers (I do it transparent) I can see buttons of TopView, but I can't click on it. It's a code from TopView
override func viewDidLoad()
{
super.viewDidLoad()
print("loaded")
}
#IBAction func btn(sender: AnyObject)
{
print("do something")
}
I see "loaded", but clicking doesn't work, how can I click through view? Thanks!
If I understand your question correctly, you're placing a translucent/transparent UIView on top of another UIView with a button you want to press?
The topmost UIView by default receives the touches. More on this here.
It's not a very standard/practical way to do things, but if you absolutely must, check out this answer: https://stackoverflow.com/a/4010809/4396258

Resources