StoreKit框架详细解析(三) —— 请求应用评级和评论(二)

版本记录

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

前言

StoreKit框架,支持应用内购买和与App Store的互动。接下来几篇我们就一起看一下这个框架。感兴趣的看下面几篇文章。
1. StoreKit框架详细解析(一) —— 基本概览(一)
2. StoreKit框架详细解析(二) —— 请求应用评级和评论(一)

源码

1. Swift

首先看下工程结构

接着看一下sb中的内容

接下来就是源码了

1. NavigationController.swift
import UIKit

final class NavigationController: UINavigationController {
  override var preferredStatusBarStyle: UIStatusBarStyle {
    return .lightContent
  }
}
2. MainViewController.swift
import UIKit

private enum State {
  case loading
  case paging([Recording], next: Int)
  case populated([Recording])
  case empty
  case error(Error)
  
  var currentRecordings: [Recording] {
    switch self {
    case .paging(let recordings, _):
      return recordings
    case .populated(let recordings):
      return recordings
    default:
      return []
    }
  }
}

class MainViewController: UIViewController {
  @IBOutlet private var tableView: UITableView!
  @IBOutlet private var activityIndicator: UIActivityIndicatorView!
  @IBOutlet private var loadingView: UIView!
  @IBOutlet private var emptyView: UIView!
  @IBOutlet private var errorLabel: UILabel!
  @IBOutlet private var errorView: UIView!
  
  private let searchController = UISearchController(searchResultsController: nil)
  private let networkingService = NetworkingService()

  private var state = State.loading {
    didSet {
      setFooterView()
      tableView.reloadData()
    }
  }
  
  override func viewDidLoad() {
    super.viewDidLoad()
    prepareSearchBar()
    loadRecordings()
  }

  override var preferredStatusBarStyle: UIStatusBarStyle {
    return .lightContent
  }
  
  // MARK: - Loading recordings
  
  @objc private func loadRecordings() {
    state = .loading
    loadPage(1)
  }
  
  private func loadPage(_ page: Int) {
    let query = searchController.searchBar.text
    networkingService.fetchRecordings(matching: query, page: page) { [weak self] response in
      guard let self = self else {
        return
      }
      
      self.searchController.searchBar.endEditing(true)
      self.update(response: response)
    }
  }
  
  private func update(response: RecordingsResult) {
    if let error = response.error {
      state = .error(error)
      return
    }
    
    guard let newRecordings = response.recordings,
      !newRecordings.isEmpty else {
        state = .empty
        return
    }
    
    var allRecordings = state.currentRecordings
    allRecordings.append(contentsOf: newRecordings)
    
    if response.hasMorePages {
      state = .paging(allRecordings, next: response.nextPage)
    } else {
      state = .populated(allRecordings)
    }
  }
  
  // MARK: - View Configuration
  
  private func setFooterView() {
    switch state {
    case .error(let error):
      errorLabel.text = error.localizedDescription
      tableView.tableFooterView = errorView
    case .loading:
      tableView.tableFooterView = loadingView
    case .paging:
      tableView.tableFooterView = loadingView
    case .empty:
      tableView.tableFooterView = emptyView
    case .populated:
      tableView.tableFooterView = nil
    }
  }
  
  private func prepareSearchBar() {
    searchController.obscuresBackgroundDuringPresentation = false
    searchController.searchBar.delegate = self
    searchController.searchBar.autocapitalizationType = .none
    searchController.searchBar.autocorrectionType = .no
    
    searchController.searchBar.tintColor = .white
    searchController.searchBar.barTintColor = .white

    let textFieldInSearchBar = UITextField.appearance(whenContainedInInstancesOf: [UISearchBar.self])
    textFieldInSearchBar.defaultTextAttributes = [
      .foregroundColor: UIColor.white
    ]
    
    navigationItem.searchController = searchController
    navigationItem.hidesSearchBarWhenScrolling = false
  }
}

// MARK: -

extension MainViewController: UISearchBarDelegate {
  func searchBar(_ searchBar: UISearchBar,
                 selectedScopeButtonIndexDidChange selectedScope: Int) {
  }
  
  func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
    NSObject.cancelPreviousPerformRequests(withTarget: self,
                                           selector: #selector(loadRecordings),
                                           object: nil)
    
    perform(#selector(loadRecordings), with: nil, afterDelay: 0.5)
  }
}

extension MainViewController: UITableViewDataSource {
  func tableView(_ tableView: UITableView,
                 numberOfRowsInSection section: Int) -> Int {
    return state.currentRecordings.count
  }
  
