(후)Step2
@lina0322
엘림 안녕하세요!!
리뷰는 못 받아도 어제 이미 (후)스텝2랑 질문을 마무리해두어서 PR을 보냅니다 😁
안 보내기 살짝 아깝기도 하고 ㅋㅋ 나중에 저희가 다시 보면 좋을 것 같아 기록도 할 겸 올리기만 할게요!!
기한에 맞추는것을 최우선으로 하다보니 시간이 엄청 빨리 지나간 프로젝트였던것 같아요~ 세심하고 핵심적인 부분에 대한 리뷰를 너무 잘해주셔서 더 깊이 공부하는 시간이 되었던것 같아요.
감사합니다!! 🙇♂️🙇🏻
### 1. view의 배경 이미지 접근 방법
- 현재 뷰컨트롤러의 배경 이미지는 view의 subview인 UIImageView로 구현되어 있습니다.
- 기기의 orientation이 바뀔 때마다 UIImageView에 접근해 매번 너비와 높이를 새로 지정해주고 있어 이미지뷰에 접근할 방법이 필요했고 다음 두 가지 방법을 비교해보았고 최종적으로는 1-1번으로 구현했습니다.
#### 1-1. view의 subviews[0]으로 접근하는 방법
- 이미지뷰는 index 0에 insert 해두었습니다. 따라서 이미지뷰에 접근할 때마다 뷰컨트롤러 view의 subviews[0]로 접근하는 방법을 생각해보았습니다.
- 장점은 뷰컨트롤러가 프로퍼티로 지니고 있을 필요가 없다는 점입니다.
- 하지만 만약 index 0에 subView를 추가하게 되면 더이상 subviews[0]으로 접근이 불가능해진다는 단점이 있어 유지보수에 불안정하다고 생각했습니다.
- 그럼에도 1-1번 방법을 택한 이유는 뷰의 0번 인덱스에 subview가 달리 추가될 일이 없다고 생각했기 때문입니다.
```swift
override func viewDidLoad() {
super.viewDidLoad()
let backgroundImage = UIImageView(frame: UIScreen.main.bounds)
backgroundImage.image = UIImage(named: "dark")!
backgroundImage.contentMode = UIView.ContentMode.scaleAspectFill
self.view.insertSubview(backgroundImage, at: 0)
setupCollectionView()
initData()
configureRefreshControl()
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
view.subviews[0].frame.size = CGSize(width: size.width, height: size.height)
}
```
#### 1-2. 프로퍼티로 가지고 있는 방법
- 이미지뷰를 프로퍼티로 들고있다면 위에서 발생한 문제로부터 안전해서 좋다고 생각했습니다. 하지만 프로퍼티로 지니고 있어야하는 단점이 있다고 생각했습니다.
- 아래 예시에서는 backgroundImage 프로퍼티로 들고있습니다.
```swift
let backgroundImage = UIImageView(frame: UIScreen.main.bounds)
override func viewDidLoad() {
super.viewDidLoad()
backgroundImage.image = UIImage(named: "dark")!
backgroundImage.contentMode = UIView.ContentMode.scaleAspectFill
self.view.insertSubview(backgroundImage, at: 0)
setupCollectionView()
initData()
configureRefreshControl()
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
backgroundImage.frame.size = CGSize(width: size.width, height: size.height)
}
```
### 2. 이미지의 크기가 44보다 작으면 오토레이아웃 에러가 발생하는 문제가 있었습니다.
* 오류내용
이미지에 height를 40보다 낮거나 같게 제약을 주면 다음과 같은 오류가 발생하였습니다.
당연히 cell은 self-Sizing을 가지고 있기에 내부 컨텐츠들의 사이즈와 같게 설정이 될거라고 생각했습니다.
하지만 UIImageView를 넣을경우 높이를 44로 고정해야된다는 오류가 왜 나오는지 알 수 없었습니다. 😵💫😭
구글링을 통해 찾아본 내용중 가장 이해가 되는것은 높이가 default로 44가 고정되어 있다고 나온내용이였는데요.이 내용을 보고 UIImageView를 제거하고 Label만 넣어서 동작시켜보았지만 따로 오류가 나온 부분이 없었습니다.
- 원인 모르겠지만 일단 저희가 떠올린 해결 방법은 다음과 같습니다.
- 이미지 크기를 44로 지정한다.
- 아니면 이미지 높이의 priority를 높게 지정한다.
```swift
"<NSLayoutConstraint:0x600001852800 UIImageView:0x7fcccfd26f80.width == UIImageView:0x7fcccfd26f80.height (active)>",
"<NSLayoutConstraint:0x600001852850 UIImageView:0x7fcccfd26f80.width == 40 (active)>",
"<NSLayoutConstraint:0x600001852990 V:|-(0)-[UIStackView:0x7fcccfd24f70] (active, names: '|':UIView:0x7fcccfd27420 )>",
"<NSLayoutConstraint:0x6000018529e0 UIStackView:0x7fcccfd24f70.bottom == UIView:0x7fcccfd27420.bottom (active)>",
"<NSLayoutConstraint:0x600001853520 'UISV-alignment' UILabel:0x7fcccfd28290.bottom == UIImageView:0x7fcccfd26f80.bottom (active)>",
"<NSLayoutConstraint:0x6000018535c0 'UISV-alignment' UILabel:0x7fcccfd28290.top == UIImageView:0x7fcccfd26f80.top (active)>",
"<NSLayoutConstraint:0x6000018533e0 'UISV-canvas-connection' UIStackView:0x7fcccfd24f70.top == UILabel:0x7fcccfd28290.top (active)>",
"<NSLayoutConstraint:0x600001853430 'UISV-canvas-connection' V:[UILabel:0x7fcccfd28290]-(0)-| (active, names: '|':UIStackView:0x7fcccfd24f70 )>",
"<NSLayoutConstraint:0x600001852d50 'UIView-Encapsulated-Layout-Height' UIView:0x7fcccfd27420.height == 44 (active)>"
)
Will attempt to recover by breaking constraint
<NSLayoutConstraint:0x600001852800 UIImageView:0x7fcccfd26f80.width == UIImageView:0x7fcccfd26f80.height (active)>
```
### 3. LocationManager
- 저희 로직에서 Location을 가져오는 내용을 다시 한번 공부해봤습니다. 하지만 공부를 하고 다시 코드를 봐도 테스트 방법에 대해서 감이 잘 오지 않았고 LocationManager를 테스트해보다가 로직이 이상한것 같아서 리팩토링만 진행한 상황입니다.🤯😭
#### 3-1. requestLocation()과 startUpdatingLocation()을 호출하지 않아도 location이 받아지는 문제
- 두 메서드 중 하나만 호출하기도 하고 둘 다 호출하지 않기도 해봤는데 예상과 다르게 매번 현재 위치를 받아왔습니다.
- 이전에 받은 위치가 저장되어 있어 그대로 받아오는 것은 아닌가 싶어 새로운 기기에서도 실행해보고 클린빌드를 해보기도 했으나 여전히 현재 위치를 가져오고 있었습니다.
- 이 부분을 어떻게 더 테스트할 수 있는지 궁금했습니다.
#### 3-2. requestLocation() vs startUpdatingLocation()
startUpdating의 경우 내부 로직에 의하여(거리)에 대한 조건이 충족되면 자동으로 위치정보를 수집하고, requsetLocation의 경우 한번 호출되고 종료되어 즉각적인 위치에 변동이 중요하지 않을떄 사용할수 있을것 같다는 생각이 들었습니다.
기기의 위치가 이동되었을 때 새로운 위치의 날씨를 받아오기 위해 startLocation()을 적용하는게 맞다고 생각했습니다. 하지만 날씨 앱의 경우 내비게이션 등과는 달리 날씨를 한 번 확인하고 앱을 종료하는 경우가 많다고 생각했습니다. 따라서 앱을 실행했을 때 한번만 위치를 받아오는 requestLocation()만으로 충분하다는 생각도 들었는데 혹시 저희가 놓친게 있는건지 아니면 더 좋은 방향이 있는건지 궁금했습니다.
- manager.requestLocation()
- 내부적으로 startUpdatingLocation을 호출하고, delegate에게 위치를 전달하며 stopUpdatingLocation을 호출할수 있는 가장 편리한 방법이라고 확인했습니다.
- manager.startUpdatingLocation()
- 검색속도가 중요하며 위치 데이터 에대한 필터 및 검색이 필요한경우 사용, 내부 로직(거리)에 의해 특정 조건이 되면 자동으로 위치 수집하는것으로 확인했습니다.
#### 3-3. 기기를 일정거리 이동시키면 뷰를 업데이트
- LocationManager를 파악하고 테스트를 진행하기 위해서는 기기가 일정 거리 이동을 했을 때 어떤 결과가 나타나는지 확인하는 작업이 꼭 필요하다고 생각했습니다.
- 시뮬레이터에서 일정 거리 이동하는 방법은 시뮬레이터의 Features > Location > Custom Location에서 위치를 조정해주면 된다고 생각했습니다. (코드적으로 하는 방법은 찾지 못 했습니다.)
- 근데 이 경우 새로고침하지 않으면 뷰에 반영되지 않았기에 CLLocationManager의 `locationManager(_:didUpdateLocations:)`에서 뷰컨트롤러에게 Notification을 보내 매번 뷰를 업데이트 하도록 시도해보았습니다.
- 하지만 Notification을 추가하니 이상하게도 처음부터 끝까지 뷰가 나타나지 않았습니다. 🤯
- desiredAccuracy를 kCLLocationAccuracyHundredMeters로 지정했더니 헤더뷰만 일부 나타나고 말았습니다.
- 해당 접근법의 오작동은 해결하지 못해 로직에는 따로 추가하지 않았습니다.
- LocationManager의 didUpdateLocations에서 노티피케이션 발송
```swift
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else { return }
let newLocation = Coordinates(longitude: location.coordinate.longitude, latitude: location.coordinate.latitude)
NotificationCenter.default.post(name: .didUpdateLocationNotification, object: nil, userInfo: ["newLocation": newLocation])
return
}
```
- 뷰컨트롤러에서 노티피케이션이 오면 뷰 업데이트
```swift
override func viewDidLoad() {
NotificationCenter.default.addObserver(self,
selector: #selector(updateLocation),
name: .didUpdateLocationNotification, object: nil)
}
```
### 4. 날씨/시간 지역화
- 명세서의 예시 이미지에서는 한국 날씨가 특정한 포맷으로 나와있는데 이번 스텝에서 한국식과 미국식으로 표현될 수 있도록 지역화를 진행했습니다.
- 하지만 DateFormatter의 기본 기능으로는 명세서의 날짜시간 포맷과 너무 동떨어진 형식이 되어 고정된 형식을 지정하기로 했습니다.
- 확실히 고정된 형식을 지정하니 명세서와 비슷하게 나타낼 수 있었지만 아쉽게도 언어에 따라 의도하지 않은대로 표현되는 경우가 있었습니다.
- 예를 들어 미국식으로는 3AM 이라고 표기하고 한국식으로는 3시라고 표기하고 싶은데 미국식으로 3AM이 나오게 하려면 한국식은 오전 3시가 되어야하고 한국식으로 3시로 나오게 하려면 미국식은 그냥 3이 나오게 됐습니다.
- 또 다른 방법으로는 Locale별로 분기처리 하는 것을 생각해보았는데, 과연 이 방법이 맞는지 의문이 들어 다르게 해결하고 싶었습니다.
- 그래서 포맷을 고정하는 방식인 dateFormat과 setLocalizedDateFormatFromTemplate()를 비교 후 setLocalizedDateFormatFromTemplate()로 구현했습니다. 하지만 두 고정포맷 방식 모두 명세서와 날짜 형식이 조금 달라지는 문제는 어쩔 수 없이 발생했습니다. 그래도 DateFormatter의 기본 기능보다는 명세서와 더 비슷해서 이렇게 구현하기로 결정했습니다.
- 저희가 생각한 방법 외에 또 다른 방법이 있는지는 아직 찾아보지 못했습니다.