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-13][iOS][Dale&Mase] 앱 초기 화면(검색 탭) 구현 #16

Merged
merged 40 commits into from
May 26, 2022

Conversation

sanghyeok-kim
Copy link
Collaborator

@sanghyeok-kim sanghyeok-kim commented May 25, 2022

안녕하세요 만사🖐
3주동안 함께 할 데일과 메이스입니다.
이번 프로젝트에서는 Custom Observable 클래스 객체를 활용해서
MVVM 패턴에 익숙해져보려 합니다.
여태 MVC만 사용해보다 처음으로 MVVM을 적용해보려니 쉽지 않네요 ㅜ-ㅜ

아직 Mock으로만 화면을 구성해 둬서, 네트워크 서비스까지 흐름이 연결되지 않았지만,
이후 네트워크 관련 모델을 설계해서, 최종적으로 JK의 Butterfly 아키텍처를 프로젝트에 적용해보는 것이 목표입니다.

CleanShot 2022-05-25 at 17 13 48@2x

그럼 3주 동안 잘 부탁드리겠습니다🙂



구현 사항

CleanShot 2022-05-25 at 15 43 44

CompositionalLayout을 이용한 UICollectionView 구현

  • UICollectionVIewCell
    각 섹션별로 사용할 Cell
    • HeroImageViewCell
    • NearDestinationViewCell
    • TravelThemeViewCell
  • UICollectionViewCompositionalLayout
    각 섹션별로 사용할 NSCollectionLayoutSection
    • heroImageLayoutSection
    • nearDestinationLayoutSection
    • themeLayoutSection
  • DestinationHeaderView 구현

Custom Observable 구현

  • RxSwift를 사용하고는 싶었으나.. 아직 정말 입문 단계에 불과해서
    우선 단일 이벤트를 subscriber들에게 전달하는 기능만 하는
    단순한 형태의 Observable 클래스를 구현해서 사용해보았습니다.
    MVVM도 Rx와 마찬가지로 이번에 처음 적용해보는 아키텍처라 조금 미흡할 수 있다는 점.. 미리 말씀드리겠습니다😭


구현할 사항

  • Mock Image가 아닌 실제 API에서부터 값을 가져와 출력하도록 변경
  • offset, font size 등 상수값 raw하게 사용하지 않고 별도로 관리하는 구조체를 구현해서 사용하는 방식으로 변경
  • 상단 SearchBar 터치시 push되는 VC의 기능 구현(여행지 검색 기능)


고민했던 점

UICollectionViewCompositionalLayout

각 섹션별로 셀의 크기가 다르도록 구현을 하려고 했습니다.

  • scrollView 안에 여러개의 collectionView 를 활용하기
  • collectionViewCell 안에 custom View 를 만들어 섹션별로 다른 View를 add 하기
  • UICollectionViewCompositionalLayout 활용하기

등등 여러 방법을 고려했고, 이번 미션의 경우 두 번째 선택지도 선택할 수 있었지만,
추후에 기존의 섹션과 스크롤 방향이 다른 섹션이 추가될 수 도 있다고 생각하여 CompositionalLayout을 활용하였습니다.



조언을 얻고 싶은 부분

  • CollectionView를 CompositionalLayout을 사용해서 구현해 보니
    하나의 CollectionView로 여러 개의 CollectionView의 효과를 낼 수 있다는 점에서는 좋은 것 같습니다.
    하지만 반대로, 하나의 CollectionView 객체만으로 모든 collection을 관리해야하므로
    각각의 collection이 갖는 item들의 DataSource를 분리할 수 없었습니다.
    CleanShot 2022-05-25 at 16 41 31@2x

      //DestinationCollecionViewDataSource.swift
      func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
          guard let sectionKind = DestinationCollectionViewSection(rawValue: section) else { return 0 }
          
          switch sectionKind {
          case .image:
              return mockImage.count
          case .nearby:
              return mockCity.count
          case .theme:
              return mockTheme.count
          }
      } // -> 따라서 하나의 DataSource에서 section의 종류에 따라 각각 분기해서 처리해야 함

    이렇게 될 경우, 만약 표현해야할 collection의 개수가 많아진다면
    DataSource가 분리되지 않아 비대해지고, 재사용하기 어려워진다는 문제점이 생기는 것 같습니다.

    이처럼 UICollectionViewFlowLayout이 아닌 CompositionalLayout을 사용할 경우 장단점이 존재하는데,
    만사는 이 부분에 대해 어떻게 생각하시는지 의견을 여쭤보고 싶습니다!

