Picker에 index 사용을 지양하자!

SteadySlower·2022년 11월 22일
0

SwiftUI

목록 보기
34/64
post-custom-banner

테스트를 개발하던 중에 index out of range error를 발견했습니다. 에러가 발생한 원인을 보니까 Picker와 관련이 있었습니다. index를 사용하던 예전의 Picker를 id를 사용해서 개선해보았습니다.

예전 피커

코드

예전 피커의 코드입니다. 일단 selection에서 index를 사용하고 있습니다. 그리고 만약에 bookList가 비어있다면 (= 아직 단어장이 없거나 서버에서 가져오는 것을 실패한 경우) Picker 자체를 보여주지 않도록 만들어 두었습니다.

struct WordBookPickerView: View {
    @EnvironmentObject private var viewModel: ViewModel
    
    var body: some View {
        if viewModel.didBooksFetched && !viewModel.bookList.isEmpty {
            Picker(selection: $viewModel.selectedBookIndex, label: Text("선택된 단어장:")) {
                ForEach(0..<viewModel.bookList.count, id: \.self) { index in
                    Text(viewModel.bookList[index].title)
                }
            }
            .padding()
        }
    }
}
// viewModel
@Published private(set) var bookList: [WordBook] = []
@Published var selectedBookIndex = 0
@Published private(set) var didBooksFetched: Bool = false
@Published private(set) var isUploading: Bool = false

private var selectedBook: WordBook {
    bookList[selectedBookIndex]
}

단점

UI 문제점

가장 큰 문제가 되는 지점은 UI입니다. 처음에 사용자는 피커가 보이지 않다가 서버에서 단어장의 리스트를 불러오면 Picker를 보게 됩니다. 사용자 입장에서는 뜬금 없는 UI 변경으로 보여질 수 있습니다.

Crash의 위험성

더 큰 문제는 위 코드는 crash의 가능성을 내포하고 있다는 것입니다. 처음에 이 코드를 개선해야겠다고 마음을 먹은 것도 이 때문이었습니다. 원인은 index에 있습니다. viewModel의 코드를 보시면 selectedBook이라는 computed property가 있는데 이 변수의 역할은 단어를 저장할 때 서버에 wordBook의 정보를 넘기기 위함입니다.

하지만 만약에 서버에서 bookList를 불러오는 것에 실패했는데 사용자가 저장버튼을 누른다면 bookList는 빈 배열이기 때문에 0번 index가 존재하지 않게 되고 앱에 크래쉬가 나게 됩니다.

개선된 피커

개선된 피커는 index 대신에 id를 사용했습니다. 그리고 Picker의 선택지에 불러온 단어장에 대한 정보를 표시할 수 있도록 했습니다. 따라서 사용자는 단어장을 불러오는 중인지, 불러오는데에 실패했는지, 혹은 단어장을 불러와서 이제 선택할 수 있는 상태인지 알 수 있습니다.

크래쉬 문제도 단어장을 저장할 때 guard 문을 통해서 선택한 단어장의 id가 nil이거나 bookList에 없는 아이디일 경우 에러를 처리하도록 했습니다.

struct WordBookPickerView: View {
    @EnvironmentObject private var viewModel: ViewModel
    
    var body: some View {
        Picker("", selection: $viewModel.selectedBookID) {
            Text(viewModel.wordBookPickerDefaultText)
                .tag(nil as String?)
            ForEach(viewModel.bookList, id: \.id) { book in
                Text(book.title)
                    .tag(book.id as String?)
            }
        }
        .padding()
    }
}
// viewModel
@Published private(set) var bookList: [WordBook] = []
@Published var selectedBookID: String?
@Published private(set) var didBooksFetched: Bool = false
@Published private(set) var isUploading: Bool = false

var wordBookPickerDefaultText: String {
    if didBooksFetched && !bookList.isEmpty {
        return "단어장을 선택해주세요"
    } else if didBooksFetched && bookList.isEmpty {
        return "단어장 리스트 불러오기 실패"
    } else {
        return "단어장 불러오는 중..."
    }
}

// 단어 서버에 저장하는 메소드
func saveWord() {
  isUploading = true
  guard let wordBookID = selectedBookID else {
      // TODO: handle error
      print("선택된 단어장이 없어서 저장할 수 없음")
      isUploading = false
      return
  }
}

보시면 단어장을 불러오는 동안에 표시되는 메시지와 불러온 후의 메시지가 달라지는 것을 볼 수 있습니다.

profile
백과사전 보다 항해일지(혹은 표류일지)를 지향합니다.
post-custom-banner

0개의 댓글