CompanyDirectoryViewController.swift 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558
  1. // _____ _
  2. // |_ _| |_ _ _ ___ ___ _ __ __ _
  3. // | | | ' \| '_/ -_) -_) ' \/ _` |_
  4. // |_| |_||_|_| \___\___|_|_|_\__,_(_)
  5. //
  6. // Threema iOS Client
  7. // Copyright (c) 2019-2020 Threema GmbH
  8. //
  9. // This program is free software: you can redistribute it and/or modify
  10. // it under the terms of the GNU Affero General Public License, version 3,
  11. // as published by the Free Software Foundation.
  12. //
  13. // This program is distributed in the hope that it will be useful,
  14. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. // GNU Affero General Public License for more details.
  17. //
  18. // You should have received a copy of the GNU Affero General Public License
  19. // along with this program. If not, see <https://www.gnu.org/licenses/>.
  20. import Foundation
  21. @objc class CompanyDirectoryViewController: ThemedViewController {
  22. @IBOutlet weak var tableView: UITableView!
  23. @IBOutlet weak var searchBar: SearchBarWithoutCancelButton!
  24. @IBOutlet weak var noEntriesFoundView: UIView!
  25. @IBOutlet weak var noEntriesFoundTitleLabel: UILabel!
  26. @IBOutlet weak var noEntriesFoundDescriptionLabel: UILabel!
  27. @IBOutlet weak var activeFiltersView: UIStackView!
  28. @IBOutlet weak var scrollView: UIScrollView!
  29. @objc var addContactActive: Bool = true
  30. var filterArray: [String] = [String]()
  31. private var contactsWithSections: [[CompanyDirectoryContact]] = [[CompanyDirectoryContact]]()
  32. private var sectionTitles: [String] = [String]()
  33. private var allSectionTitles: [String] = [String]()
  34. private var nextPage: Int = 0
  35. private var showLoadMore: Bool = false
  36. private var searchString: String = ""
  37. private var resultArray: [CompanyDirectoryContact] = [CompanyDirectoryContact]()
  38. private let collation = UILocalizedIndexedCollation.current()
  39. override func viewDidLoad() {
  40. super.viewDidLoad()
  41. searchBar.sizeToFit()
  42. searchBar.placeholder = BundleUtil.localizedString(forKey: "companydirectory_placeholder")
  43. tableView.setupAutoAdjust()
  44. noEntriesFoundTitleLabel.text = BundleUtil.localizedString(forKey: "companydirectory_noentries_title")
  45. noEntriesFoundDescriptionLabel.text = BundleUtil.localizedString(forKey: "companydirectory_noentries_description")
  46. self.title = MyIdentityStore.shared()?.companyName
  47. }
  48. override func viewWillAppear(_ animated: Bool) {
  49. super.viewWillAppear(animated)
  50. if addContactActive == false {
  51. navigationItem.leftBarButtonItem = UIBarButtonItem.init(barButtonSystemItem: .cancel, target: self, action: #selector(cancel))
  52. }
  53. navigationItem.rightBarButtonItem = UIBarButtonItem.init(image: BundleUtil.imageNamed("Filter"), style: .plain, target: self, action: #selector(filter))
  54. self.searchBar.becomeFirstResponder()
  55. performSearch()
  56. }
  57. override func viewWillDisappear(_ animated: Bool) {
  58. super.viewWillDisappear(animated)
  59. NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(performSearch), object: nil)
  60. }
  61. override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
  62. if segue.identifier == "ShowFilterSegue" {
  63. if let destinationVC = segue.destination as? CompanyDirectoryCategoryViewController {
  64. destinationVC.companyDirectoryViewController = self
  65. }
  66. }
  67. }
  68. override func refresh() {
  69. super.refresh()
  70. setupColors()
  71. updateNoEntriesFound()
  72. setupFiltersView()
  73. tableView.reloadData()
  74. }
  75. func setupColors() {
  76. Colors.update(searchBar)
  77. Colors.update(tableView)
  78. noEntriesFoundTitleLabel.textColor = Colors.fontNormal()
  79. noEntriesFoundDescriptionLabel.textColor = Colors.fontLight()
  80. }
  81. func updateNoEntriesFound() {
  82. if contactsWithSections.count > 0 {
  83. tableView.tableFooterView = nil
  84. } else {
  85. tableView.tableFooterView = noEntriesFoundView
  86. showLoadMore = false
  87. }
  88. }
  89. @objc private func performSearch() {
  90. guard let query = searchBar.text, query.trimmingCharacters(in: .whitespaces) != "" else {
  91. contactsWithSections.removeAll()
  92. sectionTitles.removeAll()
  93. resultArray.removeAll()
  94. refresh()
  95. return
  96. }
  97. searchBar.isLoading = true
  98. // call api to get the results
  99. contactsWithSections.removeAll()
  100. sectionTitles.removeAll()
  101. resultArray.removeAll()
  102. nextPage = 0
  103. searchString = query
  104. ServerAPIConnector().search(inDirectory: query, categories: filterArray, page: Int32(nextPage), for: LicenseStore.shared(), for: MyIdentityStore.shared(), onCompletion: { (contacts, paging) in
  105. self.showLoadMore = false
  106. if contacts != nil {
  107. if contacts!.count > 0 {
  108. for dict in contacts! {
  109. let contact = CompanyDirectoryContact.init(dictionary: dict as! [AnyHashable : Any?])
  110. self.resultArray.append(contact)
  111. }
  112. if UserSettings.shared().sortOrderFirstName == true {
  113. let (arrayContacts, arrayTitles, allSectionTitles) = self.collation.partitionObjects(array: self.resultArray, collationStringSelector: #selector(getter: CompanyDirectoryContact.first))
  114. self.contactsWithSections = arrayContacts as! [[CompanyDirectoryContact]]
  115. self.sectionTitles = arrayTitles
  116. self.allSectionTitles = allSectionTitles
  117. } else {
  118. let (arrayContacts, arrayTitles, allSectionTitles) = self.collation.partitionObjects(array: self.resultArray, collationStringSelector: #selector(getter: CompanyDirectoryContact.last))
  119. self.contactsWithSections = arrayContacts as! [[CompanyDirectoryContact]]
  120. self.sectionTitles = arrayTitles
  121. self.allSectionTitles = allSectionTitles
  122. }
  123. if let next = paging?["next"] as? Int {
  124. if let total = paging?["total"] as? Int {
  125. if total != self.resultArray.count {
  126. self.nextPage = next
  127. self.showLoadMore = true
  128. }
  129. }
  130. }
  131. }
  132. }
  133. self.refresh()
  134. self.searchBar.isLoading = false
  135. }) { (error) in
  136. if let theError = (error as NSError?) {
  137. if theError.code == 100 {
  138. UIAlertTemplate.showAlert(owner: self, title: BundleUtil.localizedString(forKey: "cannot_connect_title"), message: BundleUtil.localizedString(forKey: "cannot_connect_message"))
  139. }
  140. }
  141. self.refresh()
  142. self.searchBar.isLoading = false
  143. }
  144. }
  145. private func loadMore(cell: UITableViewCell) {
  146. let activityView = UIActivityIndicatorView.init(style: .gray)
  147. switch Colors.getTheme() {
  148. case ColorThemeDark, ColorThemeDarkWork:
  149. activityView.style = .white
  150. break
  151. case ColorThemeUndefined, ColorThemeLight, ColorThemeLightWork:
  152. activityView.style = .gray
  153. break
  154. default:
  155. activityView.style = .gray
  156. break
  157. }
  158. activityView.startAnimating()
  159. cell.accessoryView = activityView
  160. ServerAPIConnector().search(inDirectory: searchString, categories: filterArray, page: Int32(nextPage), for: LicenseStore.shared(), for: MyIdentityStore.shared(), onCompletion: { (contacts, paging) in
  161. self.showLoadMore = false
  162. if contacts != nil {
  163. if contacts!.count > 0 {
  164. for dict in contacts! {
  165. let contact = CompanyDirectoryContact.init(dictionary: dict as! [AnyHashable : Any?])
  166. self.resultArray.append(contact)
  167. }
  168. if UserSettings.shared().sortOrderFirstName == true {
  169. let (arrayContacts, arrayTitles, allSectionTitles) = self.collation.partitionObjects(array: self.resultArray, collationStringSelector: #selector(getter: CompanyDirectoryContact.first))
  170. self.contactsWithSections = arrayContacts as! [[CompanyDirectoryContact]]
  171. self.sectionTitles = arrayTitles
  172. self.allSectionTitles = allSectionTitles
  173. } else {
  174. let (arrayContacts, arrayTitles, allSectionTitles) = self.collation.partitionObjects(array: self.resultArray, collationStringSelector: #selector(getter: CompanyDirectoryContact.last))
  175. self.contactsWithSections = arrayContacts as! [[CompanyDirectoryContact]]
  176. self.sectionTitles = arrayTitles
  177. self.allSectionTitles = allSectionTitles
  178. }
  179. if let next = paging?["next"] as? Int {
  180. if let total = paging?["total"] as? Int {
  181. if total != self.resultArray.count {
  182. self.nextPage = next
  183. self.showLoadMore = true
  184. }
  185. }
  186. }
  187. }
  188. }
  189. activityView.stopAnimating()
  190. cell.accessoryView = nil
  191. self.refresh()
  192. self.searchBar.isLoading = false
  193. }) { (error) in
  194. activityView.stopAnimating()
  195. cell.accessoryView = nil
  196. self.searchBar.isLoading = false
  197. }
  198. }
  199. private func setupFiltersView() {
  200. if filterArray.count > 0 {
  201. self.scrollView.frame = CGRect.init(x: self.scrollView.frame.origin.x, y: self.scrollView.frame.origin.y, width: self.scrollView.frame.size.width, height: 44.0)
  202. self.scrollView.setNeedsLayout()
  203. self.scrollView.layoutIfNeeded()
  204. } else {
  205. self.scrollView.frame = CGRect.init(x: self.scrollView.frame.origin.x, y: self.scrollView.frame.origin.y, width: self.scrollView.frame.size.width, height: 0)
  206. self.scrollView.setNeedsLayout()
  207. self.scrollView.layoutIfNeeded()
  208. }
  209. for tempView in activeFiltersView.subviews {
  210. tempView.removeFromSuperview()
  211. }
  212. activeFiltersView.layoutIfNeeded()
  213. var i = 0
  214. let catDict = MyIdentityStore.shared().directoryCategories as! Dictionary<String, String>
  215. for category in filterArray {
  216. let filterLabel = createFilterLabel(text: catDict[category]!, index: i)
  217. activeFiltersView.addArrangedSubview(filterLabel)
  218. i += 1
  219. }
  220. }
  221. private func createFilterLabel(text: String, index: Int) -> UIStackView {
  222. let textWidth = text.widthOfString(usingFont: UIFont.preferredFont(forTextStyle: UIFont.TextStyle.footnote))
  223. let buttonWidth: CGFloat = 23.0
  224. let padding: CGFloat = 2.0
  225. let stackView = UIStackView.init(frame: CGRect.init(x: 0.0, y: 0.0, width: textWidth + buttonWidth + (padding * 2), height: activeFiltersView.frame.size.height))
  226. stackView.axis = .horizontal
  227. stackView.distribution = .equalSpacing
  228. stackView.spacing = 0
  229. let textlabel = UILabel.init(frame: CGRect.init(x: 0.0, y: 0.0, width: textWidth, height: 20.0))
  230. textlabel.textColor = Colors.fontInverted()
  231. textlabel.font = UIFont.preferredFont(forTextStyle: UIFont.TextStyle.footnote)
  232. textlabel.text = text
  233. stackView.addArrangedSubview(textlabel)
  234. let button = UIButton(type: .custom)
  235. button.frame = CGRect(x: 0.0, y: 0.0, width: buttonWidth, height: buttonWidth)
  236. button.backgroundColor = .clear
  237. button.layer.cornerRadius = CGFloat(button.frame.size.width)/CGFloat(2.0)
  238. button.setImage(UIImage(named: "CloseCategory", in: .white), for: .normal)
  239. button.tag = index
  240. button.imageView?.contentMode = .scaleAspectFit
  241. button.addTarget(self, action: #selector(removeTag(_:)), for: .touchUpInside)
  242. stackView.addArrangedSubview(button)
  243. let widthContraints = NSLayoutConstraint(item: button, attribute: NSLayoutConstraint.Attribute.width, relatedBy: NSLayoutConstraint.Relation.equal, toItem: nil, attribute: NSLayoutConstraint.Attribute.notAnAttribute, multiplier: 1, constant: buttonWidth)
  244. NSLayoutConstraint.activate([widthContraints])
  245. stackView.layoutIfNeeded()
  246. let size = CGSize(width: textWidth, height: 1000)
  247. let options = NSStringDrawingOptions.usesFontLeading.union(.usesLineFragmentOrigin)
  248. let attributes = [NSAttributedString.Key.font: textlabel.font]
  249. let rectangleHeight = String(text).boundingRect(with: size, options: options, attributes: attributes as [NSAttributedString.Key : Any], context: nil).height
  250. let backgroundFrame = CGRect.init(x: stackView.frame.origin.x - (padding * 3), y: ((stackView.frame.size.height - rectangleHeight) / 2) - padding, width: stackView.frame.size.width + (padding * 2), height: rectangleHeight + (padding * 2))
  251. let backgroundView = UIView.init(frame: backgroundFrame)
  252. backgroundView.layer.cornerRadius = 5
  253. backgroundView.backgroundColor = Colors.backgroundInverted()
  254. stackView.insertSubview(backgroundView, at: 0)
  255. return stackView
  256. }
  257. @objc private func cancel() {
  258. self.dismiss(animated: true, completion: nil)
  259. }
  260. @objc private func filter() {
  261. self.performSegue(withIdentifier: "ShowFilterSegue", sender: self)
  262. }
  263. @objc private func removeTag(_ sender: AnyObject) {
  264. filterArray.remove(at: (sender.tag))
  265. performSearch()
  266. }
  267. }
  268. extension CompanyDirectoryViewController: UITableViewDataSource {
  269. func numberOfSections(in tableView: UITableView) -> Int {
  270. if showLoadMore {
  271. return sectionTitles.count + 1
  272. }
  273. return sectionTitles.count
  274. }
  275. func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  276. if showLoadMore && section == sectionTitles.count {
  277. return 1
  278. }
  279. return contactsWithSections[section].count
  280. }
  281. func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
  282. if showLoadMore && indexPath.section == sectionTitles.count {
  283. return 50.0
  284. }
  285. return UITableView.automaticDimension
  286. }
  287. func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  288. if showLoadMore && indexPath.section == sectionTitles.count {
  289. let cell:UITableViewCell = tableView.dequeueReusableCell(withIdentifier: "LoadMoreCell", for: indexPath)
  290. cell.textLabel?.text = BundleUtil.localizedString(forKey: "loadMore")
  291. let image = UIImage.init(named: "ArrowDown", in: Colors.fontLight())
  292. cell.imageView?.image = image?.resizedImage(newSize: CGSize.init(width: 25.0, height: 25.0))
  293. cell.accessoryView = nil
  294. return cell
  295. } else {
  296. let cell:CompanyDirectoryContactCell = tableView.dequeueReusableCell(withIdentifier: "CompanyDirectoryContactCell", for: indexPath) as! CompanyDirectoryContactCell
  297. let contact = contactsWithSections[indexPath.section][indexPath.row]
  298. cell.addContactActive = addContactActive
  299. cell.contact = contact
  300. return cell
  301. }
  302. }
  303. func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
  304. Colors.update(cell)
  305. if cell.isKind(of: CompanyDirectoryContactCell.self) {
  306. (cell as! CompanyDirectoryContactCell).setupColors()
  307. }
  308. }
  309. func sectionIndexTitles(for tableView: UITableView) -> [String]? {
  310. return UILocalizedIndexedCollation.current().sectionIndexTitles
  311. }
  312. func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
  313. if showLoadMore && section == sectionTitles.count {
  314. return ""
  315. }
  316. return sectionTitles[section]
  317. }
  318. func tableView(_ tableView: UITableView, sectionForSectionIndexTitle title: String, at index: Int) -> Int {
  319. if title == "*" {
  320. return sectionTitles.count
  321. }
  322. if sectionTitles.contains(title) == true {
  323. return sectionTitles.firstIndex(of: title) ?? 0
  324. } else {
  325. var tempIndex:Int = 0
  326. for str in allSectionTitles {
  327. if sectionTitles.contains(str) == true {
  328. tempIndex += 1
  329. }
  330. if str == title {
  331. return tempIndex - 1
  332. }
  333. }
  334. return 0
  335. }
  336. }
  337. }
  338. extension CompanyDirectoryViewController: UITableViewDelegate {
  339. func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
  340. if nextPage > 0 && indexPath.section == sectionTitles.count {
  341. let cell = tableView.cellForRow(at: indexPath)
  342. let activityIndicator = UIActivityIndicatorView.init(style: .white)
  343. switch Colors.getTheme() {
  344. case ColorThemeDark, ColorThemeDarkWork:
  345. activityIndicator.style = .white
  346. break
  347. case ColorThemeUndefined, ColorThemeLight, ColorThemeLightWork:
  348. activityIndicator.style = .gray
  349. break
  350. default:
  351. activityIndicator.style = .gray
  352. break
  353. }
  354. activityIndicator.startAnimating()
  355. cell?.accessoryView = activityIndicator
  356. loadMore(cell: cell!)
  357. } else {
  358. let directoryContact = contactsWithSections[indexPath.section][indexPath.row]
  359. let contact = ContactStore.shared()?.addWorkContact(withIdentity: directoryContact.id, publicKey: directoryContact.pk, firstname: directoryContact.first, lastname: directoryContact.last)
  360. // show chat
  361. if contact != nil {
  362. navigationController?.dismiss(animated: true, completion: {
  363. let info = [kKeyContact: contact!, kKeyForceCompose: true] as [String : Any]
  364. NotificationCenter.default.post(name: NSNotification.Name(rawValue: kNotificationShowConversation), object: nil, userInfo: info)
  365. })
  366. }
  367. }
  368. }
  369. }
  370. extension CompanyDirectoryViewController: UISearchBarDelegate {
  371. func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
  372. //here you should call the function which will update your data source and reload table view (or other UI that you have)
  373. performSearch()
  374. }
  375. func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
  376. NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(performSearch), object: nil)
  377. if searchText.count >= 3 {
  378. perform(#selector(performSearch), with: nil, afterDelay: 0.75)
  379. } else {
  380. contactsWithSections.removeAll()
  381. sectionTitles.removeAll()
  382. resultArray.removeAll()
  383. refresh()
  384. }
  385. }
  386. func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
  387. NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(performSearch), object: nil)
  388. }
  389. }
  390. class SearchBarWithoutCancelButton:UISearchBar {
  391. override func layoutSubviews() {
  392. super.layoutSubviews()
  393. self.setShowsCancelButton(false, animated: false)
  394. }
  395. public var textField: UITextField? {
  396. let subViews = subviews.flatMap { $0.subviews }
  397. guard let textField = (subViews.filter { $0 is UITextField }).first as? UITextField else {
  398. return nil
  399. }
  400. return textField
  401. }
  402. public var activityIndicator: UIActivityIndicatorView? {
  403. return textField?.leftView?.subviews.compactMap{ $0 as? UIActivityIndicatorView }.first
  404. }
  405. var isLoading: Bool {
  406. get {
  407. return activityIndicator != nil
  408. } set {
  409. if newValue {
  410. if activityIndicator == nil {
  411. let newActivityIndicator: UIActivityIndicatorView
  412. switch Colors.getTheme() {
  413. case ColorThemeDark, ColorThemeDarkWork:
  414. newActivityIndicator = UIActivityIndicatorView(style: .white)
  415. newActivityIndicator.backgroundColor = UIColor.init(red: 32.0/255.0, green: 32.0/255.0, blue: 29.0/255.0, alpha: 1.0)
  416. break
  417. case ColorThemeUndefined, ColorThemeLight, ColorThemeLightWork:
  418. newActivityIndicator = UIActivityIndicatorView(style: .gray)
  419. newActivityIndicator.backgroundColor = .white
  420. break
  421. default:
  422. newActivityIndicator = UIActivityIndicatorView(style: .gray)
  423. newActivityIndicator.backgroundColor = .white
  424. break
  425. }
  426. newActivityIndicator.startAnimating()
  427. textField?.leftView?.addSubview(newActivityIndicator)
  428. let leftViewSize = textField?.leftView?.frame.size ?? CGSize.zero
  429. newActivityIndicator.center = CGPoint(x: leftViewSize.width/2, y: leftViewSize.height/2)
  430. }
  431. } else {
  432. activityIndicator?.removeFromSuperview()
  433. }
  434. }
  435. }
  436. }
  437. extension UITableView {
  438. func setupAutoAdjust() {
  439. NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardshown), name: UIResponder.keyboardWillShowNotification, object: nil)
  440. NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardhide), name: UIResponder.keyboardWillHideNotification, object: nil)
  441. }
  442. @objc func keyboardshown(_ notification:Notification) {
  443. if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
  444. self.fitContentInset(inset: UIEdgeInsets(top: 0, left: 0, bottom: keyboardSize.height, right: 0))
  445. }
  446. }
  447. @objc func keyboardhide(_ notification:Notification) {
  448. if ((notification.userInfo?[UIResponder.keyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue) != nil {
  449. self.fitContentInset(inset: .zero)
  450. }
  451. }
  452. func fitContentInset(inset:UIEdgeInsets!) {
  453. self.contentInset = inset
  454. self.scrollIndicatorInsets = inset
  455. }
  456. }
  457. extension UILocalizedIndexedCollation {
  458. //func for partition array in sections
  459. func partitionObjects(array:[AnyObject], collationStringSelector:Selector) -> ([AnyObject], [String], [String]) {
  460. var unsortedSections = [[AnyObject]]()
  461. //1. Create a array to hold the data for each section
  462. for _ in self.sectionTitles {
  463. unsortedSections.append([]) //appending an empty array
  464. }
  465. //2. Put each objects into a section
  466. for item in array {
  467. let index:Int = self.section(for: item, collationStringSelector:collationStringSelector)
  468. unsortedSections[index].append(item)
  469. }
  470. //3. sorting the array of each section
  471. var activeSectionTitles = [String]()
  472. var sections = [AnyObject]()
  473. for index in 0 ..< unsortedSections.count { if unsortedSections[index].count > 0 {
  474. activeSectionTitles.append(self.sectionTitles[index])
  475. sections.append(self.sortedArray(from: unsortedSections[index], collationStringSelector: collationStringSelector) as AnyObject)
  476. }
  477. }
  478. return (sections, activeSectionTitles, sectionTitles)
  479. }
  480. }
  481. extension String {
  482. func widthOfString(usingFont font: UIFont) -> CGFloat {
  483. let fontAttributes = [NSAttributedString.Key.font: font]
  484. let size = self.size(withAttributes: fontAttributes)
  485. return size.width
  486. }
  487. }