티스토리 뷰

정성스러운 피드백을 받고 하나하나 고쳐보려고 노력했다.

 

1. 폴더 및 파일 이름 수정

 

 

Scene끼리는 한 번 더 묶을 수 있을 것 같고, Struct라는 이름대신 Entity나 Model이라는 이름을 사용하면 좋을 것 같다.

Entity란 "저장되고 관리되어야하는 데이터의 집합" - Zedd님 블로그

 

 

2. Storyboard init

 

 

자주 사용하는데 반복사용을 줄일 수 있지 않을까.

 

계속해서 let sb = UIStoryboard(name: "", bundel: nil)을 반복해서 사용해왔다.

 

뷰컨트롤러선언까지 한 번에 함수로 만들고싶었지만 타입 캐스팅을 할 때 서로 다른 클래스로 하는거라 매개변수로 받을 수가 없었다.

 

아래처럼 선언해서 조금 줄일 수 있었음.

아래에 문자열 하드코딩도 enum을 이용해 수정하였음.

 

 

 

3. 불필요한 스토리보드 파일 지우기

 

 

원래 CollectionViewController를 사용하다가 ViewController에 CollectionView를 추가하는 방향으로 수정했는데, 이전의 컨트롤러가 남아있었다.

 

어차피 필요없기때문에 있어서 좋은 점이 전혀 없다.

 

 

 

4. 뷰컨트롤러마다 identity라는 타입 프로퍼티를 생성해줬는데, 공통으로 사용하는 방법

 

 

프로토콜을 선언하여 뷰컨트롤러마다 채택해주었다.

protocol Identity {

    static var identity: String { get }

}

채택한 뒤, describing을 이용하여 문자열 사용을 최소화해주었다.

 

 

 

5. 뷰컨트롤러에 필요한 세팅을 할 때, 메서드로 해보기

 

 

짧은 코드라도 메서드로 만들어서 하는게 좋을까 생각해서 안했는데 생각해보니 메서드로 관리하는게 나중에 규모가 커졌을때 유지보수하기 쉬울 것 같음.

 

아래처럼 모든 세팅을 메서드로 만들어줌.(다른 화면도 다 함)

 

 

 

6. 셀의 UI를 세팅

 

 

dequeueReusableCell은 셀을 재사용하겠다는 의미이다.

 

그래서 셀의 UI를 세팅하는 메서드를 cellForItemAt에서 호출한다면 재사용될때마다 셀의 UI를 세팅하는 것이다.

 

내가 사용하는 셀들은 전부 같은 레이아웃 형태를 가지고있기때문에 굉장히 비효율적이다.

 

그래서 awakeFromNib() 안에 작성하기로했다.

 

awakeFromNib는 객체가 초기화되면 호출되는 메서드이다.

-> 호출될때, Outlet들과 Action들이 모두 연결된 상태

 

setCell()메서드를 아래와 같이 호출하고 print로 확인해보면 재사용될때에도 setCell()이 호출된다.

 

이렇게 Cell의 클래스 내의 awakeFromNib()에서 setCell()을 호출하면, 모든 셀에서 한 번씩만 호출되고 재사용될때는 호출되지않음.

 

앞으로 레이아웃과 관련된 코드는 awakeFromNib()에 작성해야할듯.

 

마찬가지로 설정화면에서도 awakeFromNib쪽에 작성해주었다.

 

 

7. 의미없는 옵셔널

 

 

뷰컨트롤러에서 반드시 사용해야하는 값인데 옵셔널로 선언한 인스턴스가 있었다.

 

옵셔널을 푸는데 코드도 추가돼서 일을 두 번이나 했다.

 

 

 

8. 변수명은 줄여쓰지말기

 

 

나는 ImageView도 Img로 선언하고 Button도 btn으로 선언하는 버릇이있다.

 

지금이야 규모가 작고 변수도 적어서 상관없지만 커지게되면 어떤 것을 의미하는지 모호해질 수도 있다.

 

길다고 줄여쓰지말자.

 

Img와 Btn을 전부 ImageView와 Button으로 바꿔주었다.

 

 

 

9. 앱 전반적으로 문자열을 하드코딩해서 가져오고있다.

 

 

enum을 이용해 하드코딩을 줄여보았다.

위의 열거형을 이용해 이전에 "Main"으로 직접 쓰던 것을 Storyboard.main.rawValue으로 수정했다.

let sb = storyboardInit(StoryboardName.detail.rawValue)

-> 솔직히 이런식으로 바꾸는것이 맞는 방향인지 잘 모르겠어서 다시 한 번 확인해볼예정

 

