Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#35] 현재 보이는 유저 카드 셀에서의 타임이 0이 되면 자동으로 다음 셀로 스크롤 구현 #38

Merged
merged 4 commits into from
Oct 8, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions Falling/Sources/Base/TFBaseCollectionViewCell.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//
// TFBaseCollectionViewCell.swift
// Falling
//
// Created by SeungMin on 2023/10/02.
//

import UIKit

import RxSwift

class TFBaseCollectionViewCell: UICollectionViewCell {

var disposeBag = DisposeBag()

override init(frame: CGRect) {
super.init(frame: frame)
makeUI()
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

override func prepareForReuse() {
super.prepareForReuse()
self.disposeBag = DisposeBag()
}

func makeUI() { }
}
24 changes: 24 additions & 0 deletions Falling/Sources/DataLayer/API/MainAPI/DTO/UserResponse.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//
// UserResponse.swift
// Falling
//
// Created by SeungMin on 2023/10/06.
//

struct UserResponse: Codable {
let userList: [UserDTO]
}

struct UserDTO: Codable {
let userIdx: Int
// enum CodingKeys: CodingKey {
//
// }
}

struct UserSectionMapper {
static func map(list: [UserDTO]) -> [UserSection] {
let mutableSection: [UserSection] = []
return mutableSection
}
}
161 changes: 161 additions & 0 deletions Falling/Sources/Feature/Main/Cell/MainCollectionViewCell.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
//
// UserCollectionViewCell.swift
// Falling
//
// Created by SeungMin on 2023/10/02.
//

import UIKit
import RxSwift

@objc protocol TimeOverDelegate: AnyObject {
@objc func scrollToNext()
}

final class MainCollectionViewCell: TFBaseCollectionViewCell {

var viewModel: MainCollectionViewItemViewModel!
weak var delegate: TimeOverDelegate?

lazy var userImageView: UIImageView = {
let imageView = UIImageView()
imageView.image = .add
return imageView
}()

lazy var userContentView: UIView = {
let view = UIView()
view.backgroundColor = FallingAsset.Color.clear.color
return view
}()

lazy var progressContainerView: UIView = {
let view = UIView()
view.layer.cornerRadius = 15
view.backgroundColor = FallingAsset.Color.dimColor.color.withAlphaComponent(0.5)
return view
}()

lazy var timerView = CardTimerView()

lazy var progressView = CardProgressView()

override func layoutSubviews() {
self.backgroundColor = .systemGray
}

override func makeUI() {
// TODO: cornerRadius 동적으로 설정해야 할 것.
self.layer.cornerRadius = 15

self.addSubview(userImageView)
self.addSubview(userContentView)

self.userContentView.addSubview(progressContainerView)

self.progressContainerView.addSubviews([
timerView,
progressView
])

self.userImageView.snp.makeConstraints {
$0.top.equalToSuperview()
$0.leading.equalToSuperview()
$0.bottom.equalToSuperview()
$0.trailing.equalToSuperview()
}

self.userContentView.snp.makeConstraints {
$0.top.equalToSuperview()
$0.leading.equalToSuperview()
$0.bottom.equalToSuperview()
$0.trailing.equalToSuperview()
}

self.progressContainerView.snp.makeConstraints {
$0.top.equalTo(self.safeAreaLayoutGuide).inset(12)
$0.leading.trailing.equalToSuperview().inset(12)
$0.height.equalTo(32)
}

self.timerView.snp.makeConstraints {
$0.leading.equalToSuperview().inset(9)
$0.centerY.equalToSuperview()
$0.width.equalTo(22)
$0.height.equalTo(22)
}

self.progressView.snp.makeConstraints {
$0.leading.equalTo(timerView.snp.trailing).offset(9)
$0.trailing.equalToSuperview().inset(12)
$0.centerY.equalToSuperview()
$0.height.equalTo(6)
}
}

override func prepareForReuse() {
super.prepareForReuse()
disposeBag = DisposeBag()
}

func setup(item: UserDTO) {
viewModel = MainCollectionViewItemViewModel(userDTO: item)
}

func bindViewModel() {
let output = viewModel.transform(input: MainCollectionViewItemViewModel.Input())

output.timeState
.drive(self.rx.timeState)
.disposed(by: self.disposeBag)

output.isTimeOver
.do { value in
if value { self.delegate?.scrollToNext() }
}.drive()
.disposed(by: self.disposeBag)
}

func dotPosition(progress: Double, rect: CGRect) -> CGPoint {
var progress = progress
// progress가 -0.05미만 혹은 1이상은 점(dot)을 0초에 위치시키기 위함
let strokeRange: Range<Double> = -0.05..<0.95
if !(strokeRange ~= progress) { progress = 0.95 }
let radius = CGFloat(rect.height / 2 - timerView.strokeLayer.lineWidth / 2)
let angle = 2 * CGFloat.pi * CGFloat(progress) - CGFloat.pi / 2
let dotX = radius * cos(angle + 0.35)
let dotY = radius * sin(angle + 0.35)

let point = CGPoint(x: dotX, y: dotY)

return CGPoint(
x: rect.midX + point.x,
y: rect.midY + point.y
)
}
}

extension Reactive where Base: MainCollectionViewCell {
var timeState: Binder<MainCollectionViewItemViewModel.TimeState> {
return Binder(self.base) { (base, timeState) in
base.timerView.trackLayer.strokeColor = timeState.fillColor.color.cgColor
base.timerView.strokeLayer.strokeColor = timeState.color.color.cgColor
base.timerView.dotLayer.strokeColor = timeState.color.color.cgColor
base.timerView.dotLayer.fillColor = timeState.color.color.cgColor
base.timerView.timerLabel.textColor = timeState.color.color
base.progressView.progressBarColor = timeState.color.color

base.timerView.dotLayer.isHidden = timeState.isDotHidden

base.timerView.timerLabel.text = timeState.getText

base.progressView.progress = CGFloat(timeState.getProgress)

// TimerView Animation은 소수점 둘째 자리까지 표시해야 오차가 발생하지 않음
let strokeEnd = round(CGFloat(timeState.getProgress) * 100) / 100
base.timerView.dotLayer.position = base.dotPosition(progress: strokeEnd, rect: base.timerView.bounds)

base.timerView.strokeLayer.strokeEnd = strokeEnd
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
//
// MainCollectionViewItemViewModel.swift
// Falling
//
// Created by SeungMin on 2023/10/06.
//

import Foundation

import RxSwift
import RxCocoa

final class MainCollectionViewItemViewModel: ViewModelType {

let userDTO: UserDTO

init(userDTO: UserDTO) {
self.userDTO = userDTO
}

enum TimeState {
case initial(value: Double) // 7~8
case five(value: Double) // 6~7
case four(value: Double) // 5~6
case three(value: Double) // 4~5
case two(value: Double) // 3~4
case one(value: Double) // 2~3
case zero(value: Double) // 1~2
case over(value: Double) // 0~1

init(rawValue: Double) {
switch rawValue {
case 7.0..<8.0:
self = .initial(value: rawValue)
case 6.0..<7.0:
self = .five(value: rawValue)
case 5.0..<6.0:
self = .four(value: rawValue)
case 4.0..<5.0:
self = .three(value: rawValue)
case 3.0..<4.0:
self = .two(value: rawValue)
case 2.0..<3.0:
self = .one(value: rawValue)
case 1.0..<2.0:
self = .zero(value: rawValue)
default:
self = .over(value: rawValue)
}
}

var color: FallingColors {
switch self {
case .zero, .five:
return FallingAsset.Color.primary500
case .four:
return FallingAsset.Color.thtOrange100
case .three:
return FallingAsset.Color.thtOrange200
case .two:
return FallingAsset.Color.thtOrange300
case .one:
return FallingAsset.Color.thtRed
default:
return FallingAsset.Color.neutral300
}
}

var isDotHidden: Bool {
switch self {
case .initial, .over:
return true
default:
return false
}
}

var fillColor: FallingColors {
switch self {
case .over:
return FallingAsset.Color.neutral300
default:
return FallingAsset.Color.clear
}
}

var getText: String {
switch self {
case .initial, .over:
return String("-")
case .five(let value), .four(let value), .three(let value), .two(let value), .one(let value), .zero(let value):
return String(Int(value) - 1)
}
}

var getProgress: Double {
switch self {
case .initial:
return 1
case .five(let value), .four(let value), .three(let value), .two(let value), .one(let value), .zero(let value), .over(let value):
return (value - 2) / 5
}
}
}

var disposeBag: DisposeBag = DisposeBag()

struct Input {

}

struct Output {
let timeState: Driver<TimeState>
let isTimeOver: Driver<Bool>
}

func transform(input: Input) -> Output {
let time = Observable<Int>.interval(.milliseconds(10),
scheduler: MainScheduler.instance)
.take(8 * 100 + 1)
.map { round((8 - Double($0) / 100) * 100) / 100 }
.asDriver(onErrorDriveWith: Driver<Double>.empty())

let timeState = time.map { TimeState(rawValue: $0) }
let isTimeOver = time.map { $0 == 0.0 }

return Output(
timeState: timeState,
isTimeOver: isTimeOver
)
}
}
Loading