ku-kim and others added 30 commits May 23, 2022 12:08
- 대표 readme.md 추가(프로젝트, 팀원, wiki 소개)

Refs #1
docs: .github 이슈, PR 템플릿 작성
[공통] 프로젝트 소개 docs 추가
- Airbnb 프로젝트 생성
- Alamofire 설치
- Swiftlint 설치
- 초기 주석제거

Closes #3
feat: Airbnb 프로젝트 생성 및 초기설정
- 요구사항에서 사용되는 Color RGB값을 통해 직접 구현

Closes #3
- Resource, Source

Closes #3
- Resource, Source

Closes #3
- CustomTextField 구현 (SearchTextField)
- CustomView 구현 (SearchBarView)
- Delegate 구현 (빈 영역 터치시 키보드 resign)

Closes #5
- 스크롤 방향 설정 (horizontal)
- item layout 설정

Closes #5
- CollectionView Cell layout 설정
- Custom UIFont 구현

Closes #5
- SwiftLint line_length 제한 제거
- SPM을 통한 SnapKit 설치 및 기존 코드 리팩토링
- SearchBarView 삭제, SearchTextField 단독 사용

#5
- MainTabBarController 생성 및 구현
- 초기 화면을 검색 탭으로 설정

#5
- HeroImageViewCell
- NearDestinationViewCell
- TravelThemeViewCell

#5
…405/airbnb into iOS-feature/SearchHomeVC

# Conflicts:
#	iOS/AirbnbApp/AirbnbApp.xcodeproj/project.pbxproj
- Section 을 활용하여 CollectionView의 section별로 Cell의 레이아웃 변경

#5
- 각 섹션별로 cell의 layout을 다르게 그릴수 있게 변경

#5
- MainTabBarController 생성 및 구현
- 초기 화면을 검색 탭으로 설정
(conflict 해결하느라 변경사항이 사라져서 다시 커밋합니다)

#5
- 초기값이 필요한 BehaviorRelay
- 초기값이 필요 없는 PublishRelay
- SearchTextField를 UITextField에서 UISearchBar로 변경
- SearchTextField -> DestinationSearchBar로 이름 뱐걍

#5
- SF Font 파일 추가
- UIFont Extension에 반영

ref #5
- NotoSans -> SFProDisplay로 변경

ref #5
…아웃 수정

- containerStackView에서 receiveIdeaButton을 별도로 분리해서 각각 레이아웃

ref #5
- SearchHomeVC에서 SearchHomeVM의 PublishRelay타입의 프로퍼티에 바인딩을 걸고, accept를 통해 이벤트 발행
- String값을 직접 넘겨주는 방식 -> API와 통신해서 얻은 Data로 넘겨주는 방식으로 변경 예정

ref #5
- Header Label 출력되도록 추가
- CollectionView 레이아웃 일부 수정

ref #5
- SectionLayoutFactory 추가
- createCompositionalLayout 함수 static 하게 변경

ref #5
- DestinationCollectionViewDelegateFlowLayout.swift
@sanghyeok-kim sanghyeok-kim changed the title [Team-13][iOS][Dale&Mase] 앱 초기 화면(검색 탭) [Team-13][iOS][Dale&Mase] 앱 초기 화면(검색 탭) 구현 May 25, 2022
Comment on lines +40 to +57
.bind(onNext: { [weak self] in
self?.state.loadedHeader.accept(["", "가까운 여행지 둘러보기", "어디에서나, 여행은 살아보는거야!"])
})

action.loadImage
.bind(onNext: { [weak self] in
// TODO: Mock -> Network Manager로 받아온 URL 넘겨주는 방식으로 변경해보기
self?.state.loadedImage.accept("mockimage.png")
})

action.loadCityName
.bind(onNext: { [weak self] in
self?.state.loadedCityName.accept(["서울", "광주", "부산", "대구"])
})

