add DYFStore

This commit is contained in:
zhl 2021-07-22 16:13:00 +08:00
parent 7a5a2445f7
commit 423fa5f32e
15 changed files with 3824 additions and 3 deletions

View File

@ -33,6 +33,17 @@
D5FF445026A80FFB00CF155C /* KeychainPasswordItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5FF444F26A80FFB00CF155C /* KeychainPasswordItem.swift */; };
D5FF445426A948F600CF155C /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D5FF445326A948F500CF155C /* StoreKit.framework */; };
D5FF445726A94B8500CF155C /* SwiftyJSON in Frameworks */ = {isa = PBXBuildFile; productRef = D5FF445626A94B8500CF155C /* SwiftyJSON */; };
D5FF445B26A9561000CF155C /* DYFStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5FF445A26A9561000CF155C /* DYFStore.swift */; };
D5FF445D26A9563400CF155C /* DYFStoreConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5FF445C26A9563400CF155C /* DYFStoreConverter.swift */; };
D5FF445F26A9565C00CF155C /* DYFStoreKeychainPersistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5FF445E26A9565C00CF155C /* DYFStoreKeychainPersistence.swift */; };
D5FF446126A9567300CF155C /* DYFStoreReceiptVerifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5FF446026A9567300CF155C /* DYFStoreReceiptVerifier.swift */; };
D5FF446326A9568A00CF155C /* DYFStoreTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5FF446226A9568A00CF155C /* DYFStoreTransaction.swift */; };
D5FF446526A9569E00CF155C /* DYFStoreUserDefaultsPersistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5FF446426A9569E00CF155C /* DYFStoreUserDefaultsPersistence.swift */; };
D5FF446726A957A900CF155C /* DYFSwiftRuntimeProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5FF446626A957A900CF155C /* DYFSwiftRuntimeProvider.swift */; };
D5FF446926A959A600CF155C /* ZExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5FF446826A959A600CF155C /* ZExtensions.swift */; };
D5FF446B26A959FB00CF155C /* ZLoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5FF446A26A959FB00CF155C /* ZLoadingView.swift */; };
D5FF446D26A95A5C00CF155C /* ZIndefiniteAnimatedSpinner.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5FF446C26A95A5C00CF155C /* ZIndefiniteAnimatedSpinner.swift */; };
D5FF447026A95AFB00CF155C /* ZStoreManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5FF446F26A95AFB00CF155C /* ZStoreManager.swift */; };
ED50D6502436EB69005B9E1E /* CoreMedia.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ED50D64F2436EB69005B9E1E /* CoreMedia.framework */; };
ED50D6522436EB70005B9E1E /* AVKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ED50D6512436EB70005B9E1E /* AVKit.framework */; };
ED9FBC2E21830D2A005A5DF0 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ED9FBC2D21830D2A005A5DF0 /* UIKit.framework */; };
@ -74,6 +85,17 @@
D5FF444F26A80FFB00CF155C /* KeychainPasswordItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeychainPasswordItem.swift; sourceTree = "<group>"; };
D5FF445226A832BE00CF155C /* 武极天下.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = "武极天下.entitlements"; path = "../武极天下.entitlements"; sourceTree = "<group>"; };
D5FF445326A948F500CF155C /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; };
D5FF445A26A9561000CF155C /* DYFStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DYFStore.swift; sourceTree = "<group>"; };
D5FF445C26A9563400CF155C /* DYFStoreConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DYFStoreConverter.swift; sourceTree = "<group>"; };
D5FF445E26A9565C00CF155C /* DYFStoreKeychainPersistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DYFStoreKeychainPersistence.swift; sourceTree = "<group>"; };
D5FF446026A9567300CF155C /* DYFStoreReceiptVerifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DYFStoreReceiptVerifier.swift; sourceTree = "<group>"; };
D5FF446226A9568A00CF155C /* DYFStoreTransaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DYFStoreTransaction.swift; sourceTree = "<group>"; };
D5FF446426A9569E00CF155C /* DYFStoreUserDefaultsPersistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DYFStoreUserDefaultsPersistence.swift; sourceTree = "<group>"; };
D5FF446626A957A900CF155C /* DYFSwiftRuntimeProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DYFSwiftRuntimeProvider.swift; sourceTree = "<group>"; };
D5FF446826A959A600CF155C /* ZExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZExtensions.swift; sourceTree = "<group>"; };
D5FF446A26A959FB00CF155C /* ZLoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZLoadingView.swift; sourceTree = "<group>"; };
D5FF446C26A95A5C00CF155C /* ZIndefiniteAnimatedSpinner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZIndefiniteAnimatedSpinner.swift; sourceTree = "<group>"; };
D5FF446F26A95AFB00CF155C /* ZStoreManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZStoreManager.swift; sourceTree = "<group>"; };
ED50D64F2436EB69005B9E1E /* CoreMedia.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreMedia.framework; path = System/Library/Frameworks/CoreMedia.framework; sourceTree = SDKROOT; };
ED50D6512436EB70005B9E1E /* AVKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVKit.framework; path = System/Library/Frameworks/AVKit.framework; sourceTree = SDKROOT; };
ED9FBC2D21830D2A005A5DF0 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; };
@ -146,11 +168,12 @@
306A7A8F200C83F500E1EBB6 /* ios-template */ = {
isa = PBXGroup;
children = (
D5FF445926A955F200CF155C /* DYFStore */,
D5FF445226A832BE00CF155C /* 武极天下.entitlements */,
EDF3B7E424580A4B009DFBBD /* LaunchScreen.storyboard */,
D5FF444D26A80FF600CF155C /* IDUtil.swift */,
D5FF444F26A80FFB00CF155C /* KeychainPasswordItem.swift */,
D5FF445826A955DA00CF155C /* utils */,
D5FF442E26A80E5F00CF155C /* AppDelegate.swift */,
D5FF446E26A95A9200CF155C /* common */,
306A7A96200C83F500E1EBB6 /* ViewController.h */,
D5FF443026A80F8600CF155C /* ViewController.swift */,
306A7A97200C83F500E1EBB6 /* ViewController.mm */,
@ -208,6 +231,40 @@
path = doc;
sourceTree = "<group>";
};
D5FF445826A955DA00CF155C /* utils */ = {
isa = PBXGroup;
children = (
D5FF444D26A80FF600CF155C /* IDUtil.swift */,
);
name = utils;
sourceTree = "<group>";
};
D5FF445926A955F200CF155C /* DYFStore */ = {
isa = PBXGroup;
children = (
D5FF445A26A9561000CF155C /* DYFStore.swift */,
D5FF445C26A9563400CF155C /* DYFStoreConverter.swift */,
D5FF445E26A9565C00CF155C /* DYFStoreKeychainPersistence.swift */,
D5FF446026A9567300CF155C /* DYFStoreReceiptVerifier.swift */,
D5FF446226A9568A00CF155C /* DYFStoreTransaction.swift */,
D5FF446626A957A900CF155C /* DYFSwiftRuntimeProvider.swift */,
D5FF446426A9569E00CF155C /* DYFStoreUserDefaultsPersistence.swift */,
);
path = DYFStore;
sourceTree = "<group>";
};
D5FF446E26A95A9200CF155C /* common */ = {
isa = PBXGroup;
children = (
D5FF446826A959A600CF155C /* ZExtensions.swift */,
D5FF444F26A80FFB00CF155C /* KeychainPasswordItem.swift */,
D5FF446C26A95A5C00CF155C /* ZIndefiniteAnimatedSpinner.swift */,
D5FF446A26A959FB00CF155C /* ZLoadingView.swift */,
D5FF446F26A95AFB00CF155C /* ZStoreManager.swift */,
);
path = common;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@ -288,11 +345,22 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
D5FF447026A95AFB00CF155C /* ZStoreManager.swift in Sources */,
D5FF445026A80FFB00CF155C /* KeychainPasswordItem.swift in Sources */,
D5FF443126A80F8600CF155C /* ViewController.swift in Sources */,
D5FF446B26A959FB00CF155C /* ZLoadingView.swift in Sources */,
D5FF445D26A9563400CF155C /* DYFStoreConverter.swift in Sources */,
D5FF442F26A80E5F00CF155C /* AppDelegate.swift in Sources */,
D5FF446326A9568A00CF155C /* DYFStoreTransaction.swift in Sources */,
D5FF446926A959A600CF155C /* ZExtensions.swift in Sources */,
D5FF446126A9567300CF155C /* DYFStoreReceiptVerifier.swift in Sources */,
D5FF444E26A80FF600CF155C /* IDUtil.swift in Sources */,
306A7A98200C83F500E1EBB6 /* ViewController.mm in Sources */,
D5FF445B26A9561000CF155C /* DYFStore.swift in Sources */,
D5FF446D26A95A5C00CF155C /* ZIndefiniteAnimatedSpinner.swift in Sources */,
D5FF446526A9569E00CF155C /* DYFStoreUserDefaultsPersistence.swift in Sources */,
D5FF445F26A9565C00CF155C /* DYFStoreKeychainPersistence.swift in Sources */,
D5FF446726A957A900CF155C /* DYFSwiftRuntimeProvider.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

View File

@ -8,9 +8,11 @@
import UIKit
import Foundation
import StoreKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
class AppDelegate: UIResponder, UIApplicationDelegate, DYFStoreAppStorePaymentDelegate {
var window: UIWindow?
var _viewController: UIViewController?
var _imageView: UIImageView?
@ -34,6 +36,15 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
}
self.setExternalInterfaces()
// Wether to allow the logs output to console.
DYFStore.default.enableLog = true
// If more than one transaction observer is attached to the payment queue, no guarantees are made as to the order they will be called in. It is recommended that you use a single observer to process and finish the transaction.
DYFStore.default.addPaymentTransactionObserver()
// Sets the delegate processes the purchase which was initiated by user from the App Store.
DYFStore.default.delegate = self
let networkState:String? = _native.getNetworkState();
if (networkState == "NotReachable") {
print(">>>>>>>>>>>>>>>>>>>>no network")
@ -118,10 +129,29 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
_imageView?.frame = _viewController!.view.frame;
_viewController!.view.addSubview(_imageView!);
_viewController!.view.bringSubviewToFront(_imageView!);
self.showLoading("加载中")
}
func hideLoadingView(){
_imageView?.removeFromSuperview();
self.hideLoading()
}
//
func didReceiveAppStorePurchaseRequest(_ queue: SKPaymentQueue, payment: SKPayment, forProduct product: SKProduct) {
if !DYFStore.canMakePayments() {
self.showTipsMessage("Your device is not able or allowed to make payments!")
return
}
// Get account name from your own user system.
let accountName = "Handsome Jon"
// This algorithm is negotiated with server developer.
let userIdentifier = Z_SHA256_HashValue(accountName) ?? ""
DYFStoreLog("userIdentifier: \(userIdentifier)")
ZStoreManager.shared.addPayment(product.productIdentifier, userIdentifier: userIdentifier)
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,242 @@
//
// DYFStoreTransaction.swift
//
// Created by dyf on 2016/11/28. ( https://github.com/dgynfi/DYFStore )
// Copyright © 2016 dyf. All rights reserved.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import Foundation
/// The converter is used to convert json and json object to each other,
/// convert an object and a data object to each other.
public class DYFStoreConverter: NSObject {
/// Instantiates a DYFStoreConverter object.
public override init() {
super.init()
}
/// Encodes an object.
///
/// - Parameter object: An object you want to encode.
/// - Returns: The data object into which the archive is written.
@objc public static func encodeObject(_ object: Any?) -> Data? {
guard let obj = object else {
return nil
}
if #available(iOS 11.0, *) {
let archiver = NSKeyedArchiver(requiringSecureCoding: false)
archiver.encode(obj)
return archiver.encodedData // archiver.finishEncoding() and return the data.
}
return NSKeyedArchiver.archivedData(withRootObject: obj);
}
/// Returns an object initialized for decoding data.
///
/// - Parameter data: An archive previously encoded by NSKeyedArchiver.
/// - Returns: An object initialized for decoding data.
@objc public static func decodeObject(_ data: Data?) -> Any? {
guard let tData = data else {
return nil
}
if #available(iOS 11.0, *) {
do {
let unarchiver = try NSKeyedUnarchiver(forReadingFrom: tData)
unarchiver.requiresSecureCoding = false
let object = unarchiver.decodeObject()
unarchiver.finishDecoding()
return object
} catch let error {
#if DEBUG
print("\((#file as NSString).lastPathComponent):\(#function):\(#line) error: \(error.localizedDescription)")
#endif
return nil
}
}
return NSKeyedUnarchiver.unarchiveObject(with: tData)
}
/// Returns JSON data from a Foundation object. The Options for writing JSON data is equivalent to kNilOptions in Objective-C.
///
/// - Parameter obj: The object from which to generate JSON data.
/// - Returns: JSON data for obj, or nil if an internal error occurs.
@objc public static func json(withObject obj: Any?) -> Data? {
return json(withObject: obj, options: [])
}
/// Returns JSON data from a Foundation object.
///
/// - Parameters:
/// - obj: The object from which to generate JSON data.
/// - options: Options for writing JSON data. The default value is equivalent to kNilOptions in Objective-C.
/// - Returns: JSON data for obj, or nil if an internal error occurs.
@objc public static func json(withObject obj: Any?, options: JSONSerialization.WritingOptions = []) -> Data? {
guard let anObj = obj else { return nil }
do {
// let encoder = JSONEncoder()
// encoder.outputFormatting = .prettyPrinted /* The pretty output formatting. */
// let data = try encoder.encode(obj) /* The object complies with the Codable protocol. */
let data = try JSONSerialization.data(withJSONObject: anObj, options: options)
return data
} catch let error {
#if DEBUG
print("\((#file as NSString).lastPathComponent):\(#function):\(#line) error: \(error.localizedDescription)")
#endif
return nil
}
}
/// Returns JSON string from a Foundation object. The Options for writing JSON data is equivalent to kNilOptions in Objective-C.
///
/// - Parameter obj: The object from which to generate JSON string.
/// - Returns: JSON string for obj, or nil if an internal error occurs.
@objc public static func jsonString(withObject obj: Any?) -> String? {
return jsonString(withObject: obj, options: [])
}
/// Returns JSON string from a Foundation object.
///
/// - Parameters:
/// - obj: The object from which to generate JSON string.
/// - options: Options for writing JSON data. The default value is equivalent to kNilOptions in Objective-C.
/// - Returns: JSON string for obj, or nil if an internal error occurs.
@objc public static func jsonString(withObject obj: Any?, options: JSONSerialization.WritingOptions = []) -> String? {
guard let anObj = obj else { return nil }
do {
// let encoder = JSONEncoder()
// encoder.outputFormatting = .prettyPrinted /* The pretty output formatting. */
// let data = try encoder.encode(obj) /* The object complies with the Codable protocol. */
let data = try JSONSerialization.data(withJSONObject: anObj, options: options)
return String(data: data, encoding: String.Encoding.utf8)
} catch let error {
#if DEBUG
print("\((#file as NSString).lastPathComponent):\(#function):\(#line) error: \(error.localizedDescription)")
#endif
return nil
}
}
/// Returns a Foundation object from given JSON data. The options used when creating Foundation objects from JSON data is equivalent to kNilOptions in Objective-C.
///
/// - Parameter data: A data object containing JSON data.
/// - Returns: A Foundation object from the JSON data in data, or nil if an error occurs.
@objc public static func jsonObject(withData data: Data?) -> Any? {
return jsonObject(withData: data, options: [])
}
/// Returns a Foundation object from given JSON data.
///
/// - Parameters:
/// - data: A data object containing JSON data.
/// - options: Options used when creating Foundation objects from JSON data. The default value is equivalent to kNilOptions in Objective-C.
/// - Returns: A Foundation object from the JSON data in data, or nil if an error occurs.
@objc public static func jsonObject(withData data: Data?, options: JSONSerialization.ReadingOptions = []) -> Any? {
guard let aData = data else { return nil }
do {
// struct GroceryProduct: Codable {
// var name: String
// var points: Int
// var description: String?
// }
// let json = """
// {
// "name": "Durian",
// "points": 600,
// "description": "A fruit with a distinctive scent."
// }
// """.data(using: .utf8)!
// let decoder = JSONDecoder()
// let obj = try decoder.decode(GroceryProduct.self, from: json) /* The object complies with the Codable protocol. */
let obj = try JSONSerialization.jsonObject(with: aData, options: options)
return obj
} catch let error {
#if DEBUG
print("\((#file as NSString).lastPathComponent):\(#function):\(#line) error: \(error.localizedDescription)")
#endif
return nil
}
}
/// Returns a Foundation object from given JSON string. The options used when creating Foundation objects from JSON data is equivalent to kNilOptions in Objective-C.
///
/// - Parameter json: A string object containing JSON string.
/// - Returns: A Foundation object from the JSON data in data, or nil if an error occurs.
@objc public static func jsonObject(withJSON json: String?) -> Any? {
return jsonObject(withJSON: json, options: [])
}
/// Returns a Foundation object from given JSON string.
///
/// - Parameters:
/// - json: A string object containing JSON string.
/// - options: Options used when creating Foundation objects from JSON data. The default value is equivalent to kNilOptions in Objective-C.
/// - Returns: A Foundation object from the JSON data in data, or nil if an error occurs.
@objc public static func jsonObject(withJSON json: String?, options: JSONSerialization.ReadingOptions = []) -> Any? {
guard let data = json?.data(using: String.Encoding.utf8) else {
return nil
}
do {
let obj = try JSONSerialization.jsonObject(with: data, options: options)
return obj
} catch let error {
#if DEBUG
print("\((#file as NSString).lastPathComponent):\(#function):\(#line) error: \(error)")
#endif
return nil
}
}
}

View File

@ -0,0 +1,173 @@
//
// DYFStoreKeychainPersistence.swift
//
// Created by dyf on 2016/11/28. ( https://github.com/dgynfi/DYFStore )
// Copyright © 2016 dyf. All rights reserved.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import Foundation
/** Deprecated.
/// The transaction persistence using the keychain.
open class DYFStoreKeychainPersistence: NSObject {
/// Instantiates a DYFSwiftKeychain object.
private lazy var keychain: DYFSwiftKeychain = {
let keychain = DYFSwiftKeychain()
return keychain
}()
/// Loads an array whose elements are the `Dictionary` objects from the keychain.
///
/// - Returns: An array whose elements are the `Dictionary` objects.
private func loadDataFromKeychain() -> [[String : Any]]? {
let data = self.keychain.getData(DYFStoreTransactionsKey)
let array = DYFStoreConverter.jsonObject(withData: data) as? [[String : Any]]
return array
}
/// Returns a Boolean value that indicates whether a transaction is present in the keychain with a given transaction ientifier.
///
/// - Parameter transactionIdentifier: The unique server-provided identifier.
/// - Returns: True if a transaction is present in the keychain, otherwise false.
public func containsTransaction(_ transactionIdentifier: String) -> Bool {
let array = loadDataFromKeychain()
guard let arr = array, arr.count > 0 else {
return false
}
for item in arr {
let transaction = DYFSwiftRuntimeProvider.model(withDictionary: item, forClass: DYFStoreTransaction.self)
let identifier = transaction?.transactionIdentifier
if let id = identifier, id == transactionIdentifier {
return true
}
}
return false
}
/// Stores an `DYFStoreTransaction` object in the keychain item.
///
/// - Parameter transaction: An `DYFStoreTransaction` object.
public func storeTransaction(_ transaction: DYFStoreTransaction?) {
let obj = DYFSwiftRuntimeProvider.dictionary(withModel: transaction)
guard let dict = obj else {
return
}
var transactions = loadDataFromKeychain() ?? [[String : Any]]()
transactions.append(dict)
let tData = DYFStoreConverter.json(withObject: transactions)
self.keychain.set(tData, forKey: DYFStoreTransactionsKey)
}
/// Retrieves an array whose elements are the `DYFStoreTransaction` objects from the keychain.
///
/// - Returns: An array whose elements are the `DYFStoreTransaction` objects.
public func retrieveTransactions() -> [DYFStoreTransaction]? {
let array = loadDataFromKeychain()
guard let arr = array else {
return nil
}
var transactions = [DYFStoreTransaction]()
for item in arr {
let transaction = DYFSwiftRuntimeProvider.model(withDictionary: item, forClass: DYFStoreTransaction.self)
if let t = transaction {
transactions.append(t)
}
}
return transactions
}
/// Retrieves an `DYFStoreTransaction` object from the keychain with a given transaction ientifier.
///
/// - Parameter transactionIdentifier: The unique server-provided identifier.
/// - Returns: An `DYFStoreTransaction` object from the keychain.
public func retrieveTransaction(_ transactionIdentifier: String) -> DYFStoreTransaction? {
let array = retrieveTransactions()
guard let arr = array else {
return nil
}
for transaction in arr {
let identifier = transaction.transactionIdentifier
if identifier == transactionIdentifier {
return transaction
}
}
return nil
}
/// Removes an `DYFStoreTransaction` object from the keychain with a given transaction ientifier.
///
/// - Parameter transactionIdentifier: The unique server-provided identifier.
public func removeTransaction(_ transactionIdentifier: String) {
let array = loadDataFromKeychain()
guard var arr = array else {
return
}
var index: Int = -1
for (idx, item) in arr.enumerated() {
let transaction = DYFSwiftRuntimeProvider.model(withDictionary: item, forClass: DYFStoreTransaction.self)
let identifier = transaction?.transactionIdentifier
if let id = identifier, id == transactionIdentifier {
index = idx
break
}
}
guard index >= 0 else { return }
arr.remove(at: index)
let tData = DYFStoreConverter.json(withObject: arr)
self.keychain.set(tData, forKey: DYFStoreTransactionsKey)
}
/// Removes all transactions from the keychain.
public func removeTransactions() {
self.keychain.delete(DYFStoreTransactionsKey)
}
}
*/

