MapKit框架详细解析(十二) —— 自定义MapKit Tiles(二)

版本记录

版本号 时间
V1.0 2020.06.19 星期五

前言

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

源码

1. Swift

首先看下工程组织结构

MapKit框架详细解析(十二) —— 自定义MapKit Tiles(二)_第1张图片

下面就是sb中的内容

MapKit框架详细解析(十二) —— 自定义MapKit Tiles(二)_第2张图片

接着就是源码了

1. UIAlertController+Extension.swift
import UIKit

extension UIAlertController {
  func addActions(actions: [UIAlertAction]) {
    actions.forEach { action in
      self.addAction(action)
    }
  }
}
2. PointOfInterest+MapKit.swift
import Foundation
import MapKit

extension PointOfInterest: MKAnnotation {
  var coordinate: CLLocationCoordinate2D { return location.coordinate }
  var title: String? { return name }
}
3. Game.swift
import UIKit
import CoreLocation

let encounterRadius: 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
    ]
  }

  // swiftlint:disable discouraged_object_literal
  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))
    ]
  }
  // swiftlint:enable discouraged_object_literal

  // swiftlint:disable:next function_body_length
  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 < encounterRadius {
        //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
    }
  }
}
4. 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)
}
5. 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")
}
6. Adventurer.swift
import Foundation

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

  override var strength: Int {
    // swiftlint:disable:next force_cast
    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
  }
}
7. 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
  }
}

// swiftlint:disable line_length
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)
}
8. Encounter.swift
import Foundation

protocol Encounter {
}

extension Monster: Encounter {
}

extension Store: Encounter {
}
9. 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])
}
10. 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
  }
}
11. 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)
}
12. AppDelegate.swift
import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
  func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    Game.shared.adventurer = Adventurer(name: "Hero", hitPoints: 10, strength: 10, gold: 40)
    return true
  }

  // MARK: UISceneSession Lifecycle
  func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
    return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
  }
}
13. SceneDelegate.swift
import UIKit

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
  var window: UIWindow?
  let locationListener = LocationListener()
}
14. 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)
  }
}
15. MapViewController.swift
import UIKit
import MapKit

class MapViewController: UIViewController {
  // MARK: - IBOutlets
  @IBOutlet weak var mapView: MKMapView!
  @IBOutlet weak var heartsLabel: UILabel!

  // MARK: - Properties
  // swiftlint:disable implicitly_unwrapped_optional
  var tileRenderer: MKTileOverlayRenderer!
  var shimmerRenderer: ShimmerRenderer!
  // swiftlint:enable implicitly_unwrapped_optional

  // 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.cameraZoomRange = MKMapView.CameraZoomRange(
      minCenterCoordinateDistance: 7000,
      maxCenterCoordinateDistance: 60000)
    mapView.cameraBoundary = MKMapView.CameraBoundary(coordinateRegion: initialRegion)

    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)
  }

  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
    }
  }

  private func setupTileRenderer() {
    let overlay = AdventureMapOverlay()

    overlay.canReplaceMapContent = true
    mapView.addOverlay(overlay, level: .aboveLabels)
    tileRenderer = MKTileOverlayRenderer(tileOverlay: overlay)

    overlay.minimumZ = 13
    overlay.maximumZ = 16
  }

  private func setupLakeOverlay() {
    let lake = MKPolygon(coordinates: &Game.shared.reservoir, count: Game.shared.reservoir.count)
    mapView.addOverlay(lake)

    shimmerRenderer = ShimmerRenderer(overlay: lake)
    shimmerRenderer.fillColor = #colorLiteral(red: 0.2431372549, green: 0.5803921569, blue: 0.9764705882, alpha: 1)
    // swiftlint:disable:previous discouraged_object_literal
    Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak self] _ in
      self?.shimmerRenderer.updateLocations()
      self?.shimmerRenderer.setNeedsDisplay()
    }
  }

  @objc func gameUpdated(notification: Notification) {
    renderGame()
  }
}

// MARK: - MKMapViewDelegate
extension MapViewController: MKMapViewDelegate {
  // Add delegates here
  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:
      if let existingView = mapView.dequeueReusableAnnotationView(withIdentifier: "user") {
        return existingView
      } else {
        let view = MKAnnotationView(annotation: user, reuseIdentifier: "user")
        // swiftlint:disable:next discouraged_object_literal
        view.image = #imageLiteral(resourceName: "user")
        return view
      }
    case let warp as WarpZone:
      if let existingView = mapView.dequeueReusableAnnotationView(
        withIdentifier: WarpAnnotationView.identifier) {
        existingView.annotation = annotation
        return existingView
      } else {
        return WarpAnnotationView(annotation: warp, reuseIdentifier: WarpAnnotationView.identifier)
      }
    default:
      return nil
    }
  }
}

// MARK: - Game UI
extension MapViewController {
  private func heartsString() -> String {
    // swiftlint:disable:next identifier_name
    guard let hp = Game.shared.adventurer?.hitPoints else { return "☠️" }
    return String(repeating: "❤️", count: hp / 2)
  }

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

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

// MARK: - GameDelegate
extension MapViewController: GameDelegate {
  func encounteredMonster(monster: Monster) {
    showFight(monster: monster)
  }

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

    let runAction = UIAlertAction(title: "Run", style: .cancel) { _ in
      self.showFight(monster: monster, subtitle: "I think you should really fight this.")
    }

    let fightAction = UIAlertAction(title: "Fight", style: .default) { _ 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!")
      }
    }

    alertController.title = "A wild \(monster.name) appeared!"
    alertController.addActions(actions: [runAction, fightAction])
    present(alertController, animated: true)
  }

  func encounteredNPC(npc: NPC) {
    let alertController = UIAlertController()

    let noThanksAction = UIAlertAction(title: "No Thanks", style: .cancel) { _ in
      print("done with encounter")
    }

    let onMyWayAction = UIAlertAction(title: "On My Way", style: .default) { _ in
      print("did not buy anything")
    }

    alertController.title = npc.name
    alertController.addActions(actions: [noThanksAction, onMyWayAction])
    present(alertController, animated: true)
  }

  func enteredStore(store: Store) {
    let alertController = UIAlertController()

    let backOutAction = UIAlertAction(title: "Back Out", style: .cancel) { _ in
      print("did not buy anything")
    }

    let takeMoneyAction = UIAlertAction(title: "Take My ", style: .default) { _ in
      self.performSegue(withIdentifier: "shop", sender: store)
    }

    alertController.title = store.name
    alertController.addActions(actions: [backOutAction, takeMoneyAction])
    present(alertController, animated: true)
  }
}
16. 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)

    if let tile = tilePath {
      return tile
    } else {
      return Bundle.main.url(
        forResource: "parchment",
        withExtension: "png",
        subdirectory: "tiles",
        localization: nil)!
      // swiftlint:disable:previous force_unwrapping
    }
  }
}
17. 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 = path.boundingBoxOfPath
    let minX = boundingRect.minX
    let maxX = boundingRect.maxX

    // swiftlint:disable:next discouraged_object_literal
    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: [])
    // swiftlint:disable:previous force_unwrapping

    UIGraphicsPopContext()
  }
}
18. 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)
    // swiftlint:disable:next discouraged_object_literal
    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)
    // swiftlint:disable force_cast
    let imageView = cell.viewWithTag(1) as! UIImageView
    let label = cell.viewWithTag(2) as! UILabel
    // swiftlint:enable force_cast

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

    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
  }
}
19. ShopViewController.swift
import Foundation
import UIKit

class ShopViewController: UIViewController {
  // MARK: - Properties
  // swiftlint:disable:next implicitly_unwrapped_optional
  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)
    // swiftlint:disable force_cast
    let imageView = cell.viewWithTag(1) as! UIImageView
    let label = cell.viewWithTag(2) as! UILabel
    // swiftlint:enable force_cast

    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)
  }
}
20. 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 {
    // swiftlint:disable:next discouraged_object_literal
    return #imageLiteral(resourceName: "warp").maskWithColor(color: self.color)
  }
}

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

  override var annotation: MKAnnotation? {
    get { 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)
    // swiftlint:disable:next force_unwrapping
    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)
    // swiftlint:disable:next force_unwrapping
    context.draw(cgImage!, in: rect)

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

    let coloredImage = UIGraphicsGetImageFromCurrentImageContext()
    UIGraphicsEndImageContext()

    // swiftlint:disable:next force_unwrapping
    return coloredImage!
  }
}

后记

本篇主要讲述了自定义MapKit Tiles,感兴趣的给个赞或者关注~~~

MapKit框架详细解析(十二) —— 自定义MapKit Tiles(二)_第3张图片

你可能感兴趣的:(MapKit框架详细解析(十二) —— 自定义MapKit Tiles(二))