From c70dd7176707e18d4724ce7c2cae0f9472a0b82b Mon Sep 17 00:00:00 2001 From: sanghyeok-kim Date: Tue, 31 May 2022 12:47:16 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20SearchView=20=EB=8F=84=EC=8B=9C=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 근처 인기 여행지(nearCities)를 서버로부터 도시 정보, 이미지 받아와서 출력하도록 변경 - 도시 검색시 나타나던 뷰 수정 - 도시 이미지를 Mock Image -> SF Symbol로 변경 - cell 레이아웃 조정 jeremy0405/airbnb/#5 --- .../AirbnbApp.xcodeproj/project.pbxproj | 4 ++ .../Source/Extension/Int+Extension.swift | 21 +++++++++ .../Source/Present/Common/CityViewCell.swift | 9 ++-- .../PopularCollectionViewDataSource.swift | 19 +++++--- .../Present/Search/SearchResultViewCell.swift | 43 +++++++++++++------ .../Present/Search/SearchViewController.swift | 24 +++++++++-- .../SearchHomeCollectionViewDataSource.swift | 4 +- .../SearchHome/SearchHomeViewController.swift | 2 +- .../Support/SectionLayoutFactory.swift | 12 ++++-- 9 files changed, 105 insertions(+), 33 deletions(-) create mode 100644 iOS/AirbnbApp/AirbnbApp/Source/Extension/Int+Extension.swift diff --git a/iOS/AirbnbApp/AirbnbApp.xcodeproj/project.pbxproj b/iOS/AirbnbApp/AirbnbApp.xcodeproj/project.pbxproj index ab5acf1aa..01c736b38 100644 --- a/iOS/AirbnbApp/AirbnbApp.xcodeproj/project.pbxproj +++ b/iOS/AirbnbApp/AirbnbApp.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ 1E71CBDA283B9D9A00F6D2D0 /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = 1E71CBD9283B9D9A00F6D2D0 /* Alamofire */; }; 1E71CBDC283BB93600F6D2D0 /* SearchHomeSearchBarDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E71CBDB283BB93600F6D2D0 /* SearchHomeSearchBarDelegate.swift */; }; + 1EAB24E22845B68100AAA121 /* Int+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EAB24E12845B68100AAA121 /* Int+Extension.swift */; }; 1ED648A1283BD40E00AA5380 /* SearchHomeCollectionViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED648A0283BD40E00AA5380 /* SearchHomeCollectionViewDataSource.swift */; }; 1ED648A5283BD48500AA5380 /* CityViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED648A4283BD48500AA5380 /* CityViewCell.swift */; }; 1ED648A7283BDCBA00AA5380 /* SearchHomeHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED648A6283BDCBA00AA5380 /* SearchHomeHeaderView.swift */; }; @@ -60,6 +61,7 @@ /* Begin PBXFileReference section */ 1E71CBDB283BB93600F6D2D0 /* SearchHomeSearchBarDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHomeSearchBarDelegate.swift; sourceTree = ""; }; + 1EAB24E12845B68100AAA121 /* Int+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Int+Extension.swift"; sourceTree = ""; }; 1ED648A0283BD40E00AA5380 /* SearchHomeCollectionViewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHomeCollectionViewDataSource.swift; sourceTree = ""; }; 1ED648A4283BD48500AA5380 /* CityViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CityViewCell.swift; sourceTree = ""; }; 1ED648A6283BDCBA00AA5380 /* SearchHomeHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHomeHeaderView.swift; sourceTree = ""; }; @@ -175,6 +177,7 @@ isa = PBXGroup; children = ( 1ED648A8283C7EC900AA5380 /* UIColor+Extension.swift */, + 1EAB24E12845B68100AAA121 /* Int+Extension.swift */, 6C1AB744283C9C7900A0314F /* UIFont+Extension.swift */, ); path = Extension; @@ -463,6 +466,7 @@ 1ED648FD283F9FFA00AA5380 /* ViewModelBindable.swift in Sources */, 1ED6491F2843BEAC00AA5380 /* ImageManager.swift in Sources */, 1E71CBDC283BB93600F6D2D0 /* SearchHomeSearchBarDelegate.swift in Sources */, + 1EAB24E22845B68100AAA121 /* Int+Extension.swift in Sources */, 6C60EADB2843A5A000EF82F9 /* SearchCollectionViewDataSource.swift in Sources */, 6CB1D8C8284359E80051EF2F /* PopularCollectionViewDataSource.swift in Sources */, 1ED648A7283BDCBA00AA5380 /* SearchHomeHeaderView.swift in Sources */, diff --git a/iOS/AirbnbApp/AirbnbApp/Source/Extension/Int+Extension.swift b/iOS/AirbnbApp/AirbnbApp/Source/Extension/Int+Extension.swift new file mode 100644 index 000000000..6f00d333a --- /dev/null +++ b/iOS/AirbnbApp/AirbnbApp/Source/Extension/Int+Extension.swift @@ -0,0 +1,21 @@ +// +// Int+Extension.swift +// AirbnbApp +// +// Created by 김상혁 on 2022/05/31. +// + +import Foundation + +extension Int { + func convertIntoTime() -> String { + if self / 60 == 0 { + return "\(self)분" + } + + let hour = self / 60 + let minute = self % 60 + + return minute > 30 ? "\(hour + 1) 시간" : "\(Double(hour) + 0.5) 시간" + } +} diff --git a/iOS/AirbnbApp/AirbnbApp/Source/Present/Common/CityViewCell.swift b/iOS/AirbnbApp/AirbnbApp/Source/Present/Common/CityViewCell.swift index b115d95f9..716366390 100644 --- a/iOS/AirbnbApp/AirbnbApp/Source/Present/Common/CityViewCell.swift +++ b/iOS/AirbnbApp/AirbnbApp/Source/Present/Common/CityViewCell.swift @@ -60,8 +60,9 @@ private extension CityViewCell { addSubview(cityImageView) cityImageView.snp.makeConstraints { make in - make.top.leading.equalToSuperview() - make.height.width.equalTo(74) + make.top.bottom.leading.equalToSuperview() + make.centerY.equalToSuperview() + make.width.equalTo(cityImageView.snp.height) } } @@ -89,7 +90,7 @@ extension CityViewCell { cityTitleLabel.text = text } - func setDistanceLabel(text: Int) { - distanceLabel.text = "\(text)" + func setDistanceLabel(text: String) { + distanceLabel.text = "차로 \(text) 거리" } } diff --git a/iOS/AirbnbApp/AirbnbApp/Source/Present/Search/PopularCollectionViewDataSource.swift b/iOS/AirbnbApp/AirbnbApp/Source/Present/Search/PopularCollectionViewDataSource.swift index 0c2a0c349..26d9ab5b8 100644 --- a/iOS/AirbnbApp/AirbnbApp/Source/Present/Search/PopularCollectionViewDataSource.swift +++ b/iOS/AirbnbApp/AirbnbApp/Source/Present/Search/PopularCollectionViewDataSource.swift @@ -10,20 +10,29 @@ import UIKit final class PopularCollectionViewDataSource: NSObject, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout { - var mockCity: [String] = ["서울", "광주", "부산", "대구"] + var nearCities: [SearchHomeEntity.City] = [] + + let imageManager = ImageManager() func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - return mockCity.count + return nearCities.count } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CityViewCell.identifier, for: indexPath) as? CityViewCell else { return UICollectionViewCell() } - guard let image = UIImage(named: "mockimage.png") else { return cell } + let item = nearCities[indexPath.item] - cell.setCityImageView(image: image) - cell.setCityTitleLabel(text: mockCity[indexPath.item]) + let imageUrl = URL(string: item.imageUrl) + imageManager.fetchImage(from: imageUrl) { image in + DispatchQueue.main.async { + cell.setCityImageView(image: image ?? UIImage()) + // TODO: image가 nil일 경우 handling하는 에러 구현하기 + } + } + cell.setCityTitleLabel(text: item.cityName) + cell.setDistanceLabel(text: item.time.convertIntoTime()) return cell } diff --git a/iOS/AirbnbApp/AirbnbApp/Source/Present/Search/SearchResultViewCell.swift b/iOS/AirbnbApp/AirbnbApp/Source/Present/Search/SearchResultViewCell.swift index d9bf5e156..b13b10e3c 100644 --- a/iOS/AirbnbApp/AirbnbApp/Source/Present/Search/SearchResultViewCell.swift +++ b/iOS/AirbnbApp/AirbnbApp/Source/Present/Search/SearchResultViewCell.swift @@ -13,16 +13,24 @@ class SearchResultViewCell: UICollectionViewCell { return "\(self)" } + private lazy var imageContainerView: UIView = { + let view = UIView() + view.layer.cornerRadius = 10 + view.layer.borderWidth = 1 + view.layer.borderColor = UIColor.Custom.gray3.cgColor + view.clipsToBounds = true + return view + }() + private lazy var cityImageView: UIImageView = { - let image = UIImageView() - image.layer.cornerRadius = 10 - image.clipsToBounds = true - image.image = UIImage(named: "Mockimage.png") - return image + let imageView = UIImageView() + imageView.image = UIImage(systemName: "mappin.and.ellipse")? + .withAlignmentRectInsets(UIEdgeInsets(top: -21, left: -23, bottom: -21, right: -23)) + imageView.tintColor = .Custom.gray3 + return imageView }() - private lazy var descriptionLabel = CustomLabel(text: "양재동, 서초구, 서울특별시", - font: .NotoSans.regular, + private lazy var descriptionLabel = CustomLabel(font: .NotoSans.regular, fontColor: .Custom.gray1) private lazy var informationStackView: UIStackView = { @@ -39,7 +47,8 @@ class SearchResultViewCell: UICollectionViewCell { override init(frame: CGRect) { super.init(frame: frame) - layoutCityImageView() +// layoutCityImageView() + layoutImageContainerView() layoutInformationStackView() } @@ -53,12 +62,18 @@ class SearchResultViewCell: UICollectionViewCell { private extension SearchResultViewCell { - func layoutCityImageView() { - addSubview(cityImageView) + func layoutImageContainerView() { + addSubview(imageContainerView) + imageContainerView.addSubview(cityImageView) + + imageContainerView.snp.makeConstraints { make in + make.top.bottom.leading.equalToSuperview() + make.centerY.equalToSuperview() + make.width.equalTo(imageContainerView.snp.height) + } cityImageView.snp.makeConstraints { make in - make.top.leading.equalToSuperview() - make.height.width.equalTo(74) + make.centerX.centerY.equalToSuperview() } } @@ -66,9 +81,9 @@ private extension SearchResultViewCell { addSubview(informationStackView) informationStackView.snp.makeConstraints { make in - make.leading.equalTo(cityImageView.snp.trailing).offset(16) + make.leading.equalTo(imageContainerView.snp.trailing).offset(16) make.trailing.equalToSuperview() - make.centerY.equalTo(cityImageView.snp.centerY) + make.centerY.equalTo(imageContainerView.snp.centerY) } } diff --git a/iOS/AirbnbApp/AirbnbApp/Source/Present/Search/SearchViewController.swift b/iOS/AirbnbApp/AirbnbApp/Source/Present/Search/SearchViewController.swift index bc347c243..f860927d1 100644 --- a/iOS/AirbnbApp/AirbnbApp/Source/Present/Search/SearchViewController.swift +++ b/iOS/AirbnbApp/AirbnbApp/Source/Present/Search/SearchViewController.swift @@ -10,6 +10,8 @@ import MapKit final class SearchViewController: UIViewController, MKLocalSearchCompleterDelegate { + private let viewModel: NearCityViewModel + private var searchedLocations = PublishRelay<[MKLocalSearchCompletion]>() private lazy var searchBarDelegate = SearchBarDelegate() @@ -23,11 +25,21 @@ final class SearchViewController: UIViewController, MKLocalSearchCompleterDelega return searchController }() + init(viewModel: NearCityViewModel) { + self.viewModel = viewModel + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + private lazy var popularCollectionViewDataSource = PopularCollectionViewDataSource() private lazy var searchCollectionViewDataSource = SearchCollectionViewDataSource() private lazy var popularCollectionView: UICollectionView = { - let collectionView = UICollectionView(frame: .zero, collectionViewLayout: SectionLayoutFactory.createPopularDestinationLayout()) + let collectionView = UICollectionView(frame: .zero, collectionViewLayout: SectionLayoutFactory.createPopularDestinationLayout(isHeaderExist: true)) collectionView.register(CityViewCell.self, forCellWithReuseIdentifier: CityViewCell.identifier) collectionView.register(PopularHeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, @@ -37,7 +49,7 @@ final class SearchViewController: UIViewController, MKLocalSearchCompleterDelega }() private lazy var searchResultCollectionView: UICollectionView = { - let collectionView = UICollectionView(frame: .zero, collectionViewLayout: SectionLayoutFactory.createPopularDestinationLayout()) + let collectionView = UICollectionView(frame: .zero, collectionViewLayout: SectionLayoutFactory.createPopularDestinationLayout(isHeaderExist: false)) collectionView.register(SearchResultViewCell.self, forCellWithReuseIdentifier: SearchResultViewCell.identifier) collectionView.dataSource = self.searchCollectionViewDataSource collectionView.isHidden = true @@ -78,6 +90,11 @@ private extension SearchViewController { self?.searchResultCollectionView.isHidden = true self?.popularCollectionView.isHidden = false } + + viewModel.bind { [weak self] cities in + self?.popularCollectionViewDataSource.nearCities = cities + self?.popularCollectionView.reloadData() + } } func configureSearchController() { @@ -98,7 +115,8 @@ private extension SearchViewController { view.addSubview(searchResultCollectionView) searchResultCollectionView.snp.makeConstraints { make in - make.top.leading.trailing.bottom.equalToSuperview() + make.top.equalTo(view.safeAreaLayoutGuide).offset(16) + make.leading.trailing.bottom.equalToSuperview() } } diff --git a/iOS/AirbnbApp/AirbnbApp/Source/Present/SearchHome/SearchHomeCollectionViewDataSource.swift b/iOS/AirbnbApp/AirbnbApp/Source/Present/SearchHome/SearchHomeCollectionViewDataSource.swift index 37ad7634d..7f0da5d18 100644 --- a/iOS/AirbnbApp/AirbnbApp/Source/Present/SearchHome/SearchHomeCollectionViewDataSource.swift +++ b/iOS/AirbnbApp/AirbnbApp/Source/Present/SearchHome/SearchHomeCollectionViewDataSource.swift @@ -66,9 +66,7 @@ final class SearchHomeCollectionViewDataSource: NSObject, UICollectionViewDataSo } cell.setCityTitleLabel(text: item.cityName) - - // TODO: 시간, 분 나누는 로직 추가해서 String으로 넘겨주기 "0시간 00분" - cell.setDistanceLabel(text: item.time) + cell.setDistanceLabel(text: item.time.convertIntoTime()) return cell case .themeJourney: diff --git a/iOS/AirbnbApp/AirbnbApp/Source/Present/SearchHome/SearchHomeViewController.swift b/iOS/AirbnbApp/AirbnbApp/Source/Present/SearchHome/SearchHomeViewController.swift index 8cb7b44f5..0012b856f 100644 --- a/iOS/AirbnbApp/AirbnbApp/Source/Present/SearchHome/SearchHomeViewController.swift +++ b/iOS/AirbnbApp/AirbnbApp/Source/Present/SearchHome/SearchHomeViewController.swift @@ -67,7 +67,7 @@ final class SearchHomeViewController: UIViewController { private func bind() { searchBarDelegate.tapTextField .bind { [weak self] in - self?.navigationController?.pushViewController(SearchViewController(), animated: true) + self?.navigationController?.pushViewController(SearchViewController(viewModel: NearCityViewModel()), animated: true) } viewModel.bindHeroBanner { [weak self] banner in diff --git a/iOS/AirbnbApp/AirbnbApp/Source/Present/SearchHome/Support/SectionLayoutFactory.swift b/iOS/AirbnbApp/AirbnbApp/Source/Present/SearchHome/Support/SectionLayoutFactory.swift index 1ad076790..4ff80cddf 100644 --- a/iOS/AirbnbApp/AirbnbApp/Source/Present/SearchHome/Support/SectionLayoutFactory.swift +++ b/iOS/AirbnbApp/AirbnbApp/Source/Present/SearchHome/Support/SectionLayoutFactory.swift @@ -8,23 +8,28 @@ import UIKit enum SectionLayoutFactory { - static func createPopularDestinationLayout() -> UICollectionViewCompositionalLayout { + static func createPopularDestinationLayout(isHeaderExist: Bool) -> UICollectionViewCompositionalLayout { let itemSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1), - heightDimension: .fractionalWidth(0.3) + heightDimension: .fractionalWidth(0.23) ) let item = NSCollectionLayoutItem(layoutSize: itemSize) item.contentInsets = .init( top: 0, leading: 0, - bottom: 0, + bottom: 16, trailing: 0 ) let group = NSCollectionLayoutGroup.horizontal(layoutSize: itemSize, subitem: item, count: 1) let section = NSCollectionLayoutSection(group: group) section.contentInsets.leading = 15 + section.contentInsets.trailing = 15 + + guard isHeaderExist else { + return UICollectionViewCompositionalLayout(section: section) + } section.boundarySupplementaryItems = [ NSCollectionLayoutBoundarySupplementaryItem( @@ -34,6 +39,7 @@ enum SectionLayoutFactory { elementKind: UICollectionView.elementKindSectionHeader, alignment: .topLeading) ] + return UICollectionViewCompositionalLayout(section: section) }