# LottoDiary
### 컨벤션
- return 전 개행 : return 값에 대한 강조
- return 코드 한 줄일 경우 개행 x
- UI 클로저 내의 return은 개행 x
- 커스텀 타입 선언 클로저 첫 줄 개행
- extension 또한 첫 줄 개행
- 함수 클로저는 개행 x
- 100자 넘어가면 개행
- @objc func 사용 시 줄 바꿈
- 파일 내에서만 쓰이는 상수, 문자열 리터럴, systemName은 맨 하단에 fileprivate extension으로 정의
- 인스턴스 프로퍼티에 self 키워드 명시
- 빈 값이나 0 대신 .none, .zero 사용하기
### 커밋 규칙
- feat: 기능 추가. 기능 하나만. 메서드하나만 있다면. 메서드 하나만 커밋.
- fix: 버그 수정
- refactor: 기능은 동일하지만 코드 수정.
- chore: 간단한 코드 변경. (스타일, 공백제거)
- remove: 파일 삭제
- add: 파일 추가
- docs: README 수정.
### 브랜치 전략
- develop: 개발브랜치. feat, fix된 걸 여기에 병합
- feat: 기능 추가 -> feat/이슈번호/기능이름
- fix: 버그 수정
- main: 최종 브랜치
### 일정
- Coordinator + 클린아키 + 컴바인 공부
## 기술적 도전
- 무한 스크롤이 되는 캘린더
- 코디네이터 패턴 적용
- 웹 크롤링
### 트러블 슈팅
#### 1. 코디네이터와 라우터의 차이.
- 기존 ViewController = 앱 계층구조 관리 + 코디네이터 + 라우터
- 라우터 : 화면 전환만을 담당하는 객체
- 코디네이터 : navigation flow를 관리하는 객체
- **결론** : 코디네이터 패턴을 적용함으로써 ViewController이 '**뷰 계층구조의 관리**'라는 역할만을 담당할 수 있도록 구현
#### Flow를 나누는 기준.
#### 하나의 Flow에 하나의 UINavigationController
- 하나의 Flow 안에서 여러 화면으로 전환되는 경우 router를 사용한다.
- 이때 router은 해당 Flow의 rootController의 UINavigationController를 전달한다.
- 때문에 하나의 Flow 안에서 화면 전환을 할 경우, 하나의 UINavigationController를 사용한다.
```swift
// CoordinatorFactoryImp.swift
func makeHomeCoordinator(navigationController: UINavigationController?) -> Coordinator {
let coordinator = HomeCoordinator(
router: router(navigationController),
moduleFactory: ModuleFactoryImp(),
coordinatorFactory: CoordinatorFactoryImp()
)
return coordinator
}
```
➡️ router는 HomeViewController의 UINavigationController를 파라미터로 받는다.
- ApplicationCoordinator 코드
UITabBar의 각 탭들은 rootController의 UINavigationController 사용한다.(= 탭 마다 하나의 네비게이션 컨드롤러 사용)
탭을 클릭해 flow를 전환하면`setRootModule`을 사용해 해당 navigation stack을 초기화한다. (= ApplicationCoordinator의 UINavigationController의 viewControllers 속성을 초기화 한다.)
-> 이전 flow에 해당하는 화면은 초기화를 통해 지우고, 현재 flow에 해당하는 화면만 메모리에 존재하게 됨. (= 변경된 flow의 UINavigationController를 ApplicationCoordinator의 UINavigationController 스택에 추가한다.)
```swift!
func setRootModule(_ module: Presentable?) {
guard let controller = module?.toPresent() else { return }
navigationController?.setViewControllers([controller], animated: false)
}
```
- setRootModule : 기존 navigation stack을 초기화시키고 새로운 화면을 보여주는 역할
- 앱 내에서 하나의 router를 사용하기 때문이다.
- 하나의 router에는 하나의 navigation controller가 존재한다. 하나의 navigation controller로 여러 flow를 전환하려면 flow 전환마다 navigation stack을 초기화해야 한다.
- tabBarController는 navigation controller로 tabBarItem을 생성한다.
- 탭바의 특수성으로 Flow안에 Flow가 존재하는 경우 Router가 두 개 생긴다.
- 왜 tabBarCoordinator는 init의 파라미터로 TabbarView protocol을 받을까?
- TabbarView protocol : tabbar가 해야할 행동을 프로토콜로 정의
- 왜 tabBar는 ModuleFactory이 없는가?
- tabBar는 모듈로써의 자격을 충족하지 못한다.
- 모듈이란 App에서 관련 데이터와 함수를 하나로 묶은 단위이다. 모듈은 독립적으로 작동되어야 하는데 tabbar는 독립적으로 작동하지 못한다.
- 모듈이 아니기 때문에 ModuleFactory을 통해 View protocol을 받을 수 없고 직접 init의 파라미터로 받는다.
### Nav의 viewControllers프로퍼티에 뷰가 언제 담겨야하는지(tabBarController가 생길떄? tabBarCoordiator가 생길 때)
- 스토리보드 구현시, vc 인스턴스 초기화 시점과 viewDidLoad 호출 시점이 다름
- **instantiateViewController** : 스토리 보드 구현시 vc 인스턴스를 초기화하는 함수이다.
- 해당 함수를 통해 초기화 할 경우, viewDidLoad 함수는 호출되지 않는다.
- viewDidLoad는 vc가 화면에 present하는 시점에서 호출된다.
- 코드로 구현시 vc 인스턴스 초기화 시점과 viewDidLoad 호출 시점이 같음
- 코드로 구현하면 `let controller = ViewController()` 와 같은 코드로 초기화한다.
- 코드 초기화는 VC 인스턴스 초기화와 동시에 viewDidLoad() 함수가 호출된다.
### start의 역할
- 화면과 화면흐름에 필요한 기능을 설정. 흐름에 필요한 기능은 클로저나 델리게이트로 설정하는데 우리 프로젝트에서는 클로저로 가기로 결정.
### Coordinator 생성 이후 ViewController 실행
- instantiateViewController : storyboard에서 ViewController에 접근하는 메서드
- storyboard에서 ViewController의 instance를 생성하는 메서드이다.
- ""인스턴스를 생성하는 시점이다. ViewController가 인스턴스화 되는 시점이지 viewDidLoad()가 실행되는 시점은 아니다.""
-
- [참고: 실제 화면 전환이 일어나는 순간이 언제인지 알아보자 = viewDidLoad() 메소드 호출 시점](https://sweethoneybee.tistory.com/38#:~:text=iOS-,%EC%8B%A4%EC%A0%9C%20%ED%99%94%EB%A9%B4%20%EC%A0%84%ED%99%98%EC%9D%B4%20%EC%9D%BC%EC%96%B4%EB%82%98%EB%8A%94%20%EC%88%9C%EA%B0%84%EC%9D%B4%20%EC%96%B8%EC%A0%9C%EC%9D%B8%EC%A7%80%20%EC%95%8C%EC%95%84,viewDidLoad()%20%EB%A9%94%EC%86%8C%EB%93%9C%20%ED%98%B8%EC%B6%9C%20%EC%8B%9C%EC%A0%90)
### useCase 분리. 편집화면과 설정화면. 단일책임 원칙
-
### 캘린더 트러블 슈팅 여기에다가 적기!
- 날짜 데이터를 한번에 넣어 줄 것인가 vs 스크롤을 인식해서 데이터를 업데이트 시켜 줄 것인가
- 날짜 데이터를 한번에 넣어준다면 방대한 날짜 데이터를 UICollectionViewDataSource에 적용해야 한다.
- 실제 해당 방법으로 실행해보니 스크롤 시 데이터 업데이트의 버벅임이 존재했다.
- 또한 방대한 데이터를 한꺼번에 적용하는 방법이 효율적인 방법이라는 생각이 들지 못했다.
- **결국, 스크롤을 인식해서 데이터를 업데이트 하는 방법으로 결정했다.**
### 로또 캘린더 챌린지
### 1. UICollectionViewCell 내부 컬렉션 뷰가 아닌 외부 컬렉션뷰가 스크롤되는 문제
- 상황: UICollectionView내에 달력에 해당하는 UICollectionViewCell 3개가 FlowLayout으로 Horizontal로 배치되어있고 UICollectionViewCell 내의 달력 레이아웃을 Diffable로 정의.
- 문제점: 주간 캘린더로 변경 후 스크롤 시 CollectionViewCell 내의 collectionView만 업데이트되는 것이 아닌 외부의 collectionView가 업데이트가 된다.
### 2. ScrollDirection을 Horizontal로 했을 때 Horizontal groupPaging이 불가한 문제
- 상황: 하나의 컬렉션 뷰 내에 3개 월에 해당하는 day를 주입. 즉 하나의 DateCell만 존재. 컬렉션 뷰의 스크롤방향은 수평인 상태.
- 문제점: 컬렉션 뷰의 orthogonalScrollingBehavior는 레이아웃 방향의 수직 방향이라서 Horizontal의 수직이면 vertical로 된다. 그렇다고 none으로 두게 되면 월마다 맞춰져서 멈추는 것이 아니기 때문에 캘린더에 맞지않는 UX를 제공하게 됨.
### 3. visibleItemsInvalidationHandler를 이용하여 좌우 스크롤 감지 문제
- 상황: 하나의 컬렉션 뷰 내에 3개 월에 해당하는 day를 주입. 즉 하나의 DateCell만 존재. 컬렉션 뷰의 스크롤 방향은 수직인 상태.
- 문제점: 컬렉션 뷰의 스크롤 방향은 수직이기 때문에 scrollViewDelegate가 수직으로 감지되기 때문에 사용이 불가함. 따라서 visivisibleItemsInvalidationHandler를 이용해서 좌우 스크롤을 판별.
### 스크롤 뷰
- auto layout
### HomeScene - UIStackView
- UILabel의 font에 따라 내재되어 있는 높이가 변경된다.
- 초기 구현시, HomeScene의 UILabel을 UIView 내부에 구현했다.
- 하지만 UIView는 높이가 고정되어있기 때문에 만약 UILabel의 폰트 크기가 변경된다면 글자의 위, 아래가 부분적으로 잘려 보이는 문제가 발생했다.
- UILabel을 감싸는 super view를 UIStackView로 구현함으로써 UILabel의 폰트 크기가 변경될 시 유동적으로 super view의 높이 또한 변경될 수 있도록 구현했다.
### 주간, 월간 이동 시 오토레이아웃 문제
- 주간에서 월간으로 이동할 때 컬렉션뷰의 높이와 셀의 높이 둘 다 변경을 해줘야 하는 상황.
- 컬렉션 뷰의 높이를 먼저 변경하고 변경된 데이터소스를 apply한 뒤에 셀의 높이를 변경시켰더니 오토레이아웃 에러 발생.
- 화면에는 정상적으로 보였으나 로그 때문에 디버깅.
- 컬렉션 뷰의 height를 변경하고 reloadData를 안하면 셀의 height가 변경되지않은 상태에서 apply가 일어남.
- 컬렉션 뷰의 height를 변경하고 reloadData를 하면 셀의 height가 바로 변경이되고 변경된 상태에서 apply가 일어남.
- 결론은 컬렉션 뷰의 오토레이아웃을 수정할 떄는 reloadData를 통해 셀의 높이까지 완전히 변경한 후에 진행해야 한다.
### ChartScene
- Repository -> Usecase -> ViewModel -> ViewController
- 상태값을 갖고 있는 객체는 무엇인가?
- ViewModel!
### 로또 Card 테이블 뷰 불가
- 기존에 레이아웃은 셀들 사이에 간격이 있다. 테이블뷰에서는 섹션별로 간격을 줄 수는 있지만 item 사이에는 주지 않는다. 따라서 컬렉션 뷰를 차용했다.
## 스크롤뷰 오토레이아웃 잡을때는 맨 위에 오는걸 contentView.top에 맞추고 맨 아래에 오는 걸 contentView.bottom에 맞추자..
** 카메라
https://itllbegone.tistory.com/7
https://nashorn.tistory.com/entry/Swift-QR%EC%BD%94%EB%93%9C-%EC%8A%A4%EC%BA%94-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84
** 하프모달
https://iamcho2.github.io/2021/06/25/half-modal-viewcontoller
-> panGesture나중에 해보자.
## 로또 결과 화면 크롤링
### 상황 판단
- 로또 QR을 인식 후, 해당 사이트에서 로또 결과 정보를 받아오기 위해 크롤링을 해야 한다.
- URLSession을 통한 네트워킹으로 받아온 data가 encoding 이 되지 않는 문제가 발생했다.
- 시도했던 encoding type: UTF8
### 고민
#### 고민 1) 동적 페이지, 정적 페이지의 문제 ?
- ~ 동적/정적 페이지 솰라 솰라 ~
- 하지만 이 문제는 아니엿음.
#### 고민 2) 인코딩 타입의 문제 ?
- 두 번째로 추측한 원인은 '적절하지 않은 인코딩 타입'이었다.
- 이 원인이 확실한지 알아보기 위해 HTTP 응답 헤더를 확인하였다.
- HTTP 응답 헤더의 'Content-Type'을 확인하면 실제 사용되는 인코딩 타입을 찾을 수 있다.
```htmlembedded
// HTTP 응답 헤더 중 Content-Type
"Content-Type" = (
"text/html;charset=EUC-KR"
);
````
- 확인해본 결과, 해당 데이터는 **EUC-KR(Extended Unix Code-KR) 또는 CP949** 로 불리는 인코딩 타입을 사용하고 있었다.
- 해당 타입은 한글 완성형 인코딩으로 주로 오래된 한국 기업 웹페이지에서 사용한다.
### 해결
- Data 타입을 String 타입으로 변환하기 위해서는 `String(data: Data, encoding: String.Encoding)` Initializer를 사용한다.
- 하지만 String.Encoding의 타입 프로퍼티에는 "EUC-KR" 타입이 존재하지 않는다.
- 때문에 직접 해당 타입을 선언해야 한다.
- EUC-KR 타입으로 인코딩 하기 위해 `CFStringConvertEncodingToNSStringEncoding(_:)` 메서드를 사용한다.
- 파라미터로 들어온 CoreFoundation encoding 상수와 가장 가까운 Cocoa encoding 상수를 반환하는 메서드이다.
```swift!
// 적용 코드!
```
#### 고민 3) 리다이렉션
- 로또 qr로 받은 url은 요청을 보내기위한 url이고 이 url로 진입한다면 javascript코드를 통해 다른 url을 갖게 됨.
- 그래서 리다이렉션을 한번 거친 페이지의 주소 url로 변경한 뒤 URLSession에 작업을 요청
```swift
let redirectedUrl = url.replacingOccurrences(of: "/?", with: "/qr.do?&method=winQr&")
guard let url = URL(string: redirectedUrl) else {
return Fail(error: LottoQRUseCaseError.invalidURL).eraseToAnyPublisher()
}
return URLSession.shared
.dataTaskPublisher(for: url)
.tryMap { element -> Data in
guard let httpResponse = element.response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw LottoQRUseCaseError.invalidResponse
}
return element.data
}
.flatMap { data -> AnyPublisher<String, Error> in
do {
let encodingEUCKR = CFStringConvertEncodingToNSStringEncoding(0x0422)
if let html = String(data: data, encoding: String.Encoding(rawValue: encodingEUCKR)) {
let doc: Document = try SwiftSoup.parse(data.description)
let empElements: Elements = try doc.select(".winner_number").select(".tit")
// 원하는 작업을 수행한 후 결과를 반환
let result = try empElements.text()
}
```