-
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 4 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 | ||
|
||
class RecipeListViewCell: UICollectionViewCell { | ||
|
||
private let imageView = UIImageView() | ||
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. 네이밍이 좀 더 구체적이면 어떨까요? (예. recipeThumbnailView) 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. [ee35fb9] 변경했습니다 |
||
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,48 @@ | ||
// | ||
// RecipeListView.swift | ||
// HomeCafeRecipes | ||
// | ||
// Created by 김건호 on 6/10/24. | ||
// | ||
|
||
import UIKit | ||
|
||
class RecipeListView: UIView { | ||
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. [7a3ba89] 변경했습니다 |
||
|
||
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) | ||
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. collectionView를 internal로 선언한 이유가 있을까요? 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. ViewController에서 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. [22ee02b] 수정했습니다 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. delegate와 dataSource는 view에서 같이 구현 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. @GeonH0 이 부분은 아직 수정전이신가요~? 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. @f-lab-barry 내일 오전 중으로 수정 커밋 올리겠습니다! 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. |
||
|
||
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 = 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. 마진 값을 변수로 빼서 가독성을 더 높여보는건 어때요? 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. [2be84eb] 변경했습니다 |
||
layout.minimumLineSpacing = 10 | ||
layout.minimumInteritemSpacing = 10 | ||
collectionView.collectionViewLayout = layout | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,126 @@ | ||||||
// | ||||||
// 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 viewModel: RecipeListViewModel | ||||||
private var recipes: [RecipeListItemViewModel] = [] | ||||||
private let searchBar = UISearchBar() | ||||||
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. 아까 첫 커밋에서 만든 SearchBar는 어디에 쓰는거에요? 여기서 UISearchBar를 쓰고있길래 여쭤봐요! 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. [2a6372c] 수정했습니다 |
||||||
private let recipelistView = RecipeListView() | ||||||
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.
Suggested change
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. [6edb8bb] 변경했습니다 |
||||||
|
||||||
init(viewModel: RecipeListViewModel) { | ||||||
self.viewModel = viewModel | ||||||
super.init(nibName: nil, bundle: nil) | ||||||
self.viewModel.setDelegate(self) | ||||||
} | ||||||
|
||||||
required init?(coder: NSCoder) { | ||||||
fatalError("init(coder:) has not been implemented") | ||||||
} | ||||||
|
||||||
override func viewDidLoad() { | ||||||
super.viewDidLoad() | ||||||
setupUI() | ||||||
viewModel.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, constant: -25), | ||||||
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) | ||||||
]) | ||||||
|
||||||
recipelistView.collectionView.dataSource = self | ||||||
recipelistView.collectionView.delegate = self | ||||||
|
||||||
searchBar.delegate = 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.collectionView.reloadData() | ||||||
} | ||||||
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. UI를 업데이트 시키기 위해 비동기 처리가 필요하다고 판단했습니다. |
||||||
} | ||||||
|
||||||
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 = viewModel.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 { | ||||||
viewModel.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] 제거 했습니다! |
||||||
viewModel.resetSearch() | ||||||
} | ||||||
} | ||||||
|
||||||
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { | ||||||
guard let query = searchBar.text, !query.isEmpty else { | ||||||
viewModel.resetSearch() | ||||||
return | ||||||
} | ||||||
viewModel.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 | ||
|
||
class SearchBar: UIView { | ||
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. UI는 확장할 일이 없으니 final class로 작성 부탁드려요 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. [145bd41] 변경 했습니다 |
||
|
||
private let searchBar = UISearchBar() | ||
|
||
var searchText: String? { | ||
return searchBar.text | ||
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 이 없어도 될 것 같아요. 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. [db5e2ef] 제거했습니다 |
||
} | ||
|
||
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.
여기도 final 챙겨주세요~
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.
[db38695] 변경했습니다