MINT
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
    • Invite by email
      Invitee

      This note has no invitees

    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Note Insights New
    • Engagement control
    • Make a copy
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Note Insights Versions and GitHub Sync Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Engagement control Make a copy Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
  • Invite by email
    Invitee

    This note has no invitees

  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       Owned this note    Owned this note      
    Published Linked with GitHub
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    ###### tags: `project` # 🧃 주스메이커 ![](https://hackmd.io/_uploads/B1wQL_vN3.png) ## Ground Rules ### 규칙 - 월, 목 12:00 - 14:00 개인공부. ### 스크럼 - 10시에 시작. ### 프로젝트 규칙 - 코드베이스도 같이 공부해보기 - 읽고 참고해야하는 문서 전부 공부한 후에 프로젝트 진행 - 다른 사람이 올린 PR에 휩쓸리지 않기 - 많이 질문하기 - 그때그때 궁금한 점 핵엠디에 정리 ## 일일 스크럼 ### 🙌 05/08 - 오늘의 컨디션 - MINT: 굿~ - Zion: 예에~ - 특이사항 - MINT: 배고파요. - Zion: 잠을 잘 못잠. - 오늘 할 일 - [ ] initialization 공부 완료하기 - [x] 활동학습 예습 - [x] MVC ### 🙌 05/09 - 오늘의 컨디션 - MINT: 😵 - Zion: 좋아요~ 하하하 - 특이사항 - MINT: 오뉴가 활발해요 - Zion: 잠 잘자서 좋습니다. - 오늘 할 일 - [x] initialization 공부 완료하기 - [x] Access Control 공부 완료하기 - [x] Type Casting 공부 완료하기 - [x] MVC ### 🙌 05/10 - 오늘의 컨디션 - MINT: 아주 상쾌함 - Zion: 잠을 잘 못자서 피곤함 - 특이사항 - MINT: 오전에 병원? - Zion: 오전에 에어컨 기사님 오심. - 오늘 할 일 - [ ] 스토리보드 - [x] Type Casting - [x] Error Handling - [x] 코드 수정 ### 🙌 05/11 - 오늘의 컨디션 - MINT: 자꾸 깨서 조금 피곤 - Zion: 조금씩 깨는 것 같아서 조금 피곤 - 특이사항 - MINT: nothing~ - Zion: - 오늘 할 일 - [x] STEP 1 PR 코멘트 오면 수정 - [x] 활동학습 예습 - [ ] ### 🙌 05/12 - 오늘의 컨디션 - MINT: 푹 자서 해피해피 근데 자세 때문에 허리가ㅏㅏㅏ - Zion: 운동하고 와서 상쾌상쾌~ 예이~ - 특이사항 - 12시 30분에 스크럼 시작~ - MINT: 뒹구르르르를 - Zion: - 오늘 할 일 - [x] 리드미 데이 - [x] STEP 1 PR 코멘트 달기 - [x] Step2 진행시 필요한 UI 연결작업 - [x] Notification, KVO 정리하기 ### 🙌 05/15 - 오늘의 컨디션 - MINT: 왜 졸릴까요??? - Zion: 좋숩니다 후후. - 특이사항 - MINT: 낫띵 - Zion: 닭가슴살 너무 비림. - 오늘 할 일 - [x] 화면전환 - [x] delegate pattern 적용 - [ ] 활동학습 예습공부(Singleton Pattern) ### 🙌 05/16 - 오늘의 컨디션 - MINT: 상쾌~ - Zion: 조금 피곤 - 특이사항 - MINT: 많이 먹는 하루 될거에요 - Zion: 마음 한켠이 불편함. - 오늘 할 일 - [x] Step2 구현완료 및 코드 정리 - [x] Singleton 정리 및 학습 (Zion, 민트 밀린 TIL 작성) ### 🙌 05/17 - 오늘의 컨디션 - MINT: 생각보다 낫베드 / 저도 메롱할겁니다🤪 - Zion: 날씨가 많이 더워졌어요 벌써 여름!!! - 특이사항 - MINT: 저는 서울이에요!! - Zion: 오늘은 9기 모각코 하는 날~ 예이~! - 오늘 할 일 - [ ] performSegue, pushViewController, present modalpresentationStyle 공부하고 차이점 써보기 - [x] STEP 2 PR - [x] README 작성 - [ ] 민트 사탕 뺏어먹기 ### 🙌 05/18 - 오늘의 컨디션 - MINT: 졸림~ - Zion: 컨디션 좋지 않았다. - 특이사항 - MINT: 헤피~ - Zion: 속이 좋지 않았습니다. - 오늘 할 일 - [x] 활동학습 예습 - [ ] PR comment 반영 수정 - [ ] - [ ] ### 🙌 05/19 - 오늘의 컨디션 - MINT: 흠 - Zion: 속이 좋지 않습니다. - 특이사항 - MINT: ?????????????? - Zion: 츄라이츄라이 - 오늘 할 일 - [x] PR comment 반영 수정 - [x] View LifeCycle 관련 공부 -> TIL 작성 - [x] ReadMe 작성 마무리. ### 🙌 05/22 - 오늘의 컨디션 - MINT: 상쾌~ - Zion: 아침 부터 정신 없음 - 특이사항 - MINT: 저 단발했어요 힣 - Zion: 홀홀 - 오늘 할 일 - [x] delegate -> return - [ ] stepper - [x] 화면 진입시 재고수량 표시 - [x] 스터디 관련 상담 - [ ] TIL 알림판 - stepper도 tag로 구현하는 건지? - 커스텀 버튼 저는 아직 못하겠어요.. ㅜㅡㅜ 이 방법은 이번에는 패스.. - juiceMaker에서 JuiceOrderViewController로 값을 넘겨줄 때 delegate를 사용하지 않았으면 좋겠어요. -> 바꾸고 싶어요 이 부분. ### 🙌 05/23 - 오늘의 컨디션 - MINT: 원래 자도자도 졸린걸까요? - Zion: 밤에 배아파서 깻읍니다. - 특이사항 - MINT: 요즘 다니엘의 인어공주 ost에 빠졌어요 - Zion: 지각했읍니다. 반성합니다. - 오늘 할 일 - [x] 오토 레이아웃 구현 - [x] 타이틀 및 닫기 버튼 구현 - [x] stepper - [ ] TIL ### 🙌 05/24 - 오늘의 컨디션 - MINT: 너무너무 졸려요 이따 자러 갈거에요 - Zion: 잠을 잘자서 좋습니다~! - 특이사항 - MINT: 저녁에는 외식 하고 올거에요~! - Zion: 저도 저녁에 약속있어요 오늘은 건대에요~! - 오늘 할 일 - [x] PR 코멘트 수정 - [ ] 개인 공부: 싱글톤 - [ ] ### 🙌 05/25 - 오늘의 컨디션 - MINT: 기절송ㅋㅋ - Zion: 조금 졸린 것 같음... - 특이사항 - MINT: 일반화는 상속입니다. - Zion: 오늘 5시반에 직거래 있숩니다. - 오늘 할 일 - [ ] PR Comment 작성 - [ ] 활동학습 예습 - [ ] TIL 작성 ### 🙌 05/26 - 오늘의 컨디션 - MINT: 기 절 트 - Zion: 잠을 잘자서 좋습니다. - 특이사항 - MINT: 기 절 트 - Zion: 오늘이 형민트 마지막날. - 오늘 할 일 - [ ] Read me 작성 Class Diagram 포함 - [ ] autoLayout Priority 설정 - [ ] PR 코멘트 마무리 - [ ] ## FruitStore 정의 5개의 과일종류 변수 초기값 10 // 은닉화 이것들의 수를 조정하는 메서드 // 캡슐화 + a ## JuiceMaker 정의하고 내부에 FruitStore가 정의되어야하고 과일쥬스 제조 함수 과일재고가 부족시 예외처리, 제조 불가 +a ## 과일쥬스의 종류 enum ## 과일쥬스 만들시에 대한 예외처리 enum ## 오류처리는 뭘로해야하나 ~ do-catch / Result Type <br> # 🧃 주스메이커 ## 📖 목차 1. [소개](#-소개) 2. [팀원](#-팀원) 3. [타임라인](#-타임라인) 4. [시각화된 프로젝트 구조](#-시각화된-프로젝트-구조) 5. [실행 화면](#-실행-화면) 6. [트러블 슈팅](#-트러블-슈팅) 7. [참고 링크](#-참고-링크) 8. [팀 회고](#-팀-회고) </br> ## 🍀 소개 형민트(`Zion`, `MINT`)팀이 만든 주스 메이커입니다. 주스를 주문하면 메뉴의 레시피에 맞게 과일의 재고를 확인하고 사용합니다. * 주요 개념: `UIKit`, `Outlet/Action`, `Modality`, `Navigation`, `OOP`, `MVC`, `Singleton`, `Delegation` </br> ## 👨‍💻 팀원 | MINT | Zion | | :--------: | :--------: | | <Img src = "https://hackmd.io/_uploads/SJ6F7HjE2.png" width="200" height="200"> |<Img src= "https://hackmd.io/_uploads/rJqMfSoVn.png" width="200" height="200"> | |[Github Profile](https://github.com/mint3382) |[Github Profile](https://github.com/LeeZion94) | </br> ## ⏰ 타임라인 |날짜|내용| |:--:|--| |2023.05.08.| - MVC, initialization 공부 | |2023.05.09.| - changeFruitCount 함수, Fruit 열거형 생성 <br> - makeFruitJuice(menu) 생성, JuiceMake 생성시 fruitStore 주입 <br> - makeJuice, makeCollaborateJuice , verifyFruitCount 함수 <br> - makeJuice() error에 대응할 수 있도록 수정, 사용하지 않는 라이브러리 삭제 | |2023.05.10.| - getRecipe 함수를 사용해서 menu 따른 주스 만들도록 수정 | |2023.05.11.| - 네이밍 수정 <br> - FruitSore에서 과일 재고를 더할수도 있게 리펙토링 <br> - FruitStore에서 데이터 타입 변경 | |2023.05.12.| - Fruit enum 외부 파일로 분리 및 수정 <br> - 피드백 사항 반영 | |2023.05.15.| - JuiceMakeDelegate 구현 | |2023.05.16.| - successAlert, failAlert 기능 구현 | |2023.05.17.| - JuiceMaker 안에 있던 recipe를 init으로 줄 수 있게 리펙토링 <br> - 네이밍 및 컨벤션 수정 | |2023.05.19.| - Fruit에 있는 RawValue 삭제하는 방향으로 리펙토링 | |2023.05.22.| - Custom init으로 FruitStockViewController가 fruitStore를 받도록 수정 | |2023.05.23.| - 화면 전환 방식 modal로 수정 <br> - 오토 레이아웃 구현 | |2023.05.24.| - JuiceMaker 반환값 로직 수정 <br> - Stepper에서 값 전달 과정 수정 | |2023.05.25.| - 네이밍 수정 | |2023.05.26.| - 오토 레이아웃 수정 <br> - class Diagram 작성 | </br> ## 👀 시각화된 프로젝트 구조 ### Flowchart <p align="center"> <img width="800" src="https://hackmd.io/_uploads/ry47pK4Bh.png"> </p> ### Diagram <p align="center"> <img width="800" src= "https://hackmd.io/_uploads/HJ0PtnaSh.png" > </p> </br> ## 💻 실행 화면 | 주문 성공 시 알람이 뜨는 화면 | |:--------:| |<img src="https://hackmd.io/_uploads/rkd8bO4B2.gif" width="800">| | 주문 실패 시 알람이 뜨고 화면 전환 | |:--------:| |<img src= "https://hackmd.io/_uploads/S1N7i2pBn.gif" width="800">| | 재고 수정 버튼 누를 시 화면 전환 | |:--------:| |<img src= "https://hackmd.io/_uploads/B1e11nnaSh.gif" width="800">| | 재고 추가 화면에서 stpper를 이용해 재고 변화 | |:--------:| |<img src= "https://hackmd.io/_uploads/SJO59haBh.gif" width="800">| </br> ## 🧨 트러블 슈팅 1️⃣ OCP <br> - 🔒 **문제점** <br> ```swift= func verifyFruitCount(_ fruit: Fruit, count: Int) -> Bool { switch fruit { case .strawberry: return strawberry >= count ? true : false case .banana: return banana >= count ? true : false case .pineapple: return pineapple >= count ? true : false case .kiwi: return kiwi >= count ? true : false case .mango: return mango >= count ? true : false } } func changeFruitCount(_ fruit: Fruit, count: Int, isUseFruit: Bool = true) { let amount = isUseFruit ? (count * -1) : count switch fruit { case .strawberry: strawberry += amount case .banana: banana += amount case .pineapple: pineapple += amount case .kiwi: kiwi += amount case .mango: mango += amount } } ``` 기존에 알고 있던 OCP의 개념은 ``'확장성에는 열려있어야 하지만 그로인한 로직의 수정은 최소화하거나 없어야한다.'`` 였다. 위의 예로 제시한 코드로 살펴 본다면 `enum` 값에 `case orange`가 추가 되었을 때 `verifyFruitCount()`, `changeFruitCount()`가 최소한을 수정되거나 수정되는 부분이 없는 경우 OCP를 잘 준수한 것으로 볼 수 있을 것 같다. 이러한 부분으로 본다면 현재 저희가 작성한 코드에는 위의 예시코드와는 달리 `fruitsStock` 라는 `Dictionary` 형식으로 과일재고에 접근하고 있기 때문에 orange의 case가 늘었을 경우 `fruitsStock`만 수정하면되므로 수정을 최소한 줄일 수 있었다고 생각한다. 하지만 위와 같이 코드를 수정하면서 고민이 되었던 부분이 있었다. 위의 예시에서처럼 `switch`를 사용하여 로직을 구현했을 경우 새로운 case로 orange가 들어왔을 때, 컴파일 오류가 발생하게 된다. 현재 사용되고 있는 `switch`에서는 orange case를 다루는 구문이 없기 때문이다. 이 컴파일 에러는 개발자로 하여금 `switch`의 모든 경우에 대해 구문을 작성해야한다는 의무를 부여하기도 한다고 생각한다. 마치 `Optional` 값을 `Binding`해서 사용해야한다고 강요하는 것 처럼. PR로 올린 `Dictionary`로 통해 접근하는 부분을 생각해봤을 때 해당 코드는 확장되고자 했을 때, 코드 수정을 최소한으로 줄인 것은 맞지만 오히려 개발자로 하여금 '찾고 고쳐야할 부분을 직접 분석하고 탐색하여야 하는 불편함을 줄 수 있지는 않을까?' 라는 생각도 하게 되었다. 🔑 **해결방법** <br> OCP의 개념을 다루면서 확장성에 대한 많은 생각을 하게만드는 주제였다. 과일의 재고관리를 각각의 `Int`변수로 관리하는 것이 아닌 `Dictionary([Fruits:Int])` 형식으로 부여해 위의 문제를 해결했다. `Dictionary`로 변경하게 되면 존재하는 key 값에 대해 재고 값을 `Optional Binding`해서 사용하게되므로 `switch`문의 사용을 줄일 수 있었다. 또한 여러개의 `FruitStore` 가 생성되어 각 `FruitStore`별로 다른 과일 재고를 가지게 될 수 있으므로 위와 같이 `switch-case`를 사용했을 때 보다 불필요한 코드를 줄일 수 있고 확장성을 가졌을 때도 코드의 수정사항이 최소한이 될 수 있는 코드구현이 가능했다. <br> 2️⃣ **확장성** <br> - 🔒 **문제점** <br> `makeJuice` 메서드 구현당시 각각의 쥬스를 만들 과일의 갯수를 체크하는 로직을 과일이 1개인지 2개인지로 나누어서 구현했었다. 그 결과 하나의 쥬스에 2개의 과일이 들어가는 쥬스까지는 무리없이 돌아가지만 하나의 쥬스에 그 이상의 과일이 들어가게 될 경우 코드의 수정이 불가피했다. 확장성이 떨이질 수 있다는 리뷰를 듣게 되었다. 🔑 **해결방법** <br> 처음 그 말을 들었을 때는 요구사항이 바뀌는 것이 확장성과 연관이 있나? 라는 생각이 들었지만, (Step이 넘어간다고 해서 하나의 쥬스에 들어가는 과일의 갯수가 바뀌진 않기 때문에, 또한 요구사항이 개발단계까지 넘어왔다면 바뀌지 않는 것이 당연하다고 생각했기 때문에) 요구사항은 언제나 항상 바뀌게 되고 그에 따른 대응은 개발자가 해야하는 것이므로 당연히 확장성과 연관이 있을텐데... 너무나 중요하고 당연한 부분을 숙제 하듯이 개발해나가면서 놓쳤다는 생각이 들었다. 그 결과 하나의 쥬스에 몇개의 과일이 들어오더라도 문제없이 쥬스가 만들어질 수 있도록 로직을 수정했고, 오히려 가독성이 좀더 나은 코드를 구현할 수 있었다. <br> 3️⃣ **recipe의 분류** <br> - 🔒 **문제점** <br> 주스를 만들 때 `makeJuice` 함수에 매개변수로 소모되는 과일의 종류와 개수를 넣어 recipe를 따로 분류하지는 않았다. 매번 매개변수로 받다보니 만일 과일을 3개 사용하는 신메뉴가 나올 경우 혹은 더 늘어나는 경우 전체적인 로직을 수정해야하는 상황이 있었다. 코드의 확장성적인 부분에서 조금 더 고민해보라는 리뷰를 받게 되었다. ```swift= func makeJuice(menu: Fruit, count: Int) { if store.verifyFruitCount(menu, count: count) { store.changeFruitCount(menu, count: count) } } func makeCollaborateJuice(main: (fruit: Fruit, count: Int), sub: (fruit: Fruit, count: Int)) { if store.verifyFruitCount(main.fruit, count: main.count) && store.verifyFruitCount(sub.fruit, count: sub.count) { store.changeFruitCount(main.fruit, count: main.count) store.changeFruitCount(sub.fruit, count: sub.count) } } ``` 🔑 **해결방법** <br> 추후에 레시피에 들어가는 과일의 종류가 몇 개가 되든 상관없게 하기 위해 튜플 배열을 사용하면서 레시피를 따로 부르는 방법으로 변경하였다. 레시피의 내용들을 변수로 선언하여 사용하려다가 함수를 통해 메뉴를 받으면 그 메뉴에 해당하는 레시피를 넘겨주는 것 까지가 레시피의 역할과 책임으로 선언하여 사용하였다. ```swift= func canMakeJuice(menu: Menu) -> Bool { let recipe = provideRecipe(menu) guard recipe.allSatisfy({ fruit, amount in return store.isEnoughFruits(fruit, count: amount) }) else { return false } recipe.forEach { fruit, amount in store.changeFruitCount(fruit, count: amount) } return true } ``` <br> 4️⃣ **required init?** <br> 현재 `Main Storyboard`에서 `Main-ViewController`에 해당하는 `JuiceOrderViewController` 내부에서 `FruitStore`와 `JuiceMaker`를 가지고있다. `JuiceMaker`는 `FruitStore`를 초기화시 주입을 받아야하기 때문에 아래와 같이 `lazy` 키워드를 사용해서 선언했다. 하지만 굳이 `lazy`까지 사용하면서 해야할 필요성을 느끼지 못해서 `JuiceOrderViewController`에 사용자 지정 이니셜라이저를 추가해 그 부분에서 `fruitStore`와 `JuiceMaker`를 초기화하고자 했다. ```swift= private let fruitStore = FruitStore(fruitStocks: [.strawberry: 20, .banana: 20, .kiwi: 20, .mango: 20, .pineapple: 20]) private lazy var yagomJuiceStore = JuiceMaker(fruitStore, recipe) ``` 🔒 **문제점** <br> ```swift= private let fruitStore: FruitStore private var yagomJuiceStore: JuiceMaker init() { fruitStore = FruitStore(fruitStocks: [.strawberry: 20, .banana: 20, .kiwi: 20, .mango: 20, .pineapple: 20]) yagomJuiceStore = JuiceMaker(fruitStore, recipe) super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } ``` `Main-ViewController`의 특성상 초기화를 통한 의존성 주입은 불가하다고 생각했기 때문에 `init()` 속에서의 초기화만을 `JuiceOrderViewController`의 사용자 지정 이니셜라이저를 통해 진행하려 했지만 실제 프로그램 동작시 사용자 지정 이니셜라이저가 아닌 `required init?`으로 진입되어 원하는 동작을 하지 않았다. 🔑 **해결방법** <br> 먼저, 사용자 지정 이니셜라이저를 사용하는 경우 `UIViewController`에서 정의한 이니셜라이저를 가져다 사용할 수 없기 때문에 `UIViewController`에서 정의된 초기화 메서드들을 정의해줘야한다. `UIView`, `UIController`는 `NSCoding protocol`을 채택하고 있기 때문에 해당 프로토콜에서 선언한 `init`인 `required init?`을 `JuiceOrderViewController`에서 정의해줘야했다. 또한, 사용자 이니셜라이저가 아닌 `required init?`이 호출되는 이유는 스토리보드상에 존재하는 `ViewController`를 로드할 때 어떤 `init`을 사용해서 초기화를 진행해야하는지 개발자가 명시하고있지 않기 때문에 기본적으로 구현해야함을 요구한 `required init?`이 호출될 수 밖에 없다. 따라서 `Main-ViewController`의 경우 `Main-ViewController` 특성상 개발자가 어떤 `initializer`를 사용해 초기화를 진행할 것인지에 대해 명시할 수 없기 때문에 이경우에는 `required init?` 메서드에 초기화를 진행한다면 문제없이 동작함을 확인할 수 있었다. ```swift= private let fruitStore: FruitStore private var yagomJuiceStore: JuiceMaker required init?(coder: NSCoder) { fruitStore = FruitStore(fruitStocks: [.strawberry: 20, .banana: 20, .kiwi: 20, .mango: 20, .pineapple: 20]) yagomJuiceStore = JuiceMaker(fruitStore, recipe) super.init(coder: coder) } ``` 5️⃣ **stepper의 값 전달에서 객체지향적인 관점** <br> - 🔒 **문제점** <br> `FruitStockViewController`에서 `stepper`를 통해 `FruitStore`의 재고 값을 변경하는 방법에 대해 고민했다. 1. `FruitStore`에서 기존에 파라미터로 받는 `Bool`값을 통해 증가, 감소를 구분하는 형식. 2. `stepper`를 사용하는 과정에서 음료를 제조해서 과일을 소모할 때 재고를 레시피만큼 빼는 함수를 하나 분리하고 다른 함수는 재고를 입력받은 값만큼 증가시키고 감소시키는 것이 아니라 재고 자체를 새로운 값으로 덧씌우는 형식. 1번 ```swift= func changeFruitCount(_ fruit: Fruit, count: Int, isUseFruit: Bool = true) { guard let stock = fruitStocks[fruit] else { return } let fruitAmount = isUseFruit ? -count : count fruitStocks[fruit] = stock + fruitAmount } ``` 2번 ```swift= func consumeFruitCount(_ fruit: Fruit, amount: Int) { guard let stock = fruitStocks[fruit] else { return } fruitStocks[fruit] = stock - amount } func changeFruitCount(_ fruit: Fruit,_ amount: Int) { fruitStocks[fruit] = amount } ``` 2번이 함수의 기능분리적인 면에서는 나은 것 같으면서도 재고 하나를 추가할 때마다 과일의 개수를 덧씌운다는 것이 객체 지향적인 관점에서 `stepper`의 역할이 맞나? 라는 생각이 들었다. 단순한 생각으로는 더하기빼기의 기능 자체를 `ViewController`가 아니라 `Model`인 `FruitStore`에서 해야하지 않나 싶기도 해서 고민이 되었다. 🔑 **해결방법** <br> 결국 1번의 방법으로 `stepper`의 역할을 단순히 더할 수 있는 정도만 전달해 주는 것으로 수정하였다. 그렇게 하는 것이 추구하는 객체 지향적인 관점에 더 맞는 방법 같았다. 때문에 `FruitStockViewController`의 `stepperAction` 부분을 수정하였다. ```swift= @IBAction func tappedFruitStepper(_ sender: UIStepper) { guard let fruit = Fruit(rawValue: sender.tag), let oldAmount = fruitStore.provideFruitStock(fruit) else { return } let gapAmount = sender.value - Double(oldAmount) > 0 ? sender.stepValue : -sender.stepValue fruitStore.changeFruitCount(fruit, Int(gapAmount)) refreshFruitLabelText(index: sender.tag) } ``` </br> ## 📚 참고 링크 - [🍎Apple Docs: preventing override ](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/inheritance/#Preventing-Overrides) - [🍎Apple Docs: choosing between structures](https://developer.apple.com/documentation/swift/choosing-between-structures-and-classes) - [🍎Apple Docs: initialize](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/initialization/) - [🍎Apple Docs: stepper](https://developer.apple.com/documentation/swiftui/stepper) - [📘blog: delegate ](https://zeddios.tistory.com/8) </br> ## 👥 팀 회고 - [팀 회고 링크](https://github.com/mint3382/ios-juice-maker/wiki/%ED%8C%80%ED%9A%8C%EA%B3%A0) # 팀 회고 ## 우리팀이 잘한 점 - 프로젝트의 진행 속도와 무관하게 제시된 자료들은 먼저 공부하고 이해하려고 했습니다. - 한 가지의 해결방법만 생각한 것이 아니라 더 좋은 해결방법이 없는지, 객체지향적 시점으로 봐도 문제가 없는지 생각하고 구현했습니다. - 정해진 시간과 규칙에 맞춰서 자유롭게 의견을 나눴습니다. ## 우리팀 개선할 점 - PR을 올리기 전에 이전 수정사항들로 인해서 문제가 발생한 부분은 없는지 세너티 테스트를 진행하지 않고 PR을 올렸습니다. PR을 올리기전 마지막으로 같이 코드의 수정사항들을 보면서 잘못된 부분이나 동작은 없는지 확인하고 검토해야한다고 생각합니다. ## 서로에게 좋았던 점 피드백 - MINT : 어떠한 일이 있을 때도 당황하지 않고 침착하게 상황을 정리하는 모습이 좋았습니다.⚽️ 매번 제가 혼자서 해볼 수 있도록 약간의 생각할 거리와 함께 충분한 시간을 기다려줘서 더 성장할 수 있었습니다. 끌려가는 것이 아닌 제 걸음마에 맞춰서 손잡아주셔서👨‍👧 아장아장 걸어가는 기분이 들었기에 정말 고맙고 다행이었다고 생각합니다.👍 - ZION : 문제를 해결하는데 있어서 여러가지 다양한 해결방법들을 생각해볼 수 있어서 좋았습니다. 민트에 비교하면 저는 주로 틀에박힌 생각을 가졌던 것 같습니다. 또한 객체지향적인 관점에서 객체들에 대해 고민해 볼 수 있어서 좋았습니다. 메서드 호출 및 각각의 기능이 어떻게 수정되어야 조금 더 객체지향적은 흐름이 될 수 있을지 고민하는 모습이 인상적이었습니다. - som : 개인 프로젝트 할 때마다 자주 들어왔는데, 좋은 백색소음이 되어주셔서 감사합니다. 형민트 폼 무쳤다~! 근데 질문이 좀 많아요....ㅎ ## 서로에게 하고싶은 말 - MINT : 함께 팀원으로 지내면서 정말 많이 느낀 것은 시온이 배려하기 위해 노력한다는 것이었어요. 답답할 수 있는데 항상 제게서 배울 점을 찾으시는 것 같아 굉장히 인상적이었습니다. 모든 사람에게 배울 점을 찾으시려는 것 같아서 굉장히 좋고 본받고 싶은 마음 가짐이라는 생각을 했습니다. 시온이 원하는 것을 이 캠프를 통해 같이 얻어갈 수 있으면 좋겠습니다~! 😈 - ZION : 민트가 하루동안 개발에 투자하는 시간, 객체지향으로 프로그래밍을 바라보는 관점, 모르는 문제가 나타났을 때 어떻게든 해결해 나가려는 모습들을 보면 놀랄 때가 많습니다. 아직은 아는 것들 보다 모르는 것들이 더 많을 수 있겠지만 개발을 대하는 태도가 지금만 같다면 누구보다 잘하는 개발자가 될 수 있음을 믿어 의심치 않습니다.😈 ### MVC 패턴 아키텍쳐는 구조를 설계하는 것, 패턴은 경험들이 모여서 그것을 수정하기 위해 정형화 된 것. 순서의 차이가 있음. MVC 패턴은 아키텍쳐와 디자인 패턴으로 사용할 수 있는데 만들어지는 과정을 보면 디자인 패턴에 가까움. Model, view, controller로 구분되는데 view는 화면 및 화면 구성 요소들, model은 그 화면 구성 요소들에 필요한 데이터 및 무언가 화면에서 일이 일어났을 때/ 이벤트가 발생했을 때 일어나는 일(기능 = 비즈니스 로직), controller는 이러한 model과 view를 이어주는 역할/ view 갱신. view에서 이벤트 일어남 -> controller에서 인지하고 이벤트가 일어났을 경우 해야하는 기능을 지닌 model 호출, model working(ex: 데이터 갱신) -> 결과물 다시 controller로 (Notify) -> controller가 view로 결과물따라 전환. --- # STEP 1 ## 고민되었던 점 ### typealias 코드의 직관성을 위해 고민하다가 레시피에 들어가는 과일의 종류와 개수를 튜플 배열로 선언하면서 더 명확한 명시를 위해서 `typealias`를 사용하였습니다. ### recipe의 분류 처음 주스를 만들 때 `makeJuice` 함수에 매개변수로 소모되는 과일의 종류와 개수를 넣어 recipe를 따로 분류하지는 않았습니다. 추후에 레시피에 들어가는 과일의 종류가 몇 개가 되든 상관없게 하기 위해 튜플 배열을 사용하면서 레시피를 따로 부르는 방법으로 변경하였습니다. 레시피의 내용들을 변수로 선언하여 사용하려다가 함수를 통해 메뉴를 받으면 그 메뉴에 해당하는 레시피를 넘겨주는 것 까지가 레시피의 역할과 책임으로 선언하여 사용하였습니다. ### 확장성 `makeJuice` 메서드 구현당시 각각의 쥬스를 만들 과일의 갯수를 체크하는 로직을 과일이 1개인지 2개인지로 나누어서 구현했었습니다. 그 결과 하나의 쥬스에 2개의 과일이 들어가는 쥬스까지는 무리없이 돌아가지만 하나의 쥬스에 그 이상의 과일이 들어가게 될 경우 코드의 수정이 불가피했습니다. 확장성이 떨이질 수 있다는 리뷰를 들었습니다. 처음 그 말을 들었을 때는 요구사항이 바뀌는 것이 확장성과 연관이 있나? 라는 생각이 들었지만, (Step이 넘어간다고 해서 하나의 쥬스에 들어가는 과일의 갯수가 바뀌진 않기 때문에, 또한 요구사항이 개발단계까지 넘어왔다면 바뀌지 않는 것이 당연하다고 생각했기 때문에) 요구사항은 언제나 항상 바뀌게 되고 그에 따른 대응은 개발자가 해야하는 것이므로 당연히 확장성과 연관이 있을텐데... 너무나 중요하고 당연한 부분을 숙제 하듯이 개발해나가면서 놓쳤다는 생각이 들었습니다. 그 결과 하나의 쥬스에 몇개의 과일이 들어오더라도 문제없이 쥬스가 만들어질 수 있도록 로직을 수정했고, 오히려 가독성이 좀더 나은 코드를 구현할 수 있었습니다. 감사합니다 건디. ### 객체지향 최대한 객체지향적으로 프로그램이 동작할 수 있도록 설계했습니다. 유저는 메뉴만을 보고 주문만할 수 있고, `JuiceMaker`에서는 가지고 있는 레시피를 가지고 `FruitStore`을 통해 쥬스를 만들고, `FruitStore`에서는 가지고 있는 과일의 갯수를 수정하거나, 과일이 해당 `JuiceMaker`에서 원하는 만큼을 가지고 있는지 판단하는 로직을 두었습니다. 그 결과 `User`, `JuiceMaker`, `FruitStore`가 서로 상호작용을 하는 것처럼, 즉 객체를 지향하는 설계가 가능했습니다. ## 해결하지 못한 점 ## 조언을 얻고 싶은 부분 ### OCP ```swift= enum Fruit { case strawberry case banana case pineapple case kiwi case mango //case orange 확장 } func verifyFruitCount(_ fruit: Fruit, count: Int) -> Bool { switch fruit { case .strawberry: return strawberry >= count ? true : false case .banana: return banana >= count ? true : false case .pineapple: return pineapple >= count ? true : false case .kiwi: return kiwi >= count ? true : false case .mango: return mango >= count ? true : false } } func changeFruitCount(_ fruit: Fruit, count: Int, isUseFruit: Bool = true) { let amount = isUseFruit ? (count * -1) : count switch fruit { case .strawberry: strawberry += amount case .banana: banana += amount case .pineapple: pineapple += amount case .kiwi: kiwi += amount case .mango: mango += amount } } ``` 안녕하세요 건디! OCP와 관련하여 질문사항이 있습니다! 제가 알고 있는 OCP의 개념은 ``'확장성에는 열려있어야 하지만 그로인한 로직의 수정은 최소화하거나 없어야한다.'`` 의 개념으로 알고있습니다. 위의 예로 제시한 코드로 살펴 본다면 `enum` 값에 `case orange`가 추가 되었을 때 `verifyFruitCount()`, `changeFruitCount()`가 최소한을 수정되거나 수정되는 부분이 없는 경우 OCP를 잘 준수한 것으로 볼 수 있을 것 같아요! 이러한 부분으로 본다면 현재 저희가 작성한 코드에는 위의 예시코드와는 달리 `fruitsStock` 라는 `Dictionary` 형식으로 과일재고에 접근하고 있기 때문에 orange의 case가 늘었을 경우 `fruitsStock`만 수정하면되므로 수정을 최소한 줄일 수 있었다고 생각합니다. 하지만 위와 같이 코드를 수정하면서 고민이 되었던 부분이 있습니다. 위의 예시에서처럼 `switch`를 사용하여 로직을 구현했을 경우 새로운 case로 orange가 들어왔을 때, 컴파일 오류가 발생하게 됩니다. 그 이유는 현재 사용되고 있는 `switch`에서는 orange case를 다루는 구문이 없기 때문이에요. 이 컴파일 에러는 개발자로 하여금 `switch`의 모든 경우에 대해 구문을 작성해야한다는 의무를 부여하기도 한다고 생각합니다 마치 `Optional` 값을 `Binding`해서 사용해야한다고 강요하는 것 처럼. PR로 올린 `Dictionary`로 통해 접근하는 부분을 생각해봤을 때 해당 코드는 확장되고자 했을 때, 코드 수정을 최소한으로 줄인 것은 맞지만 오히려 개발자로 하여금 '찾고 고쳐야할 부분을 직접 분석하고 탐색하여야 하는 불편함을 줄 수 있지는 않을까?' 라는 생각도 하게 되었습니다. 이와 관련된 부분에 대해서 음성으로 대화할 수 있을까요? //allSatisfy 관련해서 따로 상수를 빼는 것으로 한번 생각해볼 것. # STEP 2 ## 고민했던 점 ### guard와 if 사이의 컨벤션 `makeJuice` 함수 내부에서 `canMakeJuice`의 `true`, `false`의 여부에 따라 성공과 실패 관련 사항을 처리하게 할 때 `guard 문`과 `if 문`의 사용에서 고민이 있었습니다. 처음 만들 때는 실패시 조기탈출을 위해 `guard 문`을 썼는데, 성공/ 실패에 따라 처리해야할 로직이 분명하게 나눠지기에 가독성을 위해서 `if 문`을 사용하는 것도 좋겠다는 의견이 나왔습니다. 고민 끝에 실패 시 조기탈출의 여부가 중요하다고 생각했기에 `guard 문`으로 작성하였습니다. ```swift= func makeJuice(menu: Menu) { let recipe = provideRecipe(menu) guard canMakeJuice(recipe) else { delegate?.failJuiceMake() return } consumeFruit(recipe) delegate?.successJuiceMake(menu) } ``` ```swift= func makeJuice(menu: Menu) { let recipe = provideRecipe(menu) if canMakeJuice(recipe) { consumeFruit(recipe) delegate?.successJuiceMake(menu) } else { delegate?.failJuiceMake() } } ``` ## 해결하지 못한 점 ### Main - ViewController 에서의 사용자 지정 initializer ```swift= private let fruitStore: FruitStore// = FruitStore(fruitStocks: [.strawberry: 20, .banana: 20, .kiwi: 20, .mango: 20, .pineapple: 20]) private var yagombucks: JuiceMaker// = JuiceMaker(fruitStore, recipe) init() { fruitStore = FruitStore(fruitStocks: [.strawberry: 20, .banana: 20, .kiwi: 20, .mango: 20, .pineapple: 20]) yagombucks = JuiceMaker(fruitStore, recipe) super.init() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } ``` `yagombucks`을 `lazy var`로 선언하여 연산 호출될 때 초기화를 실행하는 것 보다. 사용자 `JuiceOrderViewController`에서 사용자 지정 이니셜라이저를 사용해서 값을 초기화 하는 것이 좀 더 올바른 표현 방식이라 생각했습니다. 따라서 위와 같이 코드를 구현한 결과 잘못된 `init` 참조로 인한 `exception`이 발생했습니다. <분석내용> 먼저 `UIViewController`를 상속받은 `JuiceOrderViewController`에서는 사용자 지정 이니셜라이저를 사용하지 않을 경우 `UIViewController`의 이니셜라이저를 자동으로 가져다가 사용하고 있는 것으로 알고 있습니다. 하지만 `JuiceOrderViewController`에서 사용자 지정 이니셜라이저를 정의하게 되면 `UIView`, `UIViewController`에서 `NSCoding protocol`을 채택하고 있기 때문에 그에 따라 정의된 `init`에 해당하는 `required init?`을 정의해 줘야합니다. 하지만 위의 코드에대한 실행 결과를 보면 `Main Storyboard`에 연결된 `JuiceOrderViewController`는 사용자 지정이니셜라이저가 호출되어 초기화 되지 못하고 잘못된 경로로 들어가 `Fatal Error`를 발생시켰습니다. `Fatal Error`는 해당 초기화 메서드가 구현되지 않았음을 나타내는 것으로 알고있습니다. `Main Storyboard`에 연결된 `ViewController` 사용자 지정 이니셜라이저를 사용하여 값을 초기화 해주는 것이 불가능한 것인지 그게 아니라면, 다른 초기화 방법이 존재하는 것인지가 궁금합니다. <참고자료> [1번](https://www.zehye.kr/ios/2021/08/03/iOS_required_init_coder/) [2번](https://jeonyeohun.tistory.com/359) ## 조언을 얻고 싶은 점 ### 화면전환 (modality:x push:o) ```swift= private func moveToFruitStockViewController() { guard let pushJuiceOrderViewController = self.storyboard?.instantiateViewController(withIdentifier: "FruitStockViewController") else { return } self.navigationController?.pushViewController(pushJuiceOrderViewController, animated: true) } ``` 먼저, `NavigationControlle`r의 `rootViewController`에 해당하는 `JuiceOrderViewController`는 `NavigationController` 내부에 속한 `ViewController`이기 때문에 최대한 `Navigate`를 활용한 화면 전환을 하도록 구현했습니다. 그 과정 중 스토리보드상에 불필요하게 2개의 `NavigationController`가 사용되어지고 있는 것을 발견했고 하나를 삭제했습니다. 한편으로는 `NavigationController`로 `push`되는 `FruitStockViewController` 경우에는 `rootViewController`에 이어서 다음 화면으로 전환된다기 보다는 `rootViewController`의 데이터를 수정하는 독립적인 화면으로 판단했습니다. 따라서 이와 같이 기존 뷰에 독립적으로 출력할 수 있는 `modality` 형식으로의 화면전환을 생각해 봤으나 가로 화면에서는 상위 여백이 출력되어 `modal`임을 나타낼 수 있는 특징이 나오지 않기에 기존 `NavigationController`와의 차이점이 없어 고민 끝에 기존 사용했던 `NavigationController` 방법을 사용하였습니다. 이런 경우 형식이 `modality` 방법이나 `NavigationController` 방법이 차이가 없음에도 불구하고 이어지는 화면인지 독립적인 화면인지에 따라 방법을 선택해야 하는게 좋을 지 궁금합니다. # STEP 3 ## 고민했던 점 ### init으로 fruitStore를 FruitStockViewController로 넘겨주기 `JuiceOrderViewController`에서 `FruitStockViewController`를 부를 때 `init`을 사용해 `fruitStore`를 넘겨주고 싶었습니다. 그러기 위해서는 여러가지 수정이 필요했는데, 일단 `FruitStockViewController`에서 `Custom init`을 만들어주어야 했습니다. 그렇기에 `init`을 구현해주어야 하는데 `UIViewController`를 상속받으면 `init`을 구현해줄 때 `required init`이 필요했습니다.`UIViewController`는 `NSCoding` 프로토콜을 채택하고 있고 이 프로토콜에서는 `required init`으로 `NSCoder`라는 스토리보드로 구성한 UI 형태를 저장한 파일을 불러올 수 있게 해주는 친구를 구현하고 있기 때문입니다. 여기서 문제가 생겼었는데 -> 그동안 `instantiateViewController`로 `FruitStockViewController`를 받아온 후 `push`를 사용해 넘기고 있었는데 이때 `fruitStore`를 넘기기 위해 `creator`를 사용해야 했습니다. ```swift private func navigateToFruitStockViewController() { guard let fruitStockViewController = storyboard?.instantiateViewController(identifier: "FruitStockViewController", creator: { coder in FruitStockViewController(coder: coder, fruitStore: self.fruitStore) }) else { return } navigationController?.pushViewController(fruitStockViewController, animated: true) } ``` 이때 `coder: coder`를 넘겨주는데 이는 `FruitStockViewController`를 생성하기 위해 스토리보드에서 로드된 데이터가 포함되어 있기 때문에 없이는 사용할 수 없었습니다. 그 후 `FruitStockViewController` 에서 이 초기화 값들을 사용하기 위한 초기화 정의도 필요했습니다. ```swift init?(coder: NSCoder, fruitStore: FruitStore) { self.fruitStore = fruitStore super.init(coder: coder) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } ``` 그냥 init으로 self를 넘기거나 super.init에 다른 것들을 받으면 제대로 구현이 되지 않았습니다. `coder`로 받은 <`FruitStockViewController를 생성하기 위해 스토리보드에서 로드된 데이터`>를 선언해주지 않았기 때문입니다. 때문에 ```swift= super.init(coder: coder) ``` 로 선언해주었는데 `super.init`인 이유는 `FruitStockViewController` 안에 프로퍼티 등으로 선언되어 있던 것이 아니라 상속받고 있는 `UIViewController`가 채택한 `NSCoding` 프로토콜이 가지고 있어서 이곳에 데이터를 전해 넣어주어야 하기 때문입니다. 초반, `coder`를 사용해서 `NSCoder` 정보를 넘겨주지 않아 문제가 있었고 `creator`와 `super.init`에 선언해 넘겨줌으로서 해결했습니다. ### Stepper의 구분을 위한 Custom `IBOutlet Colleciton`을 사용하여 각각의 `Stepper`에 `sender.tag`를 활용하여 접근하는 것이 좋은 방법이 아닐 수 있겠다는 생각을해 어떻게 하면 각각의 `Stepper`를 구분하여 사용할 수 있을지 고민했습니다. 위와 같은 이슈를 해결할 수 있는 방법중 `Stepper`가 `id:String` 프로퍼티를 가질 수 있게 `Custom`하는 방법을 사용해봤습니다. 해당 방법을 사용하게 될 경우 `FruitViewController`가 처음 로드 될때 각각의` Stepper`에 `id`를 부여하게되고 `Stepper`을 구분하여 사용할 때마다 부여된 `id`를 통해 `Stepper`를 구분지어 사용하게됩니다. 하지만 `id`를 부여하는 방법은 `id`에 대한 부여를 코드상에서 표현할 수 있기 때문에 더 직관적일 수 있겠지만 이외의 모든 부분에 있어서 과하다는 생각을 했습니다. 그 이유는 `id`를 부여하는 로직 및 각각의 `id`에 맞게 `Stepper`를 분리하는 과정 또한 들어가 있어야 했기 때문입니다. 따라서 위와 같은 이유로 `IBOutlet Collection`을 사용해 `sender.tag`로 `Stepper` 및 `UILabel`을 구분지어 사용했습니다. ## 해결하지 못한 점 ## 조언을 얻고 싶은 점 ### FruitStore에서 재고 관리 함수 FruitStore에서 기존에 파라미터로 받는 Bool값을 통해 증가, 감소를 구분했었습니다. 그런데 stepper를 사용하는 과정에서 음료를 제조해서 과일을 소모할 때 재고를 레시피만큼 빼는 함수를 하나 분리하고 다른 함수는 재고를 입력받은 값만큼 증가시키고 감소시키는 것이 아니라 재고 자체를 새로운 값으로 덧씌우는 형식으로 변형했습니다. 변경 전 ```swift= func changeFruitCount(_ fruit: Fruit, count: Int, isUseFruit: Bool = true) { guard let stock = fruitStocks[fruit] else { return } let fruitAmount = isUseFruit ? -count : count fruitStocks[fruit] = stock + fruitAmount } ``` 변경 후 ```swift= func consumeFruitCount(_ fruit: Fruit, amount: Int) { guard let stock = fruitStocks[fruit] else { return } fruitStocks[fruit] = stock - amount } func changeFruitCount(_ fruit: Fruit,_ amount: Int) { fruitStocks[fruit] = amount } ``` 이렇게 방법을 변경하였는데 함수의 기능분리적인 면에서는 변경 후가 나은 것 같으면서도 재고 하나를 추가할 때마다 과일의 개수를 덧씌운다는 것이 조금 걸리기도 합니다. 단순한 생각으로는 더하기빼기의 기능 자체를 ViewController가 아니라 Model인 FruitStore에서 해야하지 않나 싶기도 해서 고민이 됩니다. 어떤 것을 더 중점적으로 두고 생각하는 것이 좋을지 건디의 생각이 듣고 싶습니다. ### 파인애플 버튼 #### stroyboard button title = attributed style = Defalut 대신 plain 등으로 #### code [버튼 크기를 늘리기](https://developer.apple.com/documentation/uikit/uiview/1622457-invalidateintrinsiccontentsize) [글자 크기를 줄이기](https://developer.apple.com/documentation/uikit/uilabel/1620546-adjustsfontsizetofitwidth)

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password

    or

    By clicking below, you agree to our terms of service.

    Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully