MapKit框架详细解析(八) —— 添加自定义图块(三)

版本记录

版本号 时间
V1.0 2019.04.25 星期四

前言

MapKit框架直接从您的应用界面显示地图或卫星图像,调出兴趣点,并确定地图坐标的地标信息。接下来几篇我们就一起看一下这个框架。感兴趣的看下面几篇文章。
1. MapKit框架详细解析(一) —— 基本概览(一)
2. MapKit框架详细解析(二) —— 基本使用简单示例(一)
3. MapKit框架详细解析(三) —— 基本使用简单示例(二)
4. MapKit框架详细解析(四) —— 一个叠加视图相关的简单示例(一)
5. MapKit框架详细解析(五) —— 一个叠加视图相关的简单示例(二)
6. MapKit框架详细解析(六) —— 添加自定义图块(一)
7. MapKit框架详细解析(七) —— 添加自定义图块(二)

源码

1. Swift

首先看下文档组织结构

接着看下sb文件

下面就是源码了

1. AppDelegate.swift
import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

  var window: UIWindow?
  let locationListener = LocationListener()

  func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    Game.shared.adventurer = Adventurer(name: "Hero", hitPoints: 10, strength: 10, gold: 40)
    return true
  }
}
2. LocationListener.swift
import Foundation
import CoreLocation

class LocationListener: NSObject {

  // MARK: - Properties
  let manager = CLLocationManager()

  // MARK: - Initializers
  override init() {
    super.init()
    manager.delegate = self
    manager.activityType = .other
    manager.requestWhenInUseAuthorization()
  }
}

// MARK: - CLLocationManagerDelegate
extension LocationListener: CLLocationManagerDelegate {

  func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
    if status == .authorizedWhenInUse {
      manager.startUpdatingLocation()
    }
  }

  func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
    print(error)
  }

  func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
    guard let lastLocation = locations.last else { return }

    Game.shared.visitedLocation(location: lastLocation)
  }
}
3. MapViewController.swift
import UIKit
import MapKit

class MapViewController: UIViewController {

  // MARK: - IBOutlets
  @IBOutlet weak var mapView: MKMapView!
  @IBOutlet weak var heartsLabel: UILabel!

  // MARK: - Properties
  var tileRenderer: MKTileOverlayRenderer!
  var shimmerRenderer: ShimmerRenderer!

  // MARK: - View Life Cycle
  override func viewDidLoad() {
    super.viewDidLoad()

    setupTileRenderer()
    setupLakeOverlay()

    let initialRegion = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 40.774669555422349, longitude: -73.964170794293238),
                                           span: MKCoordinateSpan(latitudeDelta: 0.16405544070813249, longitudeDelta: 0.1232528799585566))
    mapView.region = initialRegion
    mapView.showsUserLocation = true
    mapView.showsCompass = true
    mapView.setUserTrackingMode(.followWithHeading, animated: true)

    Game.shared.delegate = self

    NotificationCenter.default.addObserver(self, selector: #selector(gameUpdated(notification:)), name: GameStateNotification, object: nil)

    mapView.delegate = self
    mapView.addAnnotations(Game.shared.warps)
  }

  func setupTileRenderer() {
    let overlay = AdventureMapOverlay()
    
    overlay.canReplaceMapContent = true
    mapView.add(overlay, level: MKOverlayLevel.aboveLabels)
    tileRenderer = MKTileOverlayRenderer(tileOverlay: overlay)
    
    overlay.minimumZ = 13
    overlay.maximumZ = 16
  }

  func setupLakeOverlay() {
    
    // 1
    let lake = MKPolygon(coordinates: &Game.shared.reservoir, count: Game.shared.reservoir.count)
    mapView.add(lake)
    
    // 2
    shimmerRenderer = ShimmerRenderer(overlay: lake)
    shimmerRenderer.fillColor = #colorLiteral(red: 0.2431372549, green: 0.5803921569, blue: 0.9764705882, alpha: 1)
    
    // 3
    Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak self] _ in
      self?.shimmerRenderer.updateLocations()
      self?.shimmerRenderer.setNeedsDisplay()
    }
  }
  
  @objc func gameUpdated(notification: Notification) {
    renderGame()
  }

  override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    renderGame()
  }

  override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    if segue.identifier == "shop", let shopController = segue.destination as? ShopViewController, let store = sender as? Store {
      shopController.shop = store
    }
  }
}

// MARK: - MapView Delegate
extension MapViewController: MKMapViewDelegate {

  func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
    if overlay is AdventureMapOverlay {
      return tileRenderer
    } else {
      return shimmerRenderer
    }
  }
  
  func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
    switch annotation {
    case let user as MKUserLocation:
      let view = mapView.dequeueReusableAnnotationView(withIdentifier: "user")
        ?? MKAnnotationView(annotation: user, reuseIdentifier: "user")

      view.image = #imageLiteral(resourceName: "user")
      return view

    case let warp as WarpZone:
      let view = mapView.dequeueReusableAnnotationView(withIdentifier: WarpAnnotationView.identifier)
        ?? WarpAnnotationView(annotation: warp, reuseIdentifier: WarpAnnotationView.identifier)
      view.annotation = warp
      return view
      
    default:
      return nil
    }
  }
}

// MARK: - Game UI
extension MapViewController {

  private func heartsString() -> String {
    guard let hp = Game.shared.adventurer?.hitPoints else { return "☠️" }

    let heartCount = hp / 2
    var string = ""
    for _ in 1 ... heartCount {
      string += "❤️"
    }
    return string
  }

