GameplayKit框架详细解析(三) —— GameplayKit的实用状态机(二)

版本记录

版本号 时间
V1.0 2019.08.21 星期三

前言

GameplayKit框架,构建和组织你的游戏逻辑。 整合常见的游戏行为,如随机数生成,人工智能,寻路和代理行为。接下来几篇我们就一起看一下这个框架。感兴趣的看下面几篇文章。
1. GameplayKit框架详细解析(一) —— 基本概览(一)
2. GameplayKit框架详细解析(二) —— GameplayKit的实用状态机(一)

开始

1. Swift

首先看下工程组织结构

下面就是看代码了

1. Coordinator.swift
protocol Coordinator {
  func start()
}
2. ApplicationCoordinator.swift
import UIKit

class ApplicationCoordinator: Coordinator {
  let kanjiStorage: KanjiStorage
  let window: UIWindow
  let rootViewController: UINavigationController
  let stateMachine: KanjiStateMachine

  init(window: UIWindow) {
    self.window = window
    kanjiStorage = KanjiStorage()
    rootViewController = UINavigationController()
    rootViewController.navigationBar.prefersLargeTitles = true
    
    stateMachine = KanjiStateMachine(presenter: rootViewController,
                                     kanjiStorage: kanjiStorage,
                                     states: [AllState(), DetailState(), ListState()])
  }
  
  func start() {
    stateMachine.enter(AllState.self)
    window.rootViewController = rootViewController
    window.makeKeyAndVisible()
    subscribeToNotifications()
  }
  
  func subscribeToNotifications() {
    NotificationCenter.default.addObserver(
      self, selector: #selector(receivedAllKanjiNotification),
      name: Notifications.AllKanji, object: nil)
    
    NotificationCenter.default.addObserver(
      self, selector: #selector(receivedKanjiDetailNotification),
      name: Notifications.KanjiDetail, object: nil)
    
    NotificationCenter.default.addObserver(
      self, selector: #selector(receivedKanjiListNotification),
      name: Notifications.KanjiList, object: nil)
  }
  
  @objc func receivedAllKanjiNotification() {
    stateMachine.enter(AllState.self)

  }
  
  @objc func receivedKanjiDetailNotification(notification: NSNotification) {
    // 1
    guard
      let kanji = notification.object as? Kanji,
      // 2
      let detailState = stateMachine.state(forClass: DetailState.self)
      else {
        return
    }
    
    // 3
    detailState.kanji = kanji
    
    // 4
    stateMachine.enter(DetailState.self)
  }
  
  @objc func receivedKanjiListNotification(notification: NSNotification) {
    // 1
    guard
      let word = notification.object as? String,
      let listState = stateMachine.state(forClass: ListState.self)
      else {
        return
    }
    
    // 2
    listState.word = word
    
    // 3
    stateMachine.enter(ListState.self)
  }
}
3. KanjiListCoordinator.swift
import UIKit

class KanjiListCoordinator: Coordinator {
  private let presenter: UINavigationController
  private let kanjiList: [Kanji]
  private let title: String
  private var kanjiDetailCoordinator: KanjiDetailCoordinator?
  private let kanjiStorage: KanjiStorage
  
  init(presenter: UINavigationController,
       kanjiStorage: KanjiStorage,
       list: [Kanji],
       title: String) {
    self.presenter = presenter
    self.kanjiStorage = kanjiStorage
    self.kanjiList = list
    self.title = title
  }
  
  func start() {
    let listViewController = KanjiListViewController(kanjiList: kanjiList, title: title)
    listViewController.delegate = self
    presenter.pushViewController(listViewController, animated: true)
  }
}

// MARK: - KanjiListViewControllerDelegate
extension KanjiListCoordinator: KanjiListViewControllerDelegate {
  func kanjiListViewController(_ kanjiListViewController: KanjiListViewController,
                               didSelectKanji selectedKanji: Kanji) {
    NotificationCenter.default.post(name: Notifications.KanjiDetail, object: selectedKanji)
  }
}
4. KanjiDetailCoordinator.swift
import UIKit

