-
Notifications
You must be signed in to change notification settings - Fork 0
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
레시피 리스트UI를 생성해보았습니다. #9
Changes from 13 commits
6764431
10c4c85
69b6ba0
b740a44
68d17da
145bd41
db5e2ef
7a3ba89
db38695
22ee02b
6edb8bb
2a6372c
2be84eb
ee35fb9
34985f7
24d0856
24a71e2
72e9fe8
cf0c78c
15f80f0
833b474
be060c4
99c274e
c0bd5a0
05b2a58
52d89ea
1a760ca
3c03347
7077713
c1bc9a0
4eb8154
e57455d
f0115b8
d3dcc4e
76fdeba
e1a3fc2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
// | ||
// RecipeListViewCell.swift | ||
// HomeCafeRecipes | ||
// | ||
// Created by 김건호 on 6/12/24. | ||
// | ||
|
||
import UIKit | ||
|
||
final class RecipeListViewCell: UICollectionViewCell { | ||
|
||
private let imageView = UIImageView() | ||
private let titleLabel = UILabel() | ||
|
||
override init(frame: CGRect) { | ||
super.init(frame: frame) | ||
setupUI() | ||
} | ||
|
||
required init?(coder: NSCoder) { | ||
fatalError("init(coder:) has not been implemented") | ||
} | ||
|
||
private func setupUI() { | ||
contentView.addSubview(imageView) | ||
contentView.addSubview(titleLabel) | ||
|
||
imageView.translatesAutoresizingMaskIntoConstraints = false | ||
titleLabel.translatesAutoresizingMaskIntoConstraints = false | ||
|
||
NSLayoutConstraint.activate([ | ||
imageView.topAnchor.constraint(equalTo: topAnchor), | ||
imageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 10), | ||
imageView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -10), | ||
imageView.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 0.75), | ||
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. 75%면 3:4 사진 비율인건가요? 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. 네 3:4입니다 |
||
|
||
titleLabel.topAnchor.constraint(equalTo: imageView.bottomAnchor, constant: 10), | ||
titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 10), | ||
titleLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -10), | ||
titleLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -10), | ||
titleLabel.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 0.15) | ||
]) | ||
|
||
titleLabel.font = .systemFont(ofSize: 16, weight: .bold) | ||
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. |
||
titleLabel.textAlignment = .center | ||
} | ||
|
||
func configure(with viewModel: RecipeListItemViewModel) { | ||
titleLabel.text = viewModel.name | ||
if let imageUrl = viewModel.imageURL { | ||
loadImage(from: imageUrl) | ||
} else { | ||
imageView.image = nil | ||
} | ||
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. |
||
|
||
imageView.contentMode = .scaleAspectFill | ||
imageView.clipsToBounds = true | ||
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. 이건 한번만 선언해주면 되는 것 같은데 configure 메서드에 있기보다 setupUI로 이동하면 어떨까요? 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. [24a71e2] 변경했습니다 |
||
} | ||
|
||
private func loadImage(from url: URL) { | ||
URLSession.shared.dataTask(with: url) { data, response, error in | ||
guard let data = data, error == nil else { return } | ||
DispatchQueue.main.async { | ||
self.imageView.image = UIImage(data: data) | ||
} | ||
}.resume() | ||
} | ||
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. ImageView 에 extension 으로 빠져있으면 다른 화면에서 재사용이 가능한 로직으로 보이는데 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. |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
// | ||
// RecipeListView.swift | ||
// HomeCafeRecipes | ||
// | ||
// Created by 김건호 on 6/10/24. | ||
// | ||
|
||
import UIKit | ||
|
||
final class RecipeListView: UIView { | ||
|
||
private let itemSize = CGSize(width: UIScreen.main.bounds.width - 20, height: 200) | ||
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.
Metrice.itemSize 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. [1a760ca] 적용했습니다 |
||
private let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) | ||
|
||
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 | ||
addSubview(collectionView) | ||
collectionView.register(RecipeListViewCell.self, forCellWithReuseIdentifier: "RecipeCell") | ||
configureCollectionView() | ||
} | ||
|
||
private func setupLayout() { | ||
collectionView.translatesAutoresizingMaskIntoConstraints = false | ||
NSLayoutConstraint.activate([ | ||
collectionView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor), | ||
collectionView.leadingAnchor.constraint(equalTo: leadingAnchor), | ||
collectionView.trailingAnchor.constraint(equalTo: trailingAnchor), | ||
collectionView.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor) | ||
]) | ||
} | ||
|
||
private func configureCollectionView() { | ||
let layout = UICollectionViewFlowLayout() | ||
layout.itemSize = itemSize | ||
layout.minimumLineSpacing = 10 | ||
layout.minimumInteritemSpacing = 10 | ||
collectionView.collectionViewLayout = layout | ||
} | ||
|
||
func setCollectionViewDataSourceDelegate<D: UICollectionViewDataSource & UICollectionViewDelegate>(_ dataSourceDelegate: D) { | ||
collectionView.dataSource = dataSourceDelegate | ||
collectionView.delegate = dataSourceDelegate | ||
} | ||
|
||
func reloadCollectionViewData() { | ||
collectionView.reloadData() | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
// | ||
// RecipeListViewController.swift | ||
// HomeCafeRecipes | ||
// | ||
// Created by 김건호 on 6/10/24. | ||
// | ||
|
||
import UIKit | ||
|
||
class RecipeListViewController: UIViewController, RecipeListViewModelDelegate { | ||
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. 여기도 final 챙겨주세요! 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. RecipeListViewModelDelegate는 extention RecipeListViewController 으로 확장해서 가독성을 높이면 어떨까요? 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 interactor: RecipeListInteractor | ||
private var recipes: [RecipeListItemViewModel] = [] | ||
private let searchBar = SearchBar() | ||
private let recipeListView = RecipeListView() | ||
|
||
init(interactor: RecipeListInteractor) { | ||
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 viewDidLoad() { | ||
super.viewDidLoad() | ||
recipeListView.setCollectionViewDataSourceDelegate(self) | ||
setupUI() | ||
interactor.viewDidLoad() | ||
} | ||
|
||
private func setupUI() { | ||
view.backgroundColor = .white | ||
view.addSubview(searchBar) | ||
view.addSubview(recipeListView) | ||
|
||
searchBar.translatesAutoresizingMaskIntoConstraints = false | ||
recipeListView.translatesAutoresizingMaskIntoConstraints = false | ||
|
||
NSLayoutConstraint.activate([ | ||
searchBar.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), | ||
searchBar.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 10), | ||
searchBar.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -10), | ||
searchBar.heightAnchor.constraint(equalToConstant: 50), | ||
|
||
recipeListView.topAnchor.constraint(equalTo: searchBar.bottomAnchor), | ||
recipeListView.leadingAnchor.constraint(equalTo: view.leadingAnchor), | ||
recipeListView.trailingAnchor.constraint(equalTo: view.trailingAnchor), | ||
recipeListView.bottomAnchor.constraint(equalTo: view.bottomAnchor) | ||
]) | ||
|
||
searchBar.setDelegate(self) | ||
} | ||
|
||
func didFetchRecipes(_ recipes: [RecipeListItemViewModel]) { | ||
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. fetchedRecipes 어떤가요? 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. [c0bd5a0] 변경했습니다 |
||
DispatchQueue.main.async { | ||
self.recipes = recipes | ||
self.recipeListView.reloadCollectionViewData() | ||
} | ||
} | ||
|
||
func didFail(with error: Error) { | ||
print("Error: \(error.localizedDescription)") | ||
} | ||
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. fetch에 실패할 때 호출하는 메서드라면 Result 타입으로 다루던걸 그대로 전달하면서 에러처리하면 어때요? 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. |
||
} | ||
|
||
extension RecipeListViewController: UICollectionViewDataSource, UICollectionViewDelegate { | ||
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { | ||
return recipes.count | ||
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. 섹션은 1개 아닌가요? 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. 각 섹션에는 recipes의 갯수 만큼 반환해야 된다 생각했습니다 |
||
} | ||
|
||
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { | ||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "RecipeCell", for: indexPath) as! RecipeListViewCell | ||
let recipeViewModel = recipes[indexPath.item] | ||
cell.configure(with: recipeViewModel) | ||
return cell | ||
} | ||
|
||
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { | ||
let recipeListItemViewModel = recipes[indexPath.item] | ||
if let recipeItemViewModel = interactor.didSelectItem(id: recipeListItemViewModel.id) { | ||
let detailVC = RecipeDetailViewController(viewModel: recipeItemViewModel) | ||
navigationController?.pushViewController(detailVC, animated: true) | ||
} else { | ||
let RecipeIDErrorAlert = UIAlertController(title: "오류", message: "해당 정보를 찾지 못했습니다.", preferredStyle: .alert) | ||
let okAction = UIAlertAction(title: "OK", style: .default) { _ in | ||
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. 핸들러에 [weak self] 필요하지 않을까요? 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. [52d89ea] 수정했습니다 |
||
self.navigationController?.popToRootViewController(animated: true) | ||
} | ||
RecipeIDErrorAlert.addAction(okAction) | ||
present(RecipeIDErrorAlert, animated: true, completion: nil) | ||
} | ||
} | ||
|
||
func scrollViewDidScroll(_ scrollView: UIScrollView) { | ||
let offsetY = scrollView.contentOffset.y | ||
let contentHeight = scrollView.contentSize.height | ||
let height = scrollView.frame.size.height | ||
|
||
if offsetY > contentHeight - height { | ||
interactor.fetchNextPage() | ||
} | ||
} | ||
} | ||
|
||
extension RecipeListViewController: UISearchBarDelegate { | ||
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) { | ||
if searchBar.text?.count == 0 { | ||
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. String의 extenstion에 isBlank 프러퍼티를 추가해 trim 하고 비어있는지 검토하는 로직을 넣어보는건 어때요? 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. 코드에 이상은 없지만 불필요한 return 빼주시는게 swift 컨벤션에 더 맞을 것 같아요~ 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. [3c03347] 제거 했습니다! |
||
interactor.resetSearch() | ||
} | ||
} | ||
|
||
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { | ||
guard let query = searchBar.text, !query.isEmpty else { | ||
interactor.resetSearch() | ||
return | ||
} | ||
interactor.searchRecipes(with: query) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
// | ||
// SearchBar.swift | ||
// HomeCafeRecipes | ||
// | ||
// Created by 김건호 on 6/10/24. | ||
// | ||
|
||
import UIKit | ||
|
||
final class SearchBar: UIView { | ||
|
||
private let searchBar = UISearchBar() | ||
|
||
var searchText: String? { | ||
searchBar.text | ||
} | ||
|
||
override init(frame: CGRect) { | ||
super.init(frame: frame) | ||
setupUI() | ||
} | ||
|
||
required init?(coder: NSCoder) { | ||
fatalError("init(coder:) has not been implemented") | ||
} | ||
|
||
private func setupUI() { | ||
addSubview(searchBar) | ||
searchBar.translatesAutoresizingMaskIntoConstraints = false | ||
NSLayoutConstraint.activate([ | ||
searchBar.topAnchor.constraint(equalTo: self.topAnchor), | ||
searchBar.leadingAnchor.constraint(equalTo: self.leadingAnchor), | ||
searchBar.trailingAnchor.constraint(equalTo: self.trailingAnchor), | ||
searchBar.bottomAnchor.constraint(equalTo: self.bottomAnchor) | ||
]) | ||
} | ||
|
||
func setDelegate(_ delegate: UISearchBarDelegate) { | ||
searchBar.delegate = delegate | ||
} | ||
|
||
} |
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.
네이밍이 좀 더 구체적이면 어떨까요? (예. recipeThumbnailView)
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.
[ee35fb9] 변경했습니다