Skip to content
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

[Team-08][iOS] Calendar Model 생성 / CollectionView HeaderCell 추가 작업 #75

Merged
merged 9 commits into from
May 31, 2022
28 changes: 28 additions & 0 deletions iOS/Airbnb/Airbnb.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
005AF798283B775300546D5B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 005AF797283B775300546D5B /* Assets.xcassets */; };
005AF7A6283B775300546D5B /* AirbnbTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 005AF7A5283B775300546D5B /* AirbnbTests.swift */; };
008A2062283F4E220095F7BC /* BackgroundViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 008A2061283F4E220095F7BC /* BackgroundViewController.swift */; };
008A2064283F62910095F7BC /* LocationCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 008A2063283F62910095F7BC /* LocationCollectionViewCell.swift */; };
008A20A6283FE3960095F7BC /* HeaderReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 008A20A5283FE3960095F7BC /* HeaderReusableView.swift */; };
00A85F35283B872800EA32E0 /* SearchModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00A85F34283B872800EA32E0 /* SearchModel.swift */; };
00A85F38283B873F00EA32E0 /* SearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00A85F37283B873F00EA32E0 /* SearchViewController.swift */; };
00A85F3C283B876A00EA32E0 /* WishListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00A85F3B283B876A00EA32E0 /* WishListViewController.swift */; };
Expand All @@ -29,6 +31,9 @@
1B27E237283CA5A900FCB990 /* SearchCellCommonType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B27E236283CA5A900FCB990 /* SearchCellCommonType.swift */; };
1B69F369283CD506001A2860 /* MultipleRowsCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B69F368283CD506001A2860 /* MultipleRowsCollectionViewController.swift */; };
1B69F36B283D0D9C001A2860 /* MultipleRowsCollectionViewContainerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B69F36A283D0D9C001A2860 /* MultipleRowsCollectionViewContainerCell.swift */; };
1B807DCB283F529A00D6815F /* SearchCalendarModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B807DCA283F529A00D6815F /* SearchCalendarModel.swift */; };
1B90ED34283F547600E45FE8 /* SearchDayDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B90ED33283F547600E45FE8 /* SearchDayDTO.swift */; };
1B90ED36283F5B4A00E45FE8 /* AirbnbCalendarModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B90ED35283F5B4A00E45FE8 /* AirbnbCalendarModelTests.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -73,6 +78,8 @@
005AF7A5283B775300546D5B /* AirbnbTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AirbnbTests.swift; sourceTree = "<group>"; };
005AF7AB283B775300546D5B /* AirbnbUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AirbnbUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
008A2061283F4E220095F7BC /* BackgroundViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundViewController.swift; sourceTree = "<group>"; };
008A2063283F62910095F7BC /* LocationCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationCollectionViewCell.swift; sourceTree = "<group>"; };
008A20A5283FE3960095F7BC /* HeaderReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderReusableView.swift; sourceTree = "<group>"; };
00A85F34283B872800EA32E0 /* SearchModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchModel.swift; sourceTree = "<group>"; };
00A85F37283B873F00EA32E0 /* SearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewController.swift; sourceTree = "<group>"; };
00A85F3B283B876A00EA32E0 /* WishListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WishListViewController.swift; sourceTree = "<group>"; };
Expand All @@ -86,6 +93,9 @@
1B27E236283CA5A900FCB990 /* SearchCellCommonType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchCellCommonType.swift; sourceTree = "<group>"; };
1B69F368283CD506001A2860 /* MultipleRowsCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultipleRowsCollectionViewController.swift; sourceTree = "<group>"; };
1B69F36A283D0D9C001A2860 /* MultipleRowsCollectionViewContainerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultipleRowsCollectionViewContainerCell.swift; sourceTree = "<group>"; };
1B807DCA283F529A00D6815F /* SearchCalendarModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchCalendarModel.swift; sourceTree = "<group>"; };
1B90ED33283F547600E45FE8 /* SearchDayDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchDayDTO.swift; sourceTree = "<group>"; };
1B90ED35283F5B4A00E45FE8 /* AirbnbCalendarModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AirbnbCalendarModelTests.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -159,6 +169,7 @@
isa = PBXGroup;
children = (
005AF7A5283B775300546D5B /* AirbnbTests.swift */,
1B90ED35283F5B4A00E45FE8 /* AirbnbCalendarModelTests.swift */,
);
path = AirbnbTests;
sourceTree = "<group>";
Expand All @@ -178,6 +189,7 @@
children = (
00A85F36283B872E00EA32E0 /* View */,
00A85F33283B871900EA32E0 /* Model */,
1B807DCD283F540500D6815F /* DTO */,
008A2061283F4E220095F7BC /* BackgroundViewController.swift */,
);
path = Search;
Expand Down Expand Up @@ -207,6 +219,7 @@
children = (
00A85F34283B872800EA32E0 /* SearchModel.swift */,
1B27E234283CA44400FCB990 /* SearchViewModel.swift */,
1B807DCA283F529A00D6815F /* SearchCalendarModel.swift */,
);
path = Model;
sourceTree = "<group>";
Expand Down Expand Up @@ -269,10 +282,20 @@
1B27E232283CA14900FCB990 /* SearchOneInARowCollectionViewCell.swift */,
1B69F368283CD506001A2860 /* MultipleRowsCollectionViewController.swift */,
1B69F36A283D0D9C001A2860 /* MultipleRowsCollectionViewContainerCell.swift */,
008A2063283F62910095F7BC /* LocationCollectionViewCell.swift */,
008A20A5283FE3960095F7BC /* HeaderReusableView.swift */,
);
path = SubView;
sourceTree = "<group>";
};
1B807DCD283F540500D6815F /* DTO */ = {
isa = PBXGroup;
children = (
1B90ED33283F547600E45FE8 /* SearchDayDTO.swift */,
);
path = DTO;
sourceTree = "<group>";
};
/* End PBXGroup section */

