MultiStreamView.swift 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613
  1. //
  2. // MultiStreamView.swift
  3. // TUIRoomKit
  4. //
  5. // Created by CY zhao on 2024/10/22.
  6. //
  7. import Foundation
  8. import RTCRoomEngine
  9. import UIKit
  10. import Factory
  11. import Combine
  12. #if canImport(TXLiteAVSDK_TRTC)
  13. import TXLiteAVSDK_TRTC
  14. #elseif canImport(TXLiteAVSDK_Professional)
  15. import TXLiteAVSDK_Professional
  16. #endif
  17. struct DataChanges {
  18. let deletions: [IndexPath]
  19. let insertions: [IndexPath]
  20. let moves: [(from: IndexPath, to: IndexPath)]
  21. var hasChanges: Bool {
  22. return !deletions.isEmpty || !insertions.isEmpty || !moves.isEmpty
  23. }
  24. }
  25. protocol MultiStreamViewDelegate: AnyObject {
  26. func multiStreamViewDidScroll(_ scrollView: UIScrollView)
  27. func multiStreamViewDidSwitchToSpeechLayout(_ collectionView: UICollectionView)
  28. func multiStreamViewDidSwitchToGridLayout(_ collectionView: UICollectionView)
  29. }
  30. class MultiStreamView: UIView {
  31. @Injected(\.videoStore) var videoStore: VideoStore
  32. private var currentLayoutConfig: VideoLayoutConfig
  33. private var defaultLayoutConfig: VideoLayoutConfig
  34. private let CellID_Normal = "MultiStreamCell_Normal"
  35. private var isViewReady: Bool = false
  36. private var cancellables = Set<AnyCancellable>()
  37. @Published private(set) var dataSource: [UserInfo] = []
  38. @Published private var excludeVideoItems: [UserInfo] = []
  39. @Published private var speechItem: UserInfo?
  40. weak var delegate: MultiStreamViewDelegate?
  41. var engineManager: EngineManager {
  42. EngineManager.shared
  43. }
  44. // TODO: remove roomstore
  45. var roomStore: RoomStore {
  46. EngineManager.shared.store
  47. }
  48. var currentUserId: String {
  49. roomStore.currentUser.userId
  50. }
  51. private var itemStreamType: TUIVideoStreamType {
  52. if videoStore.videoState.videoSeatItems.filter({ $0.hasVideoStream }).count > 5 {
  53. return .cameraStreamLow
  54. } else {
  55. return .cameraStream
  56. }
  57. }
  58. init(maxRows: Int, maxColumns: Int) {
  59. let config = VideoLayoutConfig(maxRows: maxRows, maxColumns: maxColumns, spacing: 5.0, aspectRatio: 0)
  60. self.currentLayoutConfig = config
  61. self.defaultLayoutConfig = config
  62. super.init(frame: .zero)
  63. setupBindings()
  64. }
  65. convenience init() {
  66. self.init(maxRows: 2, maxColumns: 3)
  67. }
  68. var gridContentOffset: CGPoint {
  69. return attendeeCollectionView.contentOffset
  70. }
  71. var streamSorter: MultiStreamsSorter {
  72. return MultiStreamsSorter(currentUserId: roomStore.currentUser.userId)
  73. }
  74. func setExcludeVideoItems(items: [UserInfo]) {
  75. self.excludeVideoItems = items
  76. }
  77. func setSpeechItem(item: UserInfo?) {
  78. self.speechItem = item
  79. }
  80. func reset() {
  81. self.setSpeechItem(item: nil)
  82. self.excludeVideoItems = []
  83. }
  84. private func setupBindings() {
  85. let speechItemChanges = $speechItem
  86. speechItemChanges
  87. .debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
  88. .sink { [weak self] speechItem in
  89. guard let self = self else { return }
  90. if let layout = attendeeCollectionView.collectionViewLayout as? MultiStreamViewLayout {
  91. layout.isSpeechMode = speechItem != nil
  92. }
  93. self.attendeeCollectionView.collectionViewLayout.invalidateLayout()
  94. }
  95. .store(in: &cancellables)
  96. Publishers.CombineLatest3(
  97. videoStore.subscribe(Selector(keyPath: \VideoSeatState.videoSeatItems)).removeDuplicates(),
  98. $excludeVideoItems,
  99. speechItemChanges
  100. )
  101. .map { [weak self] videoItems, excludeInfos, speechItem -> [UserInfo] in
  102. guard let self = self else { return videoItems }
  103. let sortedItems = streamSorter.sortStreams(videoItems)
  104. var items = sortedItems.filter { item in
  105. let excluded = excludeInfos.contains(where: { $0.userId == item.userId })
  106. let notSpeechItem = speechItem.map { speech in
  107. !(item.userId == speech.userId &&
  108. item.videoStreamType == speech.videoStreamType)
  109. } ?? true
  110. return !excluded && notSpeechItem
  111. }
  112. if let speechItem = speechItem {
  113. items.insert(speechItem, at: 0)
  114. }
  115. return items
  116. }
  117. .removeDuplicates()
  118. .receive(on: RunLoop.main)
  119. .debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
  120. .sink { [weak self] videoItems in
  121. guard let self = self else { return }
  122. self.handleDataChanged(videoItems)
  123. }
  124. .store(in: &cancellables)
  125. }
  126. private func handleDataChanged(_ newItems: [UserInfo]) {
  127. let changes = calculateChanges(old: dataSource, new: newItems)
  128. guard changes.hasChanges else {return}
  129. freshCollectionView { [ weak self] in
  130. guard let self = self else { return }
  131. self.attendeeCollectionView.performBatchUpdates {
  132. self.dataSource = newItems
  133. self.attendeeCollectionView.deleteItems(at: changes.deletions)
  134. self.attendeeCollectionView.insertItems(at: changes.insertions)
  135. changes.moves.forEach { move in
  136. self.attendeeCollectionView.moveItem(at: move.from, to: move.to)
  137. }
  138. }
  139. }
  140. }
  141. private var pageControl: UIPageControl = {
  142. let control = UIPageControl()
  143. control.currentPage = 0
  144. control.numberOfPages = 1
  145. control.hidesForSinglePage = true
  146. control.isUserInteractionEnabled = false
  147. return control
  148. }()
  149. override func didMoveToWindow() {
  150. super.didMoveToWindow()
  151. guard !isViewReady else { return }
  152. constructViewHierarchy()
  153. activateConstraints()
  154. bindInteraction()
  155. isViewReady = true
  156. }
  157. override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
  158. super.traitCollectionDidChange(previousTraitCollection)
  159. if traitCollection.verticalSizeClass != previousTraitCollection?.verticalSizeClass ||
  160. traitCollection.horizontalSizeClass != previousTraitCollection?.horizontalSizeClass {
  161. let offsetYu = Int(attendeeCollectionView.contentOffset.x) % Int(attendeeCollectionView.mm_w)
  162. let offsetMuti = CGFloat(offsetYu) / attendeeCollectionView.mm_w
  163. let currentPage = (offsetMuti > 0.5 ? 1 : 0) + (Int(attendeeCollectionView.contentOffset.x) / Int(attendeeCollectionView.mm_w))
  164. attendeeCollectionView.setContentOffset(
  165. CGPoint(x: CGFloat(pageControl.currentPage) * attendeeCollectionView.frame.size.width,
  166. y: attendeeCollectionView.contentOffset.y), animated: false)
  167. }
  168. }
  169. required init?(coder: NSCoder) {
  170. fatalError("init(coder:) has not been implemented")
  171. }
  172. lazy var attendeeCollectionView: UICollectionView = {
  173. let layout = MultiStreamViewLayout(config: currentLayoutConfig)
  174. layout.delegate = self
  175. let collection = AttendeeListView(frame: CGRect(x: 0, y: 0, width: frame.size.width, height: frame.size.height), collectionViewLayout: layout)
  176. collection.register(MultiStreamCell.self, forCellWithReuseIdentifier: CellID_Normal)
  177. collection.isPagingEnabled = true
  178. collection.showsVerticalScrollIndicator = false
  179. collection.showsHorizontalScrollIndicator = false
  180. collection.isUserInteractionEnabled = true
  181. collection.contentMode = .scaleToFill
  182. collection.backgroundColor = UIColor(0x0F1014)
  183. if #available(iOS 11.0, *) {
  184. collection.contentInsetAdjustmentBehavior = .never
  185. } else {
  186. // Fallback on earlier versions
  187. }
  188. if #available(iOS 10.0, *) {
  189. collection.isPrefetchingEnabled = true
  190. } else {
  191. // Fallback on earlier versions
  192. }
  193. collection.dataSource = self
  194. collection.delegate = self
  195. collection.layoutDelegate = self
  196. return collection
  197. }()
  198. let placeholderView: UIView = {
  199. let view = UIView(frame: .zero)
  200. view.isHidden = true
  201. return view
  202. }()
  203. func constructViewHierarchy() {
  204. backgroundColor = .clear
  205. addSubview(placeholderView)
  206. addSubview(attendeeCollectionView)
  207. addSubview(pageControl)
  208. }
  209. func activateConstraints() {
  210. placeholderView.snp.makeConstraints { make in
  211. make.edges.equalToSuperview()
  212. }
  213. attendeeCollectionView.snp.makeConstraints { make in
  214. make.edges.equalToSuperview()
  215. }
  216. pageControl.snp.makeConstraints { make in
  217. make.height.equalTo(24)
  218. make.centerX.equalToSuperview()
  219. make.bottom.equalToSuperview().offset(-5)
  220. }
  221. }
  222. func bindInteraction() {
  223. addGesture()
  224. }
  225. private func addGesture() {
  226. let tap = UITapGestureRecognizer(target: self, action: #selector(clickVideoSeat))
  227. addGestureRecognizer(tap)
  228. }
  229. @objc private func clickVideoSeat() {
  230. EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_ChangeToolBarHiddenState, param: [:])
  231. guard RoomRouter.shared.hasChatWindow() else { return }
  232. EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_HiddenChatWindow, param: [:])
  233. }
  234. func updatePageControl() {
  235. let offsetYu = Int(attendeeCollectionView.contentOffset.x) % Int(attendeeCollectionView.mm_w)
  236. let offsetMuti = CGFloat(offsetYu) / attendeeCollectionView.mm_w
  237. pageControl.currentPage = (offsetMuti > 0.5 ? 1 : 0) + (Int(attendeeCollectionView.contentOffset.x) / Int(attendeeCollectionView.mm_w))
  238. let cellArray = attendeeCollectionView.visibleCells
  239. for cell in cellArray {
  240. if let seatCell = cell as? MultiStreamCell, let seatItem = seatCell.videoItem {
  241. if pageControl.currentPage != 0 && seatItem.hasVideoStream {
  242. self.startPlayVideoStream(item: seatItem, renderView: seatCell.renderView)
  243. }
  244. }
  245. }
  246. }
  247. func updateLayout(_ config: VideoLayoutConfig, animated: Bool = true) {
  248. let layout = MultiStreamViewLayout(config: config)
  249. currentLayoutConfig = config
  250. if animated {
  251. UIView.animate(withDuration: 0.3) {
  252. self.attendeeCollectionView.setCollectionViewLayout(layout, animated: true)
  253. self.updateCollectionViewBehavior()
  254. self.updatePageControlState()
  255. }
  256. } else {
  257. attendeeCollectionView.setCollectionViewLayout(layout, animated: false)
  258. updateCollectionViewBehavior()
  259. updatePageControlState()
  260. }
  261. }
  262. private func updateCollectionViewBehavior() {
  263. attendeeCollectionView.isPagingEnabled = currentLayoutConfig.isPagingEnable
  264. attendeeCollectionView.alwaysBounceHorizontal = currentLayoutConfig.isHorizontalScroll
  265. attendeeCollectionView.alwaysBounceVertical = currentLayoutConfig.isVerticalScroll
  266. }
  267. private func updatePageControlState() {
  268. if currentLayoutConfig.isPagingEnable {
  269. let itemsPerPage = currentLayoutConfig.maxRows * currentLayoutConfig.maxColumns
  270. let numberOfPages = Int(ceil(Double(dataSource.count) / Double(itemsPerPage)))
  271. pageControl.isHidden = numberOfPages <= 1
  272. pageControl.numberOfPages = numberOfPages
  273. } else {
  274. pageControl.isHidden = true
  275. }
  276. }
  277. deinit {
  278. debugPrint("deinit \(self)")
  279. }
  280. }
  281. // MARK: - TUIVideoSeatViewModelResponder
  282. extension MultiStreamView {
  283. private func calculateChanges(old: [UserInfo], new: [UserInfo]) -> DataChanges {
  284. var deletions: [IndexPath] = []
  285. var insertions: [IndexPath] = []
  286. var moves: [(from: IndexPath, to: IndexPath)] = []
  287. let oldKeys = old.map { "\($0.userId)_\($0.videoStreamType.rawValue)" }
  288. let newKeys = new.map { "\($0.userId)_\($0.videoStreamType.rawValue)" }
  289. let oldKeyToIndex = Dictionary(uniqueKeysWithValues: oldKeys.enumerated().map { ($1, $0) })
  290. let newKeyToIndex = Dictionary(uniqueKeysWithValues: newKeys.enumerated().map { ($1, $0) })
  291. let deletedKeys = Set(oldKeys).subtracting(newKeys)
  292. let insertedKeys = Set(newKeys).subtracting(oldKeys)
  293. let retainedKeys = Set(oldKeys).intersection(newKeys)
  294. deletions = deletedKeys.compactMap { oldKeyToIndex[$0] }.map { IndexPath(item: $0, section: 0) }.sorted { $0.item > $1.item }
  295. insertions = insertedKeys.compactMap { newKeyToIndex[$0] }.map { IndexPath(item: $0, section: 0) }.sorted { $0.item < $1.item }
  296. var processedIndices = Set<String>()
  297. for key in retainedKeys {
  298. guard let oldIndex = oldKeyToIndex[key],
  299. let newIndex = newKeyToIndex[key],
  300. oldIndex != newIndex,
  301. !processedIndices.contains(key) else {
  302. continue
  303. }
  304. let fromPath = IndexPath(item: oldIndex, section: 0)
  305. let toPath = IndexPath(item: newIndex, section: 0)
  306. if !deletions.contains(fromPath) && !insertions.contains(toPath) {
  307. moves.append((from: fromPath, to: toPath))
  308. processedIndices.insert(key)
  309. }
  310. }
  311. moves.sort { $0.from.item < $1.from.item }
  312. return DataChanges(
  313. deletions: deletions,
  314. insertions: insertions,
  315. moves: moves
  316. )
  317. }
  318. private func freshCollectionView(block: () -> Void) {
  319. CATransaction.begin()
  320. CATransaction.setDisableActions(true)
  321. block()
  322. CATransaction.commit()
  323. }
  324. func reloadData() {
  325. freshCollectionView { [weak self] in
  326. guard let self = self else { return }
  327. self.attendeeCollectionView.reloadData()
  328. }
  329. }
  330. func getVideoVisibleCell(_ item: VideoSeatItem) -> VideoSeatCell? {
  331. let cellArray = attendeeCollectionView.visibleCells
  332. guard let cell = cellArray.first(where: { cell in
  333. if let seatCell = cell as? VideoSeatCell, seatCell.seatItem == item {
  334. return true
  335. } else {
  336. return false
  337. }
  338. }) as? VideoSeatCell else { return nil }
  339. return cell
  340. }
  341. }
  342. // MARK: - UICollectionViewDelegateFlowLayout
  343. extension MultiStreamView: UICollectionViewDelegateFlowLayout {
  344. func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
  345. guard let seatCell = cell as? MultiStreamCell else { return }
  346. guard let dataItem = dataSource[safe: indexPath.item] else { return }
  347. guard let seatItem = seatCell.videoItem else { return }
  348. if dataItem.userId != seatItem.userId {
  349. seatCell.reset()
  350. seatCell.updateUI(item: dataItem)
  351. bindVideoState(cell: seatCell, with: dataItem)
  352. } else {
  353. if seatItem.hasVideoStream || seatItem.videoStreamType == .screenStream {
  354. self.startPlayVideoStream(item: seatItem, renderView: seatCell.renderView)
  355. } else {
  356. self.stopPlayVideoStream(item: seatItem)
  357. }
  358. }
  359. }
  360. func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
  361. guard let seatCell = cell as? MultiStreamCell else { return }
  362. if let seatItem = seatCell.videoItem {
  363. self.stopPlayVideoStream(item: seatItem)
  364. }
  365. }
  366. }
  367. extension MultiStreamView: UIScrollViewDelegate {
  368. func scrollViewDidScroll(_ scrollView: UIScrollView) {
  369. delegate?.multiStreamViewDidScroll(scrollView)
  370. }
  371. func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
  372. updatePageControl()
  373. }
  374. }
  375. // MARK: - UICollectionViewDataSource
  376. extension MultiStreamView: UICollectionViewDataSource {
  377. func numberOfSections(in collectionView: UICollectionView) -> Int {
  378. return 1
  379. }
  380. func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
  381. return dataSource.count
  382. }
  383. func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
  384. guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier:CellID_Normal, for: indexPath)
  385. as? MultiStreamCell else {
  386. return UICollectionViewCell()
  387. }
  388. if let item = dataSource[safe: indexPath.item] {
  389. cell.updateUI(item: item)
  390. bindVideoState(cell:cell, with: item)
  391. }
  392. return cell
  393. }
  394. private func bindVideoState(cell: MultiStreamCell, with item: UserInfo) {
  395. let userId = item.userId
  396. let videoItemPublisher = createVideoItemPublisher(for: userId, item: item)
  397. videoItemPublisher
  398. .receive(on: RunLoop.main)
  399. .dropFirst()
  400. .removeDuplicates { oldItem, newItem in
  401. oldItem.hasAudioStream == newItem.hasAudioStream &&
  402. oldItem.userVoiceVolume == newItem.userVoiceVolume
  403. }
  404. .sink { [weak cell] item in
  405. guard let cell = cell else { return }
  406. cell.updateUIVolume(item: item)
  407. }
  408. .store(in: &cell.cancellableSet)
  409. videoItemPublisher
  410. .receive(on: RunLoop.main)
  411. .removeDuplicates { oldItem, newItem in
  412. oldItem.hasVideoStream == newItem.hasVideoStream &&
  413. oldItem.videoStreamType == newItem.videoStreamType
  414. }
  415. .sink { [weak cell, weak self] item in
  416. guard let cell = cell else { return }
  417. guard let self = self else { return }
  418. if isCellVisible(cell) {
  419. if item.hasVideoStream || item.videoStreamType == .screenStream {
  420. self.startPlayVideoStream(item: item, renderView: cell.renderView)
  421. } else {
  422. self.stopPlayVideoStream(item: item)
  423. }
  424. }
  425. cell.updateUI(item: item)
  426. }
  427. .store(in: &cell.cancellableSet)
  428. videoItemPublisher
  429. .receive(on: RunLoop.main)
  430. .removeDuplicates { oldItem, newItem in
  431. oldItem.userName == newItem.userName &&
  432. oldItem.userRole == newItem.userRole
  433. }
  434. .sink { [weak cell] item in
  435. guard let cell = cell else { return }
  436. cell.updateUI(item: item)
  437. }
  438. .store(in: &cell.cancellableSet)
  439. }
  440. func createVideoItemPublisher(for userId: String, item: UserInfo) -> AnyPublisher<UserInfo, Never> {
  441. let seatItemsPublisher = videoStore.subscribe(Selector(keyPath: \VideoSeatState.videoSeatItems))
  442. .map { items -> UserInfo? in
  443. items.first { $0.userId == userId}
  444. }
  445. .eraseToAnyPublisher()
  446. let shareItemPublisher = videoStore.subscribe(Selector(keyPath: \VideoSeatState.shareItem))
  447. .map { shareItem -> UserInfo? in
  448. if let shareItem = shareItem, shareItem.userId == userId {
  449. return shareItem
  450. }
  451. return nil
  452. }
  453. .eraseToAnyPublisher()
  454. return (item.videoStreamType == .screenStream ? shareItemPublisher : seatItemsPublisher)
  455. .compactMap { $0 }
  456. .receive(on: RunLoop.main)
  457. .share()
  458. .eraseToAnyPublisher()
  459. }
  460. private func isCellVisible(_ cell: UICollectionViewCell) -> Bool {
  461. let visibleCells = attendeeCollectionView.visibleCells
  462. return visibleCells.contains(cell)
  463. }
  464. }
  465. // MARK: - UICollectionViewDataSource
  466. extension MultiStreamView {
  467. func startPlayVideoStream(item: UserInfo, renderView: UIView?) {
  468. guard let renderView = renderView else { return }
  469. var item = item
  470. if item.userId != currentUserId {
  471. item.videoStreamType = item.videoStreamType == .screenStream ? .screenStream : itemStreamType
  472. engineManager.setRemoteVideoView(userId: item.userId, streamType: item.videoStreamType, view: renderView)
  473. engineManager.startPlayRemoteVideo(userId: item.userId, streamType: item.videoStreamType)
  474. } else if item.hasVideoStream {
  475. engineManager.setLocalVideoView(renderView)
  476. }
  477. }
  478. func stopPlayVideoStream(item: UserInfo) {
  479. if item.userId == currentUserId {
  480. engineManager.setLocalVideoView(nil)
  481. } else {
  482. engineManager.setRemoteVideoView(userId: item.userId, streamType: item.videoStreamType, view: nil)
  483. if item.videoStreamType == .screenStream {
  484. engineManager.stopPlayRemoteVideo(userId: item.userId, streamType: .screenStream)
  485. } else {
  486. engineManager.stopPlayRemoteVideo(userId: item.userId, streamType: .cameraStream)
  487. engineManager.stopPlayRemoteVideo(userId: item.userId, streamType: .cameraStreamLow)
  488. }
  489. }
  490. }
  491. }
  492. extension MultiStreamView: MultiStreamLayoutDelegate {
  493. func updateNumberOfPages(numberOfPages: NSInteger) {
  494. pageControl.numberOfPages = numberOfPages
  495. }
  496. }
  497. extension MultiStreamView: AttendeeListViewDelegate {
  498. func attendListDidSwitchToFullScreenLayout(_ attendeeList: UICollectionView) {
  499. delegate?.multiStreamViewDidSwitchToSpeechLayout(attendeeList)
  500. }
  501. func attendListDidSwitchToGridLayout(_ attendeeList: UICollectionView) {
  502. delegate?.multiStreamViewDidSwitchToGridLayout(attendeeList)
  503. }
  504. }
  505. fileprivate protocol AttendeeListViewDelegate: AnyObject {
  506. func attendListDidSwitchToFullScreenLayout(_ attendeeList: UICollectionView)
  507. func attendListDidSwitchToGridLayout(_ attendeeList: UICollectionView)
  508. }
  509. fileprivate class AttendeeListView: UICollectionView {
  510. weak var layoutDelegate: AttendeeListViewDelegate?
  511. override func layoutSubviews() {
  512. super.layoutSubviews()
  513. if isSwitchToFullScreenLayout() {
  514. handleFullScreenLayout()
  515. } else {
  516. handleGridLayout()
  517. }
  518. }
  519. private func isSwitchToFullScreenLayout() -> Bool {
  520. if let indexPath = indexPathForItem(at: CGPoint(x: contentOffset.x, y: contentOffset.y)),
  521. indexPath.section == 0, indexPath.item == 0 {
  522. return true
  523. }
  524. return false
  525. }
  526. private func handleFullScreenLayout() {
  527. self.layoutDelegate?.attendListDidSwitchToFullScreenLayout(self)
  528. }
  529. private func handleGridLayout() {
  530. self.layoutDelegate?.attendListDidSwitchToGridLayout(self)
  531. }
  532. }