폰트에서도 반복해서 하드코딩을 하고있었기에 동일하게 열거형으로 관리하였다.

enum CustomFont: String {
    case bold = "MICEGothic OTF Bold"
    case regular = "MICEGothic OTF"
}
nameLabel.font = UIFont(name: CustomFont.bold.rawValue, size: 13)

 

또 UserDefaults의 key들도 매번 문자열을 반복적으로 사용하였다. 

 

마찬가지로 열거형을 이용해 바꿔주었다.

enum UserDefaultsKey: String {
    case name = "name"
    case tamagotchi = "tamagotchi"
    case status = "status"
}
userDefaults.setValue(encoded, forKey: UserDefaultsKey.status.rawValue)

 

이미지 이름에 대해서도 마찬가지였다.

enum ImageName: String {
    case person = "person.circle"
    case drop = "drop.circle"
    case leaf = "leaf.circle"
    case pencil = "pencil"
    case moon = "moon.fill"
    case arrow = "arrow.clockwise"
}
waterButton.setImage(UIImage(systemName: ImageName.leaf.rawValue), for: .normal)

 

 

10. willShowNotification

 

 

willShowNotification은 키보드가 나타나는지만 알려주는 코드이므로, 만약 툴바같은게 나온다면 해결이 어려울 수도 있다.

 

그래서 willShowNotification대신 keyboardWillChangeFrameNotification를 이용하였다.

 

keyboardWillChangeFrameNotification은 키보드에 어떠한 변화가 오더라도 호출된다.

(showing, hiding, orientation, QuickType..)

 

그렇다면 willHide를 쓰지 않고 둘 다 keyboardWillChangeFrameNotification을 써도 되지않을까해서 해봤는데, 제대로 동작하지않았다.

 

그래서 키보드가 나타날때는 keyboardWillChangeFrameNotification, 사라질때는 keyboardWillHideNotification을 이용해야겠다.

NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillChange(_:)), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil)

 

 

11. 키보드 사이즈에 따른 뷰 offset변화

 

 

다양한 디바이스를 확인하면서 -190이면 적당하겠다라는 생각으로 값을 설정했다.

self.navigationController?.view.frame.origin.y = -190

 

근데 190이라는 수치는 디바이스별로 위치하는 곳이 다르기때문에 좋은 방법은 아니라고 하셨다.

 

Notification중에 키보드의 사이즈 변화를 알려주는 것도 있다고하셔 적용시켜봄.

@objc func keyboardWillChange(_ sender: Notification) {
        
    guard let keyboardFrame = sender.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else { return }
    let keyboardHeight = keyboardFrame.cgRectValue.size.height
    let waterBottomSpace = view.frame.height - waterOuterView.frame.origin.y
       
    if self.navigationController?.view.frame.origin.y == 0 {
       self.navigationController?.view.frame.origin.y = -(keyboardHeight - waterBottomSpace + 80)
    }        
}

 

keyboardFrameEndUserInfoKey에서 리턴되는 값에서 height를 구하고 물주기 뷰의 bottomSpace를 계산해서 80정도의 여유만 갖도록 해주었다.

 

물론 80이라는 값도 직접 준 값이지만 키보드의 높이를 구하고 계산한 것이기때문에 괜찮을 것 같았다.

 

 

12. 밥주기, 물주기 버튼 메서드

 

 

흐름이 똑같은데 아래의 코드가 반복되므로 하나로 묶어봄

@IBAction func tapWaterButton(_ sender: UIButton) {
        
        // stateLabel(물방울 개수) 텍스트필드 조건 확인
        if waterTextField.text == "" {
            currentStatus.water += 1
        } else {
            if let waterText = waterTextField.text {
                if Int(waterText) != nil {
                    if Int(waterText)! >= 100 || Int(waterText)! <= 0{
                        showAlert(title: "너무 많잖아요..")
                    } else {
                        currentStatus.water += Int(waterText)!
                    }
                } else {
                    showAlert(title: "숫자만 입력해야합니다.")
                }
            }
        }
        // 이미지 세팅. 레벨을 연산프로퍼티에 의해 자동으로 연산
        currentStatus.typeNumber = "\(tamaData.number)"
        profileImageView.image = UIImage(named: currentStatus.profileImg)
        
        // 메세지 레이블
        messageLabel.text = messages.randomElement()
        
        // 상태 레이블 새로고침 및 저장 -> 마찬가지로 아카이빙해서 저장
        let encoder = JSONEncoder()
        if let encoded = try? encoder.encode(Status(food: currentStatus.food, water: currentStatus.water)) {
            userDefaults.setValue(encoded, forKey: UserDefaultsKey.status.rawValue)
            
        }
        
        statusLabel.text = currentStatus.statusLabel
        
        // 텍스트필드 비워주기
        waterTextField.text = ""

        // 키보드내리기
        view.endEditing(true)
    }

 

