Skip to content

Commit

Permalink
Merge pull request #38 from THT-Team/Feature/usercard_pagingview
Browse files Browse the repository at this point in the history
[#35] 현재 보이는 유저 카드 셀에서의 타임이 0이 되면 자동으로 다음 셀로 스크롤 구현
  • Loading branch information
Minny27 authored Oct 8, 2023
2 parents 67d2b10 + d59e697 commit 741304b
Show file tree
Hide file tree
Showing 9 changed files with 539 additions and 188 deletions.
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: UserDomain) {
viewModel = MainCollectionViewItemViewModel(userDomain: 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 userDomain: UserDomain

init(userDomain: UserDomain) {
self.userDomain = userDomain
}

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

0 comments on commit 741304b

Please sign in to comment.