[iOS] 알람 앱 (3) - 스톱워치 테이블뷰에 랩 타임 추가하기
[iOS] 알람 앱 (2) - 스톱워치 구현하기, Timer와 버튼 상태 변화
알람앱 (1) - 스톱워치 화면 구성 및 기본 세팅 [iOS] 알람 앱 (1) - TabBarItem, NavigationBarItem 이미지 추가하기오늘은 새로운 프로젝트 발제날! 프로젝트의 주제는 알람, 타이머, 스탑워치 기능
yujjne.tistory.com
오늘은 Stopwatch화면의 테이블뷰 안에 Lap 타임을 추가하는 기능을 구현했다.
테이블 뷰의 내용은 Lap 기록, Record 타임, Lap 타임(앞 기록과의 차이) 순으로 표현했다.
또한 최신의 랩타임이 위쪽에 표시되도록 구현했다.
테이블뷰 구성하기
우선 LapTableView에 보여줄 Cell 파일을 만들어준다.
identifier도 사용하기 쉽게 static키워드를 사용해서 만들었다.
테이블 뷰에서 보여줄 lap, record, diff 라벨을 추가해 준 내용이다.
import UIKit
import SnapKit
class StopwatchCell: UITableViewCell {
static let identifier = "StopwatchCell"
let lapLabel: UILabel = {
let label = UILabel()
label.font = UIFont.boldSystemFont(ofSize: 17)
label.textColor = UIColor(named: "textColor")
return label
}()
let recordLabel: UILabel = {
let label = UILabel()
label.font = UIFont.systemFont(ofSize: 16)
label.textColor = UIColor(named: "textColor")
return label
}()
let diffLabel: UILabel = {
let label = UILabel()
label.font = UIFont.systemFont(ofSize: 16)
label.textColor = UIColor(named: "textColor")
return label
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
configureUI()
setupConstraints()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupConstraints() {
contentView.addSubview(lapLabel)
contentView.addSubview(recordLabel)
contentView.addSubview(diffLabel)
lapLabel.snp.makeConstraints {
$0.centerY.equalToSuperview()
$0.leading.equalToSuperview().offset(16)
}
recordLabel.snp.makeConstraints {
$0.centerY.equalToSuperview()
$0.centerX.equalToSuperview()
}
diffLabel.snp.makeConstraints {
$0.centerY.equalToSuperview()
$0.trailing.equalToSuperview().inset(16)
}
}
private func configureUI() {
contentView.backgroundColor = UIColor(named: "backGroudColor")
selectionStyle = .none
}
}
Lap타임 계산에 필요한 프로퍼티들을 추가했다.
lap타임을 계산하기 위한 스톱워치 lapStopwatch, 앞 기록과의 차이를 담기 위한 diffTime, diffTableViewData 세가지 프로퍼티를 추가했다.
아래 코드는 테이블뷰 데이터 구성에 필요한 UITableViewDataSource 부분이다.
Cell 안의 라벨에 각 해당하는 값을 할당하도록 작성했다.
// MARK: - TableView DataSource
extension StopwatchViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return lapTableViewData.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: StopwatchCell.identifier, for: indexPath) as? StopwatchCell else { return UITableViewCell() }
// 최신 랩타임이 맨 위에 표시되도록 랩 번호 계산
let lapCount = lapTableViewData.count - indexPath.row
// 랩 기록
cell.lapLabel.text = "Lap \(lapCount)"
// 실제 기록
cell.recordLabel.text = "\(lapTableViewData[lapCount-1])"
// 앞 기록과의 차이
cell.diffLabel.text = "\(diffTableViewData[lapCount-1])"
return cell
}
}
우선 저장한 랩타임들을 최신 순으로 보여주기 위해 lapCount를 사용하여 테이블뷰의 IndexPath를 역순으로 계산했다.
(랩타임 개수 - 현재 행 번호) → ex) 5개의 랩타임이 저장되어 있고 현재 행 번호가 0이라면 5-0= 5
또한 record의 값은 랩타임 기록이 저장되어 있는 lapTableViewData의 값을 가져오고
diff의 값은 각 랩타임과 이전 랩타임 간의 시간 차이가 저장되어 있는 diffTableViewData의 값을 가져온다.
Lap 타임 계산하기
앞의 기록과의 차이를 구하는 부분이 조금 어려웠는데 아래에서 확인해보자.
mainStopwatch에서 마이너스 연산을 통해 Lap타임을 계산하기보다 별도의 lapStopwatch를 만들어서 사용하는 게 더 간편할 것 같아 코드 구성 방향을 변경했다.
우선 로직을 생각해보면,
Lap 버튼을 눌렀을 때 기록이 diffTableViewData에 저장이 되어야 하고 lapStopwatch가 초기화 되어야 한다!
아래 코드는 랩타임 기능이 추가된 Lap버튼 동작 코드이다.
@objc private func lapResetButtonPressed() {
// 시간이 멈춰있을 때 -> 버튼 누르면 reset 되어야 함
if !isPlay {
resetMainTimer()
resetLapTimer()
lapResetButton.isEnabled = false
changeButton(lapResetButton, title: "Lap", titleColor: UIColor.gray)
}
// 시간이 가고 있을 때 -> 테이블 뷰 셀의 데이터를 추가
// Lap 버튼을 눌렀을 때 lapStopwatch는 다시 reset이 되어야 함
else {
let timerLabelText = "\(minutesLabel.text ?? "00"):\(secondsLabel.text ?? "00"):\(milliSecondsLabel.text ?? "00")"
lapTableViewData.append(timerLabelText)
// diff 타임 배열에 추가해야함!
diffTableViewData.append(diffTime)
resetLapTimer()
unowned let weakSelf = self
lapStopwatch.timer = Timer.scheduledTimer(timeInterval: 0.01, target: weakSelf, selector: Selector.updateLapTimer, userInfo: nil, repeats: true)
// --> 타이머 생성 및 설정 0.01초마다 updateLapTimer 메서드를 호출
RunLoop.current.add(lapStopwatch.timer, forMode: RunLoop.Mode.common)
// --> 타이머를 현재 실행 루프에 추가(주기적으로 메서드가 호출), 없어도 실행은 됨
}
tableView.reloadData()
}
이때 이전 포스팅과 동일하게 Timer.scheduledTimer라는 메서드를 이용해서 타이머를 움직이도록 구현했다.
그리고 추가한 부분은 RunLoop.current.add(_:forMode:)는 타이머를 현재 실행 루프에 추가하는 메서드이다.
일반적으로 Timer.scheduledTimer 메서드로 생성한 타이머는 이미 RunLoop에 추가되어 있으므로 따로 추가하지 않아도 동작하지만 타이머 동작을 명시적으로 보여주고 다른 모드에서 타이머를 실행할 때는 필요할 수 있으므로 추가해줬다.
또한 Start버튼을 눌렀을 때도 lapStopwatch가 동작되어야 한다.
@objc private func startPauseButtonPressed() {
lapResetButton.isEnabled = true
changeButton(lapResetButton, title: "Lap", titleColor: UIColor.mainText)
// 시간이 멈춰있을 때 -> 버튼 누르면 시간이 흘러야 함
if !isPlay {
unowned let weakSelf = self
mainStopwatch.timer = Timer.scheduledTimer(timeInterval: 0.01, target: weakSelf, selector: Selector.updateMainTimer, userInfo: nil, repeats: true)
lapStopwatch.timer = Timer.scheduledTimer(timeInterval: 0.01, target: weakSelf, selector: Selector.updateLapTimer, userInfo: nil, repeats: true)
RunLoop.current.add(mainStopwatch.timer, forMode: RunLoop.Mode.common)
RunLoop.current.add(lapStopwatch.timer, forMode: RunLoop.Mode.common)
isPlay = true
changeButton(startPauseButton, title: "Stop", titleColor: UIColor.red)
}
// 시간이 흐를 때 -> 버튼 누르면 멈춰야 함
else {
mainStopwatch.timer.invalidate()
lapStopwatch.timer.invalidate()
isPlay = false
changeButton(startPauseButton, title: "Start", titleColor: UIColor.mainActive)
changeButton(lapResetButton, title: "Reset", titleColor: UIColor.mainText)
}
}
위에서 사용되는 초기화하는 resetTimer 함수와 시간을 업데이트하는 updateTimer 함수의 로직은 이전 포스팅의 함수와 동일하게 작성했다.
아래는 스톱워치 동작에 관한 함수의 익스텐션 전체 코드이다!
버튼의 스타일을 바꾸는 메서드도 만들어서 중복되는 코드를 줄였다.
// MARK: - Action Functions
extension StopwatchViewController {
private func changeButton(_ button: UIButton, title: String, titleColor: UIColor) {
button.setTitle(title, for: UIControl.State())
button.setTitleColor(titleColor, for: .normal)
button.layer.borderColor = titleColor.cgColor
}
private func resetTimer(_ stopwatch: Stopwatch, labels: [UILabel]) {
stopwatch.timer.invalidate()
stopwatch.counter = 0
for label in labels {
label.text = "00"
}
}
private func resetMainTimer() {
resetTimer(mainStopwatch, labels: [minutesLabel, secondsLabel, milliSecondsLabel])
lapTableViewData.removeAll()
tableView.reloadData()
}
private func resetLapTimer() {
lapStopwatch.timer.invalidate()
lapStopwatch.counter = 0
}
@objc func updateMainTimer() {
updateMainTimer(mainStopwatch, labels: [minutesLabel, secondsLabel, milliSecondsLabel])
}
@objc func updateLapTimer() {
updateLapTimer(lapStopwatch)
}
private func updateMainTimer(_ stopwatch: Stopwatch, labels: [UILabel]) {
stopwatch.counter += 0.01
let minutes = Int(stopwatch.counter / 60)
let seconds = Int(stopwatch.counter.truncatingRemainder(dividingBy: 60))
let milliseconds = Int((stopwatch.counter * 100).truncatingRemainder(dividingBy: 100))
labels[0].text = String(format: "%02d", minutes) // 분
labels[1].text = String(format: "%02d", seconds) // 초
labels[2].text = String(format: "%02d", milliseconds) // 밀리초
} // --> 밀리초로 계산하고 분과 초로 변환하는 방식
private func updateLapTimer(_ stopwatch: Stopwatch) {
stopwatch.counter = stopwatch.counter + 0.01
let minutes = Int(stopwatch.counter / 60)
let seconds = Int(stopwatch.counter.truncatingRemainder(dividingBy: 60))
let milliseconds = Int((stopwatch.counter * 100).truncatingRemainder(dividingBy: 100))
diffTime = String(format: "%02d", minutes) + ":" + String(format: "%02d", seconds) + ":" + String(format: "%02d", milliseconds)
}
}
// MARK: - Selector
fileprivate extension Selector {
static let updateMainTimer = #selector(StopwatchViewController.updateMainTimer)
static let updateLapTimer = #selector(StopwatchViewController.updateLapTimer)
}
기본적이지만 신경써야할 점은 테이블뷰에 기록을 추가할 때 꼭 reloadData를 해줘야한다는 점이다!
결과 화면을 확인해보자. 테이블뷰에 랩타임이 잘 출력되는 것을 확인할 수 있다 :)