1. 기존 UI가 계속 쌓이는 문제
🔍 문제
func configure(book: Book, seriesNumber: Int) {
let stacks: [UIStackView] = [
makeImageAndInfoStack(book: book, seriesNumber: seriesNumber),
makeSummaryStack(title: "Dedication", value: book.dedication),
makeSummaryStack(title: "Summary", value: book.summary, seriesNumber: seriesNumber),
makeChapterStack(title: "Chapter", value: book.chapters)
]
stacks.forEach { totalStack.addArrangedSubview($0) }
}
configure() 호출 시 UIStackView에 기존 서브뷰가 남은 상태로 추가됨
💡 해결
func configure(book: Book, seriesNumber: Int) {
totalStack.arrangedSubviews.forEach { $0.removeFromSuperview() }
}
- 재구성 전 기존
arrangedSubviews를 제거하여 해결
2. Summary 450자 이하에도 "…" 붙는 문제
🔍 문제
func configureSummary(value: String, selectedSeries: Int) {
let key = "isExpanded_\(selectedSeries)"
let isExpanded = UserDefaults.standard.bool(forKey: key)
summaryValueLabel.text = isExpanded ? value : "\(value.prefix(450))..."
}
- 문자 수와 관계없이 항상 접힌 형식이 적용됨
- 450자 이하에서도
summaryValueLabel.text에 ...이 붙음
💡 해결
func configureSummary(value: String, selectedSeries: Int) {
let key = "isExpanded_\(selectedSeries)"
let isExpanded = UserDefaults.standard.bool(forKey: key)
let isTruncate = value.count > 450
summaryValueLabel.text = isTruncate && !isExpanded
? "\(value.prefix(450))..."
: value
}
- 450자 이상인 경우에만 접힌 텍스트(
...)가 적용되도록 조건 추가
3. JSON 디코딩과 날짜 처리
🔍 문제
func loadBooks(completion: @escaping (Result<[Book], Error>) -> Void) {
do {
let data = try Data(contentsOf: URL(fileURLWithPath: path))
let bookResponse = try JSONDecoder().decode(BookResponse.self, from: data)
}
}
func configure(book: Book, selectedSeries: Int) {
releasedValueLabel.text = formattedDate(book.releaseDate)
}
func formattedDate(_ dateValue: String) -> String {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
guard let date = formatter.date(from: dateValue) else { return dateValue }
let displayFormatter = DateFormatter()
displayFormatter.dateStyle = .long
displayFormatter.locale = Locale(identifier: "en_US")
return displayFormatter.string(from: date)
}
- JSON 파싱 시 날짜(
release_date)를 String 타입으로 디코딩
formattedDate() 함수를 따로 만들어 DateFormatter를 사용해 포맷팅하고 있었음
💡 해결
func loadBooks(completion: @escaping (Result<[Book], Error>) -> Void) {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted({
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd"
return dateFormatter
}())
do {
let data = try Data(contentsOf: URL(fileURLWithPath: path))
let bookResponse = try decoder.decode(BookResponse.self, from: data)
}
}
func configure(book: Book, selectedSeries: Int) {
releasedValueLabel.text = "\(book.releaseDate, format: .long, locale: Locale(identifier: "en_US"))"
}
extension String.StringInterpolation {
mutating func appendInterpolation(_ date: Date, format: DateFormatter.Style = .long, locale: Locale = .current) {
let formatter = DateFormatter()
formatter.dateStyle = format
formatter.locale = locale
appendLiteral(formatter.string(from: date))
}
}
JSONDecoder의 dateDecodingStrategy를 사용해 날짜 문자열을 파싱 단계에서 바로 Date로 변환함
- 이후 UI에서는
DateFormatter가 아닌 String.StringInterpolation의 appendInterpolation(_:format:locale:) 확장을 활용함
Date → 포맷된 String 변환을 인터폴레이션 방식으로 간결하게 처리함
- 가독성과 재사용성이 높아짐
4. 커스텀 에러에 사용자 메시지 적용
🔍 문제
enum DataError: Error {
case fileNotFound
case parsingFailed
}
private func handleError(_ error: Error) {
let alert = UIAlertController(title: "데이터 로딩 실패", message: error.localizedDescription, preferredStyle: .alert)
alert.addAction(.init(title: "확인", style: .default))
present(alert, animated: true)
}
DataError가 단순히 Error만 채택하고 있음
error.localizedDescription이 "The operation couldn’t be completed." 같은 일반적인 메시지만 반환함
→ 사용자에게 적절한 안내 문구가 제공되지 않음
💡 해결
enum DataError: Error, LocalizedError {
case fileNotFound
case parsingFailed
var errorDescription: String? {
switch self {
case .fileNotFound:
return "데이터 파일을 찾을 수 없습니다."
case .parsingFailed:
return "데이터를 불러오는 데 실패했습니다."
}
}
}
DataError에 LocalizedError 프로토콜 채택
errorDescription을 직접 정의하여 에러별 사용자 친화적인 메시지를 제공함
5. MainView -> MainViewController 통합
🔍 문제
MainView에서는 MainHeaderView와 MainContentView를 포함하는 구조
MainViewController에서는 MainView 관리
MainView에서 컨트롤러의 일들을 처리하는 구조로, MainViewController에서 하는 일이 별로 없는 상황
💡 해결
MainView 코드를 MainViewController로 통합하여 해결
MainViewController에서 헤더/컨텐츠 구성, 네트워크 호출과 에러 핸들링 처리