Skip to content

Conversation

@doshkor
Copy link

@doshkor doshkor commented Jan 27, 2023

@protocorn93

[써니]
안녕하세요 콘☺️ 설날이 껴있고, 프로젝트가 저희 생각보다 어려워서 그런지 유난히 힘들었던 STEP3 였어요🥲 일단 요구사항에 적혀진 기능들이 돌아갈 수 있도록 코드를 구현하였지만 깔끔히 코드를 작성하지는 못한것 같아 매우 아쉬워요ㅠㅠ 주말에 조금 더 시간을 내서 더 나은 코드를 작성해볼게요!

[디오]
콘! 안녕하세요🙋
STEP 1 부터 앞선 프로젝트와 달리 UML을 제시받고 진행하다 보니 많이 헤매게 되었던 프로젝트였던거 같아요.😭 STEP 3 에서는 써니와 함께 고민한 부분이 많았는데, 만족할 만큼 완성하지 못해 너무 아쉽게 느껴져요. 정성스러운 리뷰 너무 감사드리고 다음에 또 뵈었으면 좋겠어요. 감사합니다🤗

구현 사항

숫자

  • 사용자에게 표시하는 숫자는 뒤에 0000 등 불필요한 숫자가 나타나지 않도록 합니다
  • 숫자는 최대 20자리까지만 표현합니다
  • 잘리는 숫자는 반올림합니다
  • 숫자는 3자리마다 쉼표(,)를 표기해줍니다
  • 0으로 나누기에 대해서는 결과값을 NaN으로 표기합니다

연산

  • AC는 모든 연산내역을 초기화합니다
  • CE는 현재 입력하던 숫자 혹은 연산결과만 삭제합니다
  • ⁺⁄₋ 버튼은 현재 입력한 숫자의 부호를 변환합니다
    • 입력된 숫자가 0인경우 부호를 표시하지 않고 변경하지도 않습니다
  • 숫자입력 중에 연산자(÷, ×, -, +)를 누르게 되면 숫자입력을 중지하고 다음 숫자를 입력받습니다
  • 현재 숫자입력이 없는 상태인 0에서는 연산자를 반복해서 누르더라도 연산이 이뤄지지 않습니다
  • 현재 숫자입력이 없는 상태인 0에서는 연산자의 종류만 변경합니다
  • = 버튼을 누르면 입력된 연산을 한 번에 수행합니다
    • = 버튼을 누르기 전에는 실제 연산을 수행하지 않습니다
    • 연산은 연산자 우선순위를 무시하고 앞에서부터 순서대로 계산합니다

UI

  • 계산내역이 상단 공간을 넘어 이어지는 경우, 사용자에게 제대로 보일 수 있도록 새로 추가될 때 최하단으로 자동 스크롤 할 수 있도록 합니다

구현 세부사항 및 고민

스택 추가시 자동스크롤 하는 코드

private func moveToPoint(labelHeight: CGFloat) {
    var bottomOffset = CGPoint(x: 0, y: 0)
        
    if scrollView.contentSize.height > scrollView.visibleSize.height {
        bottomOffset.y = scrollView.contentSize.height - scrollView.visibleSize.height + labelHeight
    }
    scrollView.setContentOffset(bottomOffset, animated: true)
}

스크롤 뷰의 visibleSize 와 contentSize 를 비교합니다.


Case1) visibleSizecontentSize 인 경우

화면을 이동할 필요가 없어요

그래서 오프셋을 x:0, y: 0 으로 보여주게 되요


Case2) visibleSize < contentSize 인 경우

[ 전체 높이(contentSize) 에서 스크롤 뷰 화면의 높이(visibleSize) 만큼 빼준 값 ]을 오프셋으로 설정하여 setContentOffset 을 하였습니다.


⚠️ 이때 화면이 새로 추가된 스택뷰만큼 높게 위치되게 되버리더라구요! 그래서 [새로 추가한 스택뷰의 높이] 만큼의 값을 더하여 완성하였습니다.

scrollView.contentSize.height - scrollView.visibleSize.height + labelHeight


⚠️ [질문이 있어요! 🙋]

Case2) 에서 스크롤뷰에 스텍뷰를 추가한 후 자동스크롤을 의도하였는데요

로직은 “ 1) 스크롤뷰에 스텍뷰를 추가한다. 2) 오프셋 계산을 한다 3) 화면을 이동한다 “ 순으로 진행되어요

그렇다면 추가한 것에 맞춰서 자연스럽게 이동이 될 것으로 생각하였는데.. 딱 추가한 스텍뷰가 보이지 않는 만큼만 화면이 스크롤 되더라구요!

// 추가한 스텍뷰가 보이지 않는 코드
bottomOffset.y = scrollView.contentSize.height - scrollView.visibleSize.height
// 추가한 스텍뷰가 보이는 코드
bottomOffset.y = scrollView.contentSize.height - scrollView.visibleSize.height + labelHeight

제가 생각하기에는 뷰가 화면에 로드된 후에 스텍뷰를 스크롤뷰에 추가하더라도.. 추가 전후의 스크롤뷰의 height 는 변경되지 않아서 이런 현상이 발생하는 것인가..? (다시 뷰가 로드될 때 이전에 추가된 스텍뷰를 반영하여 스크롤뷰의 height 가 늘어나는 것일까?) 라고 조심스럽게 생각해 보았습니다.

혹시 콘이 생각하시기에는 어떨까요??🙀


UIStackView 추가 및 삭제

