SnapKit框架解析(二) —— SnapKit应用的一个简单示例(二)

版本记录

版本号 时间
V1.0 2019.07.02 星期二

前言

OC中自动布局可以使用三方比如说Masonry,那么Swift呢?Swift也有很优秀的第三方框架,其中SnapKit就很优秀,接下来几篇我们就一起来看下这个框架。感兴趣的可以看下面几篇文章。
1. SnapKit框架解析(一) —— SnapKit应用的一个简单示例(一)

源码

首先,看下项目工程结构

接着,看下sb中的内容

最后,就是代码了

1. QuizViewController.swift
import UIKit

class QuizViewController: UIViewController {
  /// Curent game state
  var state = State(questions: questions)
  
  /// Current question countdown timer
  var timer: Timer?

  // MARK: - Views
  lazy var viewProgress: UIView = {
    let v = UIView(frame: .zero)
    v.backgroundColor = .green
    
    view.addSubview(v)
    
    return v
  }()
  
  lazy var lblTimer: UILabel = {
    let lbl = UILabel(frame: .zero)
    lbl.layer.cornerRadius = 8
    lbl.layer.borderColor = UIColor.black.cgColor
    lbl.layer.borderWidth = 2
    lbl.textAlignment = .center
    lbl.font = UIFont.systemFont(ofSize: 20, weight: .light)
    lbl.text = "00:10"
    
    view.addSubview(lbl)
    
    return lbl
  }()
  
  lazy var lblQuestion: UILabel = {
    let lbl = UILabel(frame: .zero)
    lbl.textAlignment = .center
    lbl.font = UIFont.systemFont(ofSize: 24, weight: .semibold)
    lbl.text = "Question placeholder"
    lbl.numberOfLines = 0
    
    view.addSubview(lbl)
    
    return lbl
  }()
  
  lazy var btnTrue: UIButton = {
    let btn = UIButton(type: .custom)
    btn.layer.cornerRadius = 12
    btn.backgroundColor = .green
    btn.setTitle("👍True", for: .normal)
    btn.setTitleColor(.black, for: .normal)
    btn.titleLabel?.font = .systemFont(ofSize: 22, weight: .semibold)
    btn.showsTouchWhenHighlighted = true
    btn.addTarget(self, action: #selector(handleAnswer(_:)), for: .touchUpInside)
    
    return btn
  }()
  
  lazy var btnFalse: UIButton = {
    let btn = UIButton(type: .custom)
    btn.layer.cornerRadius = 12
    btn.backgroundColor = .red
    btn.setTitle("👎False", for: .normal)
    btn.setTitleColor(.black, for: .normal)
    btn.titleLabel?.font = .systemFont(ofSize: 22, weight: .semibold)
    btn.showsTouchWhenHighlighted = true
    btn.addTarget(self, action: #selector(handleAnswer(_:)), for: .touchUpInside)
    
    return btn
  }()
  
  lazy var svButtons: UIStackView = {
    let stackView = UIStackView(arrangedSubviews: [btnFalse, btnTrue])
    stackView.alignment = .center
    stackView.spacing = 16
    stackView.axis = .horizontal
    stackView.distribution = .fillEqually
    
    view.addSubview(stackView)
    
    return stackView
  }()
  
  lazy var lblMessage: UILabel = {
    let lbl = UILabel(frame: .zero)
    lbl.numberOfLines = 0
    lbl.backgroundColor = UIColor.black.withAlphaComponent(0.8)
    lbl.textAlignment = .center
    lbl.font = UIFont.systemFont(ofSize: 48, weight: .semibold)
    lbl.alpha = 0
    
    self.navigationController?.view.addSubview(lbl)
    
    return lbl
  }()
  
  // - MARK: Setup
  override func viewDidLoad() {
    super.viewDidLoad()
    
    navigationItem.largeTitleDisplayMode = .always
    navigationController?.navigationBar.prefersLargeTitles = true
    navigationController?.navigationBar.barTintColor = UIColor(red: 1, green: 108.0/255, blue: 0, alpha: 1.0)
    navigationController?.navigationBar.isTranslucent = false
    
    setupConstraints()
    startGame()
  }
}
2. QuizViewController+Constraints.swift
import UIKit
import SnapKit

extension QuizViewController {
  func setupConstraints() {
    updateProgress(to: 0)
    
    lblTimer.snp.makeConstraints { make in
      make.width.equalToSuperview().multipliedBy(0.45).labeled("timerWidth")
      make.height.equalTo(45).labeled("timerHeight")
      make.top.equalTo(viewProgress.snp.bottom).offset(32).labeled("timerTop")
      make.centerX.equalToSuperview().labeled("timerCenterX")
    }
    
    lblQuestion.snp.makeConstraints { make in
      make.top.equalTo(lblTimer.snp.bottom).offset(24)
      make.leading.trailing.equalTo(view.safeAreaLayoutGuide).inset(16)
    }
    
    lblMessage.snp.makeConstraints { make in
      make.edges.equalToSuperview()
    }
    
    svButtons.snp.makeConstraints { make in
      make.leading.trailing.equalTo(lblQuestion)
      make.top.equalTo(lblQuestion.snp.bottom).offset(16)
      make.height.equalTo(80)
    }
  }
  
  func updateProgress(to progress: Double) {
    viewProgress.snp.remakeConstraints { make in
      make.top.equalTo(view.safeAreaLayoutGuide)
      make.width.equalToSuperview().multipliedBy(progress)
      make.height.equalTo(32)
      make.leading.equalToSuperview()
    }
  }
}

// MARK: - Orientation Transition Handling
extension QuizViewController {
  override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {
    super.willTransition(to: newCollection, with: coordinator)
    // 1
    let isPortrait = UIDevice.current.orientation.isPortrait
    
    // 2
    lblTimer.snp.updateConstraints { make in
      make.height.equalTo(isPortrait ? 45 : 65)
    }
    
    // 3
    lblTimer.font = UIFont.systemFont(ofSize: isPortrait ? 20 : 32, weight: .light)
  }
}
3. QuizViewController+Logic.swift
import UIKit

// MARK: - Game Logic
extension QuizViewController {
  @objc func handleAnswer(_ sender: UIButton) {
    let userSelection = sender == btnTrue
    let isCorrect = state.question.isTrue == userSelection
    
    if isCorrect {
      state.correctAnswers += 1
    }
    
    showMessage(isCorrect: isCorrect)
  }
  
  func showMessage(isCorrect: Bool) {
    lblMessage.text = isCorrect ? "That's correct!\n👍🎉😄" : "Sorry, that's wrong!\n\n👎☠️😢"
    lblMessage.textColor = isCorrect ? .green : .red
    lblMessage.transform = CGAffineTransform(scaleX: 0.1, y: 0.1)
    lblMessage.alpha = 0
    timer?.invalidate()
    
    UIView.animate(withDuration: 0.7,
                   delay: 0.0,
                   usingSpringWithDamping: 0.4,
                   initialSpringVelocity: 0.8,
                   options: .curveEaseIn,
                   animations: {
                    self.lblMessage.alpha = 1
                    self.lblMessage.transform = .identity
                   },
                   completion: { _ in
                    UIView.animate(withDuration: 0.15,
                                   delay: 0.35,
                                   animations: {
                                    self.lblMessage.alpha = 0
                                   },
                                   completion: { _ in
                                    self.goToNextQuestion()
                                   })
                   })
  }
  
  func goToNextQuestion() {
    guard !state.isLastQuestion else {
      // This is the last question
      let alert = UIAlertController(title: "Good job!",
                                    message: "You got \(state.correctAnswers) out of \(state.numberOfQuestions) right.",
        preferredStyle: .alert)
      
      alert.addAction(UIAlertAction(title: "Start Over",
                                    style: .default,
                                    handler: { [weak self] _ in self?.startGame() }))
      
      present(alert, animated: true, completion: nil)
      return
    }
    
    goToQuestion(at: state.currentQuestion + 1)
  }
  
  func goToQuestion(at position: Int) {
    state.currentQuestion = position
    
    lblQuestion.text = state.question.title
    navigationItem.title = "SnappyQuiz \(position)/\(state.numberOfQuestions)"
    
    let progress = Double(position) / Double(state.numberOfQuestions)
    updateProgress(to: progress)
    startCountdown()
  }
  
  func startGame() {
    state = State(questions: questions)
    goToQuestion(at: 1)
  }
  
  func startCountdown() {
    lblTimer.text = "00:10"
    
    timer?.invalidate()
    timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] timer in
      guard let self = self else { return }
      
      let currentSeconds = Int((self.lblTimer.text ?? "0").replacingOccurrences(of: ":", with: "")) ?? 1
      
      guard currentSeconds > 1 else {
        timer.invalidate()
        self.goToNextQuestion()
        return
      }
      
      self.lblTimer.text = "00:0\(currentSeconds - 1)"
    }
  }
}
4. Questions.swift
import Foundation