View File

@ -0,0 +1,273 @@
//
// DYFStoreReceiptVerifier.swift
//
// Created by dyf on 2016/11/28. ( https://github.com/dgynfi/DYFStoreReceiptVerifier )
// Copyright © 2016 dyf. All rights reserved.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import Foundation
/// The class is used to verify in-app purchase receipts.
open class DYFStoreReceiptVerifier: NSObject {
/// Callbacks the result of the request that verifies the in-app purchase receipt.
@objc public weak var delegate: DYFStoreReceiptVerifierDelegate?
/// The url for sandbox in the test environment.
private let sandboxUrl = "https://sandbox.itunes.apple.com/verifyReceipt"
/// The url for production in the production environment.
private let productUrl = "https://buy.itunes.apple.com/verifyReceipt"
/// The data for a POST request.
private var requestData: Data?
/// A default session configuration object.
private var urlSessionConfig = URLSessionConfiguration.default
/// An object that coordinates a group of related network data transfer tasks.
private var urlSession: URLSession?
/// A URL session task that returns downloaded data directly to the app in memory.
private var dataTask: URLSessionDataTask?
/// Whether all outstanding tasks have been cancelled and the session has been invalidated.
private var canInvalidateSession: Bool = false
/// Instantiates a DYFStoreReceiptVerifier object.
public override init() {
super.init()
self.instantiateUrlSession()
}
/// Checks the url session configuration object.
private func checkUrlSessionConfig() {
self.urlSessionConfig.allowsCellularAccess = true
}
/// Instantiates the url session object.
private func instantiateUrlSession() {
self.checkUrlSessionConfig()
self.urlSession = URLSession(configuration: urlSessionConfig)
}
/// Cancels the task.
@objc public func cancel() {
self.dataTask?.cancel()
}
/// Cancels all outstanding tasks and then invalidates the session.
@objc public func invalidateAndCancel() {
self.urlSession?.invalidateAndCancel()
self.canInvalidateSession = true
}
/// Verifies the in-app purchase receipt, but it is not recommended to use. It is better to use your own server to obtain the parameters uploaded from the client to verify the receipt from the app store server (C -> Uploaded Parameters -> S -> App Store S -> S -> Receive And Parse Data -> C).
/// If the receipts are verified by your own server, the client needs to upload these parameters, such as: "transaction identifier, bundle identifier, product identifier, user identifier, shared sceret(Subscription), receipt(Safe URL Base64), original transaction identifier(Optional), original transaction time(Optional) and the device information, etc.".
///
/// - Parameter receiptData: A signed receipt that records all information about a successful payment transaction.
@objc public func verifyReceipt(_ receiptData: Data?) {
verifyReceipt(receiptData, sharedSecret: nil)
}
/// Verifies the in-app purchase receipt, but it is not recommended to use. It is better to use your own server to obtain the parameters uploaded from the client to verify the receipt from the app store server (C -> Uploaded Parameters -> S -> App Store S -> S -> Receive And Parse Data -> C).
/// If the receipts are verified by your own server, the client needs to upload these parameters, such as: "transaction identifier, bundle identifier, product identifier, user identifier, shared sceret(Subscription), receipt(Safe URL Base64), original transaction identifier(Optional), original transaction time(Optional) and the device information, etc.".
///
/// - Parameters:
/// - receiptData: A signed receipt that records all information about a successful payment transaction.
/// - secretKey: Your apps shared secret (a hexadecimal string). Only used for receipts that contain auto-renewable subscriptions.
@objc public func verifyReceipt(_ receiptData: Data?, sharedSecret secretKey: String? = nil) {
guard let data = receiptData else {
let messae = "The received data is null."
let error = NSError(domain: "SRErrorDomain.DYFStore",
code: -12,
userInfo: [NSLocalizedDescriptionKey : messae])
self.delegate?.verifyReceipt(self, didFailWithError: error)
return
}
let receiptBase64 = data.base64EncodedString()
// Creates the JSON object that describes the request.
var requestContents: [String: Any] = [String: Any]()
requestContents["receipt-data"] = receiptBase64
if let key = secretKey {
requestContents["password"] = key
}
do {
self.requestData = try JSONSerialization.data(withJSONObject: requestContents)
self.connect(withUrl: productUrl)
} catch let error {
self.delegate?.verifyReceipt(self, didFailWithError: error as NSError)
}
}
// Make a connection to the iTunes Store on a background queue.
private func connect(withUrl url: String) {
let aURL: URL = URL(string: url)!
// Creates a POST request with the receipt data.
var request = URLRequest(url: aURL, cachePolicy: URLRequest.CachePolicy.reloadIgnoringCacheData, timeoutInterval: 15.0)
request.httpMethod = "POST"
request.httpBody = self.requestData
if self.canInvalidateSession {
self.instantiateUrlSession()
self.canInvalidateSession = false
}
self.dataTask = self.urlSession?.dataTask(with: request) { (data, response, error) in
self.didReceiveData(data, response: response, error: error)
}
self.dataTask?.resume()
}
private func didReceiveData(_ data: Data?, response: URLResponse?, error: Error?) {
if let err = error {
let nsError = err as NSError
DispatchQueue.main.async {
self.delegate?.verifyReceipt(self, didFailWithError: nsError)
}
} else {
self.processResult(data!)
}
}
private func processResult(_ data: Data) {
do {
let jsonObj = try JSONSerialization.jsonObject(with: data)
let dict = jsonObj as! Dictionary<String, Any>
let status = dict["status"] as! Int
if status == 0 {
DispatchQueue.main.async {
self.delegate?.verifyReceiptDidFinish(self, didReceiveData: dict)
}
} else if status == 21007 { // sandbox
self.connect(withUrl: sandboxUrl)
} else {
let (code, message) = matchMessage(withStatus: status)
let nsError = NSError(domain: "SRErrorDomain.DYFStore",
code: code,
userInfo: [NSLocalizedDescriptionKey : message])
DispatchQueue.main.async {
self.delegate?.verifyReceipt(self, didFailWithError: nsError)
}
}
} catch let error {
let nsError = error as NSError
DispatchQueue.main.async {
self.delegate?.verifyReceipt(self, didFailWithError: nsError)
}
}
}
/// Matches the message with the status code.
///
/// - Parameter status: The status code of the request response. More, please see [Receipt Validation Programming Guide](https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateRemotely.html#//apple_ref/doc/uid/TP40010573-CH104-SW1)
/// - Returns: A tuple that contains status code and the description of status code.
public func matchMessage(withStatus status: Int) -> (Int, String) {
var message: String = ""
switch status {
case 0:
message = "The receipt as a whole is valid."
break
case 21000:
message = "The App Store could not read the JSON object you provided."
break
case 21002:
message = "The data in the receipt-data property was malformed or missing."
break
case 21003:
message = "The receipt could not be authenticated."
break
case 21004:
message = "The shared secret you provided does not match the shared secret on file for your account."
break
case 21005:
message = "The receipt server is not currently available."
break
case 21006:
message = "This receipt is valid but the subscription has expired. When this status code is returned to your server, the receipt data is also decoded and returned as part of the response. Only returned for iOS 6 style transaction receipts for auto-renewable subscriptions."
break
case 21007:
message = "This receipt is from the test environment, but it was sent to the production environment for verification. Send it to the test environment instead."
break
case 21008:
message = "This receipt is from the production environment, but it was sent to the test environment for verification. Send it to the production environment instead."
break
case 21010:
message = "This receipt could not be authorized. Treat this the same as if a purchase was never made."
break
default: /* 21100-21199 */
message = "Internal data access error."
break
}
return (status, message)
}
/// Matches the message with the status code.
///
/// - Parameter status: The status code of the request response. More, please see [Receipt Validation Programming Guide](https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateRemotely.html#//apple_ref/doc/uid/TP40010573-CH104-SW1)
/// - Returns: A string that contains the description of status code.
@objc public func matchMessage(withStatus status: Int) -> String {
let (_, msg) = matchMessage(withStatus: status)
return msg
}
}
/// The delegate is used to callback the result of verifying the in-app purchase receipt.
@objc public protocol DYFStoreReceiptVerifierDelegate: NSObjectProtocol {
/// Tells the delegate that an in-app purchase receipt verification has completed.
///
/// - Parameters:
/// - verifier: A `DYFStoreReceiptVerifier` object.
/// - data: The data received from the server, is converted to a dictionary of key-value pairs.
@objc func verifyReceiptDidFinish(_ verifier: DYFStoreReceiptVerifier, didReceiveData data: [String : Any])
/// Tells the delegate that an in-app purchase receipt verification occurs an error.
///
/// - Parameters:
/// - verifier: A `DYFStoreReceiptVerifier` object.
/// - error: The error that caused the receipt validation to fail.
@objc func verifyReceipt(_ verifier: DYFStoreReceiptVerifier, didFailWithError error: NSError)
}

