diff --git a/ONMIR.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ONMIR.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 837c335..0bb79a2 100644 --- a/ONMIR.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ONMIR.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -138,4 +138,4 @@ } ], "version" : 3 -} +} \ No newline at end of file diff --git a/ONMIR/Assets.xcassets/ButtonBackground.colorset/Contents.json b/ONMIR/Assets.xcassets/ButtonBackground.colorset/Contents.json new file mode 100644 index 0000000..0c600f9 --- /dev/null +++ b/ONMIR/Assets.xcassets/ButtonBackground.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x00", + "green" : "0x00", + "red" : "0x00" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFF" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ONMIR/Assets.xcassets/ButtonText.colorset/Contents.json b/ONMIR/Assets.xcassets/ButtonText.colorset/Contents.json new file mode 100644 index 0000000..802fa68 --- /dev/null +++ b/ONMIR/Assets.xcassets/ButtonText.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFE" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x00", + "green" : "0x00", + "red" : "0x00" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ONMIR/Feature/Home/Components/BookListSectionRowView.swift b/ONMIR/Feature/Home/Components/BookListSectionRowView.swift new file mode 100644 index 0000000..6c35c3e --- /dev/null +++ b/ONMIR/Feature/Home/Components/BookListSectionRowView.swift @@ -0,0 +1,113 @@ +import SnapKit +import UIKit + +final class BookListSectionRowView: UIControl { + private let bookListStackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .vertical + stackView.spacing = 12 + stackView.alignment = .leading + stackView.distribution = .fill + return stackView + }() + + private let listContentStackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .horizontal + stackView.spacing = 12 + stackView.alignment = .center + stackView.distribution = .fill + return stackView + }() + + private let divider1 = DividerView() + private let divider2 = DividerView() + + private let listIcon: UIImageView = { + let imageview = UIImageView() + imageview.contentMode = .scaleAspectFit + imageview.backgroundColor = .clear + imageview.image = UIImage(systemName: "list.bullet") + imageview.tintColor = .label + return imageview + }() + + private let listTitleLabel: UILabel = { + let label = UILabel() + label.text = "All" + label.font = .systemFont(ofSize: 16) + return label + }() + + private let chevronRightIcon: UIImageView = { + let imageview = UIImageView() + imageview.contentMode = .scaleAspectFit + imageview.backgroundColor = .clear + imageview.image = UIImage(systemName: "chevron.right") + imageview.tintColor = UIColor.quaternaryLabel + return imageview + }() + + init(title: String) { + super.init(frame: .zero) + self.listTitleLabel.text = title + + setupView() + setupLayout() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func touchesEnded(_ touches: Set, with event: UIEvent?) { + guard let touch = touches.first else { return } + let location = touch.location(in: self) + if bounds.contains(location) { + sendActions(for: .touchUpInside) + } + } + + private func setupView() { + self.backgroundColor = .clear + + isUserInteractionEnabled = true + + listContentStackView.addArrangedSubview(listIcon) + listContentStackView.addArrangedSubview(listTitleLabel) + listContentStackView.addArrangedSubview(chevronRightIcon) + + bookListStackView.addArrangedSubview(divider1) + bookListStackView.addArrangedSubview(listContentStackView) + bookListStackView.addArrangedSubview(divider2) + + addSubview(bookListStackView) + } + + private func setupLayout() { + bookListStackView.snp.makeConstraints { make in + make.top.leading.trailing.bottom.equalToSuperview() + } + + listContentStackView.snp.makeConstraints { make in + make.leading.trailing.equalToSuperview() + } + + listIcon.snp.makeConstraints { make in + make.width.height.equalTo(24) + } + + chevronRightIcon.snp.makeConstraints { make in + make.width.equalTo(12) + make.height.equalTo(17) + } + + divider1.snp.makeConstraints { make in + make.leading.trailing.equalToSuperview() + } + + divider2.snp.makeConstraints { make in + make.leading.trailing.equalToSuperview() + } + } +} diff --git a/ONMIR/Feature/Home/Components/DividerView.swift b/ONMIR/Feature/Home/Components/DividerView.swift new file mode 100644 index 0000000..be8a560 --- /dev/null +++ b/ONMIR/Feature/Home/Components/DividerView.swift @@ -0,0 +1,17 @@ +import UIKit + +final class DividerView: UIView { + init(height: CGFloat = 1.0, color: UIColor = UIColor.quaternaryLabel) { + super.init(frame: .zero) + + self.backgroundColor = color + self.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + self.heightAnchor.constraint(equalToConstant: height) + ]) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/ONMIR/Feature/Home/Components/ReadingBookCell.swift b/ONMIR/Feature/Home/Components/ReadingBookCell.swift new file mode 100644 index 0000000..64d1491 --- /dev/null +++ b/ONMIR/Feature/Home/Components/ReadingBookCell.swift @@ -0,0 +1,73 @@ +import Nuke +import SnapKit +import UIKit + +extension HomeViewController { + final class ReadingBookCell: UICollectionViewCell { + static let id: String = "ReadingBookCell" + private let coverImageView: UIImageView = { + let imageView = UIImageView() + imageView.contentMode = .scaleAspectFill + imageView.clipsToBounds = true + return imageView + }() + + private let readingProgressLabel: UILabel = { + let label = UILabel() + label.font = .systemFont(ofSize: 27, weight: .bold) + return label + }() + + private var imageDownloadTask: Task? + + override init(frame: CGRect) { + super.init(frame: frame) + setupUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func prepareForReuse() { + super.prepareForReuse() + imageDownloadTask?.cancel() + } + + private func setupUI() { + self.backgroundColor = .clear + contentView.backgroundColor = .clear + + contentView.addSubview(coverImageView) + contentView.addSubview(readingProgressLabel) + + coverImageView.snp.makeConstraints { make in + make.leading.trailing.equalToSuperview() + make.height.equalTo(392) + } + + readingProgressLabel.snp.makeConstraints { make in + make.top.equalTo(coverImageView.snp.bottom).offset(8) + make.leading.equalToSuperview() + } + } + + func prepare(imageURL: URL?, currentPage: Int, totalPage: Int) { + let percentage = Int(Double(currentPage) / Double(totalPage) * 100) + self.readingProgressLabel.text = "\(percentage)%" + + if let thumbnailURL = imageURL { + imageDownloadTask = Task { + do { + let image = try await ImagePipeline.shared.image(for: thumbnailURL) + if !Task.isCancelled { + self.coverImageView.image = image + } + } catch { + Logger.error(error) + } + } + } + } + } +} diff --git a/ONMIR/Feature/Home/HomeViewController.swift b/ONMIR/Feature/Home/HomeViewController.swift new file mode 100644 index 0000000..40aba36 --- /dev/null +++ b/ONMIR/Feature/Home/HomeViewController.swift @@ -0,0 +1,151 @@ +import SnapKit +import UIKit + +public final class HomeViewController: UIViewController { + private let bookListSectionRowView: BookListSectionRowView = { + let view = BookListSectionRowView(title: "All") + view.isUserInteractionEnabled = true + return view + }() + + private lazy var readingBookCollectionView: UICollectionView = { + let layout = createCompositionalLayout() + let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) + collectionView.backgroundColor = .clear + collectionView.showsHorizontalScrollIndicator = false + collectionView.dataSource = self + collectionView.delegate = self + collectionView.register(HomeViewController.ReadingBookCell.self, forCellWithReuseIdentifier: HomeViewController.ReadingBookCell.id) + return collectionView + }() + + private let addBookButton: UIButton = { + let button = UIButton() + button.setImage(UIImage(systemName: "plus"), for: .normal) + button.backgroundColor = UIColor(named: "ButtonBackground") + button.tintColor = UIColor(named: "ButtonText") + button.layer.cornerRadius = 30 + return button + }() + + private let viewModel = HomeViewModel() + + public override func viewDidLoad() { + super.viewDidLoad() + + setupUI() + setupTarget() + setupNavigationBar() + updateContentUnavailableView(isEmpty: viewModel.books.isEmpty) + } + + private func setupNavigationBar() { + navigationItem.title = "Library" + navigationItem.largeTitleDisplayMode = .automatic + navigationController?.navigationBar.prefersLargeTitles = true + } + + private func setupUI() { + view.backgroundColor = .secondarySystemBackground + + view.addSubview(bookListSectionRowView) + view.addSubview(addBookButton) + view.addSubview(readingBookCollectionView) + + bookListSectionRowView.snp.makeConstraints { make in + make.top.equalTo(view.safeAreaLayoutGuide).inset(24) + make.leading.trailing.equalToSuperview().inset(16) + } + + readingBookCollectionView.snp.makeConstraints { make in + make.top.equalTo(bookListSectionRowView.snp.bottom).offset(30) + make.leading.equalToSuperview().inset(50) + make.trailing.bottom.equalToSuperview() + } + + addBookButton.snp.makeConstraints { make in + make.bottom.equalTo(view.safeAreaLayoutGuide).inset(20) + make.trailing.equalToSuperview().inset(16) + make.height.width.equalTo(60) + } + } + + private func setupTarget() { + bookListSectionRowView.addTarget(self, action: #selector(didTapBookListView), for: .touchUpInside) + addBookButton.addTarget(self, action: #selector(didTapAddBookButton), for: .touchUpInside) + } + + private func createCompositionalLayout() -> UICollectionViewCompositionalLayout { + return UICollectionViewCompositionalLayout { sectionIndex, environment in + let itemSize = NSCollectionLayoutSize( + widthDimension: .fractionalWidth(1.0), + heightDimension: .fractionalHeight(1.0) + ) + let item = NSCollectionLayoutItem(layoutSize: itemSize) + + let groupSize = NSCollectionLayoutSize( + widthDimension: .fractionalWidth(0.7), + heightDimension: .fractionalHeight(1.0) + ) + let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) + + let section = NSCollectionLayoutSection(group: group) + section.orthogonalScrollingBehavior = .groupPagingCentered + section.interGroupSpacing = 50 + section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 50) + return section + } + } + + private func updateContentUnavailableView(isEmpty: Bool) { + if isEmpty { + var configuration = UIContentUnavailableConfiguration.empty() + configuration.image = .init(systemName: "book") + configuration.text = "No book in progress" + configuration.secondaryText = "Books you're currently reading will appear here." + configuration.button.title = "Add Book" + configuration.button.background.backgroundColor = .systemBlue + configuration.button.baseForegroundColor = .white + + configuration.buttonProperties.primaryAction = UIAction(title: "Add Book") { [weak self] _ in + self?.didTapAddBookButton() + } + + readingBookCollectionView.isHidden = true + + self.contentUnavailableConfiguration = configuration + } else { + self.contentUnavailableConfiguration = nil + } + } + + @objc private func didTapBookListView() { + print("bookListView Tapped!") + } + + @objc private func didTapAddBookButton() { + let destination = NewBookViewController { self.dismiss(animated: true) } + destination.modalPresentationStyle = .popover + self.present(destination, animated: true) + } +} + +extension HomeViewController: UICollectionViewDelegate, UICollectionViewDataSource { + public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: HomeViewController.ReadingBookCell.id, for: indexPath) as? HomeViewController.ReadingBookCell else { + return UICollectionViewCell() + } + + let book = viewModel.books[indexPath.item] + cell.prepare( + imageURL: book.imageURL, + currentPage: book.currentPage, + totalPage: book.totalPage + ) + return cell + } + + public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return viewModel.books.count + } +} diff --git a/ONMIR/Feature/Home/HomeViewModel.swift b/ONMIR/Feature/Home/HomeViewModel.swift new file mode 100644 index 0000000..0529abd --- /dev/null +++ b/ONMIR/Feature/Home/HomeViewModel.swift @@ -0,0 +1,11 @@ +import Foundation + +struct ReadingBookInfo { + let imageURL: URL? + let currentPage: Int + let totalPage: Int +} + +final class HomeViewModel { + var books: [ReadingBookInfo] = [] +} diff --git a/ONMIR/SceneDelegate.swift b/ONMIR/SceneDelegate.swift index 5deb6aa..82381bb 100644 --- a/ONMIR/SceneDelegate.swift +++ b/ONMIR/SceneDelegate.swift @@ -16,7 +16,7 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate { ) { guard let windowScence = (scene as? UIWindowScene) else { return } window = UIWindow(windowScene: windowScence) - window?.rootViewController = UINavigationController(rootViewController: UIViewController()) + window?.rootViewController = UINavigationController(rootViewController: HomeViewController()) window?.makeKeyAndVisible() } diff --git a/ONMIR/ViewController.swift b/ONMIR/ViewController.swift deleted file mode 100644 index 353dab9..0000000 --- a/ONMIR/ViewController.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// ViewController.swift -// ONMIR -// -// Created by 정윤서 on 4/20/25. -// - -import UIKit - -class ViewController: UIViewController { - - override func viewDidLoad() { - super.viewDidLoad() - // Do any additional setup after loading the view. - } - - -} -