티스토리 뷰

728x90
반응형

내용

  • MVVM 패턴을 적용해보자
  • 데이터바인딩 방법은 Observable 클래스 사용

QR코드 뷰와 뷰모델만 소개해보겠다.

View

import UIKit
import SnapKit

class QRCodeViewController: UIViewController {

    // MARK: - Properties

    // ✅ view model
    let viewModel = QRCodeViewModel()

    let closeButton = UIButton()
    let switchShakeButton = UIButton()
    let privateQuestionButton = UIButton()

    let titleLabel = UILabel()
    let subtitleLabel = UILabel()
    let privatetextLabel = UILabel()
    let privateNumberLabel = UILabel()
    let timeLabel = UILabel()

    let qrcodeBackView = UIView()
    let qrcodeTopView = UIView()
    let qrcodeImageBackView = UIView()

    let privateStackView = UIStackView()

    let qrcodeImageView = QRCodeView()

    var isSpeechBubble = false
    var isShakeAvailable = true

    // MARK: - View Life Cycle

    override func viewDidLoad() {
        super.viewDidLoad()
        configUI()
        setLayout()
        setBinding()
        setNotification()
    }
}

// MARK: - Extensions

extension QRCodeViewController {

    // ✅ UI 의 속성 설정
    private func configUI(){
        view.backgroundColor = .white

        titleLabel.text = "입장을 위한 QR X COOV"
        titleLabel.font = UIFont.boldSystemFont(ofSize: 17)

        subtitleLabel.text = "이용하려는 시설에 QR코드로 체크인하거나 수기명부에\n휴대전화번호 대신 개인안심번호를 기재하세요."
        subtitleLabel.font = UIFont.systemFont(ofSize: 13)
        subtitleLabel.textColor = .gray
        subtitleLabel.numberOfLines = 2
        subtitleLabel.textAlignment = .center

        closeButton.setImage(UIImage(systemName: "xmark"), for: .normal)
        closeButton.tintColor = .black
        closeButton.setPreferredSymbolConfiguration(.init(pointSize: 20, weight: .regular), forImageIn: .normal)
        closeButton.addAction(UIAction { _ in
            self.viewModel.dismissToMainVC(self)
        }, for: .touchUpInside)

        switchShakeButton.tintColor = .gray
        switchShakeButton.setTitleColor(.gray, for: .normal)
        switchShakeButton.titleLabel?.font = UIFont.systemFont(ofSize: 13)
        switchShakeButton.addAction(UIAction { _ in
            self.viewModel.setShakeAvailable()
            self.viewModel.setShakeText()
            self.viewModel.setShakeImage()
            print(self.viewModel.isShakeAvailable.value)
        }, for: .touchUpInside)

        qrcodeBackView.backgroundColor = .white
        qrcodeBackView.layer.cornerRadius = 10
        qrcodeBackView.layer.shadowColor = UIColor.black.cgColor
        qrcodeBackView.layer.shadowRadius = 3
        qrcodeBackView.layer.shadowOpacity = 0.1
        qrcodeBackView.layer.shadowOffset = CGSize(width: 0, height: 0)
        qrcodeBackView.layer.masksToBounds = false

        qrcodeTopView.backgroundColor = #colorLiteral(red: 0.9877077937, green: 0.9827327132, blue: 0.8808727264, alpha: 1)
        qrcodeTopView.layer.cornerRadius = 10
        qrcodeTopView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]

        // private stack view
        privatetextLabel.text = "개인안심번호"
        privatetextLabel.font = UIFont.systemFont(ofSize: 15)

        privateQuestionButton.setImage(UIImage(systemName: "questionmark.circle.fill"), for: .normal)
        privateQuestionButton.tintColor = .gray
        privateQuestionButton.setPreferredSymbolConfiguration(.init(pointSize: 15), forImageIn: .normal)

        privateNumberLabel.text = "12현34규"
        privateNumberLabel.font = UIFont.boldSystemFont(ofSize: 15)

        var privateViewList = [UIView]()
        privateViewList.append(contentsOf: [privatetextLabel, privateQuestionButton, privateNumberLabel])
        _ = privateViewList.map {
            privateStackView.addArrangedSubview($0)
        }
        privateStackView.axis = .horizontal
        privateStackView.spacing = 5
        privateStackView.alignment = .fill
        privateStackView.distribution = .equalSpacing

