앱 개발 공부 두 번째 날 — Egg Timer

JJ
13 min readAug 11, 2020

--

주의! 프로그래밍 초보자가 보기엔 다소 어려울 수 있음

본 글은 Udemy 의 iOS 13 & Swift 5 강좌 내용을 기초로 작성되었습니다.
https://www.udemy.com/course/ios-13-app-development-bootcamp/

오늘은 날 잡아서 안젤라 누님이 혹독한 트레이닝을 시켜주셨다.
역시 편한 건 첫날 밖에 없었어..

엄청난 개념들이 쏟아지니 마음을 단단히 먹어야 한다.

크.. 당장 써먹어도 손색이 없을 듯한 달걀 타이머라니! 기대가 된다.
코드는 다음 링크로

clone 으로 git 코드를 가져온 다음 storyboard 를 열면 화면이 좀 다르다..? 해야할 게 좀 있다는 말.

먼저 화면을 똑같이 만들어보자.

Soft, Medium, Hard 글자를 나오게 하려면 배치 순서만 바꾸면 된다. 원래 이미지에는 있던게 버튼 컴포넌트에 가려져서 안나오는 것이다.

Egg Stack View 에서 Soft Egg ImageView 를 그림처럼 젤 위로 올리자. Medium, Hard 도 마찬가지. 글자는 나오게 했고 이제 밑에 있는 progress bar 를 만들어야 한다.

오른쪽 위에 + 를 눌러서 컨트롤러 추가 창이 뜨면 prog.. 똑똑한 놈이 알아서 내가 뭘 찾는지 표시해 준다. 쟤를 화면 아랫도리에 끌어다 놓고.

반드시 왼쪽 창에서 Progress Bar 를 선택한 후 작업한다. 안그러면 Timer View 가 선택되서 망한다.

왼쪽, 오른쪽을 0으로 만들고 Height 를 5로 해서 Bar 를 좀 두껍게 만든다음 Add 를 누른다. 나도 두꺼운 게 좋아.

Vertically in Container 를 선택하고 Add.

Property 창에서 Progress Tint 와 Track Tint 를 노랑과 회색으로 바꾼다. 이건 뭐 취향에 따라 해도 상관없을듯.

그러면 디자인은 끝났다. 코딩의 시간.

먼저 Progress Bar 하고 맨 위에 Label을 조지..가 아니고 조작할 거기 때문에 변수로 만들어 준다. Ctrl 누르고 컨트롤러를 클릭해서 ViewController.swift 파일로 델고 오면 만들어지는 건 알고 있겠지..

Label 은 titleLabel, ProgressBar 는 progressBar 라고 이름을 정하자.

그리고 필요한 변수를 좀 만들어야 하는데.. 먼저 Soft, Medium, Hard 각각의 익히는 시간을 지정한다. 이걸 Dictionary 로 생성했다. 굳이. 초보자를 프로로 만드려는 안젤라 누님의 빅픽쳐를 느낄 수 있었다. 그리고 Dictionary 에 대해 일장연설이…. 간단하게 설명하면 이름 그대로 사전처럼 찾아볼 수 있도록 단어들(키)과 뜻(데이터)을 짝지어놓은 것이라고 할 수 있다. 키 이름은 자유롭게 만들 수 있다. 일단 Soft, Medium, Hard 키가 3개 필요하니까 각각 시간을 5분, 7분, 12분 으로 정한다.

let eggTimes = ["Soft": 5, "Medium": 7, "Hard": 12]

let 은 변하지 않는 상수를 만드는 키워드인 것도 알고 있겠지. 이 Dictionary 이름은 eggTimes 라고 정하고 3개의 목록을 만들었다.

그 다음 시간을 계산할 수 있게 해주는 Timer 가 필요한데 이건 친절하게도 Xcode 라이브러리에서 제공해준다. 사실 모든 프로그래밍 언어에서 제공해 준다. 제공 안해주는 언어가 있다면 당신을 엿먹이고 싶어서 만든 언어이다.

var timer = Timer()

Timer() 이건 Timer 클래스의 생성자를 호출해서 객체를 만드는 것이다.

그리고 시간 계산을 하기 위해서 변수 2개를 만든다.

var totalTime = 0
var secondsPassed = 0

변수를 만들었으면 반드시 초기화 해주는 습관을 갖자. 시간 값으로 쓸 것이기 때문에 0으로 지정한다.

재료 준비는 끝났다. 요리를 시작해 볼까.

먼저 Soft, Medium, Hard 버튼을 누지르면 벌떡 일해주는 함수를 만들어야 한다. 역시 컨트롤 누르고 버튼을 끌어다 코드 창에 놓으면 된다.

