Skip to content

iOS 쥬스 메이커 재고관리 시작 저장소

Notifications You must be signed in to change notification settings

leeari95/ios-juice-maker

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

76 Commits
 
 
 
 
 
 

Repository files navigation

🍹 쥬스메이커 프로젝트

  • 팀 프로젝트(2인)
  • 프로젝트 기간: 2021.10.18 ~ 2021.11.05

목차


키워드

Swift

  • struct, class, enum
  • Initialization
  • Nested Types
  • Dictionary zip
  • typealias
  • Function Types as Parameter Types
  • Type Casting
  • Error Handling
  • Access Control
  • Dynamic Type, Content Priority

iOS

  • MVC (Model-View-Controller)
  • Singleton
  • UILabel, UIButton, UIStepper
  • HIG, Modality
  • UIAlertController, UIAlertAction
  • NavigationViewController, bar, button
  • NotificationCenter
  • prepare
  • Auto Layout

⭐️ 프로젝트 소개

UIButton을 이용하여 쥬스를 만들 수 있는 간단한 앱이에요!

또한 별도의 modal을 통해 과일 재고를 관리할 수 있어요! 🍓 🍌 🥝


✨ 프로젝트 주요기능

설명
🍹 다양한 과일을 통해 맛있는 쥬스를 만들어보세요!
🍓 재고수정 화면을 통해 재고를 추가해보세요!

UML


🛠 Trouble Shooting

"Navigation Controller가 두개로 구현되어있는 이유"

  • 상황
    • 초기 프로젝트 스토리보드에 Navigation Controller가 왜 두개로 나뉘어져 구현되어있는지 이유가 궁금했다. 알고보니 Navigation Bar를 이용하기 위함이였다. 하지만 쥬스메이커 메인화면에서 재고수정하기 버튼을 터치하게 되면 화면 이동방식은 modal이 되어야 한다고 생각했다.
  • 이유
    • 네비게이션을 따라서 스택에 따른 화면을 이동하는 방식이 적절하지 못하다고 생각했기 때문이다. 임시적으로 화면에 들어가서 재고를 수정하는 용도의 View라는 생각이 들었다.
  • 해결
    • 메인 화면을 재고수정이 구현되어있는 ViewController가 아니라 재고수정 화면에 연결되어있는 Navigation Controller에 Segue를 연결해서 modal을 구현하고, bar button을 활용하여 Cancel 버튼을 구현해주었다.

"이름은 같지만 타입이 다른 상황"

  • 상황
    • 프로토콜(LocalizedError)을 사용자 정의 타입(RequestError)에 채택 후 프로토콜이 정의한 프로퍼티가 아니라, 이름은 같지만 타입이 다른 프로퍼티(String, String?)를 구현해주었다.
    • 이후 파라미터로 값을 전달하는 과정에서 타입이 정의한 프로퍼티(errorDescription: String)가 아니라 프로토콜에서 기본 구현이 된 프로퍼티(errorDescription: String?)가 전달되었다.
  • 이유
    • 여러 테스트를 거쳐 알아낸 결과, 같은 이름이지만 타입이 다른 두 프로퍼티가 공존하고 있을 때, 파라미터로 전달할 때에는 타입이 일치하는 프로퍼티가 들어갔다.
  • 해결
    • 예를 들어 함수의 파라미터 후보로 String과 String? 두가지가 있고, 파라미터의 타입은 String? 이라면 컴파일러는 당연히 String?을 전달해주려고 할 것이다. 다만 최후의 후보가 String 뿐이라서 String? 자리에 String을 전달하는 경우 String을 String?으로 포장해줄 수는 있겠다. 당시에는 왜 String을 전달하고 있는데 왜 nil이 전달되는 것인지 이해가 가지 않았었는데, 그 의문을 질문을 통해서 해결하였다. 그리고 프로토콜을 채택한 후 정의되어있는대로 String?을 쓰지않고 논옵셔널 타입으로, 잘못된 구현을 해주고 있다는 것을 깨달았다.