View File

@ -0,0 +1,96 @@
//
// DYFStoreTransaction.swift
//
// Created by dyf on 2016/11/28. ( https://github.com/dgynfi/DYFStore )
// Copyright © 2016 dyf. All rights reserved.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import Foundation
open class DYFStoreTransaction: NSObject, NSCoding {
/// The state of this transaction. 0: purchased, 1: restored.
@objc open var state: UInt8 = 0
/// A string used to identify a product that can be purchased from within your app.
@objc open var productIdentifier: String?
/// An opaque identifier for the users account on your system.
@objc open var userIdentifier: String?
/// When a transaction is restored, the current transaction holds a new transaction timestamp. Your app will read this property to retrieve the restored transaction timestamp.
@objc open var originalTransactionTimestamp: String?
/// When a transaction is restored, the current transaction holds a new transaction identifier. Your app will read this property to retrieve the restored transaction identifier.
@objc open var originalTransactionIdentifier: String?
/// The timestamp when the transaction was added to the server queue. Only valid if state is purchased or restored.
@objc open var transactionTimestamp: String?
/// The unique server-provided identifier. Only valid if state is purchased or restored.
@objc open var transactionIdentifier: String?
/// A base64 signed receipt that records all information about a successful payment transaction.
///
/// The contents of this property are undefined except when transactionState is set to purchased.
/// The receipt is a signed chunk of data that can be sent to the App Store to verify that the payment was successfully processed. This is most useful when designing a store that uses a server separate from the iPhone to verify that payment was processed. For more information on verifying receipts, see [Receipt Validation Programming Guide](https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Introduction.html#//apple_ref/doc/uid/TP40010573).
@objc open var transactionReceipt: String?
/// Instantiates a DYFStoreTransaction object.
public override init() {
super.init()
}
/// The Secure Coding Guide should be consulted when writing methods that decode data.
//public static var supportsSecureCoding: Bool {
// return true
//}
/// Returns an object initialized from data in a given unarchiver.
///
/// - Parameter aDecoder: An unarchiver object.
public required convenience init?(coder aDecoder: NSCoder) {
self.init()
DYFSwiftRuntimeProvider.decode(aDecoder, forObject: self)
}
/// Encodes the receiver using a given archiver.
///
/// - Parameter aCoder: An archiver object.
public func encode(with aCoder: NSCoder) {
DYFSwiftRuntimeProvider.encode(aCoder, forObject: self)
}
}
/// Used to represent the state of a transaction.
@objc public enum DYFStoreTransactionState: UInt8 {
/// Indicates that the transaction has been purchased.
case purchased
/// Indicates that the transaction has been restored.
case restored
}
/// The key UserDefaults and Keychain used.
public let DYFStoreTransactionsKey = "DYFStoreTransactions"

View File

@ -0,0 +1,165 @@
//
// DYFStoreUserDefaultsPersistence.swift
//
// Created by dyf on 2016/11/28. ( https://github.com/dgynfi/DYFStore )
// Copyright © 2016 dyf. All rights reserved.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import Foundation
/// Returns the shared defaults `UserDefaults` object.
fileprivate let kUserDefaults = UserDefaults.standard
/// The transaction persistence using the UserDefaults.
open class DYFStoreUserDefaultsPersistence: NSObject {
/// Loads an array whose elements are the `Data` objects from the shared preferences search list.
///
/// - Returns: An array whose elements are the `Data` objects.
private func loadDataFromUserDefaults() -> [Data]? {
let obj = kUserDefaults.object(forKey: DYFStoreTransactionsKey)
return obj as? [Data]
}
/// Returns a Boolean value that indicates whether a transaction is present in shared preferences search list with a given transaction ientifier.
///
/// - Parameter transactionIdentifier: The unique server-provided identifier.
/// - Returns: True if a transaction is present in shared preferences search list, otherwise false.
public func containsTransaction(_ transactionIdentifier: String) -> Bool {
let array = loadDataFromUserDefaults()
guard let arr = array, arr.count > 0 else {
return false
}
for data in arr {
let obj = DYFStoreConverter.decodeObject(data)
let transaction = obj as? DYFStoreTransaction
let identifier = transaction?.transactionIdentifier
if let id = identifier, id == transactionIdentifier {
return true
}
}
return false
}
/// Stores an `DYFStoreTransaction` object in the shared preferences search list.
///
/// - Parameter transaction: An `DYFStoreTransaction` object.
public func storeTransaction(_ transaction: DYFStoreTransaction?) {
let data = DYFStoreConverter.encodeObject(transaction)
guard let aData = data else {
return
}
var transactions = loadDataFromUserDefaults() ?? [Data]()
transactions.append(aData)
kUserDefaults.set(transactions, forKey: DYFStoreTransactionsKey)
kUserDefaults.synchronize()
}
/// Retrieves an array whose elements are the `DYFStoreTransaction` objects from the shared preferences search list.
///
/// - Returns: An array whose elements are the `DYFStoreTransaction` objects.
public func retrieveTransactions() -> [DYFStoreTransaction]? {
let array = loadDataFromUserDefaults()
guard let arr = array else {
return nil
}
var transactions = [DYFStoreTransaction]()
for item in arr {
let obj = DYFStoreConverter.decodeObject(item)
if let transaction = obj as? DYFStoreTransaction {
transactions.append(transaction)
}
}
return transactions
}
/// Retrieves an `DYFStoreTransaction` object from the shared preferences search list with a given transaction ientifier.
///
/// - Parameter transactionIdentifier: The unique server-provided identifier.
/// - Returns: An `DYFStoreTransaction` object from the shared preferences search list.
public func retrieveTransaction(_ transactionIdentifier: String) -> DYFStoreTransaction? {
let array = retrieveTransactions()
guard let arr = array else {
return nil
}
for transaction in arr {
let identifier = transaction.transactionIdentifier
if identifier == transactionIdentifier {
return transaction
}
}
return nil
}
/// Removes an `DYFStoreTransaction` object from the shared preferences search list with a given transaction ientifier.
///
/// - Parameter transactionIdentifier: The unique server-provided identifier.
public func removeTransaction(_ transactionIdentifier: String) {
let array = loadDataFromUserDefaults()
guard var arr = array else {
return
}
var index = -1
for (idx, data) in arr.enumerated() {
let obj = DYFStoreConverter.decodeObject(data)
let transaction = obj as? DYFStoreTransaction
let identifier = transaction?.transactionIdentifier
if let id = identifier, id == transactionIdentifier {
index = idx
break
}
}
guard index >= 0 else { return }
arr.remove(at: index)
kUserDefaults.setValue(arr, forKey: DYFStoreTransactionsKey)
kUserDefaults.synchronize()
}
/// Removes all transactions from the shared preferences search list.
public func removeTransactions() {
kUserDefaults.removeObject(forKey: DYFStoreTransactionsKey);
kUserDefaults.synchronize()
}
}

View File

