eBay / NMessenger

A fast, lightweight messenger component built on AsyncDisplaykit and written in Swift
Other
2.42k stars 272 forks source link

I can input textfield only 5 line? #141

Open meanreaksmey opened 7 years ago

dodikk commented 7 years ago

@meanreaksmey , setting NMessengerBarView.numberOfRows should do the job for you.

You can access this property by overriding NMessengerViewController.getInputBar metod

public override func getInputBar() -> InputBarView
{
    let result = NMessengerBarView(/*initialize it*/)
    result.numberOfRows = /*whatever number you want*/
}
meanreaksmey commented 7 years ago

@dodikk thank you

meanreaksmey commented 7 years ago

@dodikk when i used 10 line and after I input more I want to have scrollView in in input textfield how can?

dodikk commented 7 years ago

@dodikk when i used 10 line and after I input more I want to have scrollView in in input textfield how can?

@meanreaksmey , I've also faced that issue but I have not solved it in a flexible manner due to some lack of time yet.

Unfortunately, NMEssenger's input limitation policy is hard coded.

I suggest creating a separate "wish" ticket for that.

P.S. Don't forget to mention it in a comment here.

dodikk commented 7 years ago

Still, you can try one of the following workarounds :

Approach Comments
1 Fork NMessenger and hard code your logic to NMessengerBarView Ugly but that's what I did
2 Use a custom input bar class instead of NMessengerBarView More work to do
3 Subclass NMessengerBarView and try to override input handling methods Might fail due to encapsulation
4 Extend NMessengerBarView by implementing a Strategy pattern and allow library users come up with their own handlers Best for community (and your karma 😉) but the most time consuming

Approach [4] is the best from a technical point of view but you should take a decision based on your resources (deadlines and abilities).

dodikk commented 7 years ago

P.S. Don't forget to mention it in a comment here.

I've done that already. https://github.com/eBay/NMessenger/issues/153

dodikk commented 7 years ago

Here is my current hard code. See if it helps you.

import UIKit
import AVFoundation
import Photos

//MARK: InputBarView
/**
 InputBarView class for NMessenger.
 Define the input bar for NMessenger. This is where the user would type text and open the camera or photo library.
 */
