강좌를 보고 공부한 앱 만들기 정리.
타이머 앱이다.
시간이 흐르는 동안 위에 있는 토마토 이미지가 빙글빙글 돌고, 시간이 멈춘 동안에는 이미지도 멈춘다.
시작을 누르면 시간이 카운트다운 된다.
취소를 누르면 타이머가 리셋된다.
시간이 다 되면 짧은 알람 소리가 난다.
단일 뷰 컨트롤러로 구성한다.
맨 위에 이미지 뷰, 그 아래에 타이머, 그리고 같은 공간에 카운트다운되는 타이머 라벨과 프로그레스바를 겹쳐 놓는다.
바로 아래에 취소 버튼과 시작(또는 일시정지와 토글 되는) 버튼을 놓는다.
개발 언어는 스위프트5
import UIKit
import AudioToolbox
화면을 구성해야 하는 코드이므로 UIKit, 그리고 알람소리를 위한 AudioToolbox를 import 한다.
enum TimerStatus {
case start
case pause
case end
}
var duration: Int = 60 // 타이머에 설정된 시간을 초로 저장하는 프로퍼티
@IBOutlet weak var imageView: UIImageView! // 애니메이션을 위한 이미지 outlet 변수
var timerStatus: TimerStatus = .end // 처음 타이머 상태 = 시작해야하니 끝부터 설정.
var timer: DispatchSourceTimer? // 타이머
var currentSeconds = 0 // 현재 초
@IBOutlet weak var cancelButton: UIButton! // 취소 버튼
@IBOutlet weak var toggleButton: UIButton! // 시작-일시정지 버튼
@IBOutlet weak var timerLabel: UILabel! // 카운트다운 라벨
@IBOutlet weak var datePicker: UIDatePicker! // 시간 선택을 위한 데이터피커
@IBOutlet weak var progressView: UIProgressView! // 프로그레스 바
private func configureToggleButton(){
self.toggleButton.setTitle("시작", for: .normal)
self.toggleButton.setTitle("일시정지", for: .selected)
}
시작-일시정지 버튼의 표시하는 글자를 지정한다. .normal 상태일 때 '시작'으로, .selected 상태일 때 '일시정지'라고 표시한다.
private func startTimer() {
if self.timer == nil { // 타이머가 메모리에 부재 시 타이머 소스를 생성하고 스케줄을 등록한다.
self.timer = DispatchSource.makeTimerSource(flags: [], queue: .main)
self.timer?.schedule(deadline: .now(), repeating: 1) // 1초마다 반복되는 걸 바로 시작
self.timer?.setEventHandler(handler: { [weak self] in
guard let self = self else { return }
self.currentSeconds -= 1
// 시간, 분, 초
let hour = self.currentSeconds / 3600
let minutes = (self.currentSeconds % 3600) / 60
let seconds = (self.currentSeconds % 3600) % 60
self.timerLabel.text = String(format: "%02d:%02d:%02d", hour, minutes, seconds) // 60:60:60의 형태로 라벨 포맷 지정
self.progressView.progress = Float(self.currentSeconds) / Float(self.duration) // progressview의 값은 소수자리여야 한다.
UIView.animate(withDuration: 0.5, delay: 0, animations: { // 이미지를 절반 돌린다.
self.imageView.transform = CGAffineTransform(rotationAngle: .pi) // 뷰 사이즈 계산 안 하고 2D 애니 가능
})
UIView.animate(withDuration: 0.5, delay: 0.5, animations: { // 이미지를 원상태로 돌린다.
self.imageView.transform = CGAffineTransform(rotationAngle: .pi * 2)
})
if self.currentSeconds <= 0 {
// 타이머 종료
self.stopTimer()
AudioServicesPlaySystemSound(1005) // 시간이 다 되면 1005번 소리를 낸다.
}
})
self.timer?.resume()
}
}
private func stopTimer(){
if self.timerStatus == .pause {
self.timer?.resume()
}
self.timerStatus = .end
self.cancelButton.isEnabled = false
UIView.animate(withDuration: 0.5, animations: {
self.timerLabel.alpha = 0
self.progressView.alpha = 0
self.datePicker.alpha = 1
self.imageView.transform = .identity
})
self.toggleButton.isSelected = false
self.timer?.cancel()
self.timer = nil // 멈추면 메모리 해제 해 줘야 함
}
@IBAction func tapCancelButton(_ sender: UIButton) {
switch self.timerStatus {
case .start, .pause:
self.stopTimer()
default:
break
}
}
타이머가 suspended 상태일 때 타이머를 멈춰버리게 되면, 남은 이벤트들이 존재하기 때문에 에러가 발생한다. 그래서 resume을 시켜서 에러가 발생하지 않게 해야 하는 게 resume을 실행한 이유다.
@IBAction func tapToggleButton(_ sender: UIButton) {
self.duration = Int(self.datePicker.countDownDuration)
switch self.timerStatus {
case .end:
self.currentSeconds = self.duration
self.timerStatus = .start
UIView.animate(withDuration: 0.5, animations: {
self.timerLabel.alpha = 1
self.progressView.alpha = 1
self.datePicker.alpha = 0
})
self.toggleButton.isSelected = true
self.cancelButton.isEnabled = true
self.startTimer()
case .start:
self.timerStatus = .pause
self.toggleButton.isSelected = false
self.timer?.suspend()
case .pause:
self.timerStatus = .start
self.toggleButton.isSelected = true
self.timer?.resume()
}
}
시작버튼을 누르면 데이터피커에서 선택한 시간을 duration으로 설정하고 currentSeconds 변수에 대입한다.
timerStatus가 .end일 때, 즉 지금 타이머가 작동하고 있지 않을 때에는 위의 동작을 실행하고 타이머 상태를 start로 변경한다. 타이머 라벨과 프로그레스 뷰 alpha값을 1로 주고 datePicker를 alpha 0로 해서 보이지 않게 만든다.
//
// ViewController.swift
// Pomodoro
//
import UIKit
import AudioToolbox
enum TimerStatus {
case start
case pause
case end
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.configureToggleButton()
}
var duration: Int = 60 // 타이머에 설정된 시간을 초로 저장하는 프로퍼티
@IBOutlet weak var imageView: UIImageView! // 애니메이션을 위한 이미지 outlet 변수
var timerStatus: TimerStatus = .end // 처음 타이머 상태 = 시작해야하니 끝부터 설정.
var timer: DispatchSourceTimer? // 타이머
var currentSeconds = 0 // 현재 초
@IBOutlet weak var cancelButton: UIButton! // 취소 버튼
@IBOutlet weak var toggleButton: UIButton! // 시작-일시정지 버튼
@IBOutlet weak var timerLabel: UILabel! // 카운트다운 라벨
@IBOutlet weak var datePicker: UIDatePicker! // 시간 선택을 위한 데이터피커
@IBOutlet weak var progressView: UIProgressView! // 프로그레스 바
// private func setTimerInfoViewVisible(isHidden: Bool) {
// self.timerLabel.isHidden = isHidden
// self.progressView.isHidden = isHidden
// }
private func configureToggleButton(){
self.toggleButton.setTitle("시작", for: .normal)
self.toggleButton.setTitle("일시정지", for: .selected)
}
private func startTimer() {
if self.timer == nil { // 타이머가 메모리에 부재 시 타이머 소스를 생성하고 스케줄을 등록한다.
self.timer = DispatchSource.makeTimerSource(flags: [], queue: .main)
self.timer?.schedule(deadline: .now(), repeating: 1) // 1초마다 반복되는 걸 바로 시작
self.timer?.setEventHandler(handler: { [weak self] in
guard let self = self else { return }
self.currentSeconds -= 1
// 시간, 분, 초
let hour = self.currentSeconds / 3600
let minutes = (self.currentSeconds % 3600) / 60
let seconds = (self.currentSeconds % 3600) % 60
self.timerLabel.text = String(format: "%02d:%02d:%02d", hour, minutes, seconds) // 60:60:60의 형태로 라벨 포맷 지정
self.progressView.progress = Float(self.currentSeconds) / Float(self.duration) // progressview의 값은 소수자리여야 한다.
UIView.animate(withDuration: 0.5, delay: 0, animations: { // 이미지를 절반 돌린다.
self.imageView.transform = CGAffineTransform(rotationAngle: .pi) // 뷰 사이즈 계산 안 하고 2D 애니 가능
})
UIView.animate(withDuration: 0.5, delay: 0.5, animations: { // 이미지를 원상태로 돌린다.
self.imageView.transform = CGAffineTransform(rotationAngle: .pi * 2)
})
if self.currentSeconds <= 0 {
// 타이머 종료
self.stopTimer()
AudioServicesPlaySystemSound(1005) // 시간이 다 되면 1005번 소리를 낸다.
}
})
self.timer?.resume()
}
}
private func stopTimer(){
if self.timerStatus == .pause {
self.timer?.resume()
}
self.timerStatus = .end
self.cancelButton.isEnabled = false
UIView.animate(withDuration: 0.5, animations: {
self.timerLabel.alpha = 0
self.progressView.alpha = 0
self.datePicker.alpha = 1
self.imageView.transform = .identity
})
self.toggleButton.isSelected = false
self.timer?.cancel()
self.timer = nil // 멈추면 메모리 해제 해 줘야 함
}
@IBAction func tapCancelButton(_ sender: UIButton) {
switch self.timerStatus {
case .start, .pause:
self.stopTimer()
default:
break
}
}
@IBAction func tapToggleButton(_ sender: UIButton) {
self.duration = Int(self.datePicker.countDownDuration)
switch self.timerStatus {
case .end:
self.currentSeconds = self.duration
self.timerStatus = .start
UIView.animate(withDuration: 0.5, animations: {
self.timerLabel.alpha = 1
self.progressView.alpha = 1
self.datePicker.alpha = 0
})
self.toggleButton.isSelected = true
self.cancelButton.isEnabled = true
self.startTimer()
case .start:
self.timerStatus = .pause
self.toggleButton.isSelected = false
self.timer?.suspend()
case .pause:
self.timerStatus = .start
self.toggleButton.isSelected = true
self.timer?.resume()
}
}
}