-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #38 from THT-Team/Feature/usercard_pagingview
[#35] 현재 보이는 유저 카드 셀에서의 타임이 0이 되면 자동으로 다음 셀로 스크롤 구현
- Loading branch information
Showing
9 changed files
with
539 additions
and
188 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
24
Falling/Sources/DataLayer/API/MainAPI/DTO/UserResponse.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
161
Falling/Sources/Feature/Main/Cell/MainCollectionViewCell.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} | ||
} |
132 changes: 132 additions & 0 deletions
132
Falling/Sources/Feature/Main/Cell/MainCollectionViewItemViewModel.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
) | ||
} | ||
} |
Oops, something went wrong.