태그를 사용해 아래처럼 메서드를 만듦

    func buttonMethod(textField: UITextField, button: UIButton) {
        if textField.text == "" {
            if button.tag == 0 {
                currentStatus.water += 1
            } else if button.tag == 1 {
                currentStatus.food += 1
            }
        } else {
            if let text = textField.text {
                if Int(text) != nil {
                    if Int(text)! >= 100 || Int(text)! <= 0{
                        showAlert(title: "너무 많잖아요..")
                    } else {
                        if button.tag == 0 {
                            currentStatus.water += Int(text)!
                        } else if button.tag == 1 {
                            currentStatus.food += Int(text)!
                        }
                    }
                } else {
                    showAlert(title: "숫자만 입력해야합니다.")
                }
            }
        }
        currentStatus.typeNumber = "\(tamaData.number)"
        profileImageView.image = UIImage(named: currentStatus.profileImg)
        
        messageLabel.text = messages.randomElement()
        
        let encoder = JSONEncoder()
        if let encoded = try? encoder.encode(Status(food: currentStatus.food, water: currentStatus.water)) {
            userDefaults.setValue(encoded, forKey: UserDefaultsKey.status.rawValue)
            
        }
        
        statusLabel.text = currentStatus.statusLabel
        
        // 텍스트필드 비워주기
        textField.text = ""

        // 키보드내리기
        view.endEditing(true)
    }

 

아래처럼 간단하게 메서드로 표현할 수 있었다.

 

여기서도 tag를 이용하면 하나의 액션 안에서도 해결 가능하다.

    //밥주기 버튼
    @IBAction func tapFoodButton(_ sender: UIButton) {

        buttonMethod(textField: foodTextField, button: sender)
    
    }
    
    // 물주기 버튼
    @IBAction func tapWaterButton(_ sender: UIButton) {
        
        buttonMethod(textField: waterTextField, button: sender)
    }
    @IBAction func tapButton(_ sender: UIButton) {

        if sender.tag == 0 {
            buttonMethod(textField: waterTextField, button: sender)
        } else {
            buttonMethod(textField: foodTextField, button: sender)
        }
    }

 

 

13. 밥주기, 물주기 버튼 UI 세팅

 

 

이렇게 메인뷰의 UI를 세팅하는 메서드 안에 비슷한 밥주기, 물주기 버튼 관련 코드가 반복되고있다.

 

이를 따로 메서드로 빼서 매개변수로 코드를 줄여보았따.

 

아래처럼 수정하였다.

func setUpButtonUI(button: UIButton, outerView: UIView, lineView: UIView, textField: UITextField, placeholder: String, buttonTitle: String, buttonImage: String) {
    outerView.backgroundColor = .sesacBackground
    lineView.backgroundColor = .sesacBorder
    textField.placeholder = placeholder
    textField.backgroundColor = .sesacBackground
    button.backgroundColor = .sesacBackground
    button.layer.cornerRadius = 5
    button.layer.borderWidth = 0.5
    button.layer.borderColor = UIColor.sesacBorder.cgColor
    button.setImage(UIImage(systemName: buttonImage), for: .normal)
    button.setTitle(buttonTitle, for: .normal)
    button.titleLabel?.font = UIFont(name: CustomFont.bold.rawValue, size: 13)
    button.setTitleColor(.sesacBorder, for: .normal)
}

 

 

14. 타입추론을 이용하자.

 

 

나는 웬만해서는 Type Annotation을 해주는 것이 좋은 줄 알았기에 거의 매번 같이 적어주었다.

 

하지만 타입을 명시하지 않는 것이 더 빠른 컴파일 속도를 얻을 수 있다고 한다.

 

위와 같이 연산프로퍼티처럼 특별한 경우가 아니라면 전부 type annotation을 없애주었다.

 

 

15. 레벨 계산 switch

 

 

원래 아래처럼 쓸데없이 스위치로 모든 경우를 다 따져주었는데 생각해보니 가운데는 10으로 나누면 레벨이 나온다.

그래서 아래처럼 바꿔주었다.

 

 

16. Int 범위

 

 

물이나 밥주기 텍스트필드에 9999999....999같이 긴 수를 넣으면 범위를 벗어나 문자로 인식한다.

 

사용자에겐 긴 숫자일뿐이므로 "숫자만 입력되어야합니다"라는 알림은 잘못되었따.

 

댓글
최근에 올라온 글
Total
Today
Yesterday