@ -0,0 +1,359 @@
//
// DYFSwiftRuntimeProvider.swift
//
//
// Created by zhl on 2021/7/22.
// Copyright © 2021 egret. All rights reserved.
//
import Foundation
/// The class for runtime wrapper that provides some common practical applications.
public class DYFSwiftRuntimeProvider: NSObject {
/// Instantiates a DYFSwiftRuntimeProvider object.
public override init() {
super.init()
}
/// Describes the instance methods implemented by a class.
///
/// - Parameter cls: The class you want to inspect.
/// - Returns: String array of the instance methods.
@objc public class func methodList(withClass cls: AnyClass?) -> [String] {
var names: [String] = [String]()
var count: UInt32 = 0
let methodList: UnsafeMutablePointer<Method>? = class_copyMethodList(cls, &count)
for index in 0..<Int(count) {
let sel = method_getName(methodList![index])
let selName = String(cString: sel_getName(sel))
names.append(selName)
}
return names
}
/// To get the class methods of a class.
///
/// - Parameter obj: The object you want to inspect.
/// - Returns: String array of the class methods.
@objc public class func classMethodList(_ obj: Any?) -> [String] {
var names: [String] = [String]()
var count: UInt32 = 0
let methodList = class_copyMethodList(object_getClass(obj), &count)
for index in 0..<Int(count) {
let sel = method_getName(methodList![index])
let selName = String(cString: sel_getName(sel))
names.append(selName)
}
return names
}
/// Describes the instance variables declared by a class.
///
/// - Parameter cls: The class you want to inspect.
/// - Returns: String array of the instance variables.
@objc public class func ivarList(withClass cls: AnyClass?) -> [String] {
var names: [String] = [String]()
var count: UInt32 = 0
let ivarList = class_copyIvarList(cls, &count)
for index in 0..<Int(count) {
let ivar = ivarList![index]
if let ivarName = ivar_getName(ivar) {
let s = String(cString: ivarName)
names.append(s)
}
}
return names
}
/// Adds a new method to a class with a given selector and implementation.
///
/// - Parameters:
/// - cls: The class to which to add a method.
/// - sel: A selector that specifies the name of the method being added.
/// - impCls: The class you want to inspect.
/// - impSel: The selector of the method you want to retrieve.
/// - Returns: A Bool value.
@objc public class func addMethod(withClass cls: AnyClass?, selector sel: Selector, impClass impCls: AnyClass? = nil, impSelector impSel: Selector) -> Bool {
let _impCls: AnyClass? = impCls ?? cls
guard let imp = class_getMethodImplementation(_impCls, impSel) else {
return false
}
var types: UnsafePointer<Int8>? = nil
let method = class_getInstanceMethod(_impCls, impSel)
if let m = method {
types = method_getTypeEncoding(m)
}
return class_addMethod(cls, sel, imp, types)
}
/// Adds a new method to a class with a given selector and implementation.
///
/// - Parameters:
/// - cls: The class to which to add a method.
/// - sel: A selector that specifies the name of the method being added.
/// - impCls: The class you want to inspect.
/// - impSel: The selector of the method you want to retrieve.
/// - types: A string describing a method's parameter and return types. e.g.: "v@:"
/// - Returns: A Bool value.
@objc public class func addMethod(withClass cls: AnyClass?, selector sel: Selector, impClass impCls: AnyClass? = nil, impSelector impSel: Selector, types: String) -> Bool {
let _impCls: AnyClass? = impCls ?? cls
guard let imp = class_getMethodImplementation(_impCls, impSel) else {
return false
}
let _types: UnsafePointer<Int8>? = (types as NSString).utf8String
return class_addMethod(cls, sel, imp, _types)
}
/// Exchanges the implementations of two methods.
///
/// - Parameters:
/// - cls: The class you want to modify.
/// - sel: A selector that identifies the method whose implementation you want to exchange.
/// - targetCls: The class you want to specify.
/// - targetSel: The selector of the method you want to retrieve.
@objc public class func exchangeMethod(withClass cls: AnyClass?, selector sel: Selector, targetClass targetCls: AnyClass?, targetSelector targetSel: Selector) {
guard let m1 = class_getInstanceMethod(cls, sel), let m2 = class_getInstanceMethod(targetCls, targetSel) else {
return
}
method_exchangeImplementations(m1, m2)
}
/// Replaces the implementation of a method for a given class.
///
/// - Parameters:
/// - cls: The class you want to modify.
/// - sel: A selector that identifies the method whose implementation you want to replace.
/// - targetCls: The class you want to specify.
/// - targetSel: The selector of the method you want to retrieve.
@objc public class func replaceMethod(withClass cls: AnyClass?, selector sel: Selector, targetClass targetCls: AnyClass? = nil, targetSelector targetSel: Selector) {
let _targetCls: AnyClass? = targetCls ?? cls
guard let imp = class_getMethodImplementation(_targetCls, targetSel) else {
return
}
var types: UnsafePointer<Int8>? = nil
let method = class_getInstanceMethod(_targetCls, targetSel)
if let m = method {
types = method_getTypeEncoding(m)
}
class_replaceMethod(cls, sel, imp, types)
}
/// Describes the properties declared by a class.
///
/// - Parameter cls: The class you want to inspect.
/// - Returns: String array of the properties.
@objc public class func propertyList(withClass cls: AnyClass?) -> [String] {
var names: [String] = [String]()
var count: UInt32 = 0
let pList = class_copyPropertyList(cls, &count)
for index in 0..<Int(count) {
let p: objc_property_t = pList![index]
let name = String(cString: property_getName(p))
names.append(name)
}
return names
}
/// Gets the swift namespace from the bundles Info.plist file.
///
/// - Returns: A string object.
public class func swiftNamespace() -> String? {
// The name of the executable in this bundle (if any).
let executableKey = kCFBundleExecutableKey as String
//A dictionary, constructed from the bundles Info.plist file.
let infoDict = Bundle.main.infoDictionary ?? [String : Any]()
// Fetches the value for a key.
guard let namespace = infoDict[executableKey] as? String else {
return nil
}
return namespace
}
/// Converts a dictionary whose elements are key-value pairs to a corresponding object.
///
/// - Parameters:
/// - dictionary: A collection whose elements are key-value pairs.
/// - cls: A class that inherits the NSObject class.
/// - Returns: A corresponding object.
public class func model<T: NSObject>(withDictionary dictionary: Dictionary<String, Any>?, forClass cls: T.Type?) -> T? {
// Gets the swift namespace.
//guard let namespace = swiftNamespace() else {
// return nil
//}
//let className = String(cString: class_getName(cls))
//if className.isEmpty { return nil }
//let clsName = "\(namespace).\(className)"
//print("clsName: \(clsName)")
//let aCls: AnyClass? = NSClassFromString(clsName)
//guard let clsType = aCls as? NSObject.Type else {
// return nil
//}
//let obj = clsType.init()
guard let clsType = cls else {
return nil
}
let obj = clsType.init()
guard let dict = dictionary else {
return obj
}
let pList = propertyList(withClass: cls)
for (k, v) in dict {
if pList.contains(k) {
obj.setValue(v, forKey: k)
}
}
return obj
}
/// Converts a dictionary whose elements are key-value pairs to a corresponding object.
///
/// - Parameters:
/// - dictionary: A collection whose elements are key-value pairs.
/// - cls: A class that inherits the NSObject class.
/// - Returns: A corresponding object.
@objc public class func model(withDictionary dictionary: Dictionary<String, Any>?, usingClass cls: NSObject.Type?) -> AnyObject? {
guard let clsType = cls else {
return nil
}
let obj = clsType.init()
guard let dict = dictionary else {
return obj
}
let pList = propertyList(withClass: clsType)
for (k, v) in dict {
if pList.contains(k) {
obj.setValue(v, forKey: k)
}
}
return obj
}
/// Converts a dictionary whose elements are key-value pairs to a corresponding object.
///
/// - Parameters:
/// - dictionary: A collection whose elements are key-value pairs.
/// - model: An object that inherits the NSObject class.
/// - Returns: A corresponding object.
@objc public class func model(withDictionary dictionary: Dictionary<String, Any>?, usingModel model: NSObject?) -> AnyObject? {
guard let dict = dictionary else {
return model
}
guard let obj = model else { return nil }
let cls: AnyClass? = object_getClass(obj)
let pList = propertyList(withClass: cls)
for (k, v) in dict {
if pList.contains(k) {
obj.setValue(v, forKey: k)
}
}
return obj
}
/// Converts a object to a corresponding dictionary whose elements are key-value pairs.
///
/// - Parameter model: A NSObject object.
/// - Returns: A corresponding dictionary.
@objc public class func dictionary(withModel model: NSObject?) -> [String: Any]? {
guard let m = model, let cls = object_getClass(m) else {
return nil
}
let pList = propertyList(withClass: cls)
if pList.isEmpty {
return nil
}
var dict = [String : Any]()
for key in pList {
if let value = m.value(forKey: key) {
dict[key] = value
} else {
dict[key] = NSNull()
}
}
return dict
}
/// Encodes an object using a given archiver.
///
/// - Parameters:
/// - encoder: An archiver object.
/// - obj: An object you want to encode.
@objc public class func encode(_ encoder: NSCoder, forObject obj: NSObject) {
let ivarNames = ivarList(withClass: obj.classForCoder)
for key in ivarNames {
let value = obj.value(forKey: key)
encoder.encode(value, forKey: key)
}
}
/// Decodes an object initialized from data in a given unarchiver.
///
/// - Parameters:
/// - decoder: An unarchiver object.
/// - obj: An object you want to decode.
@objc public class func decode(_ decoder: NSCoder, forObject obj: NSObject) {
let ivarNames = ivarList(withClass: obj.classForCoder)
for key in ivarNames {
let value = decoder.decodeObject(forKey: key)
obj.setValue(value, forKey: key)
}
}
}

View File

