Visual_Novel_iOS/YFLDragCardContainer.swift

710 lines
28 KiB
Swift
Raw Normal View History

2025-10-14 05:52:46 +00:00
//
// YFLDragCardContainer.swift
// Crush
//
// Created by AI Assistant on 2024/12/19.
// Copyright © 2024 Crush. All rights reserved.
//
import UIKit
import SnapKit
// MARK: -
protocol YFLDragCardContainerDataSource: AnyObject {
///
func numberOfRowsInYFLDragCardContainer(_ container: YFLDragCardContainer) -> Int
///
func container(_ container: YFLDragCardContainer, viewForRowsAt index: Int) -> YFLDragCardView
}
// MARK: -
protocol YFLDragCardContainerDelegate: AnyObject {
///
func container(_ container: YFLDragCardContainer, didSelectRowAt index: Int)
/// YESreloadData
func container(_ container: YFLDragCardContainer, dataSourceIsEmpty isEmpty: Bool)
/// cardview YES
func container(_ container: YFLDragCardContainer, canDragForCardView cardView: YFLDragCardView) -> Bool
///
func container(_ container: YFLDragCardContainer, dargingForCardView cardView: YFLDragCardView, direction: ContainerDragDirection, widthRate: Float, heightRate: Float)
/// v2.6 ld add
func container(_ container: YFLDragCardContainer, canDragFinishForDirection direction: ContainerDragDirection, forCardView cardView: YFLDragCardView) -> Bool
///
func container(_ container: YFLDragCardContainer, dragDidFinshForDirection direction: ContainerDragDirection, forCardView cardView: YFLDragCardView)
/// v2.6
func container(_ container: YFLDragCardContainer, lookingBack direction: ContainerDragDirection, forCardView cardView: YFLDragCardView)
func container(_ container: YFLDragCardContainer, enterSmallCardMode smallCardMode: Bool, forCardView cardView: YFLDragCardView)
}
// MARK: -
class YFLDragCardContainer: UIView {
// MARK: -
private(set) var tmpStoreNopeCardView: YFLDragCardView?
private(set) var smallCardMode: Bool = false
private(set) var isMoveIng: Bool = false
weak var dataSource: YFLDragCardContainerDataSource?
weak var delegate: YFLDragCardContainerDelegate?
// MARK: -
private var cards: [YFLDragCardView] = []
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: YFLDragConfigure!
// 🔥
private var likeOrDislikeShowLogoView: UIView!
private var likeOrDislikeShowImageView: UIImageView!
private var likeLottieView: UIView! // 🔥 Lottie
private var dislikeLottieView: UIView! // <EFBFBD><EFBFBD> Lottie
private var superlikeLottieView: UIView! // 🔥 Lottie
private var superlikeWordsShowBg: UIImageView!
private var superlikeWordsLabel: UILabel!
private var superLikeCountLeft: UILabel!
// v2.8
private var currentIntialGestureDirection: ContainerDragDirection = .default
private var canMoveView: Bool = true
// MARK: -
convenience init(frame: CGRect) {
self.init(frame: frame, configure: YFLDragCardContainer.setDefaultsConfigures())
}
init(frame: CGRect, configure: YFLDragConfigure) {
super.init(frame: frame)
initDataConfigure(configure)
}
required init?(coder: NSCoder) {
super.init(coder: coder)
initDataConfigure(YFLDragCardContainer.setDefaultsConfigures())
}
// MARK: -
private static func setDefaultsConfigures() -> YFLDragConfigure {
let configure = YFLDragConfigure()
configure.visableCount = 3
configure.containerEdge = 16.0
configure.cardEdge = 0.01
configure.cardCornerRadius = 8.0
configure.cardCornerBorderWidth = 0.0
configure.cardBordColor = UIColor.clear
configure.cardVTopEdage = 0
configure.cardVBottomEdage = 12
return configure
}
private func initDataConfigure(_ configure: YFLDragConfigure) {
resetInitData()
initialLikeDislikesShowViews()
cards = []
backgroundColor = UIColor.white
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.center.equalToSuperview()
}
// 🔥 Lottie
likeLottieView = createLottieView(animationName: "LottieResources/kuolie_swipe_like")
dislikeLottieView = createLottieView(animationName: "LottieResources/kuolie_swipe_dislike")
superlikeLottieView = createLottieView(animationName: "LottieResources/kuolie_superlike")
// 🔥
superlikeWordsShowBg = UIImageView()
superlikeWordsShowBg.image = UIImage(named: "kuolie_superlike_words_bg")
superlikeLottieView.addSubview(superlikeWordsShowBg)
superlikeWordsShowBg.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.bottom.equalToSuperview().offset(8)
make.size.equalTo(CGSize(width: 186, height: 32))
}
// 🔥
superlikeWordsLabel = UILabel()
superlikeWordsLabel.textColor = UIColor.white
superlikeWordsLabel.font = UIFont.systemFont(ofSize: 16) // 🔥
superlikeWordsLabel.text = "SUPER LIKE" // 🔥
superlikeLottieView.addSubview(superlikeWordsLabel)
superlikeWordsLabel.snp.makeConstraints { make in
make.center.equalTo(superlikeWordsShowBg)
}
}
private func createLottieView(animationName: String) -> UIView {
// 🔥 Lottie
let view = UIView()
view.contentMode = .scaleAspectFill
likeOrDislikeShowLogoView.addSubview(view)
view.isHidden = true
view.snp.makeConstraints { make in
make.center.equalTo(likeOrDislikeShowLogoView)
make.size.equalTo(CGSize(width: 120, height: 120))
}
return view
}
/// (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 i 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
var 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: YFLDragCardView) {
if loadedIndex >= 3 {
cardView.frame = lastCardFrame
} else {
let frame = cardView.frame
if firstCardFrame.isEmpty {
firstCardFrame = frame
cardCenter = cardView.center
}
}
}
///
private func moveIngStatusChange(_ scale: Float) {
//
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: YFLDragCardView, direction: ContainerDragDirection, scale: Float, 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 {
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 let dataSource = dataSource else {
assertionFailure("check dataSource and dataSource Methods!")
return
}
resetInitData()
addSubViews()
resetLayoutSubviews()
}
func getCurrentShowCardView() -> YFLDragCardView? {
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
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
}
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: YFLDragCardView?, 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:
break
}
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
}
}
return
}
if show {
likeLottieView.isHidden = true
superlikeLottieView.isHidden = true
dislikeLottieView.isHidden = false
likeOrDislikeShowLogoView.isHidden = false
likeOrDislikeShowLogoView.alpha = widthRate > 0.2 ? 1 : (widthRate / 0.2)
superLikeCountLeft.isHidden = true
} 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
}
}
return
}
if show {
likeLottieView.isHidden = false
dislikeLottieView.isHidden = true
superlikeLottieView.isHidden = true
likeOrDislikeShowLogoView.alpha = widthRate > 0.2 ? 1 : (widthRate / 0.2)
superLikeCountLeft.isHidden = true
} else {
UIView.animate(withDuration: 0.45) {
self.likeOrDislikeShowLogoView.alpha = 0
}
}
}
func showNopeLottie() {
likeLottieView.isHidden = true
superlikeLottieView.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
superlikeLottieView.isHidden = true
likeLottieView.isHidden = false
stopLottieAnimation(likeLottieView) // 🔥
likeOrDislikeShowLogoView.alpha = 1
playLottieAnimation(likeLottieView) { [weak self] finish in
if finish {
self?.likeOrDislikeShowLogoView.alpha = 0
}
}
}
func showSuperLikeLottie(complete: @escaping (Bool) -> Void) {
dislikeLottieView.isHidden = true
likeLottieView.isHidden = true
superlikeLottieView.isHidden = false
stopLottieAnimation(superlikeLottieView) // 🔥
likeOrDislikeShowLogoView.alpha = 1
playLottieAnimation(superlikeLottieView) { [weak self] finish in
self?.likeOrDislikeShowLogoView.alpha = 0
complete(finish)
}
}
func addAdaptForDragCardView(_ cardView: YFLDragCardView) {
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)
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() ?? YFLDragCardView())
}
// 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? YFLDragCardView 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 = Float((pan.view?.center.x ?? 0) - cardCenter.x) / Float(cardCenter.x)
let verticalSliderRate = Float((pan.view?.center.y ?? 0) - cardCenter.y) / Float(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 = Float((pan.view?.center.x ?? 0) - cardCenter.x) / Float(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: Float(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: YFLDragCardView) {
// option2:
if point.x == 0 {
currentIntialGestureDirection = .default
} else if point.x < 0 {
currentIntialGestureDirection = .left
} else {
currentIntialGestureDirection = .right
}
}
// 🔥 Lottie
private func isLottieAnimationPlaying(_ view: UIView) -> Bool {
// 🔥 Lottie
return false
}
private func stopLottieAnimation(_ view: UIView) {
// 🔥 Lottie
}
private func playLottieAnimation(_ view: UIView, completion: @escaping (Bool) -> Void) {
// 🔥 Lottie
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
completion(true)
}
}
}