Connection 을 Action 으로 바꿔주고 Name 은 hardnessSelected 로 쓴다. 흠.. 단단함이 선택됨이라.

그리고 Medium, Hard 도 같은 함수로 처리할 것이기 때문에 어제 배운 스킬을 쓴다. 동그라미 끌어다 놓기.

문득 왜 rare, medium, welldone 을 쓰지 않았는지 궁금해진다.
어쨌든..

어떤 놈이 눌러졌는지 확인해야 하기 때문에 변수를 만든다.

let hardness = sender.currentTitle!

지난 시간에 잘 배웠다면 이게 뭔지 단박에 이해할 수 있다. 문제는 끝에 붙은 느낌표!
드디어 안젤라 누님이 칼을 뽑아드셨다. Optional 변수에 대해 설명을 해 주셨다.
간단하게 설명하면 C 나 Java 에서 Null Pointer 를 좀 더 안전하게(프로그램이 뒤지지 않게) 쓸 수 있도록 애플에서 특별히 만든 강려크한 기능이라고 한다. 아래 참고..

Null 도 모르고 Pointer 도 모르고 Null Pointer 도 모르면 이게 한국어인지 외계어인지 이해할 수 없을 것이다. 쉽게 가보자. 이런 상황을 상상해 보라.

모니터를 인터넷에서 주문했다. 하지만 배송중에 화면에 스크래치가 나진 않을지 걱정이 되서 직접 수령을 하고 싶었다. 그래서 전화를 했다. 누군가 전화를 받았다.

담당자 : “네. XXX 입니다. 무엇을 도와드릴까요?”
나 : “제가 모니터를 주문했는데 직접 수령하고 싶어요. 어디로 가면 되나요?”

(여기서 장소는 이미 상품 페이지에 나와있을 거라는 생각은 집어 치우자. 비유를 하고 있는 중이단 말이다..)

담당자 : “그러면 XXXX 로 오세요.”
나 : “한 시간 후에 갈 건데 수령 가능한가요?”
담당자 : “네, 가능합니다.”

나는 전화를 끊고 알려준 장소로 차를 끌고 갔다. 그리고 문을 열려고 하는데.. 문이 잠겨있다. 안을 들여다 보니 불도 꺼진 것 같다. 갑자기 불길한 느낌이 들며 머리가 새햐얘진다. 다시 전화를 해 보았다. 읭? 전화를 받지 않는다. 다시 전화를 했다. 뚜루루루…..

인터넷 주문 페이지로 들어가 보았다. 모니터를 주문한 내역이 사라져 있다. OO 마켓에 전화를 했다.

“제가 분명히 XXX 에서 모니터를 주문했는데요…”
“죄송하지만 확인되지 않습니다.”

여기까지 온 여러분의 반응은? 갑자기 두뇌가 10배 빠르게 회전할 것이다. 하지만 이 상황을 마주한 우리의 프로그램은.. 그냥 강제 종료되어 버린다. 대게 죽었다고 표현한다.

프로그래밍에서 Pointer 는 (메모리)주소를 저장하고 있는 데이터이다. 메모리는 우리가 휴대폰을 사용할 때 모든 데이터가 들어있는 곳이다. 즉, 어떤 데이터가 어디에 있는지 저장하고 있는 변수가 Pointer 이다. 위 예에서는 주소를 알려준 담당자가 포인터라고 할 수 있다.

그리고 Null 은 존재하지 않는 것을 뜻한다. 해당 장소로 갔더니 문도 닫혀있고 내 모니터는 존재하지 않더라.. 그런 느낌이다.

Null Pointer 는 데이터가 어디있지 하고 전화했는데 아무도 없고 전화를 안 받는 상황인 것이다! 사기인가?

하지만 프로그램을 만들다 보면 아직 데이터를 지정하기에 때가 되지 않아서 일부러 Null 값을 Pointer 에 넣는 경우도 있다. 그래서 Pointer 는 Null 이었다 아니었다 할 수 있다. 하지만 프로그램은 Null Pointer 를 만나면 즉시 죽어버린다. 애플은 여기에서 머리를 썼다.

얘는 Null 일 수도 있으니 조심해 라고 알려주는 것이다. 바로 ? 의 정체이다. ?가 있으면 얘는 안심할 수 없으니 경계 태세를 갖추라고 컴파일러가 알려준다. 하지만 프로그램을 만들다 보면 얘는 무조건 Null(애플은 Nil이라 부른다.) 이 아니야 라고 지정할 수 있다.
바로 ! 의 정체이다. (설명이 길었다. 하지만 Swift 로 개발할 때 가장 헷갈리는 부분 중에 하나였기 때문에 확실하게 짚고 넘어가고 싶었다.)

