Skip to content

Commit

Permalink
Merge pull request #2 from Neph3779/compositionalLayout
Browse files Browse the repository at this point in the history
[Refactor] Compositional layout 적용
  • Loading branch information
Neph3779 committed Sep 11, 2022
2 parents 0a3f9fc + 9ff49ef commit 625561b
Show file tree
Hide file tree
Showing 7 changed files with 245 additions and 79 deletions.
29 changes: 27 additions & 2 deletions BuyOrNot/BuyOrNot.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@

/* Begin PBXBuildFile section */
3222C27F2756A9C300921B04 /* UIViewControllerExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3222C27E2756A9C300921B04 /* UIViewControllerExtension.swift */; };
323C512028CDC52500280836 /* Then in Frameworks */ = {isa = PBXBuildFile; productRef = 323C511F28CDC52500280836 /* Then */; };
323C512228CDDAAF00280836 /* HomeCollectionHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 323C512128CDDAAF00280836 /* HomeCollectionHeaderView.swift */; };
323C512428CDF5F000280836 /* HomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 323C512328CDF5F000280836 /* HomeViewModel.swift */; };
3245B3222757801800DF2F2F /* YoutubeCrawler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3245B3212757801800DF2F2F /* YoutubeCrawler.swift */; };
3245B3382757C01300DF2F2F /* YoutubeCrawlingResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3245B3372757C01300DF2F2F /* YoutubeCrawlingResult.swift */; };
3245B33C2757F73700DF2F2F /* YoutubeVideosResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3245B33B2757F73700DF2F2F /* YoutubeVideosResult.swift */; };
Expand Down Expand Up @@ -58,6 +61,8 @@

/* Begin PBXFileReference section */
3222C27E2756A9C300921B04 /* UIViewControllerExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewControllerExtension.swift; sourceTree = "<group>"; };
323C512128CDDAAF00280836 /* HomeCollectionHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeCollectionHeaderView.swift; sourceTree = "<group>"; };
323C512328CDF5F000280836 /* HomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewModel.swift; sourceTree = "<group>"; };
3245B3212757801800DF2F2F /* YoutubeCrawler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YoutubeCrawler.swift; sourceTree = "<group>"; };
3245B3372757C01300DF2F2F /* YoutubeCrawlingResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YoutubeCrawlingResult.swift; sourceTree = "<group>"; };
3245B33B2757F73700DF2F2F /* YoutubeVideosResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YoutubeVideosResult.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -110,6 +115,7 @@
324F25432754F0E1006EDFA6 /* RealmSwift in Frameworks */,
32657E50274A68AD0020C4A8 /* Kingfisher in Frameworks */,
32657E58274A6D210020C4A8 /* SnapKit in Frameworks */,
323C512028CDC52500280836 /* Then in Frameworks */,
32657E48274A68740020C4A8 /* Alamofire in Frameworks */,
32657E53274A68BF0020C4A8 /* SwiftSoup in Frameworks */,
324F25412754F0E1006EDFA6 /* Realm in Frameworks */,
Expand Down Expand Up @@ -177,6 +183,7 @@
isa = PBXGroup;
children = (
32657E54274A6A170020C4A8 /* HomeViewController.swift */,
323C512328CDF5F000280836 /* HomeViewModel.swift */,
328406212751176A008032E7 /* ProductDetailViewController.swift */,
3284061B274FFD97008032E7 /* RankViewController.swift */,
32657E59274A70AD0020C4A8 /* SearchViewController.swift */,
Expand Down Expand Up @@ -244,6 +251,7 @@
3284061D274FFEF3008032E7 /* RankCollectionViewCell.swift */,
3284062527512453008032E7 /* ReviewCollectionViewCell.swift */,
324F254827552856006EDFA6 /* SearchRecordTableViewCell.swift */,
323C512128CDDAAF00280836 /* HomeCollectionHeaderView.swift */,
);
path = Views;
sourceTree = "<group>";
Expand Down Expand Up @@ -272,6 +280,7 @@
32657E57274A6D210020C4A8 /* SnapKit */,
324F25402754F0E1006EDFA6 /* Realm */,
324F25422754F0E1006EDFA6 /* RealmSwift */,
323C511F28CDC52500280836 /* Then */,
);
productName = BuyOrNot;
productReference = 32528AA7274529E4006988D6 /* BuyOrNot.app */;
Expand Down Expand Up @@ -307,6 +316,7 @@
32657E51274A68BF0020C4A8 /* XCRemoteSwiftPackageReference "SwiftSoup" */,
32657E56274A6D210020C4A8 /* XCRemoteSwiftPackageReference "SnapKit" */,
324F253F2754F0E1006EDFA6 /* XCRemoteSwiftPackageReference "realm-cocoa" */,
323C511E28CDC52500280836 /* XCRemoteSwiftPackageReference "Then" */,
);
productRefGroup = 32528AA8274529E4006988D6 /* Products */;
projectDirPath = "";
Expand Down Expand Up @@ -382,13 +392,15 @@
32657E5C274A82DC0020C4A8 /* NaverShoppingResult.swift in Sources */,
328406242751219C008032E7 /* Product.swift in Sources */,
32B4564C27529DE500EADAEF /* KakaoSearchAPIClient.swift in Sources */,
323C512228CDDAAF00280836 /* HomeCollectionHeaderView.swift in Sources */,
32657E5E274A83DA0020C4A8 /* YoutubeSearchResult.swift in Sources */,
32B4564A2752929700EADAEF /* ReviewSiteKind.swift in Sources */,
32B55385274F87E500CFA954 /* ViewSize.swift in Sources */,
32B4564E27529EF300EADAEF /* KakaoBlogResult.swift in Sources */,
3245B33C2757F73700DF2F2F /* YoutubeVideosResult.swift in Sources */,
32657E55274A6A170020C4A8 /* HomeViewController.swift in Sources */,
32B5537F274F56B200CFA954 /* UITableViewExtension.swift in Sources */,
323C512428CDF5F000280836 /* HomeViewModel.swift in Sources */,
32B456502752B88500EADAEF /* ReviewContent.swift in Sources */,
32657E69274ABA220020C4A8 /* NaverBlogResult.swift in Sources */,
3245B33E27580AD200DF2F2F /* ReviewFetcher.swift in Sources */,
Expand Down Expand Up @@ -461,7 +473,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
Expand Down Expand Up @@ -516,7 +528,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
Expand Down Expand Up @@ -608,6 +620,14 @@
/* End XCConfigurationList section */

