https://youtu.be/uDPZ4EQI25w

이전 추가영상에 이어 완성입니다.

이전 영상까지 합하면 2시간정도 분량이네요.

 

튜토리얼 이라기보단 튜토리얼로 올린 내용을 완성하기까지의 영상이 되어버렸습니다.

 

중간중간 계획세우고 하는게 강의같아보이겠고 

설명도 넣으면 좋겠지만

 

아직까진 그렇게 할 생각은 없네요

 

다음 영상은 서버 연동해서 간단한 정보저장 및 불러오기를하는것을 만들어볼까합니다.

 

배경음이라도 넣는방법을 찾아봐야겠네요

 

 

영상버전 풀 소스코드는 아래링크입니다.

https://github.com/wiwi-git/Stopwatch-re001

 

스톱 워치 만들기 (1) : https://wiwi-pe.tistory.com/34

스톱워치 만들기 (2) : https://wiwi-pe.tistory.com/35

스톱워치 만들기 (3) : https://wiwi-pe.tistory.com/36

스톱워치 만들기 (4) : https://wiwi-pe.tistory.com/37

스톱워치 만들기 (5) : https://wiwi-pe.tistory.com/38

스톱워치 만들기 (6) : https://wiwi-pe.tistory.com/39

스톱워치 만들기 (7) : https://wiwi-pe.tistory.com/40


이번편은 시간이 흐르고 멈추게 할겁니다.

 

이런식으로 말하니 그야 말로 마법 같네요 

 

스톱워치에 시작버튼을 누르면 위의 시간표시부분이

매 시간마다 바뀌어져야하는데 이런 반복적인 작업을 하려고할때 바로

 

Timer라는 오브젝트를 사용합니다.

아래 링크는 타이머에 대한 도큐먼트 페이지에요

https://developer.apple.com/documentation/foundation/timer

 

Timer - Foundation | Apple Developer Documentation

A timer that fires after a certain time interval has elapsed, sending a specified message to a target object.

developer.apple.com

 

class func scheduledTimer(timeInterval: TimeInterval, target: Any, selector: Selector, userInfo: Any?, repeats: Bool) -> Timer

Creates a timer and schedules it on the current run loop in the default mode.

 

타이머 객체를 생성할때 위의 형식으로 생성하게 됩니다.

timeInterval은 몇초인지에 대해 적는부분입니다

다른 언어는 마이크로초를 많이 쓰는듯합니다만

여기는 초단위로 쓸겁니다

타겟, 셀릭터는 이전에 봣던 파라미터입니다. 같은거라 생각하시면되요

 

userInfo부분은 타이머 만료될때까지 타이머에 대한 정보를 담는다는 설명이있는데

이게 다른곳에서쓰인(이 튜토리얼에서는 다루지않을 Notification)것과같은 역할을 하는지는 모르겠습니다.

써보질않아서;;

 

리핏은 말그대로 반복여부입니다 저희는 0.1초마다 반복하게 할것이니 당연히 truer겠죠?

 

타이머 생성하는 함수는 다른파라미터를 가진애도있습니다.

 

class func scheduledTimer(withTimeInterval interval: TimeInterval, repeats: Bool, block: @escaping (Timer) -> Void) -> Timer

우리는 이것을 사용하여 타이머를 생성할것이며

파라미터가 많이 줄었고 block이라는게 생겼죠?

위의 selector로 지정해줄 함수를 바로 타이머를 생성해주면서 적어주는 곳입니다.

클로져함수에대해 설명을 자세히하고싶기도합니다만

이건 그냥 다른곳에서 검색해주세요 ㅠ

 

함수바로 옆에 {}로 이 함수에 추가될 기능을 바로 적어주는용도로 사용한다 정도로 생각해주세요

 

 

 

 

그럼 타이머를 넣어봅시다

 

우선

@IBOutlet weak var resetButton: UIButton!

라인 아래에 다음과같은 정의해줍시다

 

var mainTimer:Timer?

var timeCount = 0

메인타이머라는 이름의 타이머를 이 클래스 전체에서 접근가능하게 선언해주세요

 

현재는 생성이안된상태 Nil값이 들어가게 타입을 Timer? 라고 옵셔녈변수로 선언해주세요.

타임카운트는 타이머가 실행된 횟수를 누적시킬겁니다

 

그리고 이제

startAction 함수를 수정할꺼에요