  private func goldString() -> String {
    guard let gold = Game.shared.adventurer?.gold else { return "" }
    return "💰\(gold)"
  }

  fileprivate func renderGame() {
    heartsLabel.text = heartsString() + "\n" + goldString()
  }
}

// MARK: - Game Delegate
extension MapViewController: GameDelegate {

  func encounteredMonster(monster: Monster) {
    showFight(monster: monster)
  }

  func showFight(monster: Monster, subtitle: String = "Fight?") {
    let alert = AABlurAlertController()

    alert.addAction(action: AABlurAlertAction(title: "Run", style: .cancel) { [unowned self] _ in
      self.showFight(monster: monster, subtitle: "I think you should really fight this.")
    })

    alert.addAction(action: AABlurAlertAction(title: "Fight", style: .default) { [unowned self] _ in
      guard let result = Game.shared.fight(monster: monster) else { return }

      switch result {
      case .HeroLost:
        print("loss!")
      case .HeroWon:
        print("win!")
      case .Tie:
        self.showFight(monster: monster, subtitle: "A good row, but you are both still in the fight!")
      }
    })

    alert.blurEffectStyle = .regular

    let image = Game.shared.image(for: monster)
    alert.alertImage.image = image
    alert.alertTitle.text = "A wild \(monster.name) appeared!"
    alert.alertSubtitle.text = subtitle
    present(alert, animated: true)
  }

  func encounteredNPC(npc: NPC) {
    let alert = AABlurAlertController()

    alert.addAction(action: AABlurAlertAction(title: "No Thanks", style: .cancel) {  _ in
      print("done with encounter")
    })

    alert.addAction(action: AABlurAlertAction(title: "On My Way", style: .default) {  _ in
      print("did not buy anything")
    })

    alert.blurEffectStyle = .regular

    let image = Game.shared.image(for: npc)
    alert.alertImage.image = image
    alert.alertTitle.text = npc.name
    alert.alertSubtitle.text = npc.quest
    present(alert, animated: true)
  }

  func enteredStore(store: Store) {
    let alert = AABlurAlertController()

    alert.addAction(action: AABlurAlertAction(title: "Back Out", style: .cancel) {  _ in
      print("did not buy anything")
    })

    alert.addAction(action: AABlurAlertAction(title: "Take My 💰", style: .default) { [unowned self] _ in
      self.performSegue(withIdentifier: "shop", sender: store)
    })

    alert.blurEffectStyle = .regular

    let image = Game.shared.image(for: store)
    alert.alertImage.image = image
    alert.alertTitle.text = store.name
    alert.alertSubtitle.text = "Shopping for accessories?"
    present(alert, animated: true)
  }
}
4. AdventureMapOverlay.swift
import Foundation
import MapKit

class AdventureMapOverlay: MKTileOverlay {

  override func url(forTilePath path: MKTileOverlayPath) -> URL {
    let tilePath = Bundle.main.url(
      forResource: "\(path.y)",
      withExtension: "png",
      subdirectory: "tiles/\(path.z)/\(path.x)",
      localization: nil)
    
    guard let tile = tilePath else {
      return Bundle.main.url(
        forResource: "parchment",
        withExtension: "png",
        subdirectory: "tiles",
        localization: nil)!
    }

    return tile
  }
}
5. ShimmerRenderer.swift
import UIKit
import MapKit

class ShimmerRenderer: MKPolygonRenderer {

  // MARK: - Properties
  var iteration = 0
  var locations: [CGFloat] = [0, 0, 0]

  func updateLocations() {
    iteration = (iteration + 1) % 15
    let minL = max(0, CGFloat(iteration - 1) / 15.0)
    let maxL = min(1.0, CGFloat(iteration + 1) / 15.0)
    let center = CGFloat(iteration) / 15.0
    locations = [minL, center, maxL]
  }

  // MARK: - Overridden
  override func draw(_ mapRect: MKMapRect, zoomScale: MKZoomScale, in context: CGContext) {
    super.draw(mapRect, zoomScale: zoomScale, in: context)

    UIGraphicsPushContext(context)

    let boundingRect = self.path.boundingBoxOfPath
    let minX = boundingRect.minX
    let maxX = boundingRect.maxX

    let colors = [#colorLiteral(red: 0.2431372549, green: 0.5803921569, blue: 0.9764705882, alpha: 1).cgColor, #colorLiteral(red: 0.9999960065, green: 1, blue: 1, alpha: 0.8523706897).cgColor, #colorLiteral(red: 0.2431372549, green: 0.5803921569, blue: 0.9764705882, alpha: 1).cgColor]
    let gradient = CGGradient(colorsSpace: nil, colors: colors as CFArray, locations: locations)
    context.addPath(self.path)
    context.clip()
    context.drawLinearGradient(gradient!, start: CGPoint(x: minX, y: 0), end: CGPoint(x: maxX, y: 0), options: [])

    UIGraphicsPopContext()
  }
}
6. HeroViewController.swift
import UIKit

class HeroViewController: UIViewController {

  // MARK: - IBOutlets
  @IBOutlet weak var avatarImageView: UIImageView!
  @IBOutlet weak var nameLabel: UILabel!

  // MARK: - View Life Cycle
  override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)

    avatarImageView.image = #imageLiteral(resourceName: "adventurer")
  }
}

// MARK: - UICollectionViewDataSource
extension HeroViewController: UICollectionViewDataSource {

  var inventory: [Item] { return Game.shared.adventurer?.inventory ?? [] }