  func tableView(_ tableView: UITableView,
                 cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    
    guard let cell = tableView.dequeueReusableCell(
      withIdentifier: BirdSoundTableViewCell.reuseIdentifier)
      as? BirdSoundTableViewCell else {
        return UITableViewCell()
    }
    
    cell.load(recording: state.currentRecordings[indexPath.row])
    
    if case .paging(_, let nextPage) = state,
      indexPath.row == state.currentRecordings.count - 1 {
      loadPage(nextPage)
    }
    
    return cell
  }
}
3. SettingsViewController.swift
import UIKit

final class SettingsViewController: UITableViewController {
  // MARK: - UITableViewDelegate

  override func tableView(_ tableView: UITableView,
                          didSelectRowAt indexPath: IndexPath) {
    tableView.deselectRow(at: indexPath, animated: true)

    if indexPath.row == 0 {
      writeReview()
    } else if indexPath.row == 1 {
      share()
    }
  }

  // MARK: - Actions

  private let productURL = URL(string: "https://itunes.apple.com/app/id958625272")!

  private func writeReview() {
    var components = URLComponents(url: productURL, resolvingAgainstBaseURL: false)
    components?.queryItems = [
      URLQueryItem(name: "action", value: "write-review")
    ]

    guard let writeReviewURL = components?.url else {
      return
    }

    UIApplication.shared.open(writeReviewURL)
  }

  private func share() {
    let activityViewController = UIActivityViewController(activityItems: [productURL],
                                                          applicationActivities: nil)

    present(activityViewController, animated: true, completion: nil)
  }
}
4. BirdSoundTableViewCell.swift
import UIKit
import AVKit

class BirdSoundTableViewCell: UITableViewCell {
  static let reuseIdentifier = String(describing: BirdSoundTableViewCell.self)

  @IBOutlet private var nameLabel: UILabel!
  @IBOutlet private var playbackButton: UIButton!
  @IBOutlet private var scientificNameLabel: UILabel!
  @IBOutlet private var countryLabel: UILabel!
  @IBOutlet private var dateLabel: UILabel!
  @IBOutlet private var audioPlayerContainer: UIView!
  
  private var playbackURL: URL?
  private let player = AVPlayer()
  
  private var isPlaying = false {
    didSet {
      let newImage = isPlaying ? #imageLiteral(resourceName: "pause") : #imageLiteral(resourceName: "play")
      playbackButton.setImage(newImage, for: .normal)
      if isPlaying, let url = playbackURL {
        startPlaying(with: url)
      } else {
        stopPlaying()
      }
    }
  }

  override func prepareForReuse() {
    defer { super.prepareForReuse() }
    isPlaying = false
  }
  
  @IBAction private func togglePlayback(_ sender: Any) {
    isPlaying = !isPlaying
  }
  
  func load(recording: Recording) {
    nameLabel.text = recording.friendlyName
    scientificNameLabel.text = recording.scientificName
    countryLabel.text = recording.country
    dateLabel.text = recording.date
    playbackURL = recording.playbackURL
  }

  private func startPlaying(with playbackURL: URL) {
    let playerItem = AVPlayerItem(url: playbackURL)
    NotificationCenter.default.addObserver(self,
                                           selector: #selector(didPlayToEndTime(_:)),
                                           name: .AVPlayerItemDidPlayToEndTime,
                                           object: playerItem)

    player.replaceCurrentItem(with: playerItem)
    player.play()

    AppStoreReviewManager.requestReviewIfAppropriate()
  }

  private func stopPlaying() {
    NotificationCenter.default.removeObserver(self,
                                              name: .AVPlayerItemDidPlayToEndTime,
                                              object: player.currentItem)

    player.pause()
    player.replaceCurrentItem(with: nil)
  }
  
  @objc private func didPlayToEndTime(_: Notification) {
    isPlaying = false
  }
}
5. AppStoreReviewManager.swift
import Foundation
import StoreKit

enum AppStoreReviewManager {
  static let minimumReviewWorthyActionCount = 3

