Skip to content

레시피 상세화면 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

Merged
merged 29 commits into from
Jul 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
efaab96
Feat: RecipeDetailViewModel 정의
GeonH0 Jul 2, 2024
27b024e
Feat: RecipeDetailView 정의
GeonH0 Jul 2, 2024
c566617
Feat: RecipeDetailViewController 정의
GeonH0 Jul 2, 2024
6f92e1a
Fix: mapToRecipeItemViewModel에서 mapToRecipeDetailViewModel로 수정
GeonH0 Jul 2, 2024
80b6640
Feat: CustomNavigationBar 정의
GeonH0 Jul 2, 2024
2821348
Feat: DetailView에 들어갈 Fonts 정의
GeonH0 Jul 3, 2024
1f1c396
Fix: DeatilView에 들어갈 Fonts 적용
GeonH0 Jul 3, 2024
9881443
Fix: ViewModel 네이밍 변경
GeonH0 Jul 4, 2024
fa15e8f
Fix: 변경된 네이밍 적용
GeonH0 Jul 4, 2024
d508f7e
Fix: 써드파티와 퍼스트파티 사이 한줄 띄우기 적용
GeonH0 Jul 4, 2024
092afac
Fix: recipeNameLabel로 네이밍 수정
GeonH0 Jul 4, 2024
1b83f41
Fix: recipeDescriptionLabel로 네이밍 수정
GeonH0 Jul 4, 2024
a78dd53
Fix: recipeImageUrls로 네이밍 수정
GeonH0 Jul 4, 2024
18901d0
Fix: recipeimageUrls를 스크롤뷰를 구성하는데 바로 전달해서 사용
GeonH0 Jul 4, 2024
75ce3f8
Fix: for - in 반복문에서 forEach로 변경
GeonH0 Jul 4, 2024
efc1c64
Fix: imagesAdded를 사용하여 이미지뷰 중복 추가 방지
GeonH0 Jul 4, 2024
4830cf5
Fix: clipsToBounds 제거
GeonH0 Jul 4, 2024
0832b39
Fix: setupUI 메서드를 setupLayout,setupScrollView,setupPageControl,setupL…
GeonH0 Jul 4, 2024
60fa4e2
Fix: contentView로 네이밍 변경
GeonH0 Jul 4, 2024
61a0df9
Fix: 써드파티 사이 개행 추가
GeonH0 Jul 4, 2024
690680d
Fix: RecipeDetailViewController에 view = contentView 처리, customNavigat…
GeonH0 Jul 5, 2024
beca64c
Fix: mark 주석 한 줄 개행
GeonH0 Jul 5, 2024
1c0bb26
Fix: mapper 인스턴스 생성 및 적용
GeonH0 Jul 5, 2024
e3b3ad9
Fix: DetailTitleFont로 네이밍 수정
GeonH0 Jul 5, 2024
a82aabd
Fix: 불필요한 비동기처리 제거
GeonH0 Jul 5, 2024
ca4c88a
Add: project.pbxproj 추가
GeonH0 Jul 5, 2024
14ecdf6
Fix: 카멜케이스 적용 및 사용하지 않는 ViewModel 제거
GeonH0 Jul 5, 2024
557a785
Fix: 기존 등록한 이미지 제거후 다시 addSubview 하는 로직으로 변경
GeonH0 Jul 8, 2024
4e56909
Merge branch 'main' into feature/FeedDetailView
GeonH0 Jul 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 150 additions & 0 deletions HomeCafeRecipes/HomeCafeRecipes.xcodeproj/project.pbxproj

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

화면을 꽉 채우고 싶으신 것 같은데 UIScreen.main에 접근하는 방법 말고는 없을까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

self.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)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이렇게 스크롤뷰를 셋업할 때마다 이미지뷰를 추가해주는 거라면
setupScrollView() 가 의도치 않게 두번 호출되면 어떻게 되는걸까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그 부분을 생각 못한거 같습니다. 이미지뷰가 추가 되었는지 플래그를 사용하여 방지하게 수정하도록 하겠습니다

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[efc1c64] 수정했습니다

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

두번째 호출 된 것이 더 올바른 이미지일 수 있지 않을까요?
첫 진입을 막는 것보다 몇번을 호출되더라도 안정적으로 최종적으로 등록한 이미지가 보이도록
기존에 등록한 이미지를 제거한 후(removeFromSuperView) addSubview를 다시 호출해줘야할 것 같다는 의견이었어요.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

기존에 등록한 이미지를 제거후 다시 호출하는 메서드로 변경시키겠습니다

Choose a reason for hiding this comment

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반올림하면 페이지 수 계산에 문제 없나요?

Copy link
Collaborator Author

@GeonH0 GeonH0 Jul 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이미지를 반 이상 넘기면 바로바로 변하기 위해 넣은 건데 페이지 수 계산에는 문제가 없는데 제거 하는게 좋을까요?

Choose a reason for hiding this comment

The 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()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

disposeBag을 let으로 들고있어도 문제 없나요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

disposeBag이 일회용 구독이라 괜찮을거 같아요

Choose a reason for hiding this comment

The 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 {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mark 주석 아래 한 줄 개행 해주세요~

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ struct RecipeListMapper {
return recipes.map { RecipeListItemViewModel(recipe: $0) }
}

func mapToRecipeItemViewModel(from recipe: Recipe) -> RecipeItemViewModel {
return RecipeItemViewModel(recipe: recipe)
func mapToRecipeDetailViewModel(from recipe: Recipe) -> RecipeDetailViewModel {
return RecipeDetailViewModel(recipe: recipe)
}
}
2 changes: 2 additions & 0 deletions HomeCafeRecipes/HomeCafeRecipes/Resources/Fonts.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ import UIKit
struct Fonts {
static let titleFont: UIFont = .systemFont(ofSize: 16, weight: .bold)
static let bodyFont: UIFont = .systemFont(ofSize: 14, weight: .regular)
static let detailTitleFont: UIFont = .systemFont(ofSize: 24, weight: .bold)
static let detailBodyFont: UIFont = .systemFont(ofSize: 16, weight: .regular)
}
Loading