뽀모도로 타이머 앱

Marco Kang·2022년 1월 5일
0

swift

목록 보기
2/3
post-thumbnail

강좌를 보고 공부한 앱 만들기 정리.

기능

타이머 앱이다.
시간이 흐르는 동안 위에 있는 토마토 이미지가 빙글빙글 돌고, 시간이 멈춘 동안에는 이미지도 멈춘다.
시작을 누르면 시간이 카운트다운 된다.

취소를 누르면 타이머가 리셋된다.
시간이 다 되면 짧은 알람 소리가 난다.

화면 구성

단일 뷰 컨트롤러로 구성한다.
맨 위에 이미지 뷰, 그 아래에 타이머, 그리고 같은 공간에 카운트다운되는 타이머 라벨과 프로그레스바를 겹쳐 놓는다.
바로 아래에 취소 버튼과 시작(또는 일시정지와 토글 되는) 버튼을 놓는다.

소스

개발 언어는 스위프트5

라이브러리

import UIKit
import AudioToolbox

화면을 구성해야 하는 코드이므로 UIKit, 그리고 알람소리를 위한 AudioToolbox를 import 한다.

타이머 상태 enum

enum TimerStatus {
    case start
    case pause
    case end
}

ViewController 클래스 안의 함수들

전체 변수

    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()
        }
    }
}
profile
데이터 사이언티스트를 향해.

0개의 댓글

관련 채용 정보