  func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    return inventory.count
  }

  func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath)
    let imageView = cell.viewWithTag(1) as! UIImageView
    let label = cell.viewWithTag(2) as! UILabel

    let item = inventory[indexPath.row]
    imageView.image = Game.shared.image(for: item)

    label.text = ""
    if let weapon = item as? Weapon {
      label.text = "+\(weapon.strength)"
    }

    cell.layer.cornerRadius = 8
    cell.layer.borderColor = UIColor.black.cgColor
    cell.layer.borderWidth = 1

    return cell
  }
}
7. ShopViewController.swift
import Foundation
import UIKit

class ShopViewController: UIViewController {

  // MARK: - Properties
  var shop: Store!

  // MARK: - View Life Cycle
  override func viewDidLoad() {
    super.viewDidLoad()

    title = shop?.name
  }
}

// MARK: - UICollectionViewDataSource
extension ShopViewController: UICollectionViewDataSource {

  func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    return shop.inventory.count
  }

  func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath)
    let imageView = cell.viewWithTag(1) as! UIImageView
    let label = cell.viewWithTag(2) as! UILabel

    let item = shop.inventory[indexPath.row]
    imageView.image = Game.shared.image(for: item)

    let price = item.cost
    label.text = "💰\(price)"

    cell.layer.cornerRadius = 8
    cell.layer.borderColor = UIColor.black.cgColor
    cell.layer.borderWidth = 1

    return cell
  }
}

// MARK: - UICollectionViewDelegate
extension ShopViewController: UICollectionViewDelegate {

  func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
    let item = shop.inventory[indexPath.row]
    _ = Game.shared.purchaseItem(item: item)
    _ = navigationController?.popViewController(animated: true)
  }
}
8. PointOfInterest+MapKit.swift
import Foundation
import MapKit

extension PointOfInterest: MKAnnotation {

  var coordinate: CLLocationCoordinate2D { return location.coordinate }
  var title: String? { return name }
}
9. WarpZone.swift
import MapKit
import UIKit

class WarpZone: NSObject, MKAnnotation {

  // MARK: - Properties
  let coordinate: CLLocationCoordinate2D
  let color: UIColor

  // MARK: - Initializers
  init(latitude: CLLocationDegrees, longitude: CLLocationDegrees, color: UIColor) {
    self.coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
    self.color = color
    super.init()
  }
}

extension WarpZone {
  var image: UIImage {
    return #imageLiteral(resourceName: "warp").maskWithColor(color: self.color)
  }
}

class WarpAnnotationView: MKAnnotationView {
  static let identifier = "WarpZone"

  override var annotation: MKAnnotation? {
    get { return super.annotation }
    set {
      super.annotation = newValue
      guard let warp = newValue as? WarpZone else { return }

      self.image = warp.image
    }
  }
}

extension UIImage {

  func maskWithColor(color: UIColor) -> UIImage {
    UIGraphicsBeginImageContextWithOptions(size, false, UIScreen.main.scale)
    let context = UIGraphicsGetCurrentContext()!

    color.setFill()

    context.translateBy(x: 0, y: size.height)
    context.scaleBy(x: 1.0, y: -1.0)

    let rect = CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height)
    context.draw(cgImage!, in: rect)

    context.setBlendMode(.sourceIn)
    context.addRect(rect)
    context.drawPath(using: .fill)

    let coloredImage = UIGraphicsGetImageFromCurrentImageContext()
    UIGraphicsEndImageContext()

    return coloredImage!
  }
}
10. Game.swift
import UIKit
import CoreLocation

let ENCOUNTER_RADIUS: CLLocationDistance = 10 //meters

enum FightResult {
  case HeroWon, HeroLost, Tie
}

enum ItemResult {
  case Purchased, NotEnoughMoney
}

let GameStateNotification = Notification.Name("GameUpdated")

protocol GameDelegate: class {
  func encounteredMonster(monster: Monster)
  func encounteredNPC(npc: NPC)
  func enteredStore(store: Store)
}

class Game {
  static let shared = Game()
  var adventurer: Adventurer?
  var pointsOfInterest: [PointOfInterest] = []
  var lastPOI: PointOfInterest?
  var warps: [WarpZone] = []
  var reservoir: [CLLocationCoordinate2D] = []

  weak var delegate: GameDelegate?

  init() {
    adventurer = Adventurer(name: "Hero", hitPoints: 10, strength: 10)
    setupPOIs()
    setupWarps()
    setupResevoir()
  }

  private func setupPOIs() {
    pointsOfInterest = [.AppleStore, .Balto, .BoatHouse, .Castle, .Cloisters, .Hamilton, .Obelisk, .Met, .StrawberryFields, .StatueOfLiberty, .TavernOnGreen, .TimesSquare, .Zoo]
  }