struct Question {
  let title: String
  let isTrue: Bool
}

let questions = [
  ("As far as has ever been reported, no-one has ever seen an ostrich bury its head in the sand.", true),
  ("Approximately one quarter of human bones are in the feet.", true),
  ("Popeye’s nephews were called Peepeye, Poopeye, Pipeye and Pupeye.", true),
  ("In ancient Rome, a special room called a vomitorium was available for diners to purge food in during meals.", false),
  ("The average person will shed 10 pounds of skin during their lifetime.", false),
  ("The Great Wall Of China is visible from the moon.", false),
  ("Virtually all Las Vegas gambling casinos ensure that they have no clocks.", true),
  ("Risotto is a common Italian rice dish.", true),
  ("The prefix \"mega-\" represents one million.", true),
  ("The \"Forbidden City\" is in Beijing.", true),
  ("Hurricanes and typhoons are the same thing.", true),
  ("In Shakespeare's play, Hamlet commits suicide.", false),
  ("An American was the first man in space.", false),
  ("The \"China Syndrome\" is what hostages experience when they begin to feel empathy for their captors.", false),
  ("Other than water, coffee is the world's most popular drink.", true)
].map(Question.init)
5. State.swift
import Foundation

// MARK: - Game State
extension QuizViewController {
  struct State {
    let questions: [Question]
    let numberOfQuestions: Int
    var currentQuestion: Int
    var correctAnswers: Int
    
    init(questions: [Question]) {
      self.questions = questions
      numberOfQuestions = questions.count
      currentQuestion = 1
      correctAnswers = 0
    }
    
    var isLastQuestion: Bool {
      return currentQuestion == numberOfQuestions
    }
    
    var question: Question {
      return questions[currentQuestion - 1]
    }
  }
}

后记

本篇主要讲述了SnapKit自动布局的框架,感兴趣的给个赞或者关注~~~

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容