Swift/데모 개발

[Swift] StopWatch 만들어보기 - Step 2

언클린 2020. 3. 7. 15:43
728x90

전 글에서 제작한 베이스에서 타임 기능 등을 추가해 타임 워치를 완성시켜보겠습니다. 

추가될 기능은 아래와 같습니다.

 

1. 시작과 기록의 상태를 관리하는 변수

2. 타이머

3. 테이블 뷰를 자동으로 스크롤시키기

 

그럼 시작해보겠습니다. 


1. State 설정

저는 시작 버튼으로 기록까지 기능을 구현할 것이기 때문에 상태를 지정해서 처리를 나눌 수 있도록 해보았습니다. 

그러기 위해서 enum을 사용해서 상태를 관리하는 것이 편합니다. 그리고 초기 설정을 지정!

/// 시작과 기록의 상태
    enum WatchStatus {
        case start
        case stop
    }

/// 버튼의 상태
    var watchStatus: WatchStatus = .start

2. 타이머 개시

이제 타이머를 실행해 보겠습니다.

타이머의 timeInterval은 0.001로 지정했습니다. (밀리 세컨드까지  구하기 위해...)

그리고 반복적으로 실행해야 하기 때문에 repeat은 true로 설정

 /// 타이머 설정
    var timer: Timer!

----------------------- 생략 ------------------------

