// // VIPSubscribeSheet.swift // Crush // // Created by Leon on 2025/9/16. // class VIPSubscribeSheet: EGPopBaseView { var bgImageView: AutoRatioImageView! var titleLabel: UILabel! var closeButton: EPIconTertiaryButton! var privilegesContainer: UIView! var effectView: UIVisualEffectView! var layout = UICollectionViewFlowLayout() var cv: UICollectionView! var leftButton: EPIconTertiaryDarkButton! var rightButton: EPIconTertiaryDarkButton! var agreenmentContainer: UIView! var operateButton: StyleButton! var tableView: UITableView! // MARK: - Data Properties private var vipProducts: [VIPSubcribeProduct] = [] private var privileges: [MemberPrivDict] = [] private var currentRealCvIndex = 0 /// 0~10 private var currentPrivilegeIndex = 0 private var selectedProductIndex = 0 private var multiplier = 100 // 用于无限循环的倍数 private var initialScrollCompleted = false // 标记是否已完成初始滚动 init() { super.init(direction: .bottom) contentView.backgroundColor = .c.csbn contentLength = 693 + UIWindow.safeAreaBottom setupViews() setupData() setupEvent() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } // MARK: - Data private func setupData(){ // Group load Hud Hud.showIndicator() let group = DispatchGroup() group.enter() CLPurchase.shared.loadVIPTiersProducts { [weak self] products in group.leave() // 配置tableView中,充值档位 if let products = products { self?.vipProducts = products DispatchQueue.main.async { self?.tableView.reloadData() } } } group.enter() CLPurchase.shared.loadCurrentUserVipDetail { [weak self] output in group.leave() // get output.memberPrivList 权限展示列表,并显示在cv中 if let output = output, let privileges = output.memberPrivList { self?.privileges = privileges DispatchQueue.main.async { self?.cv.reloadData() self?.updateNavigationButtons() self?.initialScrollToMiddle() // 初始化时滚动到中间位置 } } } group.notify(queue: .main) { // 所有 enter/leave 成对后,回调执行 print("All completed") Hud.hideIndicator() } } private func setupEvent(){ NotificationCenter.default.addObserver(self, selector: #selector(notifyVIPChanged(_:)), name: AppNotificationName.vipStateChange.notificationName, object: nil) } @objc private func notifyVIPChanged(_ noti: Notification){ dismiss() } // MARK: - Functions private func updateNavigationButtons() { leftButton.isHidden = privileges.count <= 1 rightButton.isHidden = privileges.count <= 1 } private func scrollToPrivilege(at index: Int) { guard index >= 0 && index < privileges.count else { return } currentPrivilegeIndex = index // 计算当前应该滚动到的位置 let totalItems = privileges.count * multiplier let middleStart = (totalItems / 2) - (totalItems / 2) % privileges.count let targetIndex = middleStart + index currentRealCvIndex = targetIndex let indexPath = IndexPath(item: targetIndex, section: 0) cv.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true) } private func initialScrollToMiddle() { guard privileges.count > 0 && !initialScrollCompleted else { return } let totalItems = privileges.count * multiplier let middleStart = (totalItems / 2) - (totalItems / 2) % privileges.count currentRealCvIndex = middleStart let indexPath = IndexPath(item: middleStart, section: 0) DispatchQueue.main.async { self.cv.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: false) self.initialScrollCompleted = true } } // MARK: - Action @objc private func leftButtonPressed() { guard privileges.count > 0 else { return } // 计算下一个目标位置(相邻位置) let nextRealIndex = currentRealCvIndex - 1 // 检查是否需要边界处理 let totalItems = privileges.count * multiplier let middleStart = (totalItems / 2) - (totalItems / 2) % privileges.count if nextRealIndex < 0 { // 如果超出左边界,跳转到右侧对应位置 let targetIndex = totalItems - 1 currentRealCvIndex = targetIndex currentPrivilegeIndex = targetIndex % privileges.count let indexPath = IndexPath(item: targetIndex, section: 0) cv.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true) } else { // 正常向左切换 currentRealCvIndex = nextRealIndex currentPrivilegeIndex = nextRealIndex % privileges.count let indexPath = IndexPath(item: nextRealIndex, section: 0) cv.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true) } } @objc private func rightButtonPressed() { guard privileges.count > 0 else { return } // 计算下一个目标位置(相邻位置) let nextRealIndex = currentRealCvIndex + 1 // 检查是否需要边界处理 let totalItems = privileges.count * multiplier if nextRealIndex >= totalItems { // 如果超出右边界,跳转到左侧对应位置 let targetIndex = 0 currentRealCvIndex = targetIndex currentPrivilegeIndex = targetIndex % privileges.count let indexPath = IndexPath(item: targetIndex, section: 0) cv.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true) } else { // 正常向右切换 currentRealCvIndex = nextRealIndex currentPrivilegeIndex = nextRealIndex % privileges.count let indexPath = IndexPath(item: nextRealIndex, section: 0) cv.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true) } } @objc private func termsOfServicePressed() { AppRouter.goTermsOfService() dismiss() } @objc private func privacyPolicyPressed() { AppRouter.goPrivacyPolicy() dismiss() } @objc private func tapBottomButton() { // 开始续期订阅... guard selectedProductIndex < vipProducts.count else { return } let selectedProduct = vipProducts[selectedProductIndex] print("选择的产品: \(selectedProduct.productId ?? "")") guard let productId = selectedProduct.productId, let payment = selectedProduct.payAmount else{ return } let product = IAPProducts() product.productId = productId product.chargeAmount = payment IAPCore.shared.requestProducts([product]) {iapIds in if iapIds.count > 0 { let tradeId = String.randomNumber(length: 10) IAPCore.shared.addPayProductId(productId: productId, tradeId: tradeId) } } } // MARK: - UI private func setupViews() { bgImageView = { let v = AutoRatioImageView() v.setImage(UIImage(named: "vip_sheet_bg")) contentView.addSubview(v) v.snp.makeConstraints { make in make.top.leading.trailing.equalToSuperview() } return v }() closeButton = { let v = EPIconTertiaryButton(radius: .round, iconSize: .small, iconCode: .delete) v.addTarget(self, action: #selector(bgButtonPressed), for: .touchUpInside) contentView.addSubview(v) v.snp.makeConstraints { make in make.top.equalToSuperview().offset(20) make.size.equalTo(v.bgImageSize()) make.trailing.equalToSuperview().offset(-16) } return v }() titleLabel = { let v = UILabel() v.font = .t.ttm v.textColor = .text v.textAlignment = .center contentView.addSubview(v) v.snp.makeConstraints { make in make.leading.equalToSuperview().offset(24) make.trailing.equalToSuperview().offset(-24) make.top.equalToSuperview().offset(24) } v.text = "Crushlevel VIP" return v }() setupPrivilegeView() setupAgreementsView() operateButton = { let v = StyleButton() v.primary(size: .large) contentView.addSubview(v) v.snp.makeConstraints { make in make.leading.equalToSuperview().offset(CGFloat.lrs) make.trailing.equalToSuperview().offset(-CGFloat.lrs) make.bottom.equalTo(agreenmentContainer.snp.top).offset(-16) } v.addTarget(self, action: #selector(tapBottomButton), for: .touchUpInside) v.setTitle("Subscribe", for: .normal) return v }() setupVIPSubscribeItemsView() } private func setupPrivilegeView() { privilegesContainer = { let v = UIView() v.cornerRadius = 12 contentView.addSubview(v) v.snp.makeConstraints { make in make.leading.equalToSuperview().offset(CGFloat.lrs) make.trailing.equalToSuperview().offset(-CGFloat.lrs) make.top.equalToSuperview().offset(64) make.height.equalTo(229) } return v }() effectView = { let v = UIVisualEffectView(effect: UIBlurEffect(style: .light)) v.alpha = 0.9 privilegesContainer.addSubview(v) v.snp.makeConstraints { make in make.edges.equalToSuperview() } return v }() // cv is in privilegesContainer, cellwidth and height is same as privilegesContainer. pageEnable = true, scroll hori.. cv = { layout.scrollDirection = .horizontal let width = UIScreen.width - 24 * 2 layout.itemSize = CGSize(width: width, height: 229) layout.minimumLineSpacing = 0 layout.minimumInteritemSpacing = 0 layout.sectionInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) let v = UICollectionView(frame: CGRect.zero, collectionViewLayout: layout) v.backgroundColor = .clear v.register(VIPPrivilegeGridCell.self, forCellWithReuseIdentifier: "VIPPrivilegeGridCell") v.delegate = self v.dataSource = self v.isPagingEnabled = true v.showsHorizontalScrollIndicator = false privilegesContainer.addSubview(v) v.snp.makeConstraints { make in make.edges.equalToSuperview() } return v }() leftButton = { let v = EPIconTertiaryDarkButton(radius: .round, iconSize: .xs, iconCode: .arrowLeftBorder) privilegesContainer.addSubview(v) v.snp.makeConstraints { make in make.top.equalToSuperview().offset(58.5) make.leading.equalToSuperview().offset(16) make.size.equalTo(v.bgImageSize()) } return v }() rightButton = { let v = EPIconTertiaryDarkButton(radius: .round, iconSize: .xs, iconCode: .arrowRightBorder) privilegesContainer.addSubview(v) v.snp.makeConstraints { make in make.top.equalToSuperview().offset(58.5) make.trailing.equalToSuperview().offset(-16) make.size.equalTo(v.bgImageSize()) } return v }() leftButton.addTarget(self, action: #selector(leftButtonPressed), for: .touchUpInside) rightButton.addTarget(self, action: #selector(rightButtonPressed), for: .touchUpInside) } private func setupAgreementsView() { agreenmentContainer = { let v = UIView() contentView.addSubview(v) v.snp.makeConstraints { make in make.height.equalTo(32) make.leading.equalToSuperview() make.trailing.equalToSuperview() make.bottom.equalToSuperview().offset(-UIWindow.safeAreaBottom) } return v }() let line = { let v = CLLine() agreenmentContainer.addSubview(v) v.snp.makeConstraints { make in make.center.equalToSuperview() make.size.equalTo(CGSize(width: 1, height: 8)) } return v }() let leftButton = { let v = UIButton() v.titleLabel?.font = UIFont.t.tls v.setTitleColor(.c.ctsn, for: .normal) agreenmentContainer.addSubview(v) v.snp.makeConstraints { make in make.trailing.equalTo(line.snp.leading).offset(-16) make.top.bottom.equalToSuperview() } v.addTarget(self, action: #selector(termsOfServicePressed), for: .touchUpInside) return v }() let rightButton = { let v = UIButton() v.titleLabel?.font = UIFont.t.tls v.setTitleColor(.c.ctsn, for: .normal) agreenmentContainer.addSubview(v) v.snp.makeConstraints { make in make.leading.equalTo(line.snp.trailing).offset(16) make.top.bottom.equalToSuperview() } v.addTarget(self, action: #selector(privacyPolicyPressed), for: .touchUpInside) return v }() leftButton.setTitle("Terms of Service", for: .normal) rightButton.setTitle("Privacy Policy", for: .normal) } private func setupVIPSubscribeItemsView() { tableView = { let v = UITableView() v.backgroundColor = .clear v.register(VIPTierListCell.self, forCellReuseIdentifier: "VIPTierListCell") contentView.addSubview(v) v.snp.makeConstraints { make in make.leading.equalToSuperview() make.trailing.equalToSuperview() make.top.equalTo(privilegesContainer.snp.bottom).offset(16) make.bottom.equalTo(operateButton.snp.top).offset(-16) } return v }() tableView.delegate = self tableView.dataSource = self } } extension VIPSubscribeSheet: UICollectionViewDelegate, UICollectionViewDataSource { func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return privileges.count * multiplier } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "VIPPrivilegeGridCell", for: indexPath) as! VIPPrivilegeGridCell let actualIndex = indexPath.item % privileges.count if actualIndex < privileges.count { let privilege = privileges[actualIndex] cell.configure(with: privilege) } return cell } func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { // 更新当前索引 let pageWidth = scrollView.frame.width let currentPage = Int(scrollView.contentOffset.x / pageWidth) currentRealCvIndex = currentPage currentPrivilegeIndex = currentPage % privileges.count // 实现无限循环:当接近边界时,跳转到中间位置 let totalItems = privileges.count * multiplier let middleStart = (totalItems / 2) - (totalItems / 2) % privileges.count if currentPage < privileges.count { // 接近开始位置,跳转到中间偏后位置 let targetIndex = middleStart + currentPrivilegeIndex let targetIndexPath = IndexPath(item: targetIndex, section: 0) DispatchQueue.main.async { self.cv.scrollToItem(at: targetIndexPath, at: .centeredHorizontally, animated: false) } } else if currentPage >= totalItems - privileges.count { // 接近结束位置,跳转到中间偏前位置 let targetIndex = middleStart - (privileges.count - currentPrivilegeIndex) let targetIndexPath = IndexPath(item: targetIndex, section: 0) DispatchQueue.main.async { self.cv.scrollToItem(at: targetIndexPath, at: .centeredHorizontally, animated: false) } } } } extension VIPSubscribeSheet: UITableViewDelegate, UITableViewDataSource { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return vipProducts.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "VIPTierListCell", for: indexPath) as! VIPTierListCell if indexPath.row < vipProducts.count { let product = vipProducts[indexPath.row] cell.configure(with: product) cell.setupSelected(selected: indexPath.row == selectedProductIndex) } return cell } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { print("didSelectRowAt \(indexPath)") selectedProductIndex = indexPath.row tableView.reloadData() } } class VIPPrivilegeGridCell: UICollectionViewCell { var icon: UIImageView! var titleLabel: CLLabel! var descLabel: LineSpaceLabel! override init(frame: CGRect) { super.init(frame: frame) setupViews() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } private func setupViews() { backgroundColor = .clear icon = { let v = UIImageView() v.cornerRadius = 8 contentView.addSubview(v) v.snp.makeConstraints { make in make.size.equalTo(CGSize(width: 140, height: 93)) make.top.equalToSuperview().offset(24) make.centerX.equalToSuperview() } return v }() titleLabel = { let v = CLLabel() v.font = .t.tts v.textAlignment = .center contentView.addSubview(v) v.snp.makeConstraints { make in make.leading.equalToSuperview().offset(16) make.trailing.equalToSuperview().offset(-16) make.top.equalTo(icon.snp.bottom).offset(16) } return v }() descLabel = { let v = LineSpaceLabel() let typo = CLSystemToken.typography(token: .tls) v.textColor = .text v.textAlignment = .center v.config(typo) contentView.addSubview(v) v.snp.makeConstraints { make in make.leading.equalToSuperview().offset(16) make.trailing.equalToSuperview().offset(-16) make.top.equalTo(titleLabel.snp.bottom).offset(8) } return v }() } func configure(with privilege: MemberPrivDict) { titleLabel.text = privilege.title ?? "" descLabel.text = privilege.desc ?? "" // 设置图片 if let imgUrl = privilege.img, !imgUrl.isEmpty { // 使用网络图片加载 icon.loadImage(imgUrl, bgColor: .clear) } else { } } } class VIPTierListCell: UITableViewCell { var blockView: UIView! var checkButton: EPRadioButton! var coinAndLabel: CLIconLabel! var periodLabel: CLLabel! var descLabel: CLLabel! var gradientFlag: GradientView! var discountLabelOnFlag: CLLabel! override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) selectionStyle = .none backgroundColor = .clear setupViews() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } private func setupViews() { blockView = { let v = UIView() v.backgroundColor = .c.csnn v.cornerRadius = 12 v.layer.borderWidth = 0 v.layer.borderColor = UIColor.c.cpn.cgColor contentView.addSubview(v) v.snp.makeConstraints { make in make.leading.equalToSuperview().offset(CGFloat.lrs) make.trailing.equalToSuperview().offset(-CGFloat.lrs) make.top.equalToSuperview() make.bottom.equalToSuperview().offset(-12) make.height.equalTo(80) } return v }() checkButton = { let v = EPRadioButton() v.setupRadioStyle(.checkEmpty) v.isUserInteractionEnabled = false blockView.addSubview(v) v.snp.makeConstraints { make in make.trailing.equalToSuperview().offset(-24) make.centerY.equalToSuperview() } return v }() coinAndLabel = { let v = CLIconLabel() v.iconSize = CGSize(width: 16, height: 16) //v.iconImageView.image = UIImage(named: "icon_16_diamond") v.iconImageView.isHidden = true v.contentLabel.textColor = .text v.contentLabel.font = .t.tnmm blockView.addSubview(v) v.snp.makeConstraints { make in make.centerY.equalToSuperview() make.trailing.equalTo(checkButton.snp.leading).offset(-8) } return v }() periodLabel = { let v = CLLabel() v.font = .t.tts v.textColor = .text blockView.addSubview(v) v.snp.makeConstraints { make in make.centerY.equalToSuperview() make.leading.equalToSuperview().offset(24) } return v }() descLabel = { let v = CLLabel() v.font = .t.tls v.textColor = .c.ctsn blockView.addSubview(v) v.snp.makeConstraints { make in make.lastBaseline.equalTo(periodLabel) make.leading.equalTo(periodLabel.snp.trailing).offset(4) } return v }() gradientFlag = { let gradient = CLSystemToken.gradient(token: .ccvn) let v = GradientView(colors: gradient.colors(), gradientType: .leftToRight) v.cornerRadius = 12 v.backgroundColor = .c.csnn v.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMaxYCorner] blockView.addSubview(v) v.snp.makeConstraints { make in make.height.equalTo(24) make.leading.top.equalToSuperview() } return v }() discountLabelOnFlag = { let v = CLLabel() v.font = .t.tbss v.textColor = .c.cbd gradientFlag.addSubview(v) v.snp.makeConstraints { make in make.centerY.equalToSuperview() make.leading.equalToSuperview().offset(8) make.trailing.equalToSuperview().offset(-8) } return v }() } func configure(with product: VIPSubcribeProduct) { // 设置价格 // if let chargeAmount = product.chargeAmount { // let coin = Coin(cents: chargeAmount) // coinAndLabel.contentLabel.text = coin.formatted // } let period = product.period ?? .month // 设置周期 let periodText = getPeriodText(from: product.period) periodLabel.text = periodText // 设置描述价格 if let payAmount = product.payAmount { let price = Coin(cents: payAmount) if period == .month{ descLabel.isHidden = true }else if period == .season{ descLabel.isHidden = false let pricePerMonth = CGFloat(payAmount) / 3.0 let pricePerMonthCoin = Coin(cents: Int(pricePerMonth)) descLabel.text = "$\(pricePerMonthCoin.formatted)/\(getPeriodShortText(from: product.period))" }else if period == .year{ descLabel.isHidden = false let pricePerMonth = CGFloat(payAmount) / 12.0 let pricePerMonthCoin = Coin(cents: Int(pricePerMonth)) descLabel.text = "$\(pricePerMonthCoin.formatted)/\(getPeriodShortText(from: product.period))" } // CrushCoin coinAndLabel.contentLabel.text = "$\(price.formatted)" } // 设置折扣标签 if let discount = product.discount, let discountFloat = Float(discount), discountFloat > 0 { gradientFlag.isHidden = false discountLabelOnFlag.text = "\(discount)% Off" } else { gradientFlag.isHidden = true } } private func getPeriodText(from period: PriceType?) -> String { switch period { case .month: return "1 Month" case .season: return "3 Months" case .year: return "1 Year" case .none: return "1 Month" } } private func getPeriodShortText(from period: PriceType?) -> String { switch period { case .month: return "Month" case .season: return "Months" case .year: return "Year" case .none: return "Month" } } func setupSelected(selected: Bool) { checkButton.isSelected = selected blockView.layer.borderWidth = selected ? 2 : 0 } }