"prepare() 메소드로 데이터가 제대로 전달이 안되는 경우"

  • 상황
    • prepare 메소드를 이용하여 Label의 값들을 넘겨주는 기능을 추가하다가 다음 화면에서 정상적으로 값이 전달되지않아 Label.text 값이 nil인 것을 확인했다. View가 load가 되어있지 않아서 값을 전달하는게 불가능 했다.
  • 시도
    • 첫번째 방법으로 임시로 값을 담아둘 프로퍼티를 전환할 Controller에 구현해주고 넘겨주려고 했다. 하지만 해당 방법은 프로퍼티를 여러개 생성해야되서 코드 가독성 측면에서 떨어진다고 생각이 들었다.
    • 두번째 방법으로는 View를 미리 load할 수는 없을까 찾아보다가 loadViewIfNeeded() 메소드를 찾게되어 해당 메소드를 호출 후에 Label 값을 전달해주니 정상적으로 다음 화면에서 Label의 값이 적용되었다.
  • 이유
    • 그러나 위의 방법은 View를 넘어가기전에 한번 더 load를 한다는 문제점과 두번째 화면의 속성값을 첫번째 화면에서 관리한다는 것이 문제가 되었다.
  • 해결
    • 두번째 화면의 값들은 해당 화면에서 관리를 할 수 있도록 전반적으로 코드를 수정해주었고, 화면이 넘어가는 과정에서는 JuiceMaker의 인스턴스만 넘겨줄 수 있도록 로직을 수정하여 해결하였다.

