ios 18일차

bin·2026년 1월 22일

회고

오늘은 본격적으로 과제를 시작했다. 조금씩 나눠서 기록해보자

프로젝트에 관한 기록

현재, 구현 5까지 완성했지만, 나누어서 기록하고자 오늘은 구현2까지를 기록할 것이다.

구현 1

  • 구현 1의 요구사항은 책 제목 표시 Label , 시리즈 버튼 생성, data.json을 이용하여 데이터 랜더링이다.

메인 화면은 ViewController를 사용하여 구현했으며, BookInfo.swift에 data.json의 데이터를 저장할 객체 생성, DataService.swift에서 data.json을 파싱하는 로직을 사용하여 ViewController에서 파싱된 데이터를 처리하여 랜더링한다.

ViewController.swift

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)")
             }
         }
     }

BookInfo.swift

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"
        }

DataService.swift

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))
        }
    }
}

구현 2

  • 구현 2의 요구사항은 UIStackView를 사용하여 책 정보 영역을 표시한는 것이다. 구현 1과 마찬가지로 랜더링할 데이터의 정보는 data.json에서 파싱되어 넘어오는 것을 사용하도록 구현해야 했으며, 구조를 잡기가 매우 힘들었다. 다른 방법이 존재하는 지는 모르지만, 본인은 스택뷰를 여러개의 스택뷰를 합쳐서 만들도록 구현했으며, 처음해보는 작업이라 태블릿으로 그림을 그려가며 작업했다.

BookInfoStackView.swift

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.swift 수정

만들어둔 책 정보 영역을 표시하기 위해 기존의 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)")
             }
         }
     }
}

0개의 댓글