// // LNRoomOrderGuideView.swift // Gami // // Created by OneeChan on 2026/3/22. // import Foundation import UIKit import SnapKit class LNRoomOrderGuideView: UIView { static weak var joinView: UIView? static weak var mainSeatView: UIView? static weak var playmateView: UIView? static weak var orderCardView: UIView? private static weak var currentGuide: LNRoomOrderGuideView? private enum Step: Int, CaseIterable { case join case demand case playmate case order var cardSize: CGSize { switch self { case .join, .playmate, .order: .init(width: 314, height: self == .join ? 136 : 120) case .demand: .init(width: 320, height: 136) } } var focusInset: UIEdgeInsets { switch self { case .join: .init(top: -4, left: -4, bottom: -4, right: -4) case .demand: .init(top: -8, left: -4, bottom: -8, right: -4) case .playmate: .init(top: -16, left: -4, bottom: -16, right: -4) case .order: .init(top: -2, left: -2, bottom: -2, right: -2) } } var focusCornerRadius: CGFloat { switch self { case .join: 15 case .demand, .playmate: 12 case .order: 12 } } var progressText: String { "(\(rawValue + 1)/\(Self.allCases.count))" } } private let dimLayer = CAShapeLayer() private let lineLayer = CAShapeLayer() private let dotView = UIView() private let dotInnerView = UIView() private let cardView = UIView() private let cardGlowView = UIImageView(image: .primary_6) private let titleLabel = UILabel() private let descLabel = UILabel() private let nextButton = UIButton(type: .custom) private let nextTitleLabel = UILabel() private let progressLabel = UILabel() private weak var profileCard: LNRoomProfileCardPanel? private var currentStep: Step = .join { didSet { updateTexts() setNeedsLayout() } } static func show(_ container: UIView) { guard currentGuide == nil else { return } let guide = LNRoomOrderGuideView() currentGuide = guide container.addSubview(guide) guide.snp.makeConstraints { make in make.edges.equalToSuperview() } } override init(frame: CGRect) { super.init(frame: frame) setupViews() updateTexts() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func layoutSubviews() { super.layoutSubviews() dimLayer.frame = bounds lineLayer.frame = bounds updateLayout() } } private extension LNRoomOrderGuideView { var currentAnchorView: UIView? { switch currentStep { case .join: Self.joinView case .demand: Self.mainSeatView case .playmate: Self.playmateView case .order: Self.orderCardView } } func setupViews() { backgroundColor = .clear dimLayer.fillColor = UIColor.black.withAlphaComponent(0.8).cgColor dimLayer.fillRule = .evenOdd layer.addSublayer(dimLayer) lineLayer.strokeColor = UIColor.fill.cgColor lineLayer.lineWidth = 1 lineLayer.fillColor = UIColor.clear.cgColor layer.addSublayer(lineLayer) dotView.backgroundColor = UIColor.fill.withAlphaComponent(0.2) dotView.layer.cornerRadius = 7.5 addSubview(dotView) dotInnerView.backgroundColor = .fill dotInnerView.layer.cornerRadius = 3.5 dotView.addSubview(dotInnerView) dotInnerView.snp.makeConstraints { make in make.center.equalToSuperview() make.width.height.equalTo(7) } cardView.backgroundColor = .fill cardView.layer.cornerRadius = 20 cardView.clipsToBounds = true addSubview(cardView) cardView.frame = .init(x: 0, y: 0, width: 50, height: 50) cardGlowView.alpha = 0.6 cardGlowView.contentMode = .scaleToFill cardView.addSubview(cardGlowView) cardGlowView.snp.makeConstraints { make in make.edges.equalToSuperview() } titleLabel.font = .heading_h3 titleLabel.textColor = .text_5 titleLabel.numberOfLines = 0 cardView.addSubview(titleLabel) titleLabel.snp.makeConstraints { make in make.top.equalToSuperview().offset(20) make.leading.equalToSuperview().offset(15) make.trailing.equalToSuperview().offset(-15) } descLabel.font = .body_m descLabel.textColor = .text_4 descLabel.numberOfLines = 0 cardView.addSubview(descLabel) descLabel.snp.makeConstraints { make in make.top.equalTo(titleLabel.snp.bottom).offset(8) make.leading.equalToSuperview().offset(15) make.trailing.equalToSuperview().offset(-15) } nextButton.setBackgroundImage(.primary_7, for: .normal) nextButton.layer.cornerRadius = 16.5 nextButton.clipsToBounds = true nextButton.addAction(UIAction(handler: { [weak self] _ in self?.handleNext() }), for: .touchUpInside) cardView.addSubview(nextButton) nextButton.snp.makeConstraints { make in make.trailing.equalToSuperview().offset(-15) make.bottom.equalToSuperview().offset(-13) make.height.equalTo(33) } let buttonStack = UIStackView() buttonStack.axis = .horizontal buttonStack.spacing = 2 buttonStack.alignment = .center buttonStack.isUserInteractionEnabled = false nextButton.addSubview(buttonStack) buttonStack.snp.makeConstraints { make in make.leading.equalToSuperview().offset(12) make.trailing.equalToSuperview().offset(-12) make.centerY.equalToSuperview() } nextTitleLabel.font = .heading_h3 nextTitleLabel.textColor = .text_1 buttonStack.addArrangedSubview(nextTitleLabel) progressLabel.font = .body_xs progressLabel.textColor = .text_1 buttonStack.addArrangedSubview(progressLabel) } func updateTexts() { nextTitleLabel.text = .init(key: "A00373") progressLabel.text = currentStep.progressText switch currentStep { case .join: titleLabel.text = .init(key: "A00374") descLabel.attributedText = nil descLabel.text = .init(key: "A00375") case .demand: titleLabel.text = .init(key: "A00376") descLabel.attributedText = highlightedDemandText() case .playmate: titleLabel.text = .init(key: "A00381") descLabel.attributedText = nil descLabel.text = .init(key: "A00382") case .order: titleLabel.text = .init(key: "A00383") descLabel.attributedText = nil descLabel.text = .init(key: "A00384") } } func highlightedDemandText() -> NSAttributedString { let text = String(key: "A00378") let attributed = NSMutableAttributedString( string: text, attributes: [ .font: UIFont.body_m, .foregroundColor: UIColor.text_4 ] ) [String(key: "A00379"), String(key: "A00380"), String(key: "A00385")].forEach { target in let nsText = text as NSString let range = nsText.range(of: target) if range.location != NSNotFound { attributed.addAttributes([ .foregroundColor: UIColor.text_6 ], range: range) } } return attributed } func updateLayout() { guard let focusRect = currentFocusRect() else { dimLayer.path = UIBezierPath(rect: bounds).cgPath lineLayer.path = nil dotView.isHidden = true cardView.isHidden = true return } cardView.isHidden = false dotView.isHidden = false let cardFrame = currentCardFrame(for: focusRect) cardView.frame = cardFrame let maskPath = UIBezierPath(rect: bounds) maskPath.append(UIBezierPath(roundedRect: focusRect, cornerRadius: currentStep.focusCornerRadius)) dimLayer.path = maskPath.cgPath let isCardAbove = cardFrame.maxY <= focusRect.minY let dotCenter = CGPoint( x: currentStep == .demand ? focusRect.maxX - 42 : focusRect.midX, y: isCardAbove ? focusRect.minY - 7.5 : focusRect.maxY + 7.5 ) dotView.bounds = CGRect(x: 0, y: 0, width: 15, height: 15) dotView.center = dotCenter let path = UIBezierPath() path.move(to: CGPoint(x: dotCenter.x, y: isCardAbove ? cardFrame.maxY : cardFrame.minY)) path.addLine(to: dotCenter) lineLayer.path = path.cgPath } func currentFocusRect() -> CGRect? { guard let anchor = currentAnchorView, anchor.window != nil else { return nil } let rect = anchor.convert(anchor.bounds, to: self) let inset = currentStep.focusInset let focusedRect = rect.inset(by: inset) let visibleRect = focusedRect.intersection(bounds) guard !visibleRect.isNull, !visibleRect.isEmpty else { return nil } return visibleRect } func currentCardFrame(for focusRect: CGRect) -> CGRect { let size = currentStep.cardSize switch currentStep { case .join: let x = bounds.width - 12 - size.width let y = focusRect.minY - 56 - size.height return .init(x: x, y: max(100, y), width: size.width, height: size.height) case .demand: let x = bounds.width - 12 - size.width return .init(x: x, y: focusRect.maxY + 61, width: size.width, height: size.height) case .playmate: return .init(x: (bounds.width - size.width) / 2, y: focusRect.maxY + 59, width: size.width, height: size.height) case .order: let y = focusRect.minY - 66 - size.height return .init(x: 10, y: max(100, y), width: size.width, height: size.height) } } func handleNext() { switch currentStep { case .join: currentStep = .demand case .demand: currentStep = .playmate case .playmate: showOrderStepIfPossible() case .order: dismiss() profileCard?.dismiss(animated: false) } } func showOrderStepIfPossible() { let panel = LNRoomProfileCardPanel() panel.toBeExample() panel.popup(superview, animated: false) superview?.bringSubviewToFront(self) profileCard = panel currentStep = .order } func dismiss() { if Self.currentGuide === self { Self.currentGuide = nil } UIView.animate(withDuration: 0.2, animations: { self.alpha = 0 }, completion: { [weak self] _ in self?.removeFromSuperview() }) } }