action.loadTheme
.bind(onNext: { [weak self] in
self?.state.loadedTheme.accept(["자연생활을 만끽할 수 있는 숙소", "독특한 공간"])
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

추후 네트워크 모델을 설계해서, Mock String값을 직접 넣지 않고
서버로부터 받아온 값을 전달하도록 변경하겠습니다

Comment on lines +23 to +32
searchHomeViewController.tabBarItem.title = "검색"
searchHomeViewController.tabBarItem.image = UIImage(systemName: "magnifyingglass")

let wishListViewController = UIViewController()
wishListViewController.tabBarItem.title = "위시리스트"
wishListViewController.tabBarItem.image = UIImage(systemName: "heart")

let reservationViewController = UIViewController()
reservationViewController.tabBarItem.title = "내 예약"
reservationViewController.tabBarItem.image = UIImage(systemName: "person")
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

상수값을 관리하는 객체를 만들어서, String으로 raw하게 직접 넣지 않도록 개선해 보겠습니다 !

wooody92 pushed a commit that referenced this pull request May 25, 2022
…l-init

feat: [#16] application.yml 설정파일 초기작업 완료
Copy link

@ITzombietux ITzombietux left a comment

Choose a reason for hiding this comment

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

데일, 메이스 첫번째 PR대단히 수고많으셨습니다.!! Custom Observable / MVVM / CompositionalLayout 등 새롭게 도입하는 기술이 있는데도 불구하고 좋았던 것은 어떤 것을 만들려고 하시는지 명확한 의도, 시도가 보여서 좋았습니다.
3주간 지치지 않게 건강 관리 잘하시면서 프로젝트 진행하시기를 바랄게요!

질문에 대한 제 생각을 알려드리자면 좋은 해답이 될지는 모르겠지만 UICollectionViewDiffableDataSource을 사용하여 관리한다면 한 화면의 많은 데이터들이 각각 관리할 수 있는 장점이 있지 않을까 싶어요! 더해서 콜렉션뷰 섹션이 많아지고 셀 마다 데이터가 많아져도 섹션 / cell row 별로 업데이트 할 수 있게만 구조를 짠다면 괜찮지 않을까? 하는 생각을 하고 있습니다!!


import UIKit.UIColor

extension UIColor {

Choose a reason for hiding this comment

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

이렇게 필요한 부분만 import해서 사용하는 것도 좋아요!

Choose a reason for hiding this comment

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

꾸준히 신경써보도록 하겠습니다!


@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init with coder is unavailable")

Choose a reason for hiding this comment

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

fatalError는 지양하는게 좋겠죠??!! ㅎㅎ

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

처음엔 unavailable 태그를 달아서 해당 생성자를 사용할 경우 컴파일 에러가 발생하게 만들었기 때문에
생성자 안에 들어있는 내용이 fatalError여도 크게 상관 없다고 생각했었습니다.
required init에서도 fatalError가 아닌 override init과 동일하게 동작하도록 수정하겠습니다.

Comment on lines +26 to +41
let searchHomeViewController = UINavigationController(rootViewController: SearchHomeViewController(viewModel: SearchHomeViewModel()))
searchHomeViewController.tabBarItem.title = "검색"
searchHomeViewController.tabBarItem.image = UIImage(systemName: "magnifyingglass")

let wishListViewController = UIViewController()
wishListViewController.tabBarItem.title = "위시리스트"
wishListViewController.tabBarItem.image = UIImage(systemName: "heart")

let reservationViewController = UIViewController()
reservationViewController.tabBarItem.title = "내 예약"
reservationViewController.tabBarItem.image = UIImage(systemName: "person")

viewControllers = [
searchHomeViewController, wishListViewController, reservationViewController
]
}

Choose a reason for hiding this comment

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

https://betterprogramming.pub/leverage-the-coordinator-design-pattern-in-swift-5-cd5bb9e78e12

이 글도 한번 읽어보시는걸 추천드립니다!
MainTabBarViewController에 뷰컨트롤러들 인스턴스를 만들어서 탭바아이템에 추가시켜주는 방법도 있지만, 프로젝트를 진행하면서 뷰 컨트롤러간의 흐름을 제어하기에 편하고 무엇보다 앱의 흐름을 담당하는 별도의 객체를 만들어 사용하기 떄문에 앱의 상태를 독립적인 객체로 관리, 재사용하기에 좋은 패턴이라고 생각이 들고 무엇보다 사용하고자 하시는 MVVM패턴과 어울리는 패턴입니다! ㅎㅎ

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

코디네이터 패턴은 한 번도 접해보지 못했었는데 알려주셔서 감사합니다.
첨부해주신 링크를 참고해서 공부해보고 프로젝트에 적용할 수 있다면 해보도록 하겠습니다 !

Comment on lines 22 to 38
private lazy var titleLabel: UILabel = {
let label = UILabel()
label.text = "슬기로운\n자연생활"
label.numberOfLines = 0
label.font = .SFProDisplay.medium
label.textColor = .Custom.black
return label
}()

private lazy var descriptionLabel: UILabel = {
let label = UILabel()
label.text = "에어비앤비가 엄선한\n위시리스트를 만나보세요"
label.numberOfLines = 0
label.font = .SFProDisplay.regular(17)
label.textColor = .Custom.gray1
return label
}()

Choose a reason for hiding this comment

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

코드로 UI를 작성할 때 코드의 길이가 길어지면서 불가피하게 가독성이 조금 떨어지는 경우가 생기게 되는 경우가 있죠! 작게는 UILabel부터 크게는 여러 UI요소가 합쳐진 컴포넌트 까지도 의식적으로 재사용성을 고려해서 커스텀해보는 것도 좋은 연습이 될거 같아요! titleLabel과 descriptionLabel 과 같은 부분을 color / yext / font 정도만 초기화할 떄 넣어주고 재사용할 수 있게 만드는 것도 좋겠죠! 😀

Choose a reason for hiding this comment

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

CustomLabel로 분리 했습니다!


private let viewModel: SearchHomeViewModel

private lazy var searchBarDelegate = DestinationSearchBarDelegate()

Choose a reason for hiding this comment

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

일반적으로 대리자 패턴을 사용할 떄는 옵셔널로 초기화해서 Retain Cycle을 피하려는 방식을 사용하기도 하는데 인스턴스를 이렇게 초기화하신 이유가 있을까요?~

Comment on lines 96 to 100
viewModel.action.loadHeader.accept(())
viewModel.action.loadImage.accept(())
viewModel.action.loadCityName.accept(())
viewModel.action.loadTheme.accept(())
}

Choose a reason for hiding this comment

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

viewModel.action.loadHeader.accept(())

. 3번이나 들어가네요. 이 부분을 2번 정도로 줄이는 방법으로 개선한다면 좀 더 좋을거 같아요!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

viewModel이 action.loadHeader.accept()를 함수로 제공해주는 방식으로 변경해보았습니다

Comment on lines 10 to 17
protocol ViewModelProtocol {
associatedtype Action
associatedtype State

var action: Action { get }
var state: State { get }
}

Choose a reason for hiding this comment

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

SearchHomeViewModel 파일 내에 ViewModelProtocol가 있는 것보다 좀 더 보기 좋게 파일을 나누는 것도 좋을거 같아요!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

별도 파일로 분리하겠습니다 ㅎㅎ

associatedtype State

var action: Action { get }
var state: State { get }

Choose a reason for hiding this comment

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

메서드가 아닌 속성으로 선언한 이유는 있을까요? 🤔

Comment on lines +34 to +35
var action = Action()
var state = State()

Choose a reason for hiding this comment

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

항상 private하게 선언하는 방법을 선택한다면 좋겠죠! 더 나아가서 Action과 State도 프로토콜로 하는 것과의 차이를 알아보시는 것도, 비교해보는 것도 좋은 학습이 될 것 같아요!

@ITzombietux ITzombietux merged commit 7755aeb into codesquad-members-2022:team-13 May 26, 2022
sabgilhun pushed a commit that referenced this pull request May 26, 2022
SangHwi-Back pushed a commit that referenced this pull request May 27, 2022
[#16] refactor: 숙소 찾기 화면에서 사용하지 않는 메서드 제거 및 주석 제거
SangHwi-Back pushed a commit that referenced this pull request May 27, 2022
SangHwi-Back pushed a commit that referenced this pull request May 27, 2022
[#16] hotfix: 1주차 수요일 코드리뷰 반영(구찌)
wnsxor1993 added a commit that referenced this pull request May 27, 2022
[iOS] 체크인/ 체크아웃 날짜 결정하는 캘린더 뷰 구현
ink-0 pushed a commit that referenced this pull request May 28, 2022
wooody92 pushed a commit that referenced this pull request May 28, 2022
refactor: 리뷰받은 내용 및 부족한 부분 추가
GangWoon pushed a commit that referenced this pull request May 31, 2022
[iOS] info.plist 프로젝트 설정 파일 추가(위치정보)
honeySleepr added a commit that referenced this pull request Jun 1, 2022
junseokseo9306 added a commit that referenced this pull request Jun 3, 2022
Min-92 pushed a commit that referenced this pull request Jun 8, 2022
* [sally4405/#13] Fix: 커밋하다 실수로 들어가게된 코드 수정

* [sally4405/#13] Fix: 메인 히어로 이미지 주소 변경 및 API 명세에 맞게 주소 변경

- 글자가 없는 히어로 이미지로 수정
- /main 앞에 /banners 추가

* [sally4405/#13] Fix: 인기 여행지 조회 주소 수정

- API 명세에 맞게 travel에서 travels로 수정

* [sally4405/#13] Fix: 이미지 주소 추가

Co-authored-by: Louie-03 <dhdustnr0134@naver.com>

* [sally4405/#13] Fix: 테스트 코드에 imageUrl 추가

Co-authored-by: Louie-03 <dhdustnr0134@naver.com>

* [sally4405/#16] Style: RoomInformation 필드명 오타 수정

- bathroomCont 오타 수정

* [sally4405/#16] Feat: 특정 지역의 숙소 요금 정보 조회 API 개발

- JPA에 JPQL을 이용하여 입력 받은 지역이 포함된 숙소를 조회하여 해당 숙소의 가격을 리스트로 조회
- 리스트로 가져온 가격들의 평균 값을 계산
- 값을 확인하기 위해 예제 데이터 data.sql에 추가

* [sally4405/#18] Feat: 숙소 상세 정보 조회 API 개발

- Spring Data JPA가 기본 제공하는 findById() 메서드를 이용하여 숙소 조회
- 유효하지 않은 id로 요청시 IllegalArgumentException 던지도록
- dto에 wishlist 필드 업데이트는 어떻게 해야할지 의문

* [sally4405/#18] Feat: 숙소 상세 정보 조회 dto에 wish 추가

- WishRepository에서 해당 숙소의 id로 wish를 조회하여 숙소 상세 정보를 조회하는 dto에 wish 값 추가

* [sally4405-#4] Style: WishList 관련 클래스 이름 변경

* [sally4405-#17] Feat: 샘플 데이터 추가

* [sally4405-#17] Feat: 예약 상세 조회 기능 구현

* [sally4405-#17] Fix: 테스트 실패 오류 수정

* [sally4405-#25] Feat: 예약 취소 기능 구현

* [sally4405-#27] Feat: 위시리스트 조회 기능 구현

* [sally4405/#19] Feat: 숙소 예약 요금 상세 조회 API 개발

- 주 단위, 월 단위, 연 단위 할인 정책 enum 클래스를 만들어 예약한 날짜를 계산하여 할인을 적용할 수 있는 로직 추가

* [sally4405/#30] Fix: 추가한 예제 데이터를 테스트 결과에 반영하도록 수정 및 Slf4j 어노테이션 삭제

* [sally4405-#27] Feat: 위시리스트 추가 기능 구현

* [sally4405/#20] Feature: 예약 저장 API 구현

- POST 요청이 왔을 때 Request Body에 숙소 id, 체크인, 체크아웃 날짜, 예약 인원, 최종 가격이 dto에 담겨져 들어온다
- dto에 담겨진 데이터를 통해 Book 객체를 생성하여 BookRepository의 save 메서드를 이용해 예약을 저장한다.

* [sally4405/#21] Feature: 예약 목록 조회 API 구현

- BookRepository에서 예약된 Book들을 조회하여 예약 목록을 조회할 수 있도록 dto에 담아서 반환

* [sally4405/#21] Fix: 체크인, 체크아웃 타입 변경

- LocalDateTime에서 LocalDate로 변경

* [sally4405-#36] Feat: 입력한 값에 맞는 모든 숙소 조회 기능 구현

Co-authored-by: donggi-lee-bit <devdonggilee@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
review-iOS Extra attention is needed
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants