PromiseKit框架详细解析(三) —— 基于PromiseKit的天气应用的简单示例(二)

260 阅读3分钟
原文链接: www.jianshu.com

版本记录

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

前言

PromiseKit(GitHub地址) 只是 Promise 设计模式的一种实现方式。并不是我们项目中必须采用的一种方式,但是它可以增强代码的可读性和维护性,让代码更加的优雅。接下来我们就一起看一下这个框架。感兴趣的看下面几篇文章。
1. PromiseKit框架详细解析(一) —— 基本概览(一)
2. PromiseKit框架详细解析(二) —— 基于PromiseKit的天气应用的简单示例(一)

源码

1. Swift

首先看一下工程结构

下面看一下sb中的内容

下面就是代码了

1. BrokenPromise.swift
import Foundation
import PromiseKit

func brokenPromise<T>(method: String = #function) -> Promise<T> {
  return Promise<T>() { seal in
    let err = NSError(domain: "WeatherOrNot", code: 0, userInfo: [NSLocalizedDescriptionKey: "'\(method)' not yet implemented."])
    seal.reject(err)
  }
}
2. WeatherViewController.swift
import UIKit
import PromiseKit
import CoreLocation

private let errorColor = UIColor(red: 0.96, green: 0.667, blue: 0.690, alpha: 1)
private let oneHour: TimeInterval = 3600 // Seconds per hour
private let randomCities = [("Tokyo", "JP", 35.683333, 139.683333),
                            ("Jakarta", "ID", -6.2, 106.816667),
                            ("Delhi", "IN", 28.61, 77.23),
                            ("Manila", "PH", 14.58, 121),
                            ("São Paulo", "BR", -23.55, -46.633333)]

class WeatherViewController: UIViewController {
  @IBOutlet private var placeLabel: UILabel!
  @IBOutlet private var tempLabel: UILabel!
  @IBOutlet private var iconImageView: UIImageView!
  @IBOutlet private var conditionLabel: UILabel!
  @IBOutlet private var randomWeatherButton: UIButton!
  
  let weatherAPI = WeatherHelper()
  let locationHelper = LocationHelper()
  
  override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    
    updateWithCurrentLocation()
  }
  
  private func updateWithCurrentLocation() {
    locationHelper.getLocation()
      .done { [weak self] placemark in
        self?.handleLocation(placemark: placemark)
      }
      .catch { [weak self] error in
        guard let self = self else { return }

        self.tempLabel.text = "--"
        self.placeLabel.text = "--"

        switch error {
        case is CLError where (error as? CLError)?.code == .denied:
          self.conditionLabel.text = "Enable Location Permissions in Settings"
          self.conditionLabel.textColor = UIColor.white
        default:
          self.conditionLabel.text = error.localizedDescription
          self.conditionLabel.textColor = errorColor
        }
      }

    after(seconds: oneHour).done { [weak self] in
      self?.updateWithCurrentLocation()
    }
  }
  
  private func handleMockLocation() {
    let coordinate = CLLocationCoordinate2DMake(37.966667, 23.716667)
    handleLocation(city: "Athens", state: "Greece", coordinate: coordinate)
  }
  
  private func handleLocation(placemark: CLPlacemark) {
    handleLocation(city: placemark.locality,
                   state: placemark.administrativeArea,
                   coordinate: placemark.location!.coordinate)
  }
  
  private func handleLocation(city: String?,
                              state: String?,
                              coordinate: CLLocationCoordinate2D) {
    UIApplication.shared.isNetworkActivityIndicatorVisible = true

    weatherAPI.getWeather(atLatitude: coordinate.latitude,
                          longitude: coordinate.longitude)
      .then { [weak self] weatherInfo -> Promise<UIImage> in
        guard let self = self else { return brokenPromise() }

        self.updateUI(with: weatherInfo)

        return self.weatherAPI.getIcon(named: weatherInfo.weather.first!.icon)
      }
      .done(on: DispatchQueue.main) { icon in
        self.iconImageView.image = icon
      }
      .catch { error in
        self.tempLabel.text = "--"
        self.conditionLabel.text = error.localizedDescription
        self.conditionLabel.textColor = errorColor
      }
      .finally {
        UIApplication.shared.isNetworkActivityIndicatorVisible = false
      }
  }
  
  private func updateUI(with weatherInfo: WeatherHelper.WeatherInfo) {
    let tempMeasurement = Measurement(value: weatherInfo.main.temp, unit: UnitTemperature.kelvin)
    let formatter = MeasurementFormatter()
    let numberFormatter = NumberFormatter()
    numberFormatter.numberStyle = .none
    formatter.numberFormatter = numberFormatter
    let tempStr = formatter.string(from: tempMeasurement)
    self.tempLabel.text = tempStr
    self.placeLabel.text = weatherInfo.name
    self.conditionLabel.text = weatherInfo.weather.first?.description ?? "empty"
    self.conditionLabel.textColor = UIColor.white
  }
  
  @IBAction func showRandomWeather(_ sender: AnyObject) {
    randomWeatherButton.isEnabled = false

    let weatherPromises = randomCities.map { weatherAPI.getWeather(atLatitude: $0.2, longitude: $0.3) }

    UIApplication.shared.isNetworkActivityIndicatorVisible = true

    race(weatherPromises)
      .then { [weak self] weatherInfo -> Promise<UIImage> in
        guard let self = self else { return brokenPromise() }

        self.placeLabel.text = weatherInfo.name
        self.updateUI(with: weatherInfo)
        return self.weatherAPI.getIcon(named: weatherInfo.weather.first!.icon)
      }
      .done { icon in
        self.iconImageView.image = icon
      }
      .catch { error in
        self.tempLabel.text = "--"
        self.conditionLabel.text = error.localizedDescription
        self.conditionLabel.textColor = errorColor
      }
      .finally {
        UIApplication.shared.isNetworkActivityIndicatorVisible = false
        self.randomWeatherButton.isEnabled = true
      }
  }
}