open class NMessengerBarView: InputBarView
    , UITextViewDelegate
    , CameraViewDelegate
{
    //MARK: IBOutlets
    //@IBOutlet for InputBarView
    @IBOutlet open weak var inputBarView: UIView!

    @IBOutlet open weak var photoPickerButton: UIButton!
    @IBOutlet open weak var badgeLabel: UILabel?

    //@IBOutlet for send button
    @IBOutlet open weak var sendButton: UIButton!
    //@IBOutlets NSLayoutConstraint input area view height
    @IBOutlet open weak var textInputAreaViewHeight: NSLayoutConstraint!
    //@IBOutlets NSLayoutConstraint input view height
    @IBOutlet open weak var textInputViewHeight: NSLayoutConstraint!

    @IBOutlet open weak var textInputViewTopMargin   : NSLayoutConstraint?
    @IBOutlet open weak var textInputViewBottomMargin: NSLayoutConstraint?

    @IBOutlet weak var symbolsCounterLabel: UILabel?

    //MARK: Public Parameters
    //Reference to CameraViewController
    open lazy var cameraVC: UIViewController /*protocol<ICameraViewController>*/ = CameraViewController()

    open var cameraVcProto: ICameraViewController
    {
        return self.cameraVC as! ICameraViewController
    }

    open var alertUtils: IModalAlertUtilities = ModalAlertUtilities()
    {
        didSet
        {
            self.cameraVcProto.alertUtils = self.alertUtils
        }
    }

    //CGFloat to the fine the number of rows a user can type
    open var numberOfRows: CGFloat = 3
    open var maxSymolsCountInMessage: Int? = nil
    open var minSymbolsToShowCounter: Int? = nil

    //String as placeholder text in input view
    open var inputTextViewPlaceholder: String =
        Bundle.main.localizedString(forKey: "Chat.InputField.PlaceholderText",
                                     value: "==Write a message==",
                                     table: nil)
    {
        willSet(newVal)
        {
            self.textInputView.text = newVal
        }
    }

    //MARK: Private Parameters
    //CGFloat as defualt height for input view
    public var textInputViewHeightConst:CGFloat = 30

    // MARK: Initialisers
    /**
     Initialiser the view.
     */
    public required init()
    {
        super.init()
    }

    /**
     Initialiser the view.
     - parameter controller: Must be NMessengerViewController. Sets controller for the view.
     Calls helper method to setup the view
     */
    public required init(controller:NMessengerViewController)
    {
        super.init(controller: controller)
        loadFromBundle()
    }

    public required init(controller: NMessengerViewController,
                            nibName: String,
                             bundle: Bundle)
    {
        super.init(controller: controller)

        self.loadFrom(bundle: bundle, nibName: nibName)
    }

    /**
     Initialiser the view.
     - parameter controller: Must be NMessengerViewController. Sets controller for the view.
     - parameter frame: Must be CGRect. Sets frame for the view.
     Calls helper method to setup the view
     */
    public required init(controller: NMessengerViewController,
                              frame: CGRect)
    {
        super.init(controller: controller,
                        frame: frame)

        loadFromBundle()
    }
    /**
     - parameter aDecoder: Must be NSCoder
     Calls helper method to setup the view
     */
    public required init?(coder aDecoder: NSCoder)
    {
        super.init(coder: aDecoder)
        loadFromBundle()
    }

    // MARK: Initialiser helper methods
    /**
     Loads the view from nib file InputBarView and does intial setup.
     */
    fileprivate func loadFromBundle()
    {
        let nmessengerBundle = Bundle(for: NMessengerViewController.self)
        let nibName = "NMessengerBarView"

        self.loadFrom(bundle: nmessengerBundle, nibName: nibName)
    }

    fileprivate func loadFrom(bundle: Bundle,
                             nibName: String)
    {
        let nibObjects = bundle.loadNibNamed( nibName,
                                       owner: self,
                                     options: nil)
        /*let fileOwnerSelf*/ _ = nibObjects?[0] as! UIView

        self.addSubview(inputBarView)
        inputBarView.frame                = self.bounds
        textInputView.delegate            = self
        self.sendButton.isEnabled         = false
        self.sendButton.isUserInteractionEnabled = false
        self.cameraVcProto.cameraDelegate = self
        self.symbolsCounterLabel?.isHidden = true
    }

    //MARK: TextView delegate methods

    /**
     Implementing textViewShouldBeginEditing in order to set the text indictor at position 0
     */
    open func textViewShouldBeginEditing(_ textView: UITextView) -> Bool
    {
        textView.text = ""
        textView.textColor = UIColor.n1DarkestGreyColor()
        UIView.animate(withDuration: 0.1)
        {
            self.sendButton.isEnabled = true
            self.sendButton.isUserInteractionEnabled = true
        }

        DispatchQueue.main.async
        {
            textView.selectedRange = NSMakeRange(0, 0)
        }
        return true
    }

    /**
     Implementing textViewShouldEndEditing in order to re-add placeholder and hiding send button when lost focus
    */
    open func textViewShouldEndEditing(_ textView: UITextView) -> Bool
    {
        if self.textInputView.text.isEmpty
        {
            self.addInputSelectorPlaceholder()
        }

        UIView.animate(withDuration: 0.1)
        {
            self.sendButton.isEnabled = false
            self.sendButton.isUserInteractionEnabled = false
        }

        self.textInputView.resignFirstResponder()
        return true
    }

    open func textView(
        _ textView: UITextView,
        shouldChangeTextIn range: NSRange,
        replacementText text: String)
    -> Bool
    {
        //
        // Some logic deleted by A.Tyutin
        // to allow inserting "> 4k" symbols
        //
        //

        return true
    }

    private func updateSendButton() {

        var sendingAllowed = true

        let text = self.textInputView.text

        if text == nil || text!.isEmpty {
            sendingAllowed = false

        } else {

            if let maxSymolsCountInMessage = self.maxSymolsCountInMessage {

                sendingAllowed = text!.characters.count <= maxSymolsCountInMessage
            }
        }

        UIView.animate(withDuration: 0.1) {

            self.sendButton.isEnabled = sendingAllowed
            self.sendButton.isUserInteractionEnabled = sendingAllowed
        }
    }

    private func getNumberOfLines(forText newText: String,
                              inTextView textView: UITextView) -> CGFloat
    {
        let textSize = size(of: newText, in: textView)

        let numberOfLines = textSize.height / textView.font!.lineHeight

        return numberOfLines
    }

    private func topAndBottomMarginsForTextView() -> CGFloat
    {
        guard let textInputViewTopMargin    = self.textInputViewTopMargin   ,
              let textInputViewBottomMargin = self.textInputViewBottomMargin
        else
        {
            // legacy value
            //
            return 10
        }

        let result = textInputViewTopMargin.constant + textInputViewBottomMargin.constant
        return result
    }

    private func size(of text: String, in textView: UITextView) -> CGSize {

        let textViewInsets = UIEdgeInsetsInsetRect(textView.frame,
                                                   textView.textContainerInset)

        let textWidth: CGFloat = textViewInsets.width
            - 2.0 * textView.textContainer.lineFragmentPadding

        let options: NSStringDrawingOptions = [.usesLineFragmentOrigin,
                                               .usesFontLeading]

        let attributes: [String: Any] = [NSFontAttributeName: textView.font!]

        let textBounds = CGSize(width: textWidth, height: 0)
        let boundingRect: CGRect = text.boundingRect(with: textBounds,
                                                     options: options,
                                                     attributes: attributes,
                                                     context: nil)

        return CGSize(width: ceil(boundingRect.width),
                      height: ceil(boundingRect.height))
    }

    private func containerSize(for textSize: CGSize, in textView: UITextView) -> CGSize {
        let width = textSize.width
            + textView.textContainerInset.left
            + textView.textContainerInset.right

        let height = textSize.height
            + textView.textContainerInset.top
            + textView.textContainerInset.bottom

        return CGSize(width: width, height: height)
    }

    /**
     Implementing textViewDidChange in order to resize the text input area
     */
    open func textViewDidChange(_ textView: UITextView)
    {
        let fixedWidth = textView.frame.size.width

        var textSize = self.size(of: textView.text, in: textView)

        textSize.height = min(textSize.height, self.numberOfRows * textView.font!.lineHeight)

        let newSize = self.containerSize(for: textSize, in: textView)

        var newFrame = textView.frame
        let newFrameWidth  = max(newSize.width, fixedWidth)
        let newFrameHeight = max(newSize.height, self.textInputViewHeightConst)

        newFrame.size = CGSize(width: newFrameWidth,
                               height: newFrameHeight)

        let textViewMarginsHeight = self.topAndBottomMarginsForTextView()
        self.textInputViewHeight.constant = newFrame.size.height
        self.textInputAreaViewHeight.constant = newFrame.size.height + textViewMarginsHeight

        let symbolsCount = textView.text.characters.count

        if
            let maxSymbols = self.maxSymolsCountInMessage,
            let minSymbols = self.minSymbolsToShowCounter,
            let counterLabel = self.symbolsCounterLabel
        {
            counterLabel.text = "\(symbolsCount)/\n\(maxSymbols)"
            counterLabel.isHidden = symbolsCount < minSymbols
            counterLabel.textColor = symbolsCount <= maxSymbols ? UIColor.gray : UIColor.red
        }

        let isEnabled = !(textView.text.isEmpty)
        self.sendButton.isUserInteractionEnabled = isEnabled
        self.sendButton.isEnabled = isEnabled

        self.setNeedsLayout()
        self.layoutIfNeeded()

        self.updateSendButton()
    }

    //MARK: TextView helper methods
    /**
     Adds placeholder text and change the color of textInputView
     */
    fileprivate func addInputSelectorPlaceholder()
    {
        self.textInputView.text = self.inputTextViewPlaceholder
        self.textInputView.textColor = UIColor.lightGray

        self.sendButton.isEnabled = false
        self.sendButton.isUserInteractionEnabled = false
    }

    //MARK: @IBAction selectors
    /**
     Send button selector
     Sends the text in textInputView to the controller
     */
    @IBAction open func sendButtonClicked(_ sender: AnyObject)
    {
        self.doSend()
    }

    private func doSend()
    {
        guard let currentText = self.textInputView.text,
              !currentText.isEmpty
        else
        {
            return
        }

        self.controller.onSendButtonTapped(havingText: currentText)
//        images.forEach
//        {
//            _ = self.sendImage($0, isIncomingMessage: false)
//        }

        self.cleanupTextFieldAndResize()
    }

    private func cleanupTextFieldAndResize()
    {
        let textViewMarginsHeight = self.topAndBottomMarginsForTextView()
        self.textInputViewHeight.constant = self.textInputViewHeightConst

        self.textInputAreaViewHeight.constant =
            self.textInputViewHeightConst
            + textViewMarginsHeight

        self.sendButton.isEnabled = false
        self.sendButton.isUserInteractionEnabled = false

        self.textInputView.text = ""
        self.symbolsCounterLabel?.isHidden = true
    }

    /**
     Plus button selector
     Requests camera and photo library permission if needed
     Open camera and/or photo library to take/select a photo
     */
    @IBAction open func plusClicked(_ sender: AnyObject?)
    {
        self.checkCameraPermissions { [weak self] cameraPermissionsGranted in

            self?.checkPhotoLibraryPermissions { [weak self] photoLibraryPermissionsGranted in

                self?.showPickerView(executeAfterTransition: { [weak self] in

                    guard let strongSelf = self else {
                        return
                    }

                    // FIXME: Application crashes if camera permissions were not granted
                    switch (cameraPermissionsGranted, photoLibraryPermissionsGranted) {

                    case (true, true):
                        break

                    case (false, true):

                        strongSelf.alertUtils
                            .postGoToSettingToEnableCameraModal(fromController: strongSelf.cameraVC)

                    case (true, false):

                        strongSelf.alertUtils
                            .postGoToSettingToEnableLibraryModal(fromController: strongSelf.cameraVC)

                    case (false, false):

                        strongSelf.alertUtils
                            .postGoToSettingToEnableCameraAndLibraryModal(fromController: strongSelf.cameraVC)
                    }
                })
            }
        }
    }

    private func checkCameraPermissions(_ completion: @escaping (Bool) -> Void) {

        let authStatus = self.cameraVcProto.cameraAuthStatus

        if authStatus != AVAuthorizationStatus.authorized {

            self.cameraVcProto.isCameraPermissionGranted(completion)

        } else {

            completion(true)
        }
    }

    private func checkPhotoLibraryPermissions(_ completion: @escaping (Bool) -> Void) {

        let photoLibAuthStatus = self.cameraVcProto.photoLibAuthStatus

        if photoLibAuthStatus != PHAuthorizationStatus.authorized {

            self.cameraVcProto.requestPhotoLibraryPermissions(completion)

        } else {

            completion(true)
        }
    }

    //MARK: CameraView delegate methods
    //
    //

    /**
     Implemetning CameraView delegate method
     Close the CameraView and sends the image to the controller
     */
    open func pickedImages(_ images: [UIImage])
    {
        self.hidePickerView()

        self.controller.onImagesPicked(images)

//        images.forEach
//        {
//            _ = self.controller.sendImage($0, isIncomingMessage: false)
//        }
    }

    public func pickedAssets(_ assets: [PHAsset]) {

        self.controller.onAssetsPicked(assets)
    }

    private typealias ShowPickerViewCompletion = () -> Swift.Void
    private func showPickerView(
        executeAfterTransition callback: ShowPickerViewCompletion?)
    {
        DispatchQueue.main.async
        {
            self.doShowPickerView(executeAfterTransition: callback)
        }
    }

    private func doShowPickerView(
        executeAfterTransition callback: ShowPickerViewCompletion?)
    {
        if (self.isPickerCameraVc())
        {
            // legacy behaviour
            //
            self.controller.present( self.cameraVC,
                           animated: true,
                         completion: callback)
        }
        else if let navBar = self.controller.navigationController
        {
            navBar.pushViewController(self.cameraVC, animated: true)

            if let callbackUnwrap = callback
            {
                DispatchQueue.main.async
                {
                    // a hack to let the transition complete
                    callbackUnwrap()
                }
            }
        }
        else
        {
            // legacy behaviour
            //
            self.controller.present( self.cameraVC,
                                     animated: true,
                                     completion: callback)
        }
    }

    private func hidePickerView()
    {
        DispatchQueue.main.async
        {
            self.doHidePickerView()
        }
    }

    private func doHidePickerView()
    {
        if (self.isPickerCameraVc())
        {
            // legacy behaviour
            //
            self.cameraVC.dismiss(animated: true, completion: nil)
        }
        else if let navBar = self.controller.navigationController
        {
            navBar.popViewController(animated: true)
        }
        else
        {
            self.cameraVC.dismiss(animated: true, completion: nil)
        }
    }

    private func isPickerCameraVc() -> Bool
    {
        if let _ = self.cameraVC as? UIImagePickerController
        {
           return true
        }

        return false
    }

    /**
     Implemetning CameraView delegate method
     Close the CameraView
     */
    open func cameraCancelSelection()
    {
        self.hidePickerView()
    }

    /**
     Should define behavior when a photo is selected.
     Is called together with `pickedImages()`.

     Provides low level details and metadata to use with business logic.
     */
    public func pickedImageAssets(_ assets: [PHAsset])
    {
        // IDLE - this will be done in `self.pickedImages()`
        //
        // self.hidePickerView()

        self.controller.onAssetsPicked(assets)
    }
}