Day 10 개발일지 iOS - boostcamp-2020/Project03-A-TOTP GitHub Wiki
Day 10 개발일지 iOS
프로그레스 바
프로그레스 바라는 뷰를 제공해주길래 당연히 이 뷰로 만들어야 하는 줄 알고 이걸로 시도해보았다. 그러다가 다음과 같은 경고 메세지를 받았다.
ProgressView initialized with an out-of-bounds progress value. The value will be clamped to the range of 0...total.
프로그레스뷰에서 onReceive함수를 통해 타이머 이벤트가 발생할 때 마다 timeAmount의 값을 timeInterval(0.01)만큼 더해주었다. 이 때 total 값인 30이 되기 전에 0으로 초기화해야 하는데 30이 넘어가고 나서 0으로 초기화 되었기 때문에 경고창이 뜬 것이다. 그래서 다음과 같이 0.01을 빼서 30을 초과하지 않도록 수정해주었더니 경고가 사라졌다.
if timeAmount < totalTime - 0.01 {
timeAmount += 0.01
} else {
timeAmount = 0.0
}
하지만 원형 프로그레스 바는 Circle()로 만드는 것
하지만 ProgressView를 원의 형태로 만드는 방법을 찾지 못했다. Circular ProgressBar의 모든 검색 결과가 Circle()을 사용하여 만드는 방법 뿐이었다.
Circle()
.trim(from: 0.0, to: CGFloat(progressAmount / totalTime))
.stroke(style: StrokeStyle(
lineWidth: CGFloat(strokeWidth),
lineCap: .round,
lineJoin: .round))
.foregroundColor(Color.white)
.rotationEffect(Angle(degrees: 270.0))
.animation(.none)
- trim : 0~1 사이의 값으로 도형의 몇 %만 그릴 지 결정한다. progressAmount 값은 MainViewModel의 timeAmount 값에 바인딩 되어 있어, MainViewModel에서 타이머 이벤트를 수신할 때 마다 timeInterval의 값을 더해주면 CGFloat(progressAmount / totalTime)의 값이 timeInterval만큼 더해져 view가 자동으로 업데이트 된다.
- stroke : 도형의 borderline을 중앙에 놓고 그린 선이다.
- strokeBorder : 도형의 borderline 안으로 그린 선이다.
- rotationEffect : 기본적으로 도형을 그리는 시작점은 x축의 양의 축이다. 이 축으로부터 몇 도 돌려 놓을지 정하는 property다.
- animation : 여러가지 다른 애니메이션을 사용해보았지만 애니메이션을 없애고 대신에 타이머의 간격을 줄이는 것이 더 자연스러워 보여 이렇게 설정해주었다.
참고자료: How to build a circular progress bar in SwiftUI - Simple Swift Guide
색을 채워나가는 게 아니라 지워가는 걸로 바꾸는 법 고민 중
현재 애니메이션은 0에서 30까지 채워나가는 방식으로 구성되어 있다. 그런데 의미적으로는 남은 시간을 표시하는 게 더 좋을 것 같다는 의견이 나왔다. 처음에는 꽉 차 있는 상태고, 시간이 지날 수록 비어나가는 애니메이션으로 표현하는 것이다. 지금은 아직 이 애니메이션을 구현할 방법을 찾지 못하여 이렇게 구현해 놓았지만 향후 남은 시간은 표현하는 방식으로 변경할 예정이다.
타이머
이렇게 구현된 프로그레스 바는 다음과 같이 동작한다. 시간이 만료되면 초와 비밀 번호, 그리고 프로그레스바가 새롭게 업데이트 된다.
타이머가 publish하는 이벤트를 수신해서 값을 변경시키는 작업을 어떤 식으로 동작하게 하면 좋을 까? 뷰가 onReceive하는 게 좋은 구조인가?
처음에는 초를 표현하는 Text뷰, 프로그레스 뷰, 비밀번호 Text뷰에서 OnReceive로 로직을 처리해주었다. 그런데 이렇게 하면 뷰에서 데이터 처리 로직을 가지고 있게 된다. 그래서 이를 MainViewModel에 모두 옮기고 Timer에 subscribe하는 방식으로 변경하였다. sink함수에서 Published 데이터를 변경하면 여기에 바인딩 되어 있는 뷰가 자동으로 업데이트 된다.
타이머를 그대로 신뢰해도 좋은가?
타이머의 발생 주기가 0.01초이기 때문에 타이머 이벤트가 발생할 때 마다 프로그레스바의 상태값에 0.01을 더하고 이 값이 30이 되면 0으로 초기화 하면 될 거라고 생각했다. 그래서 프로그레스 바는 이렇게 로직을 구성했었다. 반면 초를 표현하는 상태값은 콤바인의 map 함수를 통해 처음 시작 시간(고정값)으로부터 몇 초가 지났는지를 매 번 계산하고 여기서 30으로 모듈러 연산을 한 값으로 표현했다.
처음에는 잘 나오는 가 싶더니, 시뮬레이터를 돌려놓고 5분 정도 지나니 초 텍스트와 프로그레스바의 초기화 시점이 안맞기 시작했다. 프로그레스바는 타이머의 이벤트 발생 시점마다 갱신되고, 초는 매번 고정 값(타이머의 시작시간)으로부터 몇 초 떨어져있는 지를 계산하여 구했던 것이 이러한 차이를 발생 시킨 것이다. 이것은 타이머가 정해진 주기마다 항상 시간 이벤트를 발생 시킨다고 보장할 수 없기 때문에 발생한 문제다.
그래서 프로그레스 바의 값도 매번 정확한 시간을 반영하여 업데이트 하도록 수정해주었다.
딱 변하는 그 한 순간만, 정수가 되는 그 한 순간에만 업데이트를 하는 방법이 없을까?
초 텍스트가 바인딩 되어 있는 timeString은 타이머의 이벤트 발생 주기 마다 업데이트 될 필요가 없다. 오직 그 값이 정수가 되는 순간에 만 한 번 업데이트 해주면 된다. 그래서 이를 위해 lastSecond라는 변수를 두어 lastSecond와 달라지는 한 순간만 업데이트 하도록 해주었다.
(지금 보니 lastSeconds가 맞는 변수명인 것 같다..!)
타이머의 시작 시간
처음에는 Timer의 시작 시간을 단순히 Date()로 설정하였다. 그래서 앱이 시작하는 시간이 타이머의 시작 시간이었다. 하지만 우리 앱은 그렇게 동작하면 안된다. 현재 시간을 30초로 모듈러 연산한 값이 타이머의 시작이 되어야 한다. 만약 앱을 실행시킨 시간이 0시 0분 15초라면, 앱을 켰을 때 프로그레스바와 초 값이 15초를 가리키고 있어야 하는 것이다.
timer
.map({ (output) in
return output.timeIntervalSince(self.now)
})
.map({ (timeInterval) in
return Int(timeInterval)
})
.sink { [weak self] (seconds) in
...
그래서 위와 같이 되어 있던 코드를 아래와 같이 수정해주었다.
timer
.map({ (output) in
return output.timeIntervalSince(self.todaySartTime ?? Date())
})
.map({ (timeInterval) in
return Int(timeInterval) % Int(self.totalTime)
})
.sink { [weak self] (seconds) in
...
todaySartTime로 오늘의 0시 0분 0초 값을 구하고, 이 순간부터 현재 시간까지 지나온 초값을 구했다. 그리고 이 값을 30초로 모듈러 연산을 해주었다.
TextView 비밀번호 출력할 때 고민
6자리 비밀번호를 뷰에 표시할 때에는 왼쪽 세자리와 오른쪽 세자리의 간격이 벌어져 있어야 한다. 이를 위해 애초에 상태값의 가운데에 spacing을 줄까 고민이 되었다. 하지만 뷰에 표시하는 방법은 뷰의 책임이라는 판단이 들어서 텍스트 사이의 공간을 두는 것은 뷰가 처리하게 하기로 했다.
그래서 HStack에 넣고 spacing으로 간격을 줄까 했지만, Text뷰에 + 연산이 된다는 것을 보고 아래와 같이 하기로 결정했다.
(Text(mainCellVM.password.prefix(3))
+ Text(" ")
+ Text(mainCellVM.password.suffix(3)))
.foregroundColor(.white)
.font(.system(size: 30))
.fontWeight(.bold)
Gradient Extension
View의 Background Color를 선형 그라데이션으로 만들려면 startColor와 endColor를 넣어주면 된다.
먼저 우리가 사용하고자 하는 Color들을 Asset에 추가했다.
그리고 저장된 Color들을 이용해 LinearGradient를 만드는 함수를 만들었다.
앞으로의 그라데이션은 이를 사용해서 간단하게 그릴 수 있게 됐다!
extension LinearGradient {
static let pink = makeGradient(startColor: Color.pink1, endColor: Color.pink2)
static let mint = makeGradient(startColor: Color.mint1, endColor: Color.mint2)
static let salmon = makeGradient(startColor: Color.salmon1, endColor: Color.salmon2)
static let blue = makeGradient(startColor: Color.blue1, endColor: Color.blue2)
static let brown = makeGradient(startColor: Color.brown1, endColor: Color.brown2)
static let navy = makeGradient(startColor: Color.navy1, endColor: Color.navy2)
static func makeGradient(startColor: Color,
endColor: Color) -> LinearGradient {
LinearGradient(gradient: Gradient(colors: [startColor, endColor]),
startPoint: .top,
endPoint: .bottom)
}
}
SwiftUI로 UI 그리기
기존 UI만 딱 구현하자 라는 마음을 갖고 재명님과 어진님은 짝코딩으로 '그리기'에만 집중했다. SwiftUI는 스토리보드에 하나하나 올려야하는 귀찮음을 겪지 않아도 되어서 비교적 UI를 빨리 그릴 수 있었던 것 같다.
그런데 이게 디테일한 요소들을 넣고, 코드를 정리하려고 하니 여간 단순한 일이 아니었다. 뷰를 어디서부터 어떻게 나누어야 할 지, 픽셀단위로 어떻게 맞춰서 넣을 수 있을지 아직 잘 모르겠다. 이 부분은 더 익숙해지고, 조금 더 공부해 보아야 할 부분인 것 같다.
Search Bar
SwiftUI는 SearchBar가 따로 존재하지 않았다. 그래서 이를 뷰를 하나하나 그리며 SearchBarView로 따로 빼서 개발하는 방법을 택했다.
SearchBar에 필요한 것
- 검색 창
- 돋보기 이미지
- 취소 버튼
- X 버튼
이를 전부 만들고 padding값도 전부 수작업으로 넣어주었다. 물론 직접 구현한 다른 개발자들이 여럿 있어서 참고할 레퍼런스는 많아 편하게 개발할 수 있었다!!ㅎㅎㅎ
로직
searchText를 State로 가지고 있고, 지금 서치를 하고있는 중인지 아닌지를 Bool값으로 가지고 있는다. 서치바를 선택하면 서치를 하고 있는 중이라는 것을 인식하고, 취소나 x버튼을누르면 text값을 지우고, 검색을 나가는 방법으로 개발했다.
다음엔..
이미 만들어 놓긴 했지만, 괜히 UIViewRepresentable을 통해 사용하는 UISearchBar로 바꾸어보고 싶다.
개인 회고
(솔직히 쓰기 - 현재 파트너 또는 누군가가 본다고 생각하지 말고 미래의 내가 본다고 생각하며 쓰면 어떨까요??😏)
어진
- 오늘 할일은 검색기능과 디자인해놓은 대로 Main UI전부 고치는것이 목표였다. 근데 오늘 집중이 대박 안 되어서 검색부분은 UI밖에 끝내지 못했다.. 그렇다고 맘놓고 쉬지도 않고 책상에 앉아있었다...... 차라리 푹 쉬고 앉아서 집중했더라면 진즉 끝냈을 수도 있겠다!!!!!
- 반면 오늘 재명님이~~ 하드캐뤼 했다. 저번주에 뚝딱뚝딱 크립토킷 사용해서 구현했던걸 오늘 비밀번호 생성하는데 적용할 수 있었다!! 아직 타이머란 좀 어렵게 느껴졌지만 재명님 코드 보고 전반적인 이해정도 할 수 있어서 좋았다.
- 아직 초보 개발자라 하나 개발하는 데에도 굉장히 오래걸린다.. 얼른 고수가 되고프다.ㅎㅎㅎ
재명
- 뱅글 뱅글 프로그레스바를 엄청 많이 봤다. 아직도 돈다.
- 저번 주에 터미널로 6자리 비밀번호 생성하고, 시간까지 출력해주는 로직을 미리 짜두었었다. 그런데 일주일 지났다고 조금 까먹어서 시작 시간 설정할 때 현재 시간을 기준으로 계산해버렸다. 그래서 좀 애를 먹었는데 이젠 해결되었다.
- TOTP 암호 알고리즘이 아직 완전히 숙지가 되어 있지 않다. 큐알로 넘어오는 값을 제대로 적용하려면 이해하고 있어야 할 것 같은데, 내일은 이 부분도 학습해야 겠다.
- SwiftUI로 본격적인 UI 작업을 시작했다. 그런데 정말 편했다. 아직 엄청 복잡한 뷰를 만든 것은 아니지만 지금까지 만든 걸 UIKit로 만들려고 생각해보면 피곤함이 벌써 느껴진다. 현업에서도 SwiftUI가 널리 퍼지길..!
- 분업을 하고 PR을 날리고 리뷰도 열심히 남겨보았다. 확실히 이전 프로젝트에 비해 협업하는 데에 익숙해진 것 같다. 아직 실수 투성이이긴 하지만 어진님이 꼼꼼하게 잘 봐주셔서 금방 바로잡을 수 있었다.