class KanjiDetailCoordinator: Coordinator {
  private let presenter: UINavigationController
  private var kanjiListCoordinator: KanjiListCoordinator?
  private let kanjiStorage: KanjiStorage
  private let kanji: Kanji
  
  init(presenter: UINavigationController, kanji: Kanji, kanjiStorage: KanjiStorage) {
    self.kanji = kanji
    self.presenter = presenter
    self.kanjiStorage = kanjiStorage
  }
  
  func start() {
    let kanjiDetailViewController = KanjiDetailViewController(kanji: kanji)
    kanjiDetailViewController.delegate = self
    
    presenter.pushViewController(kanjiDetailViewController, animated: true)
  }
}

// MARK: - KanjiDetailViewControllerDelegate
extension KanjiDetailCoordinator: KanjiDetailViewControllerDelegate {
  func kanjiDetailViewController(_ kanjiDetailViewController: KanjiDetailViewController,
                                 didSelectWord word: String) {
    NotificationCenter.default.post(name: Notifications.KanjiList, object: word)
  }
}
5. AppDelegate.swift
import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
  var window: UIWindow?
  private var applicationCoordinator: ApplicationCoordinator?
  
  func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    let window = UIWindow(frame: UIScreen.main.bounds)
    let applicationCoordinator = ApplicationCoordinator(window: window)
    
    self.window = window
    self.applicationCoordinator = applicationCoordinator
    
    applicationCoordinator.start()
    return true
  }
}
6. Notifications.swift
import Foundation

enum Notifications {
  static let AllKanji = NSNotification.Name(rawValue: "AllKanjiNotification")
  static let KanjiDetail = NSNotification.Name(rawValue: "KanjiDetailNotification")
  static let KanjiList = NSNotification.Name(rawValue: "KanjiListNotification")
}
7. KanjiStorage.swift
import Foundation

struct KanjiCache {
  let kanjiArray: [Kanji]
  let kanjiDictionary: [String: Kanji]
}

// Provides kanji data from JSON
class KanjiStorage {
  static let kanjiURL = Bundle.main.url(forResource: "kanji", withExtension: "json")
  private let allKanjiFromJSON: KanjiCache
  
  init() {
    // Parse json and store its data
    guard
      let kanjiURL = KanjiStorage.kanjiURL,
      let data = try? Data(contentsOf: kanjiURL),
      let allKanjis = try? JSONDecoder().decode([Kanji].self, from: data)
      else {
        preconditionFailure("Could not read kanji from kanji.json")
    }
    
    let kanjiDictionary = allKanjis.reduce([:]) { (dictionary, kanji) -> [String: Kanji] in
      var dictionary = dictionary
      dictionary[kanji.character] = kanji
      return dictionary
    }
    
    // Save new cache
    allKanjiFromJSON = KanjiCache(kanjiArray: allKanjis,
                                  kanjiDictionary: kanjiDictionary)
  }
  
  func allKanji() -> [Kanji] {
    return allKanjiFromJSON.kanjiArray
  }
  
  func kanjiForWord(_ word: String) -> [Kanji] {
    let kanjiInWord: [Kanji] = word.compactMap { (character) -> Kanji? in
      let kanjiForCharacter = allKanjiFromJSON.kanjiDictionary["\(character)"]
      return kanjiForCharacter
    }
    return kanjiInWord
  }
}
8. Kanji.swift
import Foundation

struct WordExample: Codable {
  let word: String
  let meaning: String
}

struct Kanji: Codable {
  let character: String
  let meaning: String
  let examples: [WordExample]
}
9. KanjiViewController.swift
import UIKit

class KanjiViewController: UIViewController {
  override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
    super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
    let allButton = UIBarButtonItem(title: "All",
                                    style: .plain,
                                    target: self,
                                    action: #selector(allTapped))
    navigationItem.rightBarButtonItem = allButton
  }
  
