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

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

作者: 刀客传奇 | 来源:发表于2018-12-18 18:54 被阅读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)
      }
    }
    

    后记

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

    相关文章

      网友评论

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

          本文链接:https://www.haomeiwen.com/subject/nbvlkqtx.html