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

2018-12-18  本文已影响45人  刀客传奇

版本记录

版本号 时间
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)
  }
}

后记

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

上一篇下一篇

猜你喜欢

热点阅读