"button에 Dynamic Type 적용이 안되는 상황"

  • 상황

    • 보통 Label은 우측 Inspector에서 간단한 체크로 Automatically Adjusts Font를 설정해줄 수가 있는데 버튼의 titleLabel은 우측 Inspector에서 설정해줄 수가 없었다.
  • 이유와 해결

    • 찾아보니 코드로 적용하는 방법 뿐이였고, 버튼들 모두 실시간으로 Dynamic Type이 적용될 수 있도록 코드로 옵션을 활성화 해주었다.
    // MARK: - Setup Label and Button
    extension JuiceMakerViewController {
        func buttonLabelFontSizeFix() {
            orderStrawberryBananaJuiceButton.titleLabel?.adjustsFontForContentSizeCategory = true
            orderMangoKiwiJuiceButton.titleLabel?.adjustsFontForContentSizeCategory = true
            orderStrawberryJuiceButton.titleLabel?.adjustsFontForContentSizeCategory = true
            orderBananaJuiceButton.titleLabel?.adjustsFontForContentSizeCategory = true
            orderPineappleJuiceButton.titleLabel?.adjustsFontForContentSizeCategory = true
            orderKiwiJuiceButton.titleLabel?.adjustsFontForContentSizeCategory = true
            orderMangoJuiceButton.titleLabel?.adjustsFontForContentSizeCategory = true
    • 또 다른 해결 방법 위와 같이 직접 설정해주어도 되지만 코드 간결화를 주고싶어서 extension을 활용하여 코드를 수정해보았다.
    // 개선 후 코드
    extension UIButton {
        func setUpTitleLabelFontAttributes() {
            titleLabel?.adjustsFontForContentSizeCategory = true
            titleLabel?.adjustsFontSizeToFitWidth = true
        }
    }
    
    func setUpbuttonLabelFontAttributes() {
        orderStrawberryBananaJuiceButton.setUpTitleLabelFontAttributes()
        orderMangoKiwiJuiceButton.setUpTitleLabelFontAttributes()
        orderStrawberryJuiceButton.setUpTitleLabelFontAttributes()
        orderBananaJuiceButton.setUpTitleLabelFontAttributes()
        orderPineappleJuiceButton.setUpTitleLabelFontAttributes()
        orderKiwiJuiceButton.setUpTitleLabelFontAttributes()
        orderMangoJuiceButton.setUpTitleLabelFontAttributes()
    }

🔥 새롭게 알게된 것

"Sequence에 대해"

  • Dictionary(uniqueKeysWithValues:)zip 사용하면서 Sequence의 대한 정확한 개념에 대해서 알아보았다.
    • Sequence는 원소들을 순서대로 하나씩 순회할 수 있는 타입을 의미한다.
    • Sequence에는 range만 들어가는 줄 알았는데 Array도 넣을 수 있었다.
    • Array는 Sequence 프로토콜을 기반으로 작성되었다는 사실을 알았다. Array 타입을 사용할 때 Sequence의 대부분의 기능을 제공해준다. map, filter뿐만 아니라 Sequence 안에서 특정 조건을 만족하는 첫번째 요소를 찾는 기능 까지 모두 다 Sequence 프로토콜 안에 정의되어 있다.
    • Sequence는 두가지 중요한 특징이 있는데 무한하거나, 유한하다. 그리고 한번만 이터레이트(iterate)할 수 있다.

"연산자도 함수다!"

  • 함수 내에서 클로저를 파라미터로 받아 +, - 등 연산자 기호를 전달해줄 수 있다.
    • 알고보니 연산자(+, -)도 하나의 함수였다. static func + (lhs: Int, rhs: Int) -> Int
    • 연산자 기호를 파라미터로 전달할 수 있다. changeAmount(count: count, of: fruit, by: -)

"Human Interface Guidelines"

  • Human Interface Guidelines를 참고하여 Alert 버튼의 위치를 구성했다.
    • Yes, No의 사용은 하지 말라고 되어있다.
    • 단순 수락시 OK, 취소는 Cancel
    • Cancel 버튼은 왼쪽에 위치해야 한다.

"중첩타입의 용도 이해"

  • 맨 처음 Model을 구현할 때 당시에는 Fruit을 밖에서 사용하지 않는다고 생각하여 FruitStore 내부에 구현을 해주었다.
  • STEP 2를 구현하다보니 외부에서도 쓰이는 상황을 마주했다. 해결 내부로 다시 빼주는 작업을 하였고, 앞으로 설계할 때 정말 안에서만 쓰이는 타입인지 잘 고민하고 중첩 타입을 사용해야겠다는 큰 깨달음을 얻었다.

"Device Orientation"

  • 이 부분을 통해서 세로모드, 가로모드를 제어할 수 있다.

"topViewController와 visibleViewController?"

  • 위 그림을 보면 topViewController와 visibleViewController가 맨 앞에 같은 VC을 가르키고 있다.
  • 하지만 topViewController와 visibleViewController는 반드시 같은 것은 아니다.
  • 예를 들어, 하나의 VC를 모달창으로 나타낸다면 visibleViewController는 모달 VC을 가르킬 것이고, topViewController는 변하지 않는다.

top

[학습 기록 흔적]

목차

STEP 1 : 쥬스 메이커 타입 정의

  • 쥬스메이커 타입을 정의합니다.

1-1. 고민했던 것

  • 과일과 주스를 사용자 정의 타입 enum으로 구현하는 것
  • 초기 재고 10개를 어떻게 기본값으로 초기화 해줄 것인지에 대한 고민
    • Dictionaryinit(uniqueKeysWithValues:), zip
    • 코드 가독성을 위하여 전역변수 defaultFruitCount를 생성
  • 각 주스 제조에 필요한 과일의 갯수를 저장하는 방법에 대한 고민
    • 코드 내부 가독성을 위해 typealias를 활용하여 Recipe 를 정의해준 부분
    • Dictionary 를 활용하여 key에는 주스, value에는 재료가 들어가게 구성
  • Juice 타입을 파라미터로 받아서 레시피를 조회하여, 필요한 과일의 갯수를 재고에서 차감해주는 방법에 대한 고민
    • 레시피 내부에 과일 종류과일 갯수를 받아서 과일의 재고가 있는지 확인하는 것
    • 재고가 부족하다면 예외처리를 하도록 구현

1-2. 의문점

  • Nested Type을 이용하여 JuiceMaker 타입 내부에 Juice 타입을 작성하는 것이 좋은 방법인지 의문이 들었다. 내부에 타입을 작성을 해준 이유는 Juice타입을 타입 내부에서만 사용하고 있기 때문인데, 이게 적절한 방법인지 의문이 들었다. Juice 타입의 값을 사용하고자 할 때에 JuiceMaker.Juice.strawberry와 같은 형태로 길게 작성을 해야하는데, 그렇다면 바깥에 타입을 생성하는게 좋은걸까? 가독성 측면에서 어떤 것이 좋은 방법인지 의문이 들었다.

  • 예외처리를 해주었는데 do-catch를 활용하여 에러를 처리해주는 부분은 구현하지 않았다. 이유는 나중에 Controller에서 사용할 때 처리해주어야 한다고 생각했기 때문인데, Model에서 미리 구현을 해놔야하는건지 헷갈린다.

  • JuiceMaker 타입에 구현한 메소드 fruitsMixer(juice:) 코드를 아래와 같이 구현해주었다.

    // JuiceMaker 타입 내부의 메소드 fruitsMixer
    func fruitsMixer(juice: Juice) throws {
        guard let juiceRecipe = juiceRecipes[juice] else { return }
        guard canMakeJuice(recipe: juiceRecipe) else {
            throw RequestError.fruitStockOut
        }
        try juiceRecipe.forEach { (fruit, count) in
            try fruitStore.subFruitStock(fruit: fruit, count: count)
        }
    }

    canMakeJuice(recipe:)를 이용해 주스를 만들 수 있는지 미리 검증해주고 있고, 해당 코드의 아래에서 사용하는 FruitStore 타입의 subFruitStock(fruit:count:) 메소드에서도 주스를 만들 수 있는지 검증해주고 있다. 즉 이중으로 검증하고 있는 것인데 불필요한 작업이 아닌가 의문이 든다.

1-3. Trouble Shooting

  • Error 메세지를 출력하려고 코드를 작성하면서 알아냈던 방법들

    1. enum안에 static 키워드를 활용하여 메서드 작성
    2. static 키워드를 없애고 as로 다운캐스팅하여 사용
    enum RequestError: Error {
        case wrongInput
        case notFound
        case fruitStockOut
        
        func printErrorMessage() {
            switch self {
            case RequestError.wrongInput:
                print("수량을 잘못 입력하였습니다.")
            case RequestError.notFound:
                print("선택한 과일이 존재하지 않습니다.")
            case RequestError.fruitStockOut:
                print("과일의 재고가 부족합니다.")
            }
        }
    }
    do {
        try maker.fruitsMixer(juice: .strawberryBanana)
    } catch let error as RequestError {
        error.printErrorMessage()
    }
    1. LocalizedError 프로토콜을 채택하여 errorDescription 프로퍼티 작성하여 활용 (현재)
    enum RequestError: Error, LocalizedError {
        case wrongCount
        case fruitNotFound
        case fruitStockOut
        
        var errorDescription: String {
            switch self {
            case RequestError.wrongCount:
                return "수량을 잘못 입력하였습니다."
            case RequestError.fruitNotFound:
                return "선택한 과일이 존재하지 않습니다."
            case RequestError.fruitStockOut:
                return "과일의 재고가 부족합니다."
            }
        }
    }
  • Dictionary(uniqueKeysWithValues:)zip 사용하면서 Sequence의 대한 정확한 개념에 대해서 알아보았다.

    • Sequence는 원소들을 순서대로 하나씩 순회할 수 있는 타입을 의미한다.
    • Sequence에는 range만 들어가는 줄 알았는데 Array도 넣을 수 있었다.
    • ArraySequence 프로토콜을 기반으로 작성되었다는 사실을 알았다. Array 타입을 사용할 때 Sequence의 대부분의 기능을 제공해준다. map, filter뿐만 아니라 Sequence 안에서 특정 조건을 만족하는 첫번째 요소를 찾는 기능 까지 모두 다 Sequence 프로토콜 안에 정의되어 있다.
    • Sequence는 두가지 중요한 특징이 있는데 무한하거나, 유한하다. 그리고 한번만 이터레이트(iterate)할 수 있다.
  • 각 주스를 만드는 데에 필요한 과일의 수를 저장하는 방법에 대해 고민해보았다.

    • 초기에 작성한 코드는 아래와 같은 형태로 switch문을 이용해 만들어줄 주스에 따라 하드코딩으로 필요한 과일의 수를 넣어주었다.

      func fruitsMixer(juice: Juice) {
          switch juice {
          case .strawberry:
              ...
          case .banana:
              ...
          case .kiwi:
              ...
          case .pineapple:
              ...
          case .strawberryBanana:
              ...
          case .mango:
              ...
          case .mangoKiwi:
              ...
          }
      }
    • 위에서 작성한 fruitsMixer(juice:)의 코드를 더 간결하게 작성하고 싶었고 Dictionary를 이용하여 해당 문제를 해결하였다.

      typealias Recipe = [FruitStore.Fruit: Int]
      
      private let juiceRecipes: [Juice: Recipe] = [
          Juice.strawberry: [.strawberry: 16],
          Juice.banana: [.banana: 2],
          Juice.kiwi: [.kiwi: 3],
          Juice.pineapple: [.pineapple: 2],
          Juice.strawberryBanana: [.strawberry: 10, .banana: 1],
          Juice.mango: [.mango: 3],
          Juice.mangoKiwi: [.mango: 2, .kiwi: 1]
      ]
      
      func fruitsMixer(juice: Juice) throws {
          guard let juiceRecipe = juiceRecipes[juice] else { return }
        ...
        try juiceRecipe.forEach { (fruit, count) in
            try fruitStore.subFruitStock(fruit: fruit, count: count)
        }
      }

1-4. 배운 개념

  • 에러 메세지를 출력할 때 사용할 수 있는 다양한 방법
  • Nested Type을 적절하게 활용하는 방법
  • typealias를 활용하여 가독성을 높이는 방법
  • 함수 내에서 클로저를 파라미터로 받아 +, - 등 연산자 기호를 해당 함수에 전달하여 사용하는 방법
    • 연산자(+, -)도 하나의 함수였다. static func + (lhs: Int, rhs: Int) -> Int
    • 연산자 기호를 파라미터로 전달할 수 있다. changeAmount(count: count, of: fruit, by: -)
  • Sequence의 대한 개념
  • 코딩 컨벤션을 정할 때 이유와 근거를 생각해보는 방법
  • 전역변수를 사용할 때 다른 타입에서 접근할 수는 없는지 고려해보자.

1-5. PR 후 개선사항

  • JuiceMaker 내부에 있던 프로퍼티 RecipeJuice 내부로 이동 후 switch문으로 변경
  • enum Juice를 타입 외부로 이동
    • 이유 Juice를 외부로에서도 사용
  • JuiceMaker 기본값 프로퍼티를 제거하고 initializer 구현
  • fruitMixer 메서드 이름을 mixFruit으로 수정
  • subFruitStock 메서드명을 subtractFruitStock 으로 수정

top

STEP 2 : 초기화면 기능구현

화면에 구현되어있는 버튼들의 기능을 추가합니다.

2-1. 고민했던 것

  • Singleton 패턴을 활용
  • 화면 이동시 이동 방식에 대한 고민
    • 이유 네비게이션을 따라서 화면을 이동하는 방식이 적절하지 못하다고 생각했기 때문이다. 임시적으로 화면에 들어가서 재고를 수정하는 용도의 View라는 생각이 들었다. 따라서 Push 보다는 Present가 적합하다고 생각했다.
  • Human Interface Guidelines에 따른 Alert 버튼 구성
    • Yes, No의 사용은 하지 말라고 되어있다.
    • 단순 수락시 OK, 취소는 Cancel
    • Cancel 버튼은 왼쪽에 위치해야 한다.
  • Alert을 메서드에 구현하여 재사용성을 고려
  • 재고 수량이 바뀔 때마다 화면에 반영시키는 방식
    • NotificationCenter를 활용
  • 하드코딩 개선방법

2-2. 의문점

  • 프로토콜을 사용자 정의 타입에 채택 후 프로토콜이 정의한 프로퍼티가 아니라 이름은 같지만 타입이 다른 프로퍼티를 구현해주었다. 이후 값을 전달하는 과정에서 타입이 정의한 프로퍼티가 아니라 프로토콜의 프로퍼티가 자꾸 전달되었는데 이유를 모르겠다.
  • 프로젝트 요구사항이 HIG 지침을 지키지 않았다.
  • FruitStore 내부에 선언되어있는 Fruit타입을 외부로 빼줘야할까?
  • Navigation Controller가 두개가 구현이 되어있다. 하나만 있어도 되지 않나?
  • UI 관련 타입들을 네이밍할때 끝에 Label이나 Button을 넣어주는 것이 널리 쓰이는 방법같은데 적절한걸까?

2-3. Trouble Shooting

1. Navigation Controller가 두개로 구현되어있는 이유

  • 이유 Navigation Bar를 이용하기 위함이였다. 쥬스메이커 메인화면에서 재고수정하기 버튼을 터치하게 되면 화면 이동방식은 modal이 되어야 한다고 생각했다. 이유는 네비게이션을 따라서 화면을 이동하는 방식이 적절하지 못하다고 생각했기 때문이다. 임시적으로 화면에 들어가서 재고를 수정하는 용도의 View라는 생각이 들었다. 해결 메인 화면을 재고수정이 구현되어있는 ViewController가 아니라 재고수정 화면에 연결되어있는 Navigation ControllerSegue를 연결해서 modal을 구현하고, bar button을 활용하여 Cancel 버튼을 구현해주었다.

2. 이름은 같지만 타입이 다른 타입

  • 상황 프로토콜(LocalizedError)을 사용자 정의 타입(RequestError)에 채택 후 프로토콜이 정의한 프로퍼티가 아니라 이름은 같지만 타입이 다른 프로퍼티를 구현해주었다. 이후 파라미터로 값을 전달하는 과정에서 타입이 정의한 프로퍼티가 아니라 프로토콜에서 기본 구현이 된 프로퍼티가 전달되었다. 이유 같은 이름이지만 타입이 다른 두 프로퍼티가 공존하고 있을 때, 파라미터로 전달할 때에는 타입이 일치하는 프로퍼티가 들어갔다. 해결 예를 들어 함수의 파라미터 후보로 StringString? 두가지가 있고, 파라미터의 타입은 String? 이라면 컴파일러는 당연히 String?을 전달해주려고 할 것이다. 다만 최후의 후보가 String 뿐이라서 String? 자리String을 전달하는 경우 String을 String?으로 포장해줄 수는 있겠다. 당시에는 왜 String을 전달하고 있는데 왜 nil이 전달되는 것인지 이해가 가지 않았었는데, 그 의문을 질문을 통해서 해결하였다. 이후 LocalizedError 프로토콜을 사용할 이유가 없어져서 채택한 것을 없애주고 해결하였다.

3. 중첩타입의 활용

  • 상황 맨 처음 Model을 구현할 때 당시에는 Fruit을 밖에서 사용하지 않는다고 생각하여 FruitStore 내부에 구현을 해주었다. 이유 STEP 2를 구현하다보니 외부에서도 쓰이는 상황을 마주했다. 해결 내부로 다시 빼주는 작업을 하였고, 앞으로 설계할 때 정말 안에서만 쓰이는 타입인지 잘 고민하고 중첩 타입을 사용해야겠다는 큰 깨달음을 얻었다.

2-4. 배운 개념

  • MVC 패턴

  • Navigation bar를 이용하여 title, button 아이템 활용방법

  • Alert을 만드는 방법

  • NotificationCenter를 활용하여 ModelController에게 데이터 변화를 알려주는 방법

  • Singleton 패턴을 활용하여 Model의 데이터를 View에서 활용

  • 직접 구현한 화면(ViewController)이 아니라 Navigation View ControllerSegue를 연결하면 modal 방식으로 화면이 이동되면서 동시에 Navigation bar buttonNavigation bar title을 사용할 수 있다.

  • Device Orientation

    • 이 부분을 통해서 세로모드, 가로모드를 제어할 수 있다.

  • 가로모드에서는 모달창을 위에서 아래로 내리는 제스처를 사용할 수 없다. (세로모드만 가능한 제스처)

  • 변수들을 따로 모아서 enum타입으로 구현하고 static 변수로 구현하여 별도로 관리하는 방법

  • Bundle을 활용하는 방법

  • extension으로 코드를 분리하여 가독성을 향상시키는 방법

2-5. PR 후 개선사항

  • 문자열을 따로 enum 타입으로 분리하여 관리하도록 개선
  • String init으로 형변환했던 Int값을 description 프로퍼티를 활용하여 개선
  • 어색한 용도로 쓰고있던 CustomStringConvertible 프로토콜을 제거
  • Alert Action Button의 title을 Bundle을 활용하여 다국어를 지원하도록 개선
  • 전역변수로 선언되어있던 FruitStore init의 기본값을 enum 타입으로 구현하여 static 변수로 구현
  • Alert 메소드 내부에 있던 handler 클로저를 분리하여 가독성 개선
  • 오버스펙으로 구현된 부분 삭제

top

STEP 3 : 재고 수정 기능구현

재고 추가 화면의 기능을 구현합니다.

3-1. 고민했던 것

  • 재고 수정 화면으로 전환시 재고 관련 Label들이 업데이트 되는 부분을 고민했다. 따로 재고수정 화면에서 재고를 조회하고 Label에 반영해주는 방법도 있겠지만 같은 일을 두번 반복하는 것이 좋지않아 보였고, JuiceMakerViewController의 Label의 값들을 다음 화면에다가 넘겨주면 좋겠다고 생각하여 prepare메소드를 이용하여 넘겨주는 방식을 선택했다.
  • Controller 내부 가독성을 어떻게 하면 향상 시킬 수 있는지 고민했다. extension을 이용하여 Controller를 분리해보았다. 실제로 가독성도 좋아지는 것 같은 효과[?]를 보았다.
  • 초기에는 Stepper가 터치될 때 (Event가 Touch Up Inside일 때) FruitStore의 과일 갯수 저장 프로퍼티 값이 바뀌도록 IBAction Method 메소드를 작성해 구현해보았다. 해당 방식으로 구현하면 Stepper의 값이 변화하지 않았을 때에도 IBAction Method를 사용하게 되어 메소드 내부에서 수동으로 값의 변화를 체크해야 했다. 자동으로 값의 변화를 체크해주는 방법을 애플에서 제공해주지 않을까 생각이 들었고 Event를 Touch Up Inside에서 Value Changed로 바꾸어주어 해당 방식을 선택했다.
  • LabelButtonDynamic Type 크기에 따라 커지고 작아질 때마다 실시간으로 반영될 수 있도록 adjustsFontForContentSizeCategory 옵션을 true로 주었다.
  • LabelButtonDynamic Type 크기에 따라 폰트 크기가 변화할 때마다 글씨가 잘리는 현상을 발견했다. 그래서 버튼 크기 너비에 맞게 크고 작아질 수 있도록 AdjustsFontSizeToFitWidth 옵션을 true로 주었다.

3-2. 의문점

  • 화면 전환 시 Label을 넘겨줄 때 prepare 메소드 내부에서 loadViewIfNeeded 메소드를 이용하여 다음 View를 미리 load 해주고 있다. 이게 적절한 타이밍에 사용했다고 생각하긴 하는데, 정말 적절한건지 모르겠다.
    • 해당 의문점은 리뷰어인 흰에게 조언을 구했고, View를 불러오기 전에 한번 더 load 하는 것이기 때문에 메모리적인 문제가 발생하여 적절하지 못하다는 리뷰를 받았다.
  • present 메소드를 통해 특정 ViewController를 띄어준 뒤 ViewController의 view 프로퍼티에 접근하여 값을 수정하였을 때 UI가 반영되었다. present 메소드 사용 이전에 ViewController의 UI 관련 값을 설정하고 present를 해주어야하는 줄 알았는데 그렇지 않아 의외였다.

3-3. Trouble Shooting

1. ViewController 내부 코드 가독성 향상에 대하여

  • 상황 Controller의 내부가 길어지고 많아져서 가독성을 향상 시킬 수 있는 방법이 없을까 하다가 extension을 알게되었다.

  • 해결 코드 배치 순서를 활용하여 가독성을 높혀주는 방법도 있겠지만 extension을 이용하여 메서드들을 분리해주었더니 위와 같이 조금은 읽기 수월해진 코드가 되었다.

2. prepare() 메소드로 데이터가 제대로 전달이 안되는 경우

  • 상황 prepare 메소드를 이용하여 Label의 값들을 넘겨주는 기능을 추가하다가 다음 화면에서 정상적으로 값이 전달되지않아 Label.text 값이 nil인 것을 확인했다. View가 load가 되어있지 않아서 값을 전달하는게 불가능 했다.
  • 시도 첫번째 방법으로 임시로 값을 담아둘 프로퍼티를 전환할 Controller에 구현해주고 넘겨주려고 했다. 하지만 해당 방법은 프로퍼티를 여러개 생성해야되서 코드 가독성 측면에서 떨어진다고 생각이 들었다.
  • 시도 두번째 방법으로는 View를 미리 load할 수는 없을까 찾아보다가 loadViewIfNeeded() 메소드를 찾게되어 해당 메소드를 호출 후에 Label 값을 전달해주니 정상적으로 다음 화면에서 Label의 값이 적용되었다.
  • 해결 그러나 위의 방법은 View를 넘어가기전에 한번 더 load를 한다는 문제점과 두번째 ViewController의 속성값을 첫번째 ViewController에서 관리한다는 것이 문제가 되었다. 두번째 ViewController의 값들은 해당 ViewController에서 관리를 할 수 있도록 전반적으로 코드를 수정해주었고, 화면이 넘어가는 과정에서는 JuiceMaker인스턴스만 넘겨줄 수 있도록 로직을 수정하여 해결하였다.

3. button에 Dynamic Type 적용이 안되는 경우

  • 상황 보통 Label은 우측 Inspector에서 간단한 체크로 Automatically Adjusts Font를 설정해줄 수가 있는데 버튼의 titleLabel은 우측 Inspector에서 설정해줄 수가 없었다.

  • 이유와 해결 찾아보니 코드로 적용하는 방법 뿐이였고, 버튼들 모두 실시간으로 Dynamic Type이 적용될 수 있도록 코드로 옵션을 활성화 해주었다.

    // MARK: - Setup Label and Button
    extension JuiceMakerViewController {
        func buttonLabelFontSizeFix() {
            orderStrawberryBananaJuiceButton.titleLabel?.adjustsFontForContentSizeCategory = true
            orderMangoKiwiJuiceButton.titleLabel?.adjustsFontForContentSizeCategory = true
            orderStrawberryJuiceButton.titleLabel?.adjustsFontForContentSizeCategory = true
            orderBananaJuiceButton.titleLabel?.adjustsFontForContentSizeCategory = true
            orderPineappleJuiceButton.titleLabel?.adjustsFontForContentSizeCategory = true
            orderKiwiJuiceButton.titleLabel?.adjustsFontForContentSizeCategory = true
            orderMangoJuiceButton.titleLabel?.adjustsFontForContentSizeCategory = true
  • 또 다른 해결 방법 위와 같이 직접 설정해주어도 되지만 코드 간결화를 주고싶어서 extension을 활용하여 코드를 수정해보았다.

    // 개선 후 코드
    extension UIButton {
        func setUpTitleLabelFontAttributes() {
            titleLabel?.adjustsFontForContentSizeCategory = true
            titleLabel?.adjustsFontSizeToFitWidth = true
        }
    }
    
    func setUpbuttonLabelFontAttributes() {
        orderStrawberryBananaJuiceButton.setUpTitleLabelFontAttributes()
        orderMangoKiwiJuiceButton.setUpTitleLabelFontAttributes()
        orderStrawberryJuiceButton.setUpTitleLabelFontAttributes()
        orderBananaJuiceButton.setUpTitleLabelFontAttributes()
        orderPineappleJuiceButton.setUpTitleLabelFontAttributes()
        orderKiwiJuiceButton.setUpTitleLabelFontAttributes()
        orderMangoJuiceButton.setUpTitleLabelFontAttributes()
    }

4. NavigationViewController에 연결되어있는 ViewController를 가져오기

  • 상황 NavigationViewController를 생성해서 Label에 접근해서 값을 전달해주려고 했으나 접근할 수가 없었다.
  • 이유 NavigationViewController는 실제로는 뷰가 존재하지 않기 때문에 연결되어있는 ViewController를 가져와야 했다.
  • 해결 topViewController라는 프로퍼티를 이용하여 ViewController를 생성해주고, 타입캐스팅으로 FruitStoreViewController를 가져와서 Label에 접근하는데에 성공했다.
    private func presentFruitStoreViewController(_ action: UIAlertAction) {
            guard let viewController = self.storyboard?.instantiateViewController(withIdentifier: "FruitStoreViewController") as? UINavigationController else { return }
            self.present(viewController, animated: true, completion: nil)
            guard let nextViewController = viewController.topViewController as? FruitStoreViewController else { return }
            setupNextViewLabel(of: nextViewController)
        }

3-4. 배운 개념

  • 화면 사이의 데이터 공유하는 여러가지의 방법
  • UIControl.Event
  • UIStepper
  • Auto Layout
    • Dynamic Type
    • Content Priority
  • prepare 메소드 활용법
  • 지정된 식별자를 가지고 스토리보드의 데이터를 초기화해서 ViewController를 가져오는 방법
  • Navigation View Controller의 스택의 맨 위에 있는 ViewController를 가져오는 방법
  • 상위 클래스를 하위 클래스로 형변환하여 사용하는 방법
    guard let navigationController = segue.destination as? UINavigationController else { return }
    guard let nextViewController = navigationController.topViewController as? FruitStoreViewController else { return }
    nextViewController.strawberryStockLabel.text = strawberryStockLabel.text

3-5. PR 후 개선사항

  • 메소드명이 동사로 시작하도록 개선
  • Singleton을 제거
  • 화면간 데이터 전달 방식을 NotificationCenter를 활용하는 것으로 전체적인 수정
  • extension을 활용하여 코드를 간결화

top

About

iOS 쥬스 메이커 재고관리 시작 저장소

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Languages

  • Swift 100.0%