# 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() } ```