포스트

Tip-Calculator (6)

결과를 Result View에 출력

현재는 bind 함수에 콘솔로 보여주게만 되어있다.

그걸 이제 result view에 출력이 되도록 한다.

1
2
3
4
5
6
7
8
9
func configure(result: Result) {
        let text = NSMutableAttributedString(
            string: String(result.amountPerPerson),
            attributes: [.font: ThemeFont.bold(ofSize: 48)])
        text.addAttributes([
            .font: ThemeFont.bold(ofSize: 24)
        ], range: NSMakeRange(0, 1))
        amountPersonLabel.attributedText = text
    }

결과값을 폰트를 별도 적용하여 label에 적용해주는 함수를 구현

TotalBillview 구현

1
2
3
4
5
6
7
8
9
10
11
12
13
private let totalBillView: AmountView = {
       let view = AmountView(
            title: "Total Bill",
            textAlignment: .left)
        return view
    }()
    
private let totalTipView: AmountView = {
       let view = AmountView(
            title: "Total Tip",
            textAlignment: .left)
        return view
    }()

다시 view를 새로 만들어 주고, 기존에

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private lazy var hStackView: UIStackView = {
        let stackView = UIStackView(arrangedSubviews: [
            AmountView(
                title: "Total Bill",
                textAlignment: .left),
            UIView(), // 사이에 끼워줌.
            AmountView(
                title: "Total Tip",
                textAlignment: .right)
        ])
        stackView.axis = .horizontal
        stackView.distribution = .fillEqually
        return stackView
    }()

hstackView에 위와 같이 만들어 뒀던것을 인스턴스를 넣어준다.

1
2
3
4
5
6
7
8
9
10
private lazy var hStackView: UIStackView = {
        let stackView = UIStackView(arrangedSubviews: [
            totalBillView,
            UIView(), // 사이에 끼워줌.
            totalTipView
        ])
        stackView.axis = .horizontal
        stackView.distribution = .fillEqually
        return stackView
    }()

AmountView에 configure 함수 구현.

1
2
3
4
5
6
7
8
9
func configure(text: String) {
        let text = NSMutableAttributedString(string: text, attributes: [
            .font: ThemeFont.bold(ofSize: 24)
        ])
        text.addAttributes([
            .font: ThemeFont.bold(ofSize: 16)
        ], range: NSMakeRange(0, 1))
        amountLabel.attributedText = text
    }

Resultview에 적용

1
2
3
4
5
6
7
8
9
10
11
func configure(result: Result) {
        let text = NSMutableAttributedString(
            string: String(result.amountPerPerson),
            attributes: [.font: ThemeFont.bold(ofSize: 48)])
        text.addAttributes([
            .font: ThemeFont.bold(ofSize: 24)
        ], range: NSMakeRange(0, 1))
        amountPersonLabel.attributedText = text
        totalBillView.configure(text: String(result.totalBill)) // added
        totalTipView.configure(text: String(result.totalTip))   // added
    }

VC에 적용

1
2
3
4
// bind
output.updateViewPublisher.sink { [unowned self] result in
            resultView.configure(result: result)
        }.store(in: &cancellables)

Simulator Screenshot - iPhone 15 Pro - 2024-05-03 at 18 19 20

좀 우스꽝스럽게 나왔다.

수정을 해야한다.

보강

Double에 대해 extension을 만든다

