diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b6ac41..d0930b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,24 @@ +## v0.3.8 +- fix + - 隐私文件增加divice id声明 + +## v0.3.7 +- feature + - 增加事件 + - guru_sdk_init_start + - guru_sdk_init_complete + - session_start + - guru_engagement + - 增加事件参数 + - session_number + - session_id + - deviceInfo.sdkVersion + - info.vendorId + +## v0.3.6 +- fix: + - 增加第三方依赖库版本约束 + ## v0.3.5 - 接口更新: - 日志打包方法eventsLogsArchive废弃,使用eventsLogsDirectory获取文件夹URL diff --git a/Example/GuruAnalytics.xcodeproj/project.pbxproj b/Example/GuruAnalytics.xcodeproj/project.pbxproj index 3338c7a..ce1e7e9 100644 --- a/Example/GuruAnalytics.xcodeproj/project.pbxproj +++ b/Example/GuruAnalytics.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -128,7 +128,7 @@ 607FACCC1AFB9204008FA782 /* Sources */, 607FACCD1AFB9204008FA782 /* Frameworks */, 607FACCE1AFB9204008FA782 /* Resources */, - 86E7176C5034831947DC0310 /* [CP] Embed Pods Frameworks */, + 65438729BAC744B5EACA2945 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -210,7 +210,7 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 86E7176C5034831947DC0310 /* [CP] Embed Pods Frameworks */ = { + 65438729BAC744B5EACA2945 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -368,7 +368,7 @@ baseConfigurationReference = D30C5441D6D5CBD750E76657 /* Pods-GuruAnalytics_Example.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - DEVELOPMENT_TEAM = 43U6TB4QK3; + DEVELOPMENT_TEAM = 69MW7VVKA9; INFOPLIST_FILE = GuruAnalytics/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = ( @@ -388,7 +388,7 @@ baseConfigurationReference = F992AC1E7C4013032773C33F /* Pods-GuruAnalytics_Example.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - DEVELOPMENT_TEAM = 43U6TB4QK3; + DEVELOPMENT_TEAM = 69MW7VVKA9; INFOPLIST_FILE = GuruAnalytics/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = ( diff --git a/Example/GuruAnalytics/ViewController.swift b/Example/GuruAnalytics/ViewController.swift index 5921954..c6d3892 100644 --- a/Example/GuruAnalytics/ViewController.swift +++ b/Example/GuruAnalytics/ViewController.swift @@ -39,7 +39,7 @@ class ViewController: UIViewController { } @IBAction func getLogs(_ sender: UIButton) { - GuruAnalytics.eventsLogsArchive({ [weak self] url in + GuruAnalytics.eventsLogsDirectory({ [weak self] url in guard let `self` = self, let url = url else { return } if MFMailComposeViewController.canSendMail() { diff --git a/Example/Podfile b/Example/Podfile index d3be955..eb06f26 100644 --- a/Example/Podfile +++ b/Example/Podfile @@ -7,7 +7,7 @@ platform :ios, '11.0' target 'GuruAnalytics_Example' do pod 'GuruAnalyticsLib', :path => '../' -# pod 'GuruAnalyticsLib', '0.3.4' +# pod 'GuruAnalyticsLib', '0.3.8' end post_install do |installer| diff --git a/GuruAnalytics/Assets/PrivacyInfo.xcprivacy b/GuruAnalytics/Assets/PrivacyInfo.xcprivacy index 458bfd4..11d3be4 100644 --- a/GuruAnalytics/Assets/PrivacyInfo.xcprivacy +++ b/GuruAnalytics/Assets/PrivacyInfo.xcprivacy @@ -2,6 +2,21 @@ + NSPrivacyCollectedDataTypes + + + NSPrivacyCollectedDataType + NSPrivacyCollectedDataTypeDeviceID + NSPrivacyCollectedDataTypeLinked + + NSPrivacyCollectedDataTypeTracking + + NSPrivacyCollectedDataTypePurposes + + NSPrivacyCollectedDataTypePurposeAnalytics + + + NSPrivacyAccessedAPITypes @@ -34,8 +49,6 @@ NSPrivacyTracking NSPrivacyTrackingDomains - - - + diff --git a/GuruAnalytics/Classes/Internal/DataModel/Models.swift b/GuruAnalytics/Classes/Internal/DataModel/Models.swift index e97322f..805b660 100644 --- a/GuruAnalytics/Classes/Internal/DataModel/Models.swift +++ b/GuruAnalytics/Classes/Internal/DataModel/Models.swift @@ -124,8 +124,16 @@ extension Entity { static func normalizeParameters(_ parameters: [String : Any]) -> [String : EventValue] { var params = [String : EventValue]() + var allParams = parameters; + + GuruAnalytics.BuiltinParametersKeys.allCases.forEach { paramKey in + if let value = allParams.removeValue(forKey: paramKey.rawValue) { + params[paramKey.rawValue] = normalizeValue(value); + } + } + var count = 0 - parameters.sorted(by: { $0.key < $1.key }).forEach({ key, value in + allParams.sorted(by: { $0.key < $1.key }).forEach({ key, value in guard count < maxParametersCount else { cdPrint("too many parameters") @@ -140,17 +148,7 @@ extension Entity { return } - if let value = value as? String { - params[normalizedKey] = Entity.EventValue(stringValue: String(value.prefix(maxParameterStringValueLength))) - } else if let value = value as? Int { - params[normalizedKey] = Entity.EventValue(longValue: Int64(value)) - } else if let value = value as? Int64 { - params[normalizedKey] = Entity.EventValue(longValue: value) - } else if let value = value as? Double { - params[normalizedKey] = Entity.EventValue(doubleValue: value) - } else { - params[normalizedKey] = Entity.EventValue(stringValue: String("\(value)".prefix(maxParameterStringValueLength))) - } + params[normalizedKey] = normalizeValue(value) count += 1 }) @@ -158,6 +156,22 @@ extension Entity { return params } + static func normalizeValue(_ value: Any) -> EventValue { + let eventValue: EventValue + if let value = value as? String { + eventValue = Entity.EventValue(stringValue: String(value.prefix(maxParameterStringValueLength))) + } else if let value = value as? Int { + eventValue = Entity.EventValue(longValue: Int64(value)) + } else if let value = value as? Int64 { + eventValue = Entity.EventValue(longValue: value) + } else if let value = value as? Double { + eventValue = Entity.EventValue(doubleValue: value) + } else { + eventValue = Entity.EventValue(stringValue: String("\(value)".prefix(maxParameterStringValueLength))) + } + return eventValue + } + static func normalizeKey(_ key: String) -> String? { var mutableKey = key @@ -192,12 +206,16 @@ extension Entity { ///用户的pseudo_id let firebaseId: String? + ///IDFV + let vendorId: String? = UIDevice().identifierForVendor?.uuidString + enum CodingKeys: String, CodingKey { case deviceId case uid case adjustId case adId case firebaseId + case vendorId } } @@ -226,3 +244,23 @@ extension Entity { let data: Int64 } } + +extension Entity { + struct Session { + let id: UUID = UUID() + + var sessionId: Int { + return id.uuidString.hash + } + } + + struct SessionNumber: Codable { + var number: Int + let createdAtMs: Int64 + + static func createNumber() -> SessionNumber { + return SessionNumber(number: 0, createdAtMs: Date().msSince1970) + } + } +} + diff --git a/GuruAnalytics/Classes/Internal/Database/Database.swift b/GuruAnalytics/Classes/Internal/Database/Database.swift index 0ef7c10..41006b9 100644 --- a/GuruAnalytics/Classes/Internal/Database/Database.swift +++ b/GuruAnalytics/Classes/Internal/Database/Database.swift @@ -176,6 +176,40 @@ WHERE \(Entity.EventRecord.CodingKeys.timestamp.rawValue) < \(earlierThan) }) } + func fetchOutdatedEventRecords(earlierThan: Int64) -> Single<[Entity.EventRecord]> { + return mapTransactionToSingle { (db) in + let querySQL: String = +""" +SELECT * FROM \(TableName.event.rawValue) +WHERE \(Entity.EventRecord.CodingKeys.timestamp.rawValue) < \(earlierThan) +""" + cdPrint(#function + "query sql: \(querySQL)") + let results = try db.executeQuery(querySQL, values: nil) //[ASC | DESC] + var t: [Entity.EventRecord] = [] + while results.next() { + guard let recordId = results.string(forColumnIndex: 0), + let eventName = results.string(forColumnIndex: 1), + let eventJson = results.string(forColumnIndex: 2) else { + continue + } + + let priority: Int = results.columnIsNull(Entity.EventRecord.CodingKeys.priority.rawValue) ? + Entity.EventRecord.Priority.DEFAULT.rawValue : Int(results.int(forColumn: Entity.EventRecord.CodingKeys.priority.rawValue)) + + let ts: Int = results.columnIsNull(Entity.EventRecord.CodingKeys.transitionStatus.rawValue) ? + Entity.EventRecord.TransitionStatus.idle.rawValue : Int(results.int(forColumn: Entity.EventRecord.CodingKeys.transitionStatus.rawValue)) + + let record = Entity.EventRecord(recordId: recordId, eventName: eventName, eventJson: eventJson, + timestamp: results.longLongInt(forColumn: Entity.EventRecord.CodingKeys.timestamp.rawValue), + priority: priority, transitionStatus: ts) + t.append(record) + } + + results.close() + return t + } + } + func resetTransitionStatus(for recordIds: [String]) -> Single { guard !recordIds.isEmpty else { return .just(()) diff --git a/GuruAnalytics/Classes/Internal/Database/Manager.swift b/GuruAnalytics/Classes/Internal/Database/Manager.swift index ef35122..e16f3da 100644 --- a/GuruAnalytics/Classes/Internal/Database/Manager.swift +++ b/GuruAnalytics/Classes/Internal/Database/Manager.swift @@ -54,6 +54,7 @@ internal class Manager { // MARK: - private members private typealias PropertyName = GuruAnalytics.PropertyName + private typealias BuiltinParametersKeys = GuruAnalytics.BuiltinParametersKeys private let bag = DisposeBag() @@ -170,8 +171,23 @@ internal class Manager { private typealias InternalEventReporter = ((_ eventCode: Int, _ info: String) -> Void) private var internalEventReporter: InternalEventReporter? + private lazy var session = Entity.Session() + private lazy var sessionNumber: Entity.SessionNumber = { + var sessionNumber = UserDefaults.sessionNumber + let dayGaps = Calendar.current.dateComponents([.day], from: Date(timeIntervalSince1970: Double(sessionNumber.createdAtMs) / 1000), to: Date()).day ?? 0 + if (dayGaps > 0) { + sessionNumber = Entity.SessionNumber.createNumber() + } + sessionNumber.number += 1 + UserDefaults.sessionNumber = sessionNumber + return sessionNumber + }() + private init() { + // + logSDKInitStart() + // first open logFirstOpenIfNeeded() @@ -188,6 +204,12 @@ internal class Manager { logFirstFgEvent() ntwkMgr.networkErrorReporter = self + + // + logSDKInitComplete() + + // + logSessionStart() } } @@ -229,7 +251,7 @@ internal extension Manager { priority: Entity.EventRecord.Priority) -> Single { return userProperty.take(1).observe(on: rxWorkScheduler).asSingle().flatMap { p in - .create { subscriber in + .create { [weak self] subscriber in do { debugPrint("userProperty thread queueName: \(Thread.current.queueName) count: \(p.count)") var userProperty = p @@ -240,6 +262,9 @@ internal extension Manager { eventParam[PropertyName.screen.rawValue] = screen } + eventParam[BuiltinParametersKeys.sessionId.rawValue] = self?.session.sessionId + eventParam[BuiltinParametersKeys.sessionNo.rawValue] = self?.sessionNumber.number + let userInfo = Entity.UserInfo( uid: userProperty.removeValue(forKey: PropertyName.uid.rawValue), deviceId: userProperty.removeValue(forKey: PropertyName.deviceId.rawValue), @@ -361,19 +386,33 @@ private extension Manager { /// 1. 删除过期的数据 serverNowMsSingle - .flatMap({ [weak self] serverNowMs -> Single in - guard let `self` = self else { return .just(()) } - let earlierThan: Int64 = serverNowMs - self.eventExpiredIntervel.int64Ms - return self.db.removeOutdatedEventRecords(earlierThan: earlierThan) - }) - .catch({ error in - cdPrint("remove outdated records error: \(error)") - return .just(()) - }) - .subscribe(onSuccess: { [weak self] _ in - self?.outdatedEventsCleared.onNext(true) - }) - .disposed(by: bag) + .flatMap({ [weak self] serverNowMs -> Single<[Entity.EventRecord]> in + guard let `self` = self else { + return .error(NSError(domain: "com.guru.analytics.manager", + code: 0, + userInfo: [NSLocalizedDescriptionKey : "Manager released"] + )) + } + let earlierThan: Int64 = serverNowMs - self.eventExpiredIntervel.int64Ms + return self.db.fetchOutdatedEventRecords(earlierThan: earlierThan) + }) + .flatMap({ [weak self] records in + return self?.db.deleteEventRecords(records.map { $0.recordId }) + .map { _ in records.count } ?? + .error(NSError(domain: "com.guru.analytics.manager", + code: 0, + userInfo: [NSLocalizedDescriptionKey : "Manager released"] + )) + }) + .catch({ error in + cdPrint("remove outdated records error: \(error)") + return .just(0) + }) + .subscribe(onSuccess: { [weak self] deletedCount in + UserDefaults.deletedEventsCount += deletedCount + self?.outdatedEventsCleared.onNext(true) + }) + .disposed(by: bag) } func logFirstOpenIfNeeded() { @@ -417,6 +456,7 @@ private extension Manager { .flatMap { self.db.addEventRecords($0) } .do(onSuccess: { _ in self.accumulateLoggedEventsCount(1) + UserDefaults.totalEventsCount += 1 self.eventsLogger.verbose("log event success") }, onError: { error in self.eventsLogger.error("log event error: \(error)") @@ -424,6 +464,28 @@ private extension Manager { }() } + func logSDKInitStart() { + _logEvent(GuruAnalytics.sdkInitStartEvent.name, parameters: [ + GuruAnalytics.sdkInitStartEvent.paramKeyType.totalEvents.rawValue : UserDefaults.totalEventsCount, + GuruAnalytics.sdkInitStartEvent.paramKeyType.deletedEvents.rawValue : UserDefaults.deletedEventsCount, + GuruAnalytics.sdkInitStartEvent.paramKeyType.uploadedEvents.rawValue : UserDefaults.uploadedEventsCount, + ], priority: .HIGH) + .subscribe() + .disposed(by: bag) + } + + func logSDKInitComplete() { + _logEvent(GuruAnalytics.sdkInitCompleteEvent.name, parameters: [ + GuruAnalytics.sdkInitCompleteEvent.paramKeyType.duration.rawValue : Date().msSince1970 - startAt.msSince1970, + ], priority: .HIGH) + .subscribe() + .disposed(by: bag) + } + func logSessionStart() { + _logEvent(GuruAnalytics.sessionStartEvent.name, parameters: nil, priority: .HIGH) + .subscribe() + .disposed(by: bag) + } } // MARK: - 轮询上传相关 @@ -536,6 +598,7 @@ private extension Manager { } .flatMap { recordIDs -> Single in self.accumulateUploadedEventsCount(recordIDs.count) + UserDefaults.uploadedEventsCount += recordIDs.count /// step4: 删除数据库中对应记录 return self.db.deleteEventRecords(recordIDs) .catch { error in diff --git a/GuruAnalytics/Classes/Internal/Database/UserDefaults.swift b/GuruAnalytics/Classes/Internal/Database/UserDefaults.swift index dd9fb51..3fa4c7c 100644 --- a/GuruAnalytics/Classes/Internal/Database/UserDefaults.swift +++ b/GuruAnalytics/Classes/Internal/Database/UserDefaults.swift @@ -39,6 +39,48 @@ internal enum UserDefaults { } } + static var totalEventsCount: Int { + get { + return defaults?.value(forKey: totalEventsCountKey) as? Int ?? 0 + } + + set { + defaults?.set(newValue, forKey: totalEventsCountKey) + } + } + + static var deletedEventsCount: Int { + get { + return defaults?.value(forKey: deletedEventsCountKey) as? Int ?? 0 + } + + set { + defaults?.set(newValue, forKey: deletedEventsCountKey) + } + } + + static var uploadedEventsCount: Int { + get { + return defaults?.value(forKey: uploadedEventsCountKey) as? Int ?? 0 + } + + set { + defaults?.set(newValue, forKey: uploadedEventsCountKey) + } + } + + static var sessionNumber: Entity.SessionNumber { + get { + let jsonString = defaults?.value(forKey: sessionNumberKey) as? String ?? "" + let sessionNumber = JSONDecoder().decodeObject(Entity.SessionNumber.self, from: jsonString) + ?? Entity.SessionNumber.createNumber() + return sessionNumber + } + set { + let jsonString = newValue.asString + defaults?.setValue(jsonString, forKey: sessionNumberKey) + } + } } extension UserDefaults { @@ -62,4 +104,21 @@ extension UserDefaults { static var fgDurationKey: String { return "fg.duration.ms" } + + static var totalEventsCountKey: String { + return "events.recorded.total.count" + } + + static var deletedEventsCountKey: String { + return "events.deleted.count" + } + + static var uploadedEventsCountKey: String { + return "events.uploaded.count" + } + + static var sessionNumberKey: String { + return "session.number" + } + } diff --git a/GuruAnalytics/Classes/Internal/GuruAnalytics+Internal.swift b/GuruAnalytics/Classes/Internal/GuruAnalytics+Internal.swift index f62e47e..598cf3c 100644 --- a/GuruAnalytics/Classes/Internal/GuruAnalytics+Internal.swift +++ b/GuruAnalytics/Classes/Internal/GuruAnalytics+Internal.swift @@ -22,7 +22,7 @@ internal extension GuruAnalytics { ///built-in events static let fgEvent: EventProto = { - var e = EventProto(paramKeyType: FgEventParametersKeys.self, name: "fg") + var e = EventProto(paramKeyType: FgEventParametersKeys.self, name: "guru_engagement") return e }() @@ -31,6 +31,21 @@ internal extension GuruAnalytics { return e }() + static let sdkInitStartEvent: EventProto = { + var e = EventProto(paramKeyType: SDKEventParametersKeys.self, name: "guru_sdk_init_start") + return e + }() + + static let sdkInitCompleteEvent: EventProto = { + var e = EventProto(paramKeyType: SDKEventParametersKeys.self, name: "guru_sdk_init_complete") + return e + }() + + static let sessionStartEvent: EventProto = { + var e = EventProto(paramKeyType: DefaultEventParametersKeys.self, name: "session_start") + return e + }() + class func setUserProperty(_ value: String?, forName name: PropertyName) { setUserProperty(value, forName: name.rawValue) } @@ -48,4 +63,23 @@ internal extension GuruAnalytics { case duration } + enum SDKEventParametersKeys: String { + case totalEvents = "total_events" + case deletedEvents = "deleted_events" + case uploadedEvents = "uploaded_events" + case duration + } + + enum DefaultEventParametersKeys { + } +} + +internal extension GuruAnalytics { + + ///built-in event parameters + enum BuiltinParametersKeys: String, CaseIterable { + case screenName = "screen_name" + case sessionNo = "session_number" + case sessionId = "session_id" + } } diff --git a/GuruAnalytics/Classes/Internal/Utility/Constants.swift b/GuruAnalytics/Classes/Internal/Utility/Constants.swift index c4d8c78..84152a0 100755 --- a/GuruAnalytics/Classes/Internal/Utility/Constants.swift +++ b/GuruAnalytics/Classes/Internal/Utility/Constants.swift @@ -26,6 +26,14 @@ internal struct Constants { return shortVersion }() + private static let sdkVersion: String = { + guard let infoDict = Bundle(for: Manager.self).infoDictionary, + let currentVersion = infoDict["CFBundleShortVersionString"] as? String else { + return "" + } + return currentVersion + }() + private static let preferredLocale: Locale = { guard let preferredIdentifier = Locale.preferredLanguages.first else { return Locale.current @@ -83,7 +91,8 @@ internal struct Constants { "screenH": Int(screenSize.h), "screenW": Int(screenSize.w), "osVersion": systemVersion, - "language" : languageCode + "language" : languageCode, + "sdkVersion" : sdkVersion, ] } diff --git a/GuruAnalytics/Classes/Internal/Utility/JSONDecoder.Extension.swift b/GuruAnalytics/Classes/Internal/Utility/JSONDecoder.Extension.swift index 13388c3..4e63ac8 100644 --- a/GuruAnalytics/Classes/Internal/Utility/JSONDecoder.Extension.swift +++ b/GuruAnalytics/Classes/Internal/Utility/JSONDecoder.Extension.swift @@ -25,4 +25,38 @@ internal extension JSONDecoder { } return try decode(type, from: unwrappedData) } + + private func decodeObject(_ type: T.Type, from data: Data) -> T? where T: Decodable { + + guard data.count > 0 else { return nil } + + var object: T? = nil + + do { + object = try decode(type, from: data) + } catch { + cdPrint("JSONDecoder decode error: \(error)") + } + + return object + } + + func decodeObject(_ type: T.Type, from jsonString: String) -> T? where T: Decodable { + guard let jsonData = jsonString.data(using: .utf8) else { return nil } + return decodeObject(type, from: jsonData) + } + + func decodeObject(_ type: T.Type, from dictionary: [String : Any]) -> T? where T: Decodable { + + var data: Data? + + do { + data = try JSONSerialization.data(withJSONObject: dictionary, options: .prettyPrinted) + } catch let error { + cdPrint(error) + } + + guard let jsonData = data else { return nil } + return decodeObject(type, from: jsonData) + } } diff --git a/GuruAnalyticsLib.podspec b/GuruAnalyticsLib.podspec index 794f097..d1c3c63 100644 --- a/GuruAnalyticsLib.podspec +++ b/GuruAnalyticsLib.podspec @@ -8,7 +8,7 @@ Pod::Spec.new do |s| s.name = 'GuruAnalyticsLib' - s.version = '0.3.5' + s.version = '0.3.8' s.summary = 'A short description of GuruAnalytics.' # This description is used to generate tags and improve search results. @@ -25,7 +25,7 @@ TODO: Add long description of the pod here. # s.screenshots = 'www.example.com/screenshots_1', 'www.example.com/screenshots_2' s.license = { :type => 'MIT', :file => 'LICENSE' } s.author = { 'devSC' => 'xiaochong2154@163.com' } - s.source = { :git => 'git@git.chengdu.pundit.company:castbox/GuruAnalytics_iOS.git', :tag => s.version.to_s } + s.source = { :git => 'git@github.com:castbox/GuruAnalytics_iOS.git', :tag => s.version.to_s } # s.social_media_url = 'https://twitter.com/' s.ios.deployment_target = '11.0' @@ -38,12 +38,12 @@ TODO: Add long description of the pod here. # s.public_header_files = 'Pod/Classes/**/*.h' # s.frameworks = 'UIKit', 'MapKit' # s.dependency 'AFNetworking', '~> 2.3' - s.dependency 'RxCocoa', '~> 6' - s.dependency 'Alamofire', '~> 5.0' - s.dependency 'FMDB' - s.dependency 'GzipSwift' - s.dependency 'CryptoSwift' - s.dependency 'SwiftyBeaver' + s.dependency 'RxCocoa', '~> 6.7.0' + s.dependency 'Alamofire', '~> 5.9' + s.dependency 'FMDB', '~> 2.0' + s.dependency 'GzipSwift', '~> 5.0' + s.dependency 'CryptoSwift', '~> 1.0' + s.dependency 'SwiftyBeaver', '~> 1.0' s.subspec 'Privacy' do |ss| ss.resource_bundles = {