From 3e1e404a6d6c48d4c1d97ef84d27b76c33d8dd6b Mon Sep 17 00:00:00 2001 From: elppaaa Date: Sun, 24 Apr 2022 21:15:39 +0900 Subject: [PATCH 01/10] feat: Add RandomMusicQuiz logic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Reactor 기능 추가 #8 --- .../Reactor/RandomMusicQuizReactor.swift | 97 ++++++++++++++++++- 1 file changed, 93 insertions(+), 4 deletions(-) diff --git a/LiarGame/Sources/Reactor/RandomMusicQuizReactor.swift b/LiarGame/Sources/Reactor/RandomMusicQuizReactor.swift index 43a7974..b362df9 100644 --- a/LiarGame/Sources/Reactor/RandomMusicQuizReactor.swift +++ b/LiarGame/Sources/Reactor/RandomMusicQuizReactor.swift @@ -9,16 +9,105 @@ import Foundation import ReactorKit final class RandomMusicQuizReactor: Reactor { + init(repository: RandomMusicRepository) { + self.repository = repository + } var initialState = State() + private let repository: RandomMusicRepository + + enum PlaySeconds: Int { + case three = 3 + case five = 5 + case ten = 10 + } + + enum Action { + case updateMusicList + case playMusic(second: PlaySeconds) + case didPlayButtonTapped + case didStopButtonTapped + case didAnswerButtonTapped + case shuffle + } + + enum Mutation { + case updatePlayingState(Bool) + case updateCurrentVersion(String) + case updateCurrentMusic(Music) + case updateAnswer((String, String)?) + case updateLoading(Bool) + case updateMusicList([Music]) + } + + struct State { + var isPlaying: Bool = false + var isLoading: Bool = false + var currentVersion: String = "" + var answer: (title: String, artist: String)? + var currentMusic: Music? + // music List 가 관리되어야 할 상태에 포함되는지 고려해야 함. + var musicList: [Music] = [] + } - enum Action { } - enum Mutation { } - struct State { } func mutate(action: Action) -> Observable { - + switch action { + case .updateMusicList: + return repository.getNewestVersion() + .asObservable() + .map { Mutation.updateMusicList($0) } + + case let .playMusic(second): + return .concat([ + .just(.updatePlayingState(true)), + .just(.updatePlayingState(false)) + .timeout(.seconds(second.rawValue), scheduler: ConcurrentDispatchQueueScheduler(qos: .default)) + ]) + + case .didPlayButtonTapped: + return .just(.updatePlayingState(true)) + case .didStopButtonTapped: + return .just(.updatePlayingState(false)) + + case .didAnswerButtonTapped: + return .just(.updateAnswer(currentAnswer())) + + case .shuffle: + return .just(.updateCurrentMusic(shuffleMusic())) + } } + func reduce(state: State, mutation: Mutation) -> State { + var state = state + switch mutation { + case let .updatePlayingState(boolean): + state.isPlaying = boolean + case let .updateCurrentVersion(version): + state.currentVersion = version + case let .updateCurrentMusic(music): + state.currentMusic = music + case let .updateAnswer(info): + state.answer = info + case let .updateLoading(boolean): + state.isLoading = boolean + case let .updateMusicList(list): + state.musicList = list + } + return state + } + + private func currentAnswer() -> (title: String, artist: String)? { + if let currentMusic = currentState.currentMusic { + return (title: currentMusic.title, artist: currentMusic.artist) + } else { + return nil + } + } + + private func shuffleMusic() -> Music { + let size = currentState.musicList.count + let randomNumber: Int = Int(arc4random()) % size + return currentState.musicList[randomNumber] } } From e15700213f7e215658bdc7ee931c80955fbae158 Mon Sep 17 00:00:00 2001 From: elppaaa Date: Tue, 26 Apr 2022 04:18:12 +0900 Subject: [PATCH 02/10] feat: Add RandomMusicQuizView MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - view 추가 - 장르 선택 영역 추가 (변경 가능성 있음) #8 --- LiarGame/Sources/Utils/Flex+Extensions.swift | 32 +++++ .../Sources/Utils/UIColor+Extensions.swift | 21 +++ .../DashedLineBorderdLabel.swift | 51 +++++++ .../RandomMusicQuiz/RandomMusicQuizView.swift | 125 ++++++++++++++++++ .../RandomMusicQuizViewController.swift | 28 +++- 5 files changed, 255 insertions(+), 2 deletions(-) create mode 100644 LiarGame/Sources/Utils/Flex+Extensions.swift create mode 100644 LiarGame/Sources/Utils/UIColor+Extensions.swift create mode 100644 LiarGame/Sources/ViewController/RandomMusicQuiz/DashedLineBorderdLabel.swift create mode 100644 LiarGame/Sources/ViewController/RandomMusicQuiz/RandomMusicQuizView.swift diff --git a/LiarGame/Sources/Utils/Flex+Extensions.swift b/LiarGame/Sources/Utils/Flex+Extensions.swift new file mode 100644 index 0000000..092ac72 --- /dev/null +++ b/LiarGame/Sources/Utils/Flex+Extensions.swift @@ -0,0 +1,32 @@ +// +// Flex+Extensions.swift +// LiarGame +// +// Created by JK on 2022/04/26. +// + +import Foundation +import CoreGraphics +import FlexLayout + +extension Flex { + @discardableResult + func horizontallySpacing(_ value: CGFloat?) -> Flex { + guard let view = view, view.subviews.count > 1 else { return self } + for (idx, subview) in view.subviews.enumerated() { + if idx == 0 { continue } + subview.flex.marginLeft(value ?? 0) + } + return self + } + + @discardableResult + func verticallySpacing(_ value: CGFloat?) -> Flex { + guard let view = view, view.subviews.count > 1 else { return self } + for (idx, subview) in view.subviews.enumerated() { + if idx == 0 { continue } + subview.flex.marginTop(value ?? 0) + } + return self + } +} diff --git a/LiarGame/Sources/Utils/UIColor+Extensions.swift b/LiarGame/Sources/Utils/UIColor+Extensions.swift new file mode 100644 index 0000000..63a9fbf --- /dev/null +++ b/LiarGame/Sources/Utils/UIColor+Extensions.swift @@ -0,0 +1,21 @@ +// +// UIColor+Extensions.swift +// LiarGame +// +// Created by JK on 2022/04/26. +// + +import UIKit + +extension UIColor { + convenience init(hexString : String) { + if let rgbValue = UInt(hexString, radix: 16) { + let red = CGFloat((rgbValue >> 16) & 0xff) / 255 + let green = CGFloat((rgbValue >> 8) & 0xff) / 255 + let blue = CGFloat((rgbValue ) & 0xff) / 255 + self.init(red: red, green: green, blue: blue, alpha: 1.0) + } else { + self.init(red: 0.0, green: 0.0, blue: 0.0, alpha: 1.0) + } + } +} diff --git a/LiarGame/Sources/ViewController/RandomMusicQuiz/DashedLineBorderdLabel.swift b/LiarGame/Sources/ViewController/RandomMusicQuiz/DashedLineBorderdLabel.swift new file mode 100644 index 0000000..e81c503 --- /dev/null +++ b/LiarGame/Sources/ViewController/RandomMusicQuiz/DashedLineBorderdLabel.swift @@ -0,0 +1,51 @@ +// +// DashedLineBorderdLabel.swift +// LiarGame +// +// Created by JK on 2022/04/26. +// + +import UIKit + +class DashedLineBorderdLabel: UILabel { + @available(*, unavailable) + required init?(coder: NSCoder) { fatalError() } + init(cornerRadius: CGFloat = 8.0, borderWidth: CGFloat = 1.0, borderColor: UIColor) { + self.borderWidth = borderWidth + self.borderColor = borderColor + self.cornerRadius = cornerRadius + super.init(frame: .zero) + self.layer.cornerRadius = cornerRadius + } + + + private let borderWidth: CGFloat + private let borderColor: UIColor + private let cornerRadius: CGFloat + + var dashBorder: CAShapeLayer? + + override func layoutSubviews() { + super.layoutSubviews() + + dashBorder?.removeFromSuperlayer() + let dashBorder = CAShapeLayer() + dashBorder.lineWidth = borderWidth + dashBorder.strokeColor = borderColor.cgColor + dashBorder.lineDashPattern = [3, 2] + dashBorder.frame = bounds + dashBorder.fillColor = nil + let horizontalInset = 8.0 + let verticalInset = 4.0 + let bounds = CGRect( + x: bounds.origin.x - horizontalInset, + y: bounds.origin.y - verticalInset, + width: bounds.width + horizontalInset * 2, + height: bounds.height + verticalInset * 2 + ) + dashBorder.path = UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius).cgPath + layer.addSublayer(dashBorder) + self.dashBorder = dashBorder + } + +} diff --git a/LiarGame/Sources/ViewController/RandomMusicQuiz/RandomMusicQuizView.swift b/LiarGame/Sources/ViewController/RandomMusicQuiz/RandomMusicQuizView.swift new file mode 100644 index 0000000..4e7323f --- /dev/null +++ b/LiarGame/Sources/ViewController/RandomMusicQuiz/RandomMusicQuizView.swift @@ -0,0 +1,125 @@ +// +// RandomMusicQuizView.swift +// LiarGame +// +// Created by JK on 2022/04/26. +// + +import UIKit +import FlexLayout +import PinLayout +import YouTubeiOSPlayerHelper + +final class RandomQuizView: UIView { + @available(*, unavailable) + required init?(coder: NSCoder) { fatalError() } + init() { + super.init(frame: .zero) + setupViews() + } + + + fileprivate let container = UIView() + private let _tintColor = UIColor(hexString: "1D5C63") + + private lazy var threeSecondButton = makeRoundedButton(tintColor: _tintColor, str: "3초") + private lazy var fiveSecondButton = makeRoundedButton(tintColor: _tintColor, str: "5초") + private lazy var tenSecondButton = makeRoundedButton(tintColor: _tintColor, str: "10초") + private lazy var playButton = makeRoundedButton(tintColor: _tintColor, str: "재생") + + private lazy var currentVersionLabel = UILabel().then { + $0.textColor = .label + } + + private lazy var updateButton = makeRoundedButton(tintColor: _tintColor).then { + $0.setImage(UIImage(systemName: "arrow.clockwise"), for: .normal) + } + private lazy var shuffleButton = makeRoundedButton(tintColor: _tintColor).then { + $0.setImage(UIImage(systemName: "shuffle"), for: .normal) + } + + private lazy var showAnswerButton = makeRoundedButton(tintColor: _tintColor, str: "정답 보기") + + private lazy var answerLabel = DashedLineBorderdLabel(borderColor: _tintColor).then { + $0.font = .preferredFont(forTextStyle: .title1) + $0.isHidden = true + } + + private let ytPlayer = YTPlayerView(frame: .zero) + + override func layoutSubviews() { + super.layoutSubviews() + + container.pin.all(pin.safeArea) + container.flex.layout() + } + + private func setupViews() { + backgroundColor = UIColor(hexString: "EDE6DB") + addSubview(container) + + container.flex + .direction(.column).justifyContent(.center).marginHorizontal(20).define { + // 장르 선택 영역 + $0.addItem(UILabel().then { $0.text = "Genre Area"; $0.backgroundColor = .systemGray; $0.textAlignment = .center }) + .width(100%).aspectRatio(1.0) + .shrink(1) + + $0.addItem().direction(.row).height(150).justifyContent(.spaceAround).alignItems(.end).define { + $0.addItem(shuffleButton).padding(8) + + $0.addItem().direction(.column).justifyContent(.start).alignItems(.center).define { + $0.addItem(currentVersionLabel) + $0.addItem(updateButton).padding(8) + .marginTop(8) + } + } + + $0.addItem().direction(.row).height(40).justifyContent(.spaceEvenly).define { flex in + [playButton, threeSecondButton, fiveSecondButton, tenSecondButton].forEach { + flex.addItem($0) + .grow(1) + } + } + .horizontallySpacing(10) + + $0.addItem().height(30) + + $0.addItem(showAnswerButton) + .width(150).height(50) + .alignSelf(.center) + $0.addItem(answerLabel) + .minHeight(answerLabel.font.lineHeight) + .minWidth(75) + .alignSelf(.center) + + } + .verticallySpacing(20) + + } + + func setAnswerLabel(_ value: String?) { + if value != nil { + answerLabel.isHidden = false + answerLabel.text = value + } else { + answerLabel.isHidden = true + } + } + +} + +fileprivate func makeRoundedButton(tintColor: UIColor, str: String? = nil) -> UIButton { + let button = UIButton() + + button.layer.cornerRadius = 15 + button.layer.cornerCurve = .continuous + button.backgroundColor = tintColor + button.tintColor = .white + str.map { + button.setTitle($0, for: .normal) + button.setTitleColor(.systemGray, for: .highlighted) + } + + return button +} diff --git a/LiarGame/Sources/ViewController/RandomMusicQuiz/RandomMusicQuizViewController.swift b/LiarGame/Sources/ViewController/RandomMusicQuiz/RandomMusicQuizViewController.swift index 88e45e3..2f442d5 100644 --- a/LiarGame/Sources/ViewController/RandomMusicQuiz/RandomMusicQuizViewController.swift +++ b/LiarGame/Sources/ViewController/RandomMusicQuiz/RandomMusicQuizViewController.swift @@ -7,20 +7,44 @@ import UIKit import ReactorKit +import RxSwift +import Then +/* + TODO: + + 랜덤 음악 퀴즈 게임 + + - 3초 재생버튼, 5초 재생버튼, 10초 재생버튼을 추가. + - 정답을 보여줄 버튼 추가 + - + + */ final class RandomMusicQuizViewController: UIViewController, View { + + private let content = RandomQuizView() + init(reactor: RandomMusicQuizReactor) { super.init(nibName: nil, bundle: nil) self.reactor = reactor setupViews() } + @available(*, unavailable) required init?(coder: NSCoder) { fatalError() } + override func loadView() { + super.loadView() + self.view = content + } + var disposeBag = DisposeBag() func bind(reactor: RandomMusicQuizReactor) { - + } - func setupViews() { } + func setupViews() { + + } } + From b9aa116b9af5b4ca9258d1d82fb338287cc27bbc Mon Sep 17 00:00:00 2001 From: elppaaa Date: Tue, 26 Apr 2022 08:42:16 +0900 Subject: [PATCH 03/10] feat: RandomMusicQuiz view, binding logic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - view 수정 - state 와 action 바인딩 - Music model float 로 변경 - 음악 재생/중지 - 업데이트, 셔플 - 정답 보기 # 8 --- LiarGame/Sources/Model/Music.swift | 9 +- .../Reactor/RandomMusicQuizReactor.swift | 58 +++++++---- .../Repository/RandomMusicRepository.swift | 10 +- .../RandomMusicQuiz/RandomMusicQuizView.swift | 81 ++++++++++----- .../RandomMusicQuizViewController.swift | 98 ++++++++++++++++--- 5 files changed, 190 insertions(+), 66 deletions(-) diff --git a/LiarGame/Sources/Model/Music.swift b/LiarGame/Sources/Model/Music.swift index 0fb6008..26ddd69 100644 --- a/LiarGame/Sources/Model/Music.swift +++ b/LiarGame/Sources/Model/Music.swift @@ -7,12 +7,12 @@ import Foundation -struct Music: Codable { +struct Music: Codable, Equatable { let title: String let artist: String /// youtube video ID let id: String - let startedAt: String + let startedAt: Float /** Music 생성자 @@ -27,12 +27,13 @@ struct Music: Codable { 길이가 맞지 않을 경우 `return nil` */ init?(from array: [String]) { - guard array.count == 4 else { return nil } + guard array.count == 4, + let startedAt = Float(array[3]) else { return nil } self.title = array[0] self.artist = array[1] self.id = array[2] - self.startedAt = array[3] + self.startedAt = startedAt } } diff --git a/LiarGame/Sources/Reactor/RandomMusicQuizReactor.swift b/LiarGame/Sources/Reactor/RandomMusicQuizReactor.swift index b362df9..2965f03 100644 --- a/LiarGame/Sources/Reactor/RandomMusicQuizReactor.swift +++ b/LiarGame/Sources/Reactor/RandomMusicQuizReactor.swift @@ -13,6 +13,7 @@ final class RandomMusicQuizReactor: Reactor { self.repository = repository } + var scheduler = SerialDispatchQueueScheduler(internalSerialQueueName: "random.music.quiz") var initialState = State() private let repository: RandomMusicRepository @@ -25,19 +26,20 @@ final class RandomMusicQuizReactor: Reactor { enum Action { case updateMusicList case playMusic(second: PlaySeconds) - case didPlayButtonTapped - case didStopButtonTapped + case didPlayToggleButtonTapped case didAnswerButtonTapped case shuffle + case playerReady + case needCurrentVersion } enum Mutation { case updatePlayingState(Bool) case updateCurrentVersion(String) - case updateCurrentMusic(Music) + case updateCurrentMusic(Music?) case updateAnswer((String, String)?) case updateLoading(Bool) - case updateMusicList([Music]) + case ignore } struct State { @@ -46,34 +48,50 @@ final class RandomMusicQuizReactor: Reactor { var currentVersion: String = "" var answer: (title: String, artist: String)? var currentMusic: Music? - // music List 가 관리되어야 할 상태에 포함되는지 고려해야 함. - var musicList: [Music] = [] } func mutate(action: Action) -> Observable { switch action { case .updateMusicList: - return repository.getNewestVersion() - .asObservable() - .map { Mutation.updateMusicList($0) } + return .concat([ + .just(.updateLoading(true)), + .just(.updatePlayingState(false)), + .just(.updateAnswer(nil)), + repository.getNewestVersion() + .asObservable() + .map { _ in Mutation.ignore }, + .just(.updateCurrentMusic(shuffleMusic())), + .just(.updateCurrentVersion(repository.currentVersion)) + ]) + .timeout(.seconds(10), other: Observable.just(Mutation.updateLoading(false)), scheduler: scheduler) case let .playMusic(second): return .concat([ .just(.updatePlayingState(true)), .just(.updatePlayingState(false)) - .timeout(.seconds(second.rawValue), scheduler: ConcurrentDispatchQueueScheduler(qos: .default)) + .delay(.seconds(second.rawValue), scheduler: ConcurrentDispatchQueueScheduler(qos: .default)) ]) - case .didPlayButtonTapped: - return .just(.updatePlayingState(true)) - case .didStopButtonTapped: - return .just(.updatePlayingState(false)) + case .didPlayToggleButtonTapped: + return .just(.updatePlayingState(!currentState.isPlaying)) case .didAnswerButtonTapped: return .just(.updateAnswer(currentAnswer())) case .shuffle: - return .just(.updateCurrentMusic(shuffleMusic())) + return .concat( + .just(.updateLoading(true)), + .just(.updatePlayingState(false)), + .just(.updateAnswer(nil)), + .just(.updateCurrentMusic(shuffleMusic())) + ) + .timeout(.seconds(10), other: Observable.just(Mutation.updateLoading(false)), scheduler: scheduler) + + case .playerReady: + return .just(.updateLoading(false)) + + case .needCurrentVersion: + return .just(.updateCurrentVersion(repository.currentVersion)) } } @@ -90,8 +108,7 @@ final class RandomMusicQuizReactor: Reactor { state.answer = info case let .updateLoading(boolean): state.isLoading = boolean - case let .updateMusicList(list): - state.musicList = list + case .ignore: break } return state } @@ -104,10 +121,11 @@ final class RandomMusicQuizReactor: Reactor { } } - private func shuffleMusic() -> Music { - let size = currentState.musicList.count + private func shuffleMusic() -> Music? { + guard repository.musicList.count > 0 else { return nil } + let size = repository.musicList.count let randomNumber: Int = Int(arc4random()) % size - return currentState.musicList[randomNumber] + return repository.musicList[randomNumber] } } diff --git a/LiarGame/Sources/Repository/RandomMusicRepository.swift b/LiarGame/Sources/Repository/RandomMusicRepository.swift index d5595e1..b11cf1d 100644 --- a/LiarGame/Sources/Repository/RandomMusicRepository.swift +++ b/LiarGame/Sources/Repository/RandomMusicRepository.swift @@ -33,7 +33,7 @@ final class RandomMusicRepository: RandomMusicRepositoryType { Self._currentVersion } - @UserDefault(key: "RandomMusicVersion", defaultValue: "") + @UserDefault(key: "RandomMusicVersion", defaultValue: "unknwon") private static var _currentVersion: String /** 저장되어있는 음악 목록을 가져옵니다. @@ -61,7 +61,7 @@ final class RandomMusicRepository: RandomMusicRepositoryType { func getNewestVersion() -> Single<[Music]> { _newsetVersion .flatMap { _version -> Single<[Music]> in - let arr = _version.components(separatedBy: ",") + let arr = _version.trimmingCharacters(in: .whitespacesAndNewlines).components(separatedBy: ",") let version = arr[0] let length = arr[1] guard version != self.currentVersion else { return .just(try self.readMuicList()) } @@ -141,8 +141,7 @@ final class RandomMusicRepository: RandomMusicRepositoryType { } /// 음악 데이터 읽어오기 - // TODO: - private 으로 변경/// - func readMuicList() throws -> [Music] { + private func readMuicList() throws -> [Music] { let path = try musicDataPath() let data = try Data(contentsOf: path) @@ -150,8 +149,7 @@ final class RandomMusicRepository: RandomMusicRepositoryType { } /// 음악 데이터를 저장하는 경로 - // TODO: - private 으로 변경/// - func musicDataPath() throws -> URL { + private func musicDataPath() throws -> URL { guard let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { throw RandomMusicRepositoryError.fileAccessFailed } diff --git a/LiarGame/Sources/ViewController/RandomMusicQuiz/RandomMusicQuizView.swift b/LiarGame/Sources/ViewController/RandomMusicQuiz/RandomMusicQuizView.swift index 4e7323f..e292872 100644 --- a/LiarGame/Sources/ViewController/RandomMusicQuiz/RandomMusicQuizView.swift +++ b/LiarGame/Sources/ViewController/RandomMusicQuiz/RandomMusicQuizView.swift @@ -22,30 +22,30 @@ final class RandomQuizView: UIView { fileprivate let container = UIView() private let _tintColor = UIColor(hexString: "1D5C63") - private lazy var threeSecondButton = makeRoundedButton(tintColor: _tintColor, str: "3초") - private lazy var fiveSecondButton = makeRoundedButton(tintColor: _tintColor, str: "5초") - private lazy var tenSecondButton = makeRoundedButton(tintColor: _tintColor, str: "10초") - private lazy var playButton = makeRoundedButton(tintColor: _tintColor, str: "재생") + lazy var threeSecondButton = makeRoundedButton(tintColor: _tintColor, str: "3초") + lazy var fiveSecondButton = makeRoundedButton(tintColor: _tintColor, str: "5초") + lazy var tenSecondButton = makeRoundedButton(tintColor: _tintColor, str: "10초") + lazy var playButton = makeRoundedButton(tintColor: _tintColor, str: "재생") private lazy var currentVersionLabel = UILabel().then { $0.textColor = .label } - private lazy var updateButton = makeRoundedButton(tintColor: _tintColor).then { + lazy var updateButton = makeRoundedButton(tintColor: _tintColor).then { $0.setImage(UIImage(systemName: "arrow.clockwise"), for: .normal) } - private lazy var shuffleButton = makeRoundedButton(tintColor: _tintColor).then { + lazy var shuffleButton = makeRoundedButton(tintColor: _tintColor).then { $0.setImage(UIImage(systemName: "shuffle"), for: .normal) } - - private lazy var showAnswerButton = makeRoundedButton(tintColor: _tintColor, str: "정답 보기") + + lazy var showAnswerButton = makeRoundedButton(tintColor: _tintColor, str: "정답 보기") private lazy var answerLabel = DashedLineBorderdLabel(borderColor: _tintColor).then { $0.font = .preferredFont(forTextStyle: .title1) $0.isHidden = true } - private let ytPlayer = YTPlayerView(frame: .zero) + let ytPlayer = YTPlayerView(frame: .zero) override func layoutSubviews() { super.layoutSubviews() @@ -54,9 +54,58 @@ final class RandomQuizView: UIView { container.flex.layout() } + func setAnswerLabel(_ value: (title: String, artist: String)?) { + if let value = value { + answerLabel.text = "\(value.title) - \(value.artist)" + answerLabel.isHidden = false + answerLabel.flex.markDirty() + container.flex.layout() + } else { + answerLabel.isHidden = true + } + } + + func setVersionLabel(_ value: String) { + currentVersionLabel.text = value + currentVersionLabel.flex.markDirty() + container.flex.layout() + } + + func changePlayButtonState(isPlaying: Bool) { + [threeSecondButton, fiveSecondButton, tenSecondButton] + .forEach { $0.isEnabled = !isPlaying } + playButton.setTitle(isPlaying ? "정지" : "시작", for: .normal) + if isPlaying { ytPlayer.playVideo() } + else { ytPlayer.stopVideo() + } + } + + private var loadingView: UIView? + + func setLoading(_ value: Bool) { + if value { + let loadingBackground = UIView(frame: bounds).then { + $0.backgroundColor = .systemGray.withAlphaComponent(0.5) + } + let indicator = UIActivityIndicatorView(style: .large) + indicator.color = _tintColor + + addSubview(loadingBackground) + loadingBackground.addSubview(indicator) + indicator.center = center + self.loadingView = loadingBackground + indicator.startAnimating() + } else { + loadingView?.removeFromSuperview() + } + + } + private func setupViews() { backgroundColor = UIColor(hexString: "EDE6DB") addSubview(container) + container.addSubview(ytPlayer) + ytPlayer.isHidden = true container.flex .direction(.column).justifyContent(.center).marginHorizontal(20).define { @@ -89,24 +138,12 @@ final class RandomQuizView: UIView { .width(150).height(50) .alignSelf(.center) $0.addItem(answerLabel) - .minHeight(answerLabel.font.lineHeight) - .minWidth(75) + .padding(1) .alignSelf(.center) } .verticallySpacing(20) - } - - func setAnswerLabel(_ value: String?) { - if value != nil { - answerLabel.isHidden = false - answerLabel.text = value - } else { - answerLabel.isHidden = true - } - } - } fileprivate func makeRoundedButton(tintColor: UIColor, str: String? = nil) -> UIButton { diff --git a/LiarGame/Sources/ViewController/RandomMusicQuiz/RandomMusicQuizViewController.swift b/LiarGame/Sources/ViewController/RandomMusicQuiz/RandomMusicQuizViewController.swift index 2f442d5..159a819 100644 --- a/LiarGame/Sources/ViewController/RandomMusicQuiz/RandomMusicQuizViewController.swift +++ b/LiarGame/Sources/ViewController/RandomMusicQuiz/RandomMusicQuizViewController.swift @@ -10,16 +10,6 @@ import ReactorKit import RxSwift import Then -/* - TODO: - - 랜덤 음악 퀴즈 게임 - - - 3초 재생버튼, 5초 재생버튼, 10초 재생버튼을 추가. - - 정답을 보여줄 버튼 추가 - - - - */ final class RandomMusicQuizViewController: UIViewController, View { private let content = RandomQuizView() @@ -27,7 +17,6 @@ final class RandomMusicQuizViewController: UIViewController, View { init(reactor: RandomMusicQuizReactor) { super.init(nibName: nil, bundle: nil) self.reactor = reactor - setupViews() } @available(*, unavailable) @@ -40,11 +29,92 @@ final class RandomMusicQuizViewController: UIViewController, View { var disposeBag = DisposeBag() func bind(reactor: RandomMusicQuizReactor) { - + reactor.action.onNext(.needCurrentVersion) + reactor.action.onNext(.shuffle) + + bindAction(reactor: reactor) + bindState(reactor: reactor) } - func setupViews() { - + private func bindAction(reactor: RandomMusicQuizReactor) { + content.threeSecondButton.rx.tap + .map { _ in Reactor.Action.playMusic(second: .three) } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + content.fiveSecondButton.rx.tap + .map { _ in Reactor.Action.playMusic(second: .five) } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + content.tenSecondButton.rx.tap + .map { _ in Reactor.Action.playMusic(second: .ten) } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + content.playButton.rx.tap + .map { _ in Reactor.Action.didPlayToggleButtonTapped } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + content.updateButton.rx.tap + .map { _ in Reactor.Action.updateMusicList } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + content.shuffleButton.rx.tap + .map { _ in Reactor.Action.shuffle } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + content.showAnswerButton.rx.tap + .map { _ in Reactor.Action.didAnswerButtonTapped } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + content.ytPlayer.rx.isReady + .map { _ in Reactor.Action.playerReady } + .bind(to: reactor.action) + .disposed(by: disposeBag) + } + + private func bindState(reactor: RandomMusicQuizReactor) { + reactor.state.map(\.answer) + .distinctUntilChanged { $0?.title == $1?.title && $0?.artist == $1?.artist } + .observe(on: MainScheduler.instance) + .subscribe(onNext: content.setAnswerLabel) + .disposed(by: disposeBag) + + reactor.state.map(\.currentVersion) + .distinctUntilChanged() + .observe(on: MainScheduler.instance) + .subscribe(onNext: content.setVersionLabel) + .disposed(by: disposeBag) + + reactor.state.map(\.currentMusic) + .distinctUntilChanged() + .compactMap { $0 } + .observe(on: MainScheduler.instance) + .subscribe(onNext: { [weak self] in + self?.content.ytPlayer.load(withVideoId: $0.id, playerVars: [ + "start": $0.startedAt + ]) + }) + .disposed(by: disposeBag) + + reactor.state.map(\.isLoading) + .distinctUntilChanged() + .subscribe(onNext: content.setLoading(_:)) + .disposed(by: disposeBag) + + reactor.state.map(\.isPlaying) + .distinctUntilChanged() + .observe(on: MainScheduler.instance) + .subscribe(onNext: content.changePlayButtonState(isPlaying:)) + .disposed(by: disposeBag) + + } + } From 3bb825d73ab7e28c3b1fd88661b37d43bebad33c Mon Sep 17 00:00:00 2001 From: elppaaa Date: Tue, 26 Apr 2022 08:46:45 +0900 Subject: [PATCH 04/10] feat: Add RandomMusic to home MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 홈에서 RandomMusicQuiz 로 이동 가능하도록 추가 - 게임 모드를 enum 으로 변경 --- LiarGame/Sources/Reactor/HomeReactor.swift | 11 +- .../ViewController/HomeViewController.swift | 105 +++++++++++------- 2 files changed, 71 insertions(+), 45 deletions(-) diff --git a/LiarGame/Sources/Reactor/HomeReactor.swift b/LiarGame/Sources/Reactor/HomeReactor.swift index 17ce538..080ee60 100644 --- a/LiarGame/Sources/Reactor/HomeReactor.swift +++ b/LiarGame/Sources/Reactor/HomeReactor.swift @@ -13,17 +13,22 @@ import RxCocoa final class HomeReactor: Reactor{ enum Action{ - case updateMode(String?) + case updateMode(GameMode) } enum Mutation{ - case setMode(String?) + case setMode(GameMode) } struct State{ - var mode: String? + var mode: GameMode? } let initialState = State() + enum GameMode { + case liarGame + case randomMusicQuiz + } + func mutate(action: Action) -> Observable { switch action { diff --git a/LiarGame/Sources/ViewController/HomeViewController.swift b/LiarGame/Sources/ViewController/HomeViewController.swift index 68d385e..273bb95 100644 --- a/LiarGame/Sources/ViewController/HomeViewController.swift +++ b/LiarGame/Sources/ViewController/HomeViewController.swift @@ -13,78 +13,99 @@ import RxCocoa import ReactorKit final class HomeViewController: UIViewController, View{ - - - typealias Reactor = HomeReactor + typealias Reactor = HomeReactor init(reactor: HomeReactor){ - super.init(nibName: nil, bundle: nil) self.reactor = reactor - self.view.addSubview(self.flexLayoutContainer) - self.flexLayoutContainer.flex.direction(.column).alignItems(.center).justifyContent(.center).padding(10).define{ flex in - flex.backgroundColor(.systemPink) - flex.addItem(self.liarGameStartButton).width(200).height(50).backgroundColor(.yellow) - } - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + setupView() + } + @available(*, unavailable) + required init?(coder: NSCoder) { fatalError() } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() - self.flexLayoutContainer.pin.all() + self.flexLayoutContainer.pin.all(view.pin.safeArea) self.flexLayoutContainer.flex.layout() } - + override func viewDidLoad() { super.viewDidLoad() - self.view.backgroundColor = .white - self.setupView() + self.view.backgroundColor = .systemPink } - let flexLayoutContainer: UIView = UIView() + private let flexLayoutContainer: UIView = UIView() var disposeBag: DisposeBag = DisposeBag() - let liarGameStartButton = UIButton() + private lazy var gameList = [liarGameStartButton, randomMusicQuiz] + private let liarGameStartButton = makeGameButton(str: "라이어 게임") + private let randomMusicQuiz = makeGameButton(str: "랜덤 음악 맞추기") } // MARK: - Setup View -extension HomeViewController{ - private func setupView(){ - liarGameStartButton.do{ - $0.setTitle("라이어 게임", for: .normal) - $0.setTitleColor(.black, for: .normal) - self.view.addSubview($0) +extension HomeViewController { + private func setupView() { + self.view.addSubview(self.flexLayoutContainer) + self.flexLayoutContainer.flex.direction(.column).alignItems(.center).justifyContent(.center).padding(10).define { flex in + gameList.forEach { + flex.addItem($0) + .width(200) + .height(50) + .backgroundColor(.yellow) + } } + .verticallySpacing(15) } } - // MARK: - Binding -extension HomeViewController{ +// MARK: - Binding +extension HomeViewController { func bind(reactor: Reactor) { - self.liarGameStartButton.rx.tap.asDriver() - .drive(onNext: { - reactor.action.onNext(.updateMode("LIAR")) - }).disposed(by: disposeBag) + liarGameStartButton.rx.tap + .subscribe(onNext: { + reactor.action.onNext(.updateMode(.liarGame)) + }) + .disposed(by: disposeBag) - reactor.state.map { $0.mode } - .subscribe(onNext: { - print($0) - if $0 == "LIAR"{ - let liarVC = LiarGameModeViewController(reactor: LiarGameModeReactor()) - liarVC.modalPresentationStyle = .fullScreen - self.present(liarVC, animated: true, completion: nil) - } - }) - .disposed(by: disposeBag) + randomMusicQuiz.rx.tap + .map { _ in Reactor.Action.updateMode(.randomMusicQuiz) } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + // TODO: - 해당 부분에서 `fullScreen` 으로 `present` 가 이루어지므로 메뉴로 돌아갈 기능 필요 + reactor.state.map(\.mode) + .compactMap { $0 } + .withUnretained(self) + .subscribe(onNext: { `self`, mode in + switch mode { + case .liarGame: + let liarVC = LiarGameModeViewController(reactor: LiarGameModeReactor()) + liarVC.modalPresentationStyle = .fullScreen + self.present(liarVC, animated: true) + case .randomMusicQuiz: + let reactor = RandomMusicQuizReactor(repository: RandomMusicRepository()) + let vc = RandomMusicQuizViewController(reactor: reactor) + vc.modalPresentationStyle = .fullScreen + self.present(vc, animated: true) + } + }) + .disposed(by: disposeBag) } - + } +fileprivate func makeGameButton(str: String) -> UIButton { + let button = UIButton() + + button.setTitle(str, for: .normal) + button.setTitleColor(.black, for: .normal) + button.setTitleColor(.systemGray, for: .normal) + + return button +} From d08979f5387a4a43c6d8453fb5294934b95e4116 Mon Sep 17 00:00:00 2001 From: elppaaa Date: Tue, 26 Apr 2022 08:51:03 +0900 Subject: [PATCH 05/10] style: Code reformatting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 들여쓰기 4개로 수정 --- LiarGame/Application/AppDelegate.swift | 16 +- LiarGame/Application/SceneDelegate.swift | 20 +- .../Sources/Model/LiarGameModeModel.swift | 3 +- LiarGame/Sources/Model/Music.swift | 55 ++-- LiarGame/Sources/Model/MusicSheetDTO.swift | 5 +- LiarGame/Sources/Reactor/HomeReactor.swift | 23 +- .../Sources/Reactor/LiarGameModeReactor.swift | 23 +- .../Reactor/RandomMusicQuizReactor.swift | 232 +++++++-------- .../Repository/RandomMusicRepository.swift | 278 +++++++++-------- LiarGame/Sources/Utils/Flex+Extensions.swift | 40 +-- .../Sources/Utils/UIColor+Extensions.swift | 18 +- .../Utils/Userdefaults+PropertyWrapper.swift | 2 +- .../Utils/YTPlayerViewDelegateProxy.swift | 126 ++++---- .../ViewController/HomeViewController.swift | 59 ++-- .../LiarGame/LiarGameModeViewController.swift | 98 +++--- .../LiarGameSubjectViewController.swift | 6 +- .../DashedLineBorderdLabel.swift | 78 +++-- .../RandomMusicQuiz/RandomMusicQuizView.swift | 280 +++++++++--------- .../RandomMusicQuizViewController.swift | 209 +++++++------ .../ViewController/SplashViewController.swift | 15 +- Project.yml | 4 + 21 files changed, 775 insertions(+), 815 deletions(-) diff --git a/LiarGame/Application/AppDelegate.swift b/LiarGame/Application/AppDelegate.swift index 3a96f8b..2765f6e 100644 --- a/LiarGame/Application/AppDelegate.swift +++ b/LiarGame/Application/AppDelegate.swift @@ -9,28 +9,22 @@ import UIKit @main class AppDelegate: UIResponder, UIApplicationDelegate { - - - - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. - return true + true } // MARK: UISceneSession Lifecycle - func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + func application(_: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options _: UIScene.ConnectionOptions) -> UISceneConfiguration { // Called when a new scene session is being created. // Use this method to select a configuration to create the new scene with. - return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) } - func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { + func application(_: UIApplication, didDiscardSceneSessions _: Set) { // Called when the user discards a scene session. // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. // Use this method to release any resources that were specific to the discarded scenes, as they will not return. } - - } - diff --git a/LiarGame/Application/SceneDelegate.swift b/LiarGame/Application/SceneDelegate.swift index d55db5d..db5387f 100644 --- a/LiarGame/Application/SceneDelegate.swift +++ b/LiarGame/Application/SceneDelegate.swift @@ -8,51 +8,45 @@ import UIKit class SceneDelegate: UIResponder, UIWindowSceneDelegate { - var window: UIWindow? - - func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + func scene(_ scene: UIScene, willConnectTo _: UISceneSession, options _: UIScene.ConnectionOptions) { // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). guard let windowScene = (scene as? UIWindowScene) else { return } - + window = UIWindow(windowScene: windowScene) // SceneDelegate의 프로퍼티에 설정해줌 let mainViewController = SplashViewController() // 맨 처음 보여줄 ViewController window?.rootViewController = mainViewController window?.makeKeyAndVisible() - } - func sceneDidDisconnect(_ scene: UIScene) { + func sceneDidDisconnect(_: UIScene) { // Called as the scene is being released by the system. // This occurs shortly after the scene enters the background, or when its session is discarded. // Release any resources associated with this scene that can be re-created the next time the scene connects. // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). } - func sceneDidBecomeActive(_ scene: UIScene) { + func sceneDidBecomeActive(_: UIScene) { // Called when the scene has moved from an inactive state to an active state. // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. } - func sceneWillResignActive(_ scene: UIScene) { + func sceneWillResignActive(_: UIScene) { // Called when the scene will move from an active state to an inactive state. // This may occur due to temporary interruptions (ex. an incoming phone call). } - func sceneWillEnterForeground(_ scene: UIScene) { + func sceneWillEnterForeground(_: UIScene) { // Called as the scene transitions from the background to the foreground. // Use this method to undo the changes made on entering the background. } - func sceneDidEnterBackground(_ scene: UIScene) { + func sceneDidEnterBackground(_: UIScene) { // Called as the scene transitions from the foreground to the background. // Use this method to save data, release shared resources, and store enough scene-specific state information // to restore the scene back to its current state. } - - } - diff --git a/LiarGame/Sources/Model/LiarGameModeModel.swift b/LiarGame/Sources/Model/LiarGameModeModel.swift index 4b6f818..f1d4f9d 100644 --- a/LiarGame/Sources/Model/LiarGameModeModel.swift +++ b/LiarGame/Sources/Model/LiarGameModeModel.swift @@ -7,8 +7,7 @@ import Foundation - -enum LiarGameMode{ +enum LiarGameMode { case normal case stupid } diff --git a/LiarGame/Sources/Model/Music.swift b/LiarGame/Sources/Model/Music.swift index 26ddd69..93e83be 100644 --- a/LiarGame/Sources/Model/Music.swift +++ b/LiarGame/Sources/Model/Music.swift @@ -8,32 +8,31 @@ import Foundation struct Music: Codable, Equatable { - let title: String - let artist: String - /// youtube video ID - let id: String - let startedAt: Float - - /** Music 생성자 - - 들어오는 배열의 구조: - [title, artist,id, startedAt] - - - title: 노래 제목 - - artist: 가수 - - id: 유튜브 동영상 id - - startedAt: 첫 재생 시각 - - 길이가 맞지 않을 경우 `return nil` - */ - init?(from array: [String]) { - guard array.count == 4, - let startedAt = Float(array[3]) else { return nil } - - self.title = array[0] - self.artist = array[1] - self.id = array[2] - self.startedAt = startedAt - } -} + let title: String + let artist: String + /// youtube video ID + let id: String + let startedAt: Float + + /** Music 생성자 + + 들어오는 배열의 구조: + [title, artist,id, startedAt] + - title: 노래 제목 + - artist: 가수 + - id: 유튜브 동영상 id + - startedAt: 첫 재생 시각 + + 길이가 맞지 않을 경우 `return nil` + */ + init?(from array: [String]) { + guard array.count == 4, + let startedAt = Float(array[3]) else { return nil } + + title = array[0] + artist = array[1] + id = array[2] + self.startedAt = startedAt + } +} diff --git a/LiarGame/Sources/Model/MusicSheetDTO.swift b/LiarGame/Sources/Model/MusicSheetDTO.swift index d7f4995..bc50edc 100644 --- a/LiarGame/Sources/Model/MusicSheetDTO.swift +++ b/LiarGame/Sources/Model/MusicSheetDTO.swift @@ -8,11 +8,8 @@ import Foundation // MARK: - MusicSheetDTO + struct MusicSheetDTO: Decodable { let range, majorDimension: String let values: [[String]] } - - - - diff --git a/LiarGame/Sources/Reactor/HomeReactor.swift b/LiarGame/Sources/Reactor/HomeReactor.swift index 080ee60..73c5ee9 100644 --- a/LiarGame/Sources/Reactor/HomeReactor.swift +++ b/LiarGame/Sources/Reactor/HomeReactor.swift @@ -7,43 +7,42 @@ import Foundation import ReactorKit -import RxSwift import RxCocoa +import RxSwift - -final class HomeReactor: Reactor{ - enum Action{ +final class HomeReactor: Reactor { + enum Action { case updateMode(GameMode) } - enum Mutation{ + + enum Mutation { case setMode(GameMode) } - struct State{ + + struct State { var mode: GameMode? } - + let initialState = State() - + enum GameMode { case liarGame case randomMusicQuiz } - - + func mutate(action: Action) -> Observable { switch action { case let .updateMode(mode): return Observable.just(Mutation.setMode(mode)) } } - + func reduce(state: State, mutation: Mutation) -> State { switch mutation { case let .setMode(mode): var newState = state newState.mode = mode return newState - } } } diff --git a/LiarGame/Sources/Reactor/LiarGameModeReactor.swift b/LiarGame/Sources/Reactor/LiarGameModeReactor.swift index e2b8b54..e16d55a 100644 --- a/LiarGame/Sources/Reactor/LiarGameModeReactor.swift +++ b/LiarGame/Sources/Reactor/LiarGameModeReactor.swift @@ -7,31 +7,31 @@ import Foundation import ReactorKit -import RxSwift import RxCocoa +import RxSwift -final class LiarGameModeReactor: Reactor{ - enum Action{ +final class LiarGameModeReactor: Reactor { + enum Action { case selectMode(LiarGameMode?) } - - enum Mutation{ + + enum Mutation { case setMode(LiarGameMode?) } - - struct State{ + + struct State { var mode: LiarGameMode? } - let initialState: State = State() - - + + let initialState: State = .init() + func mutate(action: Action) -> Observable { switch action { case let .selectMode(mode): return Observable.just(Mutation.setMode(mode)) } } - + func reduce(state: State, mutation: Mutation) -> State { switch mutation { case let .setMode(mode): @@ -40,5 +40,4 @@ final class LiarGameModeReactor: Reactor{ return newState } } - } diff --git a/LiarGame/Sources/Reactor/RandomMusicQuizReactor.swift b/LiarGame/Sources/Reactor/RandomMusicQuizReactor.swift index 2965f03..469be67 100644 --- a/LiarGame/Sources/Reactor/RandomMusicQuizReactor.swift +++ b/LiarGame/Sources/Reactor/RandomMusicQuizReactor.swift @@ -9,123 +9,123 @@ import Foundation import ReactorKit final class RandomMusicQuizReactor: Reactor { - init(repository: RandomMusicRepository) { - self.repository = repository - } - - var scheduler = SerialDispatchQueueScheduler(internalSerialQueueName: "random.music.quiz") - var initialState = State() - private let repository: RandomMusicRepository - - enum PlaySeconds: Int { - case three = 3 - case five = 5 - case ten = 10 - } - - enum Action { - case updateMusicList - case playMusic(second: PlaySeconds) - case didPlayToggleButtonTapped - case didAnswerButtonTapped - case shuffle - case playerReady - case needCurrentVersion - } - - enum Mutation { - case updatePlayingState(Bool) - case updateCurrentVersion(String) - case updateCurrentMusic(Music?) - case updateAnswer((String, String)?) - case updateLoading(Bool) - case ignore - } - - struct State { - var isPlaying: Bool = false - var isLoading: Bool = false - var currentVersion: String = "" - var answer: (title: String, artist: String)? - var currentMusic: Music? - } - - func mutate(action: Action) -> Observable { - switch action { - case .updateMusicList: - return .concat([ - .just(.updateLoading(true)), - .just(.updatePlayingState(false)), - .just(.updateAnswer(nil)), - repository.getNewestVersion() - .asObservable() - .map { _ in Mutation.ignore }, - .just(.updateCurrentMusic(shuffleMusic())), - .just(.updateCurrentVersion(repository.currentVersion)) - ]) - .timeout(.seconds(10), other: Observable.just(Mutation.updateLoading(false)), scheduler: scheduler) - - case let .playMusic(second): - return .concat([ - .just(.updatePlayingState(true)), - .just(.updatePlayingState(false)) - .delay(.seconds(second.rawValue), scheduler: ConcurrentDispatchQueueScheduler(qos: .default)) - ]) - - case .didPlayToggleButtonTapped: - return .just(.updatePlayingState(!currentState.isPlaying)) - - case .didAnswerButtonTapped: - return .just(.updateAnswer(currentAnswer())) - - case .shuffle: - return .concat( - .just(.updateLoading(true)), - .just(.updatePlayingState(false)), - .just(.updateAnswer(nil)), - .just(.updateCurrentMusic(shuffleMusic())) - ) - .timeout(.seconds(10), other: Observable.just(Mutation.updateLoading(false)), scheduler: scheduler) - - case .playerReady: - return .just(.updateLoading(false)) - - case .needCurrentVersion: - return .just(.updateCurrentVersion(repository.currentVersion)) + init(repository: RandomMusicRepository) { + self.repository = repository } - } - - func reduce(state: State, mutation: Mutation) -> State { - var state = state - switch mutation { - case let .updatePlayingState(boolean): - state.isPlaying = boolean - case let .updateCurrentVersion(version): - state.currentVersion = version - case let .updateCurrentMusic(music): - state.currentMusic = music - case let .updateAnswer(info): - state.answer = info - case let .updateLoading(boolean): - state.isLoading = boolean - case .ignore: break + + var scheduler = SerialDispatchQueueScheduler(internalSerialQueueName: "random.music.quiz") + var initialState = State() + private let repository: RandomMusicRepository + + enum PlaySeconds: Int { + case three = 3 + case five = 5 + case ten = 10 + } + + enum Action { + case updateMusicList + case playMusic(second: PlaySeconds) + case didPlayToggleButtonTapped + case didAnswerButtonTapped + case shuffle + case playerReady + case needCurrentVersion + } + + enum Mutation { + case updatePlayingState(Bool) + case updateCurrentVersion(String) + case updateCurrentMusic(Music?) + case updateAnswer((String, String)?) + case updateLoading(Bool) + case ignore + } + + struct State { + var isPlaying: Bool = false + var isLoading: Bool = false + var currentVersion: String = "" + var answer: (title: String, artist: String)? + var currentMusic: Music? + } + + func mutate(action: Action) -> Observable { + switch action { + case .updateMusicList: + return .concat([ + .just(.updateLoading(true)), + .just(.updatePlayingState(false)), + .just(.updateAnswer(nil)), + repository.getNewestVersion() + .asObservable() + .map { _ in Mutation.ignore }, + .just(.updateCurrentMusic(shuffleMusic())), + .just(.updateCurrentVersion(repository.currentVersion)), + ]) + .timeout(.seconds(10), other: Observable.just(Mutation.updateLoading(false)), scheduler: scheduler) + + case let .playMusic(second): + return .concat([ + .just(.updatePlayingState(true)), + .just(.updatePlayingState(false)) + .delay(.seconds(second.rawValue), scheduler: ConcurrentDispatchQueueScheduler(qos: .default)), + ]) + + case .didPlayToggleButtonTapped: + return .just(.updatePlayingState(!currentState.isPlaying)) + + case .didAnswerButtonTapped: + return .just(.updateAnswer(currentAnswer())) + + case .shuffle: + return .concat( + .just(.updateLoading(true)), + .just(.updatePlayingState(false)), + .just(.updateAnswer(nil)), + .just(.updateCurrentMusic(shuffleMusic())) + ) + .timeout(.seconds(10), other: Observable.just(Mutation.updateLoading(false)), scheduler: scheduler) + + case .playerReady: + return .just(.updateLoading(false)) + + case .needCurrentVersion: + return .just(.updateCurrentVersion(repository.currentVersion)) + } } - return state - } - - private func currentAnswer() -> (title: String, artist: String)? { - if let currentMusic = currentState.currentMusic { - return (title: currentMusic.title, artist: currentMusic.artist) - } else { - return nil + + func reduce(state: State, mutation: Mutation) -> State { + var state = state + switch mutation { + case let .updatePlayingState(boolean): + state.isPlaying = boolean + case let .updateCurrentVersion(version): + state.currentVersion = version + case let .updateCurrentMusic(music): + state.currentMusic = music + case let .updateAnswer(info): + state.answer = info + case let .updateLoading(boolean): + state.isLoading = boolean + case .ignore: break + } + return state + } + + private func currentAnswer() -> (title: String, artist: String)? { + if let currentMusic = currentState.currentMusic { + return (title: currentMusic.title, artist: currentMusic.artist) + } else { + return nil + } + } + + private func shuffleMusic() -> Music? { + guard repository.musicList.count > 0 else { return nil } + let size = repository.musicList.count + let randomNumber = Int(arc4random()) % size + + return repository.musicList[randomNumber] } - } - - private func shuffleMusic() -> Music? { - guard repository.musicList.count > 0 else { return nil } - let size = repository.musicList.count - let randomNumber: Int = Int(arc4random()) % size - - return repository.musicList[randomNumber] - } } diff --git a/LiarGame/Sources/Repository/RandomMusicRepository.swift b/LiarGame/Sources/Repository/RandomMusicRepository.swift index b11cf1d..bb00a79 100644 --- a/LiarGame/Sources/Repository/RandomMusicRepository.swift +++ b/LiarGame/Sources/Repository/RandomMusicRepository.swift @@ -9,159 +9,157 @@ import Foundation import RxSwift protocol RandomMusicRepositoryType { - /// 최신 버전을 확인합니다. - var newestVersion: Single { get } - /// 현재 저장되어 있는 버전을 확인합니다. - var currentVersion: String { get } - /// 버전 업데이트를 수행합니다. - func getNewestVersion() -> Single<[Music]> - /// 저장되어있는 음악 리스트를 가져옵니다. - var musicList: [Music] { get } + /// 최신 버전을 확인합니다. + var newestVersion: Single { get } + /// 현재 저장되어 있는 버전을 확인합니다. + var currentVersion: String { get } + /// 버전 업데이트를 수행합니다. + func getNewestVersion() -> Single<[Music]> + /// 저장되어있는 음악 리스트를 가져옵니다. + var musicList: [Music] { get } } - final class RandomMusicRepository: RandomMusicRepositoryType { - - /// 리스트를 담고 있는 Google Sheet ID - private var sheetID: String = "1jiAcDhKOoMbfLmlCOba33CMAZCwlpaV3enkLVvjMmIA" - private var googleAPIKey: String { - (Bundle.main.object(forInfoDictionaryKey: "GOOGLE_APIKEY") as? String) ?? "" - } - - /// 현재 저장되어 있는 버전 - var currentVersion: String { - Self._currentVersion - } - - @UserDefault(key: "RandomMusicVersion", defaultValue: "unknwon") - private static var _currentVersion: String - - /** 저장되어있는 음악 목록을 가져옵니다. - - 최신 버전을 보증하지 않음. - */ - var musicList: [Music] { - do { - return try readMuicList() - } catch { - return [] + /// 리스트를 담고 있는 Google Sheet ID + private var sheetID: String = "1jiAcDhKOoMbfLmlCOba33CMAZCwlpaV3enkLVvjMmIA" + private var googleAPIKey: String { + (Bundle.main.object(forInfoDictionaryKey: "GOOGLE_APIKEY") as? String) ?? "" } - } - - var newestVersion: Single { - _newsetVersion - .map { $0.components(separatedBy: ",")[0] } - } - - func setCurrent(version: String) { - Self._currentVersion = version - } - - /// 버전 확인 후 최신 버전을 가져옵니다. - func getNewestVersion() -> Single<[Music]> { - _newsetVersion - .flatMap { _version -> Single<[Music]> in - let arr = _version.trimmingCharacters(in: .whitespacesAndNewlines).components(separatedBy: ",") - let version = arr[0] - let length = arr[1] - guard version != self.currentVersion else { return .just(try self.readMuicList()) } - - return self.update(version: version, length: length) - } - } - - /// 최신 목록으로 업데이트를 수행합니다. - private func update(version: String, length: String) -> Single<[Music]> { - guard let url = URL(string: "https://sheets.googleapis.com/v4/spreadsheets/\(self.sheetID)/values/Sheet!A2:D\(length)?key=\(self.googleAPIKey)") else { - assertionFailure("URL String error") - return .error(RandomMusicRepositoryError.castingError) + + /// 현재 저장되어 있는 버전 + var currentVersion: String { + Self._currentVersion } - - return URLSession.shared.rx.response(request: URLRequest(url: url)) - .map { (response, data) -> Data in - guard 200..<300 ~= response.statusCode else { - throw RandomMusicRepositoryError.requestFailed - } - - return data - } - .asSingle() - // 타입 변환 - .map { data -> MusicSheetDTO in + + @UserDefault(key: "RandomMusicVersion", defaultValue: "unknwon") + private static var _currentVersion: String + + /** 저장되어있는 음악 목록을 가져옵니다. + + 최신 버전을 보증하지 않음. + */ + var musicList: [Music] { do { - return try JSONDecoder().decode(MusicSheetDTO.self, from: data) + return try readMuicList() } catch { - throw RandomMusicRepositoryError.parseError + return [] + } + } + + var newestVersion: Single { + _newsetVersion + .map { $0.components(separatedBy: ",")[0] } + } + + func setCurrent(version: String) { + Self._currentVersion = version + } + + /// 버전 확인 후 최신 버전을 가져옵니다. + func getNewestVersion() -> Single<[Music]> { + _newsetVersion + .flatMap { _version -> Single<[Music]> in + let arr = _version.trimmingCharacters(in: .whitespacesAndNewlines).components(separatedBy: ",") + let version = arr[0] + let length = arr[1] + guard version != self.currentVersion else { return .just(try self.readMuicList()) } + + return self.update(version: version, length: length) + } + } + + /// 최신 목록으로 업데이트를 수행합니다. + private func update(version: String, length: String) -> Single<[Music]> { + guard let url = URL(string: "https://sheets.googleapis.com/v4/spreadsheets/\(sheetID)/values/Sheet!A2:D\(length)?key=\(googleAPIKey)") else { + assertionFailure("URL String error") + return .error(RandomMusicRepositoryError.castingError) + } + + return URLSession.shared.rx.response(request: URLRequest(url: url)) + .map { response, data -> Data in + guard 200 ..< 300 ~= response.statusCode else { + throw RandomMusicRepositoryError.requestFailed + } + + return data + } + .asSingle() + // 타입 변환 + .map { data -> MusicSheetDTO in + do { + return try JSONDecoder().decode(MusicSheetDTO.self, from: data) + } catch { + throw RandomMusicRepositoryError.parseError + } + } + .map(\.values) + .map { $0.compactMap(Music.init) } + // 저장 + .do(onSuccess: { musicList in + self.setCurrent(version: version) + try self.writeMusicList(from: musicList) + }) + } + + /** 최신 정보를 받아옵니다. + + return 값인 String 의 구조는 다음과 같습니다. + + [업데이트된버전],[시트의 마지막행] + + ex) 20220422,5 + */ + private var _newsetVersion: Single { + guard let url = URL(string: "https://dl.dropboxusercontent.com/s/g55fwxp70a16xl1/version.txt") else { + assertionFailure("URL String error") + return .error(RandomMusicRepositoryError.castingError) } - } - .map(\.values) - .map { $0.compactMap(Music.init) } - // 저장 - .do(onSuccess: { musicList in - self.setCurrent(version: version) - try self.writeMusicList(from: musicList) - }) - } - - /** 최신 정보를 받아옵니다. - - return 값인 String 의 구조는 다음과 같습니다. - - [업데이트된버전],[시트의 마지막행] - - ex) 20220422,5 - */ - private var _newsetVersion: Single { - guard let url = URL(string: "https://dl.dropboxusercontent.com/s/g55fwxp70a16xl1/version.txt") else { - assertionFailure("URL String error") - return .error(RandomMusicRepositoryError.castingError) + let request = URLRequest(url: url) + return URLSession.shared.rx.data(request: request) + .map { + guard let str = String(data: $0, encoding: .utf8) else { + throw RandomMusicRepositoryError.requestFailed + } + + return str + } + .asSingle() } - let request = URLRequest(url: url) - return URLSession.shared.rx.data(request: request) - .map { - guard let str = String(data: $0, encoding: .utf8) else { - throw RandomMusicRepositoryError.requestFailed + + /// 음악 데이터 저장 + private func writeMusicList(from musicList: [Music]) throws { + if musicList.isEmpty { fatalError() } + let path = try musicDataPath() + do { + let data = try JSONEncoder().encode(musicList) + try data.write(to: path, options: .atomic) + } catch { + throw RandomMusicRepositoryError.fileWriteFailed } - - return str - } - .asSingle() - } - - /// 음악 데이터 저장 - private func writeMusicList(from musicList: [Music]) throws { - if musicList.isEmpty { fatalError() } - let path = try musicDataPath() - do { - let data = try JSONEncoder().encode(musicList) - try data.write(to: path, options: .atomic) - } catch { - throw RandomMusicRepositoryError.fileWriteFailed } - } - - /// 음악 데이터 읽어오기 - private func readMuicList() throws -> [Music] { - let path = try musicDataPath() - let data = try Data(contentsOf: path) - - return try JSONDecoder().decode([Music].self, from: data) - } - - /// 음악 데이터를 저장하는 경로 - private func musicDataPath() throws -> URL { - guard let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { - throw RandomMusicRepositoryError.fileAccessFailed + + /// 음악 데이터 읽어오기 + private func readMuicList() throws -> [Music] { + let path = try musicDataPath() + let data = try Data(contentsOf: path) + + return try JSONDecoder().decode([Music].self, from: data) + } + + /// 음악 데이터를 저장하는 경로 + private func musicDataPath() throws -> URL { + guard let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { + throw RandomMusicRepositoryError.fileAccessFailed + } + + return documentsURL.appendingPathComponent("MusicListData.bin") } - - return documentsURL.appendingPathComponent("MusicListData.bin") - } } enum RandomMusicRepositoryError: Error { - case requestFailed - case castingError - case parseError - case fileAccessFailed - case fileWriteFailed + case requestFailed + case castingError + case parseError + case fileAccessFailed + case fileWriteFailed } diff --git a/LiarGame/Sources/Utils/Flex+Extensions.swift b/LiarGame/Sources/Utils/Flex+Extensions.swift index 092ac72..1464dc6 100644 --- a/LiarGame/Sources/Utils/Flex+Extensions.swift +++ b/LiarGame/Sources/Utils/Flex+Extensions.swift @@ -5,28 +5,28 @@ // Created by JK on 2022/04/26. // -import Foundation import CoreGraphics import FlexLayout +import Foundation extension Flex { - @discardableResult - func horizontallySpacing(_ value: CGFloat?) -> Flex { - guard let view = view, view.subviews.count > 1 else { return self } - for (idx, subview) in view.subviews.enumerated() { - if idx == 0 { continue } - subview.flex.marginLeft(value ?? 0) - } - return self - } - - @discardableResult - func verticallySpacing(_ value: CGFloat?) -> Flex { - guard let view = view, view.subviews.count > 1 else { return self } - for (idx, subview) in view.subviews.enumerated() { - if idx == 0 { continue } - subview.flex.marginTop(value ?? 0) - } - return self - } + @discardableResult + func horizontallySpacing(_ value: CGFloat?) -> Flex { + guard let view = view, view.subviews.count > 1 else { return self } + for (idx, subview) in view.subviews.enumerated() { + if idx == 0 { continue } + subview.flex.marginLeft(value ?? 0) + } + return self + } + + @discardableResult + func verticallySpacing(_ value: CGFloat?) -> Flex { + guard let view = view, view.subviews.count > 1 else { return self } + for (idx, subview) in view.subviews.enumerated() { + if idx == 0 { continue } + subview.flex.marginTop(value ?? 0) + } + return self + } } diff --git a/LiarGame/Sources/Utils/UIColor+Extensions.swift b/LiarGame/Sources/Utils/UIColor+Extensions.swift index 63a9fbf..7ae71d8 100644 --- a/LiarGame/Sources/Utils/UIColor+Extensions.swift +++ b/LiarGame/Sources/Utils/UIColor+Extensions.swift @@ -8,14 +8,14 @@ import UIKit extension UIColor { - convenience init(hexString : String) { - if let rgbValue = UInt(hexString, radix: 16) { - let red = CGFloat((rgbValue >> 16) & 0xff) / 255 - let green = CGFloat((rgbValue >> 8) & 0xff) / 255 - let blue = CGFloat((rgbValue ) & 0xff) / 255 - self.init(red: red, green: green, blue: blue, alpha: 1.0) - } else { - self.init(red: 0.0, green: 0.0, blue: 0.0, alpha: 1.0) + convenience init(hexString: String) { + if let rgbValue = UInt(hexString, radix: 16) { + let red = CGFloat((rgbValue >> 16) & 0xFF) / 255 + let green = CGFloat((rgbValue >> 8) & 0xFF) / 255 + let blue = CGFloat(rgbValue & 0xFF) / 255 + self.init(red: red, green: green, blue: blue, alpha: 1.0) + } else { + self.init(red: 0.0, green: 0.0, blue: 0.0, alpha: 1.0) + } } - } } diff --git a/LiarGame/Sources/Utils/Userdefaults+PropertyWrapper.swift b/LiarGame/Sources/Utils/Userdefaults+PropertyWrapper.swift index 3234682..d7606e1 100644 --- a/LiarGame/Sources/Utils/Userdefaults+PropertyWrapper.swift +++ b/LiarGame/Sources/Utils/Userdefaults+PropertyWrapper.swift @@ -15,7 +15,7 @@ struct UserDefault { var wrappedValue: Value { get { - return container.object(forKey: key) as? Value ?? defaultValue + container.object(forKey: key) as? Value ?? defaultValue } set { container.set(newValue, forKey: key) diff --git a/LiarGame/Sources/Utils/YTPlayerViewDelegateProxy.swift b/LiarGame/Sources/Utils/YTPlayerViewDelegateProxy.swift index 0600a71..bd886b0 100644 --- a/LiarGame/Sources/Utils/YTPlayerViewDelegateProxy.swift +++ b/LiarGame/Sources/Utils/YTPlayerViewDelegateProxy.swift @@ -6,78 +6,76 @@ // import Foundation -import RxSwift import RxCocoa +import RxSwift import YouTubeiOSPlayerHelper -final class RxYTPlayerDelegateProxy -: DelegateProxy, - DelegateProxyType, YTPlayerViewDelegate { - - public weak private(set) var ytPlayer: YTPlayerView? - - init(ytPlayer: YTPlayerView) { - self.ytPlayer = ytPlayer - super.init(parentObject: ytPlayer, delegateProxy: RxYTPlayerDelegateProxy.self) - } - - static func registerKnownImplementations() { - self.register { RxYTPlayerDelegateProxy(ytPlayer: $0) } - } - - static func currentDelegate(for object: YTPlayerView) -> YTPlayerViewDelegate? { - object.delegate - } - - static func setCurrentDelegate(_ delegate: YTPlayerViewDelegate?, to object: YTPlayerView) { - object.delegate = delegate - } - +final class RxYTPlayerDelegateProxy: + DelegateProxy, + DelegateProxyType, YTPlayerViewDelegate +{ + public private(set) weak var ytPlayer: YTPlayerView? + + init(ytPlayer: YTPlayerView) { + self.ytPlayer = ytPlayer + super.init(parentObject: ytPlayer, delegateProxy: RxYTPlayerDelegateProxy.self) + } + + static func registerKnownImplementations() { + register { RxYTPlayerDelegateProxy(ytPlayer: $0) } + } + + static func currentDelegate(for object: YTPlayerView) -> YTPlayerViewDelegate? { + object.delegate + } + + static func setCurrentDelegate(_ delegate: YTPlayerViewDelegate?, to object: YTPlayerView) { + object.delegate = delegate + } } extension Reactive where Base: YTPlayerView { - private var delegate: DelegateProxy { - RxYTPlayerDelegateProxy.proxy(for: base) - } - - var state: Observable { - delegate - .methodInvoked(#selector(YTPlayerViewDelegate.playerView(_:didStateChanged:))) - .map { try castOrThrow(Int.self, $0[1]) } - .map { guard let value = YTPlayerState(rawValue: $0) else { - throw RxCocoaError.castingError(object: $0, targetType: YTPlayerState.self) - } - return value - } - } - - var isReady: Observable { - delegate - .methodInvoked(#selector(YTPlayerViewDelegate.playerViewDidBecomeReady(_:))) - .map { _ in base } - } - - var quality: Observable { - delegate - .methodInvoked(#selector(YTPlayerViewDelegate.playerView(_:didQualityChanged:))) - .map { try castOrThrow(YTPlaybackQuality.self, $0[1]) } - } - - var playTime: Observable { - delegate - .methodInvoked(#selector(YTPlayerViewDelegate.playerView(_:didPlayTime:))) - .map { try castOrThrow(Float.self, $0[1]) } - } - - var error: Observable { - delegate - .methodInvoked(#selector(YTPlayerViewDelegate.playerView(_:receivedError:))) - .map { try castOrThrow(YTPlayerError.self, $0[1]) } - } - + private var delegate: DelegateProxy { + RxYTPlayerDelegateProxy.proxy(for: base) + } + + var state: Observable { + delegate + .methodInvoked(#selector(YTPlayerViewDelegate.playerView(_:didStateChanged:))) + .map { try castOrThrow(Int.self, $0[1]) } + .map { guard let value = YTPlayerState(rawValue: $0) else { + throw RxCocoaError.castingError(object: $0, targetType: YTPlayerState.self) + } + return value + } + } + + var isReady: Observable { + delegate + .methodInvoked(#selector(YTPlayerViewDelegate.playerViewDidBecomeReady(_:))) + .map { _ in base } + } + + var quality: Observable { + delegate + .methodInvoked(#selector(YTPlayerViewDelegate.playerView(_:didQualityChanged:))) + .map { try castOrThrow(YTPlaybackQuality.self, $0[1]) } + } + + var playTime: Observable { + delegate + .methodInvoked(#selector(YTPlayerViewDelegate.playerView(_:didPlayTime:))) + .map { try castOrThrow(Float.self, $0[1]) } + } + + var error: Observable { + delegate + .methodInvoked(#selector(YTPlayerViewDelegate.playerView(_:receivedError:))) + .map { try castOrThrow(YTPlayerError.self, $0[1]) } + } } -fileprivate func castOrThrow(_ resultType: T.Type, _ object: Any) throws -> T { +private func castOrThrow(_ resultType: T.Type, _ object: Any) throws -> T { guard let returnValue = object as? T else { throw RxCocoaError.castingError(object: object, targetType: resultType) } diff --git a/LiarGame/Sources/ViewController/HomeViewController.swift b/LiarGame/Sources/ViewController/HomeViewController.swift index 273bb95..90d3a2b 100644 --- a/LiarGame/Sources/ViewController/HomeViewController.swift +++ b/LiarGame/Sources/ViewController/HomeViewController.swift @@ -5,52 +5,51 @@ // Created by Jay on 2022/04/19. // -import UIKit -import PinLayout import FlexLayout -import RxSwift -import RxCocoa +import PinLayout import ReactorKit +import RxCocoa +import RxSwift +import UIKit -final class HomeViewController: UIViewController, View{ +final class HomeViewController: UIViewController, View { typealias Reactor = HomeReactor - - init(reactor: HomeReactor){ + + init(reactor: HomeReactor) { super.init(nibName: nil, bundle: nil) self.reactor = reactor setupView() - } - + @available(*, unavailable) - required init?(coder: NSCoder) { fatalError() } - + required init?(coder _: NSCoder) { fatalError() } + override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() - self.flexLayoutContainer.pin.all(view.pin.safeArea) - self.flexLayoutContainer.flex.layout() + flexLayoutContainer.pin.all(view.pin.safeArea) + flexLayoutContainer.flex.layout() } - + override func viewDidLoad() { super.viewDidLoad() - self.view.backgroundColor = .systemPink + view.backgroundColor = .systemPink } - - private let flexLayoutContainer: UIView = UIView() - - var disposeBag: DisposeBag = DisposeBag() - + + private let flexLayoutContainer: UIView = .init() + + var disposeBag: DisposeBag = .init() + private lazy var gameList = [liarGameStartButton, randomMusicQuiz] private let liarGameStartButton = makeGameButton(str: "라이어 게임") private let randomMusicQuiz = makeGameButton(str: "랜덤 음악 맞추기") - } // MARK: - Setup View + extension HomeViewController { private func setupView() { - self.view.addSubview(self.flexLayoutContainer) - self.flexLayoutContainer.flex.direction(.column).alignItems(.center).justifyContent(.center).padding(10).define { flex in + view.addSubview(flexLayoutContainer) + flexLayoutContainer.flex.direction(.column).alignItems(.center).justifyContent(.center).padding(10).define { flex in gameList.forEach { flex.addItem($0) .width(200) @@ -63,20 +62,20 @@ extension HomeViewController { } // MARK: - Binding + extension HomeViewController { func bind(reactor: Reactor) { - liarGameStartButton.rx.tap .subscribe(onNext: { reactor.action.onNext(.updateMode(.liarGame)) }) .disposed(by: disposeBag) - + randomMusicQuiz.rx.tap .map { _ in Reactor.Action.updateMode(.randomMusicQuiz) } .bind(to: reactor.action) .disposed(by: disposeBag) - + // TODO: - 해당 부분에서 `fullScreen` 으로 `present` 가 이루어지므로 메뉴로 돌아갈 기능 필요 reactor.state.map(\.mode) .compactMap { $0 } @@ -96,16 +95,14 @@ extension HomeViewController { }) .disposed(by: disposeBag) } - } -fileprivate func makeGameButton(str: String) -> UIButton { +private func makeGameButton(str: String) -> UIButton { let button = UIButton() - + button.setTitle(str, for: .normal) button.setTitleColor(.black, for: .normal) button.setTitleColor(.systemGray, for: .normal) - + return button } - diff --git a/LiarGame/Sources/ViewController/LiarGame/LiarGameModeViewController.swift b/LiarGame/Sources/ViewController/LiarGame/LiarGameModeViewController.swift index ead5eb1..5948f41 100644 --- a/LiarGame/Sources/ViewController/LiarGame/LiarGameModeViewController.swift +++ b/LiarGame/Sources/ViewController/LiarGame/LiarGameModeViewController.swift @@ -5,20 +5,19 @@ // Created by Jay on 2022/04/19. // -import UIKit -import PinLayout import FlexLayout -import RxSwift -import RxCocoa +import PinLayout import ReactorKit +import RxCocoa +import RxSwift +import UIKit -final class LiarGameModeViewController: UIViewController, View{ - - init(reactor: LiarGameModeReactor){ +final class LiarGameModeViewController: UIViewController, View { + init(reactor: LiarGameModeReactor) { super.init(nibName: nil, bundle: nil) self.reactor = reactor - self.view.addSubview(flexLayoutContainer) - self.flexLayoutContainer.flex.direction(.column).justifyContent(.center).alignItems(.center).padding(10).define{ flex in + view.addSubview(flexLayoutContainer) + flexLayoutContainer.flex.direction(.column).justifyContent(.center).alignItems(.center).padding(10).define { flex in flex.backgroundColor(.brown) flex.addItem(defaultLiarGame).width(200).height(50).backgroundColor(.yellow) flex.addItem(stupidLiarGame).width(200).height(50).backgroundColor(.yellow).marginTop(10) @@ -26,90 +25,87 @@ final class LiarGameModeViewController: UIViewController, View{ flex.addItem(memberCountStepper).backgroundColor(.red).marginTop(10) } } - required init?(coder: NSCoder) { + + @available(*, unavailable) + required init?(coder _: NSCoder) { fatalError("init(coder:) has not been implemented") } + override func viewDidLayoutSubviews() { - self.flexLayoutContainer.pin.all() - self.flexLayoutContainer.flex.layout() + flexLayoutContainer.pin.all() + flexLayoutContainer.flex.layout() } + override func viewDidLoad() { - self.view.backgroundColor = .green - self.setupView() - self.bindingStepper() + view.backgroundColor = .green + setupView() + bindingStepper() } - - - let flexLayoutContainer: UIView = UIView() - var disposeBag: DisposeBag = DisposeBag() - - let defaultLiarGame: UIButton = UIButton() - let stupidLiarGame: UIButton = UIButton() - let memberCountLabel: UILabel = UILabel() - let memberCountStepper: UIStepper = UIStepper().then{ + + let flexLayoutContainer: UIView = .init() + var disposeBag: DisposeBag = .init() + + let defaultLiarGame: UIButton = .init() + let stupidLiarGame: UIButton = .init() + let memberCountLabel: UILabel = .init() + let memberCountStepper: UIStepper = .init().then { $0.wraps = false $0.autorepeat = true $0.minimumValue = 3 $0.maximumValue = 20 } - - - } // MARK: - Setup View -extension LiarGameModeViewController{ - private func setupView(){ - defaultLiarGame.do{ + +extension LiarGameModeViewController { + private func setupView() { + defaultLiarGame.do { $0.setTitle("일반 모드", for: .normal) $0.setTitleColor(.black, for: .normal) self.view.addSubview($0) } - stupidLiarGame.do{ + stupidLiarGame.do { $0.setTitle("바보 모드", for: .normal) $0.setTitleColor(.black, for: .normal) self.view.addSubview($0) } - memberCountLabel.do{ + memberCountLabel.do { $0.textAlignment = .center self.view.addSubview($0) } - memberCountStepper.do{ + memberCountStepper.do { self.view.addSubview($0) } } } - // MARK: - Bind -extension LiarGameModeViewController{ + +extension LiarGameModeViewController { func bind(reactor: LiarGameModeReactor) { - defaultLiarGame.rx.tap .throttle(.milliseconds(300), scheduler: MainScheduler.instance) - .map{ Reactor.Action.selectMode(LiarGameMode.normal)} + .map { Reactor.Action.selectMode(LiarGameMode.normal) } .bind(to: reactor.action) .disposed(by: disposeBag) - + stupidLiarGame.rx.tap .throttle(.milliseconds(300), scheduler: MainScheduler.instance) - .map{ Reactor.Action.selectMode(LiarGameMode.stupid)} + .map { Reactor.Action.selectMode(LiarGameMode.stupid) } .bind(to: reactor.action) .disposed(by: disposeBag) - - reactor.state.map { $0.mode } - .withUnretained(self) - .subscribe(onNext: { `self`, mode in - let liarGameSubjectVC = LiarGameSubjectViewController() - liarGameSubjectVC.modalPresentationStyle = .fullScreen - self.present(liarGameSubjectVC, animated: true, completion: nil) - }).disposed(by: disposeBag) - - + + reactor.state.map(\.mode) + .withUnretained(self) + .subscribe(onNext: { `self`, _ in + let liarGameSubjectVC = LiarGameSubjectViewController() + liarGameSubjectVC.modalPresentationStyle = .fullScreen + self.present(liarGameSubjectVC, animated: true, completion: nil) + }).disposed(by: disposeBag) } - - func bindingStepper(){ + func bindingStepper() { memberCountStepper.rx.value .map { String(Int($0)) } .bind(to: memberCountLabel.rx.text) diff --git a/LiarGame/Sources/ViewController/LiarGame/LiarGameSubjectViewController.swift b/LiarGame/Sources/ViewController/LiarGame/LiarGameSubjectViewController.swift index 1402092..e27fa78 100644 --- a/LiarGame/Sources/ViewController/LiarGame/LiarGameSubjectViewController.swift +++ b/LiarGame/Sources/ViewController/LiarGame/LiarGameSubjectViewController.swift @@ -7,10 +7,8 @@ import UIKit -final class LiarGameSubjectViewController: UIViewController{ - - +final class LiarGameSubjectViewController: UIViewController { override func viewDidLoad() { - self.view.backgroundColor = .green + view.backgroundColor = .green } } diff --git a/LiarGame/Sources/ViewController/RandomMusicQuiz/DashedLineBorderdLabel.swift b/LiarGame/Sources/ViewController/RandomMusicQuiz/DashedLineBorderdLabel.swift index e81c503..0482c20 100644 --- a/LiarGame/Sources/ViewController/RandomMusicQuiz/DashedLineBorderdLabel.swift +++ b/LiarGame/Sources/ViewController/RandomMusicQuiz/DashedLineBorderdLabel.swift @@ -8,44 +8,42 @@ import UIKit class DashedLineBorderdLabel: UILabel { - @available(*, unavailable) - required init?(coder: NSCoder) { fatalError() } - init(cornerRadius: CGFloat = 8.0, borderWidth: CGFloat = 1.0, borderColor: UIColor) { - self.borderWidth = borderWidth - self.borderColor = borderColor - self.cornerRadius = cornerRadius - super.init(frame: .zero) - self.layer.cornerRadius = cornerRadius - } - - - private let borderWidth: CGFloat - private let borderColor: UIColor - private let cornerRadius: CGFloat - - var dashBorder: CAShapeLayer? - - override func layoutSubviews() { - super.layoutSubviews() - - dashBorder?.removeFromSuperlayer() - let dashBorder = CAShapeLayer() - dashBorder.lineWidth = borderWidth - dashBorder.strokeColor = borderColor.cgColor - dashBorder.lineDashPattern = [3, 2] - dashBorder.frame = bounds - dashBorder.fillColor = nil - let horizontalInset = 8.0 - let verticalInset = 4.0 - let bounds = CGRect( - x: bounds.origin.x - horizontalInset, - y: bounds.origin.y - verticalInset, - width: bounds.width + horizontalInset * 2, - height: bounds.height + verticalInset * 2 - ) - dashBorder.path = UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius).cgPath - layer.addSublayer(dashBorder) - self.dashBorder = dashBorder - } - + @available(*, unavailable) + required init?(coder _: NSCoder) { fatalError() } + init(cornerRadius: CGFloat = 8.0, borderWidth: CGFloat = 1.0, borderColor: UIColor) { + self.borderWidth = borderWidth + self.borderColor = borderColor + self.cornerRadius = cornerRadius + super.init(frame: .zero) + layer.cornerRadius = cornerRadius + } + + private let borderWidth: CGFloat + private let borderColor: UIColor + private let cornerRadius: CGFloat + + var dashBorder: CAShapeLayer? + + override func layoutSubviews() { + super.layoutSubviews() + + dashBorder?.removeFromSuperlayer() + let dashBorder = CAShapeLayer() + dashBorder.lineWidth = borderWidth + dashBorder.strokeColor = borderColor.cgColor + dashBorder.lineDashPattern = [3, 2] + dashBorder.frame = bounds + dashBorder.fillColor = nil + let horizontalInset = 8.0 + let verticalInset = 4.0 + let bounds = CGRect( + x: bounds.origin.x - horizontalInset, + y: bounds.origin.y - verticalInset, + width: bounds.width + horizontalInset * 2, + height: bounds.height + verticalInset * 2 + ) + dashBorder.path = UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius).cgPath + layer.addSublayer(dashBorder) + self.dashBorder = dashBorder + } } diff --git a/LiarGame/Sources/ViewController/RandomMusicQuiz/RandomMusicQuizView.swift b/LiarGame/Sources/ViewController/RandomMusicQuiz/RandomMusicQuizView.swift index e292872..38e7bcf 100644 --- a/LiarGame/Sources/ViewController/RandomMusicQuiz/RandomMusicQuizView.swift +++ b/LiarGame/Sources/ViewController/RandomMusicQuiz/RandomMusicQuizView.swift @@ -5,158 +5,156 @@ // Created by JK on 2022/04/26. // -import UIKit import FlexLayout import PinLayout +import UIKit import YouTubeiOSPlayerHelper final class RandomQuizView: UIView { - @available(*, unavailable) - required init?(coder: NSCoder) { fatalError() } - init() { - super.init(frame: .zero) - setupViews() - } - - - fileprivate let container = UIView() - private let _tintColor = UIColor(hexString: "1D5C63") - - lazy var threeSecondButton = makeRoundedButton(tintColor: _tintColor, str: "3초") - lazy var fiveSecondButton = makeRoundedButton(tintColor: _tintColor, str: "5초") - lazy var tenSecondButton = makeRoundedButton(tintColor: _tintColor, str: "10초") - lazy var playButton = makeRoundedButton(tintColor: _tintColor, str: "재생") - - private lazy var currentVersionLabel = UILabel().then { - $0.textColor = .label - } - - lazy var updateButton = makeRoundedButton(tintColor: _tintColor).then { - $0.setImage(UIImage(systemName: "arrow.clockwise"), for: .normal) - } - lazy var shuffleButton = makeRoundedButton(tintColor: _tintColor).then { - $0.setImage(UIImage(systemName: "shuffle"), for: .normal) - } - - lazy var showAnswerButton = makeRoundedButton(tintColor: _tintColor, str: "정답 보기") - - private lazy var answerLabel = DashedLineBorderdLabel(borderColor: _tintColor).then { - $0.font = .preferredFont(forTextStyle: .title1) - $0.isHidden = true - } - - let ytPlayer = YTPlayerView(frame: .zero) - - override func layoutSubviews() { - super.layoutSubviews() - - container.pin.all(pin.safeArea) - container.flex.layout() - } - - func setAnswerLabel(_ value: (title: String, artist: String)?) { - if let value = value { - answerLabel.text = "\(value.title) - \(value.artist)" - answerLabel.isHidden = false - answerLabel.flex.markDirty() - container.flex.layout() - } else { - answerLabel.isHidden = true + @available(*, unavailable) + required init?(coder _: NSCoder) { fatalError() } + init() { + super.init(frame: .zero) + setupViews() + } + + fileprivate let container = UIView() + private let _tintColor = UIColor(hexString: "1D5C63") + + lazy var threeSecondButton = makeRoundedButton(tintColor: _tintColor, str: "3초") + lazy var fiveSecondButton = makeRoundedButton(tintColor: _tintColor, str: "5초") + lazy var tenSecondButton = makeRoundedButton(tintColor: _tintColor, str: "10초") + lazy var playButton = makeRoundedButton(tintColor: _tintColor, str: "재생") + + private lazy var currentVersionLabel = UILabel().then { + $0.textColor = .label + } + + lazy var updateButton = makeRoundedButton(tintColor: _tintColor).then { + $0.setImage(UIImage(systemName: "arrow.clockwise"), for: .normal) + } + + lazy var shuffleButton = makeRoundedButton(tintColor: _tintColor).then { + $0.setImage(UIImage(systemName: "shuffle"), for: .normal) } - } - - func setVersionLabel(_ value: String) { - currentVersionLabel.text = value - currentVersionLabel.flex.markDirty() - container.flex.layout() - } - - func changePlayButtonState(isPlaying: Bool) { - [threeSecondButton, fiveSecondButton, tenSecondButton] - .forEach { $0.isEnabled = !isPlaying } - playButton.setTitle(isPlaying ? "정지" : "시작", for: .normal) - if isPlaying { ytPlayer.playVideo() } - else { ytPlayer.stopVideo() + + lazy var showAnswerButton = makeRoundedButton(tintColor: _tintColor, str: "정답 보기") + + private lazy var answerLabel = DashedLineBorderdLabel(borderColor: _tintColor).then { + $0.font = .preferredFont(forTextStyle: .title1) + $0.isHidden = true } - } - - private var loadingView: UIView? - - func setLoading(_ value: Bool) { - if value { - let loadingBackground = UIView(frame: bounds).then { - $0.backgroundColor = .systemGray.withAlphaComponent(0.5) + + let ytPlayer = YTPlayerView(frame: .zero) + + override func layoutSubviews() { + super.layoutSubviews() + + container.pin.all(pin.safeArea) + container.flex.layout() + } + + func setAnswerLabel(_ value: (title: String, artist: String)?) { + if let value = value { + answerLabel.text = "\(value.title) - \(value.artist)" + answerLabel.isHidden = false + answerLabel.flex.markDirty() + container.flex.layout() + } else { + answerLabel.isHidden = true + } } - let indicator = UIActivityIndicatorView(style: .large) - indicator.color = _tintColor - - addSubview(loadingBackground) - loadingBackground.addSubview(indicator) - indicator.center = center - self.loadingView = loadingBackground - indicator.startAnimating() - } else { - loadingView?.removeFromSuperview() + + func setVersionLabel(_ value: String) { + currentVersionLabel.text = value + currentVersionLabel.flex.markDirty() + container.flex.layout() } - - } - - private func setupViews() { - backgroundColor = UIColor(hexString: "EDE6DB") - addSubview(container) - container.addSubview(ytPlayer) - ytPlayer.isHidden = true - - container.flex - .direction(.column).justifyContent(.center).marginHorizontal(20).define { - // 장르 선택 영역 - $0.addItem(UILabel().then { $0.text = "Genre Area"; $0.backgroundColor = .systemGray; $0.textAlignment = .center }) - .width(100%).aspectRatio(1.0) - .shrink(1) - - $0.addItem().direction(.row).height(150).justifyContent(.spaceAround).alignItems(.end).define { - $0.addItem(shuffleButton).padding(8) - - $0.addItem().direction(.column).justifyContent(.start).alignItems(.center).define { - $0.addItem(currentVersionLabel) - $0.addItem(updateButton).padding(8) - .marginTop(8) - } + + func changePlayButtonState(isPlaying: Bool) { + [threeSecondButton, fiveSecondButton, tenSecondButton] + .forEach { $0.isEnabled = !isPlaying } + playButton.setTitle(isPlaying ? "정지" : "시작", for: .normal) + if isPlaying { ytPlayer.playVideo() } + else { ytPlayer.stopVideo() } - - $0.addItem().direction(.row).height(40).justifyContent(.spaceEvenly).define { flex in - [playButton, threeSecondButton, fiveSecondButton, tenSecondButton].forEach { - flex.addItem($0) - .grow(1) - } + } + + private var loadingView: UIView? + + func setLoading(_ value: Bool) { + if value { + let loadingBackground = UIView(frame: bounds).then { + $0.backgroundColor = .systemGray.withAlphaComponent(0.5) + } + let indicator = UIActivityIndicatorView(style: .large) + indicator.color = _tintColor + + addSubview(loadingBackground) + loadingBackground.addSubview(indicator) + indicator.center = center + loadingView = loadingBackground + indicator.startAnimating() + } else { + loadingView?.removeFromSuperview() } - .horizontallySpacing(10) - - $0.addItem().height(30) - - $0.addItem(showAnswerButton) - .width(150).height(50) - .alignSelf(.center) - $0.addItem(answerLabel) - .padding(1) - .alignSelf(.center) - - } - .verticallySpacing(20) - } + } + + private func setupViews() { + backgroundColor = UIColor(hexString: "EDE6DB") + addSubview(container) + container.addSubview(ytPlayer) + ytPlayer.isHidden = true + + container.flex + .direction(.column).justifyContent(.center).marginHorizontal(20).define { + // 장르 선택 영역 + $0.addItem(UILabel().then { $0.text = "Genre Area"; $0.backgroundColor = .systemGray; $0.textAlignment = .center }) + .width(100%).aspectRatio(1.0) + .shrink(1) + + $0.addItem().direction(.row).height(150).justifyContent(.spaceAround).alignItems(.end).define { + $0.addItem(shuffleButton).padding(8) + + $0.addItem().direction(.column).justifyContent(.start).alignItems(.center).define { + $0.addItem(currentVersionLabel) + $0.addItem(updateButton).padding(8) + .marginTop(8) + } + } + + $0.addItem().direction(.row).height(40).justifyContent(.spaceEvenly).define { flex in + [playButton, threeSecondButton, fiveSecondButton, tenSecondButton].forEach { + flex.addItem($0) + .grow(1) + } + } + .horizontallySpacing(10) + + $0.addItem().height(30) + + $0.addItem(showAnswerButton) + .width(150).height(50) + .alignSelf(.center) + $0.addItem(answerLabel) + .padding(1) + .alignSelf(.center) + } + .verticallySpacing(20) + } } -fileprivate func makeRoundedButton(tintColor: UIColor, str: String? = nil) -> UIButton { - let button = UIButton() - - button.layer.cornerRadius = 15 - button.layer.cornerCurve = .continuous - button.backgroundColor = tintColor - button.tintColor = .white - str.map { - button.setTitle($0, for: .normal) - button.setTitleColor(.systemGray, for: .highlighted) - } - - return button +private func makeRoundedButton(tintColor: UIColor, str: String? = nil) -> UIButton { + let button = UIButton() + + button.layer.cornerRadius = 15 + button.layer.cornerCurve = .continuous + button.backgroundColor = tintColor + button.tintColor = .white + str.map { + button.setTitle($0, for: .normal) + button.setTitleColor(.systemGray, for: .highlighted) + } + + return button } diff --git a/LiarGame/Sources/ViewController/RandomMusicQuiz/RandomMusicQuizViewController.swift b/LiarGame/Sources/ViewController/RandomMusicQuiz/RandomMusicQuizViewController.swift index 159a819..e8e117f 100644 --- a/LiarGame/Sources/ViewController/RandomMusicQuiz/RandomMusicQuizViewController.swift +++ b/LiarGame/Sources/ViewController/RandomMusicQuiz/RandomMusicQuizViewController.swift @@ -5,116 +5,111 @@ // Created by JK on 2022/04/21. // -import UIKit import ReactorKit import RxSwift import Then +import UIKit final class RandomMusicQuizViewController: UIViewController, View { - - private let content = RandomQuizView() - - init(reactor: RandomMusicQuizReactor) { - super.init(nibName: nil, bundle: nil) - self.reactor = reactor - } - - @available(*, unavailable) - required init?(coder: NSCoder) { fatalError() } - - override func loadView() { - super.loadView() - self.view = content - } - - var disposeBag = DisposeBag() - func bind(reactor: RandomMusicQuizReactor) { - reactor.action.onNext(.needCurrentVersion) - reactor.action.onNext(.shuffle) - - bindAction(reactor: reactor) - bindState(reactor: reactor) - } - - private func bindAction(reactor: RandomMusicQuizReactor) { - content.threeSecondButton.rx.tap - .map { _ in Reactor.Action.playMusic(second: .three) } - .bind(to: reactor.action) - .disposed(by: disposeBag) - - content.fiveSecondButton.rx.tap - .map { _ in Reactor.Action.playMusic(second: .five) } - .bind(to: reactor.action) - .disposed(by: disposeBag) - - content.tenSecondButton.rx.tap - .map { _ in Reactor.Action.playMusic(second: .ten) } - .bind(to: reactor.action) - .disposed(by: disposeBag) - - content.playButton.rx.tap - .map { _ in Reactor.Action.didPlayToggleButtonTapped } - .bind(to: reactor.action) - .disposed(by: disposeBag) - - content.updateButton.rx.tap - .map { _ in Reactor.Action.updateMusicList } - .bind(to: reactor.action) - .disposed(by: disposeBag) - - content.shuffleButton.rx.tap - .map { _ in Reactor.Action.shuffle } - .bind(to: reactor.action) - .disposed(by: disposeBag) - - content.showAnswerButton.rx.tap - .map { _ in Reactor.Action.didAnswerButtonTapped } - .bind(to: reactor.action) - .disposed(by: disposeBag) - - content.ytPlayer.rx.isReady - .map { _ in Reactor.Action.playerReady } - .bind(to: reactor.action) - .disposed(by: disposeBag) - - } - - private func bindState(reactor: RandomMusicQuizReactor) { - reactor.state.map(\.answer) - .distinctUntilChanged { $0?.title == $1?.title && $0?.artist == $1?.artist } - .observe(on: MainScheduler.instance) - .subscribe(onNext: content.setAnswerLabel) - .disposed(by: disposeBag) - - reactor.state.map(\.currentVersion) - .distinctUntilChanged() - .observe(on: MainScheduler.instance) - .subscribe(onNext: content.setVersionLabel) - .disposed(by: disposeBag) - - reactor.state.map(\.currentMusic) - .distinctUntilChanged() - .compactMap { $0 } - .observe(on: MainScheduler.instance) - .subscribe(onNext: { [weak self] in - self?.content.ytPlayer.load(withVideoId: $0.id, playerVars: [ - "start": $0.startedAt - ]) - }) - .disposed(by: disposeBag) - - reactor.state.map(\.isLoading) - .distinctUntilChanged() - .subscribe(onNext: content.setLoading(_:)) - .disposed(by: disposeBag) - - reactor.state.map(\.isPlaying) - .distinctUntilChanged() - .observe(on: MainScheduler.instance) - .subscribe(onNext: content.changePlayButtonState(isPlaying:)) - .disposed(by: disposeBag) - - } - -} + private let content = RandomQuizView() + + init(reactor: RandomMusicQuizReactor) { + super.init(nibName: nil, bundle: nil) + self.reactor = reactor + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { fatalError() } + + override func loadView() { + super.loadView() + view = content + } + + var disposeBag = DisposeBag() + func bind(reactor: RandomMusicQuizReactor) { + reactor.action.onNext(.needCurrentVersion) + reactor.action.onNext(.shuffle) + + bindAction(reactor: reactor) + bindState(reactor: reactor) + } + + private func bindAction(reactor: RandomMusicQuizReactor) { + content.threeSecondButton.rx.tap + .map { _ in Reactor.Action.playMusic(second: .three) } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + content.fiveSecondButton.rx.tap + .map { _ in Reactor.Action.playMusic(second: .five) } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + content.tenSecondButton.rx.tap + .map { _ in Reactor.Action.playMusic(second: .ten) } + .bind(to: reactor.action) + .disposed(by: disposeBag) + content.playButton.rx.tap + .map { _ in Reactor.Action.didPlayToggleButtonTapped } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + content.updateButton.rx.tap + .map { _ in Reactor.Action.updateMusicList } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + content.shuffleButton.rx.tap + .map { _ in Reactor.Action.shuffle } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + content.showAnswerButton.rx.tap + .map { _ in Reactor.Action.didAnswerButtonTapped } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + content.ytPlayer.rx.isReady + .map { _ in Reactor.Action.playerReady } + .bind(to: reactor.action) + .disposed(by: disposeBag) + } + + private func bindState(reactor: RandomMusicQuizReactor) { + reactor.state.map(\.answer) + .distinctUntilChanged { $0?.title == $1?.title && $0?.artist == $1?.artist } + .observe(on: MainScheduler.instance) + .subscribe(onNext: content.setAnswerLabel) + .disposed(by: disposeBag) + + reactor.state.map(\.currentVersion) + .distinctUntilChanged() + .observe(on: MainScheduler.instance) + .subscribe(onNext: content.setVersionLabel) + .disposed(by: disposeBag) + + reactor.state.map(\.currentMusic) + .distinctUntilChanged() + .compactMap { $0 } + .observe(on: MainScheduler.instance) + .subscribe(onNext: { [weak self] in + self?.content.ytPlayer.load(withVideoId: $0.id, playerVars: [ + "start": $0.startedAt, + ]) + }) + .disposed(by: disposeBag) + + reactor.state.map(\.isLoading) + .distinctUntilChanged() + .subscribe(onNext: content.setLoading(_:)) + .disposed(by: disposeBag) + + reactor.state.map(\.isPlaying) + .distinctUntilChanged() + .observe(on: MainScheduler.instance) + .subscribe(onNext: content.changePlayButtonState(isPlaying:)) + .disposed(by: disposeBag) + } +} diff --git a/LiarGame/Sources/ViewController/SplashViewController.swift b/LiarGame/Sources/ViewController/SplashViewController.swift index 3371bcb..a06d50e 100644 --- a/LiarGame/Sources/ViewController/SplashViewController.swift +++ b/LiarGame/Sources/ViewController/SplashViewController.swift @@ -5,23 +5,20 @@ // Created by Jay on 2022/04/17. // -import UIKit -import PinLayout import FlexLayout +import PinLayout import Then +import UIKit final class SplashViewController: UIViewController { - override func viewDidLoad() { super.viewDidLoad() - self.view.backgroundColor = .brown + view.backgroundColor = .brown } - - override func viewDidAppear(_ animated: Bool) { + + override func viewDidAppear(_: Bool) { let homeVC = HomeViewController(reactor: HomeReactor()) homeVC.modalPresentationStyle = .fullScreen - self.present(homeVC, animated: true, completion: nil) + present(homeVC, animated: true, completion: nil) } - } - diff --git a/Project.yml b/Project.yml index 187c85b..c5ea8ea 100644 --- a/Project.yml +++ b/Project.yml @@ -6,6 +6,10 @@ configFiles: Debug: xcconfig/Project-Debug.xcconfig Release: xcconfig/Project-Release.xcconfig +options: + indentWidth: 4 + tabWidth: 4 + packages: Then: url: https://github.com/devxoul/Then From 96b927f0ae1fc863779da7e6598230651687dcf7 Mon Sep 17 00:00:00 2001 From: elppaaa Date: Tue, 26 Apr 2022 14:31:32 +0900 Subject: [PATCH 06/10] Revert "style: Code reformatting" This reverts commit d08979f5387a4a43c6d8453fb5294934b95e4116. --- LiarGame/Application/AppDelegate.swift | 16 +- LiarGame/Application/SceneDelegate.swift | 20 +- .../Sources/Model/LiarGameModeModel.swift | 3 +- LiarGame/Sources/Model/Music.swift | 55 ++-- LiarGame/Sources/Model/MusicSheetDTO.swift | 5 +- LiarGame/Sources/Reactor/HomeReactor.swift | 23 +- .../Sources/Reactor/LiarGameModeReactor.swift | 23 +- .../Reactor/RandomMusicQuizReactor.swift | 232 +++++++-------- .../Repository/RandomMusicRepository.swift | 278 ++++++++--------- LiarGame/Sources/Utils/Flex+Extensions.swift | 40 +-- .../Sources/Utils/UIColor+Extensions.swift | 18 +- .../Utils/Userdefaults+PropertyWrapper.swift | 2 +- .../Utils/YTPlayerViewDelegateProxy.swift | 126 ++++---- .../ViewController/HomeViewController.swift | 59 ++-- .../LiarGame/LiarGameModeViewController.swift | 98 +++--- .../LiarGameSubjectViewController.swift | 6 +- .../DashedLineBorderdLabel.swift | 78 ++--- .../RandomMusicQuiz/RandomMusicQuizView.swift | 280 +++++++++--------- .../RandomMusicQuizViewController.swift | 209 ++++++------- .../ViewController/SplashViewController.swift | 15 +- Project.yml | 4 - 21 files changed, 815 insertions(+), 775 deletions(-) diff --git a/LiarGame/Application/AppDelegate.swift b/LiarGame/Application/AppDelegate.swift index 2765f6e..3a96f8b 100644 --- a/LiarGame/Application/AppDelegate.swift +++ b/LiarGame/Application/AppDelegate.swift @@ -9,22 +9,28 @@ import UIKit @main class AppDelegate: UIResponder, UIApplicationDelegate { - func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + + + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. - true + return true } // MARK: UISceneSession Lifecycle - func application(_: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options _: UIScene.ConnectionOptions) -> UISceneConfiguration { + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { // Called when a new scene session is being created. // Use this method to select a configuration to create the new scene with. - UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) } - func application(_: UIApplication, didDiscardSceneSessions _: Set) { + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { // Called when the user discards a scene session. // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. // Use this method to release any resources that were specific to the discarded scenes, as they will not return. } + + } + diff --git a/LiarGame/Application/SceneDelegate.swift b/LiarGame/Application/SceneDelegate.swift index db5387f..d55db5d 100644 --- a/LiarGame/Application/SceneDelegate.swift +++ b/LiarGame/Application/SceneDelegate.swift @@ -8,45 +8,51 @@ import UIKit class SceneDelegate: UIResponder, UIWindowSceneDelegate { + var window: UIWindow? - func scene(_ scene: UIScene, willConnectTo _: UISceneSession, options _: UIScene.ConnectionOptions) { + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). guard let windowScene = (scene as? UIWindowScene) else { return } - + window = UIWindow(windowScene: windowScene) // SceneDelegate의 프로퍼티에 설정해줌 let mainViewController = SplashViewController() // 맨 처음 보여줄 ViewController window?.rootViewController = mainViewController window?.makeKeyAndVisible() + } - func sceneDidDisconnect(_: UIScene) { + func sceneDidDisconnect(_ scene: UIScene) { // Called as the scene is being released by the system. // This occurs shortly after the scene enters the background, or when its session is discarded. // Release any resources associated with this scene that can be re-created the next time the scene connects. // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). } - func sceneDidBecomeActive(_: UIScene) { + func sceneDidBecomeActive(_ scene: UIScene) { // Called when the scene has moved from an inactive state to an active state. // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. } - func sceneWillResignActive(_: UIScene) { + func sceneWillResignActive(_ scene: UIScene) { // Called when the scene will move from an active state to an inactive state. // This may occur due to temporary interruptions (ex. an incoming phone call). } - func sceneWillEnterForeground(_: UIScene) { + func sceneWillEnterForeground(_ scene: UIScene) { // Called as the scene transitions from the background to the foreground. // Use this method to undo the changes made on entering the background. } - func sceneDidEnterBackground(_: UIScene) { + func sceneDidEnterBackground(_ scene: UIScene) { // Called as the scene transitions from the foreground to the background. // Use this method to save data, release shared resources, and store enough scene-specific state information // to restore the scene back to its current state. } + + } + diff --git a/LiarGame/Sources/Model/LiarGameModeModel.swift b/LiarGame/Sources/Model/LiarGameModeModel.swift index f1d4f9d..4b6f818 100644 --- a/LiarGame/Sources/Model/LiarGameModeModel.swift +++ b/LiarGame/Sources/Model/LiarGameModeModel.swift @@ -7,7 +7,8 @@ import Foundation -enum LiarGameMode { + +enum LiarGameMode{ case normal case stupid } diff --git a/LiarGame/Sources/Model/Music.swift b/LiarGame/Sources/Model/Music.swift index 93e83be..26ddd69 100644 --- a/LiarGame/Sources/Model/Music.swift +++ b/LiarGame/Sources/Model/Music.swift @@ -8,31 +8,32 @@ import Foundation struct Music: Codable, Equatable { - let title: String - let artist: String - /// youtube video ID - let id: String - let startedAt: Float - - /** Music 생성자 - - 들어오는 배열의 구조: - [title, artist,id, startedAt] - - - title: 노래 제목 - - artist: 가수 - - id: 유튜브 동영상 id - - startedAt: 첫 재생 시각 - - 길이가 맞지 않을 경우 `return nil` - */ - init?(from array: [String]) { - guard array.count == 4, - let startedAt = Float(array[3]) else { return nil } - - title = array[0] - artist = array[1] - id = array[2] - self.startedAt = startedAt - } + let title: String + let artist: String + /// youtube video ID + let id: String + let startedAt: Float + + /** Music 생성자 + + 들어오는 배열의 구조: + [title, artist,id, startedAt] + + - title: 노래 제목 + - artist: 가수 + - id: 유튜브 동영상 id + - startedAt: 첫 재생 시각 + + 길이가 맞지 않을 경우 `return nil` + */ + init?(from array: [String]) { + guard array.count == 4, + let startedAt = Float(array[3]) else { return nil } + + self.title = array[0] + self.artist = array[1] + self.id = array[2] + self.startedAt = startedAt + } } + diff --git a/LiarGame/Sources/Model/MusicSheetDTO.swift b/LiarGame/Sources/Model/MusicSheetDTO.swift index bc50edc..d7f4995 100644 --- a/LiarGame/Sources/Model/MusicSheetDTO.swift +++ b/LiarGame/Sources/Model/MusicSheetDTO.swift @@ -8,8 +8,11 @@ import Foundation // MARK: - MusicSheetDTO - struct MusicSheetDTO: Decodable { let range, majorDimension: String let values: [[String]] } + + + + diff --git a/LiarGame/Sources/Reactor/HomeReactor.swift b/LiarGame/Sources/Reactor/HomeReactor.swift index 73c5ee9..080ee60 100644 --- a/LiarGame/Sources/Reactor/HomeReactor.swift +++ b/LiarGame/Sources/Reactor/HomeReactor.swift @@ -7,42 +7,43 @@ import Foundation import ReactorKit -import RxCocoa import RxSwift +import RxCocoa + -final class HomeReactor: Reactor { - enum Action { +final class HomeReactor: Reactor{ + enum Action{ case updateMode(GameMode) } - - enum Mutation { + enum Mutation{ case setMode(GameMode) } - - struct State { + struct State{ var mode: GameMode? } - + let initialState = State() - + enum GameMode { case liarGame case randomMusicQuiz } - + + func mutate(action: Action) -> Observable { switch action { case let .updateMode(mode): return Observable.just(Mutation.setMode(mode)) } } - + func reduce(state: State, mutation: Mutation) -> State { switch mutation { case let .setMode(mode): var newState = state newState.mode = mode return newState + } } } diff --git a/LiarGame/Sources/Reactor/LiarGameModeReactor.swift b/LiarGame/Sources/Reactor/LiarGameModeReactor.swift index e16d55a..e2b8b54 100644 --- a/LiarGame/Sources/Reactor/LiarGameModeReactor.swift +++ b/LiarGame/Sources/Reactor/LiarGameModeReactor.swift @@ -7,31 +7,31 @@ import Foundation import ReactorKit -import RxCocoa import RxSwift +import RxCocoa -final class LiarGameModeReactor: Reactor { - enum Action { +final class LiarGameModeReactor: Reactor{ + enum Action{ case selectMode(LiarGameMode?) } - - enum Mutation { + + enum Mutation{ case setMode(LiarGameMode?) } - - struct State { + + struct State{ var mode: LiarGameMode? } - - let initialState: State = .init() - + let initialState: State = State() + + func mutate(action: Action) -> Observable { switch action { case let .selectMode(mode): return Observable.just(Mutation.setMode(mode)) } } - + func reduce(state: State, mutation: Mutation) -> State { switch mutation { case let .setMode(mode): @@ -40,4 +40,5 @@ final class LiarGameModeReactor: Reactor { return newState } } + } diff --git a/LiarGame/Sources/Reactor/RandomMusicQuizReactor.swift b/LiarGame/Sources/Reactor/RandomMusicQuizReactor.swift index 469be67..2965f03 100644 --- a/LiarGame/Sources/Reactor/RandomMusicQuizReactor.swift +++ b/LiarGame/Sources/Reactor/RandomMusicQuizReactor.swift @@ -9,123 +9,123 @@ import Foundation import ReactorKit final class RandomMusicQuizReactor: Reactor { - init(repository: RandomMusicRepository) { - self.repository = repository + init(repository: RandomMusicRepository) { + self.repository = repository + } + + var scheduler = SerialDispatchQueueScheduler(internalSerialQueueName: "random.music.quiz") + var initialState = State() + private let repository: RandomMusicRepository + + enum PlaySeconds: Int { + case three = 3 + case five = 5 + case ten = 10 + } + + enum Action { + case updateMusicList + case playMusic(second: PlaySeconds) + case didPlayToggleButtonTapped + case didAnswerButtonTapped + case shuffle + case playerReady + case needCurrentVersion + } + + enum Mutation { + case updatePlayingState(Bool) + case updateCurrentVersion(String) + case updateCurrentMusic(Music?) + case updateAnswer((String, String)?) + case updateLoading(Bool) + case ignore + } + + struct State { + var isPlaying: Bool = false + var isLoading: Bool = false + var currentVersion: String = "" + var answer: (title: String, artist: String)? + var currentMusic: Music? + } + + func mutate(action: Action) -> Observable { + switch action { + case .updateMusicList: + return .concat([ + .just(.updateLoading(true)), + .just(.updatePlayingState(false)), + .just(.updateAnswer(nil)), + repository.getNewestVersion() + .asObservable() + .map { _ in Mutation.ignore }, + .just(.updateCurrentMusic(shuffleMusic())), + .just(.updateCurrentVersion(repository.currentVersion)) + ]) + .timeout(.seconds(10), other: Observable.just(Mutation.updateLoading(false)), scheduler: scheduler) + + case let .playMusic(second): + return .concat([ + .just(.updatePlayingState(true)), + .just(.updatePlayingState(false)) + .delay(.seconds(second.rawValue), scheduler: ConcurrentDispatchQueueScheduler(qos: .default)) + ]) + + case .didPlayToggleButtonTapped: + return .just(.updatePlayingState(!currentState.isPlaying)) + + case .didAnswerButtonTapped: + return .just(.updateAnswer(currentAnswer())) + + case .shuffle: + return .concat( + .just(.updateLoading(true)), + .just(.updatePlayingState(false)), + .just(.updateAnswer(nil)), + .just(.updateCurrentMusic(shuffleMusic())) + ) + .timeout(.seconds(10), other: Observable.just(Mutation.updateLoading(false)), scheduler: scheduler) + + case .playerReady: + return .just(.updateLoading(false)) + + case .needCurrentVersion: + return .just(.updateCurrentVersion(repository.currentVersion)) } - - var scheduler = SerialDispatchQueueScheduler(internalSerialQueueName: "random.music.quiz") - var initialState = State() - private let repository: RandomMusicRepository - - enum PlaySeconds: Int { - case three = 3 - case five = 5 - case ten = 10 - } - - enum Action { - case updateMusicList - case playMusic(second: PlaySeconds) - case didPlayToggleButtonTapped - case didAnswerButtonTapped - case shuffle - case playerReady - case needCurrentVersion - } - - enum Mutation { - case updatePlayingState(Bool) - case updateCurrentVersion(String) - case updateCurrentMusic(Music?) - case updateAnswer((String, String)?) - case updateLoading(Bool) - case ignore - } - - struct State { - var isPlaying: Bool = false - var isLoading: Bool = false - var currentVersion: String = "" - var answer: (title: String, artist: String)? - var currentMusic: Music? - } - - func mutate(action: Action) -> Observable { - switch action { - case .updateMusicList: - return .concat([ - .just(.updateLoading(true)), - .just(.updatePlayingState(false)), - .just(.updateAnswer(nil)), - repository.getNewestVersion() - .asObservable() - .map { _ in Mutation.ignore }, - .just(.updateCurrentMusic(shuffleMusic())), - .just(.updateCurrentVersion(repository.currentVersion)), - ]) - .timeout(.seconds(10), other: Observable.just(Mutation.updateLoading(false)), scheduler: scheduler) - - case let .playMusic(second): - return .concat([ - .just(.updatePlayingState(true)), - .just(.updatePlayingState(false)) - .delay(.seconds(second.rawValue), scheduler: ConcurrentDispatchQueueScheduler(qos: .default)), - ]) - - case .didPlayToggleButtonTapped: - return .just(.updatePlayingState(!currentState.isPlaying)) - - case .didAnswerButtonTapped: - return .just(.updateAnswer(currentAnswer())) - - case .shuffle: - return .concat( - .just(.updateLoading(true)), - .just(.updatePlayingState(false)), - .just(.updateAnswer(nil)), - .just(.updateCurrentMusic(shuffleMusic())) - ) - .timeout(.seconds(10), other: Observable.just(Mutation.updateLoading(false)), scheduler: scheduler) - - case .playerReady: - return .just(.updateLoading(false)) - - case .needCurrentVersion: - return .just(.updateCurrentVersion(repository.currentVersion)) - } + } + + func reduce(state: State, mutation: Mutation) -> State { + var state = state + switch mutation { + case let .updatePlayingState(boolean): + state.isPlaying = boolean + case let .updateCurrentVersion(version): + state.currentVersion = version + case let .updateCurrentMusic(music): + state.currentMusic = music + case let .updateAnswer(info): + state.answer = info + case let .updateLoading(boolean): + state.isLoading = boolean + case .ignore: break } - - func reduce(state: State, mutation: Mutation) -> State { - var state = state - switch mutation { - case let .updatePlayingState(boolean): - state.isPlaying = boolean - case let .updateCurrentVersion(version): - state.currentVersion = version - case let .updateCurrentMusic(music): - state.currentMusic = music - case let .updateAnswer(info): - state.answer = info - case let .updateLoading(boolean): - state.isLoading = boolean - case .ignore: break - } - return state - } - - private func currentAnswer() -> (title: String, artist: String)? { - if let currentMusic = currentState.currentMusic { - return (title: currentMusic.title, artist: currentMusic.artist) - } else { - return nil - } - } - - private func shuffleMusic() -> Music? { - guard repository.musicList.count > 0 else { return nil } - let size = repository.musicList.count - let randomNumber = Int(arc4random()) % size - - return repository.musicList[randomNumber] + return state + } + + private func currentAnswer() -> (title: String, artist: String)? { + if let currentMusic = currentState.currentMusic { + return (title: currentMusic.title, artist: currentMusic.artist) + } else { + return nil } + } + + private func shuffleMusic() -> Music? { + guard repository.musicList.count > 0 else { return nil } + let size = repository.musicList.count + let randomNumber: Int = Int(arc4random()) % size + + return repository.musicList[randomNumber] + } } diff --git a/LiarGame/Sources/Repository/RandomMusicRepository.swift b/LiarGame/Sources/Repository/RandomMusicRepository.swift index bb00a79..b11cf1d 100644 --- a/LiarGame/Sources/Repository/RandomMusicRepository.swift +++ b/LiarGame/Sources/Repository/RandomMusicRepository.swift @@ -9,157 +9,159 @@ import Foundation import RxSwift protocol RandomMusicRepositoryType { - /// 최신 버전을 확인합니다. - var newestVersion: Single { get } - /// 현재 저장되어 있는 버전을 확인합니다. - var currentVersion: String { get } - /// 버전 업데이트를 수행합니다. - func getNewestVersion() -> Single<[Music]> - /// 저장되어있는 음악 리스트를 가져옵니다. - var musicList: [Music] { get } + /// 최신 버전을 확인합니다. + var newestVersion: Single { get } + /// 현재 저장되어 있는 버전을 확인합니다. + var currentVersion: String { get } + /// 버전 업데이트를 수행합니다. + func getNewestVersion() -> Single<[Music]> + /// 저장되어있는 음악 리스트를 가져옵니다. + var musicList: [Music] { get } } -final class RandomMusicRepository: RandomMusicRepositoryType { - /// 리스트를 담고 있는 Google Sheet ID - private var sheetID: String = "1jiAcDhKOoMbfLmlCOba33CMAZCwlpaV3enkLVvjMmIA" - private var googleAPIKey: String { - (Bundle.main.object(forInfoDictionaryKey: "GOOGLE_APIKEY") as? String) ?? "" - } - - /// 현재 저장되어 있는 버전 - var currentVersion: String { - Self._currentVersion - } - - @UserDefault(key: "RandomMusicVersion", defaultValue: "unknwon") - private static var _currentVersion: String - /** 저장되어있는 음악 목록을 가져옵니다. - - 최신 버전을 보증하지 않음. - */ - var musicList: [Music] { - do { - return try readMuicList() - } catch { - return [] - } - } - - var newestVersion: Single { - _newsetVersion - .map { $0.components(separatedBy: ",")[0] } - } - - func setCurrent(version: String) { - Self._currentVersion = version - } - - /// 버전 확인 후 최신 버전을 가져옵니다. - func getNewestVersion() -> Single<[Music]> { - _newsetVersion - .flatMap { _version -> Single<[Music]> in - let arr = _version.trimmingCharacters(in: .whitespacesAndNewlines).components(separatedBy: ",") - let version = arr[0] - let length = arr[1] - guard version != self.currentVersion else { return .just(try self.readMuicList()) } - - return self.update(version: version, length: length) - } +final class RandomMusicRepository: RandomMusicRepositoryType { + + /// 리스트를 담고 있는 Google Sheet ID + private var sheetID: String = "1jiAcDhKOoMbfLmlCOba33CMAZCwlpaV3enkLVvjMmIA" + private var googleAPIKey: String { + (Bundle.main.object(forInfoDictionaryKey: "GOOGLE_APIKEY") as? String) ?? "" + } + + /// 현재 저장되어 있는 버전 + var currentVersion: String { + Self._currentVersion + } + + @UserDefault(key: "RandomMusicVersion", defaultValue: "unknwon") + private static var _currentVersion: String + + /** 저장되어있는 음악 목록을 가져옵니다. + + 최신 버전을 보증하지 않음. + */ + var musicList: [Music] { + do { + return try readMuicList() + } catch { + return [] } - - /// 최신 목록으로 업데이트를 수행합니다. - private func update(version: String, length: String) -> Single<[Music]> { - guard let url = URL(string: "https://sheets.googleapis.com/v4/spreadsheets/\(sheetID)/values/Sheet!A2:D\(length)?key=\(googleAPIKey)") else { - assertionFailure("URL String error") - return .error(RandomMusicRepositoryError.castingError) - } - - return URLSession.shared.rx.response(request: URLRequest(url: url)) - .map { response, data -> Data in - guard 200 ..< 300 ~= response.statusCode else { - throw RandomMusicRepositoryError.requestFailed - } - - return data - } - .asSingle() - // 타입 변환 - .map { data -> MusicSheetDTO in - do { - return try JSONDecoder().decode(MusicSheetDTO.self, from: data) - } catch { - throw RandomMusicRepositoryError.parseError - } - } - .map(\.values) - .map { $0.compactMap(Music.init) } - // 저장 - .do(onSuccess: { musicList in - self.setCurrent(version: version) - try self.writeMusicList(from: musicList) - }) + } + + var newestVersion: Single { + _newsetVersion + .map { $0.components(separatedBy: ",")[0] } + } + + func setCurrent(version: String) { + Self._currentVersion = version + } + + /// 버전 확인 후 최신 버전을 가져옵니다. + func getNewestVersion() -> Single<[Music]> { + _newsetVersion + .flatMap { _version -> Single<[Music]> in + let arr = _version.trimmingCharacters(in: .whitespacesAndNewlines).components(separatedBy: ",") + let version = arr[0] + let length = arr[1] + guard version != self.currentVersion else { return .just(try self.readMuicList()) } + + return self.update(version: version, length: length) + } + } + + /// 최신 목록으로 업데이트를 수행합니다. + private func update(version: String, length: String) -> Single<[Music]> { + guard let url = URL(string: "https://sheets.googleapis.com/v4/spreadsheets/\(self.sheetID)/values/Sheet!A2:D\(length)?key=\(self.googleAPIKey)") else { + assertionFailure("URL String error") + return .error(RandomMusicRepositoryError.castingError) } - - /** 최신 정보를 받아옵니다. - - return 값인 String 의 구조는 다음과 같습니다. - - [업데이트된버전],[시트의 마지막행] - - ex) 20220422,5 - */ - private var _newsetVersion: Single { - guard let url = URL(string: "https://dl.dropboxusercontent.com/s/g55fwxp70a16xl1/version.txt") else { - assertionFailure("URL String error") - return .error(RandomMusicRepositoryError.castingError) + + return URLSession.shared.rx.response(request: URLRequest(url: url)) + .map { (response, data) -> Data in + guard 200..<300 ~= response.statusCode else { + throw RandomMusicRepositoryError.requestFailed } - let request = URLRequest(url: url) - return URLSession.shared.rx.data(request: request) - .map { - guard let str = String(data: $0, encoding: .utf8) else { - throw RandomMusicRepositoryError.requestFailed - } - - return str - } - .asSingle() - } - - /// 음악 데이터 저장 - private func writeMusicList(from musicList: [Music]) throws { - if musicList.isEmpty { fatalError() } - let path = try musicDataPath() + + return data + } + .asSingle() + // 타입 변환 + .map { data -> MusicSheetDTO in do { - let data = try JSONEncoder().encode(musicList) - try data.write(to: path, options: .atomic) + return try JSONDecoder().decode(MusicSheetDTO.self, from: data) } catch { - throw RandomMusicRepositoryError.fileWriteFailed + throw RandomMusicRepositoryError.parseError } + } + .map(\.values) + .map { $0.compactMap(Music.init) } + // 저장 + .do(onSuccess: { musicList in + self.setCurrent(version: version) + try self.writeMusicList(from: musicList) + }) + } + + /** 최신 정보를 받아옵니다. + + return 값인 String 의 구조는 다음과 같습니다. + + [업데이트된버전],[시트의 마지막행] + + ex) 20220422,5 + */ + private var _newsetVersion: Single { + guard let url = URL(string: "https://dl.dropboxusercontent.com/s/g55fwxp70a16xl1/version.txt") else { + assertionFailure("URL String error") + return .error(RandomMusicRepositoryError.castingError) } - - /// 음악 데이터 읽어오기 - private func readMuicList() throws -> [Music] { - let path = try musicDataPath() - let data = try Data(contentsOf: path) - - return try JSONDecoder().decode([Music].self, from: data) - } - - /// 음악 데이터를 저장하는 경로 - private func musicDataPath() throws -> URL { - guard let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { - throw RandomMusicRepositoryError.fileAccessFailed + let request = URLRequest(url: url) + return URLSession.shared.rx.data(request: request) + .map { + guard let str = String(data: $0, encoding: .utf8) else { + throw RandomMusicRepositoryError.requestFailed } - - return documentsURL.appendingPathComponent("MusicListData.bin") + + return str + } + .asSingle() + } + + /// 음악 데이터 저장 + private func writeMusicList(from musicList: [Music]) throws { + if musicList.isEmpty { fatalError() } + let path = try musicDataPath() + do { + let data = try JSONEncoder().encode(musicList) + try data.write(to: path, options: .atomic) + } catch { + throw RandomMusicRepositoryError.fileWriteFailed + } + } + + /// 음악 데이터 읽어오기 + private func readMuicList() throws -> [Music] { + let path = try musicDataPath() + let data = try Data(contentsOf: path) + + return try JSONDecoder().decode([Music].self, from: data) + } + + /// 음악 데이터를 저장하는 경로 + private func musicDataPath() throws -> URL { + guard let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { + throw RandomMusicRepositoryError.fileAccessFailed } + + return documentsURL.appendingPathComponent("MusicListData.bin") + } } enum RandomMusicRepositoryError: Error { - case requestFailed - case castingError - case parseError - case fileAccessFailed - case fileWriteFailed + case requestFailed + case castingError + case parseError + case fileAccessFailed + case fileWriteFailed } diff --git a/LiarGame/Sources/Utils/Flex+Extensions.swift b/LiarGame/Sources/Utils/Flex+Extensions.swift index 1464dc6..092ac72 100644 --- a/LiarGame/Sources/Utils/Flex+Extensions.swift +++ b/LiarGame/Sources/Utils/Flex+Extensions.swift @@ -5,28 +5,28 @@ // Created by JK on 2022/04/26. // +import Foundation import CoreGraphics import FlexLayout -import Foundation extension Flex { - @discardableResult - func horizontallySpacing(_ value: CGFloat?) -> Flex { - guard let view = view, view.subviews.count > 1 else { return self } - for (idx, subview) in view.subviews.enumerated() { - if idx == 0 { continue } - subview.flex.marginLeft(value ?? 0) - } - return self - } - - @discardableResult - func verticallySpacing(_ value: CGFloat?) -> Flex { - guard let view = view, view.subviews.count > 1 else { return self } - for (idx, subview) in view.subviews.enumerated() { - if idx == 0 { continue } - subview.flex.marginTop(value ?? 0) - } - return self - } + @discardableResult + func horizontallySpacing(_ value: CGFloat?) -> Flex { + guard let view = view, view.subviews.count > 1 else { return self } + for (idx, subview) in view.subviews.enumerated() { + if idx == 0 { continue } + subview.flex.marginLeft(value ?? 0) + } + return self + } + + @discardableResult + func verticallySpacing(_ value: CGFloat?) -> Flex { + guard let view = view, view.subviews.count > 1 else { return self } + for (idx, subview) in view.subviews.enumerated() { + if idx == 0 { continue } + subview.flex.marginTop(value ?? 0) + } + return self + } } diff --git a/LiarGame/Sources/Utils/UIColor+Extensions.swift b/LiarGame/Sources/Utils/UIColor+Extensions.swift index 7ae71d8..63a9fbf 100644 --- a/LiarGame/Sources/Utils/UIColor+Extensions.swift +++ b/LiarGame/Sources/Utils/UIColor+Extensions.swift @@ -8,14 +8,14 @@ import UIKit extension UIColor { - convenience init(hexString: String) { - if let rgbValue = UInt(hexString, radix: 16) { - let red = CGFloat((rgbValue >> 16) & 0xFF) / 255 - let green = CGFloat((rgbValue >> 8) & 0xFF) / 255 - let blue = CGFloat(rgbValue & 0xFF) / 255 - self.init(red: red, green: green, blue: blue, alpha: 1.0) - } else { - self.init(red: 0.0, green: 0.0, blue: 0.0, alpha: 1.0) - } + convenience init(hexString : String) { + if let rgbValue = UInt(hexString, radix: 16) { + let red = CGFloat((rgbValue >> 16) & 0xff) / 255 + let green = CGFloat((rgbValue >> 8) & 0xff) / 255 + let blue = CGFloat((rgbValue ) & 0xff) / 255 + self.init(red: red, green: green, blue: blue, alpha: 1.0) + } else { + self.init(red: 0.0, green: 0.0, blue: 0.0, alpha: 1.0) } + } } diff --git a/LiarGame/Sources/Utils/Userdefaults+PropertyWrapper.swift b/LiarGame/Sources/Utils/Userdefaults+PropertyWrapper.swift index d7606e1..3234682 100644 --- a/LiarGame/Sources/Utils/Userdefaults+PropertyWrapper.swift +++ b/LiarGame/Sources/Utils/Userdefaults+PropertyWrapper.swift @@ -15,7 +15,7 @@ struct UserDefault { var wrappedValue: Value { get { - container.object(forKey: key) as? Value ?? defaultValue + return container.object(forKey: key) as? Value ?? defaultValue } set { container.set(newValue, forKey: key) diff --git a/LiarGame/Sources/Utils/YTPlayerViewDelegateProxy.swift b/LiarGame/Sources/Utils/YTPlayerViewDelegateProxy.swift index bd886b0..0600a71 100644 --- a/LiarGame/Sources/Utils/YTPlayerViewDelegateProxy.swift +++ b/LiarGame/Sources/Utils/YTPlayerViewDelegateProxy.swift @@ -6,76 +6,78 @@ // import Foundation -import RxCocoa import RxSwift +import RxCocoa import YouTubeiOSPlayerHelper -final class RxYTPlayerDelegateProxy: - DelegateProxy, - DelegateProxyType, YTPlayerViewDelegate -{ - public private(set) weak var ytPlayer: YTPlayerView? - - init(ytPlayer: YTPlayerView) { - self.ytPlayer = ytPlayer - super.init(parentObject: ytPlayer, delegateProxy: RxYTPlayerDelegateProxy.self) - } - - static func registerKnownImplementations() { - register { RxYTPlayerDelegateProxy(ytPlayer: $0) } - } - - static func currentDelegate(for object: YTPlayerView) -> YTPlayerViewDelegate? { - object.delegate - } - - static func setCurrentDelegate(_ delegate: YTPlayerViewDelegate?, to object: YTPlayerView) { - object.delegate = delegate - } +final class RxYTPlayerDelegateProxy +: DelegateProxy, + DelegateProxyType, YTPlayerViewDelegate { + + public weak private(set) var ytPlayer: YTPlayerView? + + init(ytPlayer: YTPlayerView) { + self.ytPlayer = ytPlayer + super.init(parentObject: ytPlayer, delegateProxy: RxYTPlayerDelegateProxy.self) + } + + static func registerKnownImplementations() { + self.register { RxYTPlayerDelegateProxy(ytPlayer: $0) } + } + + static func currentDelegate(for object: YTPlayerView) -> YTPlayerViewDelegate? { + object.delegate + } + + static func setCurrentDelegate(_ delegate: YTPlayerViewDelegate?, to object: YTPlayerView) { + object.delegate = delegate + } + } extension Reactive where Base: YTPlayerView { - private var delegate: DelegateProxy { - RxYTPlayerDelegateProxy.proxy(for: base) - } - - var state: Observable { - delegate - .methodInvoked(#selector(YTPlayerViewDelegate.playerView(_:didStateChanged:))) - .map { try castOrThrow(Int.self, $0[1]) } - .map { guard let value = YTPlayerState(rawValue: $0) else { - throw RxCocoaError.castingError(object: $0, targetType: YTPlayerState.self) - } - return value - } - } - - var isReady: Observable { - delegate - .methodInvoked(#selector(YTPlayerViewDelegate.playerViewDidBecomeReady(_:))) - .map { _ in base } - } - - var quality: Observable { - delegate - .methodInvoked(#selector(YTPlayerViewDelegate.playerView(_:didQualityChanged:))) - .map { try castOrThrow(YTPlaybackQuality.self, $0[1]) } - } - - var playTime: Observable { - delegate - .methodInvoked(#selector(YTPlayerViewDelegate.playerView(_:didPlayTime:))) - .map { try castOrThrow(Float.self, $0[1]) } - } - - var error: Observable { - delegate - .methodInvoked(#selector(YTPlayerViewDelegate.playerView(_:receivedError:))) - .map { try castOrThrow(YTPlayerError.self, $0[1]) } - } + private var delegate: DelegateProxy { + RxYTPlayerDelegateProxy.proxy(for: base) + } + + var state: Observable { + delegate + .methodInvoked(#selector(YTPlayerViewDelegate.playerView(_:didStateChanged:))) + .map { try castOrThrow(Int.self, $0[1]) } + .map { guard let value = YTPlayerState(rawValue: $0) else { + throw RxCocoaError.castingError(object: $0, targetType: YTPlayerState.self) + } + return value + } + } + + var isReady: Observable { + delegate + .methodInvoked(#selector(YTPlayerViewDelegate.playerViewDidBecomeReady(_:))) + .map { _ in base } + } + + var quality: Observable { + delegate + .methodInvoked(#selector(YTPlayerViewDelegate.playerView(_:didQualityChanged:))) + .map { try castOrThrow(YTPlaybackQuality.self, $0[1]) } + } + + var playTime: Observable { + delegate + .methodInvoked(#selector(YTPlayerViewDelegate.playerView(_:didPlayTime:))) + .map { try castOrThrow(Float.self, $0[1]) } + } + + var error: Observable { + delegate + .methodInvoked(#selector(YTPlayerViewDelegate.playerView(_:receivedError:))) + .map { try castOrThrow(YTPlayerError.self, $0[1]) } + } + } -private func castOrThrow(_ resultType: T.Type, _ object: Any) throws -> T { +fileprivate func castOrThrow(_ resultType: T.Type, _ object: Any) throws -> T { guard let returnValue = object as? T else { throw RxCocoaError.castingError(object: object, targetType: resultType) } diff --git a/LiarGame/Sources/ViewController/HomeViewController.swift b/LiarGame/Sources/ViewController/HomeViewController.swift index 90d3a2b..273bb95 100644 --- a/LiarGame/Sources/ViewController/HomeViewController.swift +++ b/LiarGame/Sources/ViewController/HomeViewController.swift @@ -5,51 +5,52 @@ // Created by Jay on 2022/04/19. // -import FlexLayout +import UIKit import PinLayout -import ReactorKit -import RxCocoa +import FlexLayout import RxSwift -import UIKit +import RxCocoa +import ReactorKit -final class HomeViewController: UIViewController, View { +final class HomeViewController: UIViewController, View{ typealias Reactor = HomeReactor - - init(reactor: HomeReactor) { + + init(reactor: HomeReactor){ super.init(nibName: nil, bundle: nil) self.reactor = reactor setupView() + } - + @available(*, unavailable) - required init?(coder _: NSCoder) { fatalError() } - + required init?(coder: NSCoder) { fatalError() } + override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() - flexLayoutContainer.pin.all(view.pin.safeArea) - flexLayoutContainer.flex.layout() + self.flexLayoutContainer.pin.all(view.pin.safeArea) + self.flexLayoutContainer.flex.layout() } - + override func viewDidLoad() { super.viewDidLoad() - view.backgroundColor = .systemPink + self.view.backgroundColor = .systemPink } - - private let flexLayoutContainer: UIView = .init() - - var disposeBag: DisposeBag = .init() - + + private let flexLayoutContainer: UIView = UIView() + + var disposeBag: DisposeBag = DisposeBag() + private lazy var gameList = [liarGameStartButton, randomMusicQuiz] private let liarGameStartButton = makeGameButton(str: "라이어 게임") private let randomMusicQuiz = makeGameButton(str: "랜덤 음악 맞추기") + } // MARK: - Setup View - extension HomeViewController { private func setupView() { - view.addSubview(flexLayoutContainer) - flexLayoutContainer.flex.direction(.column).alignItems(.center).justifyContent(.center).padding(10).define { flex in + self.view.addSubview(self.flexLayoutContainer) + self.flexLayoutContainer.flex.direction(.column).alignItems(.center).justifyContent(.center).padding(10).define { flex in gameList.forEach { flex.addItem($0) .width(200) @@ -62,20 +63,20 @@ extension HomeViewController { } // MARK: - Binding - extension HomeViewController { func bind(reactor: Reactor) { + liarGameStartButton.rx.tap .subscribe(onNext: { reactor.action.onNext(.updateMode(.liarGame)) }) .disposed(by: disposeBag) - + randomMusicQuiz.rx.tap .map { _ in Reactor.Action.updateMode(.randomMusicQuiz) } .bind(to: reactor.action) .disposed(by: disposeBag) - + // TODO: - 해당 부분에서 `fullScreen` 으로 `present` 가 이루어지므로 메뉴로 돌아갈 기능 필요 reactor.state.map(\.mode) .compactMap { $0 } @@ -95,14 +96,16 @@ extension HomeViewController { }) .disposed(by: disposeBag) } + } -private func makeGameButton(str: String) -> UIButton { +fileprivate func makeGameButton(str: String) -> UIButton { let button = UIButton() - + button.setTitle(str, for: .normal) button.setTitleColor(.black, for: .normal) button.setTitleColor(.systemGray, for: .normal) - + return button } + diff --git a/LiarGame/Sources/ViewController/LiarGame/LiarGameModeViewController.swift b/LiarGame/Sources/ViewController/LiarGame/LiarGameModeViewController.swift index 5948f41..ead5eb1 100644 --- a/LiarGame/Sources/ViewController/LiarGame/LiarGameModeViewController.swift +++ b/LiarGame/Sources/ViewController/LiarGame/LiarGameModeViewController.swift @@ -5,19 +5,20 @@ // Created by Jay on 2022/04/19. // -import FlexLayout +import UIKit import PinLayout -import ReactorKit -import RxCocoa +import FlexLayout import RxSwift -import UIKit +import RxCocoa +import ReactorKit -final class LiarGameModeViewController: UIViewController, View { - init(reactor: LiarGameModeReactor) { +final class LiarGameModeViewController: UIViewController, View{ + + init(reactor: LiarGameModeReactor){ super.init(nibName: nil, bundle: nil) self.reactor = reactor - view.addSubview(flexLayoutContainer) - flexLayoutContainer.flex.direction(.column).justifyContent(.center).alignItems(.center).padding(10).define { flex in + self.view.addSubview(flexLayoutContainer) + self.flexLayoutContainer.flex.direction(.column).justifyContent(.center).alignItems(.center).padding(10).define{ flex in flex.backgroundColor(.brown) flex.addItem(defaultLiarGame).width(200).height(50).backgroundColor(.yellow) flex.addItem(stupidLiarGame).width(200).height(50).backgroundColor(.yellow).marginTop(10) @@ -25,87 +26,90 @@ final class LiarGameModeViewController: UIViewController, View { flex.addItem(memberCountStepper).backgroundColor(.red).marginTop(10) } } - - @available(*, unavailable) - required init?(coder _: NSCoder) { + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - override func viewDidLayoutSubviews() { - flexLayoutContainer.pin.all() - flexLayoutContainer.flex.layout() + self.flexLayoutContainer.pin.all() + self.flexLayoutContainer.flex.layout() } - override func viewDidLoad() { - view.backgroundColor = .green - setupView() - bindingStepper() + self.view.backgroundColor = .green + self.setupView() + self.bindingStepper() } - - let flexLayoutContainer: UIView = .init() - var disposeBag: DisposeBag = .init() - - let defaultLiarGame: UIButton = .init() - let stupidLiarGame: UIButton = .init() - let memberCountLabel: UILabel = .init() - let memberCountStepper: UIStepper = .init().then { + + + let flexLayoutContainer: UIView = UIView() + var disposeBag: DisposeBag = DisposeBag() + + let defaultLiarGame: UIButton = UIButton() + let stupidLiarGame: UIButton = UIButton() + let memberCountLabel: UILabel = UILabel() + let memberCountStepper: UIStepper = UIStepper().then{ $0.wraps = false $0.autorepeat = true $0.minimumValue = 3 $0.maximumValue = 20 } + + + } // MARK: - Setup View - -extension LiarGameModeViewController { - private func setupView() { - defaultLiarGame.do { +extension LiarGameModeViewController{ + private func setupView(){ + defaultLiarGame.do{ $0.setTitle("일반 모드", for: .normal) $0.setTitleColor(.black, for: .normal) self.view.addSubview($0) } - stupidLiarGame.do { + stupidLiarGame.do{ $0.setTitle("바보 모드", for: .normal) $0.setTitleColor(.black, for: .normal) self.view.addSubview($0) } - memberCountLabel.do { + memberCountLabel.do{ $0.textAlignment = .center self.view.addSubview($0) } - memberCountStepper.do { + memberCountStepper.do{ self.view.addSubview($0) } } } -// MARK: - Bind -extension LiarGameModeViewController { +// MARK: - Bind +extension LiarGameModeViewController{ func bind(reactor: LiarGameModeReactor) { + defaultLiarGame.rx.tap .throttle(.milliseconds(300), scheduler: MainScheduler.instance) - .map { Reactor.Action.selectMode(LiarGameMode.normal) } + .map{ Reactor.Action.selectMode(LiarGameMode.normal)} .bind(to: reactor.action) .disposed(by: disposeBag) - + stupidLiarGame.rx.tap .throttle(.milliseconds(300), scheduler: MainScheduler.instance) - .map { Reactor.Action.selectMode(LiarGameMode.stupid) } + .map{ Reactor.Action.selectMode(LiarGameMode.stupid)} .bind(to: reactor.action) .disposed(by: disposeBag) - - reactor.state.map(\.mode) - .withUnretained(self) - .subscribe(onNext: { `self`, _ in - let liarGameSubjectVC = LiarGameSubjectViewController() - liarGameSubjectVC.modalPresentationStyle = .fullScreen - self.present(liarGameSubjectVC, animated: true, completion: nil) - }).disposed(by: disposeBag) + + reactor.state.map { $0.mode } + .withUnretained(self) + .subscribe(onNext: { `self`, mode in + let liarGameSubjectVC = LiarGameSubjectViewController() + liarGameSubjectVC.modalPresentationStyle = .fullScreen + self.present(liarGameSubjectVC, animated: true, completion: nil) + }).disposed(by: disposeBag) + + } + - func bindingStepper() { + func bindingStepper(){ memberCountStepper.rx.value .map { String(Int($0)) } .bind(to: memberCountLabel.rx.text) diff --git a/LiarGame/Sources/ViewController/LiarGame/LiarGameSubjectViewController.swift b/LiarGame/Sources/ViewController/LiarGame/LiarGameSubjectViewController.swift index e27fa78..1402092 100644 --- a/LiarGame/Sources/ViewController/LiarGame/LiarGameSubjectViewController.swift +++ b/LiarGame/Sources/ViewController/LiarGame/LiarGameSubjectViewController.swift @@ -7,8 +7,10 @@ import UIKit -final class LiarGameSubjectViewController: UIViewController { +final class LiarGameSubjectViewController: UIViewController{ + + override func viewDidLoad() { - view.backgroundColor = .green + self.view.backgroundColor = .green } } diff --git a/LiarGame/Sources/ViewController/RandomMusicQuiz/DashedLineBorderdLabel.swift b/LiarGame/Sources/ViewController/RandomMusicQuiz/DashedLineBorderdLabel.swift index 0482c20..e81c503 100644 --- a/LiarGame/Sources/ViewController/RandomMusicQuiz/DashedLineBorderdLabel.swift +++ b/LiarGame/Sources/ViewController/RandomMusicQuiz/DashedLineBorderdLabel.swift @@ -8,42 +8,44 @@ import UIKit class DashedLineBorderdLabel: UILabel { - @available(*, unavailable) - required init?(coder _: NSCoder) { fatalError() } - init(cornerRadius: CGFloat = 8.0, borderWidth: CGFloat = 1.0, borderColor: UIColor) { - self.borderWidth = borderWidth - self.borderColor = borderColor - self.cornerRadius = cornerRadius - super.init(frame: .zero) - layer.cornerRadius = cornerRadius - } - - private let borderWidth: CGFloat - private let borderColor: UIColor - private let cornerRadius: CGFloat - - var dashBorder: CAShapeLayer? - - override func layoutSubviews() { - super.layoutSubviews() - - dashBorder?.removeFromSuperlayer() - let dashBorder = CAShapeLayer() - dashBorder.lineWidth = borderWidth - dashBorder.strokeColor = borderColor.cgColor - dashBorder.lineDashPattern = [3, 2] - dashBorder.frame = bounds - dashBorder.fillColor = nil - let horizontalInset = 8.0 - let verticalInset = 4.0 - let bounds = CGRect( - x: bounds.origin.x - horizontalInset, - y: bounds.origin.y - verticalInset, - width: bounds.width + horizontalInset * 2, - height: bounds.height + verticalInset * 2 - ) - dashBorder.path = UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius).cgPath - layer.addSublayer(dashBorder) - self.dashBorder = dashBorder - } + @available(*, unavailable) + required init?(coder: NSCoder) { fatalError() } + init(cornerRadius: CGFloat = 8.0, borderWidth: CGFloat = 1.0, borderColor: UIColor) { + self.borderWidth = borderWidth + self.borderColor = borderColor + self.cornerRadius = cornerRadius + super.init(frame: .zero) + self.layer.cornerRadius = cornerRadius + } + + + private let borderWidth: CGFloat + private let borderColor: UIColor + private let cornerRadius: CGFloat + + var dashBorder: CAShapeLayer? + + override func layoutSubviews() { + super.layoutSubviews() + + dashBorder?.removeFromSuperlayer() + let dashBorder = CAShapeLayer() + dashBorder.lineWidth = borderWidth + dashBorder.strokeColor = borderColor.cgColor + dashBorder.lineDashPattern = [3, 2] + dashBorder.frame = bounds + dashBorder.fillColor = nil + let horizontalInset = 8.0 + let verticalInset = 4.0 + let bounds = CGRect( + x: bounds.origin.x - horizontalInset, + y: bounds.origin.y - verticalInset, + width: bounds.width + horizontalInset * 2, + height: bounds.height + verticalInset * 2 + ) + dashBorder.path = UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius).cgPath + layer.addSublayer(dashBorder) + self.dashBorder = dashBorder + } + } diff --git a/LiarGame/Sources/ViewController/RandomMusicQuiz/RandomMusicQuizView.swift b/LiarGame/Sources/ViewController/RandomMusicQuiz/RandomMusicQuizView.swift index 38e7bcf..e292872 100644 --- a/LiarGame/Sources/ViewController/RandomMusicQuiz/RandomMusicQuizView.swift +++ b/LiarGame/Sources/ViewController/RandomMusicQuiz/RandomMusicQuizView.swift @@ -5,156 +5,158 @@ // Created by JK on 2022/04/26. // +import UIKit import FlexLayout import PinLayout -import UIKit import YouTubeiOSPlayerHelper final class RandomQuizView: UIView { - @available(*, unavailable) - required init?(coder _: NSCoder) { fatalError() } - init() { - super.init(frame: .zero) - setupViews() - } - - fileprivate let container = UIView() - private let _tintColor = UIColor(hexString: "1D5C63") - - lazy var threeSecondButton = makeRoundedButton(tintColor: _tintColor, str: "3초") - lazy var fiveSecondButton = makeRoundedButton(tintColor: _tintColor, str: "5초") - lazy var tenSecondButton = makeRoundedButton(tintColor: _tintColor, str: "10초") - lazy var playButton = makeRoundedButton(tintColor: _tintColor, str: "재생") - - private lazy var currentVersionLabel = UILabel().then { - $0.textColor = .label - } - - lazy var updateButton = makeRoundedButton(tintColor: _tintColor).then { - $0.setImage(UIImage(systemName: "arrow.clockwise"), for: .normal) - } - - lazy var shuffleButton = makeRoundedButton(tintColor: _tintColor).then { - $0.setImage(UIImage(systemName: "shuffle"), for: .normal) + @available(*, unavailable) + required init?(coder: NSCoder) { fatalError() } + init() { + super.init(frame: .zero) + setupViews() + } + + + fileprivate let container = UIView() + private let _tintColor = UIColor(hexString: "1D5C63") + + lazy var threeSecondButton = makeRoundedButton(tintColor: _tintColor, str: "3초") + lazy var fiveSecondButton = makeRoundedButton(tintColor: _tintColor, str: "5초") + lazy var tenSecondButton = makeRoundedButton(tintColor: _tintColor, str: "10초") + lazy var playButton = makeRoundedButton(tintColor: _tintColor, str: "재생") + + private lazy var currentVersionLabel = UILabel().then { + $0.textColor = .label + } + + lazy var updateButton = makeRoundedButton(tintColor: _tintColor).then { + $0.setImage(UIImage(systemName: "arrow.clockwise"), for: .normal) + } + lazy var shuffleButton = makeRoundedButton(tintColor: _tintColor).then { + $0.setImage(UIImage(systemName: "shuffle"), for: .normal) + } + + lazy var showAnswerButton = makeRoundedButton(tintColor: _tintColor, str: "정답 보기") + + private lazy var answerLabel = DashedLineBorderdLabel(borderColor: _tintColor).then { + $0.font = .preferredFont(forTextStyle: .title1) + $0.isHidden = true + } + + let ytPlayer = YTPlayerView(frame: .zero) + + override func layoutSubviews() { + super.layoutSubviews() + + container.pin.all(pin.safeArea) + container.flex.layout() + } + + func setAnswerLabel(_ value: (title: String, artist: String)?) { + if let value = value { + answerLabel.text = "\(value.title) - \(value.artist)" + answerLabel.isHidden = false + answerLabel.flex.markDirty() + container.flex.layout() + } else { + answerLabel.isHidden = true } - - lazy var showAnswerButton = makeRoundedButton(tintColor: _tintColor, str: "정답 보기") - - private lazy var answerLabel = DashedLineBorderdLabel(borderColor: _tintColor).then { - $0.font = .preferredFont(forTextStyle: .title1) - $0.isHidden = true + } + + func setVersionLabel(_ value: String) { + currentVersionLabel.text = value + currentVersionLabel.flex.markDirty() + container.flex.layout() + } + + func changePlayButtonState(isPlaying: Bool) { + [threeSecondButton, fiveSecondButton, tenSecondButton] + .forEach { $0.isEnabled = !isPlaying } + playButton.setTitle(isPlaying ? "정지" : "시작", for: .normal) + if isPlaying { ytPlayer.playVideo() } + else { ytPlayer.stopVideo() } - - let ytPlayer = YTPlayerView(frame: .zero) - - override func layoutSubviews() { - super.layoutSubviews() - - container.pin.all(pin.safeArea) - container.flex.layout() - } - - func setAnswerLabel(_ value: (title: String, artist: String)?) { - if let value = value { - answerLabel.text = "\(value.title) - \(value.artist)" - answerLabel.isHidden = false - answerLabel.flex.markDirty() - container.flex.layout() - } else { - answerLabel.isHidden = true - } + } + + private var loadingView: UIView? + + func setLoading(_ value: Bool) { + if value { + let loadingBackground = UIView(frame: bounds).then { + $0.backgroundColor = .systemGray.withAlphaComponent(0.5) } - - func setVersionLabel(_ value: String) { - currentVersionLabel.text = value - currentVersionLabel.flex.markDirty() - container.flex.layout() + let indicator = UIActivityIndicatorView(style: .large) + indicator.color = _tintColor + + addSubview(loadingBackground) + loadingBackground.addSubview(indicator) + indicator.center = center + self.loadingView = loadingBackground + indicator.startAnimating() + } else { + loadingView?.removeFromSuperview() } - - func changePlayButtonState(isPlaying: Bool) { - [threeSecondButton, fiveSecondButton, tenSecondButton] - .forEach { $0.isEnabled = !isPlaying } - playButton.setTitle(isPlaying ? "정지" : "시작", for: .normal) - if isPlaying { ytPlayer.playVideo() } - else { ytPlayer.stopVideo() + + } + + private func setupViews() { + backgroundColor = UIColor(hexString: "EDE6DB") + addSubview(container) + container.addSubview(ytPlayer) + ytPlayer.isHidden = true + + container.flex + .direction(.column).justifyContent(.center).marginHorizontal(20).define { + // 장르 선택 영역 + $0.addItem(UILabel().then { $0.text = "Genre Area"; $0.backgroundColor = .systemGray; $0.textAlignment = .center }) + .width(100%).aspectRatio(1.0) + .shrink(1) + + $0.addItem().direction(.row).height(150).justifyContent(.spaceAround).alignItems(.end).define { + $0.addItem(shuffleButton).padding(8) + + $0.addItem().direction(.column).justifyContent(.start).alignItems(.center).define { + $0.addItem(currentVersionLabel) + $0.addItem(updateButton).padding(8) + .marginTop(8) + } } - } - - private var loadingView: UIView? - - func setLoading(_ value: Bool) { - if value { - let loadingBackground = UIView(frame: bounds).then { - $0.backgroundColor = .systemGray.withAlphaComponent(0.5) - } - let indicator = UIActivityIndicatorView(style: .large) - indicator.color = _tintColor - - addSubview(loadingBackground) - loadingBackground.addSubview(indicator) - indicator.center = center - loadingView = loadingBackground - indicator.startAnimating() - } else { - loadingView?.removeFromSuperview() + + $0.addItem().direction(.row).height(40).justifyContent(.spaceEvenly).define { flex in + [playButton, threeSecondButton, fiveSecondButton, tenSecondButton].forEach { + flex.addItem($0) + .grow(1) + } } - } - - private func setupViews() { - backgroundColor = UIColor(hexString: "EDE6DB") - addSubview(container) - container.addSubview(ytPlayer) - ytPlayer.isHidden = true - - container.flex - .direction(.column).justifyContent(.center).marginHorizontal(20).define { - // 장르 선택 영역 - $0.addItem(UILabel().then { $0.text = "Genre Area"; $0.backgroundColor = .systemGray; $0.textAlignment = .center }) - .width(100%).aspectRatio(1.0) - .shrink(1) - - $0.addItem().direction(.row).height(150).justifyContent(.spaceAround).alignItems(.end).define { - $0.addItem(shuffleButton).padding(8) - - $0.addItem().direction(.column).justifyContent(.start).alignItems(.center).define { - $0.addItem(currentVersionLabel) - $0.addItem(updateButton).padding(8) - .marginTop(8) - } - } - - $0.addItem().direction(.row).height(40).justifyContent(.spaceEvenly).define { flex in - [playButton, threeSecondButton, fiveSecondButton, tenSecondButton].forEach { - flex.addItem($0) - .grow(1) - } - } - .horizontallySpacing(10) - - $0.addItem().height(30) - - $0.addItem(showAnswerButton) - .width(150).height(50) - .alignSelf(.center) - $0.addItem(answerLabel) - .padding(1) - .alignSelf(.center) - } - .verticallySpacing(20) - } + .horizontallySpacing(10) + + $0.addItem().height(30) + + $0.addItem(showAnswerButton) + .width(150).height(50) + .alignSelf(.center) + $0.addItem(answerLabel) + .padding(1) + .alignSelf(.center) + + } + .verticallySpacing(20) + } } -private func makeRoundedButton(tintColor: UIColor, str: String? = nil) -> UIButton { - let button = UIButton() - - button.layer.cornerRadius = 15 - button.layer.cornerCurve = .continuous - button.backgroundColor = tintColor - button.tintColor = .white - str.map { - button.setTitle($0, for: .normal) - button.setTitleColor(.systemGray, for: .highlighted) - } - - return button +fileprivate func makeRoundedButton(tintColor: UIColor, str: String? = nil) -> UIButton { + let button = UIButton() + + button.layer.cornerRadius = 15 + button.layer.cornerCurve = .continuous + button.backgroundColor = tintColor + button.tintColor = .white + str.map { + button.setTitle($0, for: .normal) + button.setTitleColor(.systemGray, for: .highlighted) + } + + return button } diff --git a/LiarGame/Sources/ViewController/RandomMusicQuiz/RandomMusicQuizViewController.swift b/LiarGame/Sources/ViewController/RandomMusicQuiz/RandomMusicQuizViewController.swift index e8e117f..159a819 100644 --- a/LiarGame/Sources/ViewController/RandomMusicQuiz/RandomMusicQuizViewController.swift +++ b/LiarGame/Sources/ViewController/RandomMusicQuiz/RandomMusicQuizViewController.swift @@ -5,111 +5,116 @@ // Created by JK on 2022/04/21. // +import UIKit import ReactorKit import RxSwift import Then -import UIKit final class RandomMusicQuizViewController: UIViewController, View { - private let content = RandomQuizView() - - init(reactor: RandomMusicQuizReactor) { - super.init(nibName: nil, bundle: nil) - self.reactor = reactor - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { fatalError() } - - override func loadView() { - super.loadView() - view = content - } - - var disposeBag = DisposeBag() - func bind(reactor: RandomMusicQuizReactor) { - reactor.action.onNext(.needCurrentVersion) - reactor.action.onNext(.shuffle) - - bindAction(reactor: reactor) - bindState(reactor: reactor) - } - - private func bindAction(reactor: RandomMusicQuizReactor) { - content.threeSecondButton.rx.tap - .map { _ in Reactor.Action.playMusic(second: .three) } - .bind(to: reactor.action) - .disposed(by: disposeBag) - - content.fiveSecondButton.rx.tap - .map { _ in Reactor.Action.playMusic(second: .five) } - .bind(to: reactor.action) - .disposed(by: disposeBag) - - content.tenSecondButton.rx.tap - .map { _ in Reactor.Action.playMusic(second: .ten) } - .bind(to: reactor.action) - .disposed(by: disposeBag) - - content.playButton.rx.tap - .map { _ in Reactor.Action.didPlayToggleButtonTapped } - .bind(to: reactor.action) - .disposed(by: disposeBag) - - content.updateButton.rx.tap - .map { _ in Reactor.Action.updateMusicList } - .bind(to: reactor.action) - .disposed(by: disposeBag) - - content.shuffleButton.rx.tap - .map { _ in Reactor.Action.shuffle } - .bind(to: reactor.action) - .disposed(by: disposeBag) - - content.showAnswerButton.rx.tap - .map { _ in Reactor.Action.didAnswerButtonTapped } - .bind(to: reactor.action) - .disposed(by: disposeBag) - - content.ytPlayer.rx.isReady - .map { _ in Reactor.Action.playerReady } - .bind(to: reactor.action) - .disposed(by: disposeBag) - } - - private func bindState(reactor: RandomMusicQuizReactor) { - reactor.state.map(\.answer) - .distinctUntilChanged { $0?.title == $1?.title && $0?.artist == $1?.artist } - .observe(on: MainScheduler.instance) - .subscribe(onNext: content.setAnswerLabel) - .disposed(by: disposeBag) - - reactor.state.map(\.currentVersion) - .distinctUntilChanged() - .observe(on: MainScheduler.instance) - .subscribe(onNext: content.setVersionLabel) - .disposed(by: disposeBag) - - reactor.state.map(\.currentMusic) - .distinctUntilChanged() - .compactMap { $0 } - .observe(on: MainScheduler.instance) - .subscribe(onNext: { [weak self] in - self?.content.ytPlayer.load(withVideoId: $0.id, playerVars: [ - "start": $0.startedAt, - ]) - }) - .disposed(by: disposeBag) - - reactor.state.map(\.isLoading) - .distinctUntilChanged() - .subscribe(onNext: content.setLoading(_:)) - .disposed(by: disposeBag) - - reactor.state.map(\.isPlaying) - .distinctUntilChanged() - .observe(on: MainScheduler.instance) - .subscribe(onNext: content.changePlayButtonState(isPlaying:)) - .disposed(by: disposeBag) - } + + private let content = RandomQuizView() + + init(reactor: RandomMusicQuizReactor) { + super.init(nibName: nil, bundle: nil) + self.reactor = reactor + } + + @available(*, unavailable) + required init?(coder: NSCoder) { fatalError() } + + override func loadView() { + super.loadView() + self.view = content + } + + var disposeBag = DisposeBag() + func bind(reactor: RandomMusicQuizReactor) { + reactor.action.onNext(.needCurrentVersion) + reactor.action.onNext(.shuffle) + + bindAction(reactor: reactor) + bindState(reactor: reactor) + } + + private func bindAction(reactor: RandomMusicQuizReactor) { + content.threeSecondButton.rx.tap + .map { _ in Reactor.Action.playMusic(second: .three) } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + content.fiveSecondButton.rx.tap + .map { _ in Reactor.Action.playMusic(second: .five) } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + content.tenSecondButton.rx.tap + .map { _ in Reactor.Action.playMusic(second: .ten) } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + content.playButton.rx.tap + .map { _ in Reactor.Action.didPlayToggleButtonTapped } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + content.updateButton.rx.tap + .map { _ in Reactor.Action.updateMusicList } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + content.shuffleButton.rx.tap + .map { _ in Reactor.Action.shuffle } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + content.showAnswerButton.rx.tap + .map { _ in Reactor.Action.didAnswerButtonTapped } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + content.ytPlayer.rx.isReady + .map { _ in Reactor.Action.playerReady } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + } + + private func bindState(reactor: RandomMusicQuizReactor) { + reactor.state.map(\.answer) + .distinctUntilChanged { $0?.title == $1?.title && $0?.artist == $1?.artist } + .observe(on: MainScheduler.instance) + .subscribe(onNext: content.setAnswerLabel) + .disposed(by: disposeBag) + + reactor.state.map(\.currentVersion) + .distinctUntilChanged() + .observe(on: MainScheduler.instance) + .subscribe(onNext: content.setVersionLabel) + .disposed(by: disposeBag) + + reactor.state.map(\.currentMusic) + .distinctUntilChanged() + .compactMap { $0 } + .observe(on: MainScheduler.instance) + .subscribe(onNext: { [weak self] in + self?.content.ytPlayer.load(withVideoId: $0.id, playerVars: [ + "start": $0.startedAt + ]) + }) + .disposed(by: disposeBag) + + reactor.state.map(\.isLoading) + .distinctUntilChanged() + .subscribe(onNext: content.setLoading(_:)) + .disposed(by: disposeBag) + + reactor.state.map(\.isPlaying) + .distinctUntilChanged() + .observe(on: MainScheduler.instance) + .subscribe(onNext: content.changePlayButtonState(isPlaying:)) + .disposed(by: disposeBag) + + } + } + diff --git a/LiarGame/Sources/ViewController/SplashViewController.swift b/LiarGame/Sources/ViewController/SplashViewController.swift index a06d50e..3371bcb 100644 --- a/LiarGame/Sources/ViewController/SplashViewController.swift +++ b/LiarGame/Sources/ViewController/SplashViewController.swift @@ -5,20 +5,23 @@ // Created by Jay on 2022/04/17. // -import FlexLayout +import UIKit import PinLayout +import FlexLayout import Then -import UIKit final class SplashViewController: UIViewController { + override func viewDidLoad() { super.viewDidLoad() - view.backgroundColor = .brown + self.view.backgroundColor = .brown } - - override func viewDidAppear(_: Bool) { + + override func viewDidAppear(_ animated: Bool) { let homeVC = HomeViewController(reactor: HomeReactor()) homeVC.modalPresentationStyle = .fullScreen - present(homeVC, animated: true, completion: nil) + self.present(homeVC, animated: true, completion: nil) } + } + diff --git a/Project.yml b/Project.yml index c5ea8ea..187c85b 100644 --- a/Project.yml +++ b/Project.yml @@ -6,10 +6,6 @@ configFiles: Debug: xcconfig/Project-Debug.xcconfig Release: xcconfig/Project-Release.xcconfig -options: - indentWidth: 4 - tabWidth: 4 - packages: Then: url: https://github.com/devxoul/Then From fa2cf53f61da364605cc883c8a5d541c24102552 Mon Sep 17 00:00:00 2001 From: elppaaa Date: Tue, 26 Apr 2022 18:48:33 +0900 Subject: [PATCH 07/10] refactor: modify style #13 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 재사용 않는 클래스 final 로 변경 - 모델 파일 분리 - 네이밍 수정 --- LiarGame/Sources/Model/GameMode.swift | 13 +++++++++++++ LiarGame/Sources/Reactor/HomeReactor.swift | 6 ------ .../Sources/ViewController/HomeViewController.swift | 6 +++--- .../RandomMusicQuiz/DashedLineBorderdLabel.swift | 3 +-- 4 files changed, 17 insertions(+), 11 deletions(-) create mode 100644 LiarGame/Sources/Model/GameMode.swift diff --git a/LiarGame/Sources/Model/GameMode.swift b/LiarGame/Sources/Model/GameMode.swift new file mode 100644 index 0000000..abf35ac --- /dev/null +++ b/LiarGame/Sources/Model/GameMode.swift @@ -0,0 +1,13 @@ +// +// GameMode.swift +// LiarGame +// +// Created by JK on 2022/04/26. +// + +import Foundation + +enum GameMode { + case liarGame + case randomMusicQuiz +} diff --git a/LiarGame/Sources/Reactor/HomeReactor.swift b/LiarGame/Sources/Reactor/HomeReactor.swift index 080ee60..c4ffcb1 100644 --- a/LiarGame/Sources/Reactor/HomeReactor.swift +++ b/LiarGame/Sources/Reactor/HomeReactor.swift @@ -24,12 +24,6 @@ final class HomeReactor: Reactor{ let initialState = State() - enum GameMode { - case liarGame - case randomMusicQuiz - } - - func mutate(action: Action) -> Observable { switch action { case let .updateMode(mode): diff --git a/LiarGame/Sources/ViewController/HomeViewController.swift b/LiarGame/Sources/ViewController/HomeViewController.swift index b281bfa..38f4596 100644 --- a/LiarGame/Sources/ViewController/HomeViewController.swift +++ b/LiarGame/Sources/ViewController/HomeViewController.swift @@ -40,9 +40,9 @@ final class HomeViewController: UIViewController, View{ var disposeBag: DisposeBag = DisposeBag() - private lazy var gameList = [liarGameStartButton, randomMusicQuiz] + private lazy var gameList = [liarGameStartButton, randomMusicQuizButton] private let liarGameStartButton = makeGameButton(str: "라이어 게임") - private let randomMusicQuiz = makeGameButton(str: "랜덤 음악 맞추기") + private let randomMusicQuizButton = makeGameButton(str: "랜덤 음악 맞추기") } @@ -72,7 +72,7 @@ extension HomeViewController { }) .disposed(by: disposeBag) - randomMusicQuiz.rx.tap + randomMusicQuizButton.rx.tap .map { _ in Reactor.Action.updateMode(.randomMusicQuiz) } .bind(to: reactor.action) .disposed(by: disposeBag) diff --git a/LiarGame/Sources/ViewController/RandomMusicQuiz/DashedLineBorderdLabel.swift b/LiarGame/Sources/ViewController/RandomMusicQuiz/DashedLineBorderdLabel.swift index e81c503..db636a1 100644 --- a/LiarGame/Sources/ViewController/RandomMusicQuiz/DashedLineBorderdLabel.swift +++ b/LiarGame/Sources/ViewController/RandomMusicQuiz/DashedLineBorderdLabel.swift @@ -7,7 +7,7 @@ import UIKit -class DashedLineBorderdLabel: UILabel { +final class DashedLineBorderdLabel: UILabel { @available(*, unavailable) required init?(coder: NSCoder) { fatalError() } init(cornerRadius: CGFloat = 8.0, borderWidth: CGFloat = 1.0, borderColor: UIColor) { @@ -18,7 +18,6 @@ class DashedLineBorderdLabel: UILabel { self.layer.cornerRadius = cornerRadius } - private let borderWidth: CGFloat private let borderColor: UIColor private let cornerRadius: CGFloat From 301a1844d35a8ed7cdc3791bff296003bee35260 Mon Sep 17 00:00:00 2001 From: elppaaa Date: Wed, 27 Apr 2022 00:28:43 +0900 Subject: [PATCH 08/10] fix: Fix infinite loading MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 무한로딩되던 문제 해결 - ytPlayer 라이브러리에서 delegate 호출이 정상적으로 이루어지지 않아 발생 - ytPlayer 로의 요청중일 때 막는 방식으로 해결 --- LiarGame/Sources/Reactor/RandomMusicQuizReactor.swift | 5 ++++- .../RandomMusicQuizViewController.swift | 11 ++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/LiarGame/Sources/Reactor/RandomMusicQuizReactor.swift b/LiarGame/Sources/Reactor/RandomMusicQuizReactor.swift index 2965f03..a94e9dd 100644 --- a/LiarGame/Sources/Reactor/RandomMusicQuizReactor.swift +++ b/LiarGame/Sources/Reactor/RandomMusicQuizReactor.swift @@ -16,6 +16,7 @@ final class RandomMusicQuizReactor: Reactor { var scheduler = SerialDispatchQueueScheduler(internalSerialQueueName: "random.music.quiz") var initialState = State() private let repository: RandomMusicRepository + private var isPlayerPending = false enum PlaySeconds: Int { case three = 3 @@ -53,6 +54,7 @@ final class RandomMusicQuizReactor: Reactor { func mutate(action: Action) -> Observable { switch action { case .updateMusicList: + guard !isPlayerPending else { return .empty() } return .concat([ .just(.updateLoading(true)), .just(.updatePlayingState(false)), @@ -79,15 +81,16 @@ final class RandomMusicQuizReactor: Reactor { return .just(.updateAnswer(currentAnswer())) case .shuffle: + guard !isPlayerPending else { return .empty() } return .concat( .just(.updateLoading(true)), .just(.updatePlayingState(false)), .just(.updateAnswer(nil)), .just(.updateCurrentMusic(shuffleMusic())) ) - .timeout(.seconds(10), other: Observable.just(Mutation.updateLoading(false)), scheduler: scheduler) case .playerReady: + isPlayerPending = false return .just(.updateLoading(false)) case .needCurrentVersion: diff --git a/LiarGame/Sources/ViewController/RandomMusicQuiz/RandomMusicQuizViewController.swift b/LiarGame/Sources/ViewController/RandomMusicQuiz/RandomMusicQuizViewController.swift index 159a819..997ccae 100644 --- a/LiarGame/Sources/ViewController/RandomMusicQuiz/RandomMusicQuizViewController.swift +++ b/LiarGame/Sources/ViewController/RandomMusicQuiz/RandomMusicQuizViewController.swift @@ -27,11 +27,16 @@ final class RandomMusicQuizViewController: UIViewController, View { self.view = content } + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + reactor.map { + $0.action.onNext(.needCurrentVersion) + $0.action.onNext(.shuffle) + } + } + var disposeBag = DisposeBag() func bind(reactor: RandomMusicQuizReactor) { - reactor.action.onNext(.needCurrentVersion) - reactor.action.onNext(.shuffle) - bindAction(reactor: reactor) bindState(reactor: reactor) } From 8efc314ca6b3d9c448aa85c49a01c1c524180c3c Mon Sep 17 00:00:00 2001 From: elppaaa Date: Wed, 27 Apr 2022 04:12:58 +0900 Subject: [PATCH 09/10] fix: Fix playing timer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 비디오 재생과정은 아래와 같다. 1. 비디오 로드 // pending 2. 비디오 준비 // ready 3. 비디오 재생 명령 // .startVideo() 4-1. 비디오 버퍼링 // buffering 4-2. 비디오 재생 비디오 재생을 수행 시 버퍼링 과정이 포함되는데, 버퍼링 -> 재생까지의 시각이 매번 달라 YTPlayer 의 state 를 확인하여 재생되는 시점을 확인하고 재생되도록 하였음. - 다른 state (isReady) 와 같은 state 는 PlayerState 로 관리하도록 수정 - 재생 / 정지 동작을 명령할 state 네이밍 수정 fix #8 --- .../Reactor/RandomMusicQuizReactor.swift | 76 ++++++++++++++----- .../RandomMusicQuizViewController.swift | 29 +++++-- 2 files changed, 81 insertions(+), 24 deletions(-) diff --git a/LiarGame/Sources/Reactor/RandomMusicQuizReactor.swift b/LiarGame/Sources/Reactor/RandomMusicQuizReactor.swift index a94e9dd..ba9585d 100644 --- a/LiarGame/Sources/Reactor/RandomMusicQuizReactor.swift +++ b/LiarGame/Sources/Reactor/RandomMusicQuizReactor.swift @@ -11,31 +11,52 @@ import ReactorKit final class RandomMusicQuizReactor: Reactor { init(repository: RandomMusicRepository) { self.repository = repository +// state.subscribe(onNext: { +// print($0) +// }) +// .disposed(by: disposeBag) } + private var disposeBag = DisposeBag() + var scheduler = SerialDispatchQueueScheduler(internalSerialQueueName: "random.music.quiz") var initialState = State() private let repository: RandomMusicRepository - private var isPlayerPending = false + private var playerState: PlayerState = .unknwon + private var second: PlaySecond? - enum PlaySeconds: Int { + enum PlaySecond: Int { case three = 3 case five = 5 case ten = 10 } + enum PlayerState { + /// 비디오 로드 후 대기 중 + case pending + /// 비디오 로드 완료 + case ready + /// 비디오 재생 시 버퍼링 + case buffering + /// 비디오 재생 중 + case playing + /// stopVideo 시 cued + case cued + case unknwon + } + enum Action { case updateMusicList - case playMusic(second: PlaySeconds) + case playMusicButtonTapped(second: PlaySecond) case didPlayToggleButtonTapped case didAnswerButtonTapped case shuffle - case playerReady + case playerState(PlayerState) case needCurrentVersion } enum Mutation { - case updatePlayingState(Bool) + case updatePlayStopState(Bool) case updateCurrentVersion(String) case updateCurrentMusic(Music?) case updateAnswer((String, String)?) @@ -54,10 +75,11 @@ final class RandomMusicQuizReactor: Reactor { func mutate(action: Action) -> Observable { switch action { case .updateMusicList: - guard !isPlayerPending else { return .empty() } + guard playerState != .pending else { return .empty() } + playerState = .pending return .concat([ .just(.updateLoading(true)), - .just(.updatePlayingState(false)), + .just(.updatePlayStopState(false)), .just(.updateAnswer(nil)), repository.getNewestVersion() .asObservable() @@ -67,41 +89,40 @@ final class RandomMusicQuizReactor: Reactor { ]) .timeout(.seconds(10), other: Observable.just(Mutation.updateLoading(false)), scheduler: scheduler) - case let .playMusic(second): + case let .playMusicButtonTapped(second): + self.second = second return .concat([ - .just(.updatePlayingState(true)), - .just(.updatePlayingState(false)) - .delay(.seconds(second.rawValue), scheduler: ConcurrentDispatchQueueScheduler(qos: .default)) + .just(.updatePlayStopState(true)), ]) case .didPlayToggleButtonTapped: - return .just(.updatePlayingState(!currentState.isPlaying)) + return .just(.updatePlayStopState(!currentState.isPlaying)) case .didAnswerButtonTapped: return .just(.updateAnswer(currentAnswer())) case .shuffle: - guard !isPlayerPending else { return .empty() } + guard playerState != .pending else { return .empty() } + playerState = .pending return .concat( .just(.updateLoading(true)), - .just(.updatePlayingState(false)), + .just(.updatePlayStopState(false)), .just(.updateAnswer(nil)), .just(.updateCurrentMusic(shuffleMusic())) ) - case .playerReady: - isPlayerPending = false - return .just(.updateLoading(false)) - case .needCurrentVersion: return .just(.updateCurrentVersion(repository.currentVersion)) + + case let .playerState(state): + return playerStateHandler(state) } } func reduce(state: State, mutation: Mutation) -> State { var state = state switch mutation { - case let .updatePlayingState(boolean): + case let .updatePlayStopState(boolean): state.isPlaying = boolean case let .updateCurrentVersion(version): state.currentVersion = version @@ -131,4 +152,21 @@ final class RandomMusicQuizReactor: Reactor { return repository.musicList[randomNumber] } + + // `YTPlayerView.playVideo()` 호출 시점과 실제 재생 시점이 다름 + // `YTPlayerView` 의 state 를 확인해서 재생 타이머를 수행 + private func playerStateHandler(_ state: PlayerState) -> Observable { + self.playerState = state + guard playerState != .pending else { return .empty() } + + if case .playing = playerState, + let second = self.second { + return .just(.updatePlayStopState(false)) + .delay(.seconds(second.rawValue), scheduler: ConcurrentDispatchQueueScheduler(qos: .default)) + } else if case .ready = playerState { + return .just(.updateLoading(false)) + } else { + return .empty() + } + } } diff --git a/LiarGame/Sources/ViewController/RandomMusicQuiz/RandomMusicQuizViewController.swift b/LiarGame/Sources/ViewController/RandomMusicQuiz/RandomMusicQuizViewController.swift index 997ccae..68f4a21 100644 --- a/LiarGame/Sources/ViewController/RandomMusicQuiz/RandomMusicQuizViewController.swift +++ b/LiarGame/Sources/ViewController/RandomMusicQuiz/RandomMusicQuizViewController.swift @@ -43,17 +43,17 @@ final class RandomMusicQuizViewController: UIViewController, View { private func bindAction(reactor: RandomMusicQuizReactor) { content.threeSecondButton.rx.tap - .map { _ in Reactor.Action.playMusic(second: .three) } + .map { _ in Reactor.Action.playMusicButtonTapped(second: .three) } .bind(to: reactor.action) .disposed(by: disposeBag) content.fiveSecondButton.rx.tap - .map { _ in Reactor.Action.playMusic(second: .five) } + .map { _ in Reactor.Action.playMusicButtonTapped(second: .five) } .bind(to: reactor.action) .disposed(by: disposeBag) content.tenSecondButton.rx.tap - .map { _ in Reactor.Action.playMusic(second: .ten) } + .map { _ in Reactor.Action.playMusicButtonTapped(second: .ten) } .bind(to: reactor.action) .disposed(by: disposeBag) @@ -78,7 +78,26 @@ final class RandomMusicQuizViewController: UIViewController, View { .disposed(by: disposeBag) content.ytPlayer.rx.isReady - .map { _ in Reactor.Action.playerReady } + .map { _ in Reactor.Action.playerState(.ready) } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + content.ytPlayer.rx.state + .map { + let playerState: RandomMusicQuizReactor.PlayerState + switch $0 { + case .playing: + playerState = .playing + case .buffering: + playerState = .buffering + case .cued: + playerState = .cued + default: + playerState = .unknwon + } + return playerState + } + .map { Reactor.Action.playerState($0) } .bind(to: reactor.action) .disposed(by: disposeBag) @@ -118,7 +137,7 @@ final class RandomMusicQuizViewController: UIViewController, View { .observe(on: MainScheduler.instance) .subscribe(onNext: content.changePlayButtonState(isPlaying:)) .disposed(by: disposeBag) - + } } From 5e7e33cd83ef99b9404277e7b3bc1201f1711c7c Mon Sep 17 00:00:00 2001 From: elppaaa Date: Wed, 27 Apr 2022 04:27:00 +0900 Subject: [PATCH 10/10] fix: Fix playing time error MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 시간초 재생 후 시작을 눌렀을 때 시간초 재생이 이루어지던 오류 수정 --- .../Reactor/RandomMusicQuizReactor.swift | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/LiarGame/Sources/Reactor/RandomMusicQuizReactor.swift b/LiarGame/Sources/Reactor/RandomMusicQuizReactor.swift index ba9585d..4ec445f 100644 --- a/LiarGame/Sources/Reactor/RandomMusicQuizReactor.swift +++ b/LiarGame/Sources/Reactor/RandomMusicQuizReactor.swift @@ -11,10 +11,6 @@ import ReactorKit final class RandomMusicQuizReactor: Reactor { init(repository: RandomMusicRepository) { self.repository = repository -// state.subscribe(onNext: { -// print($0) -// }) -// .disposed(by: disposeBag) } private var disposeBag = DisposeBag() @@ -40,7 +36,7 @@ final class RandomMusicQuizReactor: Reactor { case buffering /// 비디오 재생 중 case playing - /// stopVideo 시 cued + /// 재생정지(stopVideo) 호출 시 cued case cued case unknwon } @@ -159,13 +155,15 @@ final class RandomMusicQuizReactor: Reactor { self.playerState = state guard playerState != .pending else { return .empty() } - if case .playing = playerState, - let second = self.second { + switch playerState { + case .playing: return .just(.updatePlayStopState(false)) - .delay(.seconds(second.rawValue), scheduler: ConcurrentDispatchQueueScheduler(qos: .default)) - } else if case .ready = playerState { + case .ready: return .just(.updateLoading(false)) - } else { + case .cued: + self.second = nil + fallthrough + default: return .empty() } }