stopButton.isEnabled = true 라인 아래에 다음과같이 적어줍시다

 

mainTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true, block: { (_) in

            self.timeCount += 1

            DispatchQueue.main.async {

                let timeString = self.makeTimeLabel(count: self.timeCount)

                self.timeLabel.text = timeString.0

                self.decimalSecLabel.text = ".\(timeString.1)"

            }

        })

 

지금 까지의 튜토리얼을 그대로 따라하셧다면 startAction은 다음과같아집니다

 

    func startAction() {
        print("start")
        startButton.isEnabled = false
        checkButton.isEnabled = true
        stopButton.isEnabled = true
        mainTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true, block: { (_) in
            self.timeCount += 1
            DispatchQueue.main.async {
                let timeString = self.makeTimeLabel(count: self.timeCount)
                self.timeLabel.text = timeString.0
                self.decimalLabel.text = ".\(timeString.1)"
            }
        })
    }

block 파라미터 값 부분이 {}로 들어간것을 잘 봐주세요 함수의 끝부분이 ) 로 막아져야합니다 결국 이건 Timer의 생성하는 함수였다는겁니다.

self에 대해 이야기할 기회가 없었던거같은데 

self는 자바의 this같은겁니다

self.timeCount 는 현 클래스의 내부에 있는 timeCount를 가르킵니다.

이와 같은게 필요한이유는 함수 내부에 선언될 변수와 이미 외부에 선언되있는것에대해 구분이 안되기에 정확한 지칭을 하기위해 사용됩니다.

그래서 클로져같은경우 이게 내부에 선언되고있긴하지만 바로 쓰는것과 조금 다른 공간에서 생성되고있다고 봐야합니다.

그래서 내부와 외부에대한 정확한 구분이 필요해 self 를 쓰지않으면 쓰지않았다고 오류가 나게됩니다.

물론 내부에 똑같은 이름을 가진 변수를 가르킬때는 self를 붙이지않습니다.

 

위에서 추가한내용을 그대로 말로 풀이하자면 

start버튼이 눌려지면(startAction이 호출되면)  메인타이머에 타이머를 생성하여 넣습니다.

이 타이머는 0.1초간격으로 반복하게되며 실행될때마다 timeCount에 1을 더하고, 이를 timeLabel과 decimalLabel에 표현해줍니다.

라고 되는겁니다.

 

위 처럼 startAction을 작성하셨으면 

xcode에서는 self.makeTimeLabel(count:)는 정의되어있지않아! 라며 빨간줄이 뜨고있을겁니다.

 

그럼 이제 makeTimeLabel을 만들어주죠 이걸 만드면서 timeString.0 timeString.1에대해 설명이 나올겁니다.

 

func resetAction(){} 다음행에 아래 코드를 적어줍시다.

 

    func makeTimeLabel(count:Int) -> (String,String) {
        //return - (TimeLabel, decimalLabel)
        let decimalSec  = count % 10
        let sec = (count / 10) % 60
        let min = (count / 10) / 60
        
        let sec_string = "\(sec)".count == 1 ? "0\(sec)" : "\(sec)"
        let min_string = "\(min)".count == 1 ? "0\(min)" : "\(min)"
        return ("\(min_string):\(sec_string)","\(decimalSec)")
    }

count가 들어가면 이는 카운드 1당 0.1초임을 잘 기억해두셔야합니다.

 

decimalSec = 소수점 0.1초 자리와

초단위, 그리고 분단위를 구분해줍니다.

 

그리고 여기서 나오는 물음표? ~~ : ~~ 부분은 삼항 연산자 부분입니다.

 

조건 ? 참 : 거짓 형식으로 이루져있습니다.

조건이 참이면 참영역이,

조건이 거짓이면 거짓영역이 반환됩니다

 

sec을 문자열로 바꿧을때 한자리면 앞에 0이 추가된 문자열이 아니면 바로 문자열로 바뀐 문자열이 sec_string에 넣어라 라는겁니다.

 

그리고 함수의 정의부에 반환타입이 두개의 문자열값을 반환하고있습니다.

swift에서는 이러한일이 가능하며 당연히 받는쪽에서도 두개의 영역을 준비해야 이 값을 받아볼 수 있습니다.

그리고 이러한것을 지정해줄때 0, 1 로 지칭해줄수 있습니다.

 

이제다시 startAction쪽을 보면 

let timeString = self.makeTimeLabel(count: self.timeCount)

self.timeLabel.text = timeString.0