        timeLabel.font = UIFont.systemFont(ofSize: 15)
        timeLabel.textColor = .gray
        viewModel.setTimeText()
    }

    // ✅ SnapKit 을 활용한 오토레이아웃 잡기
    private func setLayout() {

        // ✅ 여러개의 UIView 를 한번에 등록하는 addSubviews() 커스텀 메서드
        view.addSubviews([titleLabel, subtitleLabel, closeButton, switchShakeButton, qrcodeBackView])
        qrcodeBackView.addSubviews([qrcodeTopView, qrcodeImageBackView, timeLabel])
        qrcodeTopView.addSubviews([privateStackView])
        qrcodeImageBackView.addSubviews([qrcodeImageView])

        let guide = view.safeAreaLayoutGuide

        titleLabel.snp.makeConstraints {
            $0.centerX.equalToSuperview()
            $0.top.equalTo(guide).offset(70)
        }

        subtitleLabel.snp.makeConstraints {
            $0.centerX.equalToSuperview()
            $0.top.equalTo(titleLabel.snp.bottom).offset(8)
        }

        closeButton.snp.makeConstraints {
            $0.top.equalTo(guide).offset(20)
            $0.right.equalTo(guide).offset(-20)
        }

        switchShakeButton.snp.makeConstraints {
            $0.bottom.equalTo(guide).offset(-20)
            $0.centerX.equalToSuperview()
        }

        qrcodeTopView.snp.makeConstraints {
            $0.top.left.right.equalTo(qrcodeBackView)
            $0.height.equalTo(40)
        }

        qrcodeBackView.snp.makeConstraints {
            $0.top.equalTo(subtitleLabel.snp.bottom).offset(16)
            $0.bottom.equalTo(switchShakeButton.snp.top).offset(-140)
            $0.left.right.equalToSuperview().inset(16)
        }

        privateStackView.snp.makeConstraints {
            $0.height.equalTo(30)
            $0.left.right.equalTo(qrcodeBackView).inset(80)
            $0.centerY.equalTo(qrcodeTopView)
        }

        qrcodeImageBackView.snp.makeConstraints {
            $0.top.equalTo(qrcodeTopView.snp.bottom).offset(16)
            $0.left.right.equalTo(qrcodeBackView).inset(60)
            $0.height.equalTo(223)
        }

        qrcodeImageView.snp.makeConstraints {
            $0.edges.equalTo(qrcodeImageBackView)
        }

        timeLabel.snp.makeConstraints {
            $0.top.equalTo(qrcodeImageBackView.snp.bottom).offset(8)
            $0.centerX.equalToSuperview()
        }
    }

    // ✅ Binding
    private func setBinding() {
        viewModel.isShakeAvailable.bind { available in
            self.isShakeAvailable = available
        }

        viewModel.shakeText.bind { text in
            self.switchShakeButton.setTitle(text, for: .normal)
            let attributeString = NSMutableAttributedString(string: text)
            attributeString.addAttribute(.underlineStyle , value: 1 , range: NSRange.init(location: 0, length: text.count))
            self.switchShakeButton.titleLabel?.attributedText = attributeString
        }

        viewModel.shakeImg.bind { image in
            self.switchShakeButton.setImage(UIImage(systemName: image), for: .normal)
        }

        viewModel.qrcodeMsg.bind { msg in
            self.qrcodeImageView.generateCode(msg)
        }

        viewModel.timeText.bind { time in
            let text = "남은 시간 \(time)초"
            let attributeStrring = NSMutableAttributedString(string: text)
            attributeStrring.addAttribute(.foregroundColor, value:  UIColor.red, range: NSRange.init(location: 6, length: String(time).count+1))
            self.timeLabel.attributedText = attributeStrring
        }
    }

    // ✅ screenshot block
    private func setNotification() {
        NotificationCenter.default.addObserver(self, selector: #selector(blockScreenShot), name: UIApplication.userDidTakeScreenshotNotification, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(blockScreenShot), name: UIScreen.capturedDidChangeNotification, object: nil)
    }

    @objc
    func blockScreenShot() {
        let alert = UIAlertController(title: "캡쳐는 안돼요!", message: "보안 정책에 따라 스크린샷을 캡쳐할 수 없습니다.", preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "확인", style: .default, handler: nil))
        self.present(alert, animated: true, completion: nil)
    }
}

Observable

import Foundation

class Observable<T> {

    typealias Listener = (T) -> Void

    var listener: Listener?

    var value: T {
        didSet {
            listener?(value)
        }
    }

    init(_ value: T) {
        self.value = value
    }

    func bind(_ closure: @escaping (T) -> Void) {
        self.listener = closure
        listener?(value)
    }
}

View Model

import Foundation
import UIKit

public class QRCodeViewModel {

    let isShakeAvailable = Observable(true)
    let shakeText = Observable("QR 체크인 쉐이크 기능 끄기")
    let shakeImg = Observable("iphone.slash")
    let qrcodeMsg = Observable("https://gyuios.tistory.com/78")
    let timeText = Observable(15)

    func setShakeAvailable() {
        self.isShakeAvailable.value = !isShakeAvailable.value
    }

    func setShakeText() {
        if self.isShakeAvailable.value {
            self.shakeText.value = "QR 체크인 쉐이크 기능 끄기"
        } else {
            self.shakeText.value = "QR 체크인 쉐이크 기능 켜기"
        }
    }

    func setShakeImage() {
        if self.isShakeAvailable.value {
            self.shakeImg.value = "iphone.slash"
        } else {
            self.shakeImg.value = "iphone.radiowaves.left.and.right"
        }
    }

    func setQrcodeMsg() {
        self.qrcodeMsg.value = "https://gyuios.tistory.com/78"
    }

    func setTimeText() {
        Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(timerAction), userInfo: nil, repeats: true)
    }

    @objc
    func timerAction() {
        if self.timeText.value == 0 {
            self.timeText.value = 15
            // ✅ 15초 후에 qrcode 이미지가 변경되도록 함
            self.qrcodeMsg.value = "https://gyuios.tistory.com/79"
        } else {
            self.timeText.value -= 1
        }
    }

    // MARK: - presentation methods

    func dismissToMainVC(_ view: UIViewController) {
        view.dismiss(animated: true, completion: nil)
    }
}

Model

특별히 사용할 부분은 없었다.

깃허브

 

GitHub - 28th-SOPT-iOS-CloneCoding/MiraClone-KimHyunGyu: 🧚 아요 미라클론코딩 김현규

🧚 아요 미라클론코딩 김현규. Contribute to 28th-SOPT-iOS-CloneCoding/MiraClone-KimHyunGyu development by creating an account on GitHub.

github.com

 

728x90
반응형
댓글
최근에 올라온 글
최근에 달린 댓글
글 보관함
«   2024/11   »
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
링크
Total
Today
Yesterday