1
2
3
4
5
6
7
8
9
10
11
extension Double {
    var currencyFormatted: String {
        var isWholeNumber: Bool {
            isZero ? true: !isNormal ? false: self == rounded()
            }
        let formatter = NumberFormatter()
        formatter.numberStyle = .currency
        formatter.minimumFractionDigits = isWholeNumber ? 0 : 2
        return formatter.string(for: self) ?? ""
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// AmountView
func configure(amount: Double) { // modified String -> Double
        let text = NSMutableAttributedString(string: amount.currencyFormatted, attributes: [ // modified
            .font: ThemeFont.bold(ofSize: 24)
        ])
        text.addAttributes([
            .font: ThemeFont.bold(ofSize: 16)
        ], range: NSMakeRange(0, 1))
        amountLabel.attributedText = text
    }

// ResultView 
 func configure(result: Result) {
        let text = NSMutableAttributedString(
            string: result.amountPerPerson.currencyFormatted, // modified
            attributes: [.font: ThemeFont.bold(ofSize: 48)])
        text.addAttributes([
            .font: ThemeFont.bold(ofSize: 24)
        ], range: NSMakeRange(0, 1))
        amountPersonLabel.attributedText = text
        totalBillView.configure(amount: result.totalBill) // modified
        totalTipView.configure(amount: result.totalTip)   // modified
    }

simulator_screenshot_92F80971-E271-44C0-A0D4-7B90BE60A1B1

이젠 잘된다.

Tap Gesture 추가

CombineCocoa를 사용한다.

CleanShot 2024-05-03 at 18 33 48@2x

여기에 tap gesture가 있다.

퍼블리셔 생성

1
2
3
4
5
6
7
private lazy var viewTapPublisher: AnyPublisher<Void, Never> = {
        let tapGesture = UITapGestureRecognizer(target: self, action: nil)
        view.addGestureRecognizer(tapGesture)
        return tapGesture.tapPublisher.flatMap { _ in
            Just(())
        }.eraseToAnyPublisher()
    }()  

Input이 Void?

그 이유는 우리가 탭을 할때 Int나 String 이런 값을 보내지 않을 것이라서 그렇다. 그래서 just안에도 ()를 넣었음.

observe 함수 추가

1
2
3
4
5
private func observe() {
        viewTapPublisher.sink { [unowned self] value in
            view.endEditing(true)
        }.store(in: &cancellables)
    }

parameter로 value가 있지만 void이므로 어차피 리턴할게 없다.

제스쳐를 추가한 이유는 키보드가 올라왔을때 키보드를 내리게 하기 위함.

May-03-2024 19-08-12

LogoView를 탭했을 때의 이벤트 추가

1
2
3
4
5
6
7
8
 private lazy var logoviewTapPublisher: AnyPublisher<Void, Never> = {
        let tapGesture = UITapGestureRecognizer(target: self, action: nil)
        tapGesture.numberOfTapsRequired = 2 // added
        view.addGestureRecognizer(tapGesture)
        return tapGesture.tapPublisher.flatMap { _ in
            Just(())
        }.eraseToAnyPublisher()
    }()

2번 탭했을때 해당 gestureRecognizer가 발생

May-03-2024 19-11-31

GestureTapPublisher를 vm에 전달.

1
2
3
4
5
6
7
8
9
10
struct Input {
        let billPublisher: AnyPublisher<Double, Never>
        let tipPublisher: AnyPublisher<Tip, Never>
        let splitPublisher: AnyPublisher<Int, Never>
        let logoViewTapPublisher: AnyPublisher<Void, Never> // added
    }
struct Output {
        let updateViewPublisher: AnyPublisher<Result, Never>
        let resultCalculatorPublisher: AnyPublisher<Void, Never>
    }    

로고뷰에 대한 퍼블리셔를 하나 추가. vc에서 로고뷰에 대한 퍼블리셔를 void로 했기에 이것도 void로 해준다.

output에도 void로 해서 하나 만들어 준다. (Reset용)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// vc

let input = CalculatorVM.Input(
            billPublisher: billInputView.valuePublisher,
            tipPublisher: tipInputView.valuePublisher,
            splitPublisher: spiltInputView.valuePublisher,
            logoViewTapPublisher: logoviewTapPublisher) // added

let output = vm.transform(input: input)
        
        output.updateViewPublisher.sink { [unowned self] result in
            resultView.configure(result: result)
        }.store(in: &cancellables)
        
        output.resultCalculatorPublisher.sink { _ in // added
            print("hey, reset the form please")
        }.store(in: &cancellables)            

// vm

func transform(input: Input) -> Output {
        
        let updateViewPublisher = Publishers.CombineLatest3(
            input.billPublisher,
            input.tipPublisher,
            input.splitPublisher).flatMap { [unowned self] (bill, tip, split) in
                let totalTip = getTipAmount(bill: bill, tip: tip)
                let totalBill = bill + totalTip
                let amountPerPerson = totalBill / Double(split)
                let result = Result(
                    amountPerPerson: amountPerPerson,
                    totalBill: totalBill,
                    totalTip: totalTip)
                
                return Just(result)
            }.eraseToAnyPublisher()
        
        let resultCalculatorPublisher = input.logoViewTapPublisher // added
        
        return Output(updateViewPublisher: updateViewPublisher, resultCalculatorPublisher: resultCalculatorPublisher) // modified
    }

로고뷰를 탭하면

1
2
hey, reset the form please
logoview is tapped

이렇게 출력이 된다.

이제는

1
2
3
4
5
6
//vc
private func observe() {
        viewTapPublisher.sink { [unowned self] value in
            view.endEditing(true)
        }.store(in: &cancellables)
    }

logoviewTapPublisher의 내용을 지워도 된다.

bind함수에서 호출하기때문.

로고뷰 탭하면 사운드 발생 이벤트 추가

사운드파일을 넣어주고 새로운 스위프트 파일을 생성

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
protocol AudioPlayerService {
    func playSound()
}

final class DefaultAudioPlayer: AudioPlayerService {
    
    private var player: AVAudioPlayer?
    
    
    func playSound() {
        let path = Bundle.main.path(forResource: "click", ofType: "m4a")!
        let url = URL(fileURLWithPath: path)
        
        do {
            player = try AVAudioPlayer(contentsOf: url)
            player?.play()
        } catch (let error) {
            print(error.localizedDescription)
        }
    }
    
}

VM에서 사운드 플레이어 기능 구현 - 로고 터치시

1
2
3
4
5
private let audioPlayerService: AudioPlayerService

init(audioPlayerService: AudioPlayerService = DefaultAudioPlayer()) {
        self.audioPlayerService = audioPlayerService
    }

해당 부분을 initializing

1
2
3
4
5
6
7
8
9
// vm
// transform

let resultCalculatorPublisher = input.logoViewTapPublisher.handleEvents(receiveOutput: { [unowned self] in
            audioPlayerService.playSound()
        }).flatMap {
            return Just($0)
        }.eraseToAnyPublisher()

로고탭을 하면 이벤트가 발생하고 그 이벤트로 사운드를 재생시킴, 어차피 Void로 리턴하므로 아무것도 없음.

실행했지만 nil 발생

스택오버플로우

를 보고 시도.

CleanShot 2024-05-03 at 20 05 19@2x

성공.

로고 클릭시 리셋 기능 구현

리셋 함수 구현

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//billinputview
func reset() {
        textField.text = nil
        billSubject.send(0)
}
// tipinputview
func reset () {
        tipSubject.send(.none)
    }

// splitinputview
func reset () {
        splitSubject.send(1)
    }

// vc
output.resetCalculatorPublisher.sink { [unowned self] _ in
            billInputView.reset() // added
            tipInputView.reset() // added
            spiltInputView.reset() // added
        }.store(in: &cancellables)

애니메이션 구현

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
private func bind() {
        
        
        let input = CalculatorVM.Input(
            billPublisher: billInputView.valuePublisher,
            tipPublisher: tipInputView.valuePublisher,
            splitPublisher: spiltInputView.valuePublisher,
            logoViewTapPublisher: logoviewTapPublisher)
        
        
        let output = vm.transform(input: input)
        
        output.updateViewPublisher.sink { [unowned self] result in
            resultView.configure(result: result)
        }.store(in: &cancellables)
        
        output.resetCalculatorPublisher.sink { [unowned self] _ in
            billInputView.reset()
            tipInputView.reset()
            spiltInputView.reset()
            
            UIView.animate(
                withDuration: 0.1,
                delay: 0,
                usingSpringWithDamping: 5.0,
                initialSpringVelocity: 0.5,
                options: .curveEaseInOut) {
                    self.logoView.transform = .init(scaleX: 1.5, y: 1.5)
                } completion: { _ in
                    UIView.animate(withDuration: 0.1) {
                        self.logoView.transform = .identity
                    }
                }
        }.store(in: &cancellables)

    }

완성

May-03-2024 20-29-11

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.