Reactive Programming

CardView는 View에 속하는데 비해 현재 로직이 너무 많다.

	var imageIndex = 0
    fileprivate let barDeselectedColor = UIColor(white: 0, alpha: 0.1)
    
    @objc fileprivate func handleTap(gesture: UITapGestureRecognizer) {
        print("Handling tap and cycling photos")
        let tapLocation = gesture.location(in: nil)
        let shouldAdvanceNextPhoto = tapLocation.x > frame.width / 2 ? true : false
        if shouldAdvanceNextPhoto {
            imageIndex = min(imageIndex + 1, cardViewModel.imageNames.count - 1)
        } else {
            imageIndex = max(imageIndex - 1, 0)
        }
        let imageName = cardViewModel.imageNames[imageIndex]
        imageView.image = UIImage(named: imageName)
        
        barsStackView.arrangedSubviews.forEach { (v) in
            v.backgroundColor = barDeselectedColor
        }
        barsStackView.arrangedSubviews[imageIndex].backgroundColor = .white
    }

MVVM

View Model is supposed to represent the State of our View

struct CardViewModel {
    // we'll define the properties that are view will display/render out
    let imageNames: [String]
    let attributedString: NSAttributedString
    let textAlignment: NSTextAlignment
    
    var imageIndex = 0
    
    func advanceToNextPhoto() {
        imageIndex = imageIndex + 1
    }
    
    func goToPreviousPhoto() {
        imageIndex = imageIndex - 1
    }
}

Cannot assign to property: ‘self’ is immutable Mark method ‘mutating’ to make ‘self’ mutable

struct이므로 immutable을 mutable로 바꿔야 한다는 fix 내용이 나온다. fix 버튼을 누르면 func 앞에 mutating syntax가 추가된다.

mutating syntax는 매우 사용하기 까다로우므로 보통 MVVM 모델에서는 struct보다 class로 사용한다.

class에서는 initialization는 필수! init함수를 추가한다.

init(imageNames: [String], attributedString: NSAttributedString, textAlignment: NSTextAlignment) {
        self.imageNames = imageNames
        self.attributedString = attributedString
        self.textAlignment = textAlignment
    }
@objc fileprivate func handleTap(gesture: UITapGestureRecognizer) {
        print("Handling tap and cycling photos")
        let tapLocation = gesture.location(in: nil)
        let shouldAdvanceNextPhoto = tapLocation.x > frame.width / 2 ? true : false
        
        if shouldAdvanceNextPhoto {
            cardViewModel.advanceToNextPhoto()
        } else {
            cardViewModel.goToPreviousPhoto()
        }
//        if shouldAdvanceNextPhoto {
//            imageIndex = min(imageIndex + 1, cardViewModel.imageNames.count - 1)
//        } else {
//            imageIndex = max(imageIndex - 1, 0)
//        }
//        let imageName = cardViewModel.imageNames[imageIndex]
//        imageView.image = UIImage(named: imageName)
//
//        barsStackView.arrangedSubviews.forEach { (v) in
//            v.backgroundColor = barDeselectedColor
//        }
//        barsStackView.arrangedSubviews[imageIndex].backgroundColor = .white
    }

It’s not working

fileprivate var imageIndex = 0 {
        didSet {
            imageIndexObserver?()
        }
    }
    
    // Reactive Programming
    var imageIndexObserver: (() -> ())?

imageIndex 값이 변경될 때마다 didSet 부분이 실행된다.

    var imageIndexObserver: (() -> ())?

이부분은 closure

    var imageIndexObserver: ((UIImage) -> ())?

	fileprivate var imageIndex = 0 {
        didSet {
            let imageName = imageNames[imageIndex]
            let image = UIImage(named: imageName)
            imageIndexObserver?(image ?? UIImage())
        }
    }

imageIndexObserver의 closure의 인자를 UIImage type으로 바꾸게 되면 didSet에서 호출한 함수의 인자를 바꿔줘야한다.

Value of optional type ‘UIImage?’ must be unwrapped to a value of type ‘UIImage’ Coalesce using ‘??’ to provide a default when the optional value contains ‘nil’

여기서 let image는 optional이기 때문에(오른쪽 설명 참고) 인자에 넣을 때에도 optional로 넣어줘야하기 때문에 ?? 을 사용하여 optional 처리를 해줄 수 있다.

	var imageIndexObserver: ((UIImage?) -> ())?

인자를 optional로 바꿔버리면 해결된다.

		imageIndexObserver?(image)

깔끔하게 변경

// CardView.swift
	fileprivate func setupImageIndexObserver() {
        cardViewModel.imageIndexObserver = { (image) in
            print("Changing photo from view model")
            self.imageView.image = image
        }
    }

바뀐 closure대로 수정을 해준다. closure에서 (image) in을 해준다! 어렵지만 익숙해지자.

이대로 실행하면 잘 바뀌다가 overflow 발생

Thread 1: Fatal error: Index out of range

func advanceToNextPhoto() {
        imageIndex = min(imageIndex + 1, imageNames.count)
    }
    
    func goToPreviousPhoto() {
        imageIndex = max(imageIndex - 1, 0)
    }

min, max로 해결

이제 barsStackView를 바꿔주기 위해서 Int 타입도 closure 인자로 보낸다.

	var imageIndexObserver: ((Int, UIImage?) -> ())?
	fileprivate var imageIndex = 0 {
        didSet {
            let imageName = imageNames[imageIndex]
            let image = UIImage(named: imageName)
            imageIndexObserver?(imageIndex, image)
        }
    }
	fileprivate func setupImageIndexObserver() {
        cardViewModel.imageIndexObserver = { (index, image) in
            print("Changing photo from view model")
            self.imageView.image = image
            
            self.barsStackView.arrangedSubviews.forEach({ (v) in
                v.backgroundColor = self.barDeselectedColor
            })
            self.barsStackView.arrangedSubviews[index].backgroundColor = .white
        }
    }

구현 완료

weak self

fileprivate func setupImageIndexObserver() {
        cardViewModel.imageIndexObserver = { [weak self] (index, image) in
            self?.imageView.image = image
            
            self?.barsStackView.arrangedSubviews.forEach({ (v) in
                v.backgroundColor = self?.barDeselectedColor
            })
            self?.barsStackView.arrangedSubviews[index].backgroundColor = .white
        }
    }

weak self를 사용하여 self에 모두 ?를 붙여야한다.

closure 내에서 self를 사용하면 특수한 경우에는 문제가 생길 수 있기 때문에 weak를 사용한다.

간단한 설명 : closure에서 weak self의 사용

ARC와 순환참조와 클로저

태그:

카테고리:

업데이트:

댓글남기기