  static func requestReviewIfAppropriate() {
    let defaults = UserDefaults.standard
    let bundle = Bundle.main

    var actionCount = defaults.integer(forKey: .reviewWorthyActionCount)
    actionCount += 1
    defaults.set(actionCount, forKey: .reviewWorthyActionCount)

    guard actionCount >= minimumReviewWorthyActionCount else {
      return
    }

    let bundleVersionKey = kCFBundleVersionKey as String
    let currentVersion = bundle.object(forInfoDictionaryKey: bundleVersionKey) as? String
    let lastVersion = defaults.string(forKey: .lastReviewRequestAppVersion)

    guard lastVersion == nil || lastVersion != currentVersion else {
      return
    }

    SKStoreReviewController.requestReview()

    defaults.set(0, forKey: .reviewWorthyActionCount)
    defaults.set(currentVersion, forKey: .lastReviewRequestAppVersion)
  }
}
6. NetworkingService.swift
import Foundation

enum NetworkError: Error {
  case invalidURL
}

class NetworkingService {
  private let endpoint = "https://www.xeno-canto.org/api/2/recordings"
  
  private var task: URLSessionTask?
  
  func fetchRecordings(matching query: String?, page: Int, onCompletion: @escaping (RecordingsResult) -> Void) {
    func fireErrorCompletion(_ error: Error?) {
      onCompletion(RecordingsResult(recordings: nil, error: error,
                                    currentPage: 0, pageCount: 0))
    }
    
    var queryOrEmpty = "since:1970-01-02"
    
    if let query = query, !query.isEmpty {
      queryOrEmpty = query
    }
    
    var components = URLComponents(string: endpoint)
    components?.queryItems = [
      URLQueryItem(name: "query", value: queryOrEmpty),
      URLQueryItem(name: "page", value: String(page))
    ]
    
    guard let url = components?.url else {
      fireErrorCompletion(NetworkError.invalidURL)
      return
    }
    
    task?.cancel()
    
    task = URLSession.shared.dataTask(with: url) { data, response, error in
      DispatchQueue.main.async {
        if let error = error {
          guard (error as NSError).code != NSURLErrorCancelled else {
            return
          }
          fireErrorCompletion(error)
          return
        }
        
        guard let data = data else {
          fireErrorCompletion(error)
          return
        }
        
        do {
          let result = try JSONDecoder().decode(ServiceResponse.self, from: data)
          
          // For demo purposes, only return 50 at a time
          // This makes it easier to reach the bottom of the results
          let first50 = result.recordings.prefix(50)
          
          onCompletion(RecordingsResult(recordings: Array(first50),
                                        error: nil,
                                        currentPage: result.page,
                                        pageCount: result.numPages))
        } catch {
          fireErrorCompletion(error)
        }
      }
    }
    
    task?.resume()
  }
}
7. ServiceResponse.swift
import Foundation

struct ServiceResponse: Codable {
  let recordings: [Recording]
  let page: Int
  let numPages: Int
}
8. RecordingsResult.swift

import Foundation

struct RecordingsResult {
  let recordings: [Recording]?
  let error: Error?
  let currentPage: Int
  let pageCount: Int
  
  var hasMorePages: Bool {
    return currentPage < pageCount
  }
  
  var nextPage: Int {
    return hasMorePages ? currentPage + 1 : currentPage
  }
}
9. Recording.swift
import Foundation

struct Recording: Codable {
  let genus: String
  let species: String
  let friendlyName: String
  let country: String
  let fileURL: URL
  let date: String
  
  enum CodingKeys: String, CodingKey {
    case genus = "gen"
    case species = "sp"
    case friendlyName = "en"
    case country = "cnt"
    case date
    case fileURL = "file"
  }

  var scientificName: String {
    return "\(genus) \(species)"
  }

  var playbackURL: URL? {
    // The API doesn't return a scheme on the URL, add one to make it valid.
    var components = URLComponents(url: fileURL, resolvingAgainstBaseURL: false)
    components?.scheme = "https"
    return components?.url
  }
}
10. UserDefaults+Key.swift
import Foundation

extension UserDefaults {
  enum Key: String {
    case reviewWorthyActionCount
    case lastReviewRequestAppVersion
  }

  func integer(forKey key: Key) -> Int {
    return integer(forKey: key.rawValue)
  }

  func string(forKey key: Key) -> String? {
    return string(forKey: key.rawValue)
  }

  func set(_ integer: Int, forKey key: Key) {
    set(integer, forKey: key.rawValue)
  }

  func set(_ object: Any?, forKey key: Key) {
    set(object, forKey: key.rawValue)
  }
}

后记

本篇主要讲述了请求应用评级和评论,感兴趣的给个赞或者关注~~~

你可能感兴趣的:(StoreKit框架详细解析(三) —— 请求应用评级和评论(二))