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-84] Workout Session 전체 뷰 구성 + Swiftformat CI 추가 #117

Merged
merged 8 commits into from
Nov 24, 2023
26 changes: 26 additions & 0 deletions .github/workflows/iOS_CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,32 @@ on:
types: [labeled, opened, synchronize, reopened]

jobs:
swift-format:
if: contains(github.event.pull_request.labels.*.name, '📱 iOS')
runs-on: macos-13
env:
working-directory: ./iOS

steps:
- name: Code Checkout
uses: actions/checkout@v3

- name: Install Swiftformat
run: brew install swiftformat

- name: Run Swiftformat
run: swiftformat ./Projects --config .swiftformat
working-directory: ${{env.working-directory}}

- name: Check for changes
run: |
git diff
if [[ `git status --porcelain` ]]; then
echo "Code was formatted. Failing the job."
exit 1
fi
working-directory: ${{env.working-directory}}

tuist-test:
if: contains(github.event.pull_request.labels.*.name, '📱 iOS')
runs-on: macos-13
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ protocol RecordUpdateUseCaseRepresentable {
func execute(date: Date) -> AnyPublisher<[Record], Error>
}

// MARK: - RecordUpdateUsecaseError
// MARK: - RecordUpdateUseCaseError

enum RecordUpdateUseCaseError: Error {
case noRecord
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
import Combine
import CoreLocation
import DesignSystem
import MapKit
import Log
import MapKit
import UIKit

// MARK: - WorkoutRouteMapViewController
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,6 @@ final class WorkoutRouteMapViewModel {

extension WorkoutRouteMapViewModel: WorkoutRouteMapViewModelRepresentable {
public func transform(input _: WorkoutRouteMapViewModelInput) -> WorkoutRouteMapViewModelOutput {
for subscription in subscriptions {
subscription.cancel()
}
subscriptions.removeAll()

let initialState: WorkoutRouteMapViewModelOutput = Just(.idle).eraseToAnyPublisher()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,30 +21,8 @@ public final class WorkoutSessionViewController: UIViewController {

private var subscriptions: Set<AnyCancellable> = []

private let endWorkoutSubject: PassthroughSubject<Void, Never> = .init()

// MARK: UI Components

private let recordTimerLabel: UILabel = {
let label = UILabel()
label.font = .preferredFont(forTextStyle: .largeTitle)
label.text = "0분 0초"
return label
}()

private lazy var endWorkoutButton: UIButton = {
let button = UIButton(configuration: .mainCircularEnabled(title: "종료"))
button.configuration?.font = .preferredFont(forTextStyle: .largeTitle, with: .traitBold)
button.accessibilityHint = "운동을 종료합니다."
button.addAction(
UIAction { [weak self] _ in
self?.endWorkoutSubject.send(())
},
for: .touchUpInside
)
return button
}()

private lazy var participantsCollectionView: UICollectionView = {
let collectionView = UICollectionView(
frame: .zero,
Expand Down Expand Up @@ -82,47 +60,19 @@ public final class WorkoutSessionViewController: UIViewController {
// MARK: Configuration

private func setupLayouts() {
view.addSubview(recordTimerLabel)
view.addSubview(participantsCollectionView)
view.addSubview(endWorkoutButton)
}

private func setupConstraints() {
let safeArea = view.safeAreaLayoutGuide
recordTimerLabel.translatesAutoresizingMaskIntoConstraints = false
endWorkoutButton.translatesAutoresizingMaskIntoConstraints = false
participantsCollectionView.translatesAutoresizingMaskIntoConstraints = false

NSLayoutConstraint.activate(
[
recordTimerLabel.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor, constant: Metrics.horizontal),
recordTimerLabel.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor, constant: -Metrics.horizontal),
recordTimerLabel.topAnchor.constraint(equalTo: safeArea.topAnchor, constant: Metrics.recordTimerLabelTop),

participantsCollectionView.topAnchor.constraint(
equalTo: recordTimerLabel.bottomAnchor,
constant: Metrics.collectionViewTop
),
participantsCollectionView.leadingAnchor.constraint(
equalTo: safeArea.leadingAnchor,
constant: Metrics.horizontal
),
participantsCollectionView.trailingAnchor.constraint(
equalTo: safeArea.trailingAnchor,
constant: -Metrics.horizontal
),
participantsCollectionView.bottomAnchor.constraint(
equalTo: endWorkoutButton.topAnchor,
constant: -Metrics.collectionViewBottom
),

endWorkoutButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
endWorkoutButton.widthAnchor.constraint(equalToConstant: Metrics.endingWorkoutButtonSize),
endWorkoutButton.heightAnchor.constraint(equalToConstant: Metrics.endingWorkoutButtonSize),
endWorkoutButton.bottomAnchor.constraint(
equalTo: safeArea.bottomAnchor,
constant: -Metrics.endingWorkoutButtonBottom
),
participantsCollectionView.topAnchor.constraint(equalTo: safeArea.topAnchor),
participantsCollectionView.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor),
participantsCollectionView.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor),
participantsCollectionView.bottomAnchor.constraint(equalTo: safeArea.bottomAnchor),
]
)
}
Expand All @@ -132,7 +82,7 @@ public final class WorkoutSessionViewController: UIViewController {
}

private func bind() {
let output = viewModel.transform(input: .init(endWorkoutPublisher: endWorkoutSubject.eraseToAnyPublisher()))
let output = viewModel.transform(input: .init())
output.sink { state in
switch state {
case .idle:
Expand Down Expand Up @@ -204,14 +154,7 @@ public final class WorkoutSessionViewController: UIViewController {

private extension WorkoutSessionViewController {
enum Metrics {
static let recordTimerLabelTop: CGFloat = 12
static let collectionViewTop: CGFloat = 12
static let collectionViewBottom: CGFloat = 44
static let collectionViewItemSpacing: CGFloat = 6
static let horizontal: CGFloat = 36
static let endingWorkoutButtonBottom: CGFloat = 32

static let endingWorkoutButtonSize: CGFloat = 150
static let collectionViewCellHeight: CGFloat = 84
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@ import Combine

// MARK: - WorkoutSessionViewModelInput

public struct WorkoutSessionViewModelInput {
let endWorkoutPublisher: AnyPublisher<Void, Never>
}
public struct WorkoutSessionViewModelInput {}

public typealias WorkoutSessionViewModelOutput = AnyPublisher<WorkoutSessionState, Never>

Expand Down Expand Up @@ -43,16 +41,9 @@ public final class WorkoutSessionViewModel {
// MARK: WorkoutSessionViewModelRepresentable

extension WorkoutSessionViewModel: WorkoutSessionViewModelRepresentable {
public func transform(input: WorkoutSessionViewModelInput) -> WorkoutSessionViewModelOutput {
for subscription in subscriptions {
subscription.cancel()
}
public func transform(input _: WorkoutSessionViewModelInput) -> WorkoutSessionViewModelOutput {
subscriptions.removeAll()

input.endWorkoutPublisher
.sink {}
.store(in: &subscriptions)

let initialState: WorkoutSessionViewModelOutput = Just(.idle).eraseToAnyPublisher()

return initialState
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
//
// WorkoutSessionContainerViewController.swift
// RecordFeature
//
// Created by 홍승현 on 11/23/23.
// Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved.
//

import Combine
import DesignSystem
import UIKit

// MARK: - WorkoutSessionContainerViewController

final class WorkoutSessionContainerViewController: UIViewController {
// MARK: Properties

private let viewModel: WorkoutSessionContainerViewModelRepresentable

private var subscriptions: Set<AnyCancellable> = []

private let endWorkoutSubject: PassthroughSubject<Void, Never> = .init()

// MARK: UI Components

private let viewControllers: [UIViewController] = [
WorkoutSessionViewController(viewModel: WorkoutSessionViewModel()),
WorkoutRouteMapViewController(viewModel: WorkoutRouteMapViewModel()),
]

private let recordTimerLabel: UILabel = {
let label = UILabel()
label.font = .preferredFont(forTextStyle: .largeTitle)
label.text = "0분 0초"
return label
}()

private lazy var endWorkoutButton: UIButton = {
let button = UIButton(configuration: .mainCircularEnabled(title: "종료"))
button.configuration?.font = .preferredFont(forTextStyle: .largeTitle, weight: .bold)
button.accessibilityHint = "운동을 종료합니다."
return button
}()

private lazy var pageControl: GWPageControl = .init(count: viewControllers.count)

private lazy var pageViewController: UIPageViewController = {
let pageViewController = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal)
pageViewController.dataSource = self
return pageViewController
}()

// MARK: Initializations

init(viewModel: WorkoutSessionContainerViewModelRepresentable) {
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()
setupLayouts()
setupConstraints()
setupStyles()
bind()
}

// MARK: Configuration

private func setupLayouts() {
addChild(pageViewController)
view.addSubview(recordTimerLabel)
view.addSubview(pageViewController.view)
view.addSubview(pageControl)
view.addSubview(endWorkoutButton)
pageViewController.didMove(toParent: self)

if let firstViewController = viewControllers.first {
pageViewController.setViewControllers([firstViewController], direction: .forward, animated: false)
}
}

private func setupConstraints() {
let safeArea = view.safeAreaLayoutGuide

recordTimerLabel.translatesAutoresizingMaskIntoConstraints = false
endWorkoutButton.translatesAutoresizingMaskIntoConstraints = false
pageViewController.view.translatesAutoresizingMaskIntoConstraints = false
pageControl.translatesAutoresizingMaskIntoConstraints = false

NSLayoutConstraint.activate(
[
recordTimerLabel.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor, constant: Metrics.horizontal),
recordTimerLabel.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor, constant: -Metrics.horizontal),
recordTimerLabel.topAnchor.constraint(equalTo: safeArea.topAnchor, constant: Metrics.recordTimerLabelTop),

pageViewController.view.topAnchor.constraint(equalTo: recordTimerLabel.bottomAnchor, constant: Metrics.pageViewControllerTop),
pageViewController.view.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor, constant: Metrics.horizontal),
pageViewController.view.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor, constant: -Metrics.horizontal),
pageViewController.view.bottomAnchor.constraint(equalTo: pageControl.topAnchor, constant: -Metrics.pageViewControllerBottom),

pageControl.centerXAnchor.constraint(equalTo: view.centerXAnchor),
pageControl.bottomAnchor.constraint(equalTo: endWorkoutButton.topAnchor, constant: -Metrics.pageControlBottom),
pageControl.heightAnchor.constraint(equalToConstant: Metrics.pageControlHeight),

endWorkoutButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
endWorkoutButton.widthAnchor.constraint(equalToConstant: Metrics.endingWorkoutButtonSize),
endWorkoutButton.heightAnchor.constraint(equalToConstant: Metrics.endingWorkoutButtonSize),
endWorkoutButton.bottomAnchor.constraint(
equalTo: safeArea.bottomAnchor,
constant: -Metrics.endingWorkoutButtonBottom
),
]
)
}

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

private func bind() {
let output = viewModel.transform(input: .init(endWorkoutPublisher: endWorkoutSubject.eraseToAnyPublisher()))
output.sink { state in
switch state {
case .idle:
break
}
}
.store(in: &subscriptions)
}
}

// MARK: UIPageViewControllerDataSource

extension WorkoutSessionContainerViewController: UIPageViewControllerDataSource {
func pageViewController(_: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
guard let viewControllerIndex = viewControllers.firstIndex(of: viewController) else {
return nil
}

let previousIndex = viewControllerIndex - 1

guard previousIndex >= 0 else {
return nil
}

guard viewControllers.count > previousIndex else {
return nil
}
pageControl.select(at: previousIndex)
return viewControllers[previousIndex]
}

func pageViewController(_: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
guard let viewControllerIndex = viewControllers.firstIndex(of: viewController) else {
return nil
}

let nextIndex = viewControllerIndex + 1
let viewControllersCount = viewControllers.count

guard viewControllersCount != nextIndex else {
return nil
}

guard viewControllersCount > nextIndex else {
return nil
}
pageControl.select(at: nextIndex)
return viewControllers[nextIndex]
}
}

// MARK: WorkoutSessionContainerViewController.Metrics

private extension WorkoutSessionContainerViewController {
enum Metrics {
static let horizontal: CGFloat = 36
static let recordTimerLabelTop: CGFloat = 12
static let pageViewControllerTop: CGFloat = 12
static let pageViewControllerBottom: CGFloat = 24
static let pageControlBottom: CGFloat = 12
static let endingWorkoutButtonBottom: CGFloat = 32

static let endingWorkoutButtonSize: CGFloat = 150
static let pageControlHeight: CGFloat = 8
}
}

@available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, xrOS 1.0, *)
#Preview {
WorkoutSessionContainerViewController(viewModel: WorkoutSessionContainerViewModel())
}
Loading