  required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }
  
  @objc private func allTapped() {
    NotificationCenter.default.post(name: Notifications.AllKanji, object: nil)
  }
}
10. KanjiDetailViewController.swift
import Foundation
import UIKit

protocol KanjiDetailViewControllerDelegate: class {
  func kanjiDetailViewController(_ kanjiDetailViewController: KanjiDetailViewController,
                                 didSelectWord word: String)
}

class KanjiDetailViewController: KanjiViewController {
  let selectedKanji: Kanji
  var delegate: KanjiDetailViewControllerDelegate?
  
  @IBOutlet weak var detailTableView: UITableView? {
    didSet {
      guard let detailTableView = detailTableView else { return }
      detailTableView.dataSource = self
      detailTableView.delegate = self
      
      // Word cell
      let wordCellNib = UINib(nibName: "WordExampleTableViewCell", bundle: nil)
      detailTableView.register(wordCellNib, forCellReuseIdentifier: "WordExampleTableViewCell")
      
      // Detail cell
      let detailCellNib = UINib(nibName: "KanjiDataTableViewCell", bundle: nil)
      detailTableView.register(detailCellNib, forCellReuseIdentifier: "KanjiDataTableViewCell")
    }
  }
  
  init(kanji: Kanji) {
    selectedKanji = kanji
    super.init(nibName: nil, bundle: nil)
    title = "Kanji details"
  }
  
  required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }
}

// MARK: - UITableViewDataSource
extension KanjiDetailViewController: UITableViewDataSource {
  func numberOfSections(in tableView: UITableView) -> Int {
    return 2
  }
  
  func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
    switch section {
    case 1: return "Words"
    default: return nil
    }
  }
  
  func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    switch section {
    case 0: return 1
    case 1: return selectedKanji.examples.count
    default: return 0
    }
  }
  
  func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    switch indexPath.section {
    case 0:
      let cell = tableView.dequeueReusableCell(withIdentifier: "KanjiDataTableViewCell", for: indexPath)
      (cell as? KanjiDataTableViewCell)?.setupCell(data: selectedKanji)
      return cell
    case 1:
      let cell = tableView.dequeueReusableCell(withIdentifier: "WordExampleTableViewCell", for: indexPath)
      let word = selectedKanji.examples[indexPath.row]
      (cell as? WordExampleTableViewCell)?.setupCell(data: word)
      return cell
    default:
      fatalError()
    }
  }
}

// MARK: - UITableViewDelegate
extension KanjiDetailViewController: UITableViewDelegate {
  func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    defer {
      tableView.deselectRow(at: indexPath, animated: true)
    }
    
    guard indexPath.section == 1 else {
      return
    }
    let word = selectedKanji.examples[indexPath.row].word
    delegate?.kanjiDetailViewController(self, didSelectWord: word)
  }
}
11. KanjiListViewController.swift
import UIKit

protocol KanjiListViewControllerDelegate: class {
  func kanjiListViewController(_ kanjiListViewController: KanjiListViewController,
                               didSelectKanji selectedKanji: Kanji)
}

class KanjiListViewController: KanjiViewController {
  let kanjiList: [Kanji]
  var delegate: KanjiListViewControllerDelegate?
  
  @IBOutlet weak var kanjiListTableView: UITableView! {
    didSet {
      kanjiListTableView?.dataSource = self
      kanjiListTableView?.delegate = self
    }
  }
  
  init(kanjiList: [Kanji], title: String) {
    self.kanjiList = kanjiList
    super.init(nibName: nil, bundle: nil)
    self.title = title
  }
  
  required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }
  
}

extension KanjiListViewController: UITableViewDataSource, UITableViewDelegate {
  func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return kanjiList.count
  }
  
  func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell: UITableViewCell
    if let dequeuedCell = tableView.dequeueReusableCell(withIdentifier: "ListItem") {
      cell = dequeuedCell
    } else {
      cell = UITableViewCell(style: .subtitle, reuseIdentifier: "ListItem")
    }
    let kanji = kanjiList[indexPath.row]
    cell.textLabel?.text = kanji.character
    cell.detailTextLabel?.text = kanji.meaning
    cell.accessoryType = .disclosureIndicator
    return cell
  }
  
  func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    let kanji = kanjiList[indexPath.row]
    delegate?.kanjiListViewController(self, didSelectKanji: kanji)
    tableView.deselectRow(at: indexPath, animated: true)
  }
}
12. KanjiStateMachine.swift
import UIKit
import GameplayKit.GKStateMachine

