-
Notifications
You must be signed in to change notification settings - Fork 82
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
[Team-13][iOS][Dale&Mase] 앱 초기 화면(검색 탭) 구현 #16
Conversation
- 대표 readme.md 추가(프로젝트, 팀원, wiki 소개) Refs #1
docs: .github 이슈, PR 템플릿 작성
[공통] 프로젝트 소개 docs 추가
- Airbnb 프로젝트 생성 - Alamofire 설치 - Swiftlint 설치 - 초기 주석제거 Closes #3
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
.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(["자연생활을 만끽할 수 있는 숙소", "독특한 공간"]) |
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.
추후 네트워크 모델을 설계해서, Mock String값을 직접 넣지 않고
서버로부터 받아온 값을 전달하도록 변경하겠습니다
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") |
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.
상수값을 관리하는 객체를 만들어서, String으로 raw하게 직접 넣지 않도록 개선해 보겠습니다 !
…l-init feat: [#16] application.yml 설정파일 초기작업 완료
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.
데일, 메이스 첫번째 PR대단히 수고많으셨습니다.!! Custom Observable / MVVM / CompositionalLayout 등 새롭게 도입하는 기술이 있는데도 불구하고 좋았던 것은 어떤 것을 만들려고 하시는지 명확한 의도, 시도가 보여서 좋았습니다.
3주간 지치지 않게 건강 관리 잘하시면서 프로젝트 진행하시기를 바랄게요!
질문에 대한 제 생각을 알려드리자면 좋은 해답이 될지는 모르겠지만 UICollectionViewDiffableDataSource을 사용하여 관리한다면 한 화면의 많은 데이터들이 각각 관리할 수 있는 장점이 있지 않을까 싶어요! 더해서 콜렉션뷰 섹션이 많아지고 셀 마다 데이터가 많아져도 섹션 / cell row 별로 업데이트 할 수 있게만 구조를 짠다면 괜찮지 않을까? 하는 생각을 하고 있습니다!!
|
||
import UIKit.UIColor | ||
|
||
extension UIColor { |
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.
이렇게 필요한 부분만 import해서 사용하는 것도 좋아요!
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.
꾸준히 신경써보도록 하겠습니다!
|
||
@available(*, unavailable) | ||
required init?(coder: NSCoder) { | ||
fatalError("init with coder is unavailable") |
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.
fatalError는 지양하는게 좋겠죠??!! ㅎㅎ
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.
처음엔 unavailable 태그를 달아서 해당 생성자를 사용할 경우 컴파일 에러가 발생하게 만들었기 때문에
생성자 안에 들어있는 내용이 fatalError여도 크게 상관 없다고 생각했었습니다.
required init에서도 fatalError가 아닌 override init과 동일하게 동작하도록 수정하겠습니다.
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 | ||
] | ||
} |
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.
https://betterprogramming.pub/leverage-the-coordinator-design-pattern-in-swift-5-cd5bb9e78e12
이 글도 한번 읽어보시는걸 추천드립니다!
MainTabBarViewController에 뷰컨트롤러들 인스턴스를 만들어서 탭바아이템에 추가시켜주는 방법도 있지만, 프로젝트를 진행하면서 뷰 컨트롤러간의 흐름을 제어하기에 편하고 무엇보다 앱의 흐름을 담당하는 별도의 객체를 만들어 사용하기 떄문에 앱의 상태를 독립적인 객체로 관리, 재사용하기에 좋은 패턴이라고 생각이 들고 무엇보다 사용하고자 하시는 MVVM패턴과 어울리는 패턴입니다! ㅎㅎ
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.
코디네이터 패턴은 한 번도 접해보지 못했었는데 알려주셔서 감사합니다.
첨부해주신 링크를 참고해서 공부해보고 프로젝트에 적용할 수 있다면 해보도록 하겠습니다 !
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 | ||
}() |
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.
코드로 UI를 작성할 때 코드의 길이가 길어지면서 불가피하게 가독성이 조금 떨어지는 경우가 생기게 되는 경우가 있죠! 작게는 UILabel부터 크게는 여러 UI요소가 합쳐진 컴포넌트 까지도 의식적으로 재사용성을 고려해서 커스텀해보는 것도 좋은 연습이 될거 같아요! titleLabel과 descriptionLabel 과 같은 부분을 color / yext / font 정도만 초기화할 떄 넣어주고 재사용할 수 있게 만드는 것도 좋겠죠! 😀
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.
CustomLabel로 분리 했습니다!
|
||
private let viewModel: SearchHomeViewModel | ||
|
||
private lazy var searchBarDelegate = DestinationSearchBarDelegate() |
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.
일반적으로 대리자 패턴을 사용할 떄는 옵셔널로 초기화해서 Retain Cycle을 피하려는 방식을 사용하기도 하는데 인스턴스를 이렇게 초기화하신 이유가 있을까요?~
viewModel.action.loadHeader.accept(()) | ||
viewModel.action.loadImage.accept(()) | ||
viewModel.action.loadCityName.accept(()) | ||
viewModel.action.loadTheme.accept(()) | ||
} |
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.
viewModel.action.loadHeader.accept(())
. 3번이나 들어가네요. 이 부분을 2번 정도로 줄이는 방법으로 개선한다면 좀 더 좋을거 같아요!
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.
viewModel이 action.loadHeader.accept()
를 함수로 제공해주는 방식으로 변경해보았습니다
protocol ViewModelProtocol { | ||
associatedtype Action | ||
associatedtype State | ||
|
||
var action: Action { get } | ||
var state: State { get } | ||
} | ||
|
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.
SearchHomeViewModel 파일 내에 ViewModelProtocol가 있는 것보다 좀 더 보기 좋게 파일을 나누는 것도 좋을거 같아요!
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.
별도 파일로 분리하겠습니다 ㅎㅎ
associatedtype State | ||
|
||
var action: Action { get } | ||
var state: State { get } |
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.
메서드가 아닌 속성으로 선언한 이유는 있을까요? 🤔
var action = Action() | ||
var state = State() |
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.
항상 private하게 선언하는 방법을 선택한다면 좋겠죠! 더 나아가서 Action과 State도 프로토콜로 하는 것과의 차이를 알아보시는 것도, 비교해보는 것도 좋은 학습이 될 것 같아요!
- CustomLabel 구현 - UILabel -> CustomLabel 변경 ref #5
- VC가 ViewModel의 인터페이스에 의존하도록 변경
[#16] refactor: 숙소 찾기 화면에서 사용하지 않는 메서드 제거 및 주석 제거
[#16] hotfix: 1주차 수요일 코드리뷰 반영(구찌)
[iOS] 체크인/ 체크아웃 날짜 결정하는 캘린더 뷰 구현
refactor: 리뷰받은 내용 및 부족한 부분 추가
[iOS] info.plist 프로젝트 설정 파일 추가(위치정보)
Feature/android/UI/compose
* [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>
안녕하세요 만사🖐
3주동안 함께 할 데일과 메이스입니다.
이번 프로젝트에서는 Custom Observable 클래스 객체를 활용해서
MVVM 패턴에 익숙해져보려 합니다.
여태 MVC만 사용해보다 처음으로 MVVM을 적용해보려니 쉽지 않네요 ㅜ-ㅜ
아직 Mock으로만 화면을 구성해 둬서, 네트워크 서비스까지 흐름이 연결되지 않았지만,
이후 네트워크 관련 모델을 설계해서, 최종적으로 JK의 Butterfly 아키텍처를 프로젝트에 적용해보는 것이 목표입니다.
그럼 3주 동안 잘 부탁드리겠습니다🙂
구현 사항
CompositionalLayout을 이용한 UICollectionView 구현
각 섹션별로 사용할 Cell
각 섹션별로 사용할 NSCollectionLayoutSection
Custom Observable 구현
우선 단일 이벤트를 subscriber들에게 전달하는 기능만 하는
단순한 형태의 Observable 클래스를 구현해서 사용해보았습니다.
MVVM도 Rx와 마찬가지로 이번에 처음 적용해보는 아키텍처라 조금 미흡할 수 있다는 점.. 미리 말씀드리겠습니다😭
구현할 사항
고민했던 점
UICollectionViewCompositionalLayout
각 섹션별로 셀의 크기가 다르도록 구현을 하려고 했습니다.
등등 여러 방법을 고려했고, 이번 미션의 경우 두 번째 선택지도 선택할 수 있었지만,
추후에 기존의 섹션과 스크롤 방향이 다른 섹션이 추가될 수 도 있다고 생각하여 CompositionalLayout을 활용하였습니다.
조언을 얻고 싶은 부분
CollectionView를 CompositionalLayout을 사용해서 구현해 보니
![CleanShot 2022-05-25 at 16 41 31@2x](https://user-images.githubusercontent.com/57667738/170215807-9a565546-4980-49b9-bdf2-cfa0ce6014b3.png)
하나의 CollectionView로 여러 개의 CollectionView의 효과를 낼 수 있다는 점에서는 좋은 것 같습니다.
하지만 반대로, 하나의 CollectionView 객체만으로 모든 collection을 관리해야하므로
각각의 collection이 갖는 item들의 DataSource를 분리할 수 없었습니다.
이렇게 될 경우, 만약 표현해야할 collection의 개수가 많아진다면
DataSource가 분리되지 않아 비대해지고, 재사용하기 어려워진다는 문제점이 생기는 것 같습니다.
이처럼 UICollectionViewFlowLayout이 아닌 CompositionalLayout을 사용할 경우 장단점이 존재하는데,
만사는 이 부분에 대해 어떻게 생각하시는지 의견을 여쭤보고 싶습니다!