/* Begin XCRemoteSwiftPackageReference section */
323C511E28CDC52500280836 /* XCRemoteSwiftPackageReference "Then" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/devxoul/Then";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 3.0.0;
};
};
324F253F2754F0E1006EDFA6 /* XCRemoteSwiftPackageReference "realm-cocoa" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/realm/realm-cocoa.git";
Expand Down Expand Up @@ -651,6 +671,11 @@
/* End XCRemoteSwiftPackageReference section */

/* Begin XCSwiftPackageProductDependency section */
323C511F28CDC52500280836 /* Then */ = {
isa = XCSwiftPackageProductDependency;
package = 323C511E28CDC52500280836 /* XCRemoteSwiftPackageReference "Then" */;
productName = Then;
};
324F25402754F0E1006EDFA6 /* Realm */ = {
isa = XCSwiftPackageProductDependency;
package = 324F253F2754F0E1006EDFA6 /* XCRemoteSwiftPackageReference "realm-cocoa" */;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,15 @@
"revision" : "02c63b7be50bda384f22c56c64d347231754a07e",
"version" : "2.3.3"
}
},
{
"identity" : "then",
"kind" : "remoteSourceControl",
"location" : "https://github.com/devxoul/Then",
"state" : {
"revision" : "d41ef523faef0f911369f79c0b96815d9dbb6d7a",
"version" : "3.0.0"
}
}
],
"version" : 2
Expand Down
191 changes: 117 additions & 74 deletions BuyOrNot/BuyOrNot/ViewControllers/HomeViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,58 +8,100 @@
import UIKit
import SnapKit
import RealmSwift
import Then

final class HomeViewController: UIViewController {
private let searchIcon = UIImageView()
private let outerTableView = UITableView(frame: .zero, style: .grouped)
private let categoryCollectionView = CategoryCollectionView()
private let recommendCollectionView = RecommendCollectionView()
private let viewModel = HomeViewModel()
private let searchIcon = UIImageView().then {
$0.image = UIImage(named: "search")
$0.tintColor = .darkGray
$0.isUserInteractionEnabled = true
let gesture = UITapGestureRecognizer(target: HomeViewController.self, action: #selector(moveToSearchView(_:)))
$0.addGestureRecognizer(gesture)
}
private lazy var collectionView = UICollectionView(frame: .zero, collectionViewLayout: compositionalLayout()).then {
$0.register(CategoryCell.self, forCellWithReuseIdentifier: CategoryCell.reuseIdentifier)
$0.register(RecommendProductCell.self, forCellWithReuseIdentifier: RecommendProductCell.reuseIdentifier)
$0.register(HomeCollectionHeaderView.self, forSupplementaryViewOfKind: "header",
withReuseIdentifier: HomeCollectionHeaderView.reuseIdentifier)
$0.delegate = self
$0.dataSource = self
$0.showsVerticalScrollIndicator = false
$0.backgroundColor = ColorSet.backgroundColor
}
private let loadingIndicator = UIActivityIndicatorView()

override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = ColorSet.backgroundColor
setSearchIcon()
setOuterTableView()
layout()
addNotificationObserver()
}

override func viewWillAppear(_ animated: Bool) {
navigationController?.navigationBar.isHidden = true
}

private func setSearchIcon() {
searchIcon.image = UIImage(named: "search")
searchIcon.tintColor = .darkGray
searchIcon.isUserInteractionEnabled = true
let gesture = UITapGestureRecognizer(target: self, action: #selector(moveToSearchView(_:)))
searchIcon.addGestureRecognizer(gesture)
private func layout() {
view.addSubview(searchIcon)
searchIcon.snp.makeConstraints { imageView in
imageView.top.equalTo(view.safeAreaLayoutGuide).inset(10)
imageView.trailing.equalTo(view.safeAreaLayoutGuide).inset(20)
imageView.width.height.equalTo(30)
}
view.addSubview(collectionView)
collectionView.snp.makeConstraints { collection in
collection.leading.trailing.bottom.equalTo(view.safeAreaLayoutGuide)
collection.top.equalTo(searchIcon.snp.bottom).offset(10)
}
}

@objc private func moveToSearchView(_ sender: UITapGestureRecognizer) {
navigationController?.pushViewController(SearchViewController(), animated: true)
private func compositionalLayout() -> UICollectionViewLayout {
return UICollectionViewCompositionalLayout { section, _ in
if section == 0 {
let categoryItem = NSCollectionLayoutItem(layoutSize: .init(widthDimension: .fractionalWidth(1),
heightDimension: .fractionalHeight(1)))
categoryItem.contentInsets = .init(top: 0, leading: 0, bottom: 0, trailing: 10)
let categoryGroup = NSCollectionLayoutGroup
.horizontal(layoutSize: .init(widthDimension: .fractionalWidth(0.55),
heightDimension: .fractionalHeight(0.5)),
subitems: [categoryItem])
let categorySection = NSCollectionLayoutSection(group: categoryGroup)
categorySection.orthogonalScrollingBehavior = .continuous

let headerItemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1),
heightDimension: .estimated(100))
let headerItem = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerItemSize,
elementKind: "header",
alignment: .top)

categorySection.boundarySupplementaryItems = [headerItem]
categorySection.contentInsets = .init(top: 20, leading: 10, bottom: 30, trailing: 0)
return categorySection
} else {
let recommendItem = NSCollectionLayoutItem(layoutSize: .init(widthDimension: .fractionalWidth(1),
heightDimension: .fractionalHeight(1)))
recommendItem.contentInsets = .init(top: 0, leading: 0, bottom: 0, trailing: 10)
let recommendGroup = NSCollectionLayoutGroup
.horizontal(layoutSize: .init(widthDimension: .fractionalWidth(0.55),
heightDimension: .absolute(200)),
subitems: [recommendItem])
let recommendSection = NSCollectionLayoutSection(group: recommendGroup)
recommendSection.orthogonalScrollingBehavior = .continuous
let headerItemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1),
heightDimension: .estimated(100))
let headerItem = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerItemSize,
elementKind: "header",
alignment: .top)
recommendSection.boundarySupplementaryItems = [headerItem]
recommendSection.contentInsets = .init(top: 20, leading: 10, bottom: 0, trailing: 0)
return recommendSection
}
}
}

