Visual_Novel_iOS/crush/Crush/Src/Modules/Home/CardDrag/MeetDragCardContainer.swift

753 lines
29 KiB
Swift
Raw Normal View History

2025-10-09 10:29:35 +00:00
//
// MeetDragCardContainer.swift
// Crush
//
// Created by AI Assistant on 2024/12/19.
// Copyright © 2024 Crush. All rights reserved.
//
import UIKit
import SnapKit
import Lottie
// MARK: -
protocol MeetDragCardContainerDataSource: AnyObject {
///
func numberOfRowsInYFLDragCardContainer(_ container: MeetDragCardContainer) -> Int
///
func container(_ container: MeetDragCardContainer, viewForRowsAt index: Int) -> MeetDragCardView
}
// MARK: -
protocol MeetDragCardContainerDelegate: AnyObject {
/// (
func container(_ container: MeetDragCardContainer, didSelectRowAt index: Int)
/// YESreloadData
func container(_ container: MeetDragCardContainer, dataSourceIsEmpty isEmpty: Bool)
/// cardview YES
func container(_ container: MeetDragCardContainer, canDragForCardView cardView: MeetDragCardView) -> Bool
///
func container(_ container: MeetDragCardContainer, dargingForCardView cardView: MeetDragCardView, direction: ContainerDragDirection, widthRate: CGFloat, heightRate: CGFloat)
/// v2.6 ld add
func container(_ container: MeetDragCardContainer, canDragFinishForDirection direction: ContainerDragDirection, forCardView cardView: MeetDragCardView) -> Bool
///
func container(_ container: MeetDragCardContainer, dragDidFinshForDirection direction: ContainerDragDirection, forCardView cardView: MeetDragCardView)
/// v2.6
func container(_ container: MeetDragCardContainer, lookingBack direction: ContainerDragDirection, forCardView cardView: MeetDragCardView)
func container(_ container: MeetDragCardContainer, enterSmallCardMode smallCardMode: Bool, forCardView cardView: MeetDragCardView)
}
// MARK: -
class MeetDragCardContainer: UIView {
// MARK: -
private(set) var tmpStoreNopeCardView: MeetDragCardView?
private(set) var smallCardMode: Bool = false
private(set) var isMoveIng: Bool = false
weak var dataSource: MeetDragCardContainerDataSource?
weak var delegate: MeetDragCardContainerDelegate?
// MARK: -
private var cards: [MeetDragCardView] = []
private var direction: ContainerDragDirection = .default
private var loadedIndex: Int = 0
private var firstCardFrame: CGRect = .zero
private var lastCardFrame: CGRect = .zero
private var cardCenter: CGPoint = .zero
private var lastCardTransform: CGAffineTransform = .identity
private var configure: MeetDragConfigure!
private var likeOrDislikeShowLogoView: UIView!
private var likeShowImageView: UIImageView!
private var dislikeShowImageView: UIImageView!
// private var likeLottieView: LottieAnimationView!
// private var dislikeLottieView: LottieAnimationView!
// v2.8
private var currentIntialGestureDirection: ContainerDragDirection = .default
private var canMoveView: Bool = true
// MARK: -
convenience override init(frame: CGRect) {
self.init(frame: frame, configure: MeetDragCardContainer.setDefaultsConfigures())
}
init(frame: CGRect, configure: MeetDragConfigure) {
super.init(frame: frame)
initDataConfigure(configure)
}
required init?(coder: NSCoder) {
super.init(coder: coder)
initDataConfigure(MeetDragCardContainer.setDefaultsConfigures())
}
// MARK: -
private static func setDefaultsConfigures() -> MeetDragConfigure {
let configure = MeetDragConfigure()
configure.visableCount = 3
configure.containerEdge = 0//16.0
configure.cardEdge = 0.01
configure.cardCornerRadius = 48.0
configure.cardCornerBorderWidth = 0.0
configure.cardBordColor = UIColor.clear
configure.cardVTopEdage = 0
configure.cardVBottomEdage = 0//12
return configure
}
private func initDataConfigure(_ configure: MeetDragConfigure) {
resetInitData()
initialLikeDislikesShowViews()
cards = []
self.configure = configure
}
private func initialLikeDislikesShowViews() {
likeOrDislikeShowLogoView = UIView()
likeOrDislikeShowLogoView.backgroundColor = UIColor.clear
likeOrDislikeShowLogoView.isUserInteractionEnabled = false
likeOrDislikeShowLogoView.alpha = 0
likeOrDislikeShowLogoView.layer.zPosition = 10
insertSubview(likeOrDislikeShowLogoView, at: 0)
likeOrDislikeShowLogoView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
likeShowImageView = {
let v = UIImageView()
v.image = UIImage(named: "meet_big_like")
likeOrDislikeShowLogoView.addSubview(v)
v.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.bottom.equalToSuperview().offset(-143)
}
v.isHidden = true
return v
}()
dislikeShowImageView = {
let v = UIImageView()
v.image = UIImage(named: "meet_big_dislike")
likeOrDislikeShowLogoView.addSubview(v)
v.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.bottom.equalToSuperview().offset(-143)
}
v.isHidden = true
return v
}()
// likeLottieView = {
// let animation = LottieAnimation.named("meet_right_swipe_like")
// let v = LottieAnimationView(animation: animation)
//
// v.contentMode = .scaleAspectFit
// v.loopMode = .playOnce
// v.backgroundBehavior = .pauseAndRestore
// v.backgroundColor = .clear
// likeOrDislikeShowLogoView.addSubview(v)
// let size = CGSize(width: 200, height: 200)
// v.size = size
// v.snp.makeConstraints { make in
// make.centerX.equalToSuperview()
// make.bottom.equalToSuperview().offset(-143)
// make.size.equalTo(size)
// }
// v.isHidden = true
// return v
// }()
//
// dislikeLottieView = {
// let animation = LottieAnimation.named("meet_left_swipe_dislike")
// let v = LottieAnimationView(animation: animation)
//
// v.contentMode = .scaleAspectFit
// v.loopMode = .playOnce
// v.backgroundBehavior = .pauseAndRestore
// v.backgroundColor = .clear
// likeOrDislikeShowLogoView.addSubview(v)
// let size = CGSize(width: 200, height: 200)
// v.size = size
// v.snp.makeConstraints { make in
// make.centerX.equalToSuperview()
// make.bottom.equalToSuperview().offset(-143)
// make.size.equalTo(size)
// }
// v.isHidden = true
// return v
// }()
}
/// (reload)
private func resetInitData() {
loadedIndex = 0
resetCards()
direction = .default
isMoveIng = false
}
/// card view
private func resetCards() {
for view in cards {
view.removeFromSuperview()
}
cards.removeAll()
}
///
private func addSubViews() {
guard let dataSource = dataSource else { return }
let sum = dataSource.numberOfRowsInYFLDragCardContainer(self)
let preLoadViewCount = min(sum, configure.visableCount)
//
if loadedIndex < sum {
// 4warning(view+1)
let targetCount = isMoveIng ? preLoadViewCount + 1 : preLoadViewCount
for _ in cards.count..<targetCount {
let cardView = dataSource.container(self, viewForRowsAt: loadedIndex)
addAdaptForDragCardView(cardView)
cards.append(cardView)
loadedIndex += 1
}
}
}
///
private func resetLayoutSubviews() {
// | 线
UIView.animate(withDuration: 0.5, delay: 0.0, usingSpringWithDamping: 0.6, initialSpringVelocity: 0.6, options: [.allowUserInteraction, .curveEaseInOut]) {
if let delegate = self.delegate, let firstCard = self.cards.first {
delegate.container(self, dargingForCardView: firstCard, direction: self.direction, widthRate: 0, heightRate: 0)
}
for (i, cardView) in self.cards.enumerated() {
cardView.transform = .identity
let frame = self.firstCardFrame
switch i {
case 0:
cardView.frame = frame
case 1:
cardView.transform = CGAffineTransform(scaleX: secondCardScale, y: 1)
case 2:
cardView.transform = CGAffineTransform(scaleX: thirdCardScale, y: 1)
if self.lastCardFrame.isEmpty {
self.lastCardFrame = frame
self.lastCardTransform = cardView.transform
}
default:
break
}
cardView.originTransForm = cardView.transform
}
} completion: { finished in
let isEmpty = self.cards.isEmpty
self.delegate?.container(self, dataSourceIsEmpty: isEmpty)
}
}
private func recordFrame(_ cardView: MeetDragCardView) {
if loadedIndex >= 3 {
cardView.frame = lastCardFrame
} else {
let frame = cardView.frame
if firstCardFrame.isEmpty {
firstCardFrame = frame
cardCenter = cardView.center
}
}
}
///
private func moveIngStatusChange(_ scale: CGFloat) {
//
if !isMoveIng {
isMoveIng = true
addSubViews()
} else {
// cardviewscale
let absScale = min(abs(scale), boundaryRation)
let transFormtxPoor = (secondCardScale - thirdCardScale) / (boundaryRation / absScale)
let frameYPoor: CGFloat = 0
for (index, cardView) in cards.enumerated() {
guard index > 0 else { continue }
switch index {
case 1:
let scaleTransform = CGAffineTransform(scaleX: transFormtxPoor + secondCardScale, y: 1)
let translateTransform = CGAffineTransform(translationX: 0, y: -frameYPoor)
cardView.transform = scaleTransform.concatenating(translateTransform)
case 2:
let scaleTransform = CGAffineTransform(scaleX: transFormtxPoor + thirdCardScale, y: 1)
let translateTransform = CGAffineTransform(translationX: 0, y: -frameYPoor)
cardView.transform = scaleTransform.concatenating(translateTransform)
case 3:
cardView.transform = lastCardTransform
default:
break
}
}
}
}
//
private func panGesturemMoveFinishOrCancle(_ cardView: MeetDragCardView, direction: ContainerDragDirection, scale: CGFloat, isDisappear: Bool) {
if !isDisappear { //
if currentIntialGestureDirection == .left || currentIntialGestureDirection == .right || currentIntialGestureDirection == .default {
UIView.animate(withDuration: 0.5, delay: 0.0, usingSpringWithDamping: 0.6, initialSpringVelocity: 0.6, options: [.allowUserInteraction, .curveEaseInOut]) {
self.subRestoreCardState()
} completion: { finished in
}
} else { //
subRestoreCardState()
}
} else {
// v2.6
if let delegate = delegate {
if !delegate.container(self, canDragFinishForDirection: direction, forCardView: cardView) {
subRestoreCardState()
return
}
}
delegate?.container(self, dragDidFinshForDirection: self.direction, forCardView: cardView)
if direction == .left || direction == .right {
tmpStoreNopeCardView = cardView
}
let flag = direction == .left ? -1 : 2
let screenWidth = UIScreen.main.bounds.width
UIView.animate(withDuration: 0.5, delay: 0.0, options: [.curveLinear, .allowUserInteraction]) {
self.isMoveIng = true
cardView.center = CGPoint(x: CGFloat(flag) * screenWidth, y: CGFloat(flag) * screenWidth / CGFloat(scale) + self.cardCenter.y)
} completion: { finished in
self.isMoveIng = false
cardView.removeFromSuperview()
}
if let index = cards.firstIndex(of: cardView) {
cards.remove(at: index)
}
isMoveIng = false
resetLayoutSubviews()
}
}
private func subRestoreCardState() {
// .
if isMoveIng && cards.count > configure.visableCount {
if let lastView = cards.last {
lastView.removeFromSuperview()
cards.removeLast()
loadedIndex = lastView.tag
}
}
isMoveIng = false
resetLayoutSubviews()
}
// MARK: -
func reloadData() {
guard dataSource != nil else {
assertionFailure("check dataSource and dataSource Methods!")
return
}
resetInitData()
addSubViews()
resetLayoutSubviews()
}
func getCurrentShowCardView() -> MeetDragCardView? {
return cards.first
}
func getCurrentShowCardViewIndex() -> Int {
return cards.first?.tag ?? 0
}
///
func removeCardViewForDirection(_ direction: ContainerDragDirection) {
guard !isMoveIng else { return }
let screenWidth = UIScreen.main.bounds.width
var cardCenter = CGPoint.zero
var flag = 0
guard let currentShowCardView = cards.first else { return }
switch direction {
case .left:
cardCenter = CGPoint(x: -screenWidth / 2.0, y: self.cardCenter.y)
flag = -1
tmpStoreNopeCardView = currentShowCardView
case .right:
cardCenter = CGPoint(x: screenWidth * 1.5, y: self.cardCenter.y)
flag = 1
tmpStoreNopeCardView = currentShowCardView
default:
break
}
UIView.animate(withDuration: 0.35) {
self.isMoveIng = true
let translate = CGAffineTransform(translationX: CGFloat(flag) * 20, y: 0)
currentShowCardView.transform = translate.rotated(by: CGFloat(flag) * .pi / 4 / 4)
currentShowCardView.center = cardCenter
} completion: { finished in
self.isMoveIng = false
currentShowCardView.removeFromSuperview()
if let index = self.cards.firstIndex(of: currentShowCardView) {
self.cards.remove(at: index)
}
self.addSubViews()
self.resetLayoutSubviews()
}
}
/// appFinish
func removeCardViewWithCallDelegateForDirection(_ direction: ContainerDragDirection) {
guard !isMoveIng else { return }
let screenWidth = UIScreen.main.bounds.width
var cardCenter = CGPoint.zero
var flag = 0
guard let currentShowCardView = cards.first else { return }
switch direction {
case .left:
cardCenter = CGPoint(x: -screenWidth / 2.0, y: self.cardCenter.y)
flag = -1
//tmpStoreNopeCardView = currentShowCardView
case .right:
cardCenter = CGPoint(x: screenWidth * 1.5, y: self.cardCenter.y)
flag = 1
default:
break
}
tmpStoreNopeCardView = currentShowCardView
UIView.animate(withDuration: 0.35) {
let translate = CGAffineTransform(translationX: CGFloat(flag) * 20, y: 0)
currentShowCardView.transform = translate.rotated(by: CGFloat(flag) * .pi / 4 / 4)
currentShowCardView.center = cardCenter
} completion: { finished in
currentShowCardView.removeFromSuperview()
if let index = self.cards.firstIndex(of: currentShowCardView) {
self.cards.remove(at: index)
}
self.addSubViews()
self.resetLayoutSubviews()
}
delegate?.container(self, dragDidFinshForDirection: direction, forCardView: currentShowCardView)
}
/// Meet CardView
func addCardView(_ cardView: MeetDragCardView?, fromDirection direction: ContainerDragDirection) {
guard !isMoveIng else { return }
let targetCardView = cardView ?? tmpStoreNopeCardView
guard let cardView = targetCardView else { return }
let screenWidth = UIScreen.main.bounds.width
var cardCenter = CGPoint.zero
var flag = 0
switch direction {
case .left:
cardCenter = CGPoint(x: -screenWidth / 2.0, y: self.cardCenter.y)
flag = -1
//tmpStoreNopeCardView = cardView
case .right:
cardCenter = CGPoint(x: screenWidth * 1.5, y: self.cardCenter.y)
flag = 1
default:
cardCenter = CGPoint(x: self.cardCenter.x, y: self.cardCenter.y)
flag = 0
break
}
tmpStoreNopeCardView = cardView
cards.insert(cardView, at: 0)
addSubview(cardView)
cardView.center = cardCenter
let translate = CGAffineTransform(translationX: CGFloat(flag) * 20, y: 0)
cardView.transform = translate.rotated(by: CGFloat(flag) * .pi / 4 / 4)
cardCenter = CGPoint(x: self.cardCenter.x, y: self.cardCenter.y)
UIView.animate(withDuration: 0.35) {
cardView.transform = .identity
cardView.center = cardCenter
self.isMoveIng = true
} completion: { finished in
self.isMoveIng = false
self.tmpStoreNopeCardView = nil
}
delegate?.container(self, lookingBack: direction, forCardView: cardView)
}
func showNopeLogo(_ show: Bool, widthRate: CGFloat) {
if widthRate == 0 {
// if !isLottieAnimationPlaying(dislikeLottieView) {
// UIView.animate(withDuration: 0.25) {
// self.likeOrDislikeShowLogoView.alpha = 0
// }
// }
UIView.animate(withDuration: 0.5) {
self.likeOrDislikeShowLogoView.alpha = 0
}
return
}
if show {
// likeLottieView.isHidden = true
// dislikeLottieView.isHidden = false
likeShowImageView.isHidden = true
dislikeShowImageView.isHidden = false
likeOrDislikeShowLogoView.isHidden = false
likeOrDislikeShowLogoView.alpha = widthRate > 0.2 ? 1 : (widthRate / 0.2)
} else {
UIView.animate(withDuration: 0.45) {
self.likeOrDislikeShowLogoView.alpha = 0
}
}
}
func showLikeLogo(_ show: Bool, widthRate: CGFloat) {
if widthRate == 0 {
// if !isLottieAnimationPlaying(likeLottieView) {
// UIView.animate(withDuration: 0.25) {
// self.likeOrDislikeShowLogoView.alpha = 0
// }
// }
UIView.animate(withDuration: 0.5) {
self.likeOrDislikeShowLogoView.alpha = 0
}
return
}
if show {
// likeLottieView.isHidden = false
// dislikeLottieView.isHidden = true
likeShowImageView.isHidden = false
dislikeShowImageView.isHidden = true
likeOrDislikeShowLogoView.isHidden = false
likeOrDislikeShowLogoView.alpha = widthRate > 0.2 ? 1 : (widthRate / 0.2)
} else {
UIView.animate(withDuration: 0.45) {
self.likeOrDislikeShowLogoView.alpha = 0
}
}
}
// func showNopeLottie() {
// likeLottieView.isHidden = true
// dislikeLottieView.isHidden = false
// stopLottieAnimation(dislikeLottieView)
// likeOrDislikeShowLogoView.alpha = 1
//
// playLottieAnimation(dislikeLottieView) { [weak self] finish in
// if finish {
// self?.likeOrDislikeShowLogoView.alpha = 0
// }
// }
// }
// func showLikeLottie() {
// dislikeLottieView.isHidden = true
//
// likeLottieView.isHidden = false
// stopLottieAnimation(likeLottieView) // 🔥
// likeOrDislikeShowLogoView.alpha = 1
//
// playLottieAnimation(likeLottieView) { [weak self] finish in
// if finish {
// self?.likeOrDislikeShowLogoView.alpha = 0
// }
// }
// }
func showNopeLogo(){
likeShowImageView.isHidden = true
dislikeShowImageView.isHidden = false
likeOrDislikeShowLogoView.alpha = 1
UIView.animate(withDuration: 0.5) { [self] in
likeOrDislikeShowLogoView.alpha = 0
}
}
func showLikeLogo(){
likeShowImageView.isHidden = false
dislikeShowImageView.isHidden = true
likeOrDislikeShowLogoView.alpha = 1
UIView.animate(withDuration: 0.5) { [self] in
likeOrDislikeShowLogoView.alpha = 0
}
}
func addAdaptForDragCardView(_ cardView: MeetDragCardView) {
let y = configure.containerEdge + configure.cardVTopEdage
let width = frame.size.width - 2 * configure.containerEdge
let height = frame.size.height - 2 * configure.containerEdge - configure.cardVTopEdage - configure.cardVBottomEdage
cardView.frame = CGRect(x: configure.containerEdge, y: y, width: width, height: height)
cardView.setConfigure(configure)
cardView.YFLDragCardViewLayoutSubviews()
recordFrame(cardView)
cardView.tag = loadedIndex
//
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:)))
cardView.addGestureRecognizer(panGesture)
//
// let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture(_:)))
// tapGesture.numberOfTapsRequired = 1
// tapGesture.numberOfTouchesRequired = 1
// cardView.addGestureRecognizer(tapGesture)
addSubview(cardView)
sendSubviewToBack(cardView)
}
func clearTmpStoreNopeCardView() {
tmpStoreNopeCardView?.removeFromSuperview()
tmpStoreNopeCardView = nil
}
func switchAllCardsToSmallCardMode(_ smallMode: Bool) {
guard smallCardMode != smallMode else { return }
smallCardMode = smallMode
delegate?.container(self, enterSmallCardMode: smallCardMode, forCardView: getCurrentShowCardView() ?? MeetDragCardView())
}
// MARK: -
// @objc private func handleTapGesture(_ tap: UITapGestureRecognizer) {
// delegate?.container(self, didSelectRowAt: tap.view?.tag ?? 0)
// }
@objc private func handlePanGesture(_ pan: UIPanGestureRecognizer) {
guard let cardView = pan.view as? MeetDragCardView else { return }
let canEdit = delegate?.container(self, canDragForCardView: cardView) ?? true
if canEdit {
switch pan.state {
case .began:
let point = pan.translation(in: self)
doVDirectionLogicByPoint(point, forCardView: cardView)
case .changed:
guard currentIntialGestureDirection != .up && currentIntialGestureDirection != .down else { return }
if let delegate = delegate {
// >0 <0
let horizionSliderRate = CGFloat((pan.view?.center.x ?? 0) - cardCenter.x) / CGFloat(cardCenter.x)
let verticalSliderRate = CGFloat((pan.view?.center.y ?? 0) - cardCenter.y) / CGFloat(cardCenter.y)
//
moveIngStatusChange(horizionSliderRate)
if currentIntialGestureDirection == .default {
//
let point = pan.translation(in: self)
doVDirectionLogicByPoint(point, forCardView: cardView)
} else { //
// 0
let point = pan.translation(in: self)
cardView.center = CGPoint(x: (pan.view?.center.x ?? 0) + point.x * 0.5, y: pan.view?.center.y ?? 0)
// angle,,
let rotationAngle = ((pan.view?.center.x ?? 0) - cardCenter.x) / cardCenter.x * (.pi / 4 / 12)
cardView.transform = cardView.originTransForm.rotated(by: rotationAngle)
pan.setTranslation(.zero, in: self) //
}
if horizionSliderRate > 0 {
direction = .right
} else if horizionSliderRate < 0 {
direction = .left
} else {
direction = .default
}
if currentIntialGestureDirection == .default {
currentIntialGestureDirection = direction
}
delegate.container(self, dargingForCardView: cardView, direction: direction, widthRate: horizionSliderRate, heightRate: verticalSliderRate)
}
case .ended, .cancelled:
// cardcard
let horizionSliderRate = CGFloat((pan.view?.center.x ?? 0) - cardCenter.x) / CGFloat(cardCenter.x)
let moveY = (pan.view?.center.y ?? 0) - cardCenter.y
let moveX = (pan.view?.center.x ?? 0) - cardCenter.x
panGesturemMoveFinishOrCancle(cardView, direction: direction, scale: CGFloat(moveX / moveY), isDisappear: abs(horizionSliderRate) > boundaryRation)
for per in cards { // cardView
per.isHidden = false
}
currentIntialGestureDirection = .default
default:
break
}
}
}
// MARK: -
private func doVDirectionLogicByPoint(_ point: CGPoint, forCardView cardView: MeetDragCardView) {
// option2:
if point.x == 0 {
currentIntialGestureDirection = .default
} else if point.x < 0 {
currentIntialGestureDirection = .left
} else {
currentIntialGestureDirection = .right
}
}
private func isLottieAnimationPlaying(_ view: LottieAnimationView) -> Bool {
return view.isAnimationPlaying
}
private func stopLottieAnimation(_ view: LottieAnimationView) {
view.stop()
}
private func playLottieAnimation(_ view: LottieAnimationView, completion: @escaping (Bool) -> Void) {
view.play { completed in
completion(completed)
}
}
}