// MARK: - UITextFieldDelegate
extension WeatherViewController: UITextFieldDelegate {
  func textFieldShouldReturn(_ textField: UITextField) -> Bool {
    textField.resignFirstResponder()
    guard let text = textField.text else { return false }

    locationHelper.searchForPlacemark(text: text)
      .done { placemark in
        self.handleLocation(placemark: placemark)
      }
      .catch { _ in }

    return true
  }
}
3. LocationHelper.swift
import Foundation
import CoreLocation
import PromiseKit

class LocationHelper {
  let coder = CLGeocoder()
  
  func getLocation() -> Promise<CLPlacemark> {
    return CLLocationManager.requestLocation().lastValue.then { location in
      return self.coder.reverseGeocode(location: location).firstValue
    }
  }

  func searchForPlacemark(text: String) -> Promise<CLPlacemark> {
    return coder.geocode(text).firstValue
  }
}
4. WeatherHelper.swift
import Foundation
import PromiseKit

private let appID = "Enter Your API Key from http://openweathermap.org/appid"

class WeatherHelper {
  struct WeatherInfo: Codable {
    let main: Temperature
    let weather: [Weather]
    var name: String = "Error: invalid jsonDictionary! Verify your appID is correct"
  }

  struct Weather: Codable {
    let icon: String
    let description: String
  }

  struct Temperature: Codable {
    let temp: Double
  }

  func getWeatherTheOldFashionedWay(coordinate: CLLocationCoordinate2D, completion: @escaping (WeatherInfo?, Error?) -> Void) {
    let urlString = "http://api.openweathermap.org/data/2.5/weather?lat=\(coordinate.latitude)&lon=\(coordinate.longitude)&appid=\(appID)"

    guard let url = URL(string: urlString) else {
      preconditionFailure("Failed creating API URL - Make sure you set your OpenWeather API Key")
    }

    URLSession.shared.dataTask(with: url) { data, _, error in
      guard let data = data,
            let result = try? JSONDecoder().decode(WeatherInfo.self, from: data) else {
        completion(nil, error)
        return
      }
      
      completion(result, nil)
    }.resume()
  }

  func getWeather(atLatitude latitude: Double, longitude: Double) -> Promise<WeatherInfo> {
    let urlString = "http://api.openweathermap.org/data/2.5/weather?lat=" +
      "\(latitude)&lon=\(longitude)&appid=\(appID)"
    let url = URL(string: urlString)!

    return firstly {
      URLSession.shared.dataTask(.promise, with: url)
    }.compactMap {
      return try JSONDecoder().decode(WeatherInfo.self, from: $0.data)
    }
  }

  func getIcon(named iconName: String) -> Promise<UIImage> {
    return Promise<UIImage> {
      getFile(named: iconName, completion: $0.resolve)
    }
    .recover { _ in
      self.getIconFromNetwork(named: iconName)
    }
  }

  func getIconFromNetwork(named iconName: String) -> Promise<UIImage> {
    let urlString = "http://openweathermap.org/img/w/\(iconName).png"
    let url = URL(string: urlString)!

    return firstly {
      URLSession.shared.dataTask(.promise, with: url)
    }
    .then(on: DispatchQueue.global(qos: .background)) { urlResponse in
      return Promise {
        self.saveFile(named: iconName, data: urlResponse.data, completion: $0.resolve)
      }
      .then(on: DispatchQueue.global(qos: .background)) {
        return Promise.value(UIImage(data: urlResponse.data)!)
      }
    }
  }

  
  private func saveFile(named: String, data: Data, completion: @escaping (Error?) -> Void) {
    guard let path = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first?.appendingPathComponent(named+".png") else { return }

    DispatchQueue.global(qos: .background).async {
      do {
        try data.write(to: path)
        print("Saved image to: " + path.absoluteString)
        completion(nil)
      } catch {
        completion(error)
      }
    }
  }
  
  private func getFile(named: String, completion: @escaping (UIImage?, Error?) -> Void) {
    DispatchQueue.global(qos: .background).async {
      if let path = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first?.appendingPathComponent(named+".png"),
        let data = try? Data(contentsOf: path),
        let image = UIImage(data: data) {
        DispatchQueue.main.async { completion(image, nil) }
      } else {
        let error = NSError(domain: "WeatherOrNot",
                            code: 0,
                            userInfo: [NSLocalizedDescriptionKey: "Image file '\(named)' not found."])
        DispatchQueue.main.async { completion(nil, error) }
      }
    }
  }
}

后记

本篇主要讲述了基于PromiseKit的天气应用的简单示例,感兴趣的给个赞或者关注~~~