저희는 계산기에서 연산을 입력하면, 연산자와 숫자 UILabel이 있는 stackView가 scrollView에 쌓이게 구현했어요. 동일한 구조의 stackView가 반복되기 때문에 CustomStackView를 만들었어요.

// CustomStackView.swift
final class CustomStackView: UIStackView {
    let operatorLabel: UILabel = {
        let label = UILabel()
        label.textColor = .white
        label.font = .preferredFont(forTextStyle: .title3)
        label.textAlignment = .right
        label.contentMode = .left
        return label
    }()
    
    let operandLabel: UILabel = {
        let label = UILabel()
        label.textColor = .white
        label.font = .preferredFont(forTextStyle: .title3)
        label.textAlignment = .right
        label.contentMode = .left
        return label
    }()

		// ...
}
  • CustomStackView에는 연산자를 보여주는 operatorLabel과 피연산자를 보여주는 operandLabel을 만들었어요.
// ViewController.swift
class ViewController: UIViewController {
		// ...

		private func addNumberTotalStackView(operatorText: String, operandText: String) {
        let numberStackView: CustomStackView = {
            let sv = CustomStackView(frame: .zero)
            sv.operatorLabel.text = operatorText
            sv.operandLabel.text = operandText
            return sv
        }()
        numberTotalStackView.addArrangedSubview(numberStackView)
        moveToPoint(labelHeight: numberStackView.operandLabel.intrinsicContentSize.height)
    }
    
    private func moveToPoint(labelHeight: CGFloat) {
        var bottomOffset = CGPoint(x: 0, y: 0)
        
        if scrollView.contentSize.height > scrollView.visibleSize.height {
            bottomOffset.y = scrollView.contentSize.height - scrollView.visibleSize.height + labelHeight
        }
        scrollView.setContentOffset(bottomOffset, animated: true)
    }

		// ...
}
  • 연산자 Stack을 생성할 땐, CustomStackView를 새로 생성해 numberTotalStackView 에 해당 stackView를 추가해주었어요.
// ViewController.swift
private func removeAllTotalStackView() {
    numberTotalStackView.arrangedSubviews.forEach { subview in
        subview.removeFromSuperview()
    }
        
    totalInput = ""
    userOperandInput = "0"
    userOperatorInput = ""
}
  • AC를 누르거나, 새로운 연산을 시작할 땐 numberTotalStackView에 기존에 추가된 연산 stackView를 모두 제거해야 해요.
  • 기존 연산 stackView를 제거하기 위해 removeFromSuperview() 를 사용했어요.

⁺⁄₋ 부호처리

// ViewController.swift
@IBAction func plusminusButtonTapped(_ sender: UIButton) {
    if userOperandInput != "0" {
        userOperandInput = userOperandInput.contains("-") ? userOperandInput.trimmingCharacters(in: ["-"]) : "-\(userOperandInput)"
    }
}
  • 0 일 경우, -/+ 부호가 붙지 않아요.
  • userOperandInput이 - 부호를 갖고 있을 경우 -를 제거하고, - 부호를 갖고 있지 않는 경우(= + 인 경우) -를 붙여줘요.
  • 삼항연산자를 사용할 경우 가독성이 떨어질 수 있지만, 코드의 간결성을 위해 사용했어요.

연산 결과값 Formatter

// ViewController.swift
let numberFormatter: NumberFormatter = {
    let numberFormatter = NumberFormatter()
    numberFormatter.roundingMode = .halfUp
    numberFormatter.numberStyle = .decimal
    numberFormatter.maximumSignificantDigits = 20
    return numberFormatter
}()

// ...
guard let result = try ExpresstionParser.parse(from: totalInput)?.result() else { return }
guard let resultFormatted = numberFormatter.string(for: result) else { return }
operandLabel.text = resultFormatted
  1. 숫자는 최대 20자리, 잘리는 숫자는 반올림
  2. 숫자는 3자리마다 쉼표(,) 표기
  3. 소수점 끝에 들어간 무의미한 0000 제거

저희는 결과값에 대한 위와 같은 요구사항을 적용하기 위해 NumberFormatter()을 사용했어요.


⚠️ [질문이 있어요! 🙋]

numberFormatter 를 사용하던 중에 문제가 발생하였는데 질문드리고 싶어요!

문제가 발생한 코드

@IBAction func operandsButtonTapped(_ sender: UIButton) {
        if !totalInput.isEmpty && userOperatorInput.isEmpty == true {
            removeAllTotalStackView()
        }
        
        guard let operands = sender.currentTitle else { return }
        
        if userOperandInput == "0" {
            userOperandInput = ""
        }
        userOperandInput += operands
        operandLabel.text = numberFormatter.string(for: userOperandInput)
    }
  • operandLabel은 계산기에서 입력한 숫자가 나타나는 곳이에요
  • 이곳에서도 숫자가 1,000,000 콤마형식을 취하고 싶어서 numberFormatter를 사용하고자 했어요

발생한 문제

  • operandLabel.textnumberFormatter(for:) 값을 넣었는데 화면에 아무런 값이 나오지 않아요!

확인한 점

print(numberFormatter.string(for: userOperandInput) // nil
  • 화면에 터치한 값이 userOperandInput 으로 잘 들어가지만 numberFormatter.string(for:) 메서드에 넣으면 nil 값만 나오게 되는데 이게 어떻게 발생하는 현상인지 잘 모르겠습니다 😭
  • 콘이 생각하시기에는 어떤 부분이 문제일까요?😱

@doshkor doshkor changed the title Step3 계산기 [STEP 3] D.O, Sunny Jan 30, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants