LNIMChatGameMateSkillView.swift 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. //
  2. // LNIMChatGameMateSkillView.swift
  3. // Lanu
  4. //
  5. // Created by OneeChan on 2025/12/11.
  6. //
  7. import Foundation
  8. import UIKit
  9. import SnapKit
  10. import Combine
  11. class LNIMChatGameMateSkillView: UIView {
  12. private let scrollView = UIScrollView()
  13. private let stackView = UIStackView()
  14. private var itemViews: [LNIMChatGameMateSkillCell] = []
  15. private var indicatorContainer = UIStackView()
  16. private var indicators: [UIView] = []
  17. private var curPage: Int = 0 {
  18. didSet {
  19. if curPage != oldValue {
  20. for (index, indicator) in indicators.enumerated() {
  21. indicator.backgroundColor = index == curPage ? .text_6 : .init(hex: "#9FCD9B")
  22. }
  23. }
  24. }
  25. }
  26. private var skills: [LNGameMateSkillVO] = []
  27. var curSkill: LNGameMateSkillVO {
  28. skills[curPage]
  29. }
  30. var defaultId: String?
  31. weak var viewModel: LNIMChatViewModel? {
  32. didSet {
  33. viewModel?.$myOrders.sink { [weak self] list in
  34. guard let self else { return }
  35. isHidden = viewModel?.userInfo?.playmate != true
  36. || viewModel?.peerSkills.isEmpty != false
  37. || !list.isEmpty
  38. }.store(in: &cancellables)
  39. viewModel?.$peerSkills.sink { [weak self] skills in
  40. guard let self else { return }
  41. isHidden = viewModel?.userInfo?.playmate != true
  42. || skills.isEmpty
  43. || viewModel?.myOrders.isEmpty != true
  44. self.skills = skills
  45. buildSkillViews()
  46. }.store(in: &cancellables)
  47. viewModel?.$userInfo.sink { [weak self] info in
  48. guard let self else { return }
  49. isHidden = info?.playmate != true
  50. || viewModel?.peerSkills.isEmpty != false
  51. || viewModel?.myOrders.isEmpty != true
  52. }.store(in: &cancellables)
  53. }
  54. }
  55. override init(frame: CGRect) {
  56. super.init(frame: frame)
  57. setupViews()
  58. }
  59. required init?(coder: NSCoder) {
  60. fatalError("init(coder:) has not been implemented")
  61. }
  62. }
  63. extension LNIMChatGameMateSkillView: LNIMChatGameMateSkillCellDelegate {
  64. func onIMChatGameMateSkillCell(cell: LNIMChatGameMateSkillCell, didClickOrder skill: LNGameMateSkillVO) {
  65. guard let userInfo = viewModel?.userInfo else { return }
  66. let panel = LNCreateOrderPanel()
  67. panel.update(skill, user: userInfo)
  68. panel.editable = true
  69. panel.popup()
  70. }
  71. }
  72. extension LNIMChatGameMateSkillView: UIScrollViewDelegate {
  73. func scrollViewDidScroll(_ scrollView: UIScrollView) {
  74. guard skills.count > 2 else {
  75. curPage = Int(scrollView.contentOffset.x / scrollView.bounds.width)
  76. return
  77. }
  78. if scrollView.contentOffset.x < scrollView.bounds.width * 0.5 {
  79. curPage -= 1
  80. if curPage < 0 {
  81. curPage = skills.count - 1
  82. }
  83. let last = itemViews.removeLast()
  84. stackView.removeArrangedSubview(last)
  85. last.removeFromSuperview()
  86. itemViews.insert(last, at: 0)
  87. stackView.insertArrangedSubview(last, at: 0)
  88. last.snp.makeConstraints { make in
  89. make.width.equalTo(scrollView)
  90. make.height.equalToSuperview()
  91. }
  92. let skill = skills[curPage > 0 ? curPage - 1 : skills.count - 1]
  93. last.update(skill)
  94. scrollView.contentOffset.x = scrollView.contentOffset.x + scrollView.bounds.width
  95. } else if scrollView.contentOffset.x > scrollView.bounds.width * 1.5 {
  96. curPage += 1
  97. if curPage > skills.count - 1 {
  98. curPage = 0
  99. }
  100. let first = itemViews.removeFirst()
  101. stackView.removeArrangedSubview(first)
  102. first.removeFromSuperview()
  103. itemViews.append(first)
  104. stackView.addArrangedSubview(first)
  105. first.snp.makeConstraints { make in
  106. make.width.equalTo(scrollView)
  107. make.height.equalToSuperview()
  108. }
  109. let skill = skills[curPage < skills.count - 1 ? curPage + 1 : 0]
  110. first.update(skill)
  111. scrollView.contentOffset.x = scrollView.contentOffset.x - scrollView.bounds.width
  112. }
  113. }
  114. }
  115. extension LNIMChatGameMateSkillView {
  116. private func buildSkillViews() {
  117. let defaultIndex: Int = if let defaultId {
  118. skills.firstIndex(where: { $0.id == defaultId }) ?? 0
  119. } else {
  120. 0
  121. }
  122. if skills.count < 3 {
  123. scrollView.setContentOffset(.init(x: 0, y: 0), animated: false)
  124. for (index, skill) in skills.enumerated() {
  125. itemViews[index].isHidden = false
  126. itemViews[index].update(skill)
  127. }
  128. for index in skills.count..<itemViews.count {
  129. itemViews[index].isHidden = true
  130. }
  131. } else {
  132. scrollView.setContentOffset(.init(x: scrollView.bounds.width, y: 0), animated: false)
  133. for index in 0..<itemViews.count {
  134. itemViews[index].isHidden = false
  135. }
  136. itemViews[0].update(skills[defaultIndex - 1 >= 0 ? defaultIndex - 1 : skills.count - 1])
  137. itemViews[1].update(skills[defaultIndex])
  138. itemViews[2].update(skills[defaultIndex + 1 > skills.count - 1 ? 0 : defaultIndex + 1])
  139. }
  140. indicatorContainer.arrangedSubviews.forEach {
  141. indicatorContainer.removeArrangedSubview($0)
  142. $0.removeFromSuperview()
  143. }
  144. indicators.removeAll()
  145. for index in 0..<skills.count {
  146. let indicator = buildIndicator()
  147. indicator.backgroundColor = index == defaultIndex ? .text_6 : .init(hex: "#9FCD9B")
  148. indicatorContainer.addArrangedSubview(indicator)
  149. indicators.append(indicator)
  150. }
  151. curPage = defaultIndex
  152. // 移除默认定位
  153. if !skills.isEmpty {
  154. defaultId = nil
  155. }
  156. }
  157. private func setupViews() {
  158. clipsToBounds = true
  159. scrollView.backgroundColor = .clear
  160. scrollView.clipsToBounds = false
  161. scrollView.isPagingEnabled = true
  162. scrollView.showsVerticalScrollIndicator = false
  163. scrollView.showsHorizontalScrollIndicator = false
  164. scrollView.delegate = self
  165. addSubview(scrollView)
  166. scrollView.snp.makeConstraints { make in
  167. make.horizontalEdges.equalToSuperview().inset(14)
  168. make.bottom.equalToSuperview()
  169. make.top.equalToSuperview().offset(8)
  170. make.height.equalTo(72)
  171. }
  172. let fakeView = UIView()
  173. scrollView.addSubview(fakeView)
  174. fakeView.snp.makeConstraints { make in
  175. make.verticalEdges.equalToSuperview()
  176. make.leading.equalToSuperview()
  177. make.width.equalTo(0)
  178. }
  179. stackView.axis = .horizontal
  180. stackView.spacing = 0
  181. scrollView.addSubview(stackView)
  182. stackView.snp.makeConstraints { make in
  183. make.edges.equalToSuperview()
  184. make.height.equalToSuperview()
  185. }
  186. for _ in 0..<3 {
  187. let itemView = LNIMChatGameMateSkillCell()
  188. itemView.delegate = self
  189. stackView.addArrangedSubview(itemView)
  190. itemView.snp.makeConstraints { make in
  191. make.width.equalTo(scrollView)
  192. make.height.equalToSuperview()
  193. }
  194. itemViews.append(itemView)
  195. }
  196. indicatorContainer.axis = .horizontal
  197. indicatorContainer.spacing = 6
  198. addSubview(indicatorContainer)
  199. indicatorContainer.snp.makeConstraints { make in
  200. make.centerX.equalToSuperview()
  201. make.size.equalTo(0).priority(.low)
  202. make.bottom.equalToSuperview().offset(-6.5)
  203. }
  204. }
  205. private func buildIndicator() -> UIView {
  206. let dot = UIView()
  207. dot.layer.cornerRadius = 2.5
  208. dot.snp.makeConstraints { make in
  209. make.width.height.equalTo(5)
  210. }
  211. return dot
  212. }
  213. }