class KanjiStateMachine: GKStateMachine {
  let presenter: UINavigationController
  let kanjiStorage: KanjiStorage
  
  init(presenter: UINavigationController,
       kanjiStorage: KanjiStorage,
       states: [GKState]) {
    // 1
    self.presenter = presenter
    
    // 2
    self.kanjiStorage = kanjiStorage
    
    // 3
    super.init(states: states)
  }
}
13. AllState.swift
import GameplayKit.GKState

class AllState: GKState {
  // 1
  lazy var allKanjiListCoordinator = makeAllKanjiCoordinator()
  
  // 2
  override func didEnter(from previousState: GKState?) {
    if previousState == nil {
      allKanjiListCoordinator?.start()
    } else {
      (stateMachine as? KanjiStateMachine)?.presenter.popToRootViewController(animated: true)
    }
  }
  
  override func isValidNextState(_ stateClass: AnyClass) -> Bool {
    return stateClass == DetailState.self
  }
  
  private func makeAllKanjiCoordinator() -> KanjiListCoordinator? {
    // 3
    guard let kanjiStateMachine = stateMachine as? KanjiStateMachine else {
      return nil
    }
    
    let kanjiStorage = kanjiStateMachine.kanjiStorage
    
    // 4
    return KanjiListCoordinator(presenter: kanjiStateMachine.presenter,
                                kanjiStorage: kanjiStorage,
                                list: kanjiStorage.allKanji(),
                                title: "Kanji List")
  }
}
14. DetailState.swift
import GameplayKit.GKState

class DetailState: GKState {
  // 1
  var kanji: Kanji?
  var kanjiDetailCoordinator: KanjiDetailCoordinator?
  
  override func didEnter(from previousState: GKState?) {
    guard
      let kanji = kanji,
      let kanjiStateMachine = (stateMachine as? KanjiStateMachine)
      else {
        return
    }
    
    // 2
    let kanjiDetailCoordinator = KanjiDetailCoordinator(
      presenter: kanjiStateMachine.presenter,
      kanji: kanji,
      kanjiStorage: kanjiStateMachine.kanjiStorage)
    
    self.kanjiDetailCoordinator = kanjiDetailCoordinator
    kanjiDetailCoordinator.start()
  }

  override func isValidNextState(_ stateClass: AnyClass) -> Bool {
    return stateClass == AllState.self || stateClass == ListState.self
  }
}
15. ListState.swift
import GameplayKit.GKState

class ListState: GKState {
  // 1
  var word: String?
  var kanjiListCoordinator: KanjiListCoordinator?
  
  override func didEnter(from previousState: GKState?) {
    guard
      let word = word,
      let kanjiStateMachine = (stateMachine as? KanjiStateMachine)
      else {
        return
    }
    
    let kanjiStorage = kanjiStateMachine.kanjiStorage
    
    // 2
    let kanjiForWord = kanjiStorage.kanjiForWord(word)
    
    // 3
    let kanjiListCoordinator = KanjiListCoordinator(
      presenter: kanjiStateMachine.presenter, kanjiStorage: kanjiStorage,
      list: kanjiForWord, title: word)
    
    self.kanjiListCoordinator = kanjiListCoordinator
    kanjiListCoordinator.start()
  }
  
  override func isValidNextState(_ stateClass: AnyClass) -> Bool {
    return stateClass == DetailState.self || stateClass == AllState.self
  }
}

后记

本篇主要讲述了GameplayKit的实用状态机,感兴趣的给个赞或者关注~~~

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

推荐阅读更多精彩内容