-
Notifications
You must be signed in to change notification settings - Fork 5
Day 20 개발일지 iOS
원래는 TextField의 onChange() 함수에다가 search하는 함수를 넣어놓았었음
.onChange(of: searchText) { _ in
search(text: searchText)
}
그런데 취소버튼으로 endSearch 함수를 불러주고 main으로 돌아가려고 하니
searchText가 ""으로 바뀌면서 onChange를 통한 search함수와, main으로 돌아가려는 함수가 충돌(?) 나면서 뷰 버그가 발생했다.
그래서
TextFiled의 editing을 감지하는 changeSearchState함수를 따로 만들어주고,
TextField("검색", text: $searchText, onEditingChanged: changeSearchState)
search함수를 불러올 때
viewModel.state.isSearching이 false이면 서치하지 않게 했다.
sheet modal에서 토큰 변경 후 dissmiss하고 어떻게 refresh할 수 있을까..? 로그를 찍어보니 dissmiss한 후, mainView에서는 onAppear함수가 실행되지 않는 것을 확인했다.
여기서 메인을 refresh하는 함수는 mainViewModel에 존재했기 때문에 이를 어떻게 가능하게 할까??!?!!?
먼저, 메인뷰에서 불러오는 TokenCellView
에 refreshAction을 넘겨주고,
TokenCellView(service: viewModel.state.service,
token: token,
isMain: false,
checkBoxMode: $viewModel.state.checkBoxMode,
isSelected: viewModel.state.selectedTokens[token.id],
refreshAction: {
viewModel.trigger(.refreshTokens)
})
TokenCellViewModel
에서 refreshAction을 받아 hideEditView에서 실행할 수 있게 했다.
case .hideEditView:
TOTPTimer.shared.startAll()
state.isShownEditView = false
refreshAction?()
}
Token의 정보들을 키체인에 넣을 수 있게 키체인 CRUD를 만들었다.
Update부분은 사실 다시 새로 넣는는 부분이기 때문에, 그냥 그 코드는 삭제하고 새로 만들 때 지워주고 다시 넣는 방식으로 개발했다.
그리고 이 StorageManager를 Singleton으로 사용해도 좋겠다 라고 생각했는데, 재명님이 어차피 TokenService에서만 사용하는거니까 TokenService에다만 주입해도 될 것 같다라고 해서 그런식으로 개발하게 됐다!!!!! 굳굳
기존 코드에서는 같은 기능의 버튼을 여러 곳에서 사용하고 있었다. 각각 따로 작성했던 것을 어떻게 합칠 수 있을까 하다가
saveButton이라는 변수를 만들어서 놓고, 그 변수를 가져다 사용했다.
saveButton.foregroundColor(.black)
or
saveButton.foregroundColor(.white)
var saveButton: some View {
Button(action: {
showingAlert = text.isEmpty || text.count > 17
if !showingAlert {
dismiss()
addToken()
}
}, label: {
HStack {
Spacer()
Text("저장")
Spacer()
}
})
.modifier(AlertModifier(isShowing: $showingAlert))
}
타이머 자체에서 restart 기능이 없다. 타이머 자체에서는 끊는 기능(cancel) 뿐이다. 다시 시작하려면 타이머를 사용하는 곳에서 타이머에 다시 subscribe 해야한다. 이를 위해 다음과 같은 사고의 흐름을 거쳤다.
-
구독자의 구독 로직을 타이머 객체에 클로저로 넘겨준다. 그리고 타이머가 재시작 함수에서 해당 클로저를 실행시킨다.
-
타이머를 사용하는 뷰에서 onDisapear, onAppear에서 호출 → 타이머가 시작, 멈춤을 제어하는 게 아니라 구독자가 제어. → 그럼 타이머에서 cancel을 제공할 필요도 없음. → 치명적 제약 조건.... 시트로 띄우면 onDisappear, onAppear가 호출이 안된다. → 그럼 에딧 버튼 눌렀을 떄 stop해주고 endEdit할 떄 start 해주면 되지 않을까 → 모든 셀에 이게 반영이 되어야 하는데 가능할까..? 다른 셀 ViewModel은 어떻게 다시 구독하게 하지? → 이렇게 하려면 1번 방법을 사용해야 할듯...
-
타이머를 각 셀에 놓는다? → 마찬가지로 다른 셀 ViewModel은 어떻게 다시 구독하게 하지?
-
어쩔 수 없다..! 1번 방법을 사용해야겠다. → 셀이 삭제되면 타이머 구독 클로저 array에서도 해당 클로저를 삭제해줘야 한다. → 어차피 이럴꺼면 그냥 cancellable 객체만 넘겨주면 되지 않아?
-
각 셀 뷰 모델의 캔슬러블을 Timer객체에 넘겨줘서 이 캔슬러블로 시작 스탑을 하면?
→ 우선 캔슬러블로 시작 스탑 가능한지 보기 → 애초에 캔슬러블 객체로 타이머를 다시 시작하는 게 불가능함. → 근데 이렇게 하면 Timer에서 cancellable 객체를 참조하여 셀 뷰 모델이 deinit될 떄 cancellable는 deinit이 안되는 문제가 생긴다. → 이건 셀 삭제 될때 타이머의 cancellable어레이에서 삭제해주면 되긴 하다.
-
결국 1번 방법을 사용하기로 했다. 구독 로직을 타이머 객체에게 넘겨주고 타이머 객체가 다시 시작할 때 클로저를 실행하는 것이다.
- 커링 이 과정에서 뷰모델이 타이머 init 함수에 key값을 넘겨주고 타이머 init 함수가 클로저를 반환하는 방법을 사용했다. 이렇게 하면 key를 뷰모델의 프로퍼티로 두지 않아도 된다.
class TOTPTimer {
// ...
let eventHandler: AnyCancellable
// ...
init() {
// ...
eventHandler = timer.handleEvents( receiveCancel: {
print("reveive cancel")
}, receiveRequest: { _ in
print("receive request")
})
.sink(receiveValue: { _ in })
}
// ...
}
뷰가 새로 생성되면서 뷰 모델이 새로 생성되었다. 이로 인해 타이머 객체에 클로저가 계속 쌓이는 문제가 생겼다. 처음에는 뷰모델의 deinit에서 타이머의 delete함수를 호출하여 클로저를 제거해 주려고 했다. 그런데 클로저를 어떻게 식별해야 하는지와 같은 문제에 직면하게 되었다.
그래서 subscribers를 딕셔너리로 바꾸고 token.id를 키값으로 줘서 해당 키값의 클로저를 뷰모델이 생성할 때 마다 갱신해주었더니 해결되었다. 해당 토큰에 새로운 클로저가 추가될때 그냥 갱신해주면 이전 클로저는 사라진다. 토큰이 삭제될 때에는 딕셔너리에서 해당 토큰을 키값으로 하는 값을 삭제해주면 된다.
그렇지 않으면 해당 토큰 아이디의 클로저가 계속 남아있게 된다. 아래처럼 토큰은 3개밖에 없는데 클로저가 처음 6개 그대로 남아있게 된다.
- 문제
셀이 3개 뿐인데 클로저의 개수는 6개다.
그래서 메인 뷰모델에서 토큰 삭제시 해당 토큰 아이디를 가지는 클로저를 삭제해주어야 한다.
case .deleteSelectedTokens:
let deletedTokens = state.selectedTokens
.filter { $0.value == true}
.map { $0.key }
TOTPTimer.shared.deleteSubscribers(tokenIDs: deletedTokens)
- 해결
셀의 개수와 클로저의 개수가 동일해졌다.
셀뷰모델의 init에서는 30초 중에 현재 시간을 계산해서 프로그레스 바에 더해준다. 그런데 클로저에는 이 과정이 없고 그냥 타임 인터벌 만큼 더해주는 로직만 있다. 그래서 타이머를 다시 시작했을 때에는 이전 프로그레스 상태 값에 0.1초만 더해진 것이다.
애초에 init에서 30초중 현재 시간을 더해주는 게 아니라 sink에서도 그 로직을 해주어야 하는 것이다. 그저 0.1을 계속 더하는 게 아니라, 현재 시간중 몇초인지를 double로 계산해서 이 값을 할당해주었더니 해결되었다.
You don't (always) need [weak self]
guard let self = self else { return }
self.momentOfSecondsChanged(seconds: seconds, key: key)
이렇게 self를 바로 사용해도 된다!
mock 서비스 추가위해
@main
struct DaDaIkSeonApp: App {
var body: some Scene {
#if DEBUG
let service = MockTokenService()
#else
let storageManager = StorageManager()
let service = TokenService(storageManager)
#endif
WindowGroup {
MainView(service: service).environmentObject(NavigationFlowObject())
}
}
}
Conditional Compilation, Part 1: Precise Feature Flags
처음 로드할 때 maincell을 따로 지정하지 않고, 키체인에서 가져온 tokens에서 isMain에 true로 되어 있는 token을 메인으로 사용한다. 이전에는 다른 방법으로 메인을 정했는데, 그 때 키체인에 저장했던 토큰에는 isToken에 값이 지정되어 있지 않다. 그래서 키체인 방식으로 로드했을 때 메인 셀이 지정되지 않는 문제가 발생했다. 그래서 service의 init에서 isMain이 true인 값이 없다면 무조건 맨앞에 있는 token의 isMain을 true로 만들어주도록 변경했다.
위 자료를 보고 적용해 보았는데, 우리가 원하는 애니메이션도 아니었고 repeatForever를 하면 풀리지가 않았다. 내일 다시 시도해봐야겠다.
(솔직히 쓰기 - 현재 파트너 또는 누군가가 본다고 생각하지 말고 미래의 내가 본다고 생각하며 쓰면 어떨까요??😏)
- 주말이지만 이제는 쉴 수 없다. 갈 길은 멀고 해야할 일들은 많다.
- 멘토님께 연락을 드려봤는데 많이 바쁘신 것 같다ㅠㅠ 주말이라 그런거겠지..?
- 치명적인 버그를 모두 잡았다. 이제 좀 안심이 되지만, 멈출 수 없다.
© Boostcamp