-
Notifications
You must be signed in to change notification settings - Fork 0
레시피 상세화면 UI를 정의해 보았습니다. #12
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
efaab96
27b024e
c566617
6f92e1a
80b6640
2821348
1f1c396
9881443
fa15e8f
d508f7e
092afac
1b83f41
a78dd53
18901d0
75ce3f8
efc1c64
4830cf5
0832b39
60fa4e2
61a0df9
690680d
beca64c
1c0bb26
e3b3ad9
a82aabd
ca4c88a
14ecdf6
557a785
4e56909
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
// | ||
// CustomNavigationBar.swift | ||
// HomeCafeRecipes | ||
// | ||
// Created by 김건호 on 6/22/24. | ||
// | ||
|
||
import UIKit | ||
|
||
final class CustomNavigationBar: UIView { | ||
|
||
private let titleLabel = UILabel() | ||
let backButton = UIButton(type: .system) | ||
|
||
override init(frame: CGRect) { | ||
super.init(frame: frame) | ||
setupUI() | ||
} | ||
|
||
required init?(coder: NSCoder) { | ||
fatalError("init(coder:) has not been implemented") | ||
} | ||
|
||
private func setupUI() { | ||
|
||
backButton.setImage(UIImage(systemName: "chevron.backward"), for: .normal) | ||
backButton.tintColor = .black | ||
addSubview(backButton) | ||
|
||
titleLabel.font = UIFont.systemFont(ofSize: 18, weight: .bold) | ||
titleLabel.textAlignment = .center | ||
addSubview(titleLabel) | ||
|
||
backButton.translatesAutoresizingMaskIntoConstraints = false | ||
titleLabel.translatesAutoresizingMaskIntoConstraints = false | ||
|
||
NSLayoutConstraint.activate([ | ||
backButton.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16), | ||
backButton.centerYAnchor.constraint(equalTo: centerYAnchor), | ||
|
||
titleLabel.centerXAnchor.constraint(equalTo: centerXAnchor), | ||
titleLabel.centerYAnchor.constraint(equalTo: centerYAnchor) | ||
]) | ||
} | ||
|
||
func setTitle(_ title: String) { | ||
titleLabel.text = title | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
// | ||
// RecipeItemViewModel.swift | ||
// HomeCafeRecipes | ||
// | ||
// Created by 김건호 on 6/13/24. | ||
// | ||
|
||
import Foundation | ||
|
||
struct RecipeDetailViewModel { | ||
let id: Int | ||
let recipeName: String | ||
let recipeDescription: String | ||
let recipeImageUrls: [URL] | ||
let isLiked: Bool | ||
|
||
init(recipe: Recipe) { | ||
self.id = recipe.id | ||
self.recipeName = recipe.name | ||
self.recipeDescription = recipe.description | ||
self.recipeImageUrls = recipe.imageUrls.compactMap { URL(string: $0) } | ||
self.isLiked = recipe.isLikedByCurrentUser | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
// | ||
// RecipeDetailView.swift | ||
// HomeCafeRecipes | ||
// | ||
// Created by 김건호 on 6/13/24. | ||
// | ||
|
||
import UIKit | ||
|
||
import Kingfisher | ||
|
||
final class RecipeDetailView: UIView { | ||
|
||
let customNavigationBar = CustomNavigationBar() | ||
private let scrollView = UIScrollView() | ||
private let pageControl = UIPageControl() | ||
private let recipeNameLabel = UILabel() | ||
private let recipeDescriptionLabel = UILabel() | ||
private let photoIndexLabel = UILabel() | ||
private var imagesAdded = false | ||
|
||
override init(frame: CGRect) { | ||
super.init(frame: frame) | ||
setupUI() | ||
setupLayout() | ||
} | ||
|
||
required init?(coder: NSCoder) { | ||
fatalError("init(coder:) has not been implemented") | ||
} | ||
|
||
private func setupUI() { | ||
backgroundColor = .white | ||
setupNavigationBar() | ||
setupScrollView() | ||
setupPageControl() | ||
setupLabels() | ||
} | ||
|
||
private func setupNavigationBar() { | ||
addSubview(customNavigationBar) | ||
customNavigationBar.translatesAutoresizingMaskIntoConstraints = false | ||
} | ||
|
||
private func setupScrollView() { | ||
addSubview(scrollView) | ||
scrollView.translatesAutoresizingMaskIntoConstraints = false | ||
scrollView.isPagingEnabled = true | ||
scrollView.showsHorizontalScrollIndicator = false | ||
scrollView.delegate = self | ||
} | ||
|
||
private func setupPageControl() { | ||
addSubview(pageControl) | ||
pageControl.translatesAutoresizingMaskIntoConstraints = false | ||
} | ||
|
||
private func setupLabels() { | ||
addSubview(recipeNameLabel) | ||
addSubview(recipeDescriptionLabel) | ||
addSubview(photoIndexLabel) | ||
|
||
recipeNameLabel.translatesAutoresizingMaskIntoConstraints = false | ||
recipeDescriptionLabel.translatesAutoresizingMaskIntoConstraints = false | ||
photoIndexLabel.translatesAutoresizingMaskIntoConstraints = false | ||
|
||
recipeNameLabel.font = Fonts.detailTitleFont | ||
recipeNameLabel.numberOfLines = 0 | ||
recipeDescriptionLabel.font = Fonts.detailBodyFont | ||
recipeDescriptionLabel.numberOfLines = 0 | ||
photoIndexLabel.font = Fonts.detailBodyFont | ||
} | ||
|
||
private func setupLayout() { | ||
NSLayoutConstraint.activate([ | ||
customNavigationBar.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor), | ||
customNavigationBar.leadingAnchor.constraint(equalTo: leadingAnchor), | ||
customNavigationBar.trailingAnchor.constraint(equalTo: trailingAnchor), | ||
customNavigationBar.heightAnchor.constraint(equalToConstant: 44), | ||
|
||
scrollView.topAnchor.constraint(equalTo: customNavigationBar.bottomAnchor, constant: 10), | ||
scrollView.leadingAnchor.constraint(equalTo: leadingAnchor), | ||
scrollView.trailingAnchor.constraint(equalTo: trailingAnchor), | ||
scrollView.heightAnchor.constraint(equalToConstant: 200), | ||
|
||
pageControl.topAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: 10), | ||
pageControl.centerXAnchor.constraint(equalTo: centerXAnchor), | ||
|
||
photoIndexLabel.topAnchor.constraint(equalTo: pageControl.bottomAnchor, constant: 10), | ||
photoIndexLabel.centerXAnchor.constraint(equalTo: centerXAnchor), | ||
|
||
recipeNameLabel.topAnchor.constraint(equalTo: photoIndexLabel.bottomAnchor, constant: 20), | ||
recipeNameLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20), | ||
recipeNameLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20), | ||
|
||
recipeDescriptionLabel.topAnchor.constraint(equalTo: recipeNameLabel.bottomAnchor, constant: 20), | ||
recipeDescriptionLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20), | ||
recipeDescriptionLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20) | ||
]) | ||
} | ||
|
||
|
||
func configure(with viewModel: RecipeDetailViewModel) { | ||
recipeNameLabel.text = viewModel.recipeName | ||
recipeDescriptionLabel.text = viewModel.recipeDescription | ||
setupScrollViewContent(with: viewModel.recipeImageUrls) | ||
pageControl.numberOfPages = viewModel.recipeImageUrls.count | ||
updatePhotoIndexLabel(currentPage: 0) | ||
} | ||
|
||
private func setupScrollViewContent(with recipeImageUrls: [URL]) { | ||
scrollView.subviews.forEach { $0.removeFromSuperview() } | ||
|
||
let imageViewWidth = UIScreen.main.bounds.width | ||
|
||
recipeImageUrls.enumerated().forEach { index, url in | ||
let imageView = UIImageView() | ||
imageView.kf.setImage(with: url) | ||
imageView.contentMode = .scaleAspectFill | ||
|
||
let xPos = CGFloat(index) * imageViewWidth | ||
imageView.frame = CGRect(x: xPos, y: 0, width: imageViewWidth, height: 200) | ||
scrollView.addSubview(imageView) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이렇게 스크롤뷰를 셋업할 때마다 이미지뷰를 추가해주는 거라면 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 그 부분을 생각 못한거 같습니다. 이미지뷰가 추가 되었는지 플래그를 사용하여 방지하게 수정하도록 하겠습니다 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [efc1c64] 수정했습니다 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 두번째 호출 된 것이 더 올바른 이미지일 수 있지 않을까요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 기존에 등록한 이미지를 제거후 다시 호출하는 메서드로 변경시키겠습니다 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 확인했습니다~ |
||
} | ||
|
||
let contentWidth = imageViewWidth * CGFloat(recipeImageUrls.count) | ||
scrollView.contentSize = CGSize(width: contentWidth, height: 200) | ||
} | ||
|
||
private func updatePhotoIndexLabel(currentPage: Int) { | ||
photoIndexLabel.text = "\(currentPage + 1) / \(pageControl.numberOfPages)" | ||
} | ||
} | ||
|
||
extension RecipeDetailView: UIScrollViewDelegate { | ||
func scrollViewDidScroll(_ scrollView: UIScrollView) { | ||
let pageIndex = round(scrollView.contentOffset.x / UIScreen.main.bounds.width) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 반올림하면 페이지 수 계산에 문제 없나요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이미지를 반 이상 넘기면 바로바로 변하기 위해 넣은 건데 페이지 수 계산에는 문제가 없는데 제거 하는게 좋을까요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 의도하신거라면 괜찮습니다! |
||
pageControl.currentPage = Int(pageIndex) | ||
updatePhotoIndexLabel(currentPage: Int(pageIndex)) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
// | ||
// RecipeDetailViewController.swift | ||
// HomeCafeRecipes | ||
// | ||
// Created by 김건호 on 6/14/24. | ||
// | ||
|
||
import UIKit | ||
|
||
import RxSwift | ||
|
||
final class RecipeDetailViewController: UIViewController { | ||
|
||
private let contentView = RecipeDetailView() | ||
private let customNavigationBar = CustomNavigationBar() | ||
private let interactor: RecipeDetailInteractor | ||
private let disposeBag = DisposeBag() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. disposeBag을 let으로 들고있어도 문제 없나요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. disposeBag이 일회용 구독이라 괜찮을거 같아요 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 취소는 필요하지 않나보군요 넵! |
||
private var recipeDetailViewModel: RecipeDetailViewModel? | ||
private let recipeListMapper = RecipeListMapper() | ||
|
||
init(interactor: RecipeDetailInteractor) { | ||
self.interactor = interactor | ||
super.init(nibName: nil, bundle: nil) | ||
self.interactor.setDelegate(self) | ||
} | ||
|
||
required init?(coder: NSCoder) { | ||
fatalError("init(coder:) has not been implemented") | ||
} | ||
|
||
override func loadView() { | ||
view = contentView | ||
} | ||
|
||
override func viewDidLoad() { | ||
super.viewDidLoad() | ||
interactor.viewDidLoad() | ||
contentView.customNavigationBar.backButton.addTarget(self, action: #selector(backButtonTapped), for: .touchUpInside) | ||
} | ||
|
||
private func displayError(_ error: Error) { | ||
let alert = UIAlertController(title: "해당 레시피를 로드하는데 실패했습니다.", message: error.localizedDescription, preferredStyle: .alert) | ||
alert.addAction(UIAlertAction(title: "OK", style: .default)) | ||
present(alert, animated: true) | ||
} | ||
|
||
@objc private func backButtonTapped() { | ||
navigationController?.popViewController(animated: true) | ||
} | ||
} | ||
|
||
// MARK: - RecipeDetailInteractorDelegate | ||
|
||
extension RecipeDetailViewController: RecipeDetailInteractorDelegate { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. mark 주석 아래 한 줄 개행 해주세요~ There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [beca64c] 수정했습니다 |
||
func fetchedRecipe(result: Result<Recipe, Error>) { | ||
switch result { | ||
case .success(let recipe): | ||
let recipeItemViewModel = recipeListMapper.mapToRecipeDetailViewModel(from: recipe) | ||
DispatchQueue.main.async { | ||
self.contentView.configure(with: recipeItemViewModel) | ||
} | ||
case .failure(let error): | ||
self.displayError(error) | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
화면을 꽉 채우고 싶으신 것 같은데 UIScreen.main에 접근하는 방법 말고는 없을까요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
self.bounds.width 를 사용하는 방법도 있을거 같아요!