@ -0,0 +1,183 @@
//
// ZExtensions.swift
//
//
// Created by zhl on 2021/7/22.
// Copyright © 2021 egret. All rights reserved.
//
import Foundation
import UIKit
fileprivate var LoadingViewKey = "LoadingViewKey"
extension NSObject {
/// Returns The view controller associated with the currently visible view.
///
/// - Returns: The view controller associated with the currently visible view.
public func currentViewController() -> UIViewController? {
let sharedApp = UIApplication.shared
let window = sharedApp.keyWindow ?? sharedApp.windows[0]
let viewController = window.rootViewController
return findCurrentViewController(from: viewController)
}
private func findCurrentViewController(from viewController: UIViewController?) -> UIViewController? {
guard var vc = viewController else {
return nil
}
while true {
if let tvc = vc.presentedViewController {
vc = tvc
} else if vc.isKind(of: UITabBarController.self) {
let tbc = vc as! UITabBarController
if let tvc = tbc.selectedViewController {
vc = tvc
}
} else if vc.isKind(of: UINavigationController.self) {
let nc = vc as! UINavigationController
if let tvc = nc.visibleViewController {
vc = tvc
}
} else {
if vc.children.count > 0 {
if let tvc = vc.children.last {
vc = tvc
}
}
break
}
}
return vc
}
/// Shows the tips for user.
public func showTipsMessage(_ message: String) -> Void {
guard let vc = self.currentViewController(), !vc.isKind(of: UIAlertController.self) else {
return
}
let alertController = UIAlertController(title: message, message: nil, preferredStyle: UIAlertController.Style.alert)
vc.present(alertController, animated: true, completion: nil)
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
alertController.dismiss(animated: true, completion: nil)
}
}
/// Shows an alert view controller.
public func showAlert(withTitle title: String?,
message: String?,
cancelButtonTitle: String? = nil,
cancel cancelHandler: ((UIAlertAction) -> Void)? = nil,
confirmButtonTitle: String?,
execute executableHandler: ((UIAlertAction) -> Void)? = nil) {
guard let vc = self.currentViewController() else {
return
}
let alertController = UIAlertController(title: title, message: message, preferredStyle: UIAlertController.Style.alert)
if let t = cancelButtonTitle, t.count > 0 {
let action = UIAlertAction(title: t, style: UIAlertAction.Style.cancel, handler: cancelHandler)
alertController.addAction(action)
}
if let t = confirmButtonTitle, t.count > 0 {
let action = UIAlertAction(title: t, style: UIAlertAction.Style.default, handler: executableHandler)
alertController.addAction(action)
}
vc.present(alertController, animated: true, completion: nil)
}
/// Shows a loading panel.
public func showLoading(_ text: String) {
let value = objc_getAssociatedObject(self, &LoadingViewKey)
if value != nil {
return
}
let loadingView = ZLoadingView()
loadingView.show(text)
loadingView.color = COLOR_RGBA(10, 10, 10, 0.75)
loadingView.indicatorColor = COLOR_RGB(54, 205, 64)
loadingView.textColor = COLOR_RGB(248, 248, 248)
objc_setAssociatedObject(self, &LoadingViewKey, loadingView, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
/// Hides a loading panel.
public func hideLoading() {
let value = objc_getAssociatedObject(self, &LoadingViewKey)
guard let loadingView = value as? ZLoadingView else {
return
}
loadingView.hide()
objc_setAssociatedObject(self, &LoadingViewKey, nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
extension UIView {
/// This method is used to set the corner.
///
/// - Parameters:
/// - rectCorner: The corners of a rectangle.
/// - radius: The radius of each corner.
public func setCorner(rectCorner: UIRectCorner = UIRectCorner.allCorners, radius: CGFloat) {
let maskLayer = CAShapeLayer()
let w = self.bounds.size.width
let h = self.bounds.size.height
maskLayer.frame = CGRect(x: 0, y: 0, width: w, height: h)
let path = UIBezierPath(roundedRect: self.bounds, byRoundingCorners: rectCorner, cornerRadii: CGSize(width: radius, height: radius))
maskLayer.path = path.cgPath
self.layer.mask = maskLayer
}
/// This method is used to set the border.
///
/// - Parameters:
/// - rectCorner: The corners of a rectangle.
/// - radius: The radius of each corner.
/// - lineWidth: Specifies the line width of the shapes path.
/// - color: The color used to stroke the shapes path.
public func setBorder(rectCorner: UIRectCorner = UIRectCorner.allCorners, radius: CGFloat, lineWidth: CGFloat, color: UIColor?) {
let maskLayer = CAShapeLayer()
let w = self.bounds.size.width
let h = self.bounds.size.height
maskLayer.frame = CGRect(x: 0, y: 0, width: w, height: h)
let borderLayer = CAShapeLayer()
borderLayer.frame = CGRect(x: 0, y: 0, width: w, height: h)
borderLayer.lineWidth = lineWidth
borderLayer.strokeColor = color?.cgColor
borderLayer.fillColor = UIColor.clear.cgColor
let path = UIBezierPath(roundedRect: self.bounds, byRoundingCorners: rectCorner, cornerRadii: CGSize(width: radius, height: radius))
borderLayer.path = path.cgPath
maskLayer.path = path.cgPath
self.layer.insertSublayer(borderLayer, at: 0)
self.layer.mask = maskLayer
}
}

View File

@ -0,0 +1,216 @@
//
// ZIndefiniteAnimatedSpinner.swift
//
//
// Created by zhl on 2021/7/22.
// Copyright © 2021 egret. All rights reserved.
//
import UIKit
public class ZIndefiniteAnimatedSpinner: UIView {
/// A structure is named "AnimationKey".
private struct AnimationKey {
static let stroke = "spinner.animkey.stroke"
static let rotation = "spinner.animkey.rotation"
}
/// The property indicates whether the view is currently animating.
public private(set) var isAnimating: Bool = false
/// Sets whether the view is hidden when not animating.
public var hidesWhenStopped: Bool = true
/// Specifies the timing function to use for the control's animation. Defaults to kCAMediaTimingFunctionEaseInEaseOut.
public var timingFunction: CAMediaTimingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
/// A layer that draws a arc progress in its coordinate space.
private lazy var progressLayer: CAShapeLayer = {
let layer = CAShapeLayer()
layer.strokeColor = nil
layer.fillColor = nil
layer.lineWidth = 1.0
return layer
}()
/// Sets the line width of the spinner's circle.
public var lineWidth: CGFloat {
get {
return self.progressLayer.lineWidth
}
set {
self.progressLayer.lineWidth = newValue
self.updatePath()
}
}
/// Sets the line color of the spinner's circle.
public var lineColor: UIColor? {
get {
guard let color = self.progressLayer.strokeColor else {
return nil
}
return UIColor.init(cgColor: color)
}
set (newColor) {
self.progressLayer.strokeColor = newColor?.cgColor
}
}
public override init(frame: CGRect) {
super.init(frame: frame)
self.setup()
}
public required init?(coder: NSCoder) {
super.init(coder: coder)
// Supports an Interface Builder archive, or nib file.
}
public override func awakeFromNib() {
super.awakeFromNib()
self.setup()
}
private func setup() {
self.layer.addSublayer(self.progressLayer)
let selector = #selector(ZIndefiniteAnimatedSpinner.resetAnimations)
let name = UIApplication.didBecomeActiveNotification
NotificationCenter.default.addObserver(self, selector: selector, name: name, object: nil)
}
/// Starts animation of the spinner.
public func startAnimating() {
if self.isAnimating {
return
}
self.isAnimating = true
self.addLayerAnimations()
self.isHidden = false
}
/// Stops animation of the spinnner.
public func stopAnimating() {
if !self.isAnimating {
return
}
self.isAnimating = false
self.progressLayer.removeAnimation(forKey: AnimationKey.rotation)
self.progressLayer.removeAnimation(forKey: AnimationKey.stroke)
if self.hidesWhenStopped {
self.isHidden = true
}
}
private func addLayerAnimations() {
let animation = CABasicAnimation()
animation.keyPath = "transform.rotation"
animation.duration = 2.0
animation.fromValue = NSNumber(value: 0.0)
animation.toValue = NSNumber(value: 2*Double.pi)
animation.repeatCount = Float.infinity
self.progressLayer.add(animation, forKey: AnimationKey.rotation)
let headAnimation = CABasicAnimation()
headAnimation.keyPath = "strokeStart"
headAnimation.duration = 1.0
headAnimation.fromValue = NSNumber(value: 0.0)
headAnimation.toValue = NSNumber(value: 0.25)
headAnimation.timingFunction = self.timingFunction
let tailAnimation = CABasicAnimation()
tailAnimation.keyPath = "strokeEnd"
tailAnimation.duration = 1.0
tailAnimation.fromValue = NSNumber(value: 0.0)
tailAnimation.toValue = NSNumber(value: 1.0)
tailAnimation.timingFunction = self.timingFunction
let endHeadAnimation = CABasicAnimation()
endHeadAnimation.keyPath = "strokeStart"
endHeadAnimation.beginTime = 1.0
endHeadAnimation.duration = 0.5
endHeadAnimation.fromValue = NSNumber(value: 0.25)
endHeadAnimation.toValue = NSNumber(value: 1.0)
endHeadAnimation.timingFunction = self.timingFunction
let endTailAnimation = CABasicAnimation()
endTailAnimation.keyPath = "strokeEnd"
endTailAnimation.beginTime = 1.0
endTailAnimation.duration = 0.5
endTailAnimation.fromValue = NSNumber(value: 1.0)
endTailAnimation.toValue = NSNumber(value: 1.0)
endTailAnimation.timingFunction = self.timingFunction
let animGroup = CAAnimationGroup()
animGroup.repeatCount = Float.infinity
animGroup.duration = 1.5
animGroup.animations = [headAnimation,
tailAnimation,
endHeadAnimation,
endTailAnimation]
self.progressLayer.add(animGroup, forKey: AnimationKey.stroke)
}
@objc private func resetAnimations() {
if self.isAnimating {
self.stopAnimating()
self.startAnimating()
}
}
public override func layoutSubviews() {
super.layoutSubviews()
let sW = self.bounds.size.width
let sH = self.bounds.size.height
self.progressLayer.frame = CGRect(x: 0, y: 0, width: sW, height: sH)
self.updatePath()
}
private func updatePath() {
let sW = self.bounds.size.width
let sH = self.bounds.size.height
let center = CGPoint(x: sW/2, y: sH/2)
let radius = min(sW/2, sH/2) - self.lineWidth/2
let startAngle = CGFloat(0.0)
let endAngle = CGFloat(2*Double.pi)
let path = UIBezierPath(arcCenter: center,
radius: radius,
startAngle: startAngle,
endAngle: endAngle,
clockwise: true)
self.progressLayer.path = path.cgPath
self.progressLayer.strokeStart = 0.0
self.progressLayer.strokeEnd = 0.0
}
private func executeWhenReleasing() {
self.stopAnimating()
let name = UIApplication.didBecomeActiveNotification
NotificationCenter.default.removeObserver(self, name: name, object: nil)
}
deinit {
#if DEBUG
print("[\((#file as NSString).lastPathComponent):\(#function)]")
#endif
self.executeWhenReleasing()
}
}

View File

@ -0,0 +1,299 @@
//
// ZLoadingView.swift
//
//
// Created by zhl on 2021/7/22.
// Copyright © 2021 egret. All rights reserved.
//
import UIKit
/// Creates and returns a color object using the specified opacity and RGB component values.
///
/// - Parameters:
/// - r: The red value of the color object, specified as a value from 0.0 to 255.0.
/// - g: The green value of the color object, specified as a value from 0.0 to 255.0.
/// - b: The blue value of the color object, specified as a value from 0.0 to 255.0.
/// - alp: The opacity value of the color object, specified as a value from 0.0 to 1.0.
/// - Returns: The color object. The color information represented by this object is in an RGB colorspace.
public func COLOR_RGBA(_ r: CGFloat,
_ g: CGFloat,
_ b: CGFloat,
_ alp: CGFloat) -> UIColor {
return UIColor(red: r/255.0, green: g/255.0, blue: b/255.0, alpha: alp)
}
/// Creates and returns a color object using the specified opacity and RGB component values.
///
/// - Parameters:
/// - r: The red value of the color object, specified as a value from 0.0 to 255.0.
/// - g: The green value of the color object, specified as a value from 0.0 to 255.0.
/// - b: The blue value of the color object, specified as a value from 0.0 to 255.0.
/// - Returns: The color object. The color information represented by this object is in an RGB colorspace.
public func COLOR_RGB(_ r: CGFloat,
_ g: CGFloat,
_ b: CGFloat) -> UIColor {
return COLOR_RGBA(r, g, b, 1.0)
}
/// Returns the width of the screen for the device.
public let SCREEN_W = UIScreen.main.bounds.size.width
/// Returns the height of the screen for the device.
public let SCREEN_H = UIScreen.main.bounds.size.height
public class ZLoadingView: UIView {
/// It is used to act as background mask panel.
private lazy var maskPanel: UIView = {
let view = UIView()
view.backgroundColor = COLOR_RGBA(20, 20, 20, 0.5)
return view
}()
/// It is used to render the content.
private lazy var contentView: UIView = {
let view = UIView()
view.backgroundColor = COLOR_RGB(255, 255, 255)
return view
}()
/// The spinner is used to provide an indefinite animation.
private lazy var indicator: ZIndefiniteAnimatedSpinner = {
let spinner = ZIndefiniteAnimatedSpinner()
spinner.backgroundColor = UIColor.clear
spinner.lineColor = COLOR_RGB(100, 100, 100)
return spinner
}()
/// It is used to show the text.
private lazy var textLabel: UILabel = {
let label = UILabel()
label.backgroundColor = UIColor.clear
label.textColor = COLOR_RGB(60, 60, 60)
return label
}()
/// Returns the current window of the app.
private func appWindow() -> UIWindow {
let sharedApp = UIApplication.shared
return sharedApp.keyWindow ?? sharedApp.windows[0]
}
/// The color to set the background color of the content view.
public var color: UIColor? {
get {
return self.contentView.backgroundColor
}
set {
self.contentView.backgroundColor = newValue
}
}
/// The color to set the line color of the indicator.
public var indicatorColor: UIColor? {
get {
return self.indicator.lineColor
}
set {
self.indicator.lineColor = newValue
}
}
/// The color to set the text color of the text label.
public var textColor: UIColor? {
get {
return self.textLabel.textColor
}
set (newColor) {
self.textLabel.textColor = newColor
}
}
/// Initializes and returns a newly allocated view object with the specified frame rectangle.
/// - Parameter frame: The frame rectangle for the view.
public override init(frame: CGRect) {
super.init(frame: frame)
}
/// Returns an object initialized from data in a given unarchiver.
/// - Parameter coder: An unarchiver object.
public required init?(coder: NSCoder) {
super.init(coder: coder)
}
public override func awakeFromNib() {
// Prepares the receiver for service after it has been loaded
// from an Interface Builder archive, or nib file.
}
/// It will be displayed on the screen with the text.
/// - Parameter text: The text to prompt the user.
public func show(_ text: String) {
self.configure(text)
self.loadView()
self.beginAnimating()
}
/// Configures properties for the widget used.
private func configure(_ text: String) {
self.autoresizingMask = UIView.AutoresizingMask(rawValue: UIView.AutoresizingMask.flexibleLeftMargin.rawValue |
UIView.AutoresizingMask.flexibleTopMargin.rawValue |
UIView.AutoresizingMask.flexibleWidth.rawValue |
UIView.AutoresizingMask.flexibleHeight.rawValue
)
self.maskPanel.autoresizingMask = UIView.AutoresizingMask(rawValue: UIView.AutoresizingMask.flexibleLeftMargin.rawValue |
UIView.AutoresizingMask.flexibleTopMargin.rawValue |
UIView.AutoresizingMask.flexibleWidth.rawValue |
UIView.AutoresizingMask.flexibleHeight.rawValue
)
let cw = 200.0
self.contentView.frame = CGRect(x: 0, y: 0, width: cw, height: 0.6*cw)
self.contentView.setCorner(radius: 10.0)
let offset = 10.0
let iw = 60.0
let ix = cw/2 - iw/2
let iy = 1.5*offset
self.indicator.frame = CGRect(x: ix, y: iy, width: iw, height: iw)
self.indicator.lineWidth = 2.0
let lh = 20.0
self.textLabel.center = CGPoint(x: cw/2, y: 0.6*cw - lh/2 - 1.5*offset)
self.textLabel.bounds = CGRect(x: 0, y: 0, width: cw - 2*offset, height: lh)
self.textLabel.text = text
self.textLabel.font = UIFont.boldSystemFont(ofSize: 16.0)
self.textLabel.textAlignment = NSTextAlignment.center
self.textLabel.numberOfLines = 1
}
/// Addds the subviews to its corresponding superview.
private func loadView() {
if let vc = self.appCurrentViewController() {
vc.view.addSubview(self)
vc.view.bringSubviewToFront(self)
} else {
let window = self.appWindow()
window.addSubview(self)
window.bringSubviewToFront(self)
}
self.addSubview(self.maskPanel)
self.addSubview(self.contentView)
self.bringSubviewToFront(self.contentView)
self.contentView.addSubview(self.indicator)
self.contentView.addSubview(self.textLabel)
}
/// Prepares to begin animating.
private func beginAnimating() {
self.indicator.startAnimating()
self.alpha = 0.0
UIView.animate(withDuration: 0.3) {
self.alpha = 1.0
}
}
/// Hides from its own superview.
public func hide() {
let opts = UIView.AnimationOptions.curveEaseInOut
UIView.animate(withDuration: 0.3, delay: 1.0, options: opts, animations: {
self.alpha = 0.0
}) { (finished) in
self.indicator.stopAnimating()
self.removeAllViews()
}
}
/// Removes all views at the end of the hidden animation.
private func removeAllViews() {
for view in self.subviews {
view.removeFromSuperview()
}
self.removeFromSuperview()
}
/// Finds out the current view controller.
private func appCurrentViewController() -> UIViewController? {
guard var vc = self.appWindow().rootViewController else {
return nil
}
while true {
if let tvc = vc.presentedViewController {
vc = tvc
} else if vc.isKind(of: UITabBarController.self) {
let tbc = vc as! UITabBarController
if let tvc = tbc.selectedViewController {
vc = tvc
}
} else if vc.isKind(of: UINavigationController.self) {
let nc = vc as! UINavigationController
if let tvc = nc.visibleViewController {
vc = tvc
}
} else {
if vc.children.count > 0 {
if let tvc = vc.children.last {
vc = tvc
}
}
break
}
}
return vc
}
public override func layoutSubviews() {
var self_w: CGFloat = 0.0
var self_h: CGFloat = 0.0
if let supv = self.superview {
self_w = supv.bounds.size.width
self_h = supv.bounds.size.height
} else {
self_w = SCREEN_W
self_h = SCREEN_H
}
self.frame = CGRect(x: 0, y: 0, width: self_w, height: self_h)
self.maskPanel.frame = CGRect(x: 0, y: 0, width: self_w, height: self_h)
self.contentView.center = CGPoint(x: self_w/2, y: self_h/2)
}
deinit {
#if DEBUG
print("[\((#file as NSString).lastPathComponent):\(#function)]")
#endif
}
}

View File

@ -0,0 +1,420 @@
//
// ZStoreManager.swift
//
//
// Created by zhl on 2021/7/22.
// Copyright © 2021 egret. All rights reserved.
//
import Foundation
import CommonCrypto
/// Custom method to calculate the SHA-256 hash using Common Crypto.
///
/// - Parameter s: A string to calculate hash.
/// - Returns: A SHA-256 hash value.
public func Z_SHA256_HashValue(_ s: String) -> String? {
let digestLength = Int(CC_SHA256_DIGEST_LENGTH) // 32
let cStr = s.cString(using: String.Encoding.utf8)!
let cStrLen = Int(s.lengthOfBytes(using: String.Encoding.utf8))
// Confirm that the length of C string is small enough
// to be recast when calling the hash function.
if cStrLen > UINT32_MAX {
print("C string too long to hash: \(s)")
return nil
}
let md = UnsafeMutablePointer<CUnsignedChar>.allocate(capacity: digestLength)
CC_SHA256(cStr, CC_LONG(cStrLen), md)
// Convert the array of bytes into a string showing its hex represention.
let hash = NSMutableString()
for i in 0..<digestLength {
// Add a dash every four bytes, for readability.
if i != 0 && i%4 == 0 {
//hash.append("-")
}
hash.appendFormat("%02x", md[i])
}
md.deallocate()
return hash as String
}
open class ZStoreManager: NSObject, DYFStoreReceiptVerifierDelegate {
/// The property contains the purchase information.
private var purchaseInfo: DYFStore.NotificationInfo!
/// The property contains the download information.
private var downloadInfo: DYFStore.NotificationInfo!
/// Creates and returns a receipt verifier by using lazy loading.
private lazy var receiptVerifier: DYFStoreReceiptVerifier = {
let verifier = DYFStoreReceiptVerifier()
verifier.delegate = self
return verifier
}()
/// Returns a store manager singleton.
public static let shared = ZStoreManager()
/// A struct named "Static".
// private struct Static {
// static var instance: ZStoreManager? = nil
// }
/// Returns a store manager singleton.
// public class var shared: ZStoreManager {
//
// objc_sync_enter(self)
// defer { objc_sync_exit(self) }
//
// guard let instance = Static.instance else {
// let storeManager = ZStoreManager()
// Static.instance = storeManager
// return storeManager
// }
//
// return instance
// }
/// Overrides default constructor.
public override init() {
super.init()
self.addStoreObserver()
}
/// deinit
deinit {
self.removeStoreObserver()
}
/// Make sure the class has only one instance.
open override func copy() -> Any {
return self
}
/// Make sure the class has only one instance.
open override func mutableCopy() -> Any {
return self
}
/// Requests payment of the product with the given product identifier, an opaque identifier for the users account on your system.
///
/// - Parameters:
/// - productIdentifier: A given product identifier.
/// - userIdentifier: An opaque identifier for the users account on your system.
public func addPayment(_ productIdentifier: String?, userIdentifier: String? = nil) {
self.showLoading("Waiting...") // Initiate purchase request.
DYFStore.default.purchaseProduct(productIdentifier, userIdentifier: userIdentifier)
}
/// Requests to restore previously completed purchases with an opaque identifier for the users account on your system.
///
/// - Parameter userIdentifier: An opaque identifier for the users account on your system.
public func restorePurchases(_ userIdentifier: String? = nil) {
DYFStoreLog("userIdentifier: \(userIdentifier ?? "")")
self.showLoading("Restoring...")
DYFStore.default.restoreTransactions(userIdentifier: userIdentifier)
}
private func addStoreObserver() {
NotificationCenter.default.addObserver(self, selector: #selector(ZStoreManager.processPurchaseNotification(_:)), name: DYFStore.purchasedNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(ZStoreManager.processDownloadNotification(_:)), name: DYFStore.downloadedNotification, object: nil)
}
private func removeStoreObserver() {
NotificationCenter.default.removeObserver(self, name: DYFStore.purchasedNotification, object: nil)
NotificationCenter.default.removeObserver(self, name: DYFStore.downloadedNotification, object: nil)
}
@objc private func processPurchaseNotification(_ notification: Notification) {
self.hideLoading()
self.purchaseInfo = (notification.object as! DYFStore.NotificationInfo)
switch self.purchaseInfo.state! {
case .purchasing:
self.showLoading("Purchasing...")
break
case .cancelled:
self.sendNotice("You cancel the purchase")
break
case .failed:
self.sendNotice(String(format: "An error occurred, \(self.purchaseInfo.error!.code)"))
break
case .succeeded, .restored:
self.completePayment()
break
case .restoreFailed:
self.sendNotice(String(format: "An error occurred, \(self.purchaseInfo.error!.code)"))
break
case .deferred:
DYFStoreLog("Deferred")
break
}
}
@objc private func processDownloadNotification(_ notification: Notification) {
self.downloadInfo = (notification.object as! DYFStore.NotificationInfo)
switch self.downloadInfo.downloadState! {
case .started:
DYFStoreLog("The download started")
break
case .inProgress:
DYFStoreLog("The download progress: \(self.downloadInfo.downloadProgress)%%")
break
case .cancelled:
DYFStoreLog("The download cancelled")
break
case .failed:
DYFStoreLog("The download failed")
break
case .succeeded:
DYFStoreLog("The download succeeded: 100%%")
break
}
}
private func completePayment() {
let info = self.purchaseInfo!
let persister = DYFStoreUserDefaultsPersistence()
let identifier = info.transactionIdentifier!
if !persister.containsTransaction(identifier) {
self.storeReceipt()
return
}
let transaction = persister.retrieveTransaction(identifier)
if let tx = transaction {
DYFStoreLog("transaction.state: \(tx.state)")
DYFStoreLog("transaction.productIdentifier: \(tx.productIdentifier!)")
DYFStoreLog("transaction.userIdentifier: \(tx.userIdentifier ?? "null")")
DYFStoreLog("transaction.transactionIdentifier: \(tx.transactionIdentifier!)")
DYFStoreLog("transaction.transactionTimestamp: \(tx.transactionTimestamp!)")
DYFStoreLog("transaction.originalTransactionIdentifier: \(tx.originalTransactionIdentifier ?? "null")")
DYFStoreLog("transaction.originalTransactionTimestamp: \(tx.originalTransactionTimestamp ?? "null")")
if let receiptData = tx.transactionReceipt!.base64DecodedData() {
DYFStoreLog("transaction.transactionReceipt: \(receiptData)")
self.verifyReceipt(receiptData)
}
}
}
private func storeReceipt() {
DYFStoreLog()
guard let url = DYFStore.receiptURL() else {
self.refreshReceipt()
return
}
do {
let data = try Data(contentsOf: url)
let info = self.purchaseInfo!
let persister = DYFStoreUserDefaultsPersistence()
let transaction = DYFStoreTransaction()
if info.state! == .succeeded {
transaction.state = DYFStoreTransactionState.purchased.rawValue
} else if info.state! == .restored {
transaction.state = DYFStoreTransactionState.restored.rawValue
}
transaction.productIdentifier = info.productIdentifier
transaction.userIdentifier = info.userIdentifier
transaction.transactionTimestamp = info.transactionDate?.timestamp()
transaction.transactionIdentifier = info.transactionIdentifier
transaction.originalTransactionTimestamp = info.originalTransactionDate?.timestamp()
transaction.originalTransactionIdentifier = info.originalTransactionIdentifier
transaction.transactionReceipt = data.base64EncodedString()
persister.storeTransaction(transaction)
self.verifyReceipt(data)
} catch let error {
DYFStoreLog("error: \(error.localizedDescription)")
self.refreshReceipt()
return
}
}
private func refreshReceipt() {
DYFStoreLog()
self.showLoading("Refresh receipt...")
DYFStore.default.refreshReceipt(onSuccess: {
self.storeReceipt()
}) { (error) in
self.failToRefreshReceipt()
}
}
private func failToRefreshReceipt() {
DYFStoreLog()
self.hideLoading()
self.showAlert(withTitle: NSLocalizedString("Notification", tableName: nil, comment: ""),
message: "Fail to refresh receipt! Please check if your device can access the internet.",
cancelButtonTitle: "Cancel",
cancel: { (cancelAction) in },
confirmButtonTitle: NSLocalizedString("Retry", tableName: nil, comment: ""))
{ (action) in
self.refreshReceipt()
}
}
// It is better to use your own server to obtain the parameters uploaded from the client to verify the receipt from the app store server (C -> Uploaded Parameters -> S -> App Store S -> S -> Receive And Parse Data -> C).
// If the receipts are verified by your own server, the client needs to upload these parameters, such as: "transaction identifier, bundle identifier, product identifier, user identifier, shared sceret(Subscription), receipt(Safe URL Base64), original transaction identifier(Optional), original transaction time(Optional) and the device information, etc.".
private func verifyReceipt(_ receiptData: Data?) {
DYFStoreLog()
self.hideLoading()
self.showLoading("Verify receipt...")
var data: Data!
if let tempData = receiptData {
data = tempData
} else {
if let url = DYFStore.receiptURL() {
do {
data = try Data(contentsOf: url)
} catch let error {
DYFStoreLog("error: \(error.localizedDescription)")
self.failToRefreshReceipt()
return
}
}
}
DYFStoreLog("data: \(data!)")
self.receiptVerifier.verifyReceipt(data)
// Only used for receipts that contain auto-renewable subscriptions.
//self.receiptVerifier.verifyReceipt(data, sharedSecret: "A43512564ACBEF687924646CAFEFBDCAEDF4155125657")
}
private func retryToVerifyReceipt() {
let info = self.purchaseInfo!
let persister = DYFStoreUserDefaultsPersistence()
let identifier = info.transactionIdentifier!
let transaction = persister.retrieveTransaction(identifier)
if let tx = transaction, let receiptData = tx.transactionReceipt!.base64DecodedData() {
self.verifyReceipt(receiptData)
}
}
private func sendNotice(_ message: String) {
self.showAlert(withTitle: NSLocalizedString("Notification", tableName: nil, comment: ""),
message: message,
cancelButtonTitle: nil,
cancel: nil,
confirmButtonTitle: NSLocalizedString("I see!", tableName: nil, comment: ""))
{ (action) in
DYFStoreLog("alert action title: \(action.title!)")
}
}
// MARK: - DYFStoreReceiptVerifierDelegate
public func verifyReceiptDidFinish(_ verifier: DYFStoreReceiptVerifier, didReceiveData data: [String : Any]) {
DYFStoreLog("data: \(data)")
self.hideLoading()
self.showTipsMessage("Purchase Successfully")
DispatchQueue.main.asyncAfter(delay: 1.5) {
let info = self.purchaseInfo!
let store = DYFStore.default
let persister = DYFStoreUserDefaultsPersistence()
let identifier = info.transactionIdentifier!
if info.state! == .restored {
let transaction = store.extractRestoredTransaction(identifier)
store.finishTransaction(transaction)
} else {
let transaction = store.extractPurchasedTransaction(identifier)
// The transaction can be finished only after the client and server adopt secure communication and data encryption and the receipt verification is passed. In this way, we can avoid refreshing orders and cracking in-app purchase. If we were unable to complete the verification, we want `StoreKit` to keep reminding us that there are still outstanding transactions.
store.finishTransaction(transaction)
}
persister.removeTransaction(identifier)
if let id = info.originalTransactionIdentifier {
persister.removeTransaction(id)
}
}
}
public func verifyReceipt(_ verifier: DYFStoreReceiptVerifier, didFailWithError error: NSError) {
// Prints the reason of the error.
DYFStoreLog("error: \(error.code), \(error.localizedDescription)")
self.hideLoading()
// An error occurs that has nothing to do with in-app purchase. Maybe it's the internet.
if error.code < 21000 {
// After several attempts, you can cancel refreshing receipt.
self.showAlert(withTitle: NSLocalizedString("Notification", tableName: nil, comment: ""),
message: "Fail to verify receipt! Please check if your device can access the internet.",
cancelButtonTitle: "Cancel",
cancel: nil,
confirmButtonTitle: NSLocalizedString("Retry", tableName: nil, comment: ""))
{ (action) in
DYFStoreLog("alert action title: \(action.title!)")
self.verifyReceipt(nil)
}
return
}
self.showTipsMessage("Fail to purchase product!")
DispatchQueue.main.asyncAfter(delay: 1.5) {
let info = self.purchaseInfo!
let store = DYFStore.default
let persister = DYFStoreUserDefaultsPersistence()
let identifier = info.transactionIdentifier!
if info.state! == .restored {
let transaction = store.extractRestoredTransaction(identifier)
store.finishTransaction(transaction)
} else {
let transaction = store.extractPurchasedTransaction(identifier)
// The transaction can be finished only after the client and server adopt secure communication and data encryption and the receipt verification is passed. In this way, we can avoid refreshing orders and cracking in-app purchase. If we were unable to complete the verification, we want `StoreKit` to keep reminding us that there are still outstanding transactions.
store.finishTransaction(transaction)
}
persister.removeTransaction(identifier)
if let id = info.originalTransactionIdentifier {
persister.removeTransaction(id)
}
}
}
}