OEMentions.swift 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470
  1. //
  2. // OEMentions.swift
  3. // OEMentions
  4. //
  5. // Created by Omar Alessa on 7/31/16.
  6. // Copyright © 2016 omaressa. All rights reserved.
  7. //
  8. import UIKit
  9. protocol OEMentionsDelegate
  10. {
  11. // To respond to the selected name
  12. func mentionSelected(id:Int, name:String)
  13. func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath, oeObject: OEObject) -> UITableViewCell
  14. func tableViewPositionUpdated()
  15. func textViewShouldUpdateTextColor()
  16. }
  17. class OEMentions: NSObject, UITextViewDelegate, UITableViewDelegate, UITableViewDataSource {
  18. // UIViewController view
  19. var mainView:UIView?
  20. // UIView for the textview container
  21. var containerView:UIView?
  22. // The UITextView we want to add mention to
  23. var textView:UITextView?
  24. // List of names to show in the list
  25. private var oeObjects:[OEObject]?
  26. // [Index:Length] of added mentions to textview
  27. var mentionsIndexes = [Int:[String: Any]]()
  28. // Keep track if still searching for a name
  29. var isMentioning = Bool()
  30. // The search query
  31. var mentionQuery = String()
  32. // The start of mention index
  33. var startMentionIndex = Int()
  34. // Character that show the mention list (Default is "@"), It can be changed using changeMentionCharacter func
  35. private var mentionCharater = "@"
  36. // Keyboard hieght after it shows
  37. var keyboardHieght:CGFloat?
  38. // Mentions tableview
  39. var tableView: UITableView!
  40. //MARK: Customizable mention list properties
  41. // Color of the mention tableview name text
  42. var nameColor = UIColor.blue
  43. // Font of the mention tableview name text
  44. var nameFont = UIFont.boldSystemFont(ofSize: 14.0)
  45. // Color if the rest of the UITextView text
  46. var notMentionColor = UIColor.black
  47. // OEMention table view full in container view
  48. var showMentionFullInContainer:Bool = true
  49. private var filteredOEObjects: [OEObject]?
  50. // OEMention Delegate
  51. var delegate:OEMentionsDelegate?
  52. var textViewWidth:CGFloat?
  53. var textViewHeight:CGFloat?
  54. var textViewYPosition:CGFloat?
  55. var containerHieght:CGFloat?
  56. //MARK: class init without container
  57. init(textView:UITextView, mainView:UIView, oeObjects:[OEObject]){
  58. super.init()
  59. self.mainView = mainView
  60. self.setOeObjects(oeObjects: oeObjects)
  61. self.textView = textView
  62. self.textViewWidth = textView.frame.width
  63. initMentionsList()
  64. NotificationCenter.default.addObserver(self, selector: #selector(OEMentions.keyboardWillShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil)
  65. }
  66. //MARK: class init with container
  67. init(containerView:UIView, textView:UITextView, mainView:UIView, oeObjects:[OEObject]){
  68. super.init()
  69. self.containerView = containerView
  70. self.mainView = mainView
  71. self.setOeObjects(oeObjects: oeObjects)
  72. self.textView = textView
  73. self.containerHieght = containerView.frame.height
  74. self.textViewWidth = textView.frame.width
  75. self.textViewHeight = textView.frame.height
  76. self.textViewYPosition = textView.frame.origin.y
  77. initMentionsList()
  78. NotificationCenter.default.addObserver(self, selector: #selector(OEMentions.keyboardWillShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil)
  79. }
  80. func setOeObjects(oeObjects: [OEObject]?) {
  81. self.oeObjects = oeObjects
  82. self.filteredOEObjects = oeObjects
  83. }
  84. // Set the mention character. Should be one character only, default is "@"
  85. func changeMentionCharacter(character: String){
  86. if character.count == 1 && character != " " {
  87. self.mentionCharater = character
  88. }
  89. }
  90. // Change tableview background color
  91. func changeMentionTableviewBackground(color: UIColor){
  92. self.tableView.backgroundColor = color
  93. }
  94. func changeMentionTableviewSeparatorColor(color: UIColor) {
  95. self.tableView.separatorColor = color
  96. }
  97. //MARK: UITextView delegate functions:
  98. func textViewDidEndEditing(_ textView: UITextView) {
  99. self.mentionQuery = ""
  100. self.isMentioning = false
  101. UIView.animate(withDuration: 0.2, animations: {
  102. self.tableView.isHidden = true
  103. })
  104. }
  105. func textViewDidChange(_ textView: UITextView) {
  106. self.textView!.isScrollEnabled = false
  107. self.textView!.sizeToFit()
  108. self.textView!.frame.size.width = textViewWidth!
  109. updatePosition()
  110. }
  111. func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
  112. let str = String(textView.text)
  113. var lastCharacter = "nothing"
  114. if !str.isEmpty && range.location != 0{
  115. lastCharacter = String(str[str.index(before: str.endIndex)])
  116. }
  117. // Check if there is mentions
  118. if mentionsIndexes.count != 0 {
  119. var indexDiff = 0
  120. for (index,dict) in mentionsIndexes {
  121. let length = dict["length"] as! Int
  122. if case index+1 ... index+length-1 = range.location {
  123. // If start typing within a mention rang delete that name:
  124. mentionsIndexes.removeValue(forKey: index)
  125. indexDiff += -length
  126. textView.replace(textView.textRangeFromNSRange(range: NSMakeRange(index, length))!, withText: "")
  127. }
  128. else if (range.location + range.length < index+length) && (range.location + range.length > index) {
  129. mentionsIndexes.removeValue(forKey: index)
  130. }
  131. else if (index > range.location && index+length <= range.location + range.length) || (range.location < index + length && range.location + range.length >= index+length) {
  132. mentionsIndexes.removeValue(forKey: index)
  133. }
  134. else if index >= range.location && range.length == 0 {
  135. mentionsIndexes.removeValue(forKey: index)
  136. mentionsIndexes[index + indexDiff + text.utf16.count] = dict
  137. }
  138. else if index >= range.location && range.length > 0 {
  139. mentionsIndexes.removeValue(forKey: index)
  140. mentionsIndexes[index + indexDiff + text.utf16.count - range.length] = dict
  141. }
  142. else if index < 0 {
  143. mentionsIndexes.removeValue(forKey: index)
  144. }
  145. if case index+length = range.location {
  146. // If start typing within a mention rang delete that name:
  147. delegate?.textViewShouldUpdateTextColor()
  148. }
  149. }
  150. }
  151. if isMentioning {
  152. if text == " " || (text.count == 0 && self.mentionQuery == ""){ // If Space or delete the "@"
  153. self.mentionQuery = ""
  154. self.isMentioning = false
  155. updateTableView()
  156. UIView.animate(withDuration: 0.2, animations: {
  157. self.tableView.isHidden = true
  158. })
  159. }
  160. else if text.count == 0 {
  161. self.mentionQuery.remove(at: self.mentionQuery.index(before: self.mentionQuery.endIndex))
  162. updateTableView()
  163. }
  164. else {
  165. self.mentionQuery += text
  166. updateTableView()
  167. }
  168. } else {
  169. if text == self.mentionCharater && ( range.location == 0 || lastCharacter == " " || range.length == 0 ) { /* (Beginning of textView) OR (space then @) */
  170. self.isMentioning = true
  171. self.startMentionIndex = range.location
  172. updateTableView()
  173. UIView.animate(withDuration: 0.2, animations: {
  174. self.tableView.isHidden = false
  175. })
  176. }
  177. }
  178. return true
  179. }
  180. //MARK: Keyboard will show NSNotification:
  181. @objc func keyboardWillShow(notification:NSNotification) {
  182. let userInfo:NSDictionary = notification.userInfo! as NSDictionary
  183. let keyboardFrame:NSValue = userInfo.value(forKey: UIResponder.keyboardFrameEndUserInfoKey) as! NSValue
  184. let keyboardRectangle = keyboardFrame.cgRectValue
  185. let thekeyboardHeight = keyboardRectangle.height
  186. self.keyboardHieght = thekeyboardHeight
  187. UIView.animate(withDuration: 0.3, animations: {
  188. self.updatePosition()
  189. })
  190. }
  191. //Mentions UITableView init
  192. func initMentionsList(){
  193. tableView = UITableView(frame: CGRect(x: 0, y: 0, width: self.mainView!.frame.width, height: 100), style: UITableView.Style.plain)
  194. tableView.delegate = self
  195. tableView.dataSource = self
  196. tableView.tableFooterView = UIView()
  197. tableView.allowsSelection = true
  198. tableView.separatorColor = UIColor.clear
  199. tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
  200. self.mainView!.addSubview(self.tableView)
  201. self.tableView.isHidden = true
  202. }
  203. //MARK: Mentions UITableView deleget functions:
  204. func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  205. return self.filteredOEObjects!.count
  206. }
  207. func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  208. var cell:UITableViewCell?
  209. if delegate != nil {
  210. cell = delegate?.tableView(tableView, cellForRowAt: indexPath, oeObject: filteredOEObjects![indexPath.row])
  211. if cell != nil {
  212. return cell!
  213. }
  214. }
  215. cell = UITableViewCell(style: UITableViewCell.CellStyle.subtitle, reuseIdentifier: "cell")
  216. cell!.backgroundColor = UIColor.clear
  217. cell!.selectionStyle = UITableViewCell.SelectionStyle.none
  218. cell!.textLabel!.text = filteredOEObjects![indexPath.row].name
  219. cell!.textLabel!.textColor = nameColor
  220. return cell!
  221. }
  222. func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
  223. guard let selectedMention = filteredOEObjects?[indexPath.row] else {
  224. return
  225. }
  226. addMentionToTextView(oeObject: selectedMention)
  227. self.mentionQuery = ""
  228. self.isMentioning = false
  229. UIView.animate(withDuration: 0.2, animations: {
  230. self.tableView.isHidden = true
  231. })
  232. if delegate != nil {
  233. self.delegate!.mentionSelected(id: selectedMention.id, name: selectedMention.name)
  234. }
  235. }
  236. // Add a mention name to the UITextView
  237. func addMentionToTextView(oeObject: OEObject){
  238. let name = oeObject.name as String
  239. // add a space at the end and beginning of the mention (if needed)
  240. var mentionBeginChar = ""
  241. var mentionEndChat = ""
  242. if self.startMentionIndex > 0 {
  243. if let range = Range.init(NSMakeRange(self.startMentionIndex - 1, 1), in: self.textView!.text) {
  244. if self.textView!.text[range] != " " {
  245. mentionBeginChar = " "
  246. }
  247. }
  248. }
  249. if self.startMentionIndex + self.mentionQuery.count + 1 < self.textView!.text.utf16.count {
  250. if let range = Range.init(NSMakeRange(self.startMentionIndex + self.mentionQuery.count + 1, 1), in: self.textView!.text) {
  251. if self.textView!.text[range] != " " {
  252. mentionEndChat = " "
  253. }
  254. }
  255. } else {
  256. if self.startMentionIndex + self.mentionQuery.count + 1 == self.textView!.text.utf16.count {
  257. mentionEndChat = " "
  258. }
  259. }
  260. let dict = ["key": oeObject.key, "length": name.utf16.count] as [String : Any]
  261. let newStartMentionIndex = mentionBeginChar.utf16.count > 0 ? self.startMentionIndex+1 : self.startMentionIndex
  262. mentionsIndexes[newStartMentionIndex] = dict
  263. let range = NSRange.init(location: self.startMentionIndex, length: self.mentionQuery.count + 1)
  264. let swiftRange = Range.init(range, in: self.textView!.text)
  265. let replaceString = mentionBeginChar + oeObject.name + mentionEndChat
  266. self.textView!.text.replaceSubrange(swiftRange!, with: replaceString)
  267. let indexDiff = replaceString.utf16.count - 1
  268. if mentionsIndexes.count != 0 {
  269. for (index,dict) in mentionsIndexes {
  270. if index != newStartMentionIndex {
  271. if index > range.location && range.length == 0 {
  272. mentionsIndexes.removeValue(forKey: index)
  273. mentionsIndexes[index + indexDiff] = dict
  274. }
  275. else if index > range.location && range.length > 0 {
  276. mentionsIndexes.removeValue(forKey: index)
  277. mentionsIndexes[index + indexDiff] = dict
  278. }
  279. }
  280. }
  281. }
  282. if let theText = self.textView!.text {
  283. var attributes = [NSAttributedString.Key: AnyObject]()
  284. attributes[.foregroundColor] = notMentionColor
  285. attributes[.font] = nameFont
  286. let attributedString: NSMutableAttributedString = NSMutableAttributedString.init(string: theText, attributes: attributes)
  287. self.textView!.attributedText = attributedString
  288. if let cursorLocation = self.textView!.position(from: self.textView!.beginningOfDocument, offset: self.startMentionIndex + name.utf16.count + 1) {
  289. self.textView!.selectedTextRange = self.textView!.textRange(from: cursorLocation, to: cursorLocation)
  290. }
  291. }
  292. updatePosition()
  293. }
  294. // Update views potision for the textview and tableview
  295. func updatePosition(){
  296. if keyboardHieght == nil {
  297. return
  298. }
  299. if #available(iOS 11.0, *) {
  300. self.tableView.frame.size.width = mainView!.safeAreaLayoutGuide.layoutFrame.size.width
  301. self.tableView.frame.origin.x = mainView!.safeAreaLayoutGuide.layoutFrame.origin.x
  302. } else {
  303. self.tableView.frame.size.width = mainView!.frame.size.width
  304. self.tableView.frame.origin.x = mainView!.frame.origin.x
  305. }
  306. if containerView != nil {
  307. self.textView!.frame.origin.y = self.textViewYPosition!
  308. let fullTableViewHeight = UIScreen.main.bounds.height - self.keyboardHieght! - self.containerView!.frame.size.height
  309. if showMentionFullInContainer == true {
  310. if fullTableViewHeight != self.tableView.frame.size.height {
  311. self.tableView.frame.size.height = fullTableViewHeight
  312. self.tableView.frame.origin.y = 0
  313. }
  314. } else {
  315. if self.tableView.contentSize.height < fullTableViewHeight {
  316. self.tableView.frame.size.height = self.tableView.contentSize.height
  317. self.tableView.frame.origin.y = UIScreen.main.bounds.height - self.keyboardHieght! - containerView!.frame.size.height - self.tableView.frame.size.height
  318. } else {
  319. if fullTableViewHeight != self.tableView.frame.size.height {
  320. self.tableView.frame.size.height = fullTableViewHeight
  321. self.tableView.frame.origin.y = 0
  322. }
  323. }
  324. }
  325. }
  326. else {
  327. self.textView!.frame.origin.y = UIScreen.main.bounds.height - self.keyboardHieght! - self.textView!.frame.height
  328. self.tableView.frame.size.height = UIScreen.main.bounds.height - self.keyboardHieght! - self.textView!.frame.height
  329. self.tableView.frame.origin.y = 0
  330. }
  331. if delegate != nil {
  332. delegate!.tableViewPositionUpdated()
  333. }
  334. }
  335. private func updateTableView() {
  336. if mentionQuery.count > 0 {
  337. self.filteredOEObjects = self.oeObjects?.filter {
  338. $0.name.lowercased().localizedStandardContains(self.mentionQuery.lowercased()) || $0.key.lowercased().localizedStandardContains(self.mentionQuery.lowercased())
  339. }
  340. } else {
  341. self.filteredOEObjects = oeObjects
  342. }
  343. self.tableView.reloadData()
  344. }
  345. }
  346. // OEMentions object (id,name)
  347. class OEObject {
  348. var id:Int
  349. var name:String
  350. var key:String
  351. var object: Any?
  352. init(id:Int, name:String, key: String, object: Any?){
  353. self.id = id
  354. self.name = name
  355. self.key = key
  356. self.object = object
  357. }
  358. }
  359. extension UITextView
  360. {
  361. func textRangeFromNSRange(range:NSRange) -> UITextRange?
  362. {
  363. let beginning = self.beginningOfDocument
  364. guard let start = self.position(from: beginning, offset: range.location), let end = self.position(from: start, offset: range.length) else { return nil}
  365. return self.textRange(from: start, to: end)
  366. }
  367. }