1. 구현해야 할 것
2. 문제 해결
import Foundation
import UIKit
struct BookResponse: Decodable {
let data: [Book]
}
struct Book: Decodable {
let attributes: Attributes
}
struct Attributes: Decodable {
let title: String
let author: String
let pages: Int
let release_date: String
let dedication: String
let summary: String
let wiki: String
let chapters: [Title]
}
struct Title: Decodable {
let title: String
}
bookResponse의 data에 접근해서 그 안에 요소들을 전부 매핑해줘야하는데, 매핑하게 되면 Attributes 객체에 바로 접근해 매핑을 하기 때문에 타입 에러가 난다. 결국 Book의 배열로 Result의 성공 값을 지정해주었으니 매핑 하는 과정에서 Book이 리턴되어야 한다.
저 중간 객체의 네이밍을 뭐라지을지 헤매다가 이렇게 코드를 작성하고 문제가 생긴것인데,
struct BookResponse: Decodable {
let data: [BookData]
}
struct BookData: Decodable {
let attributes: Book
}
struct Book: Decodable {
let title: String
let author: String
let pages: Int
let release_date: String
let dedication: String
let summary: String
let wiki: String
let chapters: [Title]
}
struct Title: Decodable {
let title: String
}
BookResponse의 data에서 즉 [BookData]에서의 attributes로 접근하는 것이기 때문에 그 attributes를 반환하고 결국 그 타입은 Book이어야 한다. 네이밍을 BookData로 하지 않아도 결국 해당 객체는 중간 매개 역할을 해야하기 때문에 저렇게 수정했다.
3. 구현 사항 - 구조
우선 MVVM 패턴을 더 익혀보고자 이렇게 디렉토리 구조를 나눴다. 과제에서 data.json 파일을 제공해주기에 이를 파싱하는 등 다양한 역할을 좀 더 분리해야할 필요가 있겠다 싶어 이렇게 구조를 정해보았다.
class MainViewModel {
private let dataService = DataService()
private var books: [Book] = []
func loadBooks() {
dataService.loadBooks { [weak self] result in
guard let self = self else { return }
switch result {
case .success(let books):
self.books = books
case .failure(let error):
print(error)
}
}
}
func getBook(index: Int) -> Book? {
guard 0 ..< books.count ~= index else { print("getBook range is unvalid."); return nil } // 로드된 책 개수 안에서 하나를 반환하도록 예외 처리
return books[index]
}
}
level 1은 책 한 권에 대한 정보를 타겟으로 하기 때문에 이렇게 책들을 불러와 저장할 수 있도록 하고 별도의 한 권을 지정해 반환할 수 있도록 하는 메서드를 제공해주었다.
class MainViewController: UIViewController {
private let mainView = MainView()
private let mainViewModel = MainViewModel()
private var myBook: Book? = nil
override func viewDidLoad() {
super.viewDidLoad()
loadBook(index: 1)
guard let book = self.myBook else { print("[MainVC] book is nil"); return }
mainView.configureBook(book: book, index: 1)
}
override func loadView() {
view = mainView
}
private func loadBook(index: Int) {
mainViewModel.loadBooks()
guard let book = mainViewModel.getBook(index: index) else { print("getBook returned nil"); return }
print(book)
self.myBook = book
print(myBook)
}
}
좀 더 효율적인 코드는 있을 것 같지만, 우선 뷰컨트롤러와 뷰를 나누어 뷰모델에서 데이터를 가져와 뷰에 바인딩하는 과정을 뷰컨트롤러에게 담당하도록 했다. 뷰가 로드될 시 데이터를 불러올 수 있게 해당 뷰컨트롤러에서 생명주기를 통해 설정해주었다.
import UIKit
import SnapKit
import Then
class MainView: UIView {
private let titleLabel = UILabel().then {
$0.font = UIFont.systemFont(ofSize: 24, weight: .bold)
$0.text = "Title"
$0.textAlignment = .center
$0.textColor = .black
$0.numberOfLines = 0
}
private let seriesButton = UIButton().then {
$0.setTitleColor( .white, for: .normal)
$0.titleLabel?.font = UIFont.systemFont(ofSize: 16)
$0.backgroundColor = UIColor.systemBlue
$0.setTitle("?", for: .normal)
$0.isUserInteractionEnabled = false // 임시 레벨 1 기준 세팅
}
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented.")
}
private func setupUI() {
setupHierarchy()
setupConstraints()
seriesButton.layer.cornerRadius = 16
}
private func setupHierarchy() {
addSubview(titleLabel)
addSubview(seriesButton)
}
private func setupConstraints() {
titleLabel.snp.makeConstraints {
$0.centerX.equalToSuperview()
$0.horizontalEdges.greaterThanOrEqualToSuperview().inset(20)
$0.top.equalTo(safeAreaLayoutGuide.snp.top).offset(10)
}
seriesButton.snp.makeConstraints {
$0.centerX.equalToSuperview()
$0.top.equalTo(titleLabel.snp.bottom).offset(16)
$0.leading.greaterThanOrEqualToSuperview().offset(20)
$0.trailing.lessThanOrEqualToSuperview().offset(-20)
$0.width.height.equalTo(32)
}
}
func configureBook(book: Book, index: Int) {
titleLabel.text = book.title
seriesButton.setTitle(String(index), for: .normal)
}
}
view 측에선 모든 컴포넌트를 private로 지정하고 해당 컴포넌트의 값을 변경하고 싶을 땐, 별도의 준비된 함수를 통해 설정할 수 있도록 제공한다.
아마 후에 이 클래스를 상속받는 클래스가 없을 것 같다면 final 키워드로 지정해주어 최적화에 조금 더 힘을 보탤 수 있지 않을까 싶다.
AI 도움없이 직접 구현하려니 정말 기초적인 내용인데도 데이터 파싱에 관해 모델 설계 과정에서 애를 먹었다. 정말 보다가 안되겠을때 솔루션적인 면에서만 잠깐 물어보았는데, 코드로 구현을 위해서 계속 도움 받게된다면 꽤나 위험해질 것 같다.
'iOS > Swift' 카테고리의 다른 글
[TIL / 25.03.28] 과제 5, 스택뷰가 보이지 않는 문제 해결 (2) | 2025.03.28 |
---|---|
[TIL / 25.03.27] HarryPotterBooks 과제 2~4Lv 구현 및 회고 (1) | 2025.03.27 |
[TIL / 25.03.24] UIKit No Storyboard 초기 세팅 (0) | 2025.03.24 |
[TIL / 25.03.20] 최적화(OptimizationTips) 2 (0) | 2025.03.20 |
[TIL / 25.03.19] playground, command line tool에서의 비동기 함수 (feat. escaping closure) (2) | 2025.03.19 |