Visual_Novel_iOS/YFLDragCardContainer.swift

710 lines
28 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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