# 쥬스 메이커 [STEP 1] 써니쿠키, SummerCat
## 1. 프로젝트 구성
### `FruitStore`
- `FruitStore`는 `enum` 타입의 `Fruit`를 통해 취급하는 과일을 명시합니다.
- 각 과일의 재고는 `[Fruit: Int]` 형태의 딕셔너리 `fruitStock` 프로퍼티에서 관리합니다.
```swift=
private var fruitStock: Dictionary<Fruit, Int> = [
.strawberry : 10,
.banana : 10,
.pineapple : 10,
.kiwi : 10,
.mango : 10,
]
```
- `fruitStock` 딕셔너리 작성 시 마지막 Element 뒤에 `,`를 붙인 이유
- [구글의 Swift 컨벤션](https://google.github.io/swift/#braces)
- array와 dictionary의 각 element가 한 줄에 하나씩 작성되었을 경우, 뒤에 꼭 `,`를 붙여야 한다. 이렇게 해야 나중에 항목이 추가되었을 때 더 명확하게 차이를 알 수 있다.

- 접근제어자를 `private`으로 설정한 이유: 과일가게의 과일의 재고 값을 외부에서 마음대로 바꾸지 못하도록 해야 한다고 생각해서 `private`을 사용해 `FruitStore` 내에서만 접근이 가능하도록 설정했습니다.
- `changeStockOf` 함수
- `FruitStore`가 가지고 있는 과일의 재고의 수량을 변경하는 함수입니다.
```swift
func changeStockOf(fruit: Fruit, by quantity: Int) {
guard let currentStock = fruitStock[fruit] else {
return
}
fruitStock[fruit] = currentStock + quantity
}
```
- `guard`문을 사용해 현재의 재고 수량을 옵셔널 바인딩 해서 `currentStock`에 넣은 후, 조정한 수량만큼 `fruitStock[fruit]`에 넣어줍니다.
- `fruitStock[fruit] = currentStock = quantity`에서 옵셔널 바인딩이 필요하지 않은 이유는, `fuitStock[fruit]`의 값이 존재하지 않더라도, `currentStock + quantity`의 값을 새로 할당해주기 때문입니다.
- `checkStockOf` 함수
- `JuiceMaker`가 요청한 과일의 수량을 확인하는 함수입니다.
- 요청이 들어온 과일이 존재하지 않을 경우, `noSuchFruit` 에러를 반환합니다.
- 요청이 들어온 과일의 수량이 부족할 경우, `stockShortage` 에러를 반환합니다.
### `JuiceMaker`
- `JuiceMaker`는 `FruitStore`를 소유하기 위해 `fruitStore`라는 `FruitStore`의 인스턴스를 갖습니다.
- `JuiceMaker`는 `enum` 타입의 `Juice`를 통해 취급하는 쥬스를 명시합니다. `enum` 타입 안에 `juiceName` 프로퍼티를 만들어 각 쥬스의 한글이름을 반환할 수 있도록 하고, `recipe` 프로퍼티에 사용하는 과일과 갯수를 딕셔너리로 정리해 주었습니다.
- 딕셔너리를 사용해서 `recipe`를 작성한 이유: **고민한 부분**에 작성
- `makeJuice` 메서드는 쥬스 제작이 가능한지 확인하기 위해 `checkFruitStore` 메서드를 통해 레시피에 맞는 과일갯수를 체크하고, 사용한 과일만큼 `useFruitStore` 메서드를 통해 `fruitStore`의 과일 재고를 조정합니다.
- 이 과정중 do - try - catch 구문을 사용하여 error를 처리합니다.
- `JuiceMakerError`로 다운캐스팅 된 `error` 상수를 catch에서 정의해, `errorDescription`으로 에러 메시지를 불러옵니다.
- error가 catch 될 시에 음료제작을 중단하기 위해 return처리 됩니다.
- `checkFruitStore` 메서드는 `recipe`에서 필요한 과일의 개수와 `fruitStore`의 재고를 비교하기 위해 `FruitStore`의 `checkStockOf` 메서드를 이용합니다. `checkStockOf`에 과일의 종류와, 해당 과일의 필요한 총량을 전달합니다. 딕셔너리의 key값을 이용하기 위해 for - in 구문을 이용했습니다.
- `JuiceMaker`의 `changeStockOf`에서 던진 오류를 `checkFruitStore`에서 처리할 수도 있지만, 그렇게 작성할 경우 함수의 depth가 깊어져 가독성이 떨어진다고 판단해 오류를 한번 더 던져서 `makeJuice`에서 오류를 처리하도록 했습니다.
- `useFruit` 메서드는 `fruitStore`의 과일의 재고를 조정합니다.
### `JuiceMakerError`
- `JuiceMakerError`에서는 `Error` 프로토콜을 채택한 `enum`타입을 이용해 에러를 case별로 정리했습니다.
- `extension`을 이용해 do-catch구문에서 에러메세지로 이용할 구문을 `errorDescription` 프로퍼티에 정리했습니다. 사용시에는 as로 다운캐스팅하여 `errorDescription`을 이용해 에러메세지를 출력할 수 있도록 하였습니다.
- `LocalizedError` 프로토콜에 이미 `errorDescription`이 구현되어 있지만 `LocalizedError`를 채택하지 않은 이유는, `LocalizedError`에 구현되어 있는 `errorDescription`의 반환값이 `String?`이기 때문입니다.
- 정의된 유형의 에러가 발생했을 때에만 `errorDescription`을 내보내는 방향으로 설계했기 때문에 옵셔널 타입으로 반환할 필요가 없다고 생각해, `String`을 반환하는 `errorDescription`을 `extension`에 새로 정의해서 사용했습니다.
---
## 2. 실행 예시
| 코드 | 출력 |
|------|---|
|<img width="350" src="https://i.imgur.com/DuDhrbk.png"> |<img width="350" src="https://i.imgur.com/82VzY4r.png">|
- `딸바쥬스`를 만들고 난 후`딸기`의 재고가 0이기 때문에, 이후에 `딸바쥬스`를 만들어달라고 요청한 경우 쥬스를 만들지 않고 `재고 부족` 메시지를 출력합니다.
- 이 때, 두 과일(`딸기`, `바나나`) 중 하나라도 재고가 부족한 경우 쥬스를 만들지 않고 두 과일의 재고 모두 조정되지 않습니다.
---
### `enum Juice` 타입에서 사용하는 과일의 종류, 갯수 연결하기
열거형으로 정리한 쥬스타입의 case마다 레시피로 사용되는 각 과일의 갯수를 연관지어 지정해놓고 싶었습니다. 예를 들어, 딸기바나나쥬스는 딸기 10개 바나나 1개를 사용하기 때문에 `case 딸기바나나쥬스` 에는 딸기 10개, 바나나 1개를 연결하고 싶었습니다.
- **시도1 : 연관값(Associated Values) 사용**
```swift
case strawberryBananaJuice(strawberry: 10, banana: 1)
```
- 연관값을 사용할 경우, 만들 쥬스를 고르기 위해 case를 불러올 때 연관값을 반드시 초기화(할당)해야 해서 최초에 할당한 연관값이 사용되지 않는 문제가 있었습니다.
- **시도2 : 튜플을 리턴해주는 메서드, 프로퍼티 사용**
```swift
enum Juice {
case bananaJuice
case strawberryBananaJuice
var ingredient: (first: FruitStore.Fruit, second: FruitStore.Fruit?) {
switch self {
case .bananaJuice:
return (.banana, nil)
case .strawberryBananaJuice:
return (.strawberry, .banana)
}
}
func ingredientsCount() -> (first: Int, second: Int) {
switch self {
case .bananaJuice:
return (2,0)
case .strawberryBananaJuice:
return (10, 1)
}
}
//사용 시
let firstfruitType = Juice.ingredient.first
let firstfruitCount = Juice.ingredients.first
let secondfruitType = Juice.ingredient.second //옵셔널바인딩 포함
let secondruitCount = Juice.ingredients.socond
```
- 과일타입과 갯수를 튜플로 묶어 리턴하는 방법을 시도했지만, 과일이름과 갯수를 서로 다른 프로퍼티와 메서드에서 따로 리턴해주기 때문에 코드를 읽는 사람입장에서 어떤 과일이 몇개가 쓰이는지 연결짓기가 어려울 것 같다고 생각했습니다.
- 한개의 튜플에 라벨을 달아 과일명과 갯수를 같이 넣어주는 방법도 있지만 튜플은 그 크기가 정해져있기에 과일을 한 개만 사용하는 쥬스와 두 개를 사용하는 쥬스를 프로퍼티(혹은 메서드)한 곳에 같이 담기가 어려워 사용하지 않았습니다.
- **시도 3. 딕셔너리 타입을 리턴해주는 프로퍼티 사용**
```swift
var recipe: Dictionary<FruitStore.Fruit, Int> {
switch self {
case .strawberryJuice:
return [.strawberry: 16]
case .strawberryBananaJuice:
return [.strawberry: 10, .banana: 1]
}
}
```
- `recipe` 프로퍼티에 과일 종류와 개수를 딕셔너리로 반환시켜주는 컨셉을 채택했습니다. 과일 종류와 개수를 명확히 보여줄 수 있고, 그 크기도 정해져있지 않고 순서도 중요하지 않기 때문에 딕셔너리를 사용하는게 베스트라고 생각했습니다.
---
## 4. 해결 안 된 부분
### 컬렉션 타입 결정
프로젝트를 처음 clone했을 때, `JuiceMaker`는 구조체로 `FruitStore`는 클래스로 선언되어 있었습니다. 둘이 왜 다른 컬렉션 타입으로 선언되어야 하는지 이유를 찾지 못했습니다.
---
# 쥬스 메이커 [STEP 2] 써니쿠키, SummerCat
## 1. 프로젝트 구성
사용자가 쥬스를 주문하는 버튼을 누르면, `ViewController`의 `orderJuice` 메서드에서 `makeJuice`를 호출합니다. `makeJuice`가 반환한 결과값이 `.success`일 경우 성공 alert을 화면에 표시하고, `.failure`일 경우 오류 메시지를 출력합니다.
- 이 과정에서 기존에 `JuiceMaker`에 구현한 `makeJuice`의 반환 타입을 `Void`에서 `Result<String, Error>`로 변경하고, `JuiceMakerError`의 `errorDescription`을 제조 실패 시 alert에 출력할 메시지로 변경하였습니다. `Result<String, Error>`로 변경한 이유는 `.success`에서 전달한 문자열을 그대로 성공 alert의 `message`로 전달해 사용하고, `.falure`일 경우 전달받은 `error`의 `errorDescription`을 실패 alert의 `message`로 전달해 사용하기 위해서 입니다.
```swift
func makeJuice(_ juice: Juice, total: Int) -> Result<String, Error> {
do {
try checkFruitStore(for: juice, total: total)
useFruit(juice, total: total)
return .success("\(juice.juiceName) 나왔습니다! 맛있게 드세요!")
} catch let error as JuiceMakerError {
return .failure(error)
} catch {
return .failure(error)
}
}
```
```swift
extension JuiceMakerError {
var errorDescription: String {
switch self {
case .stockShortage:
return "재료가 모자라요. 재고를 수정할까요?"
}
}
}
```
쥬스를 성공적으로 제조한 경우, `viewDidLoad()` 메서드를 호출해 화면에 보이는 과일의 잔여 재고 수량을 갱신합니다. 쥬스 제조에 실패한 경우에는 갱신하지 않습니다.
```swift=
switch result {
case .success(let message):
showSuccessAlert(message: message)
viewDidLoad()
case .failure(let error):
guard let juiceMakerError = error as? JuiceMakerError else {
return showFailureAlert(message: error.localizedDescription)
}
showFailureAlert(message: juiceMakerError.errorDescription)
}
```
---
## 2. 실행 예시
<img width = 500, src = "https://i.imgur.com/Dn6hyiB.gif" >
</br>
</br>
사용자가 쥬스 주문 버튼을 탭 하면
- **쥬스 레시피별 과일 재고 있을 때** :
제작 완료 alert을 띄운 후 사용량 만큼 과일의 재고 감소를 반영합니다.
- **쥬스 레시피별 과일 재고 부족할 때** :
재료 부족 alert을 띄운 후 재고수정을 원하는지 물어봅니다. `아니오` 버튼을 탭하면 alert이 닫히고, `예` 버튼을 탭하면 재고수정 뷰로 이동합니다.
<img width = 500, src = "https://i.imgur.com/yihXfid.gif" >
</br>
</br>
사용자가 재고수정 버튼을 탭하면 재고수정 뷰로 이동합니다.
---
## 3. 고민한 부분
### 3-1 `재고수정` 버튼을 통해 `재고 추가` 화면으로 이동할 때, 내비게이션 방식과 모달 방식 중 어느 것을 쓸 것인가?
`내비게이션 방식`은 현재 화면의 연장선상으로 세부사항을 열어보거나 다음 step으로 넘어갈 때 사용합니다.
`모달 방식`은 지금 화면에서 조금 벗어나 잠시 다른 일을 처리하거나, 연장선을 벗어나 새로운 갈래로 나갈 때 사용합니다.
쥬스를 주문하는 현재 화면에서 재고를 변경해주는 화면으로 이동할 때는 쥬스 주문과는 별개로 잠시 연장선상을 벗어나 다른 일을 처리한 후 돌아온다고 생각하여 화면 전환 방식으로 모달 방식을 채택했습니다.
### 3-2 과일 재고의 개수를 메인 `ViewController`와 `FruitStockViewController`간에 공유하게 하는 방법
검색을 통해 프로퍼티로 직접 전달하는 방법, delegation을 이용하는 방법 등 다양한 방법이 있다는 것을 알게 되었지만, 활동학습 시간에 배운 Singleton Pattern을 이용하기로 했습니다.
그 이유는 이 프로젝트가 매우 단순한 구조의 프로젝트이기 때문이기도 하고, 현재 구조체로 정의되어 있는 `JuiceMaker`가 프로퍼티로 클래스 `FruitStore`의 인스턴스를 `private`으로 가지고 있었기 때문에, 클래스로 되어있는 `FruitStore`에 싱글톤 패턴을 적용하는 편이 낫다고 생각했습니다.
`FruitStore`에 싱글톤 패턴을 적용할 경우, `JuiceMaker`, `ViewController`, `FruitStockViewController`에서 모두 하나의 인스턴스에 접근이 가능하기 때문입니다.
(현재는 step2 요구사항에 `FruitStockViewController`에 재고 수량을 반영하는 부분이 기재되어 있지 않아 그 부분은 구현하지 않았습니다.)
### 3-3 `UILabel`과 `UIButton`을 다룰 때, 코드에서 중복되는 부분을 줄일 방법
처음에는 각 `label`과 `button`을 일일이 직접 매핑하거나 `switch case`로 구분하도록 작성했었습
니다. 하지만 두 가지 경우 모두 각 케이스마다 중복되는 코드가 많아 중복되는 코드를 줄일 방법을 고민(구글링)해 보았고, 아래의 글을 참고해 개선해 보았습니다.
(참고: https://stackoverflow.com/questions/52548196/shorten-long-switch-case)
`label`의 경우, `.text = String(FruitStore.sharedFruitStore.fetchStockOf(.strawberry))`부분이 `.strawberry`라는 과일 값만 빼고는 모두 똑같이 중복되고 있습니다.
```swift
// 최초에 작성한 코드 - label을 일일이 매핑
strawberryStockLabel.text = String(FruitStore.sharedFruitStore.fetchStockOf(.strawberry))
bananaStockLabel.text = String(FruitStore.sharedFruitStore.fetchStockOf(.banana))
pineappleStockLabel.text = String(FruitStore.sharedFruitStore.fetchStockOf(.pineapple))
kiwiStockLabel.text = String(FruitStore.sharedFruitStore.fetchStockOf(.kiwi))
mangoStockLabel.text = String(FruitStore.sharedFruitStore.fetchStockOf(.mango))
```
아래와 같이 `[UILabel: Fruit]` 딕셔너리를 생성하고, `for-in`을 사용해 딕셔너리를 순회하면서 값을 `label`에 넣어주도록 수정해 보았습니다.
```swift
// 최종 수정한 코드
let fruitLabel: [UILabel: Fruit] = [
strawberryStockLabel: .strawberry,
bananaStockLabel: .banana,
pineappleStockLabel: .pineapple,
kiwiStockLabel: .kiwi,
mangoStockLabel: .mango,
]
for (label, fruit) in fruitLabel {
label.text = String(FruitStore.sharedFruitStore.fetchStockOf(fruit))
}
```
`button`을 식별해서 `switch case`를 이용해 `juiceMaker.makeJuice(Juice, total: Int)`에 넣어주는 부분 역시 `makeJuice` 메서드를 호출해서 `result`에 넣어주는 부분이 반복되고 있었습니다.
```swift
// 최초에 작성한 코드 - button을 switch case로 구분
@IBAction func orderJuice(sender: UIButton) {
switch sender {
case strawberryBananaJuiceButton:
result = juiceMaker.makeJuice(.strawberryBananaJuice, total: 1)
case mangoKiwiJuiceButton:
result = juiceMaker.makeJuice(.mangoKiwiJuice, total: 1)
case strawberryJuiceButton:
result = juiceMaker.makeJuice(.strawberryJuice, total: 1)
case bananaJuiceButton:
result = juiceMaker.makeJuice(.bananaJuice, total: 1)
case pineappleJuiceButton:
result = juiceMaker.makeJuice(.pineappleJuice, total: 1)
case kiwiJuiceButton:
result = juiceMaker.makeJuice(.kiwiJuice, total: 1)
case mangoJuiceButton:
result = juiceMaker.makeJuice(.mangoJuice, total: 1)
default:
return
}
...
}
```
이 부분도 아래와 같이 `[UIButton: Juice]` 딕셔너리를 생성하고, `juiceButton[sender]`의 값을 `juice`에 할당해 옵셔널 바인딩 해주니 `result`를 생성하는 `makeJuice()` 메서드를 한 번만 호출하도록 개선할 수 있었습니다.
```swift
@IBAction func orderJuice(sender: UIButton) {
let juiceButton: [UIButton: Juice] = [
strawberryBananaJuiceButton: .strawberryBananaJuice,
mangoKiwiJuiceButton: .mangoKiwiJuice,
strawberryJuiceButton: .strawberryJuice,
bananaJuiceButton: .bananaJuice,
pineappleJuiceButton: .pineappleJuice,
kiwiJuiceButton: .kiwiJuice,
mangoJuiceButton: .mangoJuice,
]
guard let juice = juiceButton[sender] else { return }
let result = juiceMaker.makeJuice(juice, total: 1)
...
}
```
---
## 4. 궁금한 점
### 4-1 ViewController의 함수에 접근제어를 설정
ViewController에 구현한 함수에도 접근제어를 설정해야 하는지 고민했습니다.
ViewController 파일에서 `showSuccessAlert(message:)`, `showFailureAlert(message:)`, `moveToFruitStockVC()` 와 같은 함수들과 화면과 연동되어있는 `@IBAction 함수`, `@IBOulet 프로퍼티` 도 `private`으로 처리를 해줘야 하는지 궁금합니다.
`private`으로 설정해도 시뮬레이션은 정상 작동됨을 확인했는데, 뷰 컨트롤러 내부의 요소들에 접근 제어를 설정하는 것이 불필요한 사항인지 필수사항인지 개념이 잡히지 않아 현재는 접근 제어를 설정하지 않고 PR드렸습니다..
### 4-2 화면 전환 방식 (함수가 구현되어있을 때 segue를 이용하는지)
화면을 전환하는 함수가 구현되어 있어도 segue를 이용하시는지 궁금합니다.
이번 step2에서는 모달 view로 넘어가는 경로가 두 개였습니다. 첫 번째는 `재고수정` 버튼을 눌렀을때이고 두 번째는 쥬스제조시 재고가 부족하다는 alert에서 `예`를 눌렀을 때입니다.
`예`를 눌렀을 경우를 위해 화면전환 함수를 코드로 구현했는데, 이럴 때 `재고수정` 버튼에도 같은 함수를 이용해 통일을 하는게 나을지 segue 방식을 사용해도 되는지 현업에서는 어떻게 하는지 궁금합니다.
---
# 쥬스 메이커 [STEP 3] 써니쿠키, SummerCat
## 1. 프로젝트 구성
### `FruitStockVC (재고 수정 화면) `
`navigation bar button` `닫기`를 통해 모달 방식으로 띄운 재고수정 View를 `dismiss`해 이전 화면으로 돌아갈 수 있습니다.
화면이 켜졌을 때 과일의 현재 재고를 각각 Label로 표기합니다.
- 창이 띄워질 때 호출되는 `viewDidLoad` 메서드 내부에서`updateFruitStockLabel`메서드를 호출합니다.
- `updateFruitStockLabel` 메서드는 과일과 과일 label로 짝지어 선언된 딕셔너리 `fruitLabel`을 이용해 labeling합니다.
stepper를 이용해 유저가 과일의 재고를 변경 할 수 있습니다.
- 처음에 stepper의 value를 셋팅 해주기 위해 창이 띄워질 때 호출되는 `viewDidLoad` 메서드에서`assignStepperValue` 메서드를 호출합니다.
- `assignStepperValue` 메서드는 과일과 `해당 과일의 stepper`로 짝지어 선언된 `fruitStepper` 를 이용해 각 stepper의 value값을 할당합니다.
- 모든 과일의 stepper가 터치될 때 작동하는 `changeFruitStock` 메서드를 이용해 과일 재고 label과 및 실제 `sharedFruitStore`의 과일 재고를 변경합니다.
</br>
### `VC (쥬스 주문 화면)`
`FruitStockVC`에서 과일 재고를 변경한 후, `VC`의 과일 재고 label을 업데이트 하기 위해 `viewWillAppear` 메서드 내부에서 `updateFruitStockLabel` 메서드를 호출합니다.
`FruitStockVC` 모달이 dismiss될 때 `VC`에서 `viewWillAppear` 메서드를 호출할 수 있도록 modal스타일을 `fullScreen`으로 지정합니다.
- `fullScreen`으로 명시적으로 설정하지 않은 경우, modal로 present하면 기존 화면의 일부만을 가리고 새로운 창이 나타나는 형식이기 때문에, 새로운 창을 dismiss해도`viewWillAppear`하지 않기 때문입니다 (새로운 창을 띄웠을 때 기존의 view가 disappear하지 않았기 때문).
---
## 2. 실행 예시
- iphone SE 에서 실행 예시
<img width = 500, src = "https://i.imgur.com/ImLktxf.gif">
- iphone 13 Pro MaX에서 실행 예시
<img width = 500, src = "https://i.imgur.com/1qlXlIb.gif">
---
## 3. 고민한 부분
### 네비게이션 bar -> Navigation controller로 이동
#### **[방법1]** `VC -> FruitStockVC`로 직접 화면 전환
`FruitStockVC` 내비게이션 바가 보이지 않아 새로운 내비게이션 바를 화면에 추가해 보았습니다.
- 이럴 경우, 한 VC에 2개의 내비게이션 바가 존재하는 다소 이상한 상황이라고 생각해 다른 방법을 찾아보기로 했습니다.
- 스토리보드상에는 보였던 `FruitStockVC`의 내비게이션 바가 실행 화면에서 보이지 않았던 이유가, `FruitStockVC`가 어떤 내비게이션 스택 안에도 담기지 않았기 때문이라고 생각했습니다.
#### **[방법2]** `VC -> NavigationController -> FruitStockVC`로 이동
`VC`에서 `FruitStockVC`로 직접 이동하지 않고, `FruitStockVC`를 담고 있는 내비게이션 컨트롤러로 이동해 그 내비게이션 컨트롤러의 루트 뷰 컨트롤러인 `FruitStockVC`를 띄우도록 하면 `FruitStockVC`는 내비게이션 스택 안에 존재하게 되므로 내비게이션 바가 화면에 보일 것이라고 생각했습니다.
내비게이션 바를 새로 추가하지 않고도 스토리보드 상에 보이는 내비게이션 바가 실행 화면에서도 정상적으로 출력되어 title을 작성할 수 있었고 navigationBarButton을 이용해 '닫기'버튼을 구현할 수 있었습니다.
### stepper `-` 버튼이 작동하지 않는 문제
Stepper를 이용해서 재고를 수정하기 위해 처음에 작성했던 코드는 아래와 같습니다.
```swift
@IBAction func changeFruitStock(_ sender: UIStepper) {
let fruitStepper: [UIStepper: Fruit] = [
strawberryStepper: .strawberry,
bananaStepper: .banana,
pineappleStepper: .pineapple,
kiwiStepper: .kiwi,
mangoStepper: .mango
]
sender.minimumValue = -1
guard let fruit = fruitStepper[sender],
let fruitLabel = fruitLabel[fruit] else { return }
FruitStore.sharedFruitStore.changeStockOf(fruit: fruit, by: Int(sender.value))
fruitLabel.text = String(FruitStore.sharedFruitStore.fetchStockOf(fruit))
sender.value = 0
}
```
- `-` 값을 전달할 수 있도록 stepper가 전달하는 값의 최소값을 `-1`로 설정
- stepper를 누를 시, stepper는 값을 기억하고 있으므로 stepper를 누를 때마다 stepper의 value가 1 > 2 > 3 > 4 > ... 로 증가하게 되어 `changeStockOf`에 전달되는 값은 +1 > +3 > +6 > ... 로 변하게 됩니다.
- 한 번 누를 때마다 `sender.value`를 통해 전달되는 값이 1 또는 -1이 되기를 원해서, 마지막에 `sender.value = 0`으로 stepper가 기억하고 있는 값을 초기화 해 주었습니다.
- 그런데.. 아래와 같이 코드를 작성할 시 재고를 수정하는 모달 창으로 넘어왔을 때 Stepper의 `-` 버튼을 눌러도 아무런 동작을 하지 않는 문제가 발생했습니다.
- `+` 버튼을 한 번이라도 누른 후 `-` 버튼을 누를 시 정상적으로 재고의 개수가 줄어들었지만, `-` 버튼만 누르면 아무 동작을 하지 않았습니다.
- `sender.value`를 출력하는 함수를 작성해보니, 맨 처음에 `-` 버튼을 누를 시 아무것도 출력되지 않는 것을 확인해 작동 자체를 하지 않았다는 것을 알 수 있었습니다.
stepper의 작동 방식에 대해 열심히 구글링 해 보았지만 저희가 원하는 답을 찾을 수 없어, 동료 캠퍼들은 어떻게 구현했는지 질문해서 답을 얻을 수 있었습니다.
```swift
func assignStepperValue() {
for (stepper, fruit) in fruitStepper {
stepper.value = Double(FruitStore.sharedFruitStore.fetchStockOf(fruit))
}
}
@IBAction func changeFruitStock(_ sender: UIStepper) {
guard let fruit = fruitStepper[sender],
let fruitLabel = fruitLabel[fruit] else { return }
let changedValue = Int(sender.value) - FruitStore.sharedFruitStore.fetchStockOf(fruit)
FruitStore.sharedFruitStore.changeStockOf(fruit: fruit, by: changedValue)
fruitLabel.text = String(FruitStore.sharedFruitStore.fetchStockOf(fruit))
}
```
- 각 stepper에 각 과일의 재고를 value로 할당하면, stepper가 설계된 의도대로 `+`를 두 번 누를 시 +2가 되고, `-`를 5번 누르면 -5가 되도록 작동하는 것을 확인할 수 있었습니다.
### autolayout -> stackView 이용
`과일 이미지, 과일 재고 label, stepper`가 담긴 `Stack View`의 `alignment`를 `fill`로 설정할 경우, 기기에 따라 과일 이미지와 과일 재고 label의 크기는 커지지만 stepper의 크기는 커지지 않았고, stepper는 `leading`으로 정렬된 것 처럼 보였습니다.
Stepper는 크기를 변경할 수 없기 때문에, 세 요소의 너비를 일치시키기 위해 과일 재고 label과 과일 이미지의 크기를 stepper의 크기에 맞추는 제약을 설정한 후, `Stack View`의 `alignment`를 `center`로 변경하여 해결했습니다.
---
## 4. 궁금한 점
### 타입 내에서 프로퍼티(변수, 메서드 등)의 배치 순서
타입(뷰 컨트롤러) 내에 프로퍼티가 어떤 순서로 배치되어야 하는지에 대한 고민을 해결하지 못했습니다.
변수/상수가 타입 구현부의 최상단에 모두 위치하는 것이 맞는지, 해당 변수/상수가 사용되는 함수와 가까운 곳에 위치하는 것이 맞는지 잘 모르겠습니다...
메서드의 순서가 호출되는 순서대로 배치되는게 맞는지, 메서드1 안에서 호출되는 메서드2가 메서드 1보다 상단부에 배치되어야 하는게 맞는지 궁금합니다. 만약 개인 취향대로 배치하는 거라면 웨더는 어떤방식으로 배치하고 있는지 궁금합니다.
```swift
// 호출 순서대로 배치한 경우
class ViewController: UIViewController {
func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear()
updateFruitStockLabel()
}
func updateFruitStockLabel() {
for (fruit, label) in fruitLabel {
label.text = String(FruitStore.sharedFruitStore.fetchSTockOf(fruit))
}
}
}
```
```swift
// 메서드2를 메서드1보다 위에 배치
class ViewController: UIViewController {
func viewDidLoad() {
super.viewDidLoad()
}
// 메서드2: 다른 메서드에서 호출되는 메서드
func updateFruitStockLabel() {
for (fruit, label) in fruitLabel {
label.text = String(FruitStore.sharedFruitStore.fetchSTockOf(fruit))
}
}
// 메서드1: 다른 메서드를 호출하는 메서드
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear()
updateFruitStockLabel()
}
}
```
---
## 과일가게 타입은 왜 클래스로 구현하라고 요구했을까요? 두 가지 이상의 이유를 제시해주세요.
- 객체지향을 염두해두고 생각해봤을 때
- 하나 이상의 juice 가게가 존재해서, 같은 FruitStore에서 과일을 사오는 거라고 한다면 FruitStore의 과일재고가 두 가게에 똑같이 공유 될 수 있도록 메모리 참조형태를 띄어야 한다고 생각합니다.
- 두 개의 서로 다른 화면(쥬스 주문 화면, 재고 수정 화면)에서 같은 과일 가게(동일한 인스턴스)의 재고를 보여주기 위해서, Singleton 이나 Delegation등의 패턴을 이용해야 할 가능성이 높기 때문에
- Singleton은 구조체로 구현했을 때에는 원본 인스턴스의 값이 바뀌지 않기 때문에 의미가 없고, Delegation을 구현하기 위해서는 `NSObject`를 상속받는 클래스 타입이어야 하기 때문에 클래스로 구현하라고 요구한 것 같습니다.
- KVO를 이용해야 하는 경우에도 Objective-C와 상호작용이 가능한 class를 사용해야 합니다(`NSObject`를 상속해야 하기 때문)
- Notification Center 를 사용할 때는 FruitStore의 재고에서 재고 수정 화면의 label 또는 stepper의 값을 감시해서 자신의 값을 변경하기 위해 Notification Center를 활용해야합니다.
이 때 `@objc` 함수를 FruitStore 내부에 구현해야 하기 때문에, FruitStore는 클래스 타입이어야 합니다. (`@objc` attribute는 구조체에서 사용 불가능합니다.)
(`@objc`가 붙은 함수를 별도로 구현하지 않고, `addObserver` 메서드에 클로저를 이용해서 원하는 기능을 구현할 경우, 구조체에서 Notification Center를 등록할 수도 있습니다.)
## 쥬스메이커 타입은 왜 구조체로 구현하라고 요구했을까요?
(참고문서)
- [Choosing Between Structures and Classes](https://developer.apple.com/documentation/swift/choosing-between-structures-and-classes)
- [Structures and Classes](https://docs.swift.org/swift-book/LanguageGuide/ClassesAndStructures.html)
<img width = 500, src = "https://i.imgur.com/JKT8BwQ.png" >
1. 공식문서에서 소개하길 "클래스는 지원하는 기능들은 복잡성을 증가시키고, 일반적인 가이드라인으로 추론하기 쉬운 구조체(struct)를 선호하며 클래스에서 지원하는 기능을 사용해야 하거나, 클래스를 사용하는 것이 적절할 때 클래스를 사용한다고 설명합니다.
- Struct 이용이 코드에 대해 더 쉽게 추론할 수 있게 할 수 있다고 소개하는 내용을 풀어 써보겠습니다.
쥬스가게가 Struct를 이용해 값타입을 갖게되면서, 쥬스가게의 인스턴스는 stack 메모리에 할당 되고 그 메모리 안에서 쥬스가게의 모든 프로퍼티와 메서드를 알 수 있습니다.
만약 쥬스가게 class를 이용한다면 쥬스가게의 프로퍼티와 메서드들이 heap메모리에 이곳 저곳에 할당 되고, 주소를 참조하는 방식을 사용하게 될 것입니다. 만약 쥬스가게가 더 많은 프로퍼티와 메서드를 갖고 있다고 생각해본다면 참조를 추적할 일이 더 많아 지는 것이고 Struct를 이용하는 게 쥬스가게의 코드에 대해 더 쉽게 추론할 수 있다는 말을 이해할 수 있습니다.
- 속도 : 참조 타입은 참조 추적에 비용이 많이 듭니다. 값 타입은 시스템 리소스가 적게 들어갑니다.
- 안전성 : 클래스는 참조 타입이기 때문에 멀티쓰레드 환경에서 여기저기 참조하여 작업하면 데이터가 꼬일 가능성이 큽니다.
2. 쥬스가게는 외부와 공유할 메서드, 프로퍼티를 갖고있지 않습니다
- 만약 쥬스가게에서 쥬스를 제조하는 자체 레시피를 프로퍼티로 혹은 nested Type으로 갖고 있었다고 가정해봅니다. 쥬스 가게가 class일 경우 레시피가 heap메모리에 할당되고 '참조'로 레시피를 사용하게 됩니다. 이럴 때 외부에서 그 레시피를 변경할 수도 있기때문에 코드 전체를 신경써야 합니다.
- 사실 쥬스가게는 자체 레시피를 외부로 공유하지 않아도 되고, 변경하더라도 쥬스가게 혼자만 알고 있으면 되기때문에 Struct를 사용하여 의도적으로 알리지 않는 한 app의 다른부분에서 보이지 않게 해도 상관없습니다.
---
커멘트
### 1. IBOulet에서 weak라는 아이는 왜 사용하는지 사용하지 않으면 안되는지 이유를 알고 계신가요?
스위프트의 메모리관리를 공부해보았습니다.
스위프트가 ARC를 통해 Heap영역의 메모리를 관리하는 법, 약한참조(weak)와 강한참조(strong)시 메모리에서 어떻게 할당과 해제가 작동되는지 알 수 있었습니다.
IBOutlet에서 약한 참조인 weak를 쓰는 이유는 **메모리 부족시 발생할 수 있는 메모리 누수를 막기 위함입니다**. 메모리가 부족하게 되면 ViewController는 `didReceiveMemoryWarning`를 자동으로 호출하게 되는데 이 때 mainView를 nil로 만들면서 그 subView들까지 모두 메모리 해제를 시켜 메모리를 확보하려합니다. 이 때 만약 subView들의 IBOulet이 Strong으로 연결되어 있다면 Reference Count가 1 이하로 내려갈 수 없게 되고, ParentView가 nil이 되도 subView는 메모리 해제가 될수 없게됩니다.
즉 강한 참조로 선언된 subView에서는 parentView가 예기치않게 종료될 때에 메모리 누수가 발생할 수 있음을 뜻합니다. 그렇기 때문에 예방차원에서 약한참조인 weak를 사용하는게 메모리관점에서 유리합니다.
그리고 일반적으로 `weak`를 사용하는 경우는 reference cycle을 방지하기 위해서입니다. 현재 저희가 작성한 코드에서는 `weak` 키워드를 지우고 강한 참조로 선언해도 reference cycle 문제는 발생하지 않습니다.
### 2. lazy를 사용하신 이유는 무엇인가요?
사실 Xcode에서 시키는 대로 했는데.. 웨더 덕분에 많이 공부할 수 있었습니다. 감사합니다!
`fruitLabel` 딕셔너리 안에서 value로 사용되고 있는 `strawberryStockLabel`은 사실 `self.strawberryStockLabel`과 같은 표현입니다.
여기서 `self`는, 인스턴스 자신을 가리키고 있기 때문에 인스턴스의 초기화가 완료되기 전까지는 `self.strawberryStockLabel`에 접근할 수 없습니다.
하지만 어떤 구조체나 클래스 등을 인스턴스화 해서 메모리에 올릴 때, 어떤 프로퍼티가 먼저 메모리에 올라갈 지 알 수 없으므로 항상 `self.strawberryStockLabel`에 접근할 수 있다고 보장할 수 없습니다. 그렇기 때문에 `strawberryStockLabel`은 인스턴스가 초기화 될 때 무조건 초기화가 이루어지고, `fruitLabel` 딕셔너리는 `lazy`로 선언해서 해당 딕셔너리에 접근 되었을 때 초기화가 되도록 해서 `strawberryStockLabel`이 먼저 초기화가 되어있어서 값을 가져올 수 있도록 보장하는 것이라고 생각합니다.
처음에는 `fruitLabel` 딕셔너리를 사용하는 함수 내에서 선언했기 때문에 `lazy`로 선언할 필요가 없었습니다. 하지만 해당 딕셔너리를 `updateFruitStockLabel`, `changeFruitStock` 두 개의 메서드에서 사용하기 위해 클래스의 멤버 변수로 선언하도록 변경하면서 `lazy`를 사용하게 되었습니다.
아직 저희는 경험하지 못한 일이지만, 앱의 다른 부분에서 `lazy`로 선언된 변수에 접근하려고 할 때 이 `lazy`한 변수에서 요구하는 다른 프로퍼티의 값이 설정되어 있지 않아 런타임 오류가 발생할 수도 있다는 이야기를 들었습니다..
그렇다면 `@IBOutlet`으로 선언된 `view` 프로퍼티에는 언제 값(인스턴스)가 할당되는지 궁금해져서 뷰 컨트롤러 & 뷰 라이프 사이클에 대해 공부해보게 되었습니다.
1. ViewController의 `init` 메서드 호출
- 스토리보드로 VC를 초기화하는 경우 nib 파일에서 unarchive되어 불리게 된다
- 스토리보드를 사용할 경우, 스토리보드 객체에서 로드되었을 때 뷰가 설정된다
- `UIStoryboard`의 `instantiateViewController(withIdentifier: )`메서드로 지정한 identifer의 스토리보드의 데이터를 가지고 뷰 컨트롤러를 생성하고 초기화한다
2. ViewController의 `loadView` 메서드 호출
- 뷰를 로드하거나 생성해서 VC의 `view` 프로퍼티에 할당
- 스토리보드에 뷰가 존재할 경우, nib파일에서 뷰를 로드한다
- 뷰의 `init` 메서드 호출
- VC가 가지고 있는 뷰를 초기화하고, `view` 프로퍼티에 할당
3. ViewController의 `prepare` 메서드 호출
4. ViewController의 `viewDidLoad` 메서드 호출
- 뷰의 로드가 모두 끝났음을 의미
- 뷰에 대한 다른 custom 초기화 작업을 이 메서드를 오버라이딩해서 구현할 수 있다
- 실제로 `loadView()` 메서드를 뷰 컨트롤러 내에서 오버라이딩했더니, `@IBOutlet`이 붙은 프로퍼티의 뷰를 생성하지 못해서 컴파일러 오류가 발생했다.
- 뷰 컨트롤러에 의해 호출된`loadView()` 메서드가 각 `view`의 인스턴스를 생성한 후, `@IBOutlet`이 붙은 `view` 프로퍼티들에 가서 값을 할당한다.
- <img width = 500, src = "https://i.imgur.com/oKumGlV.png">
- <img width = 500, src = "https://i.imgur.com/UpklLiv.png">
5. 그 후 `fruitLabel` 딕셔너리에서 value로 사용된 `strawberryStockLabel`에
4에서 값을 할당해준 `@IBOutlet`이 붙은 `view` 프로퍼티들이 가진 인스턴스가 값으로 할당된다.
출처1: https://developer.apple.com/documentation/uikit/uiviewcontroller/1621454-loadview
출처2: https://medium.com/@Alpaca_iOSStudy/viewcontroller-view-lifecycle-daed5766e02b
---