/* Begin PBXNativeTarget section */
Expand Down Expand Up @@ -412,11 +435,13 @@
buildActionMask = 2147483647;
files = (
00A85F3E283B877700EA32E0 /* WishModel.swift in Sources */,
1B807DCB283F529A00D6815F /* SearchCalendarModel.swift in Sources */,
1B27E233283CA14900FCB990 /* SearchOneInARowCollectionViewCell.swift in Sources */,
1B27E231283CA14200FCB990 /* SearchTwoInARowCollectionViewCell.swift in Sources */,
00A85F35283B872800EA32E0 /* SearchModel.swift in Sources */,
00A85F3C283B876A00EA32E0 /* WishListViewController.swift in Sources */,
00A85F41283B87A300EA32E0 /* MyInfoViewController.swift in Sources */,
008A2064283F62910095F7BC /* LocationCollectionViewCell.swift in Sources */,
008A2062283F4E220095F7BC /* BackgroundViewController.swift in Sources */,
00230CF1283BB77500802765 /* TabBarViewController.swift in Sources */,
1B69F369283CD506001A2860 /* MultipleRowsCollectionViewController.swift in Sources */,
Expand All @@ -425,9 +450,11 @@
1B69F36B283D0D9C001A2860 /* MultipleRowsCollectionViewContainerCell.swift in Sources */,
1B27E22F283CA12D00FCB990 /* SearchTitleCollectionViewCell.swift in Sources */,
005AF78F283B775300546D5B /* AppDelegate.swift in Sources */,
008A20A6283FE3960095F7BC /* HeaderReusableView.swift in Sources */,
1B27E237283CA5A900FCB990 /* SearchCellCommonType.swift in Sources */,
00A85F38283B873F00EA32E0 /* SearchViewController.swift in Sources */,
005AF791283B775300546D5B /* SceneDelegate.swift in Sources */,
1B90ED34283F547600E45FE8 /* SearchDayDTO.swift in Sources */,
00A85F44283B87B900EA32E0 /* MyInfoModel.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand All @@ -437,6 +464,7 @@
buildActionMask = 2147483647;
files = (
005AF7A6283B775300546D5B /* AirbnbTests.swift in Sources */,
1B90ED36283F5B4A00E45FE8 /* AirbnbCalendarModelTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
1 change: 0 additions & 1 deletion iOS/Airbnb/Airbnb/Search/BackgroundViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ class BackgroundViewController: UIViewController {
func setUpNavigationAppearance() {
let appearance = UINavigationBarAppearance()
appearance.configureWithOpaqueBackground()
appearance.backgroundColor = .yellow
navigationItem.standardAppearance = appearance
navigationItem.scrollEdgeAppearance = appearance
}
Expand Down
22 changes: 22 additions & 0 deletions iOS/Airbnb/Airbnb/Search/DTO/SearchDayDTO.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//
// SearchDayDTO.swift
// Airbnb
//
// Created by 백상휘 on 2022/05/26.
//

import Foundation

struct SearchDayDTO {
let date: Date
let components: DateComponents
let day: String
// 캘린더에서 표시하지 않는 빈칸일 경우 true 입니다.
let isOccupied: Bool

let isSelected: Bool = false
// 선택되었을 시 왼쪽부터 서서히 채워나가는 이펙트를 줌
var fadeLeft: Bool = false
// 선택되었을 시 오른쪽부터 서서히 채워나가는 이펙트를 줌
var fadeRight: Bool = false
Comment on lines +19 to +21

Choose a reason for hiding this comment

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

fadeLeft와 fadeRight가 동시에 true일 경우도 있는 걸까요?👀

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

말씀 듣고 보니 디자인에는 둘 다 true 일 수 없군요!

fade 라는 것 자체를 enum 타입으로 변경하는 것이 의미 상 맞는 것 같습니다!!

}
101 changes: 101 additions & 0 deletions iOS/Airbnb/Airbnb/Search/Model/SearchCalendarModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
//
// SearchCalendarModel.swift
// Airbnb
//
// Created by 백상휘 on 2022/05/26.
//

import Foundation

class SearchCalendarModel {

private(set) var localCalendar: Calendar = {
var calendar = Calendar(identifier: .gregorian)
calendar.locale = Locale(identifier: "ko_kr")
return calendar
}()

/// 전달되는 기준 날짜를 이용하여 날짜 DTO를 가져온다.
func getMonthDays(from baseDate: Date) -> CalendarResult {
generateMonth(from: baseDate, isPrevious: nil)
}

/// 전달되는 기준 날짜를 이용하여 다음월 날짜 DTO를 가져온다.
func getNextMonthDays(from baseDate: Date) -> CalendarResult {
generateMonth(from: baseDate, isPrevious: false)
}

/// 전달되는 기준 날짜를 이용하여 전월 날짜 DTO를 가져온다.
func getPreviousMonthDays(from baseDate: Date) -> CalendarResult {
generateMonth(from: baseDate, isPrevious: true)
}

private func generateMonth(from baseDate: Date, isPrevious: Bool?) -> CalendarResult {

var moveNum = 0

if let previous = isPrevious {
moveNum = previous ? -1 : 1
}

guard let dateMonthMoved = localCalendar.date(
byAdding: .month,
value: moveNum,
to: baseDate)
else {
return CalendarResult(date: baseDate, result: [])
}

var dateComponent = localCalendar.dateComponents([.year,.month,.day,.hour], from: dateMonthMoved)
dateComponent.day = 2
dateComponent.hour = 0

guard
let dateFirstInMonth = localCalendar.date(from: dateComponent),
let numberOfDays = localCalendar.range(
of: .day,
in: .month,
for: dateFirstInMonth)?.count
else {
return CalendarResult(date: baseDate, result: [])
}

// Swift는 요일을 0부터 6까지로 매기고, weekday는 1부터 7까지로 매깁니다.
// 미국 날짜와 한국 날짜는 하루 차이가 납니다.
// 위와 아래의 사실에 의거하여 2를 뺍니다.
let weekdayTemp = localCalendar.component(.weekday, from: dateFirstInMonth) - 2
let weekday = weekdayTemp < 0 ? 6 : weekdayTemp

guard var date = localCalendar.date(
byAdding: .day,
value: weekday * -1,
to: dateFirstInMonth)
else {
return CalendarResult(date: baseDate, result: [])
}

var result = [SearchDayDTO]()

for day in 1...weekday+numberOfDays {
result.append(
SearchDayDTO(
date: date,
components: localCalendar.dateComponents([.year,.month,.day], from: date),
day: (weekday >= day ? "" : "\(day-weekday)"),
isOccupied: (weekday >= day)
)
)

if let targetDate = localCalendar.date(byAdding: .day, value: 1, to: date) {
date = targetDate
}
}

return CalendarResult(date: dateFirstInMonth, result: result)
}
}

struct CalendarResult {
let date: Date
let result: [SearchDayDTO]
}
93 changes: 84 additions & 9 deletions iOS/Airbnb/Airbnb/Search/View/LocationViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,18 @@
//

import UIKit
import SnapKit

class LocationViewController: BackgroundViewController, CommonViewControllerProtocol {


private let locations: [String] = [
"서울", "광주", "의정부시", "수원시", "대구", "울산", "대전", "부천시"
]

private let searchController: UISearchController = {
let searchController = UISearchController(searchResultsController: nil)
let searchBar = searchController.searchBar
searchBar.searchBarStyle = .minimal
searchBar.placeholder = "어디로 여행가세요?"
searchBar.searchTextField.clearButtonMode = .never
searchBar.searchTextField.returnKeyType = .go
Expand All @@ -20,40 +26,70 @@ class LocationViewController: BackgroundViewController, CommonViewControllerProt
return searchController
}()

private var nearbyLoactionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .vertical
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.register(LocationCollectionViewCell.self, forCellWithReuseIdentifier: LocationCollectionViewCell.reuseIdentifier)
collectionView.register(HeaderReusableView.self,
forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: HeaderReusableView.ID)
return collectionView
}()

lazy var removeButton: UIBarButtonItem = {
lazy var removeSearchTextField: UIBarButtonItem = {

Choose a reason for hiding this comment

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

이 버튼은 네이밍이 메소드스럽네요ㅎㅎ
그리고 내용을 비우는 거라면 clearSearchField 메소드와 대응되도록 remove보다는 clear가 적절해보입니다 :)

let barButton = UIBarButtonItem(title: "지우기", style: .done,
target: self, action: #selector(self.clearSearchField(_:)))
barButton.tintColor = .gray
return barButton
}()

func attribute() {

setUpDelegates()
navigationItem.searchController = searchController
navigationItem.title = "숙소 찾기"
super.setUpNavigationAppearance()
view.backgroundColor = .systemBackground
}

func layout() {
let sideMargin: CGFloat = 16
view.addSubview(nearbyLoactionView)

nearbyLoactionView.snp.makeConstraints {
$0.top.bottom.equalTo(view)
$0.leading.equalTo(view).offset(sideMargin)
$0.trailing.equalTo(view).inset(sideMargin)
}
}

func bind() {

}

private func setUpSearchController() {
self.navigationItem.searchController = searchController
private func setUpDelegates() {
nearbyLoactionView.delegate = self
nearbyLoactionView.dataSource = self
searchController.searchBar.delegate = self
searchController.searchResultsUpdater = self
definesPresentationContext = true
}

override func viewDidLoad() {
super.viewDidLoad()
super.setUpNavigationAppearance()
setUpSearchController()
attribute()
layout()
bind()
}

}

extension LocationViewController: UISearchBarDelegate, UITextFieldDelegate {
extension LocationViewController: UISearchBarDelegate, UITextFieldDelegate, UISearchResultsUpdating
{
func updateSearchResults(for searchController: UISearchController) {
guard let query = searchController.searchBar.text else { return }
dump(query)
}


@objc func clearSearchField(_ sender: Any) {
searchController.searchBar.text = nil
Expand All @@ -71,7 +107,46 @@ extension LocationViewController: UISearchBarDelegate, UITextFieldDelegate {

func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
!searchText.isEmpty
? (navigationItem.rightBarButtonItem = removeButton)
? (navigationItem.rightBarButtonItem = removeSearchTextField)
: (navigationItem.rightBarButtonItem = nil)
Comment on lines 109 to 111

Choose a reason for hiding this comment

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

참고로 이 부분은 아래와 같이 쓸 수도 있습니다 :)

navigationItem.rightBarButtonItem = !searchText.isEmpty ? removeSearchTextField : nil

}
}

extension LocationViewController: UICollectionViewDelegateFlowLayout, UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
locations.count
}

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = nearbyLoactionView.dequeueReusableCell(withReuseIdentifier: LocationCollectionViewCell.reuseIdentifier, for: indexPath) as? LocationCollectionViewCell else {
return UICollectionViewCell()
}
cell.cityName.text = locations[indexPath.row]
cell.spendingTime.text = locations[indexPath.row]
Comment on lines +124 to +125

Choose a reason for hiding this comment

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

locations.count가 곧 셀 개수가 되긴 하지만, index를 통해 정보를 빼 내올 때엔 늘 안전장치를 마련해두는 것이 좋습니다!
존재하지 않는 인덱스를 참조하게 되면 바로 크래시가 나게 되니까요.
그리고 여기서 쓰이는 location들은 추후 다른 정보들을 포함하게 될 가능성도 있어 보이네요. 별개의 타입으로 만들기에 적절해보입니다 :)

Choose a reason for hiding this comment

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

그리고 locations를 뷰 컨트롤러에서 가지고 있을 때의 장단점에 대해서도 생각해보면 좋겠네요ㅎㅎ

return cell
}

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: collectionView.frame.width, height: 64)
}



func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) ->
UICollectionReusableView {
switch kind {
case UICollectionView.elementKindSectionHeader:
let headerView = nearbyLoactionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: HeaderReusableView.ID, for: indexPath)
return headerView
default:
assert(false, "header, footer, withReuseIdentifier 를 확인하세요.")
}
}


func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
let width: CGFloat = nearbyLoactionView.frame.width
let headerViewHeight: CGFloat = 78
return CGSize(width: width, height: headerViewHeight)
}
}
Loading