Files
GitHawk/SlackTextViewController/Examples/Messenger-Swift/MessageViewController.swift
2017-07-23 06:54:55 -04:00

754 lines
26 KiB
Swift
Executable File

//
// MessageViewController.swift
// Messenger
//
// Created by Ignacio Romero Zurbuchen on 10/16/14.
// Copyright (c) 2014 Slack Technologies, Inc. All rights reserved.
//
let DEBUG_CUSTOM_TYPING_INDICATOR = false
class MessageViewController: SLKTextViewController {
var messages = [Message]()
var users: Array = ["Allen", "Anna", "Alicia", "Arnold", "Armando", "Antonio", "Brad", "Catalaya", "Christoph", "Emerson", "Eric", "Everyone", "Steve"]
var channels: Array = ["General", "Random", "iOS", "Bugs", "Sports", "Android", "UI", "SSB"]
var emojis: Array = ["-1", "m", "man", "machine", "block-a", "block-b", "bowtie", "boar", "boat", "book", "bookmark", "neckbeard", "metal", "fu", "feelsgood"]
var commands: Array = ["msg", "call", "text", "skype", "kick", "invite"]
var searchResult: [String]?
var pipWindow: UIWindow?
var editingMessage = Message()
override var tableView: UITableView {
get {
return super.tableView!
}
}
// MARK: - Initialisation
override class func tableViewStyle(for decoder: NSCoder) -> UITableViewStyle {
return .plain
}
func commonInit() {
NotificationCenter.default.addObserver(self.tableView, selector: #selector(UITableView.reloadData), name: NSNotification.Name.UIContentSizeCategoryDidChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(MessageViewController.textInputbarDidMove(_:)), name: NSNotification.Name.SLKTextInputbarDidMove, object: nil)
}
override func viewDidLoad() {
// Register a SLKTextView subclass, if you need any special appearance and/or behavior customisation.
self.registerClass(forTextView: MessageTextView.classForCoder())
if DEBUG_CUSTOM_TYPING_INDICATOR == true {
// Register a UIView subclass, conforming to SLKTypingIndicatorProtocol, to use a custom typing indicator view.
self.registerClass(forTypingIndicatorView: TypingIndicatorView.classForCoder())
}
super.viewDidLoad()
self.commonInit()
// Example's configuration
self.configureDataSource()
self.configureActionItems()
// SLKTVC's configuration
self.bounces = true
self.shakeToClearEnabled = true
self.isKeyboardPanningEnabled = true
self.shouldScrollToBottomAfterKeyboardShows = false
self.isInverted = true
self.leftButton.setImage(UIImage(named: "icn_upload"), for: UIControlState())
self.leftButton.tintColor = UIColor.gray
self.rightButton.setTitle(NSLocalizedString("Send", comment: ""), for: UIControlState())
self.textInputbar.autoHideRightButton = true
self.textInputbar.maxCharCount = 256
self.textInputbar.counterStyle = .split
self.textInputbar.counterPosition = .top
self.textInputbar.editorTitle.textColor = UIColor.darkGray
self.textInputbar.editorLeftButton.tintColor = UIColor(red: 0/255, green: 122/255, blue: 255/255, alpha: 1)
self.textInputbar.editorRightButton.tintColor = UIColor(red: 0/255, green: 122/255, blue: 255/255, alpha: 1)
if DEBUG_CUSTOM_TYPING_INDICATOR == false {
self.typingIndicatorView!.canResignByTouch = true
}
self.tableView.separatorStyle = .none
self.tableView.register(MessageTableViewCell.classForCoder(), forCellReuseIdentifier: MessengerCellIdentifier)
self.autoCompletionView.register(MessageTableViewCell.classForCoder(), forCellReuseIdentifier: AutoCompletionCellIdentifier)
self.registerPrefixes(forAutoCompletion: ["@", "#", ":", "+:", "/"])
self.textView.placeholder = "Message";
self.textView.registerMarkdownFormattingSymbol("*", withTitle: "Bold")
self.textView.registerMarkdownFormattingSymbol("_", withTitle: "Italics")
self.textView.registerMarkdownFormattingSymbol("~", withTitle: "Strike")
self.textView.registerMarkdownFormattingSymbol("`", withTitle: "Code")
self.textView.registerMarkdownFormattingSymbol("```", withTitle: "Preformatted")
self.textView.registerMarkdownFormattingSymbol(">", withTitle: "Quote")
}
// MARK: - Lifeterm
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
deinit {
NotificationCenter.default.removeObserver(self)
}
}
extension MessageViewController {
// MARK: - Example's Configuration
func configureDataSource() {
var array = [Message]()
for _ in 0..<100 {
let words = Int((arc4random() % 40)+1)
let message = Message()
message.username = LoremIpsum.name()
message.text = LoremIpsum.words(withNumber: words)
array.append(message)
}
let reversed = array.reversed()
self.messages.append(contentsOf: reversed)
}
func configureActionItems() {
let arrowItem = UIBarButtonItem(image: UIImage(named: "icn_arrow_down"), style: .plain, target: self, action: #selector(MessageViewController.hideOrShowTextInputbar(_:)))
let editItem = UIBarButtonItem(image: UIImage(named: "icn_editing"), style: .plain, target: self, action: #selector(MessageViewController.editRandomMessage(_:)))
let typeItem = UIBarButtonItem(image: UIImage(named: "icn_typing"), style: .plain, target: self, action: #selector(MessageViewController.simulateUserTyping(_:)))
let appendItem = UIBarButtonItem(image: UIImage(named: "icn_append"), style: .plain, target: self, action: #selector(MessageViewController.fillWithText(_:)))
let pipItem = UIBarButtonItem(image: UIImage(named: "icn_pic"), style: .plain, target: self, action: #selector(MessageViewController.togglePIPWindow(_:)))
self.navigationItem.rightBarButtonItems = [arrowItem, pipItem, editItem, appendItem, typeItem]
}
// MARK: - Action Methods
func hideOrShowTextInputbar(_ sender: AnyObject) {
guard let buttonItem = sender as? UIBarButtonItem else {
return
}
let hide = !self.isTextInputbarHidden
let image = hide ? UIImage(named: "icn_arrow_up") : UIImage(named: "icn_arrow_down")
self.setTextInputbarHidden(hide, animated: true)
buttonItem.image = image
}
func fillWithText(_ sender: AnyObject) {
if self.textView.text.characters.count == 0 {
var sentences = Int(arc4random() % 4)
if sentences <= 1 {
sentences = 1
}
self.textView.text = LoremIpsum.sentences(withNumber: sentences)
}
else {
self.textView.slk_insertText(atCaretRange: " " + LoremIpsum.word())
}
}
func simulateUserTyping(_ sender: AnyObject) {
if !self.canShowTypingIndicator() {
return
}
if DEBUG_CUSTOM_TYPING_INDICATOR == true {
guard let view = self.typingIndicatorProxyView as? TypingIndicatorView else {
return
}
let scale = UIScreen.main.scale
let imgSize = CGSize(width: kTypingIndicatorViewAvatarHeight*scale, height: kTypingIndicatorViewAvatarHeight*scale)
// This will cause the typing indicator to show after a delay ¯\_()_/¯
LoremIpsum.asyncPlaceholderImage(with: imgSize, completion: { (image) -> Void in
guard let cgImage = image?.cgImage else {
return
}
let thumbnail = UIImage(cgImage: cgImage, scale: scale, orientation: .up)
view.presentIndicator(withName: LoremIpsum.name(), image: thumbnail)
})
}
else {
self.typingIndicatorView!.insertUsername(LoremIpsum.name())
}
}
func didLongPressCell(_ gesture: UIGestureRecognizer) {
guard let view = gesture.view else {
return
}
if gesture.state != .began {
return
}
if #available(iOS 8, *) {
let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
alertController.modalPresentationStyle = .popover
alertController.popoverPresentationController?.sourceView = view.superview
alertController.popoverPresentationController?.sourceRect = view.frame
alertController.addAction(UIAlertAction(title: "Edit Message", style: .default, handler: { [unowned self] (action) -> Void in
self.editCellMessage(gesture)
}))
alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
self.navigationController?.present(alertController, animated: true, completion: nil)
}
else {
self.editCellMessage(gesture)
}
}
func editCellMessage(_ gesture: UIGestureRecognizer) {
guard let cell = gesture.view as? MessageTableViewCell else {
return
}
self.editingMessage = self.messages[cell.indexPath.row]
self.editText(self.editingMessage.text)
self.tableView.scrollToRow(at: cell.indexPath, at: .bottom, animated: true)
}
func editRandomMessage(_ sender: AnyObject) {
var sentences = Int(arc4random() % 10)
if sentences <= 1 {
sentences = 1
}
self.editText(LoremIpsum.sentences(withNumber: sentences))
}
func editLastMessage(_ sender: AnyObject?) {
if self.textView.text.characters.count > 0 {
return
}
let lastSectionIndex = self.tableView.numberOfSections-1
let lastRowIndex = self.tableView.numberOfRows(inSection: lastSectionIndex)-1
let lastMessage = self.messages[lastRowIndex]
self.editText(lastMessage.text)
self.tableView.scrollToRow(at: IndexPath(row: lastRowIndex, section: lastSectionIndex), at: .bottom, animated: true)
}
func togglePIPWindow(_ sender: AnyObject) {
if self.pipWindow == nil {
self.showPIPWindow(sender)
}
else {
self.hidePIPWindow(sender)
}
}
func showPIPWindow(_ sender: AnyObject) {
var frame = CGRect(x: self.view.frame.width - 60.0, y: 0.0, width: 50.0, height: 50.0)
frame.origin.y = self.textInputbar.frame.minY - 60.0
self.pipWindow = UIWindow(frame: frame)
self.pipWindow?.backgroundColor = UIColor.black
self.pipWindow?.layer.cornerRadius = 10
self.pipWindow?.layer.masksToBounds = true
self.pipWindow?.isHidden = false
self.pipWindow?.alpha = 0.0
UIApplication.shared.keyWindow?.addSubview(self.pipWindow!)
UIView.animate(withDuration: 0.25, animations: { [unowned self] () -> Void in
self.pipWindow?.alpha = 1.0
})
}
func hidePIPWindow(_ sender: AnyObject) {
UIView.animate(withDuration: 0.3, animations: { [unowned self] () -> Void in
self.pipWindow?.alpha = 0.0
}, completion: { [unowned self] (finished) -> Void in
self.pipWindow?.isHidden = true
self.pipWindow = nil
})
}
func textInputbarDidMove(_ note: Notification) {
guard let pipWindow = self.pipWindow else {
return
}
guard let userInfo = (note as NSNotification).userInfo else {
return
}
guard let value = userInfo["origin"] as? NSValue else {
return
}
var frame = pipWindow.frame
frame.origin.y = value.cgPointValue.y - 60.0
pipWindow.frame = frame
}
}
extension MessageViewController {
// MARK: - Overriden Methods
override func ignoreTextInputbarAdjustment() -> Bool {
return super.ignoreTextInputbarAdjustment()
}
override func forceTextInputbarAdjustment(for responder: UIResponder!) -> Bool {
if #available(iOS 8.0, *) {
guard let _ = responder as? UIAlertController else {
// On iOS 9, returning YES helps keeping the input view visible when the keyboard if presented from another app when using multi-tasking on iPad.
return UIDevice.current.userInterfaceIdiom == .pad
}
return true
}
else {
return UIDevice.current.userInterfaceIdiom == .pad
}
}
// Notifies the view controller that the keyboard changed status.
override func didChangeKeyboardStatus(_ status: SLKKeyboardStatus) {
switch status {
case .willShow:
print("Will Show")
case .didShow:
print("Did Show")
case .willHide:
print("Will Hide")
case .didHide:
print("Did Hide")
}
}
// Notifies the view controller that the text will update.
override func textWillUpdate() {
super.textWillUpdate()
}
// Notifies the view controller that the text did update.
override func textDidUpdate(_ animated: Bool) {
super.textDidUpdate(animated)
}
// Notifies the view controller when the left button's action has been triggered, manually.
override func didPressLeftButton(_ sender: Any!) {
super.didPressLeftButton(sender)
self.dismissKeyboard(true)
self.performSegue(withIdentifier: "Push", sender: nil)
}
// Notifies the view controller when the right button's action has been triggered, manually or by using the keyboard return key.
override func didPressRightButton(_ sender: Any!) {
// This little trick validates any pending auto-correction or auto-spelling just after hitting the 'Send' button
self.textView.refreshFirstResponder()
let message = Message()
message.username = LoremIpsum.name()
message.text = self.textView.text
let indexPath = IndexPath(row: 0, section: 0)
let rowAnimation: UITableViewRowAnimation = self.isInverted ? .bottom : .top
let scrollPosition: UITableViewScrollPosition = self.isInverted ? .bottom : .top
self.tableView.beginUpdates()
self.messages.insert(message, at: 0)
self.tableView.insertRows(at: [indexPath], with: rowAnimation)
self.tableView.endUpdates()
self.tableView.scrollToRow(at: indexPath, at: scrollPosition, animated: true)
// Fixes the cell from blinking (because of the transform, when using translucent cells)
// See https://github.com/slackhq/SlackTextViewController/issues/94#issuecomment-69929927
self.tableView.reloadRows(at: [indexPath], with: .automatic)
super.didPressRightButton(sender)
}
override func didPressArrowKey(_ keyCommand: UIKeyCommand?) {
guard let keyCommand = keyCommand else { return }
if keyCommand.input == UIKeyInputUpArrow && self.textView.text.characters.count == 0 {
self.editLastMessage(nil)
}
else {
super.didPressArrowKey(keyCommand)
}
}
override func keyForTextCaching() -> String? {
return Bundle.main.bundleIdentifier
}
// Notifies the view controller when the user has pasted a media (image, video, etc) inside of the text view.
override func didPasteMediaContent(_ userInfo: [AnyHashable: Any]) {
super.didPasteMediaContent(userInfo)
let mediaType = (userInfo[SLKTextViewPastedItemMediaType] as? NSNumber)?.intValue
let contentType = userInfo[SLKTextViewPastedItemContentType]
let data = userInfo[SLKTextViewPastedItemData]
print("didPasteMediaContent : \(contentType) (type = \(mediaType) | data : \(data))")
}
// Notifies the view controller when a user did shake the device to undo the typed text
override func willRequestUndo() {
super.willRequestUndo()
}
// Notifies the view controller when tapped on the right "Accept" button for commiting the edited text
override func didCommitTextEditing(_ sender: Any) {
self.editingMessage.text = self.textView.text
self.tableView.reloadData()
super.didCommitTextEditing(sender)
}
// Notifies the view controller when tapped on the left "Cancel" button
override func didCancelTextEditing(_ sender: Any) {
super.didCancelTextEditing(sender)
}
override func canPressRightButton() -> Bool {
return super.canPressRightButton()
}
override func canShowTypingIndicator() -> Bool {
if DEBUG_CUSTOM_TYPING_INDICATOR == true {
return true
}
else {
return super.canShowTypingIndicator()
}
}
override func shouldProcessText(forAutoCompletion text: String) -> Bool {
return true
}
override func didChangeAutoCompletionPrefix(_ prefix: String, andWord word: String) {
var array:Array<String> = []
let wordPredicate = NSPredicate(format: "self BEGINSWITH[c] %@", word);
self.searchResult = nil
if prefix == "@" {
if word.characters.count > 0 {
array = self.users.filter { wordPredicate.evaluate(with: $0) };
}
else {
array = self.users
}
}
else if prefix == "#" {
if word.characters.count > 0 {
array = self.channels.filter { wordPredicate.evaluate(with: $0) };
}
else {
array = self.channels
}
}
else if (prefix == ":" || prefix == "+:") && word.characters.count > 0 {
array = self.emojis.filter { wordPredicate.evaluate(with: $0) };
}
else if prefix == "/" && self.foundPrefixRange.location == 0 {
if word.characters.count > 0 {
array = self.commands.filter { wordPredicate.evaluate(with: $0) };
}
else {
array = self.commands
}
}
var show = false
if array.count > 0 {
let sortedArray = array.sorted { $0.localizedCaseInsensitiveCompare($1) == ComparisonResult.orderedAscending }
self.searchResult = sortedArray
show = sortedArray.count > 0
}
self.showAutoCompletionView(show)
}
override func heightForAutoCompletionView() -> CGFloat {
guard let searchResult = self.searchResult else {
return 0
}
let cellHeight = self.autoCompletionView.delegate?.tableView!(self.autoCompletionView, heightForRowAt: IndexPath(row: 0, section: 0))
guard let height = cellHeight else {
return 0
}
return height * CGFloat(searchResult.count)
}
}
extension MessageViewController {
// MARK: - UITableViewDataSource Methods
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if tableView == self.tableView {
return self.messages.count
}
else {
if let searchResult = self.searchResult {
return searchResult.count
}
}
return 0
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if tableView == self.tableView {
return self.messageCellForRowAtIndexPath(indexPath)
}
else {
return self.autoCompletionCellForRowAtIndexPath(indexPath)
}
}
func messageCellForRowAtIndexPath(_ indexPath: IndexPath) -> MessageTableViewCell {
let cell = self.tableView.dequeueReusableCell(withIdentifier: MessengerCellIdentifier) as! MessageTableViewCell
if cell.gestureRecognizers?.count == nil {
let longPress = UILongPressGestureRecognizer(target: self, action: #selector(MessageViewController.didLongPressCell(_:)))
cell.addGestureRecognizer(longPress)
}
let message = self.messages[(indexPath as NSIndexPath).row]
cell.titleLabel.text = message.username
cell.bodyLabel.text = message.text
cell.indexPath = indexPath
cell.usedForMessage = true
// Cells must inherit the table view's transform
// This is very important, since the main table view may be inverted
cell.transform = self.tableView.transform
return cell
}
func autoCompletionCellForRowAtIndexPath(_ indexPath: IndexPath) -> MessageTableViewCell {
let cell = self.autoCompletionView.dequeueReusableCell(withIdentifier: AutoCompletionCellIdentifier) as! MessageTableViewCell
cell.indexPath = indexPath
cell.selectionStyle = .default
guard let searchResult = self.searchResult else {
return cell
}
guard let prefix = self.foundPrefix else {
return cell
}
var text = searchResult[(indexPath as NSIndexPath).row]
if prefix == "#" {
text = "# " + text
}
else if prefix == ":" || prefix == "+:" {
text = ":\(text):"
}
cell.titleLabel.text = text
return cell
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if tableView == self.tableView {
let message = self.messages[(indexPath as NSIndexPath).row]
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineBreakMode = .byWordWrapping
paragraphStyle.alignment = .left
let pointSize = MessageTableViewCell.defaultFontSize()
let attributes = [
NSFontAttributeName : UIFont.systemFont(ofSize: pointSize),
NSParagraphStyleAttributeName : paragraphStyle
]
var width = tableView.frame.width-kMessageTableViewCellAvatarHeight
width -= 25.0
let titleBounds = (message.username as NSString).boundingRect(with: CGSize(width: width, height: CGFloat.greatestFiniteMagnitude), options: .usesLineFragmentOrigin, attributes: attributes, context: nil)
let bodyBounds = (message.text as NSString).boundingRect(with: CGSize(width: width, height: CGFloat.greatestFiniteMagnitude), options: .usesLineFragmentOrigin, attributes: attributes, context: nil)
if message.text.characters.count == 0 {
return 0
}
var height = titleBounds.height
height += bodyBounds.height
height += 40
if height < kMessageTableViewCellMinimumHeight {
height = kMessageTableViewCellMinimumHeight
}
return height
}
else {
return kMessageTableViewCellMinimumHeight
}
}
// MARK: - UITableViewDelegate Methods
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if tableView == self.autoCompletionView {
guard let searchResult = self.searchResult else {
return
}
var item = searchResult[(indexPath as NSIndexPath).row]
if self.foundPrefix == "@" && self.foundPrefixRange.location == 0 {
item += ":"
}
else if self.foundPrefix == ":" || self.foundPrefix == "+:" {
item += ":"
}
item += " "
self.acceptAutoCompletion(with: item, keepPrefix: true)
}
}
}
extension MessageViewController {
// MARK: - UIScrollViewDelegate Methods
// Since SLKTextViewController uses UIScrollViewDelegate to update a few things, it is important that if you override this method, to call super.
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
super.scrollViewDidScroll(scrollView)
}
}
extension MessageViewController {
// MARK: - UITextViewDelegate Methods
override func textViewShouldBeginEditing(_ textView: UITextView) -> Bool {
return true
}
override func textViewShouldEndEditing(_ textView: UITextView) -> Bool {
// Since SLKTextViewController uses UIScrollViewDelegate to update a few things, it is important that if you override this method, to call super.
return true
}
override func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
return super.textView(textView, shouldChangeTextIn: range, replacementText: text)
}
override func textView(_ textView: SLKTextView, shouldOfferFormattingForSymbol symbol: String) -> Bool {
if symbol == ">" {
let selection = textView.selectedRange
// The Quote formatting only applies new paragraphs
if selection.location == 0 && selection.length > 0 {
return true
}
// or older paragraphs too
let prevString = (textView.text as NSString).substring(with: NSMakeRange(selection.location-1, 1))
if CharacterSet.newlines.contains(UnicodeScalar((prevString as NSString).character(at: 0))!) {
return true
}
return false
}
return super.textView(textView, shouldOfferFormattingForSymbol: symbol)
}
override func textView(_ textView: SLKTextView, shouldInsertSuffixForFormattingWithSymbol symbol: String, prefixRange: NSRange) -> Bool {
if symbol == ">" {
return false
}
return super.textView(textView, shouldInsertSuffixForFormattingWithSymbol: symbol, prefixRange: prefixRange)
}
}