| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367 |
- //
- // 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()
- })
- }
- }
|