오늘은 본격적으로 과제를 시작했다. 조금씩 나눠서 기록해보자
현재, 구현 5까지 완성했지만, 나누어서 기록하고자 오늘은 구현2까지를 기록할 것이다.
메인 화면은 ViewController를 사용하여 구현했으며, BookInfo.swift에 data.json의 데이터를 저장할 객체 생성, DataService.swift에서 data.json을 파싱하는 로직을 사용하여 ViewController에서 파싱된 데이터를 처리하여 랜더링한다.
import UIKit
import SnapKit
let dataService = DataService() // DataService 생성
var books: [Book] = [] //받아온 데이터 저장용 배열
let titleText = UILabel()
let seriesButton = SeriesButton()
override func viewDidLoad() {
super.viewDidLoad()
configUI()
loadBooks()
}
}
extension ViewController {
private func configUI() {
view.backgroundColor = .white
// titleText.text = "ASDFASDFASDFASDFSADFSADFASDFSADFSADFSADFSADS"
titleText.textColor = .black
titleText.font = .systemFont(ofSize: 24, weight: .bold)
titleText.numberOfLines = 0 // 줄 바꿈 제한 x
titleText.textAlignment = .center // 텍스트 중앙 정렬
seriesButton.setTitle("1", for: .normal)
seriesButton.setTitleColor(.white , for: .normal)
seriesButton.titleLabel?.font = .systemFont(ofSize: 16)
seriesButton.backgroundColor = .systemBlue
// seriesButton.layer.cornerRadius = 8
[titleText, seriesButton].forEach { view.addSubview($0) }
titleText.snp.makeConstraints {
$0.leading.trailing.equalToSuperview().inset(20)
$0.top.equalTo(view.safeAreaLayoutGuide).offset(10)
}
seriesButton.snp.makeConstraints {
// $0.leading.trailing.equalToSuperview().inset(20)
$0.centerX.equalToSuperview()
$0.top.equalTo(titleText.snp.bottom).offset(16)
$0.width.equalTo(seriesButton.snp.height) // height에 width 고정 -> 가로, 세로 비율 유지
}
}
}
// UIButton 상속받는 커스텀 SeriesButton 생성 : ( 레이아웃 결정 이후 cornerRadius 적용되기 때문 )
class SeriesButton: UIButton {
override func layoutSubviews() {
super.layoutSubviews()
self.layer.cornerRadius = self.frame.height / 2
self.clipsToBounds = true
}
// data.json 파싱 이후 titleText에 적용
extension ViewController {
func loadBooks() {
dataService.loadBooks { [weak self] result in
guard let self = self else { return }
switch result {
case .success(let books):
self.books = books
if let firstBook = books.first {
self.titleText.text = firstBook.title
}
case .failure(let error):
print("에러 : \(error)")
}
}
}
import Foundation
struct BookResponse: Codable {
let data: [BookData]
}
struct BookData: Codable {
let attributes: Book
}
struct Book: Codable {
let title: String
let author: String
let pages: Int
let releaseDate: String
let summary: String
// data.json 형식 맞추기 : releaseDate는 data.json의 release_date
enum CodingKeys: String, CodingKey {
case title, author, pages, summary
case releaseDate = "release_date"
}
import Foundation
class DataService {
enum DataError: Error {
case fileNotFound
case parsingFailed
}
func loadBooks(completion: @escaping (Result<[Book], Error>) -> Void) {
guard let path = Bundle.main.path(forResource: "data", ofType: "json") else {
completion(.failure(DataError.fileNotFound))
return
}
do {
let data = try Data(contentsOf: URL(fileURLWithPath: path))
let bookResponse = try JSONDecoder().decode(BookResponse.self, from: data)
let books = bookResponse.data.map { $0.attributes }
completion(.success(books))
} catch {
print("🚨 JSON 파싱 에러 : \(error)")
completion(.failure(DataError.parsingFailed))
}
}
}
import UIKit
import SnapKit
class BookInfoStackView: UIStackView {
let titleImageView = UIImageView()
let contentStackView = UIStackView()
let titleLabel = UILabel()
let authorStackView = UIStackView()
let authorLabel = UILabel()
let authorValueLabel = UILabel()
let releaseDateStackView = UIStackView()
let releaseDateLabel = UILabel()
let releaseDateValueLabel = UILabel()
let pageStackView = UIStackView()
let pageLabel = UILabel()
let pageValueLabel = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
setAttribute()
setLayout()
}
required init(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension BookInfoStackView {
// 속성 정의
private func setAttribute() {
self.axis = .horizontal
self.spacing = 20
self.alignment = .top
titleImageView.backgroundColor = .systemBlue
titleImageView.contentMode = .scaleAspectFill
titleImageView.clipsToBounds = true // 원본 이미지와 맞지 않을 경우 바깥으로 빠져나감 -> 잘라내도록
contentStackView.axis = .vertical
contentStackView.spacing = 12
contentStackView.alignment = .leading // 좌측을 기준으로 필요한만큼 공간 할당
titleLabel.font = .systemFont(ofSize: 20, weight: .bold)
titleLabel.textColor = .black
titleLabel.numberOfLines = 0 // 줄 수 제약 x
// contentStackView 내부의 3개의 StackView의 조건은 같음 -> 묶어서 forEach 사용
[authorStackView, releaseDateStackView, pageStackView].forEach {
$0.axis = .horizontal
$0.spacing = 8
}
authorLabel.text = "Author"
authorLabel.textColor = .black
authorLabel.font = .systemFont(ofSize: 16, weight: .bold)
authorValueLabel.font = .systemFont(ofSize: 18)
authorValueLabel.textColor = .darkGray
releaseDateLabel.text = "Release"
releaseDateLabel.textColor = .black
releaseDateLabel.font = .systemFont(ofSize: 14, weight: .bold)
releaseDateValueLabel.font = .systemFont(ofSize: 14)
releaseDateLabel.textColor = .darkGray
pageLabel.text = "Page"
pageLabel.textColor = .black
pageLabel.font = .systemFont(ofSize: 14, weight: .bold)
pageValueLabel.font = .systemFont(ofSize: 14)
pageValueLabel.textColor = .gray
}
// 레이아웃 정의
private func setLayout() {
// 각 Label -> StackView에 넣기
[authorLabel, authorValueLabel].forEach { authorStackView.addArrangedSubview($0) }
[releaseDateLabel, releaseDateValueLabel].forEach { releaseDateStackView.addArrangedSubview($0) }
[pageLabel, pageValueLabel].forEach { pageStackView.addArrangedSubview($0) }
// 각 StackView -> contentStackView에 넣기
[titleLabel, authorStackView, releaseDateStackView, pageStackView].forEach { contentStackView.addArrangedSubview($0) }
// titleImage와 contentStackView 합치기
[titleImageView, contentStackView].forEach { addArrangedSubview($0) }
titleImageView.snp.makeConstraints {
$0.width.equalTo(100)
$0.height.equalTo(titleImageView.snp.width).multipliedBy(1.5) //가로 세로 비율 1:1.5
}
}
}
// 데이터 불러오기
extension BookInfoStackView {
func configure(with book: Book) {
titleLabel.text = book.title
authorValueLabel.text = book.author
releaseDateValueLabel.text = book.releaseDate
pageValueLabel.text = "\(book.pages)"
}
만들어둔 책 정보 영역을 표시하기 위해 기존의 ViewController의 수정이 필요했다. ViewController에 bookInfoView를 생성하여 속성을 정의해주고, addSubview를 사용하여 bookInfoView를 화면에 표시해주었다. 또한, loadBooks 메서드를 수정하여 data.json 파일에서 책 정보를 표시하기 위해 필요한 정보들을 넘겨준다.
import UIKit
import SnapKit
class ViewController: UIViewController {
let dataService = DataService() // DataService 생성
var books: [Book] = [] //받아온 데이터 저장용 배열
let titleText = UILabel()
let seriesButton = SeriesButton()
let bookInfoView = BookInfoStackView() // bookInfoView 생성
override func viewDidLoad() {
super.viewDidLoad()
configUI()
loadBooks()
}
}
extension ViewController {
private func configUI() {
view.backgroundColor = .white
// titleText.text = "ASDFASDFASDFASDFSADFSADFASDFSADFSADFSADFSADS"
titleText.textColor = .black
titleText.font = .systemFont(ofSize: 24, weight: .bold)
titleText.numberOfLines = 0 // 줄 바꿈 제한 x
titleText.textAlignment = .center // 텍스트 중앙 정렬
seriesButton.setTitle("1", for: .normal)
seriesButton.setTitleColor(.white , for: .normal)
seriesButton.titleLabel?.font = .systemFont(ofSize: 16)
seriesButton.backgroundColor = .systemBlue
// seriesButton.layer.cornerRadius = 8
[titleText, seriesButton].forEach { view.addSubview($0) }
view.addSubview(bookInfoView) // bookInfoView 추가
titleText.snp.makeConstraints {
$0.leading.trailing.equalToSuperview().inset(20)
$0.top.equalTo(view.safeAreaLayoutGuide).offset(10)
}
seriesButton.snp.makeConstraints {
// $0.leading.trailing.equalToSuperview().inset(20)
$0.centerX.equalToSuperview()
$0.top.equalTo(titleText.snp.bottom).offset(16)
$0.width.equalTo(seriesButton.snp.height) // height에 width 고정 -> 가로, 세로 비율 유지
}
bookInfoView.snp.makeConstraints {
$0.top.equalTo(seriesButton.snp.bottom).offset(20)
$0.leading.trailing.equalTo(view.safeAreaLayoutGuide).inset(20)
}
}
}
// UIButton 상속받는 커스텀 SeriesButton 생성 : ( 레이아웃 결정 이후 cornerRadius 적용되기 때문 )
class SeriesButton: UIButton {
override func layoutSubviews() {
super.layoutSubviews()
self.layer.cornerRadius = self.frame.height / 2
self.clipsToBounds = true
}
}
// data.json 파싱 이후 titleText에 적용, book에도 적용
extension ViewController {
func loadBooks() {
dataService.loadBooks { [weak self] result in
guard let self = self else { return }
switch result {
case .success(let books):
self.books = books
if let firstBook = books.first {
self.titleText.text = firstBook.title
self.bookInfoView.configure(with: firstBook)
}
case .failure(let error):
print("에러 : \(error)")
}
}
}
}