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)
}
}
后记
本篇主要讲述了请求应用评级和评论,感兴趣的给个赞或者关注~~~