  private func setupWarps() {
    warps = [WarpZone(latitude: 40.765158, longitude: -73.974774, color: #colorLiteral(red: 0.9882352941, green: 0.8, blue: 0.03921568627, alpha: 1)),
             WarpZone(latitude: 40.768712, longitude: -73.981590, color: #colorLiteral(red: 1, green: 0.3882352941, blue: 0.09803921569, alpha: 1)),
             WarpZone(latitude: 40.768712, longitude: -73.981590, color: #colorLiteral(red: 0, green: 0.2235294118, blue: 0.6509803922, alpha: 1)),
             WarpZone(latitude: 40.776219, longitude: -73.976247, color: #colorLiteral(red: 1, green: 0.3882352941, blue: 0.09803921569, alpha: 1)),
             WarpZone(latitude: 40.776219, longitude: -73.976247, color: #colorLiteral(red: 0, green: 0.2235294118, blue: 0.6509803922, alpha: 1)),
             WarpZone(latitude: 40.781987, longitude: -73.972020, color: #colorLiteral(red: 1, green: 0.3882352941, blue: 0.09803921569, alpha: 1)),
             WarpZone(latitude: 40.781987, longitude: -73.972020, color: #colorLiteral(red: 0, green: 0.2235294118, blue: 0.6509803922, alpha: 1)),
             WarpZone(latitude: 40.785253, longitude: -73.969638, color: #colorLiteral(red: 1, green: 0.3882352941, blue: 0.09803921569, alpha: 1)),
             WarpZone(latitude: 40.785253, longitude: -73.969638, color: #colorLiteral(red: 0, green: 0.2235294118, blue: 0.6509803922, alpha: 1)),
             WarpZone(latitude: 40.791605, longitude: -73.964853, color: #colorLiteral(red: 1, green: 0.3882352941, blue: 0.09803921569, alpha: 1)),
             WarpZone(latitude: 40.791605, longitude: -73.964853, color: #colorLiteral(red: 0, green: 0.2235294118, blue: 0.6509803922, alpha: 1)),
             WarpZone(latitude: 40.796089, longitude: -73.961463, color: #colorLiteral(red: 1, green: 0.3882352941, blue: 0.09803921569, alpha: 1)),
             WarpZone(latitude: 40.796089, longitude: -73.961463, color: #colorLiteral(red: 0, green: 0.2235294118, blue: 0.6509803922, alpha: 1)),
             WarpZone(latitude: 40.799988, longitude: -73.958480, color: #colorLiteral(red: 1, green: 0.3882352941, blue: 0.09803921569, alpha: 1)),
             WarpZone(latitude: 40.799988, longitude: -73.958480, color: #colorLiteral(red: 0, green: 0.2235294118, blue: 0.6509803922, alpha: 1)),
             WarpZone(latitude: 40.798493, longitude: -73.952622, color: #colorLiteral(red: 0.9333333333, green: 0.2078431373, blue: 0.1803921569, alpha: 1)),
             WarpZone(latitude: 40.755238, longitude: -73.987405, color: #colorLiteral(red: 0.7254901961, green: 0.2, blue: 0.6784313725, alpha: 1)),
             WarpZone(latitude: 40.754344, longitude: -73.987105, color: #colorLiteral(red: 0.9882352941, green: 0.8, blue: 0.03921568627, alpha: 1)),
             WarpZone(latitude: 40.865757, longitude: -73.927088, color: #colorLiteral(red: 0, green: 0.2235294118, blue: 0.6509803922, alpha: 1)),
             WarpZone(latitude: 40.701789, longitude: -74.013004, color: #colorLiteral(red: 0.9333333333, green: 0.2078431373, blue: 0.1803921569, alpha: 1))
    ]
  }

  private func setupResevoir() {
    reservoir = [
    CLLocationCoordinate2D(latitude: 40.78884, longitude: -73.95857),
    CLLocationCoordinate2D(latitude: 40.78889, longitude: -73.95824),
    CLLocationCoordinate2D(latitude: 40.78882, longitude: -73.95786),
    CLLocationCoordinate2D(latitude: 40.78867, longitude: -73.95758),
    CLLocationCoordinate2D(latitude: 40.78838, longitude: -73.95749),
    CLLocationCoordinate2D(latitude: 40.78793, longitude: -73.95764),
    CLLocationCoordinate2D(latitude: 40.78744, longitude: -73.95777),
    CLLocationCoordinate2D(latitude: 40.78699, longitude: -73.95777),
    CLLocationCoordinate2D(latitude: 40.78655, longitude: -73.95779),
    CLLocationCoordinate2D(latitude: 40.78609, longitude: -73.95818),
    CLLocationCoordinate2D(latitude: 40.78543, longitude: -73.95867),
    CLLocationCoordinate2D(latitude: 40.78469, longitude: -73.95919),
    CLLocationCoordinate2D(latitude: 40.78388, longitude: -73.95975),
    CLLocationCoordinate2D(latitude: 40.78325, longitude: -73.96022),
    CLLocationCoordinate2D(latitude: 40.78258, longitude: -73.96067),
    CLLocationCoordinate2D(latitude: 40.78227, longitude: -73.96101),
    CLLocationCoordinate2D(latitude: 40.78208, longitude: -73.96136),
    CLLocationCoordinate2D(latitude: 40.782, longitude: -73.96172),
    CLLocationCoordinate2D(latitude: 40.78201, longitude: -73.96202),
    CLLocationCoordinate2D(latitude: 40.78214, longitude: -73.96247),
    CLLocationCoordinate2D(latitude: 40.78237, longitude: -73.96279),
    CLLocationCoordinate2D(latitude: 40.78266, longitude: -73.96309),
    CLLocationCoordinate2D(latitude: 40.7832, longitude: -73.96331),
    CLLocationCoordinate2D(latitude: 40.78361, longitude: -73.96363),
    CLLocationCoordinate2D(latitude: 40.78382, longitude: -73.96395),
    CLLocationCoordinate2D(latitude: 40.78401, longitude: -73.96453),
    CLLocationCoordinate2D(latitude: 40.78416, longitude: -73.96498),
    CLLocationCoordinate2D(latitude: 40.78437, longitude: -73.9656),
    CLLocationCoordinate2D(latitude: 40.78456, longitude: -73.96601),
    CLLocationCoordinate2D(latitude: 40.78479, longitude: -73.96636),
    CLLocationCoordinate2D(latitude: 40.78502, longitude: -73.96661),
    CLLocationCoordinate2D(latitude: 40.78569, longitude: -73.96659),
    CLLocationCoordinate2D(latitude: 40.78634, longitude: -73.9664),
    CLLocationCoordinate2D(latitude: 40.78705, longitude: -73.96623),
    CLLocationCoordinate2D(latitude: 40.78762, longitude: -73.96603),
    CLLocationCoordinate2D(latitude: 40.78791, longitude: -73.96571),
    CLLocationCoordinate2D(latitude: 40.78816, longitude: -73.96533),
    CLLocationCoordinate2D(latitude: 40.78822, longitude: -73.9649),
    CLLocationCoordinate2D(latitude: 40.7882, longitude: -73.96445),
    CLLocationCoordinate2D(latitude: 40.78819, longitude: -73.96404),
    CLLocationCoordinate2D(latitude: 40.78814, longitude: -73.96378),
    CLLocationCoordinate2D(latitude: 40.7882, longitude: -73.96354),
    CLLocationCoordinate2D(latitude: 40.78819, longitude: -73.96327),
    CLLocationCoordinate2D(latitude: 40.78817, longitude: -73.96301),
    CLLocationCoordinate2D(latitude: 40.7882, longitude: -73.96269),
    CLLocationCoordinate2D(latitude: 40.7882, longitude: -73.96245),
    CLLocationCoordinate2D(latitude: 40.7883, longitude: -73.96217),
    CLLocationCoordinate2D(latitude: 40.7885, longitude: -73.96189),
    CLLocationCoordinate2D(latitude: 40.78874, longitude: -73.96161),
    CLLocationCoordinate2D(latitude: 40.78884, longitude: -73.96127),
    CLLocationCoordinate2D(latitude: 40.78885, longitude: -73.96093),
    CLLocationCoordinate2D(latitude: 40.78879, longitude: -73.9606),
    CLLocationCoordinate2D(latitude: 40.78869, longitude: -73.96037),
    CLLocationCoordinate2D(latitude: 40.78864, longitude: -73.96009),
    CLLocationCoordinate2D(latitude: 40.78863, longitude: -73.95972),
    CLLocationCoordinate2D(latitude: 40.78863, longitude: -73.95936),
    CLLocationCoordinate2D(latitude: 40.78867, longitude: -73.95895)]
  }

  func visitedLocation(location: CLLocation) {
    guard let currentPOI = poiAtLocation(location: location) else { return }
    if currentPOI.isRegenPoint {
      regenAdventurer()
    }

    switch currentPOI.encounter {
    case let npc as NPC:
      delegate?.encounteredNPC(npc: npc)
    case let monster as Monster:
      delegate?.encounteredMonster(monster: monster)
    case let store as Store:
      delegate?.enteredStore(store: store)
    default:
      break
    }
  }

  func poiAtLocation(location: CLLocation) -> PointOfInterest? {
    for point in pointsOfInterest {
      let center = point.location
      let distance = abs(location.distance(from: center))
      if distance < ENCOUNTER_RADIUS {
        //debounce staying in the same spot for awhile
        if point != lastPOI {
          lastPOI = point
          return point
        } else {
          return nil
        }
      }
    }
    lastPOI = nil
    return nil
  }

  func regenAdventurer() {
    guard let adventurer = adventurer else { return }
    adventurer.hitPoints = adventurer.maxHitPoints
    adventurer.isDefeated = false
  }

  func fight(monster: Monster) -> FightResult? {
    guard let adventurer = adventurer else { return nil }
    defer { NotificationCenter.default.post(name: GameStateNotification, object: self) }

    //give the hero a fighting chance
    monster.hitPoints -= adventurer.strength
    if monster.hitPoints <= 0 {
      adventurer.gold += monster.gold
      return .HeroWon
    }

    adventurer.hitPoints -= monster.strength
    if adventurer.hitPoints <= 0 {
      adventurer.isDefeated = true
      return .HeroLost
    }

    return .Tie
  }

  func purchaseItem(item: Item) -> ItemResult? {
    guard let adventurer = adventurer else { return nil }
    defer { NotificationCenter.default.post(name: GameStateNotification, object: self) }

    if adventurer.gold >= item.cost {
      adventurer.gold -= item.cost
      adventurer.inventory.append(item)
      return .Purchased
    } else {
      return .NotEnoughMoney
    }

  }
}

extension Game {
  func image(for monster: Monster) -> UIImage? {
    switch monster.name {
    case Monster.Goblin.name:
      return UIImage(named: "goblin")
    case NPC.King.name:
      return UIImage(named: "king")
    default:
      return nil
    }
  }

  func image(for store: Store) -> UIImage? {
    return UIImage(named: "store")
  }

  func image(for item: Item) -> UIImage? {
    switch item.name {
    case Weapon.Sword6Plus.name:
      return UIImage(named: "sword")
    default:
      return nil
    }

  }
}
11. Monster.swift
import Foundation

class Monster {

  // MARK: - Properties
  let name: String
  var hitPoints: Int
  var baseStrength: Int
  var gold: Int

  var strength: Int { return baseStrength }

  // MARK: - Initializers
  init(name: String, hitPoints: Int, strength: Int, gold: Int = 0) {
    self.name = name
    self.hitPoints = hitPoints
    self.baseStrength = strength
    self.gold = gold
  }
}

extension Monster {
  static let Goblin = Monster(name: "Goblin", hitPoints: 1, strength: 1, gold: 10)
}
12. NPC.swift
import Foundation

class NPC: Monster {

  // MARK: - Properties
  let quest: String

  // MARK: - Initializers
  init(quest: String, name: String) {
    self.quest = quest
    super.init(name: name, hitPoints: 0, strength: 0)
  }
}

extension NPC {
  static let King = NPC(quest: "Bring me the ears of ten goblins, and you'll get a great reward", name: "King")
}
13. Adventurer.swift
import Foundation

class Adventurer: Monster {

  // MARK: - Properties
  var isDefeated = false
  var maxHitPoints: Int = 0
  var inventory: [Item] = []

  override var strength: Int {
    return baseStrength + inventory.filter { $0 is Weapon}.reduce(0, { max($0, ($1 as! Weapon).strength) })
  }

  // MARK: - Initializers
  override init(name: String, hitPoints: Int, strength: Int, gold: Int = 100) {
    super.init(name: name, hitPoints: hitPoints, strength: strength, gold: gold)
    maxHitPoints = hitPoints
  }
}
14. PointOfInterest.swift
import Foundation
import CoreLocation

class PointOfInterest: NSObject { //has to be NSObject to use with MKAnnotation ... boo :(

  // MARK: - Properties
  let location: CLLocation
  let name: String
  let isRegenPoint: Bool
  let encounter: Encounter?

  // MARK: - Initializers
  init(name: String, location: CLLocation, isRegenPoint: Bool, encounter: Encounter? = nil) {
    self.name = name
    self.location = location
    self.isRegenPoint = isRegenPoint
    self.encounter = encounter
  }
}

extension PointOfInterest {
  static let AppleStore = PointOfInterest(name: "\"Fruit\" Store", location: CLLocation(latitude: 40.763560, longitude: -73.972321), isRegenPoint: true, encounter: Store.AppleStore)
  static let Balto = PointOfInterest(name: "Balto Statue", location: CLLocation(latitude: 40.7699631, longitude: -73.9732103), isRegenPoint: true)
  static let BoatHouse = PointOfInterest(name: "Entrance to Water Level", location: CLLocation(latitude: 40.7772265, longitude: -73.972275), isRegenPoint: true)
  static let Castle = PointOfInterest(name: "Castle", location: CLLocation(latitude: 40.7794379, longitude: -73.9712102), isRegenPoint: false, encounter: NPC.King)
  static let Cloisters = PointOfInterest(name: "Monastery", location: CLLocation(latitude: 40.8648668, longitude: -73.9339161), isRegenPoint: false)
  static let Hamilton = PointOfInterest(name: "Warrior's Memorial", location: CLLocation(latitude: 40.7796328, longitude: -73.9676018), isRegenPoint: false)
  static let Met = PointOfInterest(name: "Art Palace", location: CLLocation(latitude: 40.7790478, longitude: -73.96627832), isRegenPoint: false)
  static let Obelisk = PointOfInterest(name: "Obelisk", location: CLLocation(latitude: 40.7796328, longitude: -73.9676018), isRegenPoint: false)
  static let StatueOfLiberty = PointOfInterest(name: "Colossus", location: CLLocation(latitude: 40.6892534, longitude: -74.0466891), isRegenPoint: false)
  static let StrawberryFields = PointOfInterest(name: "Imagine Fields", location: CLLocation(latitude: 40.775556, longitude: -73.975), isRegenPoint: true)
  static let TavernOnGreen = PointOfInterest(name: "Tavern", location: CLLocation(latitude: 40.7721909, longitude: -73.9799102), isRegenPoint: true)
  static let TimesSquare = PointOfInterest(name: "Town", location: CLLocation(latitude: 40.758899, longitude: -73.9873197), isRegenPoint: false)
  static let Zoo = PointOfInterest(name: "Monster Menagerie", location: CLLocation(latitude: 40.767769, longitude: -73.971870), isRegenPoint: false, encounter: Monster.Goblin)
}
15. Encounter.swift
import Foundation

protocol Encounter {
}

extension Monster: Encounter {
}

extension Store: Encounter {
}
16. Store.swift
import Foundation

class Store {

  // MARK: - Properties
  let name: String
  var inventory: [Item]

  // MARK: - Initializers
  init(name: String, items: [Item]) {
    self.name = name
    self.inventory = items
  }
}

extension Store {
  static let AppleStore = Store(name: "The \"Fruit\" Store", items: [Weapon.Sword6Plus])
}
17. Item.swift
import Foundation

class Item {

  // MARK: - Properties
  let name: String
  let cost: Int

  // MARK: - Initializers
  init(name: String, cost: Int) {
    self.cost = cost
    self.name = name
  }
}
18. Weapon.swift
import Foundation

class Weapon: Item {

  // MARK: - Properties
  let strength: Int

  // MARK: - Initializers
  init(name: String, cost: Int, strength: Int) {
    self.strength = strength
    super.init(name: name, cost: cost)
  }
}

extension Weapon {
  static let Sword6Plus = Weapon(name: "Sword 6+", cost: 50, strength: 6)
}
19. AABlurAlertController.swift
import UIKit

public enum AABlurActionStyle {
    case `default`, cancel
}

open class AABlurAlertAction: UIButton {
    fileprivate var handler: ((AABlurAlertAction) -> Void)? = nil
    fileprivate var style: AABlurActionStyle = AABlurActionStyle.default
    fileprivate var parent: AABlurAlertController? = nil

    public init(title: String?, style: AABlurActionStyle, handler: ((AABlurAlertAction) -> Void)?) {
        super.init(frame: CGRect.zero)

        self.style = style
        self.handler = handler

        self.addTarget(self, action: #selector(buttonTapped), for: UIControlEvents.touchUpInside)
        self.setTitle(title, for: UIControlState.normal)

        switch self.style {
        case .cancel:
            self.setTitleColor(UIColor(red:0.47, green:0.50, blue:0.55, alpha:1.00), for: UIControlState.normal)
            self.backgroundColor = UIColor(red:0.93, green:0.94, blue:0.95, alpha:1.00)
            self.layer.borderColor = UIColor(red:0.74, green:0.77, blue:0.79, alpha:1.00).cgColor
        default:
            self.setTitleColor(UIColor.white, for: UIControlState.normal)
            self.backgroundColor = UIColor(red:0.31, green:0.57, blue:0.87, alpha:1.00)
            self.layer.borderColor = UIColor(red:0.17, green:0.38, blue:0.64, alpha:1.00).cgColor
        }
        self.setTitleColor(self.titleColor(for: UIControlState.normal)?.withAlphaComponent(0.5), for: UIControlState.highlighted)
        self.layer.borderWidth = 1
        self.layer.cornerRadius = 5
        self.layer.shadowOffset = CGSize(width: 0, height: 2)
        self.layer.shadowRadius = 4
        self.layer.shadowOpacity = 0.1
    }

    required public init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }

    @objc fileprivate func buttonTapped(_ sender: AABlurAlertAction) {
        self.parent?.dismiss(animated: true, completion: {
            self.handler?(sender)
        })
    }
}

open class AABlurAlertController: UIViewController {

    open var blurEffectStyle: UIBlurEffectStyle = .light
    open var imageHeight: Float = 175
    open var alertViewWidth: Float?

    /**
     Set the max alert view width
     If you don't want to have a max width set this to nil.
     It will take 70% of the superview width by default
     Default : 450
     */
    open var maxAlertViewWidth: CGFloat? = 450

    fileprivate var backgroundImage : UIImageView = UIImageView()
    fileprivate var alertView: UIView = {
        let view = UIView()
        view.translatesAutoresizingMaskIntoConstraints = false
        view.backgroundColor = UIColor(red:0.98, green:0.98, blue:0.98, alpha:1.00)
        view.layer.cornerRadius = 5
        view.layer.shadowColor = UIColor.black.cgColor
        view.layer.shadowOffset = CGSize(width: 0, height: 15)
        view.layer.shadowRadius = 12
        view.layer.shadowOpacity = 0.22
        return view
    }()
    open var alertImage : UIImageView = {
        let imgView = UIImageView()
        imgView.translatesAutoresizingMaskIntoConstraints = false
        imgView.contentMode = .scaleAspectFit
        return imgView
    }()
    open let alertTitle : UILabel = {
        let lbl = UILabel()
        lbl.translatesAutoresizingMaskIntoConstraints = false
        lbl.font = UIFont.boldSystemFont(ofSize: 17)
        lbl.textColor = UIColor(red:0.20, green:0.22, blue:0.26, alpha:1.00)
        lbl.textAlignment = .center
        return lbl
    }()
    open let alertSubtitle : UILabel = {
        let lbl = UILabel()
        lbl.translatesAutoresizingMaskIntoConstraints = false
        lbl.font = UIFont.boldSystemFont(ofSize: 14)
        lbl.textColor = UIColor(red:0.51, green:0.54, blue:0.58, alpha:1.00)
        lbl.textAlignment = .center
        lbl.numberOfLines = 0
        return lbl
    }()

    fileprivate let buttonsStackView : UIStackView = {
        let sv = UIStackView()
        sv.translatesAutoresizingMaskIntoConstraints = false
        sv.distribution = .fillEqually
        sv.spacing = 16
        return sv
    }()

    public init() {
        super.init(nibName: nil, bundle: nil)
        self.modalTransitionStyle = .crossDissolve
    }

    required public init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    fileprivate func setup() {
        // Clean the views
        self.view.subviews.forEach{ $0.removeFromSuperview() }
        self.backgroundImage.subviews.forEach{ $0.removeFromSuperview() }
        // Set up view
        self.view.frame = UIScreen.main.bounds
        self.view.autoresizingMask = [.flexibleHeight, .flexibleWidth]
        self.view.backgroundColor = UIColor.blue.withAlphaComponent(0.5)
        // Set up background image
        self.backgroundImage.frame = self.view.bounds
        self.backgroundImage.autoresizingMask = [.flexibleHeight, .flexibleWidth]
        self.view.addSubview(backgroundImage)
        // Set up the alert view
        self.view.addSubview(alertView)
        // Set up alertImage
        self.alertView.addSubview(alertImage)
        // Set up alertTitle
        self.alertView.addSubview(alertTitle)
        // Set up alertSubtitle
        self.alertView.addSubview(alertSubtitle)
        // Set up buttonsStackView
        self.alertView.addSubview(buttonsStackView)

        // Set up background Tap
        if buttonsStackView.arrangedSubviews.count <= 0 {
            let tapGesture = UITapGestureRecognizer(target: self, action: #selector(tapOnBackground))
            self.backgroundImage.isUserInteractionEnabled = true
            self.backgroundImage.addGestureRecognizer(tapGesture)
        }

        setupConstraints()
    }

    fileprivate func setupConstraints() {
        let viewsDict: [String: Any] = [
            "alertView": alertView,
            "alertImage": alertImage,
            "alertTitle": alertTitle,
            "alertSubtitle": alertSubtitle,
            "buttonsStackView": buttonsStackView
        ]
        let spacing = 14
        let viewMetrics: [String: Any] = [
            "margin": spacing * 2,
            "buttonMargin": 10,
            "spacing": spacing,
            "alertViewWidth": 450,
            "alertImageHeight": (alertImage.image != nil) ? imageHeight : 0,
            "alertTitleHeight": 22,
            "buttonsStackViewHeight": (buttonsStackView.arrangedSubviews.count > 0) ? 40 : 0
        ]

        if let alertViewWidth = alertViewWidth {
            self.view.addConstraints(NSLayoutConstraint.constraints(
                withVisualFormat: "H:[alertView(alertViewWidth)]", options: [],
                metrics: ["alertViewWidth":alertViewWidth], views: viewsDict))
        } else {
            let widthConstraints = NSLayoutConstraint(item: alertView,
                               attribute: NSLayoutAttribute.width,
                               relatedBy: NSLayoutRelation.equal,
                               toItem: self.view,
                               attribute: NSLayoutAttribute.width,
                               multiplier: 0.7, constant: 0)
            if let maxAlertViewWidth = maxAlertViewWidth {
                widthConstraints.priority = UILayoutPriority(rawValue: 999)
                self.view.addConstraint(NSLayoutConstraint(
                    item: alertView,
                    attribute: NSLayoutAttribute.width,
                    relatedBy: NSLayoutRelation.lessThanOrEqual,
                    toItem: nil,
                    attribute: NSLayoutAttribute.width,
                    multiplier: 1,
                    constant: maxAlertViewWidth))
            }
            self.view.addConstraint(widthConstraints)
        }

        let alertSubtitleVconstraint = (alertSubtitle.text != nil) ? "spacing-[alertSubtitle]-" : ""
        [NSLayoutConstraint(item: alertView, attribute: .centerX, relatedBy: .equal,
                            toItem: view, attribute: .centerX, multiplier: 1, constant: 0),
         NSLayoutConstraint(item: alertView, attribute: .centerY, relatedBy: .equal,
                            toItem: view, attribute: .centerY, multiplier: 1, constant: 0)
            ].forEach { self.view.addConstraint($0)}
        [NSLayoutConstraint.constraints(withVisualFormat: "V:|-margin-[alertImage(alertImageHeight)]-spacing-[alertTitle(alertTitleHeight)]-\(alertSubtitleVconstraint)margin-[buttonsStackView(buttonsStackViewHeight)]-margin-|",
            options: [], metrics: viewMetrics, views: viewsDict),
         NSLayoutConstraint.constraints(withVisualFormat: "H:|-margin-[alertImage]-margin-|",
                                        options: [], metrics: viewMetrics, views: viewsDict),
         NSLayoutConstraint.constraints(withVisualFormat: "H:|-margin-[alertTitle]-margin-|",
                                        options: [], metrics: viewMetrics, views: viewsDict),
         NSLayoutConstraint.constraints(withVisualFormat: "H:|-margin-[alertSubtitle]-margin-|",
                                        options: [], metrics: viewMetrics, views: viewsDict),
         NSLayoutConstraint.constraints(withVisualFormat: "H:|-buttonMargin-[buttonsStackView]-buttonMargin-|",
                                        options: [], metrics: viewMetrics, views: viewsDict)
            ].forEach { NSLayoutConstraint.activate($0) }
    }

    open override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        setup()

        // Set up blur effect
        backgroundImage.image = snapshot()
        let blurEffect = UIBlurEffect(style: blurEffectStyle)
        let blurEffectView = UIVisualEffectView(effect: blurEffect)
        blurEffectView.frame = backgroundImage.bounds
        blurEffectView.autoresizingMask = [.flexibleWidth, .flexibleHeight]

        let vibrancyEffect = UIVibrancyEffect(blurEffect: blurEffect)
        let vibrancyEffectView = UIVisualEffectView(effect: vibrancyEffect)
        vibrancyEffectView.frame = backgroundImage.bounds
        vibrancyEffectView.autoresizingMask = [.flexibleWidth, .flexibleHeight]

        blurEffectView.contentView.addSubview(vibrancyEffectView)
        backgroundImage.addSubview(blurEffectView)
    }

    open func addAction(action: AABlurAlertAction) {
        action.parent = self
        buttonsStackView.addArrangedSubview(action)
    }

    fileprivate func snapshot() -> UIImage? {
        guard let window = UIApplication.shared.keyWindow else { return nil }
        UIGraphicsBeginImageContextWithOptions(window.bounds.size, false, window.screen.scale)
        window.drawHierarchy(in: window.bounds, afterScreenUpdates: false)
        let snapshotImage = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return snapshotImage
    }

    @objc func tapOnBackground(sender: UITapGestureRecognizer) {
        if sender.state == .ended {
            self.dismiss(animated: true, completion: nil)
        }
    }
}

后记

本篇主要介绍了添加自定义图块,感兴趣的给个赞或者关注~~~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,784评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,745评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,702评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,229评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,245评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,376评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,798评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,471评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,655评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,485评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,535评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,235评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,793评论 3 304
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,863评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,096评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,654评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,233评论 2 341

推荐阅读更多精彩内容