SelectMemberView.swift 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  1. //
  2. // SelectMemberView.swift
  3. // TUIRoomKit
  4. //
  5. // Created by CY zhao on 2024/6/18.
  6. //
  7. import UIKit
  8. import RTCRoomEngine
  9. protocol SelectMemberViewDelegate: AnyObject {
  10. func selectView(_ selectView: SelectMemberView, didSearchWith searchText:String)
  11. func didBackButtonClicked(in selectView: SelectMemberView)
  12. func didExpandButtonClicked(in selectView: SelectMemberView)
  13. func didConfirmButtonClicked(in selectView: SelectMemberView )
  14. }
  15. class SelectMemberView: UIView {
  16. weak var delegate: SelectMemberViewDelegate?
  17. static let maxVisiableAvatars = 10
  18. private let navigationBar: UIView = {
  19. let view = UIView()
  20. return view
  21. }()
  22. let backButton: UIButton = {
  23. let button = UIButton()
  24. button.setImage(UIImage(named: "room_back_black", in: tuiRoomKitBundle(), compatibleWith: nil), for: .normal)
  25. button.backgroundColor = .clear
  26. return button
  27. }()
  28. let titleLabel: UILabel = {
  29. let label = UILabel()
  30. label.text = .selectMemberText
  31. label.font = UIFont.boldSystemFont(ofSize: 18)
  32. label.textAlignment = .center
  33. return label
  34. }()
  35. let label: UILabel = {
  36. let label = UILabel()
  37. label.textColor = UIColor.tui_color(withHex: "2B2E38")
  38. label.font = UIFont(name: "PingFangSC-Regular", size: 16)
  39. label.text = .selectMemberText
  40. label.sizeToFit()
  41. return label
  42. }()
  43. let searchBar: UISearchBar = {
  44. let searchBar = UISearchBar()
  45. searchBar.placeholder = .enterUserIdText
  46. searchBar.setBackgroundImage(UIImage(), for: .any, barMetrics: .default)
  47. return searchBar
  48. }()
  49. let searchControl: UIControl = {
  50. let view = UIControl()
  51. view.backgroundColor = .clear
  52. view.isHidden = true
  53. return view
  54. }()
  55. let tableView: UITableView = {
  56. let tableView = UITableView(frame: .zero, style: .plain)
  57. tableView.register(ContactCell.self, forCellReuseIdentifier: ContactCell.reuseIdentifier)
  58. if #available(iOS 15.0, *) {
  59. tableView.sectionHeaderTopPadding = 0
  60. }
  61. return tableView
  62. }()
  63. let bottomView: UIView = {
  64. let view = UIView()
  65. return view
  66. }()
  67. let selectedUserView: UICollectionView = {
  68. let layout = UICollectionViewFlowLayout()
  69. layout.scrollDirection = .horizontal
  70. layout.itemSize = CGSize(width: 32, height: 32)
  71. layout.minimumLineSpacing = 8
  72. let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
  73. collectionView.showsHorizontalScrollIndicator = false
  74. collectionView.register(AvatarCell.self, forCellWithReuseIdentifier: AvatarCell.reuseIdentifier)
  75. collectionView.backgroundColor = .clear
  76. return collectionView
  77. }()
  78. let expandButton: UIButton = {
  79. let button = UIButton(type: .custom)
  80. button.titleLabel?.font = UIFont(name: "PingFangSC-Regular", size: 14)
  81. button.setTitleColor(UIColor.tui_color(withHex: "#22262E"), for: .normal)
  82. button.setTitle(.selectedText, for: .normal)
  83. let normalIcon = UIImage(named: "room_up_black_arrow", in: tuiRoomKitBundle(), compatibleWith: nil)
  84. button.setImage(normalIcon, for: .normal)
  85. button.sizeToFit()
  86. button.isHidden = true
  87. return button
  88. }()
  89. let confirmButton: UIButton = {
  90. let button = UIButton()
  91. button.backgroundColor = UIColor.tui_color(withHex: "1C66E5")
  92. button.titleLabel?.font = UIFont(name: "PingFangSC-Medium", size: 14)
  93. button.setTitleColor(UIColor(0xFFFFFF), for: .normal)
  94. button.setTitle(.confirmText, for: .normal)
  95. button.titleLabel?.textAlignment = .center
  96. button.layer.cornerRadius = 16
  97. button.isEnabled = false
  98. button.contentEdgeInsets = UIEdgeInsets(top: 5, left: 24, bottom: 5, right: 24)
  99. button.sizeToFit()
  100. return button
  101. }()
  102. lazy var currentLanguage: String = {
  103. let locale = Locale.current
  104. let languageCode = locale.languageCode ?? "en"
  105. return languageCode
  106. }()
  107. override func layoutSubviews() {
  108. super.layoutSubviews()
  109. if let textField = searchBar.value(forKey: "searchField") as? UITextField {
  110. textField.layer.cornerRadius = textField.bounds.height / 2
  111. textField.clipsToBounds = true
  112. }
  113. }
  114. private var isViewReady: Bool = false
  115. override func didMoveToWindow() {
  116. super.didMoveToWindow()
  117. guard !isViewReady else { return }
  118. constructViewHierarchy()
  119. activateConstraints()
  120. bindInteraction()
  121. isViewReady = true
  122. }
  123. private func constructViewHierarchy() {
  124. addSubview(navigationBar)
  125. navigationBar.addSubview(backButton)
  126. navigationBar.addSubview(titleLabel)
  127. addSubview(searchBar)
  128. addSubview(tableView)
  129. addSubview(bottomView)
  130. bottomView.addSubview(selectedUserView)
  131. bottomView.addSubview(expandButton)
  132. bottomView.addSubview(confirmButton)
  133. addSubview(searchControl)
  134. }
  135. private func activateConstraints() {
  136. navigationBar.snp.makeConstraints { make in
  137. make.top.equalTo(safeAreaLayoutGuide.snp.top)
  138. make.leading.trailing.equalToSuperview()
  139. make.height.equalTo(50)
  140. }
  141. backButton.snp.makeConstraints { make in
  142. make.leading.equalToSuperview().offset(22)
  143. make.centerY.equalToSuperview()
  144. }
  145. titleLabel.snp.makeConstraints { make in
  146. make.centerX.equalToSuperview()
  147. make.centerY.equalToSuperview()
  148. }
  149. searchBar.snp.makeConstraints { make in
  150. make.leading.equalToSuperview().offset(16)
  151. make.trailing.equalToSuperview().offset(-16)
  152. make.top.equalTo(navigationBar.snp.bottom).offset(12)
  153. make.height.equalTo(42)
  154. }
  155. tableView.snp.makeConstraints { make in
  156. make.leading.trailing.equalToSuperview()
  157. make.top.equalTo(searchBar.snp.bottom).offset(16)
  158. make.bottom.equalTo(bottomView.snp.top)
  159. }
  160. bottomView.snp.makeConstraints { make in
  161. make.leading.trailing.bottom.equalToSuperview()
  162. make.height.equalTo(84)
  163. }
  164. confirmButton.snp.makeConstraints { make in
  165. make.trailing.equalToSuperview().offset(-16)
  166. make.top.equalToSuperview().offset(10)
  167. }
  168. selectedUserView.snp.makeConstraints { make in
  169. make.leading.equalToSuperview().offset(32)
  170. make.trailing.equalTo(confirmButton.snp.leading).offset(-16)
  171. make.top.equalToSuperview()
  172. make.height.equalTo(50)
  173. }
  174. expandButton.snp.makeConstraints { make in
  175. make.leading.equalToSuperview().offset(19)
  176. make.top.equalToSuperview().offset(16)
  177. make.width.greaterThanOrEqualTo(103)
  178. }
  179. searchControl.snp.makeConstraints { make in
  180. make.edges.equalToSuperview()
  181. }
  182. }
  183. private func bindInteraction() {
  184. backButton.addTarget(self, action: #selector(onBackButtonTapped(sender:)), for: .touchUpInside)
  185. expandButton.addTarget(self, action: #selector(onExpandButtonTapped(sender:)), for: .touchUpInside)
  186. confirmButton.addTarget(self, action: #selector(onConfirmButtonTapped(sender:)), for: .touchUpInside)
  187. searchBar.delegate = self
  188. let tap = UITapGestureRecognizer(target: self, action: #selector(hideSearchControl(sender:)))
  189. searchControl.addGestureRecognizer(tap)
  190. }
  191. @objc func hideSearchControl(sender: UIView) {
  192. if #available(iOS 13, *) {
  193. searchBar.searchTextField.resignFirstResponder()
  194. } else {
  195. searchBar.resignFirstResponder()
  196. }
  197. searchControl.isHidden = true
  198. }
  199. @objc func onBackButtonTapped(sender: UIButton) {
  200. delegate?.didBackButtonClicked(in: self)
  201. }
  202. @objc func onExpandButtonTapped(sender: UIButton) {
  203. delegate?.didExpandButtonClicked(in: self)
  204. }
  205. @objc func onConfirmButtonTapped(sender: UIButton) {
  206. delegate?.didConfirmButtonClicked(in: self)
  207. }
  208. func updateSelectedView(with count: Int) {
  209. if count <= SelectMemberView.maxVisiableAvatars {
  210. self.expandButton.isHidden = true
  211. self.selectedUserView.isHidden = false
  212. self.selectedUserView.reloadData()
  213. } else {
  214. self.expandButton.isHidden = false
  215. self.selectedUserView.isHidden = true
  216. self.updateExpandButton(with: count)
  217. }
  218. }
  219. func updateConfirmButton(with count: Int) {
  220. if count > 0 {
  221. confirmButton.isEnabled = true
  222. }
  223. if count <= SelectMemberView.maxVisiableAvatars && count > 0 {
  224. let text = .confirmText + "(" + "\(count)" + ")"
  225. confirmButton.setTitle(text, for: .normal)
  226. } else {
  227. confirmButton.setTitle(.confirmText, for: .normal)
  228. }
  229. }
  230. private func updateExpandButton(with count: Int) {
  231. var text = .selectedText + ": " + "\(count)"
  232. if currentLanguage == "zh" || currentLanguage == "zh-Hant" {
  233. text += "人"
  234. }
  235. expandButton.setTitle(text, for: .normal)
  236. let imageWidth = expandButton.imageView?.bounds.size.width ?? 0
  237. let titleWidth = expandButton.titleLabel?.bounds.size.width ?? 0
  238. let spacing: CGFloat = 4
  239. expandButton.titleEdgeInsets = UIEdgeInsets(top: 0,
  240. left: -imageWidth,
  241. bottom: 0,
  242. right: imageWidth + spacing);
  243. expandButton.imageEdgeInsets = UIEdgeInsets(top: 0,
  244. left: titleWidth + spacing,
  245. bottom: 0,
  246. right: -titleWidth)
  247. }
  248. }
  249. extension SelectMemberView: UISearchBarDelegate {
  250. func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
  251. let searchContentText = searchText.trimmingCharacters(in: .whitespaces)
  252. delegate?.selectView(self, didSearchWith: searchContentText)
  253. }
  254. func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
  255. hideSearchControl(sender: searchBar)
  256. }
  257. func searchBarShouldBeginEditing(_ searchBar: UISearchBar) -> Bool {
  258. searchControl.isHidden = false
  259. return true
  260. }
  261. }
  262. class ContactCell: UITableViewCell {
  263. static let reuseIdentifier = "ContactCell"
  264. let checkBox: UIButton = {
  265. let button = UIButton(type: .custom)
  266. button.setImage(UIImage(named: "room_check_mark_unselect", in: tuiRoomKitBundle(), compatibleWith: nil), for: .normal)
  267. button.setImage(UIImage(named: "room_check_mark", in: tuiRoomKitBundle(), compatibleWith: nil), for: .selected)
  268. button.isUserInteractionEnabled = false
  269. return button
  270. }()
  271. let avatarImageView: UIImageView = {
  272. let imgView = UIImageView()
  273. imgView.layer.cornerRadius = 2
  274. imgView.layer.masksToBounds = true
  275. return imgView
  276. }()
  277. let nameLabel: UILabel = {
  278. let label = UILabel()
  279. label.textColor = UIColor.tui_color(withHex: "22262E")
  280. label.textAlignment = .left
  281. label.font = UIFont(name: "PingFangSC-Regular", size: 14)
  282. label.numberOfLines = 1
  283. return label
  284. }()
  285. private var isViewReady = false
  286. override func didMoveToWindow() {
  287. super.didMoveToWindow()
  288. guard !isViewReady else {
  289. return
  290. }
  291. isViewReady = true
  292. selectionStyle = .none
  293. constructViewHierarchy()
  294. activateConstraints()
  295. contentView.backgroundColor = .clear
  296. }
  297. private func constructViewHierarchy() {
  298. contentView.addSubview(checkBox)
  299. contentView.addSubview(avatarImageView)
  300. contentView.addSubview(nameLabel)
  301. }
  302. private func activateConstraints() {
  303. checkBox.snp.makeConstraints { make in
  304. make.leading.equalToSuperview().offset(20)
  305. make.centerY.equalToSuperview()
  306. make.width.height.equalTo(16)
  307. }
  308. avatarImageView.snp.makeConstraints { make in
  309. make.leading.equalTo(checkBox.snp.trailing).offset(6)
  310. make.centerY.equalToSuperview()
  311. make.width.height.equalTo(32)
  312. }
  313. nameLabel.snp.makeConstraints { make in
  314. make.leading.equalTo(avatarImageView.snp.trailing).offset(6)
  315. make.centerY.equalToSuperview()
  316. }
  317. }
  318. func setupViewState(with info: User, isSelected: Bool, isDisaled: Bool) {
  319. let placeholder = UIImage(named: "room_default_avatar_rect", in: tuiRoomKitBundle(), compatibleWith: nil)
  320. if let url = URL(string: info.avatarUrl) {
  321. avatarImageView.sd_setImage(with: url, placeholderImage: placeholder)
  322. } else {
  323. avatarImageView.image = placeholder
  324. }
  325. if !info.userName.isEmpty {
  326. nameLabel.text = info.userName
  327. } else {
  328. nameLabel.text = info.userId
  329. }
  330. checkBox.isSelected = isSelected || isDisaled
  331. if isDisaled {
  332. contentView.alpha = 0.5
  333. } else {
  334. contentView.alpha = 1
  335. }
  336. }
  337. }
  338. class AvatarCell: UICollectionViewCell {
  339. static let reuseIdentifier = "AvatarCell"
  340. let imageView: UIImageView = {
  341. let imageView = UIImageView()
  342. imageView.contentMode = .scaleAspectFill
  343. imageView.clipsToBounds = true
  344. imageView.layer.cornerRadius = 2
  345. return imageView
  346. }()
  347. private var isViewReady = false
  348. override func didMoveToWindow() {
  349. super.didMoveToWindow()
  350. guard !isViewReady else {
  351. return
  352. }
  353. isViewReady = true
  354. constructViewHierarchy()
  355. activateConstraints()
  356. contentView.backgroundColor = .clear
  357. }
  358. private func constructViewHierarchy() {
  359. contentView.addSubview(imageView)
  360. }
  361. private func activateConstraints() {
  362. imageView.snp.makeConstraints { make in
  363. make.top.leading.bottom.trailing.equalTo(contentView)
  364. }
  365. }
  366. func setupViewState(with info: User) {
  367. let placeholder = UIImage(named: "room_default_avatar_rect", in: tuiRoomKitBundle(), compatibleWith: nil)
  368. if let url = URL(string: info.avatarUrl) {
  369. imageView.sd_setImage(with: url, placeholderImage: placeholder)
  370. } else {
  371. imageView.image = placeholder
  372. }
  373. }
  374. }
  375. private extension String {
  376. static let selectMemberText = localized("Select Members")
  377. static let enterUserIdText = localized("Enter userID or username")
  378. static let confirmText = localized("OK")
  379. static let selectedText = localized("Selected")
  380. }