private func setOuterTableView() {
outerTableView.register(UITableViewCell.self, forCellReuseIdentifier: "outerTableViewCell")
outerTableView.register(UITableViewHeaderFooterView.self,
forHeaderFooterViewReuseIdentifier: "outerTableViewHeader")
outerTableView.dataSource = self
outerTableView.delegate = self
outerTableView.backgroundColor = .clear
outerTableView.separatorColor = .clear

view.addSubview(outerTableView)
outerTableView.snp.makeConstraints { tableView in
tableView.top.equalTo(searchIcon.snp.bottom).offset(10)
tableView.leading.trailing.bottom.equalTo(view.safeAreaLayoutGuide)
}
@objc private func moveToSearchView(_ sender: UITapGestureRecognizer) {
navigationController?.pushViewController(SearchViewController(), animated: true)
}

private func setLoadingIndicator(cell: UITableViewCell) {
Expand Down Expand Up @@ -99,66 +141,67 @@ final class HomeViewController: UIViewController {
}
}

extension HomeViewController: UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int { 2 }

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 1 }

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = outerTableView.dequeueReusableCell(withIdentifier: "outerTableViewCell") else {
return UITableViewCell()
}
cell.backgroundColor = .clear
cell.selectionStyle = .none

if indexPath.section == 0 {
cell.setContentView(view: categoryCollectionView)
} else if indexPath.section == 1 {
cell.setContentView(view: recommendCollectionView)
extension HomeViewController: UICollectionViewDataSource {
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 2
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
if section == 0 {
return ProductCategory.allCases.count
} else {
if let productCount = viewModel.products?.count {
return productCount
}
return 0
}

return cell
}
}

extension HomeViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if indexPath.section == 0 {
let cellHeight = ViewSize.categoryCellSize.height
return cellHeight + 20
guard let categoryCell = collectionView
.dequeueReusableCell(withReuseIdentifier: CategoryCell.reuseIdentifier, for: indexPath)
as? CategoryCell else { return UICollectionViewCell() }
categoryCell.setUpContents(category: ProductCategory.allCases[indexPath.row])
return categoryCell
} else {
let cellHeight = ViewSize.recommendCellSize.height
return cellHeight + 20
guard let recommnedCell = collectionView
.dequeueReusableCell(withReuseIdentifier: RecommendProductCell.reuseIdentifier, for: indexPath)
as? RecommendProductCell else { return UICollectionViewCell() }
if let product = viewModel.products?[indexPath.row] {
recommnedCell.setContents(product: product)
}

return recommnedCell
}
}
}

func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
guard let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "outerTableViewHeader") else {
return UIView()
extension HomeViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
guard let header = collectionView
.dequeueReusableSupplementaryView(ofKind: "header",
withReuseIdentifier: HomeCollectionHeaderView.reuseIdentifier,
for: indexPath) as? HomeCollectionHeaderView else {
return UICollectionReusableView()
}

if #available(iOS 14.0, *) {
var content = headerView.defaultContentConfiguration()
content.textProperties.font = section == 0 ? .boldSystemFont(ofSize: 40)
: .boldSystemFont(ofSize: 18)

content.text = section == 0 ? "Category" : "이런 제품은 어떠세요?"
content.textProperties.color = .darkGray
headerView.contentConfiguration = content
if indexPath.section == 0 {
header.setUpContents(section: .category)
} else {
headerView.textLabel?.font = section == 0 ? .boldSystemFont(ofSize: 40)
: .boldSystemFont(ofSize: 18)

headerView.textLabel?.text = section == 0 ? "Category" : "이런 제품은 어떠세요?"
headerView.textLabel?.textColor = .darkGray
header.setUpContents(section: .recommend)
}

return headerView
return header
}

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if indexPath == IndexPath(row: 0, section: 1) {
setLoadingIndicator(cell: cell)
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if indexPath.section == 0 {
guard let cell = collectionView.cellForItem(at: indexPath) as? CategoryCell else { return }
let category = cell.category
navigationController?.pushViewController(RankViewController(category: category), animated: true)
} else {
guard let cell = collectionView.cellForItem(at: indexPath) as? RecommendProductCell,
let product = cell.product else { return }
navigationController?.pushViewController(ProductDetailViewController(product: product), animated: true)
}
}
}

0 comments on commit 625561b

Please sign in to comment.