@IBAction func didTappedStartButton(_ sender: UIButton) {
        switch self.watchStatus {
        case .start:
            self.watchStatus = .stop // 시작 상태가 되었기 때문에 상태를 바꾸어 줍니다.
            self.timer = Timer.scheduledTimer(timeInterval: 0.001,
                                              target: self,
                                              selector: #selector(timeUp),
                                              userInfo: nil,
                                              repeats: true)
        case .stop: // 기록 기능
            let hour = self.hourLabel.text ?? ""
            let minute = self.minuteLabel.text ?? ""
            let second = self.secondLabel.text ?? ""
            let milliSecond = self.milliSecondLabel.text ?? ""

            let record = "\(hour) : \(minute) : \(second) : \(milliSecond)"
            self.recordList.append(record)

            self.timeTableView.reloadData()
        }

@objc
    private func timeUp() {
           // TODO: 라벨 표시 갱신
    }

stop상태일 때는 기록을 해주기 때문에 간단하게 현재 표시 중인 라벨의 값 들을 조합해서 배열에 넣어주는 작업을 하겠습니다.

넣었으면 테이블 뷰를 갱신해주는 것은 필수!

3. 타이머 계산

다음으로는 계산식이 필요합니다...

본격적으로 타이머가 작동할 때 시간에 맞추어 라벨의 표시가 바뀌는 것을 보실 수 있습니다.

fmod는 전자의 인수를 후자의 인수로 나누는 기능을 제공합니다. 반환 타입이 Double이기 때문에 Int로 강제 형 변환도 같이 진행해주겠습니다. 

floor는 간단히 설명드리자면 소수점 이하를 버려 버는 처리를 합니다. 

/// 시작 시간
    var startTime =  Date() // 우선 변수를 하나 더 추가합니다.

----------------------- 생략 ------------------------

@IBAction func didTappedStartButton(_ sender: UIButton) {
        switch self.watchStatus {
        case .start:
                                      ...

                     self.startTime = Date() // 스톱 워치 시작 시 그 시간을 지정해줍니다. 후에 계산할 때 사용하게 됩니다.

                                      ...

----------------------- 생략 ------------------------

@objc
    private func timeUp() {
        let timeInterval = Date().timeIntervalSince(self.startTime)

        let hour = (Int)(fmod((timeInterval/60/60), 12)) // 분을 12로 나누어 시를 구한다
        let minute = (Int)(fmod((timeInterval/60), 60)) // 초를 60으로 나누어 분을 구한다
        let second = (Int)(fmod(timeInterval, 60)) // 초를 구한다
        let milliSecond = (Int)((timeInterval - floor(timeInterval))*1000)

        self.hourLabel.text = String(format:"%02d", hour) 
        self.minuteLabel.text = String(format:"%02d", minute)
        self.secondLabel.text = String(format:"%02d", second)
        self.milliSecondLabel.text = String(format:"%03d", milliSecond)
    }

%02d 와 %03d 는 앞의 0을 포함시키는 것을 나타냅니다. 예를 들어 2를 %3d로 나타내게 되면 002가 됩니다.

4. 기록 추가

타임에 관련된 기능은 끝이 났습니다. 이제 reset버튼을 누를 시 테이블 뷰에 그 기록을 삭제하는 처리를 해보겠습니다. 

한 번 더 누를 시는 모든 스톱 워치의 기능을 취소하는 기능도 같이 진행하겠습니다. 

@IBAction func didTappedResetButton(_ sender: UIButton) {
        if self.recordList.isEmpty { // 배열이 비어 있을 시 즉, 모든 기능을 취소한다.
            self.watchStatus = .start // 상태도 원래의 상태로 돌립니다.
            self.timer.invalidate() // 타이머 종료

            self.hourLabel.text = "00"
            self.minuteLabel.text = "00"
            self.secondLabel.text = "00"
            self.milliSecondLabel.text = "000" // 모든 라벨을 다시 돌립니다. 
        } else { // 타이머는 계속 진행, 테이블 뷰만 리셋한다.
            self.recordList.removeAll()
            self.timeTableView.reloadData()
        }
    }

5. 버튼 갱신

거의 끝이 보입니다. 하지만 여기서 한 가지 놓치고 진행한 것이 있습니다. 버튼의 상태는 바꾸지만 버튼의 이름은 바꾸지 않았던 것입니다. 별로 어려운 것이 아니니 빠르게 진행해 줍니다. 

뭔가 그냥 추가하기보다 한 가지 확장을 더 추가해서 간단히 함수화를 해주겠습니다. 

후에 테이블 뷰 스크롤 함수도 여기 추가된 확장에 추가하겠습니다.

@IBAction func didTappedStartButton(_ sender: UIButton) {
        switch self.watchStatus {
        case .start:
                                      ...

                     self.watchStatus = .stop 
                     self.setButton(_ string: "Stop") //  상태를 변경할 때 같이 설정해 주면 됩니다.

                                      ...

----------------------- 생략 ------------------------

@IBAction func didTappedResetButton(_ sender: UIButton) {
        if self.recordList.isEmpty { 
                                      ...

                     self.watchStatus = .start
                     self.setButton(_ string: "Start")

                                      ...

----------------------- 생략 ------------------------

extension ViewController {
    private func setButton(_ string: String) {
        self.startButton.setTitle(string, for: .normal)
        self.startButton.setTitle(string, for: .highlighted)
    }
}

6. 테이블 뷰 스크롤 갱신

기록이 테이블 뷰의 높이를 넘어가 버리면 보이지 않게 되어 사용자가 집적 손으로 스크롤하면서 확인을 해야 하고 어디까지 진행되어 있는지 보기가 좀 불편할 수 도 있기 때문에 자연스럽게 자동으로 스크롤이 갱신할 수 있도록 처리해 주겠습니다. 위에 만든 확장에 함수화 하여 구현해보겠습니다. 

이 기능은 기록을 추가해 주는 부분에 넣어주면 됩니다. 

case .stop: // 기록 기능

----------------------- 생략 ------------------------    

            self.timeTableView.reloadData()
            self.tableViewBottomScroll() // 테이블 뷰가 갱신 한 뒤 실행해 준다.
        }

extension ViewController {

----------------------- 생략 ------------------------    

    private func tableViewBottomScroll() {
        let numberOfSections = self.timeTableView.numberOfSections
        let numberOfRows = self.timeTableView.numberOfRows(inSection: numberOfSections - 1)
        let lastPath = IndexPath(row: numberOfRows - 1, section: numberOfSections - 1) 
        self.timeTableView.scrollToRow(at: lastPath, at: .bottom, animated: true)
    }
}

6. 실행해보기

이제 모든 작업이 완료되었습니다. 한 번 실행해보면서 생각한 대로 잘 움직이는지 확인해보겠습니다. 

 

7. 마무리

이로서 스톱 워치를 만들어 보았습니다. 여기서 디자인만 많이 가꾼다면 하나의 멋진 앱이 될 수 있을 것이라 생각합니다.

많은 도움이 되었으면 좋겠습니다.

지적 사항이나 질문은 댓글에 부탁드립니다.


환경 

Xcode 11.3.1

swift 5

 

728x90