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

[GWL-142] 운동 시작 3,2,1 타이머 뷰컨트롤러 구현하기 #148

Merged
merged 16 commits into from
Nov 28, 2023
Merged
Show file tree
Hide file tree
Changes from 15 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
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,22 @@

import Foundation

/// 운동을 어떻게 할지(혼자 혹은 랜덤 매칭) 정보를 나타내는 데이터 타입 입니다.
/// 이 Entity는 어떻게 운동 할지 카드 UI를 만들 때 사용하는 엔티티 입니다.
/// 또한 이 타입을 갖고 내부 로직을 통해
/// 혼자 혹은 여럿이 운동할 수 있게 도와줍니다.
struct PeerType: Hashable {
// 카드에서 이미지를 나타내는 String 입니다.
// UIImage(systemName: peerType.iconSystemImage)처럼 활용됩니다.
let iconSystemImage: String

/// 카드 title의 Text를 나타내기 위해 쓰입니다.
let titleText: String

/// 카드 subTitle의 Text를 나타내기 위해 쓰입니다.
let descriptionText: String

/// 운동을 어떻게 할지(혼자 혹은 랜덤 매칭) 코드를 알려줍니다.
let typeCode: Int

init(icon: String, title: String, description: String, typeCode: Int) {
Comment on lines +11 to 29
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,10 @@ import Foundation
struct WorkoutSetting {
let workoutType: WorkoutType
let workoutPeerType: PeerType
let isWorkoutAlone: Bool
init(workoutType: WorkoutType, workoutPeerType: PeerType, isWorkoutAlone: Bool = true) {
self.workoutType = workoutType
self.workoutPeerType = workoutPeerType
self.isWorkoutAlone = isWorkoutAlone
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,31 @@

import Foundation

/// 사용자가 어떤 운동을 하는지 알려주기 위해 사용하는 Entity입니다.
/// 이 Entity를 통해 Card View를 보여줍니다.
public struct WorkoutType: Hashable {
/// Card에 나타나는 아이콘의 이미지 이름입니다. (SF Font 이미지 이름)
/// 다음과 같이 활용할 수 있습니다.UIimage(systemName: workoutType.workoutIcon)
let workoutIcon: String
let workoutIconDescription: String

/// 운동이름을 나타냅니다.
/// eg) 달리기 수영 사이클
let workoutTitle: String

// 어떤 운동을 하는지 숫자로 정의합니다.
// 중요: 서버측과 통신을 위해 Int로 만들었습니다.
// eg) 달리기는 1번, 사이클은...
let typeCode: Int

init(workoutIcon: String, workoutIconDescription: String, typeCode: Int) {
self.workoutIcon = workoutIcon
self.workoutIconDescription = workoutIconDescription
workoutTitle = workoutIconDescription
self.typeCode = typeCode
}

init(workoutTypesDTO dto: WorkoutTypeDTO) {
workoutIcon = dto.icon
workoutIconDescription = dto.description
workoutTitle = dto.description
typeCode = dto.typeCode
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
//
// CountDownBeforeWorkoutStartTimerUsecase.swift
// RecordFeature
//
// Created by MaraMincho on 11/28/23.
// Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved.
//

import Combine
import Foundation

// MARK: - CountDownBeforeWorkoutStartTimerUsecaseRepresentable

protocol CountDownBeforeWorkoutStartTimerUsecaseRepresentable {
func beforeWorkoutTimerTextPublisher() -> AnyPublisher<String, Never>
mutating func startTimer()
mutating func stopTimer()
}

// MARK: - CountDownBeforeWorkoutStartTimerUsecase

struct CountDownBeforeWorkoutStartTimerUsecase {
let initDate: Date
var timerCancellable: AnyCancellable?
let beforeWorkoutTimerTextSubject: CurrentValueSubject<String, Never> = .init("")
init(initDate: Date) {
self.initDate = initDate
timerCancellable = nil
}
}

// MARK: CountDownBeforeWorkoutStartTimerUsecaseRepresentable

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p3: Usecase -> UseCase로 변경해주실 수 있나요? 저희 네이밍이 전부 그렇게 되어있어서요.

extension CountDownBeforeWorkoutStartTimerUsecase: CountDownBeforeWorkoutStartTimerUsecaseRepresentable {
func beforeWorkoutTimerTextPublisher() -> AnyPublisher<String, Never> {
return beforeWorkoutTimerTextSubject.eraseToAnyPublisher()
}

func beforeStartingWorkoutTime() -> Double {
return initDate.timeIntervalSince(.now)
}

/// 뷰컨트롤러의 던져줄 타이머에 관해서 세팅합니다.
mutating func startTimer() {
timerCancellable = Timer.publish(every: 0.1, on: RunLoop.main, in: .common)
.autoconnect()
.sink { [self] _ in
let beforeTime = beforeStartingWorkoutTime()
let firstMumberMilisecondsFromNow = String(format: "%.1f", beforeStartingWorkoutTime()).suffix(1)
if firstMumberMilisecondsFromNow == "0" {
let message = Int(beforeTime)
// 중요 만약 던지는 뷰에 전달해야 할 타이머 숫자가 0 이라면, timerSubject의 complet시킨다.
message != 0
? beforeWorkoutTimerTextSubject.send(message.description)
: beforeWorkoutTimerTextSubject.send(completion: .finished)
}
}
}

mutating func stopTimer() {
timerCancellable = nil
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// WorkoutSettingCoordinating.swift
// WorkoutEnvironmentSetUpCoordinating.swift
// RecordFeature
//
// Created by 안종표 on 2023/11/20.
Expand All @@ -9,7 +9,7 @@
import Coordinator
import Foundation

protocol WorkoutSettingCoordinating: Coordinating {
protocol WorkoutEnvironmentSetUpCoordinating: Coordinating {
func pushWorkoutSelectViewController()
func pushWorkoutEnvironmentSetupViewController()
func pushPeerRandomMatchingViewController(workoutSetting: WorkoutSetting)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ protocol WorkoutSessionCoordinating: Coordinating {
/// 운동 요약 화면으로 이동합니다.
/// - Parameter recordID: 요약 화면을 보여주기 위한 기록 Identifier
func pushWorkoutSummaryViewController(recordID: Int)
func pushCountDownBeforeWokroutViewController()
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public final class RecordFeatureCoordinator: RecordFeatureCoordinating {
}

func showSettingFlow() {
let workoutSettingCoordinator = WorkoutSettingCoordinator(navigationController: navigationController)
let workoutSettingCoordinator = WorkoutEnvironmentSetUpCoordinator(navigationController: navigationController)
childCoordinators.append(workoutSettingCoordinator)
workoutSettingCoordinator.finishDelegate = self
workoutSettingCoordinator.settingDidFinishedDelegate = self
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// WorkoutSettingCoordinator.swift
// WorkoutEnvironmentSetUpCoordinator.swift
// RecordFeature
//
// Created by 안종표 on 2023/11/20.
Expand All @@ -11,9 +11,9 @@ import Log
import Trinet
import UIKit

// MARK: - WorkoutSettingCoordinator
// MARK: - WorkoutEnvironmentSetUpCoordinator

final class WorkoutSettingCoordinator: WorkoutSettingCoordinating {
final class WorkoutEnvironmentSetUpCoordinator: WorkoutEnvironmentSetUpCoordinating {
var navigationController: UINavigationController
var childCoordinators: [Coordinating] = []
weak var finishDelegate: CoordinatorFinishDelegate?
Expand Down Expand Up @@ -67,12 +67,21 @@ final class WorkoutSettingCoordinator: WorkoutSettingCoordinating {
// TODO: 뷰 컨트롤러 시작 로직 작성
}

func finish(workoutSetting: WorkoutSetting) {
settingDidFinishedDelegate?.workoutSettingCoordinatorDidFinished(workoutSetting: workoutSetting)
func finish(workoutSetting _: WorkoutSetting) {
let useCase = CountDownBeforeWorkoutStartTimerUsecase(initDate: .now + 8)

let vm = CountDownBeforeWorkoutViewModel(coordinator: self, useCase: useCase)

let vc = CountDownBeforeWorkoutViewController(viewModel: vm)
navigationController.pushViewController(vc, animated: true)

// TODO: 주석 풀고 코디네이팅 연결 하는 작업 필요
// 현재는 타이머 뷰컨 실험할려고 잠깐 죽인 코드
// settingDidFinishedDelegate?.workoutSettingCoordinatorDidFinished(workoutSetting: workoutSetting)
Comment on lines +78 to +80
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p3: PR에 불필요한 코드라면 git stash를 사용하는 것도 좋은 방법 중에 하나가 될 것 같아요 :)

}
}

private extension WorkoutSettingCoordinator {
private extension WorkoutEnvironmentSetUpCoordinator {
func makeMockDataFromRnaomMatching() -> URLSessionProtocol {
let mockSession = MockURLSession(mockDataByURLString: makeMockDataFromRnaomMatchingDataByURLString())
return mockSession
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,10 @@ final class WorkoutSessionCoordinator: WorkoutSessionCoordinating {
let workoutSummaryViewController = WorkoutSummaryViewController(viewModel: viewModel)
navigationController.setViewControllers([workoutSummaryViewController], animated: true)
}

func pushCountDownBeforeWokroutViewController() {
// TODO: CountDown 관련 ViewController 생성
// let viewModel = CountDownBeforeWorkoutViewModel(coordinator: self)
// let viewController = CountDownBeforeWorkoutViewController(viewModel: viewModel)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
//
// CountDownBeforeWorkoutViewController.swift
// RecordFeature
//
// Created by MaraMincho on 11/27/23.
// Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved.
//

import Combine
import DesignSystem
import Log
import UIKit

// MARK: - CountDownBeforeWorkoutViewController

final class CountDownBeforeWorkoutViewController: UIViewController {
// MARK: Properties

private let viewModel: CountDownBeforeWorkoutViewModelRepresentable

private var subscriptions: Set<AnyCancellable> = []

private var didFinishTimerTextSubscriptionSubject: PassthroughSubject<Void, Never> = .init()
private var viewDidAppearSubject: PassthroughSubject<Void, Never> = .init()

// MARK: UI Components

private let countDownLabel: UILabel = {
let label = UILabel()
label.font = UIConsts.contDownFontSize
label.textColor = DesignSystemColor.primaryBackground

label.translatesAutoresizingMaskIntoConstraints = false
return label
}()

private let countDownLabelCover: UIView = {
let view = UIView()
view.backgroundColor = DesignSystemColor.main03

view.layer.cornerRadius = Metrics.coverWidthAndHeight / 2
view.clipsToBounds = true

view.translatesAutoresizingMaskIntoConstraints = false
return view
}()

// MARK: Initializations

init(viewModel: CountDownBeforeWorkoutViewModelRepresentable) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}

@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

// MARK: Life Cycles

override func viewDidLoad() {
super.viewDidLoad()
setup()
}

override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
viewDidAppearSubject.send(())
}
}

private extension CountDownBeforeWorkoutViewController {
// MARK: Configuration

private func setup() {
setupHierarchyAndConstraints()
bind()
setupStyles()
}

func setupHierarchyAndConstraints() {
view.addSubview(countDownLabelCover)
countDownLabelCover.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
countDownLabelCover.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
countDownLabelCover.widthAnchor.constraint(equalToConstant: Metrics.coverWidthAndHeight).isActive = true
countDownLabelCover.heightAnchor.constraint(equalToConstant: Metrics.coverWidthAndHeight).isActive = true

view.addSubview(countDownLabel)
countDownLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
countDownLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
}

func setupStyles() {
view.backgroundColor = DesignSystemColor.primaryBackground
}

func bindViewModel() {
let input = CountDownBeforeWorkoutViewModelInput(
viewDidApperPubilsehr: viewDidAppearSubject.eraseToAnyPublisher(),
didFinsihTimerSubscrion: didFinishTimerTextSubscriptionSubject.eraseToAnyPublisher()
)

viewModel
.transform(input: input)
.sink(receiveCompletion: { [weak self] stateResults in
switch stateResults {
case .failure(_),
.finished:
self?.didFinishTimerTextSubscriptionSubject.send(())
}
}, receiveValue: { [weak self] state in
switch state {
case let .updateMessage(message): self?.makeLabelAnimation(labelText: message)
case .idle: break
}
})
.store(in: &subscriptions)
}

func bind() {
subscriptions.removeAll()

bindViewModel()
}

func makeLabelAnimation(labelText: String) {
Log.make().debug("viewController makeLabelAnimation: \(labelText)")
countDownLabel.text = labelText
countDownLabel.transform = CGAffineTransform(scaleX: 1, y: 1)
view.layoutIfNeeded()

UIView.animate(withDuration: 0.4, delay: 0, options: .curveEaseOut) { [weak self] in
guard let self else { return }
let scale = UIConsts.minFontTransormScale
countDownLabel.transform = CGAffineTransform(scaleX: scale, y: scale)
}
}

enum Metrics {
static let coverWidthAndHeight: CGFloat = 240
}

enum UIConsts {
static let contDownFontSize: UIFont = .systemFont(ofSize: 120, weight: .bold)
static let minFontTransormScale: CGFloat = 0.6
}
}
Loading