self.decimalLabel.text = ".\(timeString.1)"

이 makeTimeLabel에서 두개의 문자열을 반환하는데 이게 timeString에 저장되게됩니다

timeString은 당연히 두개의 값을 가지고 있겠죠?

그래서 첫번째값인 timeString.0을 분,초를 나타내는 라벨값으로

두번째값인 timeString.1을 소수점을 나타내는 라벨값을 넣어준겁니다.

 

startAction의 세세한 내부는 다 봤는데

DispatchQueue.main.async { } 부분에 대한 이야기를 빼먹었네요

이것을 해주지않았을경우도 딱히 큰문제는 발생하지않습니다.

다만 앱화면의 갱신이 있을때 같이 화면갱신이 일어나기에 소수점단위가 바로바로 갱신이 안될수도 있습니다.

이를 메인스레드에서 실행은 하되 async하게 실행해주라는것에대한 지정을해줍니다.

 

저도 아직 GCD에 대해서는 영 감이 안잡히네요

더욱 자세히 알고싶으신분은 GCD에 대해 공부해보시는걸 추천드립니다!

 

아무튼 이제 실행을 해볼까요

 

 

 

처음 스타트를 누르면 정상적으로 잘 돌아가는것을 볼 수 있습니다 ㅎ

 

그런데 이제 stop을 누르고 start를 누르고 반복을하면!

 

시간이 빠르게 흐르게됩니다

 

마법같죠?

 

실제로는 타이머는 하나에다가 생성해주는데 왜 타이머가 스타트누를때마다 중첩되어 늘어날까요?

 

그답은 저도 모르기에 구글신에게 패스를.....

 

이러한 상황을 방지해주기위해 stop을 누를때 이 타이머 자체를 죽여줘야합니다.

stopActon을 아래과같이 변경해주세요

    func stopAction() {
        print("stop")
        mainTimer?.invalidate()
        mainTimer = nil
        startButton.isEnabled = true
        checkButton.isEnabled = false
        stopButton.isEnabled = false
    }
    

타이머의 invalidedate를 호출해주고

혹시 모르니 mainTimer를 다시 nil값을 지정해줍시다.

 

func invalidate()

Stops the timer from ever firing again and requests its removal from its run loop.

 

invalidate는 위의 설명과같이 루프중인 스레드에서 제거를 하고 요청하는겁니다.

이제 start를 누르면 생성되는 타이머는 stop을 누르면 제거가 되겠네요 ㅎ

돌려봅시다

 

 

잘 동작하네요 ㅎ

 

이제 reset버튼을 동작하도록 변경해 봅시다

 

현재 스타트를하면 시간이흐르고 stop을 누르면 정지됩니다.

리셋을 누르면 이 숫자가 다시 0이 되고 start로생성된 타이머는 멈춰져야 겠죠?

 

타이머를 지우는내용은 stopAction에서 햇던것과

라벨들을 다시 초기에 넣었던 내용으로 지정해주면됩니다

 

restetAction이 아래코드와 같으면됩니다

 

    func resetAction() {
        print("reset")
        mainTimer?.invalidate()
        mainTimer = nil
        timeCount = 0
        timeLabel.text = "00:00"
        decimalLabel.text = ".0"
        startButton.isEnabled = true
        checkButton.isEnabled = false
        stopButton.isEnabled = true
    }

우선 타이머를 제거해주고

타이머로인해 꾸준히 누적되던 count를 0으로 지정해주고 라벨을 다시 0으로 해주면됩니다.

위에서 사용했던 makeLabel을 사용해도 되나 굳이 이미 결과를 아는것을 연산해줄필요는 없다 생각해 바로 지정해줬습니다 ㅎ

 

그리고 실행하면 !

 

잘동작하네요~~~

이 튜토리얼의 끝이보입니다 ㅠㅠ

 

정말 간단한 시리즈로 만들고싶었는데 어느새 8번째 포스트...

엔딩이 보이는 이 튜토리얼이지만  이제 가장 중요한 기능이 남았습니다

Check버튼기능이죠!

랩타임이 찍혀야해요

메인기능이죠!

 

이게없으면 그냥 이건 아무런 쓸모도없는 수를 세는 기계일뿐입니다

이거에대해서는 다음포스트에서 다루겠습니다.

그럼 다들 여기까지 고생하셨어요 조금만더 가면 앱완성입니다 ^^

+ Recent posts