let hardness = sender.currentTitle!

결국 이 코드는 hardness 는 무조건 Soft, Medium, Hard 셋 중에 하나야! Nil 일 수 없어! 라고 말하는 것이다.

그리고 timer 는 사용할 때마다 0으로 초기화를 해주어야 한다. 안그러면 이전에 썼던 시간에 이어서 작동할 테니 엉망이 되겠지..

timer.invalidate()

그 다음 어떤 달걀을 선택했냐에 따라 타이머가 동작하는 시간이 다르니까

totalTime = eggTimes[hardness]!

총 시간을 정해주고,
progressBar 는 시작을 0으로 만들어야 한다. 안하면 웃긴 일이 생김.

progressBar.progress = 0.0

시작 시간을 초기화 하고 Label 에 선택한 단단함이 표시되도록 해준다.

secondsPassed = 0
titleLabel.text = hardness

스위프트는 ; 이 없는게 참 적응이 안된다.
그리고 타이머를 시작해 준다. 째깍째깍..

timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(updateTimer), userInfo: nil, repeats: true)

이걸 자세히 알려고 하지말자. 주의 사항은 #selector 에서 updateTimer 라는 함수를 선택했다는 것만 알아두면 된다.
그러면 함수 하나는 완성..

@IBAction func hardnessSelected(_ sender: UIButton) {        
let hardness = sender.currentTitle!
timer.invalidate()
totalTime = eggTimes[hardness]!
progressBar.progress = 0.0
secondsPassed = 0
titleLabel.text = hardness

timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(updateTimer), userInfo: nil, repeats: true)
}

그러면 updateTimer 함수만 만들면 끝이다.

@objc func updateTimer(){
if (secondsPassed < totalTime) {
progressBar.progress = Float(secondsPassed) / Float(totalTime)
secondsPassed += 1
}
else {
timer.invalidate()
titleLabel.text = "DONE!"
progressBar.progress = 1.0
playAlarm()
}
}

이런게 프로그래밍이지.
if 는 총 시간이 안됐을 때 progressBar 를 키우면서 1초씩 더해주는 거다. Float 를 안 붙이면 또 한 번 재미난 현상을 볼 수 있다. 이걸 이해시키려고 안젤라 누님은 15분을 또 투자하셨지..

else 는 시간이 다 되면 하는 짓을 적어놓았다. 타이머를 초기화하고, 끝! 이라는 글자를 띄우고 progressBar 를 꽉채우고.. playAlarm() 함수를 실행한다.

playAlarm 함수는 추가 미션이다.

프로젝트에 이미 alarm_sound.mp3 파일이 들어가 있다. 지난번 처럼..
playAlarm 까지 구현하면 처음 영상에 봤던 해괴한 소리를 들을 수 있다. 미안 안젤라 누나.. 사람이 완벽할 순 없지. 목소리가 좋으니 괜찮아.

테스트가 끝났으니 이제 시간을 초가 아닌 분으로 바꾸어야 한다.

totalTime = eggTimes[hardness]! * 60

앱을 완성하고 이제 계란을 삶으러 간다. 안젤라 누나가 검증을 끝마쳤다고 하니 잘 되겠지. 난 반숙이 좋아!

다음 앱은 뭐가 나올까..

전체 코드 (수학 문제집의 답지 느낌)

import UIKit
import AVFoundation
class ViewController: UIViewController {

@IBOutlet weak var progressBar: UIProgressView!
@IBOutlet weak var titleLabel: UILabel!
let eggTimes = ["Soft": 5, "Medium": 7, "Hard": 12]
var timer = Timer()
var totalTime = 0
var secondsPassed = 0
var player: AVAudioPlayer!
@IBAction func hardnessSelected(_ sender: UIButton) {

let hardness = sender.currentTitle!
timer.invalidate()
totalTime = eggTimes[hardness]! * 60
progressBar.progress = 0.0
secondsPassed = 0
titleLabel.text = hardness

timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(updateTimer), userInfo: nil, repeats: true)
}

@objc func updateTimer(){
if (secondsPassed < totalTime) {
progressBar.progress = Float(secondsPassed) / Float(totalTime)
secondsPassed += 1
}
else {
timer.invalidate()
titleLabel.text = "DONE!"
progressBar.progress = 1.0
playAlarm()
}
}

func playAlarm(){
let url = Bundle.main.url(forResource: "alarm_sound", withExtension: "mp3")
player = try! AVAudioPlayer(contentsOf: url!)
player.play()
}

}

--

--

JJ
0 Followers

개발자가 적성에 맞지만 온갖 것을 하기 좋아하는