From 21659d3154c3d39f45bdcc48ce1e7b0f6cbb94e2 Mon Sep 17 00:00:00 2001 From: iCodesign Date: Tue, 2 Aug 2016 13:53:40 +0800 Subject: [PATCH 1/8] Change Localized to PtoatsoBase --- Potatso.xcodeproj/project.pbxproj | 8 ++++---- {PotatsoLibrary => PotatsoBase}/Localized.swift | 0 PotatsoBase/PotatsoBase.h | 3 ++- 3 files changed, 6 insertions(+), 5 deletions(-) rename {PotatsoLibrary => PotatsoBase}/Localized.swift (100%) diff --git a/Potatso.xcodeproj/project.pbxproj b/Potatso.xcodeproj/project.pbxproj index d583d77..4467ef6 100644 --- a/Potatso.xcodeproj/project.pbxproj +++ b/Potatso.xcodeproj/project.pbxproj @@ -54,7 +54,6 @@ 9B9B15C51C21595B000B6541 /* PotatsoLibrary.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9B9B15BD1C21595B000B6541 /* PotatsoLibrary.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 9BB3F73C1CC3D3DE00C2DD05 /* RecentRequestsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BB3F73B1CC3D3DE00C2DD05 /* RecentRequestsCell.swift */; }; 9BB3F7411CC60B5300C2DD05 /* Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BB3F73E1CC60B5300C2DD05 /* Image.swift */; }; - 9BB3F7421CC60B5300C2DD05 /* Localized.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BB3F73F1CC60B5300C2DD05 /* Localized.swift */; }; 9BB3F7441CC60B6F00C2DD05 /* Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BB3F7431CC60B6F00C2DD05 /* Error.swift */; }; 9BB3F7471CC60CBE00C2DD05 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 9BB3F7491CC60CBE00C2DD05 /* Localizable.strings */; }; 9BB3F74F1CC6308000C2DD05 /* RecentRequestsVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BB3F74E1CC6308000C2DD05 /* RecentRequestsVC.swift */; }; @@ -191,6 +190,7 @@ B8CCC6E51CFDCFD8000E7E2E /* RuleSetCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8CCC6E41CFDCFD8000E7E2E /* RuleSetCell.swift */; }; B8CCC6E71CFDD99E000E7E2E /* ProxyListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8CCC6E61CFDD99E000E7E2E /* ProxyListViewController.swift */; }; B8CCC6EB1CFF1501000E7E2E /* ProxyRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8CCC6EA1CFF1501000E7E2E /* ProxyRow.swift */; }; + B8D8CC0A1D506CB900CE6C0D /* Localized.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D8CC091D506CB900CE6C0D /* Localized.swift */; }; B8EAC5941D40BF310046963C /* TunnelError.m in Sources */ = {isa = PBXBuildFile; fileRef = B8EAC5931D40BF310046963C /* TunnelError.m */; }; B8F7845C1D36760D00F02FF5 /* DashboardVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8F7845B1D36760D00F02FF5 /* DashboardVC.swift */; }; B8F784671D3678B400F02FF5 /* Settings.h in Headers */ = {isa = PBXBuildFile; fileRef = B8F784651D3678B400F02FF5 /* Settings.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -379,7 +379,6 @@ 9B9B15C11C21595B000B6541 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 9BB3F73B1CC3D3DE00C2DD05 /* RecentRequestsCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecentRequestsCell.swift; sourceTree = ""; }; 9BB3F73E1CC60B5300C2DD05 /* Image.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Image.swift; sourceTree = ""; }; - 9BB3F73F1CC60B5300C2DD05 /* Localized.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Localized.swift; sourceTree = ""; }; 9BB3F7431CC60B6F00C2DD05 /* Error.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Error.swift; sourceTree = ""; }; 9BB3F7481CC60CBE00C2DD05 /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = Base; path = Base.lproj/Localizable.strings; sourceTree = ""; }; 9BB3F74A1CC60CBF00C2DD05 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; @@ -871,6 +870,7 @@ B8CCC6E61CFDD99E000E7E2E /* ProxyListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProxyListViewController.swift; sourceTree = ""; }; B8CCC6EA1CFF1501000E7E2E /* ProxyRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ProxyRow.swift; path = ../ProxyRow.swift; sourceTree = ""; }; B8CF2F991D3DF69E009C9FF3 /* Confidential.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Confidential.h; sourceTree = ""; }; + B8D8CC091D506CB900CE6C0D /* Localized.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Localized.swift; sourceTree = ""; }; B8EAC5921D40BF310046963C /* TunnelError.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TunnelError.h; sourceTree = ""; }; B8EAC5931D40BF310046963C /* TunnelError.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TunnelError.m; sourceTree = ""; }; B8F7845B1D36760D00F02FF5 /* DashboardVC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DashboardVC.swift; sourceTree = ""; }; @@ -1202,7 +1202,6 @@ 9BF3034C1CCA20D80096588E /* Logging.swift */, 9BC215C61CB51148002AADD1 /* Config.swift */, 9BB3F73E1CC60B5300C2DD05 /* Image.swift */, - 9BB3F73F1CC60B5300C2DD05 /* Localized.swift */, 9B9B15C11C21595B000B6541 /* Info.plist */, B82574A81D1D98CF007BAF40 /* Pollution.swift */, ); @@ -1227,6 +1226,7 @@ isa = PBXGroup; children = ( 9BC6001F1CB2921C00E5EA61 /* PotatsoBase.h */, + B8D8CC091D506CB900CE6C0D /* Localized.swift */, 9BC600291CB2922E00E5EA61 /* Potatso.h */, 9BC6002A1CB2922E00E5EA61 /* Potatso.m */, B8F784651D3678B400F02FF5 /* Settings.h */, @@ -2894,7 +2894,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 9BB3F7421CC60B5300C2DD05 /* Localized.swift in Sources */, 9BF3034D1CCA20D80096588E /* Logging.swift in Sources */, 9BC215CC1CB5E584002AADD1 /* Manager.swift in Sources */, 9BB3F7411CC60B5300C2DD05 /* Image.swift in Sources */, @@ -2907,6 +2906,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + B8D8CC0A1D506CB900CE6C0D /* Localized.swift in Sources */, 9BC6002C1CB2922E00E5EA61 /* Potatso.m in Sources */, 9BC600341CB299DE00E5EA61 /* NSError+Helper.m in Sources */, B8F784681D3678B400F02FF5 /* Settings.m in Sources */, diff --git a/PotatsoLibrary/Localized.swift b/PotatsoBase/Localized.swift similarity index 100% rename from PotatsoLibrary/Localized.swift rename to PotatsoBase/Localized.swift diff --git a/PotatsoBase/PotatsoBase.h b/PotatsoBase/PotatsoBase.h index ee0d0f7..0f579ea 100644 --- a/PotatsoBase/PotatsoBase.h +++ b/PotatsoBase/PotatsoBase.h @@ -19,4 +19,5 @@ FOUNDATION_EXPORT const unsigned char PotatsoBaseVersionString[]; #import "Potatso.h" #import "JSONUtils.h" #import "NSError+Helper.h" -#import "Settings.h" \ No newline at end of file +#import "Settings.h" +#import "PotatsoBase-Swift.h" \ No newline at end of file From 0506b3b4bccfaf83f581b329ea1ae24fb471de88 Mon Sep 17 00:00:00 2001 From: iCodesign Date: Tue, 2 Aug 2016 21:39:54 +0800 Subject: [PATCH 2/8] Crawling --- Potatso.xcodeproj/project.pbxproj | 23 +++++++++ Potatso/DataInitializer.swift | 7 +-- Potatso/Potatso.entitlements | 8 +++ Potatso/Sync/ICloudSyncService.swift | 16 ++++++ Potatso/Sync/SyncManager.swift | 74 ++++++++++++++++++++++++++++ PotatsoModel/BaseModel.swift | 34 ++++++++++++- PotatsoModel/Proxy.swift | 22 +++++++++ 7 files changed, 179 insertions(+), 5 deletions(-) create mode 100644 Potatso/Sync/ICloudSyncService.swift create mode 100644 Potatso/Sync/SyncManager.swift diff --git a/Potatso.xcodeproj/project.pbxproj b/Potatso.xcodeproj/project.pbxproj index 4467ef6..160c689 100644 --- a/Potatso.xcodeproj/project.pbxproj +++ b/Potatso.xcodeproj/project.pbxproj @@ -191,6 +191,9 @@ B8CCC6E71CFDD99E000E7E2E /* ProxyListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8CCC6E61CFDD99E000E7E2E /* ProxyListViewController.swift */; }; B8CCC6EB1CFF1501000E7E2E /* ProxyRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8CCC6EA1CFF1501000E7E2E /* ProxyRow.swift */; }; B8D8CC0A1D506CB900CE6C0D /* Localized.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D8CC091D506CB900CE6C0D /* Localized.swift */; }; + B8D8CC0C1D508F7500CE6C0D /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B8D8CC0B1D508F7500CE6C0D /* CloudKit.framework */; }; + B8D8CC111D50D5CD00CE6C0D /* SyncManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D8CC101D50D5CD00CE6C0D /* SyncManager.swift */; }; + B8D8CC131D50D5DB00CE6C0D /* ICloudSyncService.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D8CC121D50D5DB00CE6C0D /* ICloudSyncService.swift */; }; B8EAC5941D40BF310046963C /* TunnelError.m in Sources */ = {isa = PBXBuildFile; fileRef = B8EAC5931D40BF310046963C /* TunnelError.m */; }; B8F7845C1D36760D00F02FF5 /* DashboardVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8F7845B1D36760D00F02FF5 /* DashboardVC.swift */; }; B8F784671D3678B400F02FF5 /* Settings.h in Headers */ = {isa = PBXBuildFile; fileRef = B8F784651D3678B400F02FF5 /* Settings.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -871,6 +874,9 @@ B8CCC6EA1CFF1501000E7E2E /* ProxyRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ProxyRow.swift; path = ../ProxyRow.swift; sourceTree = ""; }; B8CF2F991D3DF69E009C9FF3 /* Confidential.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Confidential.h; sourceTree = ""; }; B8D8CC091D506CB900CE6C0D /* Localized.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Localized.swift; sourceTree = ""; }; + B8D8CC0B1D508F7500CE6C0D /* CloudKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CloudKit.framework; path = System/Library/Frameworks/CloudKit.framework; sourceTree = SDKROOT; }; + B8D8CC101D50D5CD00CE6C0D /* SyncManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncManager.swift; sourceTree = ""; }; + B8D8CC121D50D5DB00CE6C0D /* ICloudSyncService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ICloudSyncService.swift; sourceTree = ""; }; B8EAC5921D40BF310046963C /* TunnelError.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TunnelError.h; sourceTree = ""; }; B8EAC5931D40BF310046963C /* TunnelError.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TunnelError.m; sourceTree = ""; }; B8F7845B1D36760D00F02FF5 /* DashboardVC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DashboardVC.swift; sourceTree = ""; }; @@ -902,6 +908,7 @@ 9B9B15C41C21595B000B6541 /* PotatsoLibrary.framework in Frameworks */, 9BC6FFF11CB28B4B00E5EA61 /* PotatsoModel.framework in Frameworks */, B88559EB1D21319D00B1243E /* YAML.framework in Frameworks */, + B8D8CC0C1D508F7500CE6C0D /* CloudKit.framework in Frameworks */, 234575943B38F4EAD3E2B481 /* Pods_Potatso.framework in Frameworks */, B8822CD31D2B81C400AD252C /* libcrypto.a in Frameworks */, B8822CD41D2B81C400AD252C /* libssl.a in Frameworks */, @@ -1061,6 +1068,7 @@ 9BC858581C32CEFB00992032 /* Core */, 9BC858571C32B4AB00992032 /* Base */, 9B17F8EC1C53392D00679FCB /* Utils */, + B8D8CC0F1D50D5B700CE6C0D /* Sync */, 9B8705F51C1D77CE00651424 /* Potatso.entitlements */, 9B1F745D1C2F83AD0028C1A6 /* config.plist */, B8CF2F991D3DF69E009C9FF3 /* Confidential.h */, @@ -1970,9 +1978,19 @@ path = ..; sourceTree = ""; }; + B8D8CC0F1D50D5B700CE6C0D /* Sync */ = { + isa = PBXGroup; + children = ( + B8D8CC101D50D5CD00CE6C0D /* SyncManager.swift */, + B8D8CC121D50D5DB00CE6C0D /* ICloudSyncService.swift */, + ); + path = Sync; + sourceTree = ""; + }; C8B2149E8757C44C469576D2 /* Frameworks */ = { isa = PBXGroup; children = ( + B8D8CC0B1D508F7500CE6C0D /* CloudKit.framework */, B8822CD51D2B861E00AD252C /* StoreKit.framework */, B8822CD11D2B81C400AD252C /* libcrypto.a */, B8822CD21D2B81C400AD252C /* libssl.a */, @@ -2255,6 +2273,9 @@ com.apple.Push = { enabled = 1; }; + com.apple.iCloud = { + enabled = 1; + }; }; }; 9B0CFA1E1C1C0B1B007BD7C6 = { @@ -2806,6 +2827,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + B8D8CC111D50D5CD00CE6C0D /* SyncManager.swift in Sources */, B83AA5701D38E6F7007905B4 /* RequestDetailVC.swift in Sources */, B83D3E7D1D2BA8C8007655CE /* Receipt.swift in Sources */, B8CCC6DD1CFD5D5D000E7E2E /* CollectionViewController.swift in Sources */, @@ -2836,6 +2858,7 @@ B88096B01D0579F5008BEB87 /* CloudDetailViewController.swift in Sources */, B8319A0A1D1B975C001E50C2 /* RegexUtils.swift in Sources */, B84551401CF83D07005779CD /* HomeVC.swift in Sources */, + B8D8CC131D50D5DB00CE6C0D /* ICloudSyncService.swift in Sources */, B8CCC6E71CFDD99E000E7E2E /* ProxyListViewController.swift in Sources */, 9B76EEB91C911CBF002BF5D1 /* ProxySelectionViewController.swift in Sources */, 9BF303461CC8A1F60096588E /* LogDetailViewController.swift in Sources */, diff --git a/Potatso/DataInitializer.swift b/Potatso/DataInitializer.swift index 21f8960..e267a5a 100644 --- a/Potatso/DataInitializer.swift +++ b/Potatso/DataInitializer.swift @@ -9,6 +9,7 @@ import UIKit import ICSMainFramework import NetworkExtension +import CloudKit class DataInitializer: NSObject, AppLifeCycleProtocol { @@ -52,9 +53,9 @@ class DataInitializer: NSObject, AppLifeCycleProtocol { func deleteOrphanRules() { let orphanRules = defaultRealm.objects(Rule).filter("rulesets.@count == 0") - _ = try? defaultRealm.write({ - defaultRealm.delete(orphanRules) - }) + if orphanRules.count > 0 { + _ = try? orphanRules.delete() + } } } diff --git a/Potatso/Potatso.entitlements b/Potatso/Potatso.entitlements index 58c4426..eeb3337 100644 --- a/Potatso/Potatso.entitlements +++ b/Potatso/Potatso.entitlements @@ -2,6 +2,14 @@ + com.apple.developer.icloud-container-identifiers + + iCloud.$(CFBundleIdentifier) + + com.apple.developer.icloud-services + + CloudKit + com.apple.developer.networking.networkextension packet-tunnel-provider diff --git a/Potatso/Sync/ICloudSyncService.swift b/Potatso/Sync/ICloudSyncService.swift new file mode 100644 index 0000000..0266afc --- /dev/null +++ b/Potatso/Sync/ICloudSyncService.swift @@ -0,0 +1,16 @@ +// +// ICloudSyncService.swift +// Potatso +// +// Created by LEI on 8/2/16. +// Copyright © 2016 TouchingApp. All rights reserved. +// + +import Foundation + +class ICloudSyncService: SyncServiceProtocol { + + func setup(completion: (ErrorType? -> Void)?) { + + } +} \ No newline at end of file diff --git a/Potatso/Sync/SyncManager.swift b/Potatso/Sync/SyncManager.swift new file mode 100644 index 0000000..5cdf543 --- /dev/null +++ b/Potatso/Sync/SyncManager.swift @@ -0,0 +1,74 @@ +// +// SyncManager.swift +// Potatso +// +// Created by LEI on 8/2/16. +// Copyright © 2016 TouchingApp. All rights reserved. +// + +import Foundation +import CloudKit + +public enum SyncServiceType: String { + case None + case iCloud +} + +public protocol SyncServiceProtocol { + func setup(completion: (ErrorType? -> Void)?) +} + +public class SyncManager { + + public static let shared = SyncManager() + + private var services: [SyncServiceType: SyncServiceProtocol] = [:] + private static let serviceTypeKey = "serviceTypeKey" + + public var currentSyncServiceType: SyncServiceType { + get { + if let raw = NSUserDefaults.standardUserDefaults().objectForKey(SyncManager.serviceTypeKey) as? String, type = SyncServiceType(rawValue: raw) { + return type + } + return .None + } + set(new) { + guard currentSyncServiceType != new else { + return + } + NSUserDefaults.standardUserDefaults().setObject(new.rawValue, forKey: SyncManager.serviceTypeKey) + NSUserDefaults.standardUserDefaults().synchronize() + } + } + + public var selectedSyncServiceType: SyncServiceType = .None + + init() { + selectedSyncServiceType = currentSyncServiceType + } + + public func getSelectedSyncService() -> SyncServiceProtocol? { + let type = selectedSyncServiceType + if let service = services[type] { + return service + } + let s: SyncServiceProtocol + switch type { + case .iCloud: + s = ICloudSyncService() + default: + return nil + } + services[type] = s + return s + } + +} + +extension SyncManager { + + public func setup(completion: (ErrorType? -> Void)?) { + getSelectedSyncService()?.setup(completion) + } + +} \ No newline at end of file diff --git a/PotatsoModel/BaseModel.swift b/PotatsoModel/BaseModel.swift index dc78f9f..50fa45a 100644 --- a/PotatsoModel/BaseModel.swift +++ b/PotatsoModel/BaseModel.swift @@ -8,8 +8,9 @@ import RealmSwift import PotatsoBase +import CloudKit -private let version: UInt64 = 10 +private let version: UInt64 = 11 public var defaultRealm = try! Realm() public func setupDefaultReaml() { @@ -31,10 +32,18 @@ public func setupDefaultReaml() { Realm.Configuration.defaultConfiguration = config } +protocol CloudKitRecord { + static var recordType: String { get } + var recordId: CKRecordID { get } + func toCloudKitRecord() -> CKRecord +} + public class BaseModel: Object { public dynamic var uuid = NSUUID().UUIDString public dynamic var createAt = NSDate().timeIntervalSince1970 - + public dynamic var updatedAt = NSDate().timeIntervalSince1970 + public dynamic var deleted = false + override public static func primaryKey() -> String? { return "uuid" } @@ -45,4 +54,25 @@ public class BaseModel: Object { return f } + func fillInRecord(record: CKRecord) { + for key in ["uuid", "createAt", "updatedAt", "deleted"] { + record.setValue(self.valueForKey(key), forKey: key) + } + } + +} + +// API +extension Results { + + public func delete() throws { + defaultRealm.beginWrite() + for object in self { + if let m = object as? BaseModel { + m.deleted = true + } + } + try defaultRealm.commitWrite() + } + } \ No newline at end of file diff --git a/PotatsoModel/Proxy.swift b/PotatsoModel/Proxy.swift index f5a7677..9f8ec92 100644 --- a/PotatsoModel/Proxy.swift +++ b/PotatsoModel/Proxy.swift @@ -7,6 +7,7 @@ // import RealmSwift +import CloudKit public enum ProxyType: String { case Shadowsocks = "SS" @@ -144,6 +145,27 @@ public class Proxy: BaseModel { } +extension Proxy: CloudKitRecord { + + static var recordType: String { + return "Proxy" + } + + var recordId: CKRecordID { + return CKRecordID(recordName: uuid) + } + + public func toCloudKitRecord() -> CKRecord { + let record = CKRecord(recordType: Proxy.recordType, recordID: recordId) + fillInRecord(record) + for key in ["typeRaw", "name", "host", "port", "authscheme", "user", "password", "ota", "ssrProtocol", "ssrObfs", "ssrObfsParam"] { + record.setValue(self.valueForKey(key), forKey: key) + } + return record + } + +} + extension Proxy { public var type: ProxyType { From 64dcb8c8ea4a6be68b3a9faf392e2caf72205fd5 Mon Sep 17 00:00:00 2001 From: iCodesign Date: Thu, 4 Aug 2016 16:26:27 +0800 Subject: [PATCH 3/8] Add some db utility functions --- Podfile | 1 + Potatso.xcodeproj/project.pbxproj | 48 +++- .../ProxyConfigurationViewController.swift | 4 +- Potatso/Core/API.swift | 8 +- Potatso/DataInitializer.swift | 6 +- Potatso/HomePresenter.swift | 23 +- Potatso/Sync/AlertOperation.swift | 85 ++++++ Potatso/Sync/CloudKitRecord.swift | 219 +++++++++++++++ Potatso/Sync/FetchCloudChangesOperation.swift | 203 ++++++++++++++ Potatso/Sync/ICloudSyncService.swift | 38 ++- Potatso/Sync/PrepareZoneOperation.swift | 61 +++++ Potatso/Sync/PushLocalChangesOperation.swift | 250 ++++++++++++++++++ Potatso/Sync/RealmCloud.swift | 203 ++++++++++++++ Potatso/Sync/SyncManager.swift | 5 + Potatso/Sync/SyncOperation.swift | 138 ++++++++++ Potatso/Utils/KeychainUtils.swift | 12 + Potatso/Utils/Receipt.swift | 2 - PotatsoModel/BaseModel.swift | 16 +- PotatsoModel/ConfigurationGroup.swift | 29 +- PotatsoModel/DBUtils.swift | 147 ++++++++++ PotatsoModel/Proxy.swift | 71 ++--- PotatsoModel/RuleSet.swift | 2 +- 22 files changed, 1464 insertions(+), 107 deletions(-) create mode 100644 Potatso/Sync/AlertOperation.swift create mode 100644 Potatso/Sync/CloudKitRecord.swift create mode 100644 Potatso/Sync/FetchCloudChangesOperation.swift create mode 100644 Potatso/Sync/PrepareZoneOperation.swift create mode 100644 Potatso/Sync/PushLocalChangesOperation.swift create mode 100644 Potatso/Sync/RealmCloud.swift create mode 100644 Potatso/Sync/SyncOperation.swift create mode 100644 Potatso/Utils/KeychainUtils.swift create mode 100644 PotatsoModel/DBUtils.swift diff --git a/Podfile b/Podfile index c3d9024..21b0abf 100644 --- a/Podfile +++ b/Podfile @@ -45,6 +45,7 @@ target "Potatso" do pod 'ObjectMapper' pod 'CocoaLumberjack/Swift' pod 'Helpshift', '5.6.1' + pod 'PSOperations', '~> 2.3' tunnel library fabric diff --git a/Potatso.xcodeproj/project.pbxproj b/Potatso.xcodeproj/project.pbxproj index 160c689..9a54917 100644 --- a/Potatso.xcodeproj/project.pbxproj +++ b/Potatso.xcodeproj/project.pbxproj @@ -154,6 +154,7 @@ B344F7C30FF7755C7EA32B8C /* Pods_PotatsoLibraryTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A27A0B817AADB557908316E3 /* Pods_PotatsoLibraryTests.framework */; }; B803A6961D0165EA003EA9AA /* CloudViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B803A6951D0165EA003EA9AA /* CloudViewController.swift */; }; B803A6981D02B768003EA9AA /* API.swift in Sources */ = {isa = PBXBuildFile; fileRef = B803A6971D02B768003EA9AA /* API.swift */; }; + B821B0F01D51DD8F0061E7B9 /* KeychainUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = B821B0EF1D51DD8F0061E7B9 /* KeychainUtils.swift */; }; B82574A91D1D98CF007BAF40 /* Pollution.swift in Sources */ = {isa = PBXBuildFile; fileRef = B82574A81D1D98CF007BAF40 /* Pollution.swift */; }; B829C1721D4395BC00C17B82 /* QRCodeScannerVC.m in Sources */ = {isa = PBXBuildFile; fileRef = B829C1711D4395BC00C17B82 /* QRCodeScannerVC.m */; }; B8319A0A1D1B975C001E50C2 /* RegexUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8319A091D1B975C001E50C2 /* RegexUtils.swift */; }; @@ -182,6 +183,13 @@ B88559EC1D21319D00B1243E /* YAML.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B88559EA1D21319D00B1243E /* YAML.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; B88559F01D21371A00B1243E /* PotatsoModel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9BC6FFEA1CB28B4B00E5EA61 /* PotatsoModel.framework */; }; B88874491D18186100AEF002 /* ShadowPath.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B86B08E91D17F84900613014 /* ShadowPath.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + B8A09D691D51B42B00A9A989 /* CloudKitRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8A09D681D51B42B00A9A989 /* CloudKitRecord.swift */; }; + B8A09D7B1D51B9A900A9A989 /* AlertOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8A09D741D51B9A900A9A989 /* AlertOperation.swift */; }; + B8A09D7C1D51B9A900A9A989 /* FetchCloudChangesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8A09D751D51B9A900A9A989 /* FetchCloudChangesOperation.swift */; }; + B8A09D7E1D51B9A900A9A989 /* PrepareZoneOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8A09D771D51B9A900A9A989 /* PrepareZoneOperation.swift */; }; + B8A09D7F1D51B9A900A9A989 /* PushLocalChangesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8A09D781D51B9A900A9A989 /* PushLocalChangesOperation.swift */; }; + B8A09D801D51B9A900A9A989 /* RealmCloud.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8A09D791D51B9A900A9A989 /* RealmCloud.swift */; }; + B8A09D811D51B9A900A9A989 /* SyncOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8A09D7A1D51B9A900A9A989 /* SyncOperation.swift */; }; B8AD81681D4AF9FB00C12FC1 /* YAML.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B88559EA1D21319D00B1243E /* YAML.framework */; }; B8C256AE1D1A93DA0074D3B1 /* FlatButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8C256AD1D1A93DA0074D3B1 /* FlatButton.swift */; }; B8C256B01D1A957A0074D3B1 /* HomePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8C256AF1D1A957A0074D3B1 /* HomePresenter.swift */; }; @@ -190,6 +198,7 @@ B8CCC6E51CFDCFD8000E7E2E /* RuleSetCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8CCC6E41CFDCFD8000E7E2E /* RuleSetCell.swift */; }; B8CCC6E71CFDD99E000E7E2E /* ProxyListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8CCC6E61CFDD99E000E7E2E /* ProxyListViewController.swift */; }; B8CCC6EB1CFF1501000E7E2E /* ProxyRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8CCC6EA1CFF1501000E7E2E /* ProxyRow.swift */; }; + B8D7F1841D518FF000B115F3 /* DBUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D7F1831D518FF000B115F3 /* DBUtils.swift */; }; B8D8CC0A1D506CB900CE6C0D /* Localized.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D8CC091D506CB900CE6C0D /* Localized.swift */; }; B8D8CC0C1D508F7500CE6C0D /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B8D8CC0B1D508F7500CE6C0D /* CloudKit.framework */; }; B8D8CC111D50D5CD00CE6C0D /* SyncManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D8CC101D50D5CD00CE6C0D /* SyncManager.swift */; }; @@ -838,6 +847,7 @@ B03E5F0D3FAB0B3302CA1A4E /* Pods_PacketTunnel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_PacketTunnel.framework; sourceTree = BUILT_PRODUCTS_DIR; }; B803A6951D0165EA003EA9AA /* CloudViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudViewController.swift; sourceTree = ""; }; B803A6971D02B768003EA9AA /* API.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = API.swift; sourceTree = ""; }; + B821B0EF1D51DD8F0061E7B9 /* KeychainUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeychainUtils.swift; sourceTree = ""; }; B82574A81D1D98CF007BAF40 /* Pollution.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Pollution.swift; sourceTree = ""; }; B829C1701D4395BC00C17B82 /* QRCodeScannerVC.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QRCodeScannerVC.h; sourceTree = ""; }; B829C1711D4395BC00C17B82 /* QRCodeScannerVC.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QRCodeScannerVC.m; sourceTree = ""; }; @@ -865,6 +875,13 @@ B8822CD21D2B81C400AD252C /* libssl.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libssl.a; path = "Library/ShadowPath/ShadowPath/shadowsocks-libev/libopenssl/lib/libssl.a"; sourceTree = ""; }; B8822CD51D2B861E00AD252C /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; }; B88559EA1D21319D00B1243E /* YAML.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = YAML.framework; path = Carthage/Build/iOS/YAML.framework; sourceTree = ""; }; + B8A09D681D51B42B00A9A989 /* CloudKitRecord.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudKitRecord.swift; sourceTree = ""; }; + B8A09D741D51B9A900A9A989 /* AlertOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlertOperation.swift; sourceTree = ""; }; + B8A09D751D51B9A900A9A989 /* FetchCloudChangesOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchCloudChangesOperation.swift; sourceTree = ""; }; + B8A09D771D51B9A900A9A989 /* PrepareZoneOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrepareZoneOperation.swift; sourceTree = ""; }; + B8A09D781D51B9A900A9A989 /* PushLocalChangesOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PushLocalChangesOperation.swift; sourceTree = ""; }; + B8A09D791D51B9A900A9A989 /* RealmCloud.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RealmCloud.swift; sourceTree = ""; }; + B8A09D7A1D51B9A900A9A989 /* SyncOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncOperation.swift; sourceTree = ""; }; B8C256AD1D1A93DA0074D3B1 /* FlatButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlatButton.swift; sourceTree = ""; }; B8C256AF1D1A957A0074D3B1 /* HomePresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomePresenter.swift; sourceTree = ""; }; B8CCC6DC1CFD5D5D000E7E2E /* CollectionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectionViewController.swift; sourceTree = ""; }; @@ -873,6 +890,7 @@ B8CCC6E61CFDD99E000E7E2E /* ProxyListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProxyListViewController.swift; sourceTree = ""; }; B8CCC6EA1CFF1501000E7E2E /* ProxyRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ProxyRow.swift; path = ../ProxyRow.swift; sourceTree = ""; }; B8CF2F991D3DF69E009C9FF3 /* Confidential.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Confidential.h; sourceTree = ""; }; + B8D7F1831D518FF000B115F3 /* DBUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DBUtils.swift; sourceTree = ""; }; B8D8CC091D506CB900CE6C0D /* Localized.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Localized.swift; sourceTree = ""; }; B8D8CC0B1D508F7500CE6C0D /* CloudKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CloudKit.framework; path = System/Library/Frameworks/CloudKit.framework; sourceTree = SDKROOT; }; B8D8CC101D50D5CD00CE6C0D /* SyncManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncManager.swift; sourceTree = ""; }; @@ -1099,6 +1117,7 @@ B87A04391D193ABC001132F2 /* LoggerUtils.swift */, B8319A091D1B975C001E50C2 /* RegexUtils.swift */, B83D3E781D2BA689007655CE /* Event.swift */, + B821B0EF1D51DD8F0061E7B9 /* KeychainUtils.swift */, B83D3E7C1D2BA8C8007655CE /* Receipt.swift */, B8822CCB1D2AA70A00AD252C /* Receipt.h */, B8822CCC1D2AA70A00AD252C /* Receipt.m */, @@ -1257,6 +1276,7 @@ 9BC215BC1CB4DE6B002AADD1 /* Rule.swift */, 9BC215BE1CB4E06C002AADD1 /* RuleSet.swift */, 9BC215C01CB5083B002AADD1 /* ConfigurationGroup.swift */, + B8D7F1831D518FF000B115F3 /* DBUtils.swift */, 9BC6FFEE1CB28B4B00E5EA61 /* Info.plist */, ); path = PotatsoModel; @@ -1949,6 +1969,21 @@ path = udpgw_client; sourceTree = ""; }; + B821B0F11D5318F80061E7B9 /* iCloud */ = { + isa = PBXGroup; + children = ( + B8D8CC121D50D5DB00CE6C0D /* ICloudSyncService.swift */, + B8A09D681D51B42B00A9A989 /* CloudKitRecord.swift */, + B8A09D741D51B9A900A9A989 /* AlertOperation.swift */, + B8A09D751D51B9A900A9A989 /* FetchCloudChangesOperation.swift */, + B8A09D771D51B9A900A9A989 /* PrepareZoneOperation.swift */, + B8A09D781D51B9A900A9A989 /* PushLocalChangesOperation.swift */, + B8A09D791D51B9A900A9A989 /* RealmCloud.swift */, + B8A09D7A1D51B9A900A9A989 /* SyncOperation.swift */, + ); + name = iCloud; + sourceTree = ""; + }; B8367A841D1B6D6800D50C25 /* Row */ = { isa = PBXGroup; children = ( @@ -1982,7 +2017,7 @@ isa = PBXGroup; children = ( B8D8CC101D50D5CD00CE6C0D /* SyncManager.swift */, - B8D8CC121D50D5DB00CE6C0D /* ICloudSyncService.swift */, + B821B0F11D5318F80061E7B9 /* iCloud */, ); path = Sync; sourceTree = ""; @@ -2838,6 +2873,7 @@ B8C256AE1D1A93DA0074D3B1 /* FlatButton.swift in Sources */, 9B8193ED1CBE4DCA00BE320D /* UrlHandler.swift in Sources */, 9B1F74761C3164AD0028C1A6 /* VPN.swift in Sources */, + B8A09D7B1D51B9A900A9A989 /* AlertOperation.swift in Sources */, 9B8750551CC761D000A11715 /* RequestModel.swift in Sources */, B87A043A1D193ABC001132F2 /* LoggerUtils.swift in Sources */, B8CCC6EB1CFF1501000E7E2E /* ProxyRow.swift in Sources */, @@ -2847,12 +2883,15 @@ 9B1F74601C2F84250028C1A6 /* AppInitializer.swift in Sources */, 9B54CC441C1C266C00DDEEBB /* UIViewControllerExtensions.swift in Sources */, 9B0CFA0F1C1C0B1B007BD7C6 /* AppDelegate.swift in Sources */, + B8A09D801D51B9A900A9A989 /* RealmCloud.swift in Sources */, + B8A09D7E1D51B9A900A9A989 /* PrepareZoneOperation.swift in Sources */, 9BB3F73C1CC3D3DE00C2DD05 /* RecentRequestsCell.swift in Sources */, 9BD4CACA1CB9600D00F99BF9 /* AlertUtils.swift in Sources */, B829C1721D4395BC00C17B82 /* QRCodeScannerVC.m in Sources */, B87B98041D3B423B00FA66BF /* PaddingLabel.swift in Sources */, B8CCC6E51CFDCFD8000E7E2E /* RuleSetCell.swift in Sources */, B803A6961D0165EA003EA9AA /* CloudViewController.swift in Sources */, + B8A09D7F1D51B9A900A9A989 /* PushLocalChangesOperation.swift in Sources */, 9BC858A51C33D30100992032 /* BaseSafariViewController.swift in Sources */, B84551421CF878BD005779CD /* ConfigGroupChooseVC.swift in Sources */, B88096B01D0579F5008BEB87 /* CloudDetailViewController.swift in Sources */, @@ -2865,6 +2904,7 @@ 9B1D9B7B1CA4C45F0078D814 /* HUDUtils.swift in Sources */, B8822CCD1D2AA70A00AD252C /* Receipt.m in Sources */, B8CCC6E11CFDCADC000E7E2E /* RuleSetListViewController.swift in Sources */, + B821B0F01D51DD8F0061E7B9 /* KeychainUtils.swift in Sources */, 9B2290FF1C8E5B9600EEC901 /* DataInitializer.swift in Sources */, 9BB62FA31C8FFC1D002ADC54 /* RuleSetConfigurationViewController.swift in Sources */, 9BF3034B1CCA1B060096588E /* BaseEmptyView.swift in Sources */, @@ -2879,7 +2919,10 @@ 9B76EEB71C90740C002BF5D1 /* RuleSetsSelectionViewController.swift in Sources */, B83AA5741D38E728007905B4 /* SegmentPageVC.swift in Sources */, 9BB3F74F1CC6308000C2DD05 /* RecentRequestsVC.swift in Sources */, + B8A09D811D51B9A900A9A989 /* SyncOperation.swift in Sources */, 9B8285481CE20DE40027D15C /* HMScanner.m in Sources */, + B8A09D7C1D51B9A900A9A989 /* FetchCloudChangesOperation.swift in Sources */, + B8A09D691D51B42B00A9A989 /* CloudKitRecord.swift in Sources */, 9BB3F7441CC60B6F00C2DD05 /* Error.swift in Sources */, 9B76EEAD1C9005D2002BF5D1 /* RuleConfigurationViewController.swift in Sources */, ); @@ -2946,6 +2989,7 @@ 9BC215B91CB4DA9E002AADD1 /* Proxy.swift in Sources */, 9BC215C11CB5083B002AADD1 /* ConfigurationGroup.swift in Sources */, 9BC215BD1CB4DE6B002AADD1 /* Rule.swift in Sources */, + B8D7F1841D518FF000B115F3 /* DBUtils.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3536,6 +3580,7 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 114; DYLIB_INSTALL_NAME_BASE = "@rpath"; + EMBEDDED_CONTENT_CONTAINS_SWIFT = NO; INFOPLIST_FILE = PotatsoBase/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 9.0; @@ -3562,6 +3607,7 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 114; DYLIB_INSTALL_NAME_BASE = "@rpath"; + EMBEDDED_CONTENT_CONTAINS_SWIFT = NO; INFOPLIST_FILE = PotatsoBase/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 9.0; diff --git a/Potatso/Advance/ProxyConfigurationViewController.swift b/Potatso/Advance/ProxyConfigurationViewController.swift index fd57b4a..3072c3f 100644 --- a/Potatso/Advance/ProxyConfigurationViewController.swift +++ b/Potatso/Advance/ProxyConfigurationViewController.swift @@ -208,7 +208,6 @@ class ProxyConfigurationViewController: FormViewController { break } let ota = values[kProxyFormOta] as? Bool ?? false - defaultRealm.beginWrite() upstreamProxy.type = type upstreamProxy.name = name upstreamProxy.host = host @@ -220,8 +219,7 @@ class ProxyConfigurationViewController: FormViewController { upstreamProxy.ssrProtocol = values[kProxyFormProtocol] as? String upstreamProxy.ssrObfs = values[kProxyFormObfs] as? String upstreamProxy.ssrObfsParam = values[kProxyFormObfsParam] as? String - defaultRealm.add(upstreamProxy, update: true) - try defaultRealm.commitWrite() + try DBUtils.add(upstreamProxy) close() }catch { showTextHUD("\(error)", dismissAfterDelay: 1.0) diff --git a/Potatso/Core/API.swift b/Potatso/Core/API.swift index 40237f6..89e7a69 100644 --- a/Potatso/Core/API.swift +++ b/Potatso/Core/API.swift @@ -81,16 +81,12 @@ extension RuleSet { static func addRemoteObject(ruleset: RuleSet, update: Bool = true) throws { ruleset.isSubscribe = true - try defaultRealm.write { - defaultRealm.add(ruleset, update: true) - } + try DBUtils.add(ruleset) } static func addRemoteArray(rulesets: [RuleSet], update: Bool = true) throws { rulesets.forEach({ $0.isSubscribe = true }) - try defaultRealm.write { - defaultRealm.add(rulesets, update: true) - } + try DBUtils.add(rulesets) } } diff --git a/Potatso/DataInitializer.swift b/Potatso/DataInitializer.swift index e267a5a..8404e29 100644 --- a/Potatso/DataInitializer.swift +++ b/Potatso/DataInitializer.swift @@ -12,6 +12,8 @@ import NetworkExtension import CloudKit class DataInitializer: NSObject, AppLifeCycleProtocol { + + let s = ICloudSyncService() func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject : AnyObject]?) -> Bool { do { @@ -19,6 +21,7 @@ class DataInitializer: NSObject, AppLifeCycleProtocol { }catch { error.log("Fail to setup manager") } + SyncManager.shared.sync() return true } @@ -32,6 +35,7 @@ class DataInitializer: NSObject, AppLifeCycleProtocol { func applicationWillEnterForeground(application: UIApplication) { Receipt.shared.validate() + SyncManager.shared.sync() } func applicationDidBecomeActive(application: UIApplication) { @@ -54,7 +58,7 @@ class DataInitializer: NSObject, AppLifeCycleProtocol { func deleteOrphanRules() { let orphanRules = defaultRealm.objects(Rule).filter("rulesets.@count == 0") if orphanRules.count > 0 { - _ = try? orphanRules.delete() + _ = try? DBUtils.delete(orphanRules) } } diff --git a/Potatso/HomePresenter.swift b/Potatso/HomePresenter.swift index aea4faf..fafb42f 100644 --- a/Potatso/HomePresenter.swift +++ b/Potatso/HomePresenter.swift @@ -51,12 +51,7 @@ class HomePresenter: NSObject { func chooseProxy() { let chooseVC = ProxyListViewController(allowNone: true) { [unowned self] proxy in do { - try defaultRealm.write { - self.group.proxies.removeAll() - if let proxy = proxy { - self.group.proxies.append(proxy) - } - } + try ConfigurationGroup.changeProxy(forGroupId: self.group.uuid, proxyId: proxy?.uuid) self.delegate?.handleRefreshUI() }catch { self.vc.showTextHUD("\("Fail to add ruleset".localized()): \((error as NSError).localizedDescription)", dismissAfterDelay: 1.5) @@ -99,9 +94,7 @@ class HomePresenter: NSObject { } let group = ConfigurationGroup() group.name = trimmedName - try defaultRealm.write { - defaultRealm.add(group) - } + try DBUtils.add(group) CurrentGroupManager.shared.group = group } @@ -124,9 +117,7 @@ class HomePresenter: NSObject { return } do { - try defaultRealm.write { - group.ruleSets.append(ruleSet) - } + try ConfigurationGroup.appendRuleSet(forGroupId: group.uuid, rulesetId: ruleSet.uuid) self.delegate?.handleRefreshUI() }catch { self.vc.showTextHUD("\("Fail to add ruleset".localized()): \((error as NSError).localizedDescription)", dismissAfterDelay: 1.5) @@ -152,9 +143,7 @@ class HomePresenter: NSObject { } } do { - try defaultRealm.write { - self.group.dns = dns - } + try ConfigurationGroup.changeDNS(forGroupId: group.uuid, dns: dns) }catch { self.vc.showTextHUD("\("Fail to change dns".localized()): \((error as NSError).localizedDescription)", dismissAfterDelay: 1.5) } @@ -172,9 +161,9 @@ class HomePresenter: NSObject { urlTextField = textField } alert.addAction(UIAlertAction(title: "OK".localized(), style: .Default, handler: { [unowned self] (action) in - if let input = urlTextField?.text { + if let newName = urlTextField?.text { do { - try self.group.changeName(input) + try ConfigurationGroup.changeName(forGroupId: self.group.uuid, name: newName) }catch { Alert.show(self.vc, title: "Failed to change name", message: "\(error)") } diff --git a/Potatso/Sync/AlertOperation.swift b/Potatso/Sync/AlertOperation.swift new file mode 100644 index 0000000..5b41ebd --- /dev/null +++ b/Potatso/Sync/AlertOperation.swift @@ -0,0 +1,85 @@ +/* + Copyright (C) 2015 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + This file shows how to present an alert as part of an operation. + */ + +import UIKit +import PSOperations +import CloudKit + +public class AlertOperation: Operation { + // MARK: Properties + + private let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .Alert) + private let presentationContext: UIViewController? + + var title: String? { + get { + return alertController.title + } + + set { + alertController.title = newValue + name = newValue + } + } + + var message: String? { + get { + return alertController.message + } + + set { + alertController.message = newValue + } + } + + // MARK: Initialization + + init(presentationContext: UIViewController? = nil) { + self.presentationContext = presentationContext ?? UIApplication.sharedApplication().keyWindow?.rootViewController + + super.init() + + addCondition(AlertPresentation()) + + /* + This operation modifies the view controller hierarchy. + Doing this while other such operations are executing can lead to + inconsistencies in UIKit. So, let's make them mutally exclusive. + */ + addCondition(MutuallyExclusive()) + } + + func addAction(title: String, style: UIAlertActionStyle = .Default, handler: AlertOperation -> Void = { _ in }) { + let action = UIAlertAction(title: title, style: style) { [weak self] _ in + if let strongSelf = self { + handler(strongSelf) + } + + self?.finish() + } + + alertController.addAction(action) + } + + override public func execute() { + guard let presentationContext = presentationContext else { + finish() + + return + } + + dispatch_async(dispatch_get_main_queue()) { + if self.alertController.actions.isEmpty { + self.addAction("OK") + } + + presentationContext.presentViewController(self.alertController, animated: true, completion: nil) + } + } +} + diff --git a/Potatso/Sync/CloudKitRecord.swift b/Potatso/Sync/CloudKitRecord.swift new file mode 100644 index 0000000..d787b66 --- /dev/null +++ b/Potatso/Sync/CloudKitRecord.swift @@ -0,0 +1,219 @@ +// +// CloudKitRecord.swift +// Potatso +// +// Created by LEI on 8/3/16. +// Copyright © 2016 TouchingApp. All rights reserved. +// + +import Foundation +import PotatsoModel +import CloudKit +import Realm +import RealmSwift + +let potatsoZoneId = CKRecordZoneID(zoneName: "PotatsoCloud", ownerName: CKOwnerDefaultName) + +public protocol CloudKitRecord { + static var recordType: String { get } + static var keys: [String] { get } + var recordId: CKRecordID { get } + func toCloudKitRecord() -> CKRecord + static func fromCloudKitRecord(record: CKRecord) -> BaseModel +} + +extension BaseModel { + + public static var basekeys: [String] { + return ["uuid", "createAt", "updatedAt", "deleted"] + } + +} + +extension Proxy: CloudKitRecord { + + public static var recordType: String { + return "Proxy" + } + + public static var keys: [String] { + return basekeys + ["typeRaw", "name", "host", "port", "authscheme", "user", "password", "ota", "ssrProtocol", "ssrObfs", "ssrObfsParam"] + } + + public var recordId: CKRecordID { + return CKRecordID(recordName: uuid, zoneID: potatsoZoneId) + } + + public func toCloudKitRecord() -> CKRecord { + let record = CKRecord(recordType: Proxy.recordType, recordID: recordId) + for key in Proxy.keys { + record.setValue(self.valueForKey(key), forKey: key) + } + return record + } + + public static func fromCloudKitRecord(record: CKRecord) -> BaseModel { + let proxy = Proxy() + for key in Proxy.keys { + if let v = record.valueForKey(key) { + proxy.setValue(v, forKey: key) + } + } + return proxy + } +} + +extension Rule: CloudKitRecord { + + public static var recordType: String { + return "Rule" + } + + public static var keys: [String] { + return basekeys + ["typeRaw", "content", "order"] + } + + public var recordId: CKRecordID { + return CKRecordID(recordName: uuid, zoneID: potatsoZoneId) + } + + public func toCloudKitRecord() -> CKRecord { + let record = CKRecord(recordType: Rule.recordType, recordID: recordId) + for key in Rule.keys { + record.setValue(self.valueForKey(key), forKey: key) + } + return record + } + + public static func fromCloudKitRecord(record: CKRecord) -> BaseModel { + let rule = Rule() + for key in Rule.keys { + if let v = record.valueForKey(key) { + rule.setValue(v, forKey: key) + } + } + return rule + } +} + +extension RuleSet: CloudKitRecord { + + public static var recordType: String { + return "RuleSet" + } + + public static var keys: [String] { + return basekeys + ["editable", "name", "updateAt", "desc", "ruleCount", "isSubscribe", "isOfficial"] + } + + public var recordId: CKRecordID { + return CKRecordID(recordName: uuid, zoneID: potatsoZoneId) + } + + public func toCloudKitRecord() -> CKRecord { + let record = CKRecord(recordType: RuleSet.recordType, recordID: recordId) + for key in RuleSet.keys { + record.setValue(self.valueForKey(key), forKey: key) + } + record["rules"] = rules.map({ $0.uuid }).joinWithSeparator(",") + return record + } + + public static func fromCloudKitRecord(record: CKRecord) -> BaseModel { + let ruleset = RuleSet() + for key in RuleSet.keys { + if let v = record.valueForKey(key) { + ruleset.setValue(v, forKey: key) + } + } + if let rulesUUIDs = record["rules"] as? String { + let realm = try! Realm() + let uuids = rulesUUIDs.componentsSeparatedByString(",") + let rules = uuids.flatMap({ realm.objects(Rule).filter("uuid = '\($0)'").first }) + ruleset.rules.appendContentsOf(rules) + } + return ruleset + } +} + +extension ConfigurationGroup: CloudKitRecord { + + public static var recordType: String { + return "ConfigurationGroup" + } + + public static var keys: [String] { + return basekeys + ["editable", "name", "defaultToProxy"] + } + + public var recordId: CKRecordID { + return CKRecordID(recordName: uuid, zoneID: potatsoZoneId) + } + + public func toCloudKitRecord() -> CKRecord { + let record = CKRecord(recordType: ConfigurationGroup.recordType, recordID: recordId) + for key in ConfigurationGroup.keys { + record.setValue(self.valueForKey(key), forKey: key) + } + record["proxies"] = proxies.map({ $0.uuid }).joinWithSeparator(",") + record["ruleSets"] = ruleSets.map({ $0.uuid }).joinWithSeparator(",") + return record + } + + public static func fromCloudKitRecord(record: CKRecord) -> BaseModel { + let group = ConfigurationGroup() + for key in ConfigurationGroup.keys { + if let v = record.valueForKey(key) { + group.setValue(v, forKey: key) + } + } + let realm = try! Realm() + if let rulesUUIDs = record["proxies"] as? String { + let uuids = rulesUUIDs.componentsSeparatedByString(",") + let rules = uuids.flatMap({ realm.objects(Proxy).filter("uuid = '\($0)'").first }) + group.proxies.appendContentsOf(rules) + } + if let rulesUUIDs = record["ruleSets"] as? String { + let uuids = rulesUUIDs.componentsSeparatedByString(",") + let rules = uuids.flatMap({ realm.objects(RuleSet).filter("uuid = '\($0)'").first }) + group.ruleSets.appendContentsOf(rules) + } + return group + } +} + + +func changeLocalRecord(record: CKRecord, objectClass: CloudKitRecord.Type) throws { + let realm = try! Realm() + let realmObject: BaseModel + let local: BaseModel? + switch record.recordType { + case "Proxy": + realmObject = Proxy.fromCloudKitRecord(record) + realmObject.synced = true + local = realm.objects(Proxy).filter("uuid = '\(realmObject.uuid)'").first + default: + return + } + if let local = local { + if local.updatedAt > realmObject.updatedAt { + try DBUtils.mark(local, synced: false) + return + } else if local.updatedAt == realmObject.updatedAt { + try DBUtils.mark(local, synced: true) + return + } + } + try DBUtils.add(realmObject) +} + +func deleteLocalRecord(recordID: CKRecordID, objectClass: CloudKitRecord.Type) throws { + let realm = try! Realm() + let id = recordID.recordName + // FIXME: Unsafe realm casting + if let object = realm.objectForPrimaryKey(objectClass as! BaseModel.Type, key: id) { + print("Deleting local record.") + try DBUtils.delete(object) + } +} + diff --git a/Potatso/Sync/FetchCloudChangesOperation.swift b/Potatso/Sync/FetchCloudChangesOperation.swift new file mode 100644 index 0000000..d442d6a --- /dev/null +++ b/Potatso/Sync/FetchCloudChangesOperation.swift @@ -0,0 +1,203 @@ +import Foundation +import RealmSwift +import CloudKit +import PSOperations + +struct FetchResults { + var changedRecords: [CKRecord] = [] + var deletedRecordIDs: [CKRecordID] = [] + + var count: Int { + return changedRecords.count + deletedRecordIDs.count + } +} + +class FetchCloudChangesOperation: Operation { + + let zoneID: CKRecordZoneID + var changeToken: CKServerChangeToken? + + let delayOperationQueue = OperationQueue() + let maximumRetryAttempts: Int + var retryAttempts: Int = 0 + + let objectClass: CloudKitRecord.Type + + init(zoneID: CKRecordZoneID, objectClass: CloudKitRecord.Type, previousServerChangeToken: CKServerChangeToken?, + maximumRetryAttempts: Int = 3) { + self.zoneID = zoneID + self.objectClass = objectClass + self.changeToken = previousServerChangeToken + self.maximumRetryAttempts = maximumRetryAttempts + + super.init() + name = "Fetch Cloud Changes" + } + + override func execute() { + print("\(self.name!) started") + + fetchCloudChanges(changeToken) { + (nsError) in + self.finishWithError(nsError) + } + } + + func fetchCloudChanges(changeToken: CKServerChangeToken?, + completionHandler: (NSError!) -> ()) { + + let fetchOperation = CKFetchRecordChangesOperation(recordZoneID: zoneID, previousServerChangeToken: changeToken) + + var results = FetchResults() + + // Enable resultsLimit to test moreComing + // fetchOperation.resultsLimit = 10 + + fetchOperation.recordChangedBlock = { + (record) in + results.changedRecords.append(record) + } + + fetchOperation.recordWithIDWasDeletedBlock = { + (recordID) in + results.deletedRecordIDs.append(recordID) + } + + fetchOperation.fetchRecordChangesCompletionBlock = { + (serverChangeToken, clientChangeToken, nsError) in + + if let error = nsError { + self.handleCloudKitFetchError(error, completionHandler: completionHandler) + + } else { + + // Ensure no errors processing the fetch before updating the local change token + let writeError = self.processFetchResults(results) + + if let writeError = writeError { + self.finishWithError(writeError) + + } else { + setZoneChangeToken(self.zoneID, changeToken: serverChangeToken) + self.changeToken = serverChangeToken + + if fetchOperation.moreComing { + print(" more coming...") + self.fetchCloudChanges(self.changeToken, + completionHandler: completionHandler) + } else { + completionHandler(nsError) + } + } + } + } + fetchOperation.start() + } + + func processFetchResults(results: FetchResults) -> NSError? { + var error: NSError? + + do { + print("changedRecords: \(results.changedRecords.map({ $0.recordID.recordName }))") + for record in results.changedRecords { + try changeLocalRecord(record, objectClass: self.objectClass) + } + + print("deletedRecordIDs: \(results.deletedRecordIDs.map({ $0.recordName }))") + for recordID in results.deletedRecordIDs { + try deleteLocalRecord(recordID, objectClass: self.objectClass) + } + } catch let realmError as NSError { + error = realmError + } + + return error + } + + // MARK: - Retry + + // Wait a default of 3 seconds + func parseRetryTime(error: NSError) -> Double { + var retrySecondsDouble: Double = 3 + if let retrySecondsString = error.userInfo[CKErrorRetryAfterKey] as? String { + retrySecondsDouble = Double(retrySecondsString)! + } + return retrySecondsDouble + } + + // After `maximumRetryAttempts` this function will return an error + func retryFetch(error: NSError, retryAfter: Double, completionHandler: (NSError!) -> ()) { + + if self.retryAttempts < self.maximumRetryAttempts { + self.retryAttempts += 1 + + let delayOperation = DelayOperation(interval: retryAfter) + let finishObserver = BlockObserver { operation, error in + self.fetchCloudChanges(self.changeToken, completionHandler: completionHandler) + } + delayOperation.addObserver(finishObserver) + delayOperationQueue.addOperation(delayOperation) + } else { + completionHandler(error) + } + } + + // MARK: - Error Handling + + /** + Implement custom logic here for handling CloudKit fetch errors. + */ + func handleCloudKitFetchError(error: NSError, completionHandler: (NSError!) -> ()) { + + let ckErrorCode: CKErrorCode = CKErrorCode(rawValue: error.code)! + + switch ckErrorCode { + case .ZoneBusy, .RequestRateLimited, .ServiceUnavailable, .NetworkFailure, .NetworkUnavailable, .ResultsTruncated: + // Retry necessary + retryFetch(error, retryAfter: parseRetryTime(error), completionHandler: completionHandler) + + case .BadDatabase, .InternalError, .BadContainer, .MissingEntitlement, + .ConstraintViolation, .IncompatibleVersion, .AssetFileNotFound, + .AssetFileModified, .InvalidArguments, + .PermissionFailure, .ServerRejectedRequest: + // Developer issue + completionHandler(error) + + case .UnknownItem: + // Developer issue + // - Never delete CloudKit Record Types. + // - This issue will arise if you created some records of this type + // and then deleted the type. Even if the records were also deleted, + // you must keep the type around because deleted recordIDs are stored + // along with type information. When fetching, this is unfortunately + // checked. + // - A possible hack is to save a new record with the missing record type + // name. This works because field information is not saved on deleted + // record IDs. Unfortunately you might accidentally overwrite an existing + // record type which will lead to further errors. + completionHandler(error) + + case .QuotaExceeded, .OperationCancelled: + // User issue. Provide alert. + completionHandler(error) + + case .LimitExceeded, .PartialFailure, .ServerRecordChanged, + .BatchRequestFailed: + // Not possible in a fetch operation (I think). + completionHandler(error) + + case .NotAuthenticated: + // Handled as condition of sync operation. + completionHandler(error) + + case .ZoneNotFound, .UserDeletedZone: + // Handled in PrepareZoneOperation. + completionHandler(error) + + case .ChangeTokenExpired: + // TODO: Determine correct handling + // CK Docs: The previousServerChangeToken value is too old and the client must re-sync from scratch + completionHandler(error) + } + } +} diff --git a/Potatso/Sync/ICloudSyncService.swift b/Potatso/Sync/ICloudSyncService.swift index 0266afc..2e7d435 100644 --- a/Potatso/Sync/ICloudSyncService.swift +++ b/Potatso/Sync/ICloudSyncService.swift @@ -7,10 +7,44 @@ // import Foundation +import CloudKit +import PSOperations +import PotatsoModel class ICloudSyncService: SyncServiceProtocol { + let operationQueue = OperationQueue() + + init() { + + } + func setup(completion: (ErrorType? -> Void)?) { - + } -} \ No newline at end of file + + func sync() { + let proxySyncOp = SyncOperation(zoneID: potatsoZoneId, objectClass: Proxy.self, syncType: SyncType.FetchCloudChangesAndThenPushLocalChanges) { + print("sync proxies completed") + } + let ruleSyncOp = SyncOperation(zoneID: potatsoZoneId, objectClass: Rule.self, syncType: SyncType.FetchCloudChangesAndThenPushLocalChanges) { + print("sync rules completed") + } + let ruleSetSyncOp = SyncOperation(zoneID: potatsoZoneId, objectClass: RuleSet.self, syncType: SyncType.FetchCloudChangesAndThenPushLocalChanges) { + print("sync rulesets completed") + } + ruleSetSyncOp.addDependency(ruleSetSyncOp) + let configGroupSyncOp = SyncOperation(zoneID: potatsoZoneId, objectClass: ConfigurationGroup.self, syncType: SyncType.FetchCloudChangesAndThenPushLocalChanges) { + print("sync config groups completed") + } + configGroupSyncOp.addDependency(proxySyncOp) + configGroupSyncOp.addDependency(ruleSyncOp) + configGroupSyncOp.addDependency(ruleSetSyncOp) + + operationQueue.addOperation(proxySyncOp) + operationQueue.addOperation(ruleSyncOp) + operationQueue.addOperation(ruleSetSyncOp) + operationQueue.addOperation(configGroupSyncOp) + } + +} diff --git a/Potatso/Sync/PrepareZoneOperation.swift b/Potatso/Sync/PrepareZoneOperation.swift new file mode 100644 index 0000000..c9f0e21 --- /dev/null +++ b/Potatso/Sync/PrepareZoneOperation.swift @@ -0,0 +1,61 @@ +import Foundation +import CloudKit +import PSOperations + +/** + Ensure the record zone exists before attempting to write or fetch from it. + */ +class PrepareZoneOperation: Operation { + + let zoneID: CKRecordZoneID + + init(zoneID: CKRecordZoneID) { + self.zoneID = zoneID + super.init() + name = "Prepare Zone Operation" + } + + override func execute() { + print("\(self.name!) started") + prepareCKRecordZone(self.zoneID) { (nsError) in + self.finishWithError(nsError) + } + } + + func prepareCKRecordZone(zoneID: CKRecordZoneID, completionHandler: (NSError!) -> ()) { + let privateDB = CKContainer.defaultContainer().privateCloudDatabase + // Per CloudKitCatalog, not using NSOperation here + privateDB.fetchAllRecordZonesWithCompletionHandler { + (zones, nsError) in + if nsError != nil { + print(nsError) + completionHandler(nsError) + } else if let zones = zones { + var foundZone = false + for zone in zones { + if zone.zoneID == zoneID { + foundZone = true + } + } + if foundZone { + print("Zone \(zoneID.zoneName) exists, nothing to do here.") + completionHandler(nsError) + } else { + print("Zone \(zoneID.zoneName) does not exist. Creating it now.") + // TODO: check NSUserDefault boolean value for the zoneName. + // If it exists and we are here then the user must have deleted their + // cloud data. If so we need to recreate that zone and reupload + // all user data from that zone to the cloud. To do so mark all + // live records as locally modified. Deleted records which have also + // been deleted locally will be gone forever. + privateDB.saveRecordZone(CKRecordZone(zoneID: zoneID)) { + (recordZone, nsError) in + // TODO: set a boolean NSUserDefault value equal to the + // zoneName to keep track of zones this device has previously used + completionHandler(nsError) + } + } + } + } + } +} diff --git a/Potatso/Sync/PushLocalChangesOperation.swift b/Potatso/Sync/PushLocalChangesOperation.swift new file mode 100644 index 0000000..1d6a483 --- /dev/null +++ b/Potatso/Sync/PushLocalChangesOperation.swift @@ -0,0 +1,250 @@ +import Foundation +import RealmSwift +import CloudKit +import PSOperations + +class PushLocalChangesOperation: Operation { + + let zoneID: CKRecordZoneID + var recordsToSave: [CKRecord]? + var recordIDsToDelete: [CKRecordID]? + + let delayOperationQueue = OperationQueue() + let maximumRetryAttempts: Int + var retryAttempts: Int = 0 + + let objectClass: CloudKitRecord.Type + + init(zoneID: CKRecordZoneID, objectClass: CloudKitRecord.Type, maximumRetryAttempts: Int = 3) { + self.zoneID = zoneID + self.objectClass = objectClass + self.maximumRetryAttempts = maximumRetryAttempts + + super.init() + name = "Push Local Changes" + } + + override func execute() { + print("\(self.name!) started") + + // Query records + let realm = try! Realm() + + // FIXME: Unsafe realm casting + let toSyncObjects = realm.objects(self.objectClass as! BaseModel.Type) + .filter("synced == false") + let toDeleteObjects = realm.objects(self.objectClass as! BaseModel.Type) + .filter("deleted == true") + print("toSyncObjects: \(toSyncObjects.map({ $0.uuid }).joinWithSeparator(", "))") + print("toDeleteObjects: \(toDeleteObjects.map({ $0.uuid }).joinWithSeparator(", "))") + + self.recordsToSave = toSyncObjects.map { + ($0 as! CloudKitRecord).toCloudKitRecord() + } + self.recordIDsToDelete = toDeleteObjects.map { + ($0 as! CloudKitRecord).recordId + } + + modifyRecords(self.recordsToSave, recordIDsToDelete: self.recordIDsToDelete) { + (nsError) in + self.finishWithError(nsError) + } + } + + func modifyRecords(recordsToSave: [CKRecord]?, + recordIDsToDelete: [CKRecordID]?, + completionHandler: (NSError!) -> ()) { + + let modifyOperation = CKModifyRecordsOperation( + recordsToSave: recordsToSave, + recordIDsToDelete: recordIDsToDelete) + + modifyOperation.modifyRecordsCompletionBlock = { + (savedRecords, deletedRecordIDs, nsError) -> Void in + + if let error = nsError { + + self.handleCloudKitPushError( + savedRecords, + deletedRecordIDs: deletedRecordIDs, + error: error, + completionHandler: completionHandler) + + } else { + + do { + // Update local modified flag + if let savedRecords = savedRecords { + for record in savedRecords { + // FIXME: Unsafe realm casting + try DBUtils.mark(type: self.objectClass as! BaseModel.Type, objectId: record.recordID.recordName, synced: true) + } + } + + if let recordIDsToDelete = recordIDsToDelete { + for recordID in recordIDsToDelete { + try deleteLocalRecord(recordID, objectClass: self.objectClass) + } + } + + } catch let realmError as NSError { + self.finishWithError(realmError) + } + + completionHandler(nsError) + } + } + + modifyOperation.start() + } + + // MARK: - Error Handling + + /** + Implement custom logic here for handling CloudKit push errors. + */ + func handleCloudKitPushError( + savedRecords: [CKRecord]?, + deletedRecordIDs: [CKRecordID]?, + error: NSError, + completionHandler: (NSError!) -> ()) { + + let ckErrorCode: CKErrorCode = CKErrorCode(rawValue: error.code)! + + switch ckErrorCode { + + case .PartialFailure: + self.resolvePushConflictsAndRetry( + savedRecords, + deletedRecordIDs: deletedRecordIDs, + error: error, + completionHandler: completionHandler) + + case .LimitExceeded: + self.splitModifyOperation(error, completionHandler: completionHandler) + + case .ZoneBusy, .RequestRateLimited, .ServiceUnavailable, .NetworkFailure, .NetworkUnavailable, .ResultsTruncated: + // Retry necessary + retryPush(error, + retryAfter: parseRetryTime(error), + completionHandler: completionHandler) + + case .BadDatabase, .InternalError, .BadContainer, .MissingEntitlement, + .ConstraintViolation, .IncompatibleVersion, .AssetFileNotFound, + .AssetFileModified, .InvalidArguments, .UnknownItem, + .PermissionFailure, .ServerRejectedRequest: + // Developer issue + completionHandler(error) + + case .QuotaExceeded, .OperationCancelled: + // User issue. Provide alert. + completionHandler(error) + + case .BatchRequestFailed, .ServerRecordChanged: + // Not possible for push operation (I think) only possible for + // individual records within the userInfo dictionary of a PartialFailure + completionHandler(error) + + case .NotAuthenticated: + // Handled as condition of SyncOperation + // TODO: add logic to retry entire operation + completionHandler(error) + + case .ZoneNotFound, .UserDeletedZone: + // Handled in PrepareZoneOperation. + // TODO: add logic to retry entire operation + completionHandler(error) + + case .ChangeTokenExpired: + // TODO: Determine correct handling + completionHandler(error) + } + } + + /** + In the case of a .LimitExceeded error split the CKModifyOperation in half. For simplicity, + also split the save and delete operations. + */ + func splitModifyOperation(error: NSError, completionHandler: (NSError!) -> ()) { + + if let recordsToSave = self.recordsToSave { + + if recordsToSave.count > 0 { + print("Receiving CKErrorLimitExceeded with <= 1 records.") + + let recordsToSaveLeft = Array(recordsToSave.prefixUpTo(recordsToSave.count/2)) + let recordsToSaveRight = Array(recordsToSave.suffixFrom(recordsToSave.count/2)) + + self.modifyRecords(recordsToSaveLeft, + recordIDsToDelete: nil, + completionHandler: completionHandler) + + self.modifyRecords(recordsToSaveRight, + recordIDsToDelete: nil, + completionHandler: completionHandler) + } + } + + if let recordIDsToDelete = self.recordIDsToDelete { + + if recordIDsToDelete.count > 0 { + + let recordIDsToDeleteLeft = Array(recordIDsToDelete.prefixUpTo(recordIDsToDelete.count/2)) + let recordIDsToDeleteRight = Array(recordIDsToDelete.suffixFrom(recordIDsToDelete.count/2)) + + self.modifyRecords(nil, + recordIDsToDelete: recordIDsToDeleteLeft, + completionHandler: completionHandler) + + self.modifyRecords(nil, + recordIDsToDelete: recordIDsToDeleteRight, + completionHandler: completionHandler) + } + } + } + + func resolvePushConflictsAndRetry(savedRecords: [CKRecord]?, + deletedRecordIDs: [CKRecordID]?, + error: NSError, + completionHandler: (NSError!) -> ()) { + + let adjustedRecords = resolveConflicts(error, + completionHandler: completionHandler, + resolver: overwriteFromClient) + + modifyRecords(adjustedRecords, recordIDsToDelete: deletedRecordIDs, completionHandler: completionHandler) + } + + // MARK: - Retry + + // Wait a default of 3 seconds + func parseRetryTime(error: NSError) -> Double { + var retrySecondsDouble: Double = 3 + if let retrySecondsString = error.userInfo[CKErrorRetryAfterKey] as? String { + retrySecondsDouble = Double(retrySecondsString)! + } + return retrySecondsDouble + } + + /** + After `maximumRetryAttempts` this function will return an error. + */ + func retryPush(error: NSError, retryAfter: Double, completionHandler: (NSError!) -> ()) { + + if self.retryAttempts < self.maximumRetryAttempts { + self.retryAttempts += 1 + + let delayOperation = DelayOperation(interval: retryAfter) + let finishObserver = BlockObserver { operation, error in + self.modifyRecords(self.recordsToSave, + recordIDsToDelete: self.recordIDsToDelete, + completionHandler: completionHandler) + } + delayOperation.addObserver(finishObserver) + delayOperationQueue.addOperation(delayOperation) + + } else { + completionHandler(error) + } + } +} \ No newline at end of file diff --git a/Potatso/Sync/RealmCloud.swift b/Potatso/Sync/RealmCloud.swift new file mode 100644 index 0000000..80c31ca --- /dev/null +++ b/Potatso/Sync/RealmCloud.swift @@ -0,0 +1,203 @@ +import Foundation +import CloudKit +import RealmSwift + + +// MARK: - CloudKit related functions + +/** + Set the `changeToken` for this `zoneID`. + */ +public func setZoneChangeToken(zoneID: CKRecordZoneID, changeToken: CKServerChangeToken?) { + if let changeToken = changeToken { + NSUserDefaults.standardUserDefaults().setObject( + NSKeyedArchiver.archivedDataWithRootObject(changeToken), + forKey:"\(zoneID.zoneName)_serverChangeToken") + NSUserDefaults.standardUserDefaults().synchronize() + } +} + +/** + Get the local change token for this `zoneID` if one exists. + */ +public func getZoneChangeToken(zoneID: CKRecordZoneID) -> CKServerChangeToken? { + let encodedObjectData = NSUserDefaults.standardUserDefaults().objectForKey("\(zoneID.zoneName)_serverChangeToken") as? NSData + var decodedData: CKServerChangeToken? = nil + if encodedObjectData != nil { + decodedData = NSKeyedUnarchiver.unarchiveObjectWithData(encodedObjectData!) as? CKServerChangeToken + } + return decodedData +} + +/** + Archive the CKRecord to the local database. This data will be used next time + the updated record is send to CloudKit. + */ +public func recordToLocalData(record: CKRecord) -> NSData { + // Archive CKRecord into NSMutableData + let archivedData = NSMutableData() + let archiver = NSKeyedArchiver(forWritingWithMutableData: archivedData) + archiver.requiresSecureCoding = true + record.encodeSystemFieldsWithCoder(archiver) + archiver.finishEncoding() + return archivedData +} + +// MARK: - Local database modification + + +// MARK: - Conflict resolution + +/** + Resolve conflicts between two CKRecords. + + Best practice is to perform desired changes on server record and then resend. + */ +public func resolveConflicts(error: NSError, + completionHandler: (NSError!) -> (), + resolver: (CKRecord, serverRecord: CKRecord) -> CKRecord) -> [CKRecord]? { + + var adjustedRecords = [CKRecord]() + + if let errorDict = error.userInfo[CKPartialErrorsByItemIDKey] + as? [CKRecordID : NSError] { + + for (_, partialError) in errorDict { + let errorCode = CKErrorCode(rawValue: partialError.code) + if errorCode == .ServerRecordChanged { + let userInfo = partialError.userInfo + + guard let serverRecord = userInfo[CKRecordChangedErrorServerRecordKey] as? CKRecord, + ancestorRecord = userInfo[CKRecordChangedErrorAncestorRecordKey] as? CKRecord, + clientRecord = userInfo[CKRecordChangedErrorClientRecordKey] as? CKRecord else { + + completionHandler(error) + // TODO: correctly handle error here + return nil + } + + print("Client change tag: \(clientRecord.recordChangeTag)") + print("Server change tag: \(serverRecord.recordChangeTag)") + print("Ancestor change tag: \(ancestorRecord.recordChangeTag)") + + if serverRecord.recordChangeTag != clientRecord.recordChangeTag { + + print("Client text: \(clientRecord["text"])") + print("Server text: \(serverRecord["text"])") + + let adjustedRecord = resolver( + clientRecord, + serverRecord: serverRecord) + + print("Adjusted text: \(adjustedRecord["text"])") + + adjustedRecords.append(adjustedRecord) + + } else { + completionHandler(error) + } + } + } + } + return adjustedRecords +} + +public func overwriteFromClient(clientRecord: CKRecord, serverRecord: CKRecord) -> CKRecord { + let adjustedRecord = serverRecord + for key in clientRecord.allKeys() { + adjustedRecord[key] = clientRecord[key] + } + return adjustedRecord +} + + +/** + TODO: implement diff3 conflict resolution using client, server and ancestor records. + + https://opensource.apple.com/source/gnudiff/gnudiff-10/diffutils/diff3.c + */ + +// MARK: - Helpers + +// I can't figure out how to extend realm errors so I'm creating a custom swift error code. +public enum CustomRealmErrorCode: Int { + case Fail + case FileAccess + case FilePermissionDenied + case FileExists + case FileNotFound + case FileFormatUpgradeRequired + case IncompatibleLockFile + case AddressSpaceExhausted + case SchemaMismatch +} + +public func createAlertOperation(error: NSError) -> AlertOperation { + let alert = AlertOperation() + + switch error.domain { + case "io.realm": + var errorString = "Unknown" + + if let rlmErrorCode = CustomRealmErrorCode(rawValue: error.code) { + errorString = String(rlmErrorCode) + } + + alert.title = "Write Error" + alert.message = + "Cannot write data to iPhone." + + "\n\n" + + "Error Code: RLMError.\(errorString)" + + case CKErrorDomain: + let ckErrorCode: CKErrorCode = CKErrorCode(rawValue: error.code)! + + alert.title = "Cloud Error" + alert.message = + "Cannot complete sync operation. Try again later." + + "\n\n" + + "Error Code: CKError.\(String(ckErrorCode))" + + default: + alert.title = "Error" + alert.message = "Cannot complete sync operation. Try again later." + } + + return alert +} + +// Extend `CKErrorCode` to provide more descriptive errors to user. +extension CKErrorCode: CustomStringConvertible { + public var description: String { + switch self { + case InternalError: return "InternalError" + case PartialFailure: return "PartialFailure" + case NetworkUnavailable: return "NetworkUnavailable" + case NetworkFailure: return "NetworkFailure" + case BadContainer: return "BadContainer" + case ServiceUnavailable: return "ServiceUnavailable" + case RequestRateLimited: return "RequestRateLimited" + case MissingEntitlement: return "MissingEntitlement" + case NotAuthenticated: return "NotAuthenticated" + case PermissionFailure: return "PermissionFailure" + case UnknownItem: return "UnknownItem" + case InvalidArguments: return "InvalidArguments" + case ResultsTruncated: return "ResultsTruncated" + case ServerRecordChanged: return "ServerRecordChanged" + case ServerRejectedRequest: return "ServerRejectedRequest" + case AssetFileNotFound: return "AssetFileNotFound" + case AssetFileModified: return "AssetFileModified" + case IncompatibleVersion: return "IncompatibleVersion" + case ConstraintViolation: return "ConstraintViolation" + case OperationCancelled: return "OperationCancelled" + case ChangeTokenExpired: return "ChangeTokenExpired" + case BatchRequestFailed: return "BatchRequestFailed" + case ZoneBusy: return "ZoneBusy" + case BadDatabase: return "BadDatabase" + case QuotaExceeded: return "QuotaExceeded" + case ZoneNotFound: return "ZoneNotFound" + case LimitExceeded: return "LimitExceeded" + case UserDeletedZone: return "UserDeletedZone" + } + } +} diff --git a/Potatso/Sync/SyncManager.swift b/Potatso/Sync/SyncManager.swift index 5cdf543..4ba941b 100644 --- a/Potatso/Sync/SyncManager.swift +++ b/Potatso/Sync/SyncManager.swift @@ -16,6 +16,7 @@ public enum SyncServiceType: String { public protocol SyncServiceProtocol { func setup(completion: (ErrorType? -> Void)?) + func sync() } public class SyncManager { @@ -70,5 +71,9 @@ extension SyncManager { public func setup(completion: (ErrorType? -> Void)?) { getSelectedSyncService()?.setup(completion) } + + public func sync() { + getSelectedSyncService()?.sync() + } } \ No newline at end of file diff --git a/Potatso/Sync/SyncOperation.swift b/Potatso/Sync/SyncOperation.swift new file mode 100644 index 0000000..bc263a8 --- /dev/null +++ b/Potatso/Sync/SyncOperation.swift @@ -0,0 +1,138 @@ +import Foundation +import RealmSwift +import CloudKit +import PSOperations + +public enum SyncType: CustomStringConvertible { + case PushLocalChanges + case FetchCloudChanges + case FetchCloudChangesAndThenPushLocalChanges + + public var description : String { + switch self { + case .PushLocalChanges: + return "Push Local Changes" + case .FetchCloudChanges: + return "Fetch Cloud Changes" + case .FetchCloudChangesAndThenPushLocalChanges: + return "Fetch Cloud Changes And Then Push Local Changes" + } + } +} + +/** + Sync local realm database with CloudKit. + + Required Conditions: + - Reachability + - iCloud + + Order of Operations: + - PrepareZoneOperation + - PushLocalChangesOperation + - FetchCloudChangesOperation + - completionHandler + + */ +public class SyncOperation: GroupOperation { + + private var hasProducedAlert = false + + public init(zoneID: CKRecordZoneID, + objectClass: CloudKitRecord.Type, + syncType: SyncType, + completionHandler: () -> Void) { + + let zoneChangeToken = getZoneChangeToken(zoneID) + + // Setup Conditions + + // ReachabilityCondition as written requires a URL so rather than rewriting, ping google + let url = NSURL(string: "http://www.apple.com")! + let reachabilityCondition = ReachabilityCondition(host: url) + + let container = CKContainer.defaultContainer() + let iCloudCapability = Capability(iCloudContainer(container: container)) + + // Setup common operations + let prepareZoneOperation = PrepareZoneOperation(zoneID: zoneID) + let finishOperation = NSBlockOperation(block: completionHandler) + + prepareZoneOperation.addCondition(reachabilityCondition) + prepareZoneOperation.addCondition(iCloudCapability) + + // Setup operations relating to SyncType + let pushLocalChangesOperation: PushLocalChangesOperation + let fetchCloudChangesOperation: FetchCloudChangesOperation + let operations: [NSOperation] + + switch syncType { + case .PushLocalChanges: + pushLocalChangesOperation = PushLocalChangesOperation(zoneID: zoneID, + objectClass: objectClass) + + pushLocalChangesOperation.addDependency(prepareZoneOperation) + finishOperation.addDependency(pushLocalChangesOperation) + + operations = [ + prepareZoneOperation, + pushLocalChangesOperation, + finishOperation] + + case .FetchCloudChanges: + fetchCloudChangesOperation = FetchCloudChangesOperation(zoneID: zoneID, objectClass: objectClass, previousServerChangeToken: zoneChangeToken) + + fetchCloudChangesOperation.addDependency(prepareZoneOperation) + finishOperation.addDependency(fetchCloudChangesOperation) + + operations = [ + prepareZoneOperation, + fetchCloudChangesOperation, + finishOperation] + + case .FetchCloudChangesAndThenPushLocalChanges: + pushLocalChangesOperation = PushLocalChangesOperation(zoneID: zoneID, objectClass: objectClass) + fetchCloudChangesOperation = FetchCloudChangesOperation(zoneID: zoneID, objectClass: objectClass, previousServerChangeToken: zoneChangeToken) + + pushLocalChangesOperation.addDependency(prepareZoneOperation) + fetchCloudChangesOperation.addDependency(pushLocalChangesOperation) + finishOperation.addDependency(fetchCloudChangesOperation) + + operations = [ + prepareZoneOperation, + fetchCloudChangesOperation, + pushLocalChangesOperation, + finishOperation] + } + + super.init(operations: operations) + + name = "Sync \(syncType)" + } + + override public func finished(errors: [NSError]) { + if self.cancelled { + print("Sync operation was cancelled.") + } + } + + override public func operationDidFinish(operation: NSOperation, withErrors errors: [NSError]) { + if let firstError = errors.first { + produceAlert(firstError) + } else { + if let name = operation.name { + print(" \(name) finished") + } + } + } + + private func produceAlert(error: NSError) { + /* + We only want to show the first error, since subsequent errors might + be caused by the first. + */ + if hasProducedAlert { return } + produceOperation(createAlertOperation(error)) + hasProducedAlert = true + } +} diff --git a/Potatso/Utils/KeychainUtils.swift b/Potatso/Utils/KeychainUtils.swift new file mode 100644 index 0000000..aa6a968 --- /dev/null +++ b/Potatso/Utils/KeychainUtils.swift @@ -0,0 +1,12 @@ +// +// KeychainUtils.swift +// Potatso +// +// Created by LEI on 8/3/16. +// Copyright © 2016 TouchingApp. All rights reserved. +// + +import Foundation +import KeychainAccess + +let keychain = Keychain(service: "com.touchingapp.potatso") diff --git a/Potatso/Utils/Receipt.swift b/Potatso/Utils/Receipt.swift index bbd5515..122d30d 100644 --- a/Potatso/Utils/Receipt.swift +++ b/Potatso/Utils/Receipt.swift @@ -8,13 +8,11 @@ import Foundation import ICSMainFramework -import KeychainAccess class Receipt: NSObject, SKRequestDelegate { static let shared = Receipt() - private let keychain = Keychain(service: "com.touchingapp.potatso") private override init() {} diff --git a/PotatsoModel/BaseModel.swift b/PotatsoModel/BaseModel.swift index 50fa45a..72994d0 100644 --- a/PotatsoModel/BaseModel.swift +++ b/PotatsoModel/BaseModel.swift @@ -10,7 +10,7 @@ import RealmSwift import PotatsoBase import CloudKit -private let version: UInt64 = 11 +private let version: UInt64 = 13 public var defaultRealm = try! Realm() public func setupDefaultReaml() { @@ -32,17 +32,13 @@ public func setupDefaultReaml() { Realm.Configuration.defaultConfiguration = config } -protocol CloudKitRecord { - static var recordType: String { get } - var recordId: CKRecordID { get } - func toCloudKitRecord() -> CKRecord -} public class BaseModel: Object { public dynamic var uuid = NSUUID().UUIDString public dynamic var createAt = NSDate().timeIntervalSince1970 public dynamic var updatedAt = NSDate().timeIntervalSince1970 public dynamic var deleted = false + public dynamic var synced = false override public static func primaryKey() -> String? { return "uuid" @@ -54,10 +50,8 @@ public class BaseModel: Object { return f } - func fillInRecord(record: CKRecord) { - for key in ["uuid", "createAt", "updatedAt", "deleted"] { - record.setValue(self.valueForKey(key), forKey: key) - } + public func validate(inRealm realm: Realm) throws { + // } } @@ -75,4 +69,4 @@ extension Results { try defaultRealm.commitWrite() } -} \ No newline at end of file +} diff --git a/PotatsoModel/ConfigurationGroup.swift b/PotatsoModel/ConfigurationGroup.swift index 9069e17..8dab31f 100644 --- a/PotatsoModel/ConfigurationGroup.swift +++ b/PotatsoModel/ConfigurationGroup.swift @@ -42,11 +42,19 @@ public class ConfigurationGroup: BaseModel { return ["name"] } - public func validate(inRealm realm: Realm, includeSelf: Bool = false) throws { + public override func validate(inRealm realm: Realm) throws { guard name.characters.count > 0 else { throw ConfigurationGroupError.EmptyName } - guard realm.objects(ConfigurationGroup).filter("name = '\(name)'").count <= (includeSelf ? 1 : 0) else { + let count = realm.objects(ConfigurationGroup).filter("name = '\(name)'").count + if count == 1 { + let g = realm.objects(ConfigurationGroup).filter("name = '\(name)'").first! + if g.uuid != uuid { + throw ConfigurationGroupError.NameAlreadyExists + } + } else if count == 0 { + + } else { throw ConfigurationGroupError.NameAlreadyExists } } @@ -92,23 +100,6 @@ extension ConfigurationGroup { } -// API -extension ConfigurationGroup { - - public func changeName(name: String) throws { - defaultRealm.beginWrite() - self.name = name - do { - try validate(inRealm: defaultRealm, includeSelf: true) - }catch { - defaultRealm.cancelWrite() - throw error - } - try defaultRealm.commitWrite() - } - -} - public func ==(lhs: ConfigurationGroup, rhs: ConfigurationGroup) -> Bool { return lhs.uuid == rhs.uuid } \ No newline at end of file diff --git a/PotatsoModel/DBUtils.swift b/PotatsoModel/DBUtils.swift new file mode 100644 index 0000000..eba1fdd --- /dev/null +++ b/PotatsoModel/DBUtils.swift @@ -0,0 +1,147 @@ +// +// DBUtils.swift +// Potatso +// +// Created by LEI on 8/3/16. +// Copyright © 2016 TouchingApp. All rights reserved. +// + +import Foundation +import Realm +import RealmSwift + +public class DBUtils { + + public static func add(object: BaseModel, update: Bool = true) throws { + let realm = try! Realm() + realm.beginWrite() + object.setModified() + realm.add(object, update: update) + try realm.commitWrite() + } + + public static func add(objects: S, update: Bool = true) throws { + let realm = try! Realm() + realm.beginWrite() + objects.forEach({ + $0.setModified() + }) + realm.add(objects, update: update) + try realm.commitWrite() + } + + public static func delete(object: BaseModel, update: Bool = true) throws { + let realm = try! Realm() + realm.beginWrite() + object.deleted = true + object.setModified() + try realm.commitWrite() + } + + public static func delete(objects: S, update: Bool = true) throws { + let realm = try! Realm() + realm.beginWrite() + objects.forEach({ + $0.deleted = true + $0.setModified() + }) + try realm.commitWrite() + } + + public static func mark(object: BaseModel, synced: Bool) throws { + let realm = try! Realm() + realm.beginWrite() + object.synced = synced + try realm.commitWrite() + } + + public static func mark(type type: BaseModel.Type, objectId: String, synced: Bool) throws { + let realm = try! Realm() + guard let object = realm.objects(type).filter("uuid = '\(objectId)'").first else { + return + } + try mark(object, synced: synced) + } + +} + + +// Query +extension DBUtils { + + public static func get(uuid: String, inRealm realm: Realm? = nil) -> T? { + var mRealm = realm + if mRealm == nil { + mRealm = try! Realm() + } + return mRealm?.objects(T).filter("uuid = '\(uuid)'").first + } + + public static func modify(type: T.Type, id: String, modifyBlock: ((Realm, T) -> ErrorType?)) throws { + let realm = try! Realm() + guard let object: T = DBUtils.get(id, inRealm: realm) else { + return + } + realm.beginWrite() + if let error = modifyBlock(realm, object) { + throw error + } + do { + try object.validate(inRealm: realm) + }catch { + realm.cancelWrite() + throw error + } + try realm.commitWrite() + } + +} + +// BaseModel API +extension BaseModel { + + func setModified() { + updatedAt = NSDate().timeIntervalSince1970 + synced = false + } + +} + + +// Config Group API +extension ConfigurationGroup { + + public static func changeProxy(forGroupId groupId: String, proxyId: String?) throws { + try DBUtils.modify(ConfigurationGroup.self, id: groupId) { (realm, group) -> ErrorType? in + group.proxies.removeAll() + if let proxyId = proxyId, proxy: Proxy = DBUtils.get(proxyId, inRealm: realm){ + group.proxies.append(proxy) + } + return nil + } + } + + public static func appendRuleSet(forGroupId groupId: String, rulesetId: String) throws { + try DBUtils.modify(ConfigurationGroup.self, id: groupId) { (realm, group) -> ErrorType? in + if let ruleset: RuleSet = DBUtils.get(rulesetId, inRealm: realm) { + group.ruleSets.append(ruleset) + } + return nil + } + } + + public static func changeDNS(forGroupId groupId: String, dns: String?) throws { + try DBUtils.modify(ConfigurationGroup.self, id: groupId) { (realm, group) -> ErrorType? in + group.dns = dns ?? "" + return nil + } + } + + public static func changeName(forGroupId groupId: String, name: String) throws { + try DBUtils.modify(ConfigurationGroup.self, id: groupId) { (realm, group) -> ErrorType? in + group.name = name + return nil + } + } + +} diff --git a/PotatsoModel/Proxy.swift b/PotatsoModel/Proxy.swift index 9f8ec92..a55b0b1 100644 --- a/PotatsoModel/Proxy.swift +++ b/PotatsoModel/Proxy.swift @@ -117,7 +117,11 @@ public class Proxy: BaseModel { "chacha20-ietf" ] - public func validate(inRealm realm: Realm) throws { + public override static func indexedProperties() -> [String] { + return ["name"] + } + + public override func validate(inRealm realm: Realm) throws { guard let _ = ProxyType(rawValue: typeRaw)else { throw ProxyError.InvalidType } @@ -145,27 +149,7 @@ public class Proxy: BaseModel { } -extension Proxy: CloudKitRecord { - - static var recordType: String { - return "Proxy" - } - - var recordId: CKRecordID { - return CKRecordID(recordName: uuid) - } - - public func toCloudKitRecord() -> CKRecord { - let record = CKRecord(recordType: Proxy.recordType, recordID: recordId) - fillInRecord(record) - for key in ["typeRaw", "name", "host", "port", "authscheme", "user", "password", "ota", "ssrProtocol", "ssrObfs", "ssrObfsParam"] { - record.setValue(self.valueForKey(key), forKey: key) - } - return record - } - -} - +// Public Accessor extension Proxy { public var type: ProxyType { @@ -177,12 +161,31 @@ extension Proxy { } } - public override static func indexedProperties() -> [String] { - return ["name"] + public var uri: String { + switch type { + case .Shadowsocks: + if let authscheme = authscheme, password = password { + return "ss://\(authscheme):\(password)@\(host):\(port)" + } + default: + break + } + return "" + } + public override var description: String { + return name } } +// API +extension Proxy { + + + +} + +// Import extension Proxy { public convenience init(dictionary: [String: AnyObject], inRealm realm: Realm) throws { @@ -312,26 +315,6 @@ extension Proxy { return uri.lowercaseString.hasPrefix(Proxy.ssUriPrefix) || uri.lowercaseString.hasPrefix(Proxy.ssrUriPrefix) } - - -} - -extension Proxy { - - public var uri: String { - switch type { - case .Shadowsocks: - if let authscheme = authscheme, password = password { - return "ss://\(authscheme):\(password)@\(host):\(port)" - } - default: - break - } - return "" - } - public override var description: String { - return name - } } public func ==(lhs: Proxy, rhs: Proxy) -> Bool { diff --git a/PotatsoModel/RuleSet.swift b/PotatsoModel/RuleSet.swift index 363e484..6982bc6 100644 --- a/PotatsoModel/RuleSet.swift +++ b/PotatsoModel/RuleSet.swift @@ -39,7 +39,7 @@ public final class RuleSet: BaseModel { public dynamic var isSubscribe = false public dynamic var isOfficial = false - public func validate(inRealm realm: Realm) throws { + public override func validate(inRealm realm: Realm) throws { guard name.characters.count > 0 else { throw RuleSetError.EmptyName } From 363f7dd313267b05f907bbe1b32323e858ed71c5 Mon Sep 17 00:00:00 2001 From: iCodesign Date: Sat, 6 Aug 2016 18:02:05 +0800 Subject: [PATCH 4/8] Add sync --- Potatso.xcodeproj/project.pbxproj | 16 ++ Potatso/Base/ActionRow.swift | 29 +++ Potatso/Core/FeedbackManager.swift | 40 +++ Potatso/More/SettingsViewController.swift | 245 ++++++++---------- Potatso/Sync/FetchCloudChangesOperation.swift | 7 +- Potatso/Sync/ICloudSetupOperation.swift | 34 +++ Potatso/Sync/ICloudSyncService.swift | 8 +- Potatso/Sync/PushLocalChangesOperation.swift | 5 +- Potatso/Sync/RealmCloud.swift | 4 +- Potatso/Sync/SyncManager.swift | 36 ++- Potatso/Sync/SyncOperation.swift | 8 +- Potatso/Sync/SyncVC.swift | 72 +++++ PotatsoModel/BaseModel.swift | 7 +- 13 files changed, 345 insertions(+), 166 deletions(-) create mode 100644 Potatso/Base/ActionRow.swift create mode 100644 Potatso/Core/FeedbackManager.swift create mode 100644 Potatso/Sync/ICloudSetupOperation.swift create mode 100644 Potatso/Sync/SyncVC.swift diff --git a/Potatso.xcodeproj/project.pbxproj b/Potatso.xcodeproj/project.pbxproj index 9a54917..cc6b6d6 100644 --- a/Potatso.xcodeproj/project.pbxproj +++ b/Potatso.xcodeproj/project.pbxproj @@ -155,6 +155,9 @@ B803A6961D0165EA003EA9AA /* CloudViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B803A6951D0165EA003EA9AA /* CloudViewController.swift */; }; B803A6981D02B768003EA9AA /* API.swift in Sources */ = {isa = PBXBuildFile; fileRef = B803A6971D02B768003EA9AA /* API.swift */; }; B821B0F01D51DD8F0061E7B9 /* KeychainUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = B821B0EF1D51DD8F0061E7B9 /* KeychainUtils.swift */; }; + B821B0F31D5334D50061E7B9 /* ActionRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = B821B0F21D5334D50061E7B9 /* ActionRow.swift */; }; + B821B0F51D5335DA0061E7B9 /* FeedbackManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B821B0F41D5335DA0061E7B9 /* FeedbackManager.swift */; }; + B821B0F71D539CFD0061E7B9 /* SyncVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B821B0F61D539CFD0061E7B9 /* SyncVC.swift */; }; B82574A91D1D98CF007BAF40 /* Pollution.swift in Sources */ = {isa = PBXBuildFile; fileRef = B82574A81D1D98CF007BAF40 /* Pollution.swift */; }; B829C1721D4395BC00C17B82 /* QRCodeScannerVC.m in Sources */ = {isa = PBXBuildFile; fileRef = B829C1711D4395BC00C17B82 /* QRCodeScannerVC.m */; }; B8319A0A1D1B975C001E50C2 /* RegexUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8319A091D1B975C001E50C2 /* RegexUtils.swift */; }; @@ -183,6 +186,7 @@ B88559EC1D21319D00B1243E /* YAML.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B88559EA1D21319D00B1243E /* YAML.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; B88559F01D21371A00B1243E /* PotatsoModel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9BC6FFEA1CB28B4B00E5EA61 /* PotatsoModel.framework */; }; B88874491D18186100AEF002 /* ShadowPath.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B86B08E91D17F84900613014 /* ShadowPath.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + B896A8401D548AAC009E4BF5 /* ICloudSetupOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = B896A83F1D548AAC009E4BF5 /* ICloudSetupOperation.swift */; }; B8A09D691D51B42B00A9A989 /* CloudKitRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8A09D681D51B42B00A9A989 /* CloudKitRecord.swift */; }; B8A09D7B1D51B9A900A9A989 /* AlertOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8A09D741D51B9A900A9A989 /* AlertOperation.swift */; }; B8A09D7C1D51B9A900A9A989 /* FetchCloudChangesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8A09D751D51B9A900A9A989 /* FetchCloudChangesOperation.swift */; }; @@ -848,6 +852,9 @@ B803A6951D0165EA003EA9AA /* CloudViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudViewController.swift; sourceTree = ""; }; B803A6971D02B768003EA9AA /* API.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = API.swift; sourceTree = ""; }; B821B0EF1D51DD8F0061E7B9 /* KeychainUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeychainUtils.swift; sourceTree = ""; }; + B821B0F21D5334D50061E7B9 /* ActionRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActionRow.swift; sourceTree = ""; }; + B821B0F41D5335DA0061E7B9 /* FeedbackManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedbackManager.swift; sourceTree = ""; }; + B821B0F61D539CFD0061E7B9 /* SyncVC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncVC.swift; sourceTree = ""; }; B82574A81D1D98CF007BAF40 /* Pollution.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Pollution.swift; sourceTree = ""; }; B829C1701D4395BC00C17B82 /* QRCodeScannerVC.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QRCodeScannerVC.h; sourceTree = ""; }; B829C1711D4395BC00C17B82 /* QRCodeScannerVC.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QRCodeScannerVC.m; sourceTree = ""; }; @@ -875,6 +882,7 @@ B8822CD21D2B81C400AD252C /* libssl.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libssl.a; path = "Library/ShadowPath/ShadowPath/shadowsocks-libev/libopenssl/lib/libssl.a"; sourceTree = ""; }; B8822CD51D2B861E00AD252C /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; }; B88559EA1D21319D00B1243E /* YAML.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = YAML.framework; path = Carthage/Build/iOS/YAML.framework; sourceTree = ""; }; + B896A83F1D548AAC009E4BF5 /* ICloudSetupOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ICloudSetupOperation.swift; sourceTree = ""; }; B8A09D681D51B42B00A9A989 /* CloudKitRecord.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudKitRecord.swift; sourceTree = ""; }; B8A09D741D51B9A900A9A989 /* AlertOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlertOperation.swift; sourceTree = ""; }; B8A09D751D51B9A900A9A989 /* FetchCloudChangesOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchCloudChangesOperation.swift; sourceTree = ""; }; @@ -1301,6 +1309,7 @@ 9B1F74751C3164AC0028C1A6 /* VPN.swift */, 9B3247891CC0F1D200A3BAFF /* Importer.swift */, B803A6971D02B768003EA9AA /* API.swift */, + B821B0F41D5335DA0061E7B9 /* FeedbackManager.swift */, ); path = Core; sourceTree = ""; @@ -1973,6 +1982,7 @@ isa = PBXGroup; children = ( B8D8CC121D50D5DB00CE6C0D /* ICloudSyncService.swift */, + B896A83F1D548AAC009E4BF5 /* ICloudSetupOperation.swift */, B8A09D681D51B42B00A9A989 /* CloudKitRecord.swift */, B8A09D741D51B9A900A9A989 /* AlertOperation.swift */, B8A09D751D51B9A900A9A989 /* FetchCloudChangesOperation.swift */, @@ -1990,6 +2000,7 @@ B8CCC6EA1CFF1501000E7E2E /* ProxyRow.swift */, B87B98091D3B64BE00FA66BF /* RequestEventRow.swift */, B8367A801D1B6D5400D50C25 /* BaseButtonRow.swift */, + B821B0F21D5334D50061E7B9 /* ActionRow.swift */, ); name = Row; sourceTree = ""; @@ -2017,6 +2028,7 @@ isa = PBXGroup; children = ( B8D8CC101D50D5CD00CE6C0D /* SyncManager.swift */, + B821B0F61D539CFD0061E7B9 /* SyncVC.swift */, B821B0F11D5318F80061E7B9 /* iCloud */, ); path = Sync; @@ -2862,6 +2874,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + B821B0F31D5334D50061E7B9 /* ActionRow.swift in Sources */, B8D8CC111D50D5CD00CE6C0D /* SyncManager.swift in Sources */, B83AA5701D38E6F7007905B4 /* RequestDetailVC.swift in Sources */, B83D3E7D1D2BA8C8007655CE /* Receipt.swift in Sources */, @@ -2873,6 +2886,7 @@ B8C256AE1D1A93DA0074D3B1 /* FlatButton.swift in Sources */, 9B8193ED1CBE4DCA00BE320D /* UrlHandler.swift in Sources */, 9B1F74761C3164AD0028C1A6 /* VPN.swift in Sources */, + B821B0F51D5335DA0061E7B9 /* FeedbackManager.swift in Sources */, B8A09D7B1D51B9A900A9A989 /* AlertOperation.swift in Sources */, 9B8750551CC761D000A11715 /* RequestModel.swift in Sources */, B87A043A1D193ABC001132F2 /* LoggerUtils.swift in Sources */, @@ -2919,9 +2933,11 @@ 9B76EEB71C90740C002BF5D1 /* RuleSetsSelectionViewController.swift in Sources */, B83AA5741D38E728007905B4 /* SegmentPageVC.swift in Sources */, 9BB3F74F1CC6308000C2DD05 /* RecentRequestsVC.swift in Sources */, + B821B0F71D539CFD0061E7B9 /* SyncVC.swift in Sources */, B8A09D811D51B9A900A9A989 /* SyncOperation.swift in Sources */, 9B8285481CE20DE40027D15C /* HMScanner.m in Sources */, B8A09D7C1D51B9A900A9A989 /* FetchCloudChangesOperation.swift in Sources */, + B896A8401D548AAC009E4BF5 /* ICloudSetupOperation.swift in Sources */, B8A09D691D51B42B00A9A989 /* CloudKitRecord.swift in Sources */, 9BB3F7441CC60B6F00C2DD05 /* Error.swift in Sources */, 9B76EEAD1C9005D2002BF5D1 /* RuleConfigurationViewController.swift in Sources */, diff --git a/Potatso/Base/ActionRow.swift b/Potatso/Base/ActionRow.swift new file mode 100644 index 0000000..4c4d065 --- /dev/null +++ b/Potatso/Base/ActionRow.swift @@ -0,0 +1,29 @@ +// +// ActionRow.swift +// Potatso +// +// Created by LEI on 8/4/16. +// Copyright © 2016 TouchingApp. All rights reserved. +// + +import Foundation +import Eureka + +public final class ActionRow: _LabelRow, RowType { + + public required init(tag: String?) { + super.init(tag: tag) + } + + public override func updateCell() { + super.updateCell() + cell.selectionStyle = .Default + cell.accessoryType = .DisclosureIndicator + } + + public override func didSelect() { + super.didSelect() + cell.setSelected(false, animated: true) + } + +} \ No newline at end of file diff --git a/Potatso/Core/FeedbackManager.swift b/Potatso/Core/FeedbackManager.swift new file mode 100644 index 0000000..56760e6 --- /dev/null +++ b/Potatso/Core/FeedbackManager.swift @@ -0,0 +1,40 @@ +// +// FeedbackManager.swift +// Potatso +// +// Created by LEI on 8/4/16. +// Copyright © 2016 TouchingApp. All rights reserved. +// + +import Foundation +import ICSMainFramework + +class FeedbackManager { + static let shared = FeedbackManager() + + func showFeedback(inVC vc: UIViewController? = nil) { + guard let currentVC = vc ?? UIApplication.sharedApplication().keyWindow?.rootViewController else { + return + } + let options = [ + "gotoConversationAfterContactUs": "YES" + ] + let rulesets = Manager.sharedManager.defaultConfigGroup.ruleSets.map({ $0.name }).joinWithSeparator(", ") + let defaultToProxy = Manager.sharedManager.defaultConfigGroup.defaultToProxy + var tags: [String] = [] + if AppEnv.isTestFlight { + tags.append("testflight") + } else if AppEnv.isAppStore { + tags.append("store") + } + HelpshiftSupport.setMetadataBlock { () -> [NSObject : AnyObject]! in + return [ + "Full Version": AppEnv.fullVersion, + "Default To Proxy": defaultToProxy ? "true": "false", + "Rulesets": rulesets, + HelpshiftSupportTagsKey: tags + ] + } + HelpshiftSupport.showConversation(currentVC, withOptions: options) + } +} \ No newline at end of file diff --git a/Potatso/More/SettingsViewController.swift b/Potatso/More/SettingsViewController.swift index ed7453b..186d5ca 100644 --- a/Potatso/More/SettingsViewController.swift +++ b/Potatso/More/SettingsViewController.swift @@ -32,160 +32,133 @@ class SettingsViewController: FormViewController, MFMailComposeViewControllerDel override func viewDidLoad() { super.viewDidLoad() navigationItem.title = "More".localized() + } + + override func viewWillAppear(animated: Bool) { + super.viewWillAppear(animated) generateForm() } - + func generateForm() { - form +++ Section() - <<< LabelRow() { + form.delegate = nil + form.removeAll() + form +++ generateManualSection() + form +++ generateSyncSection() + form +++ generateRateSection() + form +++ generateAboutSection() + form.delegate = self + tableView?.reloadData() + } + + func generateManualSection() -> Section { + let section = Section() + section + <<< ActionRow { + $0.title = "User Manual".localized() + }.onCellSelection({ [unowned self] (cell, row) in + self.showUserManual() + }) + <<< ActionRow { $0.title = "Feedback".localized() - }.cellSetup({ (cell, row) -> () in - cell.selectionStyle = .Default - cell.accessoryType = .DisclosureIndicator - }).onCellSelection({ [unowned self] (cell, row) -> () in - cell.setSelected(false, animated: true) - self.feedback() - }) - +++ Section() - <<< LabelRow() { + }.onCellSelection({ (cell, row) in + FeedbackManager.shared.showFeedback() + }) + return section + } + + func generateSyncSection() -> Section { + let section = Section() + section + <<< ActionRow() { + $0.title = "Sync".localized() + $0.value = SyncManager.shared.currentSyncServiceType.rawValue + }.onCellSelection({ [unowned self] (cell, row) -> () in + SyncManager.shared.showSyncVC(inVC: self) + }) + <<< ActionRow() { $0.title = "Import From URL".localized() - }.cellSetup({ (cell, row) -> () in - cell.selectionStyle = .Default - cell.accessoryType = .DisclosureIndicator - }).onCellSelection({ (cell, row) -> () in - cell.setSelected(false, animated: true) - let importer = Importer(vc: self) - importer.importConfigFromUrl() - }) - <<< LabelRow() { + }.onCellSelection({ [unowned self] (cell, row) -> () in + let importer = Importer(vc: self) + importer.importConfigFromUrl() + }) + <<< ActionRow() { $0.title = "Import From QRCode".localized() - }.cellSetup({ (cell, row) -> () in - cell.selectionStyle = .Default - cell.accessoryType = .DisclosureIndicator - }).onCellSelection({ (cell, row) -> () in - cell.setSelected(false, animated: true) - let importer = Importer(vc: self) - importer.importConfigFromQRCode() - }) - +++ Section() - <<< ButtonRow() { - $0.title = "User Manual".localized() - $0.presentationMode = PresentationMode.PresentModally(controllerProvider: ControllerProvider.Callback(builder: { [unowned self]() -> BaseSafariViewController in - let url = "http://manual.potatso.com/" - let vc = BaseSafariViewController(URL: NSURL(string: url)!, entersReaderIfAvailable: false) - vc.delegate = self - return vc - }), completionCallback: { (vc) -> () in - - }) - } -// +++ Section() -// <<< ActionSheetRow() { -// $0.title = "Logging" -// $0.selectorTitle = "Logging" -// $0.options = [.OFF, .DEBUG] -// $0.value = LoggingLevel.currentLoggingLevel -// }.cellUpdate({ (cell, row) -> () in -// cell.accessoryType = .DisclosureIndicator -// }).onChange({ [unowned self] (row) in -// if let v = row.value { -// LoggingLevel.currentLoggingLevel = v -//// self.showTextHUD("works after next restart", dismissAfterDelay: 1.0) -// self.showTextHUD("暂时不起作用", dismissAfterDelay: 1.0) -// } -// }) - +++ Section() - <<< LabelRow() { - $0.title = "Rate on App Store".localized() - }.cellSetup({ (cell, row) -> () in - cell.selectionStyle = .Default - cell.accessoryType = .DisclosureIndicator - }).onCellSelection({ (cell, row) -> () in - cell.setSelected(false, animated: true) - Appirater.rateApp() - }) - <<< LabelRow() { - $0.title = "Share with friends".localized() - }.cellSetup({ (cell, row) -> () in - cell.selectionStyle = .Default - cell.accessoryType = .DisclosureIndicator - }).onCellSelection({ [unowned self] (cell, row) -> () in - cell.setSelected(false, animated: true) - var shareItems: [AnyObject] = [] - shareItems.append("Potatso [https://itunes.apple.com/us/app/id1070901416]") - shareItems.append(UIImage(named: "AppIcon60x60")!) - let shareVC = UIActivityViewController(activityItems: shareItems, applicationActivities: nil) - self.presentViewController(shareVC, animated: true, completion: nil) - }) - form +++ Section() - <<< LabelRow() { + }.onCellSelection({ [unowned self] (cell, row) -> () in + let importer = Importer(vc: self) + importer.importConfigFromQRCode() + }) + return section + } + + func generateRateSection() -> Section { + let section = Section() + section + <<< ActionRow() { + $0.title = "Rate on App Store".localized() + }.onCellSelection({ (cell, row) -> () in + Appirater.rateApp() + }) + <<< ActionRow() { + $0.title = "Share with friends".localized() + }.onCellSelection({ [unowned self] (cell, row) -> () in + self.shareWithFriends() + }) + return section + } + + func generateAboutSection() -> Section { + let section = Section() + section + <<< ActionRow() { $0.title = "Follow on Twitter".localized() $0.value = "@PotatsoApp" - }.cellSetup({ (cell, row) -> () in - cell.selectionStyle = .Default - cell.accessoryType = .DisclosureIndicator - }).onCellSelection({ (cell, row) -> () in - cell.setSelected(false, animated: true) - UIApplication.sharedApplication().openURL(NSURL(string: "https://twitter.com/intent/user?screen_name=potatsoapp")!) + }.onCellSelection({ [unowned self] (cell, row) -> () in + self.followTwitter() }) - <<< LabelRow() { + <<< ActionRow() { $0.title = "Follow on Weibo".localized() $0.value = "@Potatso" - }.cellSetup({ (cell, row) -> () in - cell.selectionStyle = .Default - cell.accessoryType = .DisclosureIndicator - }).onCellSelection({ (cell, row) -> () in - cell.setSelected(false, animated: true) - UIApplication.sharedApplication().openURL(NSURL(string: "http://weibo.com/potatso")!) - }) - <<< LabelRow() { - $0.title = "Telegram Channel".localized() + }.onCellSelection({ [unowned self] (cell, row) -> () in + self.followWeibo() + }) + <<< ActionRow() { + $0.title = "Join Telegram Group".localized() $0.value = "@Potatso" - }.cellSetup({ (cell, row) -> () in - cell.selectionStyle = .Default - cell.accessoryType = .DisclosureIndicator - }).onCellSelection({ (cell, row) -> () in - cell.setSelected(false, animated: true) - UIApplication.sharedApplication().openURL(NSURL(string: "https://telegram.me/potatso")!) - }) - <<< LabelRow() { - $0.title = "Website".localized() - $0.value = "http://potatso.com" - }.cellSetup({ (cell, row) -> () in - cell.selectionStyle = .Default - cell.accessoryType = .DisclosureIndicator - }).onCellSelection({ (cell, row) -> () in - cell.setSelected(false, animated: true) - UIApplication.sharedApplication().openURL(NSURL(string: "http://potatso.com")!) - }) + }.onCellSelection({ [unowned self] (cell, row) -> () in + self.joinTelegramGroup() + }) <<< LabelRow() { $0.title = "Version".localized() $0.value = AppEnv.fullVersion } + return section + } + func showUserManual() { + let url = "https://manual.potatso.com/" + let vc = BaseSafariViewController(URL: NSURL(string: url)!, entersReaderIfAvailable: false) + vc.delegate = self + presentViewController(vc, animated: true, completion: nil) } - - func feedback() { - let options = [ - "gotoConversationAfterContactUs": "YES" - ] - let rulesets = Manager.sharedManager.defaultConfigGroup.ruleSets.map({ $0.name }).joinWithSeparator(", ") - let defaultToProxy = Manager.sharedManager.defaultConfigGroup.defaultToProxy - var tags: [String] = [] - if AppEnv.isTestFlight { - tags.append("testflight") - } else if AppEnv.isAppStore { - tags.append("store") - } - HelpshiftSupport.setMetadataBlock { () -> [NSObject : AnyObject]! in - return [ - "Full Version": AppEnv.fullVersion, - "Default To Proxy": defaultToProxy ? "true": "false", - "Rulesets": rulesets, - HelpshiftSupportTagsKey: tags - ] - } - HelpshiftSupport.showConversation(self, withOptions: options) + + func followTwitter() { + UIApplication.sharedApplication().openURL(NSURL(string: "https://twitter.com/intent/user?screen_name=potatsoapp")!) + } + + func followWeibo() { + UIApplication.sharedApplication().openURL(NSURL(string: "http://weibo.com/potatso")!) + } + + func joinTelegramGroup() { + UIApplication.sharedApplication().openURL(NSURL(string: "https://telegram.me/joinchat/BT0c4z49OGNZXwl9VsO0uQ")!) + } + + func shareWithFriends() { + var shareItems: [AnyObject] = [] + shareItems.append("Potatso [https://itunes.apple.com/us/app/id1070901416]") + shareItems.append(UIImage(named: "AppIcon60x60")!) + let shareVC = UIActivityViewController(activityItems: shareItems, applicationActivities: nil) + self.presentViewController(shareVC, animated: true, completion: nil) } @objc func safariViewControllerDidFinish(controller: SFSafariViewController) { diff --git a/Potatso/Sync/FetchCloudChangesOperation.swift b/Potatso/Sync/FetchCloudChangesOperation.swift index d442d6a..3f00cab 100644 --- a/Potatso/Sync/FetchCloudChangesOperation.swift +++ b/Potatso/Sync/FetchCloudChangesOperation.swift @@ -23,11 +23,9 @@ class FetchCloudChangesOperation: Operation { let objectClass: CloudKitRecord.Type - init(zoneID: CKRecordZoneID, objectClass: CloudKitRecord.Type, previousServerChangeToken: CKServerChangeToken?, - maximumRetryAttempts: Int = 3) { + init(zoneID: CKRecordZoneID, objectClass: CloudKitRecord.Type, maximumRetryAttempts: Int = 3) { self.zoneID = zoneID self.objectClass = objectClass - self.changeToken = previousServerChangeToken self.maximumRetryAttempts = maximumRetryAttempts super.init() @@ -36,7 +34,8 @@ class FetchCloudChangesOperation: Operation { override func execute() { print("\(self.name!) started") - + changeToken = getZoneChangeToken(zoneID) + fetchCloudChanges(changeToken) { (nsError) in self.finishWithError(nsError) diff --git a/Potatso/Sync/ICloudSetupOperation.swift b/Potatso/Sync/ICloudSetupOperation.swift new file mode 100644 index 0000000..2a25ae4 --- /dev/null +++ b/Potatso/Sync/ICloudSetupOperation.swift @@ -0,0 +1,34 @@ +// +// ICloudSetupOperation.swift +// Potatso +// +// Created by LEI on 8/5/16. +// Copyright © 2016 TouchingApp. All rights reserved. +// + +import Foundation +import PSOperations +import CloudKit + +class ICloudSetupOperation: BlockOperation { + + init(completion: (ErrorType? -> Void)? = nil) { + super.init(block: nil) + let url = NSURL(string: "http://www.apple.com")! + let reachabilityCondition = ReachabilityCondition(host: url) + + let container = CKContainer.defaultContainer() + let iCloudCapability = Capability(iCloudContainer(container: container)) + + let finishObserver = BlockObserver { operation, error in + print("ICloudSetupOperation finished! \(error)") + completion?(error.first) + } + + addCondition(reachabilityCondition) + addCondition(iCloudCapability) + + addObserver(finishObserver) + } + +} \ No newline at end of file diff --git a/Potatso/Sync/ICloudSyncService.swift b/Potatso/Sync/ICloudSyncService.swift index 2e7d435..386e61d 100644 --- a/Potatso/Sync/ICloudSyncService.swift +++ b/Potatso/Sync/ICloudSyncService.swift @@ -20,7 +20,8 @@ class ICloudSyncService: SyncServiceProtocol { } func setup(completion: (ErrorType? -> Void)?) { - + let setupOp = ICloudSetupOperation(completion: completion) + operationQueue.addOperation(setupOp) } func sync() { @@ -30,15 +31,14 @@ class ICloudSyncService: SyncServiceProtocol { let ruleSyncOp = SyncOperation(zoneID: potatsoZoneId, objectClass: Rule.self, syncType: SyncType.FetchCloudChangesAndThenPushLocalChanges) { print("sync rules completed") } + ruleSyncOp.addDependency(proxySyncOp) let ruleSetSyncOp = SyncOperation(zoneID: potatsoZoneId, objectClass: RuleSet.self, syncType: SyncType.FetchCloudChangesAndThenPushLocalChanges) { print("sync rulesets completed") } - ruleSetSyncOp.addDependency(ruleSetSyncOp) + ruleSetSyncOp.addDependency(ruleSyncOp) let configGroupSyncOp = SyncOperation(zoneID: potatsoZoneId, objectClass: ConfigurationGroup.self, syncType: SyncType.FetchCloudChangesAndThenPushLocalChanges) { print("sync config groups completed") } - configGroupSyncOp.addDependency(proxySyncOp) - configGroupSyncOp.addDependency(ruleSyncOp) configGroupSyncOp.addDependency(ruleSetSyncOp) operationQueue.addOperation(proxySyncOp) diff --git a/Potatso/Sync/PushLocalChangesOperation.swift b/Potatso/Sync/PushLocalChangesOperation.swift index 1d6a483..1e3cdd6 100644 --- a/Potatso/Sync/PushLocalChangesOperation.swift +++ b/Potatso/Sync/PushLocalChangesOperation.swift @@ -32,9 +32,9 @@ class PushLocalChangesOperation: Operation { // FIXME: Unsafe realm casting let toSyncObjects = realm.objects(self.objectClass as! BaseModel.Type) - .filter("synced == false") + .filter("synced == false && deleted == false") let toDeleteObjects = realm.objects(self.objectClass as! BaseModel.Type) - .filter("deleted == true") + .filter("synced == false && deleted == true") print("toSyncObjects: \(toSyncObjects.map({ $0.uuid }).joinWithSeparator(", "))") print("toDeleteObjects: \(toDeleteObjects.map({ $0.uuid }).joinWithSeparator(", "))") @@ -63,7 +63,6 @@ class PushLocalChangesOperation: Operation { (savedRecords, deletedRecordIDs, nsError) -> Void in if let error = nsError { - self.handleCloudKitPushError( savedRecords, deletedRecordIDs: deletedRecordIDs, diff --git a/Potatso/Sync/RealmCloud.swift b/Potatso/Sync/RealmCloud.swift index 80c31ca..b343baa 100644 --- a/Potatso/Sync/RealmCloud.swift +++ b/Potatso/Sync/RealmCloud.swift @@ -147,7 +147,7 @@ public func createAlertOperation(error: NSError) -> AlertOperation { alert.message = "Cannot write data to iPhone." + "\n\n" + - "Error Code: RLMError.\(errorString)" + "Error Code: RLMError.\(errorString) (\(error.localizedDescription))" case CKErrorDomain: let ckErrorCode: CKErrorCode = CKErrorCode(rawValue: error.code)! @@ -156,7 +156,7 @@ public func createAlertOperation(error: NSError) -> AlertOperation { alert.message = "Cannot complete sync operation. Try again later." + "\n\n" + - "Error Code: CKError.\(String(ckErrorCode))" + "Error Code: CKError.\(String(ckErrorCode)) (\(error.localizedDescription))" default: alert.title = "Error" diff --git a/Potatso/Sync/SyncManager.swift b/Potatso/Sync/SyncManager.swift index 4ba941b..f078d61 100644 --- a/Potatso/Sync/SyncManager.swift +++ b/Potatso/Sync/SyncManager.swift @@ -21,12 +21,13 @@ public protocol SyncServiceProtocol { public class SyncManager { - public static let shared = SyncManager() + static let shared = SyncManager() + public static let syncServiceChangedNotification = "syncServiceChangedNotification" private var services: [SyncServiceType: SyncServiceProtocol] = [:] private static let serviceTypeKey = "serviceTypeKey" - public var currentSyncServiceType: SyncServiceType { + var currentSyncServiceType: SyncServiceType { get { if let raw = NSUserDefaults.standardUserDefaults().objectForKey(SyncManager.serviceTypeKey) as? String, type = SyncServiceType(rawValue: raw) { return type @@ -39,17 +40,18 @@ public class SyncManager { } NSUserDefaults.standardUserDefaults().setObject(new.rawValue, forKey: SyncManager.serviceTypeKey) NSUserDefaults.standardUserDefaults().synchronize() + NSNotificationCenter.defaultCenter().postNotificationName(SyncManager.syncServiceChangedNotification, object: nil) } } - public var selectedSyncServiceType: SyncServiceType = .None - init() { - selectedSyncServiceType = currentSyncServiceType } - public func getSelectedSyncService() -> SyncServiceProtocol? { - let type = selectedSyncServiceType + func getCurrentSyncService() -> SyncServiceProtocol? { + return getSyncService(forType: currentSyncServiceType) + } + + func getSyncService(forType type: SyncServiceType) -> SyncServiceProtocol? { if let service = services[type] { return service } @@ -64,16 +66,28 @@ public class SyncManager { return s } + func showSyncVC(inVC vc:UIViewController? = nil) { + guard let currentVC = vc ?? UIApplication.sharedApplication().keyWindow?.rootViewController else { + return + } + let syncVC = SyncVC() + currentVC.showViewController(syncVC, sender: self) + } + } extension SyncManager { - public func setup(completion: (ErrorType? -> Void)?) { - getSelectedSyncService()?.setup(completion) + func setupNewService(type: SyncServiceType, completion: (ErrorType? -> Void)?) { + getSyncService(forType: type)?.setup(completion) + } + + func setup(completion: (ErrorType? -> Void)?) { + getCurrentSyncService()?.setup(completion) } - public func sync() { - getSelectedSyncService()?.sync() + func sync() { + getCurrentSyncService()?.sync() } } \ No newline at end of file diff --git a/Potatso/Sync/SyncOperation.swift b/Potatso/Sync/SyncOperation.swift index bc263a8..26789cc 100644 --- a/Potatso/Sync/SyncOperation.swift +++ b/Potatso/Sync/SyncOperation.swift @@ -42,9 +42,7 @@ public class SyncOperation: GroupOperation { objectClass: CloudKitRecord.Type, syncType: SyncType, completionHandler: () -> Void) { - - let zoneChangeToken = getZoneChangeToken(zoneID) - + // Setup Conditions // ReachabilityCondition as written requires a URL so rather than rewriting, ping google @@ -80,7 +78,7 @@ public class SyncOperation: GroupOperation { finishOperation] case .FetchCloudChanges: - fetchCloudChangesOperation = FetchCloudChangesOperation(zoneID: zoneID, objectClass: objectClass, previousServerChangeToken: zoneChangeToken) + fetchCloudChangesOperation = FetchCloudChangesOperation(zoneID: zoneID, objectClass: objectClass) fetchCloudChangesOperation.addDependency(prepareZoneOperation) finishOperation.addDependency(fetchCloudChangesOperation) @@ -92,7 +90,7 @@ public class SyncOperation: GroupOperation { case .FetchCloudChangesAndThenPushLocalChanges: pushLocalChangesOperation = PushLocalChangesOperation(zoneID: zoneID, objectClass: objectClass) - fetchCloudChangesOperation = FetchCloudChangesOperation(zoneID: zoneID, objectClass: objectClass, previousServerChangeToken: zoneChangeToken) + fetchCloudChangesOperation = FetchCloudChangesOperation(zoneID: zoneID, objectClass: objectClass) pushLocalChangesOperation.addDependency(prepareZoneOperation) fetchCloudChangesOperation.addDependency(pushLocalChangesOperation) diff --git a/Potatso/Sync/SyncVC.swift b/Potatso/Sync/SyncVC.swift new file mode 100644 index 0000000..7febc24 --- /dev/null +++ b/Potatso/Sync/SyncVC.swift @@ -0,0 +1,72 @@ +// +// SyncVC.swift +// Potatso +// +// Created by LEI on 8/4/16. +// Copyright © 2016 TouchingApp. All rights reserved. +// + +import Foundation +import Eureka + +class SyncVC: FormViewController { + + override func viewDidLoad() { + super.viewDidLoad() + navigationItem.title = "Sync".localized() + } + + override func viewWillAppear(animated: Bool) { + super.viewWillAppear(animated) + NSNotificationCenter.defaultCenter().addObserverForName(SyncManager.syncServiceChangedNotification, object: nil, queue: NSOperationQueue.mainQueue()) { [unowned self] (noti) in + self.generateForm() + } + generateForm() + } + + override func viewWillDisappear(animated: Bool) { + super.viewWillDisappear(animated) + NSNotificationCenter.defaultCenter().removeObserver(self) + } + + func generateForm() { + form.delegate = nil + form.removeAll() + form +++ generateServiceSection() + form.delegate = self + tableView?.reloadData() + } + + func generateServiceSection() -> Section { + let section = Section() + section + <<< PushRow() { + $0.title = "Sync Service".localized() + $0.options = [.None, .iCloud] + $0.value = SyncManager.shared.currentSyncServiceType + $0.selectorTitle = "Choose Sync Service".localized() + }.onChange({ [weak self] (row) in + if let type = row.value { + SyncManager.shared.setupNewService(type, completion: { (error) in + if let error = error { + if let vc = self { + Alert.show(vc, title: "Setup Failed", message: "\((error as NSError).localizedDescription)") + } + } else { + SyncManager.shared.currentSyncServiceType = type + } + }) + } + }) + section + <<< ButtonRow { + $0.title = "Sync Now" + $0.hidden = Condition.Function([""]) { form in + return SyncManager.shared.currentSyncServiceType == .None + } + }.onCellSelection({ (cell, row) in + SyncManager.shared.sync() + }) + return section + } +} \ No newline at end of file diff --git a/PotatsoModel/BaseModel.swift b/PotatsoModel/BaseModel.swift index 72994d0..a622e48 100644 --- a/PotatsoModel/BaseModel.swift +++ b/PotatsoModel/BaseModel.swift @@ -15,7 +15,12 @@ public var defaultRealm = try! Realm() public func setupDefaultReaml() { var config = Realm.Configuration() - let sharedURL = Potatso.sharedDatabaseUrl() +// #if DEBUG + let path = (NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true).first! as NSString).stringByAppendingPathComponent("./potatso.realm") + let sharedURL = NSURL(string: path)! +// #else +// let sharedURL = Potatso.sharedDatabaseUrl() +// #endif if let originPath = config.fileURL?.path { if NSFileManager.defaultManager().fileExistsAtPath(originPath) { _ = try? NSFileManager.defaultManager().moveItemAtPath(originPath, toPath: sharedURL.path!) From 1e39c2d6770fbec0176fd75f202dce278361eaf6 Mon Sep 17 00:00:00 2001 From: iCodesign Date: Tue, 9 Aug 2016 00:20:36 +0800 Subject: [PATCH 5/8] Polish sync --- .../RuleConfigurationViewController.swift | 6 +- .../RuleSetConfigurationViewController.swift | 5 +- Potatso/CloudDetailViewController.swift | 6 +- Potatso/ConfigGroupChooseVC.swift | 4 +- Potatso/Core/API.swift | 15 ++- Potatso/Core/Importer.swift | 4 +- Potatso/DataInitializer.swift | 51 +++++--- Potatso/NotificationHandler.swift | 10 +- Potatso/ProxyListViewController.swift | 4 +- Potatso/RuleSetListViewController.swift | 4 +- Potatso/Sync/CloudKitRecord.swift | 71 ++++++---- Potatso/Sync/FetchCloudChangesOperation.swift | 13 +- Potatso/Sync/ICloudSetupOperation.swift | 22 +++- Potatso/Sync/ICloudSyncService.swift | 43 +++++- Potatso/Sync/PrepareZoneOperation.swift | 5 +- Potatso/Sync/PushLocalChangesOperation.swift | 27 ++-- Potatso/Sync/RealmCloud.swift | 7 +- Potatso/Sync/SyncManager.swift | 14 +- Potatso/Sync/SyncOperation.swift | 30 +---- Potatso/Sync/SyncVC.swift | 7 +- PotatsoModel/BaseModel.swift | 2 +- PotatsoModel/DBUtils.swift | 122 +++++++++++------- PotatsoModel/Rule.swift | 2 +- PotatsoModel/RuleSet.swift | 2 +- 24 files changed, 299 insertions(+), 177 deletions(-) diff --git a/Potatso/Advance/RuleConfigurationViewController.swift b/Potatso/Advance/RuleConfigurationViewController.swift index d75da77..7f12d58 100644 --- a/Potatso/Advance/RuleConfigurationViewController.swift +++ b/Potatso/Advance/RuleConfigurationViewController.swift @@ -67,7 +67,7 @@ class RuleConfigurationViewController: FormViewController { <<< PushRow(kRuleFormType) { $0.title = "Type".localized() $0.selectorTitle = "Choose type of rule".localized() - $0.options = [RuleType.URL, RuleType.DomainSuffix, RuleType.DomainMatch, RuleType.Domain, RuleType.IPCIDR, RuleType.GeoIP] + $0.options = [RuleType.DomainSuffix, RuleType.DomainMatch, RuleType.Domain, RuleType.IPCIDR, RuleType.GeoIP] $0.value = self.rule.type $0.disabled = Condition(booleanLiteral: !editable) }.cellSetup({ (cell, row) -> () in @@ -105,10 +105,8 @@ class RuleConfigurationViewController: FormViewController { guard let action = values[kRuleFormAction] as? RuleAction else { throw "You must choose a action".localized() } - defaultRealm.beginWrite() rule.update(type, action: action, value: value) - defaultRealm.add(rule, update: true) - try defaultRealm.commitWrite() + try DBUtils.add(rule) callback?(rule) close() }catch { diff --git a/Potatso/Advance/RuleSetConfigurationViewController.swift b/Potatso/Advance/RuleSetConfigurationViewController.swift index a6b32a2..90e14a9 100644 --- a/Potatso/Advance/RuleSetConfigurationViewController.swift +++ b/Potatso/Advance/RuleSetConfigurationViewController.swift @@ -125,13 +125,10 @@ class RuleSetConfigurationViewController: FormViewController { throw "Name already exists".localized() } } - - defaultRealm.beginWrite() ruleSet.name = name ruleSet.rules.removeAll() ruleSet.rules.appendContentsOf(rules) - defaultRealm.add(ruleSet, update: true) - try defaultRealm.commitWrite() + try DBUtils.add(ruleSet) callback?(ruleSet) close() }catch { diff --git a/Potatso/CloudDetailViewController.swift b/Potatso/CloudDetailViewController.swift index c5d86e9..7e740ca 100644 --- a/Potatso/CloudDetailViewController.swift +++ b/Potatso/CloudDetailViewController.swift @@ -68,11 +68,9 @@ class CloudDetailViewController: UIViewController, UITableViewDataSource, UITabl func subscribe() { let uuid = ruleSet.uuid if isExist(uuid) { - let sets = defaultRealm.objects(RuleSet).filter("uuid = %@", uuid) + let ids = defaultRealm.objects(RuleSet).filter("uuid = %@", uuid).map({ $0.uuid }) do { - try defaultRealm.write { - defaultRealm.delete(sets) - } + try DBUtils.softDelete(ids, type: RuleSet.self) }catch { self.showTextHUD("Fail to unsubscribe".localized(), dismissAfterDelay: 1.0) return diff --git a/Potatso/ConfigGroupChooseVC.swift b/Potatso/ConfigGroupChooseVC.swift index 37659e1..4a1e025 100644 --- a/Potatso/ConfigGroupChooseVC.swift +++ b/Potatso/ConfigGroupChooseVC.swift @@ -158,9 +158,7 @@ class ConfigGroupChooseVC: UIViewController, UITableViewDataSource, UITableViewD } do { groups.removeAtIndex(indexPath.row) - try defaultRealm.write { - defaultRealm.delete(item) - } + try DBUtils.softDelete(item.uuid, type: ConfigurationGroup.self) tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic) }catch { self.showTextHUD("\("Fail to delete item".localized()): \((error as NSError).localizedDescription)", dismissAfterDelay: 1.5) diff --git a/Potatso/Core/API.swift b/Potatso/Core/API.swift index 89e7a69..05e549a 100644 --- a/Potatso/Core/API.swift +++ b/Potatso/Core/API.swift @@ -70,7 +70,7 @@ extension RuleSet: Mappable { uuid <- map["id"] name <- map["name"] createAt <- (map["created_at"], DateTransform()) - updateAt <- (map["updated_at"], DateTransform()) + remoteUpdatedAt <- (map["updated_at"], DateTransform()) desc <- map["description"] ruleCount <- map["rule_count"] isOfficial <- map["is_official"] @@ -81,12 +81,21 @@ extension RuleSet { static func addRemoteObject(ruleset: RuleSet, update: Bool = true) throws { ruleset.isSubscribe = true + let id = ruleset.uuid + guard let local: RuleSet = DBUtils.get(id) else { + try DBUtils.add(ruleset) + return + } + if local.remoteUpdatedAt == ruleset.remoteUpdatedAt { + return + } try DBUtils.add(ruleset) } static func addRemoteArray(rulesets: [RuleSet], update: Bool = true) throws { - rulesets.forEach({ $0.isSubscribe = true }) - try DBUtils.add(rulesets) + for ruleset in rulesets { + try addRemoteObject(ruleset, update: update) + } } } diff --git a/Potatso/Core/Importer.swift b/Potatso/Core/Importer.swift index dd705ca..7ed3086 100644 --- a/Potatso/Core/Importer.swift +++ b/Potatso/Core/Importer.swift @@ -77,9 +77,7 @@ struct Importer { proxy.name = text do { try proxy.validate(inRealm: defaultRealm) - try defaultRealm.write { - defaultRealm.add(proxy) - } + try DBUtils.add(proxy) self.onConfigSaveCallback(true, error: nil) }catch { self.onConfigSaveCallback(false, error: error) diff --git a/Potatso/DataInitializer.swift b/Potatso/DataInitializer.swift index 8404e29..ac8ba70 100644 --- a/Potatso/DataInitializer.swift +++ b/Potatso/DataInitializer.swift @@ -10,10 +10,14 @@ import UIKit import ICSMainFramework import NetworkExtension import CloudKit +import Async +import RealmSwift +import Realm class DataInitializer: NSObject, AppLifeCycleProtocol { let s = ICloudSyncService() + var token: RLMNotificationToken? = nil func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject : AnyObject]?) -> Bool { do { @@ -21,44 +25,61 @@ class DataInitializer: NSObject, AppLifeCycleProtocol { }catch { error.log("Fail to setup manager") } - SyncManager.shared.sync() + updateCloudSets() + sync() +// token = defaultRealm.addNotificationBlock({ [weak self] (notification, realm) in +// self?.sync() +// }) return true } func applicationDidEnterBackground(application: UIApplication) { _ = try? Manager.sharedManager.regenerateConfigFiles() } - + func applicationWillTerminate(application: UIApplication) { _ = try? Manager.sharedManager.regenerateConfigFiles() } func applicationWillEnterForeground(application: UIApplication) { Receipt.shared.validate() - SyncManager.shared.sync() + sync() } - func applicationDidBecomeActive(application: UIApplication) { - deleteOrphanRules() + func updateCloudSets() { let uuids = defaultRealm.objects(RuleSet).filter("isSubscribe = true").map({$0.uuid}) - API.updateRuleSetListDetail(uuids) { (response) in - if let sets = response.result.value { - do { - try RuleSet.addRemoteArray(sets) - }catch { - error.log("Unable to save updated rulesets") - return + Async.background(after: 1.5) { + API.updateRuleSetListDetail(uuids) { (response) in + if let sets = response.result.value { + do { + try RuleSet.addRemoteArray(sets) + }catch { + error.log("Unable to save updated rulesets") + return + } + }else { + response.result.error?.log("Fail to update ruleset details") } - }else { - response.result.error?.log("Fail to update ruleset details") } } } + func sync() { + cleanupData() + SyncManager.shared.sync() + } + + func cleanupData() { + deleteOrphanRules() + } + func deleteOrphanRules() { let orphanRules = defaultRealm.objects(Rule).filter("rulesets.@count == 0") if orphanRules.count > 0 { - _ = try? DBUtils.delete(orphanRules) + let ids = orphanRules.map({ $0.uuid }) + for id in ids { + _ = try? DBUtils.softDelete(id, type: Rule.self) + } } } diff --git a/Potatso/NotificationHandler.swift b/Potatso/NotificationHandler.swift index 8b4d550..92c21db 100644 --- a/Potatso/NotificationHandler.swift +++ b/Potatso/NotificationHandler.swift @@ -8,6 +8,7 @@ import Foundation import ICSMainFramework +import CloudKit class NotificationHandler: NSObject, AppLifeCycleProtocol { @@ -34,7 +35,7 @@ class NotificationHandler: NSObject, AppLifeCycleProtocol { } func application(application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: NSData) { - print("didRegisterForRemoteNotificationsWithDeviceToken: \(deviceToken.hexString())") + DDLogInfo("didRegisterForRemoteNotificationsWithDeviceToken: \(deviceToken.hexString())") HelpshiftCore.registerDeviceToken(deviceToken) } @@ -48,6 +49,13 @@ class NotificationHandler: NSObject, AppLifeCycleProtocol { return } } + if let dict = userInfo as? [String: NSObject] { + let ckNotification = CKNotification(fromRemoteNotificationDictionary: dict) + if ckNotification.subscriptionID == potatsoSubscriptionId { + DDLogInfo("received a CKNotification") + SyncManager.shared.sync() + } + } completionHandler(.NoData) } diff --git a/Potatso/ProxyListViewController.swift b/Potatso/ProxyListViewController.swift index c4a4c2e..76fa14f 100644 --- a/Potatso/ProxyListViewController.swift +++ b/Potatso/ProxyListViewController.swift @@ -97,9 +97,7 @@ class ProxyListViewController: FormViewController { } do { proxies.removeAtIndex(indexPath.row) - try defaultRealm.write { - defaultRealm.delete(item) - } + try DBUtils.softDelete(item.uuid, type: Proxy.self) form[indexPath].hidden = true form[indexPath].evaluateHidden() }catch { diff --git a/Potatso/RuleSetListViewController.swift b/Potatso/RuleSetListViewController.swift index 7845f96..990eaf2 100644 --- a/Potatso/RuleSetListViewController.swift +++ b/Potatso/RuleSetListViewController.swift @@ -102,9 +102,7 @@ class RuleSetListViewController: UIViewController, UITableViewDataSource, UITabl } do { ruleSets.removeAtIndex(indexPath.row) - try defaultRealm.write { - defaultRealm.delete(item) - } + try DBUtils.softDelete(item.uuid, type: RuleSet.self) tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic) }catch { self.showTextHUD("\("Fail to delete item".localized()): \((error as NSError).localizedDescription)", dismissAfterDelay: 1.5) diff --git a/Potatso/Sync/CloudKitRecord.swift b/Potatso/Sync/CloudKitRecord.swift index d787b66..4f9b1df 100644 --- a/Potatso/Sync/CloudKitRecord.swift +++ b/Potatso/Sync/CloudKitRecord.swift @@ -13,13 +13,15 @@ import Realm import RealmSwift let potatsoZoneId = CKRecordZoneID(zoneName: "PotatsoCloud", ownerName: CKOwnerDefaultName) +let potatsoDB = CKContainer.defaultContainer().privateCloudDatabase +let potatsoSubscriptionId = "allSubscription" public protocol CloudKitRecord { static var recordType: String { get } static var keys: [String] { get } var recordId: CKRecordID { get } func toCloudKitRecord() -> CKRecord - static func fromCloudKitRecord(record: CKRecord) -> BaseModel + static func fromCloudKitRecord(record: CKRecord) -> Self } extension BaseModel { @@ -52,8 +54,8 @@ extension Proxy: CloudKitRecord { return record } - public static func fromCloudKitRecord(record: CKRecord) -> BaseModel { - let proxy = Proxy() + public static func fromCloudKitRecord(record: CKRecord) -> Self { + let proxy = self.init() for key in Proxy.keys { if let v = record.valueForKey(key) { proxy.setValue(v, forKey: key) @@ -85,8 +87,8 @@ extension Rule: CloudKitRecord { return record } - public static func fromCloudKitRecord(record: CKRecord) -> BaseModel { - let rule = Rule() + public static func fromCloudKitRecord(record: CKRecord) -> Self { + let rule = self.init() for key in Rule.keys { if let v = record.valueForKey(key) { rule.setValue(v, forKey: key) @@ -103,7 +105,7 @@ extension RuleSet: CloudKitRecord { } public static var keys: [String] { - return basekeys + ["editable", "name", "updateAt", "desc", "ruleCount", "isSubscribe", "isOfficial"] + return basekeys + ["editable", "name", "remoteUpdatedAt", "desc", "ruleCount", "isSubscribe", "isOfficial"] } public var recordId: CKRecordID { @@ -119,8 +121,8 @@ extension RuleSet: CloudKitRecord { return record } - public static func fromCloudKitRecord(record: CKRecord) -> BaseModel { - let ruleset = RuleSet() + public static func fromCloudKitRecord(record: CKRecord) -> Self { + let ruleset = self.init() for key in RuleSet.keys { if let v = record.valueForKey(key) { ruleset.setValue(v, forKey: key) @@ -160,8 +162,8 @@ extension ConfigurationGroup: CloudKitRecord { return record } - public static func fromCloudKitRecord(record: CKRecord) -> BaseModel { - let group = ConfigurationGroup() + public static func fromCloudKitRecord(record: CKRecord) -> Self { + let group = self.init() for key in ConfigurationGroup.keys { if let v = record.valueForKey(key) { group.setValue(v, forKey: key) @@ -182,38 +184,63 @@ extension ConfigurationGroup: CloudKitRecord { } } +extension CKRecord { + + var realmClassType: BaseModel.Type? { + let type: BaseModel.Type? + switch recordType { + case "Proxy": + type = Proxy.self + case "Rule": + type = Rule.self + case "RuleSet": + type = RuleSet.self + case "ConfigurationGroup": + type = ConfigurationGroup.self + default: + return nil + } + return type + } -func changeLocalRecord(record: CKRecord, objectClass: CloudKitRecord.Type) throws { +} + +func changeLocalRecord(record: CKRecord, objectClass: T.Type) throws { let realm = try! Realm() let realmObject: BaseModel - let local: BaseModel? + let local: T? = DBUtils.get(record.recordID.recordName, inRealm: realm) switch record.recordType { case "Proxy": realmObject = Proxy.fromCloudKitRecord(record) realmObject.synced = true - local = realm.objects(Proxy).filter("uuid = '\(realmObject.uuid)'").first + case "Rule": + realmObject = Rule.fromCloudKitRecord(record) + realmObject.synced = true + case "RuleSet": + realmObject = RuleSet.fromCloudKitRecord(record) + realmObject.synced = true + case "ConfigurationGroup": + realmObject = ConfigurationGroup.fromCloudKitRecord(record) + realmObject.synced = true default: return } - if let local = local { + if let local = local, type = record.realmClassType { if local.updatedAt > realmObject.updatedAt { - try DBUtils.mark(local, synced: false) + try DBUtils.mark(local.uuid, type: type, synced: false) return } else if local.updatedAt == realmObject.updatedAt { - try DBUtils.mark(local, synced: true) + try DBUtils.mark(local.uuid, type: type, synced: true) return } } try DBUtils.add(realmObject) } -func deleteLocalRecord(recordID: CKRecordID, objectClass: CloudKitRecord.Type) throws { - let realm = try! Realm() +func deleteLocalRecord(recordID: CKRecordID, objectClass: T.Type) throws { let id = recordID.recordName // FIXME: Unsafe realm casting - if let object = realm.objectForPrimaryKey(objectClass as! BaseModel.Type, key: id) { - print("Deleting local record.") - try DBUtils.delete(object) - } + print("Deleting local record.") + try DBUtils.hardDelete(id, type: objectClass) } diff --git a/Potatso/Sync/FetchCloudChangesOperation.swift b/Potatso/Sync/FetchCloudChangesOperation.swift index 3f00cab..3cfe071 100644 --- a/Potatso/Sync/FetchCloudChangesOperation.swift +++ b/Potatso/Sync/FetchCloudChangesOperation.swift @@ -12,7 +12,7 @@ struct FetchResults { } } -class FetchCloudChangesOperation: Operation { +class FetchCloudChangesOperation: Operation { let zoneID: CKRecordZoneID var changeToken: CKServerChangeToken? @@ -21,20 +21,20 @@ class FetchCloudChangesOperation: Operation { let maximumRetryAttempts: Int var retryAttempts: Int = 0 - let objectClass: CloudKitRecord.Type + let objectClass: T.Type - init(zoneID: CKRecordZoneID, objectClass: CloudKitRecord.Type, maximumRetryAttempts: Int = 3) { + init(zoneID: CKRecordZoneID, objectClass: T.Type, maximumRetryAttempts: Int = 3) { self.zoneID = zoneID self.objectClass = objectClass self.maximumRetryAttempts = maximumRetryAttempts super.init() - name = "Fetch Cloud Changes" + name = "Fetch Cloud Changes of \(objectClass)" } override func execute() { - print("\(self.name!) started") changeToken = getZoneChangeToken(zoneID) + print(">>>>>>>>> \(self.name!) started with token: \(changeToken)") fetchCloudChanges(changeToken) { (nsError) in @@ -61,6 +61,9 @@ class FetchCloudChangesOperation: Operation { (recordID) in results.deletedRecordIDs.append(recordID) } + + fetchOperation.database = potatsoDB + fetchOperation.recordZoneID = potatsoZoneId fetchOperation.fetchRecordChangesCompletionBlock = { (serverChangeToken, clientChangeToken, nsError) in diff --git a/Potatso/Sync/ICloudSetupOperation.swift b/Potatso/Sync/ICloudSetupOperation.swift index 2a25ae4..472302f 100644 --- a/Potatso/Sync/ICloudSetupOperation.swift +++ b/Potatso/Sync/ICloudSetupOperation.swift @@ -9,26 +9,36 @@ import Foundation import PSOperations import CloudKit +import Async -class ICloudSetupOperation: BlockOperation { +class ICloudSetupOperation: GroupOperation { init(completion: (ErrorType? -> Void)? = nil) { - super.init(block: nil) let url = NSURL(string: "http://www.apple.com")! let reachabilityCondition = ReachabilityCondition(host: url) let container = CKContainer.defaultContainer() let iCloudCapability = Capability(iCloudContainer(container: container)) + let dummyOp = BlockOperation(block: nil) + let finishObserver = BlockObserver { operation, error in print("ICloudSetupOperation finished! \(error)") - completion?(error.first) + Async.main { + completion?(error.first) + } } - addCondition(reachabilityCondition) - addCondition(iCloudCapability) + let prepareZoneOperation = PrepareZoneOperation(zoneID: potatsoZoneId) + + prepareZoneOperation.addCondition(reachabilityCondition) + prepareZoneOperation.addCondition(iCloudCapability) + + prepareZoneOperation.addObserver(finishObserver) + + dummyOp.addDependency(prepareZoneOperation) - addObserver(finishObserver) + super.init(operations: [prepareZoneOperation, dummyOp]) } } \ No newline at end of file diff --git a/Potatso/Sync/ICloudSyncService.swift b/Potatso/Sync/ICloudSyncService.swift index 386e61d..34bff42 100644 --- a/Potatso/Sync/ICloudSyncService.swift +++ b/Potatso/Sync/ICloudSyncService.swift @@ -22,29 +22,62 @@ class ICloudSyncService: SyncServiceProtocol { func setup(completion: (ErrorType? -> Void)?) { let setupOp = ICloudSetupOperation(completion: completion) operationQueue.addOperation(setupOp) + subscribe() } - func sync() { + func sync(manually: Bool = false) { + if manually { + setZoneChangeToken(potatsoZoneId, changeToken: nil) + _ = try? DBUtils.markAll(false) + } + let setupOp = ICloudSetupOperation(completion: nil) + let proxySyncOp = SyncOperation(zoneID: potatsoZoneId, objectClass: Proxy.self, syncType: SyncType.FetchCloudChangesAndThenPushLocalChanges) { - print("sync proxies completed") + print("<<<<<<<<< sync proxies completed") } let ruleSyncOp = SyncOperation(zoneID: potatsoZoneId, objectClass: Rule.self, syncType: SyncType.FetchCloudChangesAndThenPushLocalChanges) { - print("sync rules completed") + print("<<<<<<<<< sync rules completed") } ruleSyncOp.addDependency(proxySyncOp) let ruleSetSyncOp = SyncOperation(zoneID: potatsoZoneId, objectClass: RuleSet.self, syncType: SyncType.FetchCloudChangesAndThenPushLocalChanges) { - print("sync rulesets completed") + print("<<<<<<<<< sync rulesets completed") } ruleSetSyncOp.addDependency(ruleSyncOp) let configGroupSyncOp = SyncOperation(zoneID: potatsoZoneId, objectClass: ConfigurationGroup.self, syncType: SyncType.FetchCloudChangesAndThenPushLocalChanges) { - print("sync config groups completed") + print("<<<<<<<<< sync config groups completed") } configGroupSyncOp.addDependency(ruleSetSyncOp) + operationQueue.addOperation(setupOp) operationQueue.addOperation(proxySyncOp) operationQueue.addOperation(ruleSyncOp) operationQueue.addOperation(ruleSetSyncOp) operationQueue.addOperation(configGroupSyncOp) } + func subscribe() { + let subscription = CKSubscription(zoneID: potatsoZoneId, subscriptionID: potatsoSubscriptionId, options: CKSubscriptionOptions(rawValue: 0)) + let info = CKNotificationInfo() + info.alertBody = "Potatso iCloud updated" + subscription.notificationInfo = info + + potatsoDB.saveSubscription(subscription) { (sub, error) in + if let error = error { + DDLogError("save cloudkit subscription error: \(error.localizedDescription)") + } else { + DDLogInfo("save cloudkit subscription success") + } + } + } + + func stop() { + potatsoDB.deleteSubscriptionWithID(potatsoSubscriptionId) { (id, error) in + if let error = error { + DDLogError("delete cloudkit subscription error: \(error.localizedDescription)") + } else { + DDLogInfo("delete cloudkit subscription success") + } + } + } + } diff --git a/Potatso/Sync/PrepareZoneOperation.swift b/Potatso/Sync/PrepareZoneOperation.swift index c9f0e21..e3b94ce 100644 --- a/Potatso/Sync/PrepareZoneOperation.swift +++ b/Potatso/Sync/PrepareZoneOperation.swift @@ -23,9 +23,8 @@ class PrepareZoneOperation: Operation { } func prepareCKRecordZone(zoneID: CKRecordZoneID, completionHandler: (NSError!) -> ()) { - let privateDB = CKContainer.defaultContainer().privateCloudDatabase // Per CloudKitCatalog, not using NSOperation here - privateDB.fetchAllRecordZonesWithCompletionHandler { + potatsoDB.fetchAllRecordZonesWithCompletionHandler { (zones, nsError) in if nsError != nil { print(nsError) @@ -48,7 +47,7 @@ class PrepareZoneOperation: Operation { // all user data from that zone to the cloud. To do so mark all // live records as locally modified. Deleted records which have also // been deleted locally will be gone forever. - privateDB.saveRecordZone(CKRecordZone(zoneID: zoneID)) { + potatsoDB.saveRecordZone(CKRecordZone(zoneID: zoneID)) { (recordZone, nsError) in // TODO: set a boolean NSUserDefault value equal to the // zoneName to keep track of zones this device has previously used diff --git a/Potatso/Sync/PushLocalChangesOperation.swift b/Potatso/Sync/PushLocalChangesOperation.swift index 1e3cdd6..5d49c17 100644 --- a/Potatso/Sync/PushLocalChangesOperation.swift +++ b/Potatso/Sync/PushLocalChangesOperation.swift @@ -3,7 +3,7 @@ import RealmSwift import CloudKit import PSOperations -class PushLocalChangesOperation: Operation { +class PushLocalChangesOperation: Operation { let zoneID: CKRecordZoneID var recordsToSave: [CKRecord]? @@ -13,36 +13,36 @@ class PushLocalChangesOperation: Operation { let maximumRetryAttempts: Int var retryAttempts: Int = 0 - let objectClass: CloudKitRecord.Type + let objectClass: T.Type - init(zoneID: CKRecordZoneID, objectClass: CloudKitRecord.Type, maximumRetryAttempts: Int = 3) { + init(zoneID: CKRecordZoneID, objectClass: T.Type, maximumRetryAttempts: Int = 3) { self.zoneID = zoneID self.objectClass = objectClass self.maximumRetryAttempts = maximumRetryAttempts super.init() - name = "Push Local Changes" + name = "Push Local Changes of \(objectClass)" } override func execute() { - print("\(self.name!) started") + print(">>>>>>>>> \(self.name!) started") // Query records let realm = try! Realm() // FIXME: Unsafe realm casting - let toSyncObjects = realm.objects(self.objectClass as! BaseModel.Type) + let toSyncObjects = realm.objects(self.objectClass) .filter("synced == false && deleted == false") - let toDeleteObjects = realm.objects(self.objectClass as! BaseModel.Type) + let toDeleteObjects = realm.objects(self.objectClass) .filter("synced == false && deleted == true") print("toSyncObjects: \(toSyncObjects.map({ $0.uuid }).joinWithSeparator(", "))") print("toDeleteObjects: \(toDeleteObjects.map({ $0.uuid }).joinWithSeparator(", "))") self.recordsToSave = toSyncObjects.map { - ($0 as! CloudKitRecord).toCloudKitRecord() + $0.toCloudKitRecord() } self.recordIDsToDelete = toDeleteObjects.map { - ($0 as! CloudKitRecord).recordId + $0.recordId } modifyRecords(self.recordsToSave, recordIDsToDelete: self.recordIDsToDelete) { @@ -58,11 +58,11 @@ class PushLocalChangesOperation: Operation { let modifyOperation = CKModifyRecordsOperation( recordsToSave: recordsToSave, recordIDsToDelete: recordIDsToDelete) - + modifyOperation.savePolicy = .ChangedKeys modifyOperation.modifyRecordsCompletionBlock = { (savedRecords, deletedRecordIDs, nsError) -> Void in - if let error = nsError { + print("modifyRecords error: \(error), \(savedRecords?.count), \(recordIDsToDelete?.count)") self.handleCloudKitPushError( savedRecords, deletedRecordIDs: deletedRecordIDs, @@ -70,17 +70,18 @@ class PushLocalChangesOperation: Operation { completionHandler: completionHandler) } else { - do { // Update local modified flag if let savedRecords = savedRecords { + print("savedRecords: \(savedRecords.map({ $0.recordID.recordName }).joinWithSeparator(", "))") for record in savedRecords { // FIXME: Unsafe realm casting - try DBUtils.mark(type: self.objectClass as! BaseModel.Type, objectId: record.recordID.recordName, synced: true) + try DBUtils.mark(record.recordID.recordName, type: self.objectClass, synced: true) } } if let recordIDsToDelete = recordIDsToDelete { + print("recordIDsToDelete: \(recordIDsToDelete.map({ $0.recordName }).joinWithSeparator(", "))") for recordID in recordIDsToDelete { try deleteLocalRecord(recordID, objectClass: self.objectClass) } diff --git a/Potatso/Sync/RealmCloud.swift b/Potatso/Sync/RealmCloud.swift index b343baa..24d673e 100644 --- a/Potatso/Sync/RealmCloud.swift +++ b/Potatso/Sync/RealmCloud.swift @@ -9,12 +9,15 @@ import RealmSwift Set the `changeToken` for this `zoneID`. */ public func setZoneChangeToken(zoneID: CKRecordZoneID, changeToken: CKServerChangeToken?) { + let key = "\(zoneID.zoneName)_serverChangeToken" if let changeToken = changeToken { NSUserDefaults.standardUserDefaults().setObject( NSKeyedArchiver.archivedDataWithRootObject(changeToken), - forKey:"\(zoneID.zoneName)_serverChangeToken") - NSUserDefaults.standardUserDefaults().synchronize() + forKey: key) + } else { + NSUserDefaults.standardUserDefaults().removeObjectForKey(key) } + NSUserDefaults.standardUserDefaults().synchronize() } /** diff --git a/Potatso/Sync/SyncManager.swift b/Potatso/Sync/SyncManager.swift index f078d61..614a4a3 100644 --- a/Potatso/Sync/SyncManager.swift +++ b/Potatso/Sync/SyncManager.swift @@ -16,7 +16,8 @@ public enum SyncServiceType: String { public protocol SyncServiceProtocol { func setup(completion: (ErrorType? -> Void)?) - func sync() + func sync(manually: Bool) + func stop() } public class SyncManager { @@ -38,6 +39,7 @@ public class SyncManager { guard currentSyncServiceType != new else { return } + getCurrentSyncService()?.stop() NSUserDefaults.standardUserDefaults().setObject(new.rawValue, forKey: SyncManager.serviceTypeKey) NSUserDefaults.standardUserDefaults().synchronize() NSNotificationCenter.defaultCenter().postNotificationName(SyncManager.syncServiceChangedNotification, object: nil) @@ -79,15 +81,19 @@ public class SyncManager { extension SyncManager { func setupNewService(type: SyncServiceType, completion: (ErrorType? -> Void)?) { - getSyncService(forType: type)?.setup(completion) + if let service = getSyncService(forType: type) { + service.setup(completion) + } else { + completion?(nil) + } } func setup(completion: (ErrorType? -> Void)?) { getCurrentSyncService()?.setup(completion) } - func sync() { - getCurrentSyncService()?.sync() + func sync(manually: Bool = false) { + getCurrentSyncService()?.sync(manually) } } \ No newline at end of file diff --git a/Potatso/Sync/SyncOperation.swift b/Potatso/Sync/SyncOperation.swift index 26789cc..cc891f5 100644 --- a/Potatso/Sync/SyncOperation.swift +++ b/Potatso/Sync/SyncOperation.swift @@ -34,34 +34,20 @@ public enum SyncType: CustomStringConvertible { - completionHandler */ -public class SyncOperation: GroupOperation { +public class SyncOperation: GroupOperation { private var hasProducedAlert = false public init(zoneID: CKRecordZoneID, - objectClass: CloudKitRecord.Type, + objectClass: T.Type, syncType: SyncType, completionHandler: () -> Void) { - // Setup Conditions - - // ReachabilityCondition as written requires a URL so rather than rewriting, ping google - let url = NSURL(string: "http://www.apple.com")! - let reachabilityCondition = ReachabilityCondition(host: url) - - let container = CKContainer.defaultContainer() - let iCloudCapability = Capability(iCloudContainer(container: container)) - - // Setup common operations - let prepareZoneOperation = PrepareZoneOperation(zoneID: zoneID) let finishOperation = NSBlockOperation(block: completionHandler) - - prepareZoneOperation.addCondition(reachabilityCondition) - prepareZoneOperation.addCondition(iCloudCapability) - + // Setup operations relating to SyncType - let pushLocalChangesOperation: PushLocalChangesOperation - let fetchCloudChangesOperation: FetchCloudChangesOperation + let pushLocalChangesOperation: PushLocalChangesOperation + let fetchCloudChangesOperation: FetchCloudChangesOperation let operations: [NSOperation] switch syncType { @@ -69,22 +55,18 @@ public class SyncOperation: GroupOperation { pushLocalChangesOperation = PushLocalChangesOperation(zoneID: zoneID, objectClass: objectClass) - pushLocalChangesOperation.addDependency(prepareZoneOperation) finishOperation.addDependency(pushLocalChangesOperation) operations = [ - prepareZoneOperation, pushLocalChangesOperation, finishOperation] case .FetchCloudChanges: fetchCloudChangesOperation = FetchCloudChangesOperation(zoneID: zoneID, objectClass: objectClass) - fetchCloudChangesOperation.addDependency(prepareZoneOperation) finishOperation.addDependency(fetchCloudChangesOperation) operations = [ - prepareZoneOperation, fetchCloudChangesOperation, finishOperation] @@ -92,12 +74,10 @@ public class SyncOperation: GroupOperation { pushLocalChangesOperation = PushLocalChangesOperation(zoneID: zoneID, objectClass: objectClass) fetchCloudChangesOperation = FetchCloudChangesOperation(zoneID: zoneID, objectClass: objectClass) - pushLocalChangesOperation.addDependency(prepareZoneOperation) fetchCloudChangesOperation.addDependency(pushLocalChangesOperation) finishOperation.addDependency(fetchCloudChangesOperation) operations = [ - prepareZoneOperation, fetchCloudChangesOperation, pushLocalChangesOperation, finishOperation] diff --git a/Potatso/Sync/SyncVC.swift b/Potatso/Sync/SyncVC.swift index 7febc24..232cfca 100644 --- a/Potatso/Sync/SyncVC.swift +++ b/Potatso/Sync/SyncVC.swift @@ -47,25 +47,28 @@ class SyncVC: FormViewController { $0.selectorTitle = "Choose Sync Service".localized() }.onChange({ [weak self] (row) in if let type = row.value { + self?.showProgreeHUD() SyncManager.shared.setupNewService(type, completion: { (error) in + self?.hideHUD() if let error = error { if let vc = self { Alert.show(vc, title: "Setup Failed", message: "\((error as NSError).localizedDescription)") } } else { SyncManager.shared.currentSyncServiceType = type + SyncManager.shared.sync(true) } }) } }) section <<< ButtonRow { - $0.title = "Sync Now" + $0.title = "Sync Manually" $0.hidden = Condition.Function([""]) { form in return SyncManager.shared.currentSyncServiceType == .None } }.onCellSelection({ (cell, row) in - SyncManager.shared.sync() + SyncManager.shared.sync(true) }) return section } diff --git a/PotatsoModel/BaseModel.swift b/PotatsoModel/BaseModel.swift index a622e48..620ff85 100644 --- a/PotatsoModel/BaseModel.swift +++ b/PotatsoModel/BaseModel.swift @@ -10,7 +10,7 @@ import RealmSwift import PotatsoBase import CloudKit -private let version: UInt64 = 13 +private let version: UInt64 = 14 public var defaultRealm = try! Realm() public func setupDefaultReaml() { diff --git a/PotatsoModel/DBUtils.swift b/PotatsoModel/DBUtils.swift index eba1fdd..3601d45 100644 --- a/PotatsoModel/DBUtils.swift +++ b/PotatsoModel/DBUtils.swift @@ -12,57 +12,93 @@ import RealmSwift public class DBUtils { - public static func add(object: BaseModel, update: Bool = true) throws { - let realm = try! Realm() - realm.beginWrite() + private static func currentRealm(realm: Realm?) -> Realm { + var mRealm = realm + if mRealm == nil { + mRealm = try! Realm() + } + return mRealm! + } + + public static func add(object: BaseModel, update: Bool = true, inRealm realm: Realm? = nil) throws { + let mRealm = currentRealm(realm) + mRealm.beginWrite() object.setModified() - realm.add(object, update: update) - try realm.commitWrite() + mRealm.add(object, update: update) + try mRealm.commitWrite() } - public static func add(objects: S, update: Bool = true) throws { - let realm = try! Realm() - realm.beginWrite() + public static func add(objects: S, update: Bool = true, inRealm realm: Realm? = nil) throws { + let mRealm = currentRealm(realm) + mRealm.beginWrite() objects.forEach({ $0.setModified() }) - realm.add(objects, update: update) - try realm.commitWrite() + mRealm.add(objects, update: update) + try mRealm.commitWrite() } - public static func delete(object: BaseModel, update: Bool = true) throws { - let realm = try! Realm() - realm.beginWrite() + public static func softDelete(id: String, type: T.Type, inRealm realm: Realm? = nil) throws { + let mRealm = currentRealm(realm) + guard let object: T = DBUtils.get(id, inRealm: mRealm) else { + return + } + mRealm.beginWrite() object.deleted = true object.setModified() - try realm.commitWrite() + try mRealm.commitWrite() } - public static func delete(objects: S, update: Bool = true) throws { - let realm = try! Realm() - realm.beginWrite() - objects.forEach({ - $0.deleted = true - $0.setModified() - }) - try realm.commitWrite() + public static func softDelete(ids: [String], type: T.Type, inRealm realm: Realm? = nil) throws { + for id in ids { + try softDelete(id, type: type, inRealm: realm) + } } - public static func mark(object: BaseModel, synced: Bool) throws { - let realm = try! Realm() - realm.beginWrite() - object.synced = synced - try realm.commitWrite() + public static func hardDelete(id: String, type: T.Type, inRealm realm: Realm? = nil) throws { + let mRealm = currentRealm(realm) + print(type) + guard let object: T = DBUtils.get(id, inRealm: mRealm) else { + return + } + mRealm.beginWrite() + mRealm.delete(object) + try mRealm.commitWrite() } - public static func mark(type type: BaseModel.Type, objectId: String, synced: Bool) throws { - let realm = try! Realm() - guard let object = realm.objects(type).filter("uuid = '\(objectId)'").first else { + public static func hardDelete(ids: [String], type: T.Type, inRealm realm: Realm? = nil) throws { + for id in ids { + try hardDelete(id, type: type, inRealm: realm) + } + } + + public static func mark(id: String, type: T.Type, synced: Bool, inRealm realm: Realm? = nil) throws { + let mRealm = currentRealm(realm) + guard let object: T = DBUtils.get(id, inRealm: mRealm) else { return } - try mark(object, synced: synced) + mRealm.beginWrite() + object.synced = synced + try mRealm.commitWrite() } + public static func markAll(syncd: Bool) throws { + let mRealm = try! Realm() + mRealm.beginWrite() + for proxy in mRealm.objects(Proxy) { + proxy.synced = false + } + for rule in mRealm.objects(Rule) { + rule.synced = false + } + for ruleset in mRealm.objects(RuleSet) { + ruleset.synced = false + } + for group in mRealm.objects(ConfigurationGroup) { + group.synced = false + } + try mRealm.commitWrite() + } } @@ -70,29 +106,27 @@ public class DBUtils { extension DBUtils { public static func get(uuid: String, inRealm realm: Realm? = nil) -> T? { - var mRealm = realm - if mRealm == nil { - mRealm = try! Realm() - } - return mRealm?.objects(T).filter("uuid = '\(uuid)'").first + let mRealm = currentRealm(realm) + return mRealm.objects(T).filter("uuid = '\(uuid)'").first } - public static func modify(type: T.Type, id: String, modifyBlock: ((Realm, T) -> ErrorType?)) throws { - let realm = try! Realm() - guard let object: T = DBUtils.get(id, inRealm: realm) else { + public static func modify(type: T.Type, id: String, inRealm realm: Realm? = nil, modifyBlock: ((Realm, T) -> ErrorType?)) throws { + let mRealm = currentRealm(realm) + guard let object: T = DBUtils.get(id, inRealm: mRealm) else { return } - realm.beginWrite() - if let error = modifyBlock(realm, object) { + mRealm.beginWrite() + if let error = modifyBlock(mRealm, object) { throw error } do { - try object.validate(inRealm: realm) + try object.validate(inRealm: mRealm) }catch { - realm.cancelWrite() + mRealm.cancelWrite() throw error } - try realm.commitWrite() + object.setModified() + try mRealm.commitWrite() } } diff --git a/PotatsoModel/Rule.swift b/PotatsoModel/Rule.swift index f7772df..8001f63 100644 --- a/PotatsoModel/Rule.swift +++ b/PotatsoModel/Rule.swift @@ -114,7 +114,7 @@ extension Rule { public var type : RuleType { get { - return RuleType(rawValue: typeRaw) ?? .URL + return RuleType(rawValue: typeRaw) ?? .DomainSuffix } set(v) { typeRaw = v.rawValue diff --git a/PotatsoModel/RuleSet.swift b/PotatsoModel/RuleSet.swift index 6982bc6..009be63 100644 --- a/PotatsoModel/RuleSet.swift +++ b/PotatsoModel/RuleSet.swift @@ -32,7 +32,7 @@ extension RuleSetError: CustomStringConvertible { public final class RuleSet: BaseModel { public dynamic var editable = true public dynamic var name = "" - public dynamic var updateAt = NSDate().timeIntervalSince1970 + public dynamic var remoteUpdatedAt: NSTimeInterval = NSDate().timeIntervalSince1970 public dynamic var desc = "" public dynamic var ruleCount = 0 public let rules = List() From 189ca3c1d577d022ca2baa19e6d6205dbf25a91e Mon Sep 17 00:00:00 2001 From: iCodesign Date: Wed, 10 Aug 2016 00:58:12 +0800 Subject: [PATCH 6/8] Fix sync once issue --- Potatso/Core/API.swift | 2 +- Potatso/Sync/CloudKitRecord.swift | 16 ++++-- Potatso/Sync/FetchCloudChangesOperation.swift | 15 +++--- Potatso/Sync/ICloudSyncService.swift | 26 ++------- Potatso/Sync/PushLocalChangesOperation.swift | 28 ++++------ Potatso/Sync/SyncOperation.swift | 21 +++----- PotatsoModel/DBUtils.swift | 53 +++++++++++++++---- 7 files changed, 85 insertions(+), 76 deletions(-) diff --git a/Potatso/Core/API.swift b/Potatso/Core/API.swift index 05e549a..0877832 100644 --- a/Potatso/Core/API.swift +++ b/Potatso/Core/API.swift @@ -82,7 +82,7 @@ extension RuleSet { static func addRemoteObject(ruleset: RuleSet, update: Bool = true) throws { ruleset.isSubscribe = true let id = ruleset.uuid - guard let local: RuleSet = DBUtils.get(id) else { + guard let local = DBUtils.get(id, type: RuleSet.self) else { try DBUtils.add(ruleset) return } diff --git a/Potatso/Sync/CloudKitRecord.swift b/Potatso/Sync/CloudKitRecord.swift index 4f9b1df..904aa3f 100644 --- a/Potatso/Sync/CloudKitRecord.swift +++ b/Potatso/Sync/CloudKitRecord.swift @@ -205,10 +205,13 @@ extension CKRecord { } -func changeLocalRecord(record: CKRecord, objectClass: T.Type) throws { - let realm = try! Realm() +func changeLocalRecord(record: CKRecord) throws { let realmObject: BaseModel - let local: T? = DBUtils.get(record.recordID.recordName, inRealm: realm) + guard let type = record.realmClassType else { + return + } + let id = record.recordID.recordName + let local: BaseModel? = DBUtils.get(id, type: type) switch record.recordType { case "Proxy": realmObject = Proxy.fromCloudKitRecord(record) @@ -237,10 +240,13 @@ func changeLocalRecord(record: CKRecord, o try DBUtils.add(realmObject) } -func deleteLocalRecord(recordID: CKRecordID, objectClass: T.Type) throws { +func deleteLocalRecord(recordID: CKRecordID) throws { let id = recordID.recordName // FIXME: Unsafe realm casting print("Deleting local record.") - try DBUtils.hardDelete(id, type: objectClass) + try DBUtils.hardDelete(id, type: Proxy.self) + try DBUtils.hardDelete(id, type: Rule.self) + try DBUtils.hardDelete(id, type: RuleSet.self) + try DBUtils.hardDelete(id, type: ConfigurationGroup.self) } diff --git a/Potatso/Sync/FetchCloudChangesOperation.swift b/Potatso/Sync/FetchCloudChangesOperation.swift index 3cfe071..b1a4229 100644 --- a/Potatso/Sync/FetchCloudChangesOperation.swift +++ b/Potatso/Sync/FetchCloudChangesOperation.swift @@ -12,7 +12,7 @@ struct FetchResults { } } -class FetchCloudChangesOperation: Operation { +class FetchCloudChangesOperation: Operation { let zoneID: CKRecordZoneID var changeToken: CKServerChangeToken? @@ -20,16 +20,13 @@ class FetchCloudChangesOperation: Operatio let delayOperationQueue = OperationQueue() let maximumRetryAttempts: Int var retryAttempts: Int = 0 - - let objectClass: T.Type - - init(zoneID: CKRecordZoneID, objectClass: T.Type, maximumRetryAttempts: Int = 3) { + + init(zoneID: CKRecordZoneID, maximumRetryAttempts: Int = 3) { self.zoneID = zoneID - self.objectClass = objectClass self.maximumRetryAttempts = maximumRetryAttempts super.init() - name = "Fetch Cloud Changes of \(objectClass)" + name = "Fetch Cloud Changes" } override func execute() { @@ -102,12 +99,12 @@ class FetchCloudChangesOperation: Operatio do { print("changedRecords: \(results.changedRecords.map({ $0.recordID.recordName }))") for record in results.changedRecords { - try changeLocalRecord(record, objectClass: self.objectClass) + try changeLocalRecord(record) } print("deletedRecordIDs: \(results.deletedRecordIDs.map({ $0.recordName }))") for recordID in results.deletedRecordIDs { - try deleteLocalRecord(recordID, objectClass: self.objectClass) + try deleteLocalRecord(recordID) } } catch let realmError as NSError { error = realmError diff --git a/Potatso/Sync/ICloudSyncService.swift b/Potatso/Sync/ICloudSyncService.swift index 34bff42..304922f 100644 --- a/Potatso/Sync/ICloudSyncService.swift +++ b/Potatso/Sync/ICloudSyncService.swift @@ -28,31 +28,15 @@ class ICloudSyncService: SyncServiceProtocol { func sync(manually: Bool = false) { if manually { setZoneChangeToken(potatsoZoneId, changeToken: nil) - _ = try? DBUtils.markAll(false) + _ = try? DBUtils.markAll(syncd: false) } let setupOp = ICloudSetupOperation(completion: nil) - - let proxySyncOp = SyncOperation(zoneID: potatsoZoneId, objectClass: Proxy.self, syncType: SyncType.FetchCloudChangesAndThenPushLocalChanges) { - print("<<<<<<<<< sync proxies completed") - } - let ruleSyncOp = SyncOperation(zoneID: potatsoZoneId, objectClass: Rule.self, syncType: SyncType.FetchCloudChangesAndThenPushLocalChanges) { - print("<<<<<<<<< sync rules completed") - } - ruleSyncOp.addDependency(proxySyncOp) - let ruleSetSyncOp = SyncOperation(zoneID: potatsoZoneId, objectClass: RuleSet.self, syncType: SyncType.FetchCloudChangesAndThenPushLocalChanges) { - print("<<<<<<<<< sync rulesets completed") + let syncOp = SyncOperation(zoneID: potatsoZoneId, syncType: SyncType.FetchCloudChangesAndThenPushLocalChanges) { + print("<<<<<<<<< sync completed") } - ruleSetSyncOp.addDependency(ruleSyncOp) - let configGroupSyncOp = SyncOperation(zoneID: potatsoZoneId, objectClass: ConfigurationGroup.self, syncType: SyncType.FetchCloudChangesAndThenPushLocalChanges) { - print("<<<<<<<<< sync config groups completed") - } - configGroupSyncOp.addDependency(ruleSetSyncOp) - + operationQueue.addOperation(setupOp) - operationQueue.addOperation(proxySyncOp) - operationQueue.addOperation(ruleSyncOp) - operationQueue.addOperation(ruleSetSyncOp) - operationQueue.addOperation(configGroupSyncOp) + operationQueue.addOperation(syncOp) } func subscribe() { diff --git a/Potatso/Sync/PushLocalChangesOperation.swift b/Potatso/Sync/PushLocalChangesOperation.swift index 5d49c17..484a94d 100644 --- a/Potatso/Sync/PushLocalChangesOperation.swift +++ b/Potatso/Sync/PushLocalChangesOperation.swift @@ -3,7 +3,7 @@ import RealmSwift import CloudKit import PSOperations -class PushLocalChangesOperation: Operation { +class PushLocalChangesOperation: Operation { let zoneID: CKRecordZoneID var recordsToSave: [CKRecord]? @@ -12,37 +12,29 @@ class PushLocalChangesOperation: Operation let delayOperationQueue = OperationQueue() let maximumRetryAttempts: Int var retryAttempts: Int = 0 - - let objectClass: T.Type - - init(zoneID: CKRecordZoneID, objectClass: T.Type, maximumRetryAttempts: Int = 3) { + + init(zoneID: CKRecordZoneID, maximumRetryAttempts: Int = 3) { self.zoneID = zoneID - self.objectClass = objectClass self.maximumRetryAttempts = maximumRetryAttempts super.init() - name = "Push Local Changes of \(objectClass)" + name = "Push Local Changes" } override func execute() { print(">>>>>>>>> \(self.name!) started") - // Query records - let realm = try! Realm() - // FIXME: Unsafe realm casting - let toSyncObjects = realm.objects(self.objectClass) - .filter("synced == false && deleted == false") - let toDeleteObjects = realm.objects(self.objectClass) - .filter("synced == false && deleted == true") + let toSyncObjects = DBUtils.allObjectsToSyncModified() + let toDeleteObjects = DBUtils.allObjectsToSyncDeleted() print("toSyncObjects: \(toSyncObjects.map({ $0.uuid }).joinWithSeparator(", "))") print("toDeleteObjects: \(toDeleteObjects.map({ $0.uuid }).joinWithSeparator(", "))") self.recordsToSave = toSyncObjects.map { - $0.toCloudKitRecord() + ($0 as! CloudKitRecord).toCloudKitRecord() } self.recordIDsToDelete = toDeleteObjects.map { - $0.recordId + ($0 as! CloudKitRecord).recordId } modifyRecords(self.recordsToSave, recordIDsToDelete: self.recordIDsToDelete) { @@ -76,14 +68,14 @@ class PushLocalChangesOperation: Operation print("savedRecords: \(savedRecords.map({ $0.recordID.recordName }).joinWithSeparator(", "))") for record in savedRecords { // FIXME: Unsafe realm casting - try DBUtils.mark(record.recordID.recordName, type: self.objectClass, synced: true) + try DBUtils.mark(record.recordID.recordName, type: record.realmClassType!, synced: true) } } if let recordIDsToDelete = recordIDsToDelete { print("recordIDsToDelete: \(recordIDsToDelete.map({ $0.recordName }).joinWithSeparator(", "))") for recordID in recordIDsToDelete { - try deleteLocalRecord(recordID, objectClass: self.objectClass) + try deleteLocalRecord(recordID) } } diff --git a/Potatso/Sync/SyncOperation.swift b/Potatso/Sync/SyncOperation.swift index cc891f5..6657221 100644 --- a/Potatso/Sync/SyncOperation.swift +++ b/Potatso/Sync/SyncOperation.swift @@ -34,26 +34,22 @@ public enum SyncType: CustomStringConvertible { - completionHandler */ -public class SyncOperation: GroupOperation { +public class SyncOperation: GroupOperation { private var hasProducedAlert = false - public init(zoneID: CKRecordZoneID, - objectClass: T.Type, - syncType: SyncType, - completionHandler: () -> Void) { + public init(zoneID: CKRecordZoneID, syncType: SyncType, completionHandler: () -> Void) { let finishOperation = NSBlockOperation(block: completionHandler) // Setup operations relating to SyncType - let pushLocalChangesOperation: PushLocalChangesOperation - let fetchCloudChangesOperation: FetchCloudChangesOperation + let pushLocalChangesOperation: PushLocalChangesOperation + let fetchCloudChangesOperation: FetchCloudChangesOperation let operations: [NSOperation] switch syncType { case .PushLocalChanges: - pushLocalChangesOperation = PushLocalChangesOperation(zoneID: zoneID, - objectClass: objectClass) + pushLocalChangesOperation = PushLocalChangesOperation(zoneID: zoneID) finishOperation.addDependency(pushLocalChangesOperation) @@ -62,8 +58,7 @@ public class SyncOperation: GroupOperation finishOperation] case .FetchCloudChanges: - fetchCloudChangesOperation = FetchCloudChangesOperation(zoneID: zoneID, objectClass: objectClass) - + fetchCloudChangesOperation = FetchCloudChangesOperation(zoneID: zoneID) finishOperation.addDependency(fetchCloudChangesOperation) operations = [ @@ -71,8 +66,8 @@ public class SyncOperation: GroupOperation finishOperation] case .FetchCloudChangesAndThenPushLocalChanges: - pushLocalChangesOperation = PushLocalChangesOperation(zoneID: zoneID, objectClass: objectClass) - fetchCloudChangesOperation = FetchCloudChangesOperation(zoneID: zoneID, objectClass: objectClass) + pushLocalChangesOperation = PushLocalChangesOperation(zoneID: zoneID) + fetchCloudChangesOperation = FetchCloudChangesOperation(zoneID: zoneID) fetchCloudChangesOperation.addDependency(pushLocalChangesOperation) finishOperation.addDependency(fetchCloudChangesOperation) diff --git a/PotatsoModel/DBUtils.swift b/PotatsoModel/DBUtils.swift index 3601d45..af9e089 100644 --- a/PotatsoModel/DBUtils.swift +++ b/PotatsoModel/DBUtils.swift @@ -40,7 +40,7 @@ public class DBUtils { public static func softDelete(id: String, type: T.Type, inRealm realm: Realm? = nil) throws { let mRealm = currentRealm(realm) - guard let object: T = DBUtils.get(id, inRealm: mRealm) else { + guard let object: T = DBUtils.get(id, type: type, inRealm: mRealm) else { return } mRealm.beginWrite() @@ -58,7 +58,7 @@ public class DBUtils { public static func hardDelete(id: String, type: T.Type, inRealm realm: Realm? = nil) throws { let mRealm = currentRealm(realm) print(type) - guard let object: T = DBUtils.get(id, inRealm: mRealm) else { + guard let object: T = DBUtils.get(id, type: type, inRealm: mRealm) else { return } mRealm.beginWrite() @@ -74,7 +74,7 @@ public class DBUtils { public static func mark(id: String, type: T.Type, synced: Bool, inRealm realm: Realm? = nil) throws { let mRealm = currentRealm(realm) - guard let object: T = DBUtils.get(id, inRealm: mRealm) else { + guard let object: T = DBUtils.get(id, type: type, inRealm: mRealm) else { return } mRealm.beginWrite() @@ -82,7 +82,7 @@ public class DBUtils { try mRealm.commitWrite() } - public static func markAll(syncd: Bool) throws { + public static func markAll(syncd syncd: Bool) throws { let mRealm = try! Realm() mRealm.beginWrite() for proxy in mRealm.objects(Proxy) { @@ -105,14 +105,14 @@ public class DBUtils { // Query extension DBUtils { - public static func get(uuid: String, inRealm realm: Realm? = nil) -> T? { + public static func get(uuid: String, type: T.Type, inRealm realm: Realm? = nil) -> T? { let mRealm = currentRealm(realm) - return mRealm.objects(T).filter("uuid = '\(uuid)'").first + return mRealm.objects(type).filter("uuid = '\(uuid)'").first } public static func modify(type: T.Type, id: String, inRealm realm: Realm? = nil, modifyBlock: ((Realm, T) -> ErrorType?)) throws { let mRealm = currentRealm(realm) - guard let object: T = DBUtils.get(id, inRealm: mRealm) else { + guard let object: T = DBUtils.get(id, type: type, inRealm: mRealm) else { return } mRealm.beginWrite() @@ -131,6 +131,40 @@ extension DBUtils { } +// Sync +extension DBUtils { + + public static func allObjectsToSyncModified() -> [BaseModel] { + let mRealm = currentRealm(nil) + let filter = "synced == false && deleted == false" + let proxies = mRealm.objects(Proxy.self).filter(filter).map({ $0 }) + let rules = mRealm.objects(Rule.self).filter(filter).map({ $0 }) + let rulesets = mRealm.objects(RuleSet.self).filter(filter).map({ $0 }) + let groups = mRealm.objects(ConfigurationGroup.self).filter(filter).map({ $0 }) + var objects: [BaseModel] = [] + objects.appendContentsOf(proxies as [BaseModel]) + objects.appendContentsOf(rules as [BaseModel]) + objects.appendContentsOf(rulesets as [BaseModel]) + objects.appendContentsOf(groups as [BaseModel]) + return objects + } + + public static func allObjectsToSyncDeleted() -> [BaseModel] { + let mRealm = currentRealm(nil) + let filter = "synced == false && deleted == true" + let proxies = mRealm.objects(Proxy.self).filter(filter).map({ $0 }) + let rules = mRealm.objects(Rule.self).filter(filter).map({ $0 }) + let rulesets = mRealm.objects(RuleSet.self).filter(filter).map({ $0 }) + let groups = mRealm.objects(ConfigurationGroup.self).filter(filter).map({ $0 }) + var objects: [BaseModel] = [] + objects.appendContentsOf(proxies as [BaseModel]) + objects.appendContentsOf(rules as [BaseModel]) + objects.appendContentsOf(rulesets as [BaseModel]) + objects.appendContentsOf(groups as [BaseModel]) + return objects + } +} + // BaseModel API extension BaseModel { @@ -148,7 +182,7 @@ extension ConfigurationGroup { public static func changeProxy(forGroupId groupId: String, proxyId: String?) throws { try DBUtils.modify(ConfigurationGroup.self, id: groupId) { (realm, group) -> ErrorType? in group.proxies.removeAll() - if let proxyId = proxyId, proxy: Proxy = DBUtils.get(proxyId, inRealm: realm){ + if let proxyId = proxyId, proxy = DBUtils.get(proxyId, type: Proxy.self, inRealm: realm){ group.proxies.append(proxy) } return nil @@ -157,7 +191,7 @@ extension ConfigurationGroup { public static func appendRuleSet(forGroupId groupId: String, rulesetId: String) throws { try DBUtils.modify(ConfigurationGroup.self, id: groupId) { (realm, group) -> ErrorType? in - if let ruleset: RuleSet = DBUtils.get(rulesetId, inRealm: realm) { + if let ruleset = DBUtils.get(rulesetId, type: RuleSet.self, inRealm: realm) { group.ruleSets.append(ruleset) } return nil @@ -179,3 +213,4 @@ extension ConfigurationGroup { } } + From 72593110e1aea0976188b039ab8aafed0933fe14 Mon Sep 17 00:00:00 2001 From: iCodesign Date: Wed, 10 Aug 2016 10:45:18 +0800 Subject: [PATCH 7/8] New YF Build --- PacketProcessor/Info.plist | 4 ++-- PacketTunnel/Info.plist | 4 ++-- Potatso.xcodeproj/project.pbxproj | 32 +++++++++++++++---------------- Potatso/Info.plist | 4 ++-- PotatsoBase/Info.plist | 4 ++-- PotatsoBase/PotatsoBase.h | 1 - PotatsoLibrary/Info.plist | 4 ++-- PotatsoLibraryTests/Info.plist | 4 ++-- PotatsoModel/Info.plist | 4 ++-- PotatsoTests/Info.plist | 4 ++-- TodayWidget/Info.plist | 4 ++-- 11 files changed, 34 insertions(+), 35 deletions(-) diff --git a/PacketProcessor/Info.plist b/PacketProcessor/Info.plist index ea871b7..7e697a4 100644 --- a/PacketProcessor/Info.plist +++ b/PacketProcessor/Info.plist @@ -15,11 +15,11 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 1.4.5 + 1.4.6 CFBundleSignature ???? CFBundleVersion - 114 + 116 NSPrincipalClass diff --git a/PacketTunnel/Info.plist b/PacketTunnel/Info.plist index cc5c7bf..50ac9a3 100644 --- a/PacketTunnel/Info.plist +++ b/PacketTunnel/Info.plist @@ -17,11 +17,11 @@ CFBundlePackageType XPC! CFBundleShortVersionString - 1.4.5 + 1.4.6 CFBundleSignature ???? CFBundleVersion - 114 + 116 Fabric APIKey diff --git a/Potatso.xcodeproj/project.pbxproj b/Potatso.xcodeproj/project.pbxproj index cc6b6d6..bcc31b6 100644 --- a/Potatso.xcodeproj/project.pbxproj +++ b/Potatso.xcodeproj/project.pbxproj @@ -3526,10 +3526,10 @@ APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 114; + CURRENT_PROJECT_VERSION = 116; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 114; + DYLIB_CURRENT_VERSION = 116; DYLIB_INSTALL_NAME_BASE = "@rpath"; EMBEDDED_CONTENT_CONTAINS_SWIFT = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -3558,10 +3558,10 @@ APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 114; + CURRENT_PROJECT_VERSION = 116; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 114; + DYLIB_CURRENT_VERSION = 116; DYLIB_INSTALL_NAME_BASE = "@rpath"; EMBEDDED_CONTENT_CONTAINS_SWIFT = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -3590,11 +3590,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 114; + CURRENT_PROJECT_VERSION = 116; DEBUG_INFORMATION_FORMAT = dwarf; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 114; + DYLIB_CURRENT_VERSION = 116; DYLIB_INSTALL_NAME_BASE = "@rpath"; EMBEDDED_CONTENT_CONTAINS_SWIFT = NO; INFOPLIST_FILE = PotatsoBase/Info.plist; @@ -3618,10 +3618,10 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 114; + CURRENT_PROJECT_VERSION = 116; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 114; + DYLIB_CURRENT_VERSION = 116; DYLIB_INSTALL_NAME_BASE = "@rpath"; EMBEDDED_CONTENT_CONTAINS_SWIFT = NO; INFOPLIST_FILE = PotatsoBase/Info.plist; @@ -3645,11 +3645,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 114; + CURRENT_PROJECT_VERSION = 116; DEBUG_INFORMATION_FORMAT = dwarf; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 114; + DYLIB_CURRENT_VERSION = 116; DYLIB_INSTALL_NAME_BASE = "@rpath"; EMBEDDED_CONTENT_CONTAINS_SWIFT = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -3678,10 +3678,10 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 114; + CURRENT_PROJECT_VERSION = 116; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 114; + DYLIB_CURRENT_VERSION = 116; DYLIB_INSTALL_NAME_BASE = "@rpath"; EMBEDDED_CONTENT_CONTAINS_SWIFT = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -3708,11 +3708,11 @@ CLANG_ANALYZER_NONNULL = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 114; + CURRENT_PROJECT_VERSION = 116; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 114; + DYLIB_CURRENT_VERSION = 116; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_BITCODE = NO; GCC_PREPROCESSOR_DEFINITIONS = ( @@ -3749,10 +3749,10 @@ CLANG_ANALYZER_NONNULL = YES; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 114; + CURRENT_PROJECT_VERSION = 116; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 114; + DYLIB_CURRENT_VERSION = 116; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_BITCODE = NO; GCC_PREPROCESSOR_DEFINITIONS = ( diff --git a/Potatso/Info.plist b/Potatso/Info.plist index 4b0c5c4..4486313 100644 --- a/Potatso/Info.plist +++ b/Potatso/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.4.5 + 1.4.6 CFBundleSignature ???? CFBundleURLTypes @@ -32,7 +32,7 @@ CFBundleVersion - 114 + 116 Fabric APIKey diff --git a/PotatsoBase/Info.plist b/PotatsoBase/Info.plist index ea871b7..7e697a4 100644 --- a/PotatsoBase/Info.plist +++ b/PotatsoBase/Info.plist @@ -15,11 +15,11 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 1.4.5 + 1.4.6 CFBundleSignature ???? CFBundleVersion - 114 + 116 NSPrincipalClass diff --git a/PotatsoBase/PotatsoBase.h b/PotatsoBase/PotatsoBase.h index 0f579ea..8d927f3 100644 --- a/PotatsoBase/PotatsoBase.h +++ b/PotatsoBase/PotatsoBase.h @@ -20,4 +20,3 @@ FOUNDATION_EXPORT const unsigned char PotatsoBaseVersionString[]; #import "JSONUtils.h" #import "NSError+Helper.h" #import "Settings.h" -#import "PotatsoBase-Swift.h" \ No newline at end of file diff --git a/PotatsoLibrary/Info.plist b/PotatsoLibrary/Info.plist index ea871b7..7e697a4 100644 --- a/PotatsoLibrary/Info.plist +++ b/PotatsoLibrary/Info.plist @@ -15,11 +15,11 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 1.4.5 + 1.4.6 CFBundleSignature ???? CFBundleVersion - 114 + 116 NSPrincipalClass diff --git a/PotatsoLibraryTests/Info.plist b/PotatsoLibraryTests/Info.plist index fd3c153..7388539 100644 --- a/PotatsoLibraryTests/Info.plist +++ b/PotatsoLibraryTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.4.5 + 1.4.6 CFBundleSignature ???? CFBundleVersion - 114 + 116 diff --git a/PotatsoModel/Info.plist b/PotatsoModel/Info.plist index ea871b7..7e697a4 100644 --- a/PotatsoModel/Info.plist +++ b/PotatsoModel/Info.plist @@ -15,11 +15,11 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 1.4.5 + 1.4.6 CFBundleSignature ???? CFBundleVersion - 114 + 116 NSPrincipalClass diff --git a/PotatsoTests/Info.plist b/PotatsoTests/Info.plist index fd3c153..7388539 100644 --- a/PotatsoTests/Info.plist +++ b/PotatsoTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.4.5 + 1.4.6 CFBundleSignature ???? CFBundleVersion - 114 + 116 diff --git a/TodayWidget/Info.plist b/TodayWidget/Info.plist index b2c612f..0f63442 100644 --- a/TodayWidget/Info.plist +++ b/TodayWidget/Info.plist @@ -17,11 +17,11 @@ CFBundlePackageType XPC! CFBundleShortVersionString - 1.4.5 + 1.4.6 CFBundleSignature ???? CFBundleVersion - 114 + 116 NSExtension NSExtensionPointIdentifier From 9e46119084972df445bb63486f70d2fcba098da1 Mon Sep 17 00:00:00 2001 From: iCodesign Date: Wed, 10 Aug 2016 11:18:52 +0800 Subject: [PATCH 8/8] Silent sync notification --- Potatso.xcodeproj/project.pbxproj | 4 ++-- Potatso/Sync/ICloudSyncService.swift | 15 ++++++++++++--- PotatsoLibrary/Manager.swift | 6 +++--- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/Potatso.xcodeproj/project.pbxproj b/Potatso.xcodeproj/project.pbxproj index bcc31b6..d304087 100644 --- a/Potatso.xcodeproj/project.pbxproj +++ b/Potatso.xcodeproj/project.pbxproj @@ -3318,7 +3318,7 @@ OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" -DDEBUG"; PRODUCT_BUNDLE_IDENTIFIER = com.touchingapp.potatso; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE = ""; + PROVISIONING_PROFILE = "419c75b8-4b16-43ab-84ac-e4490e1db11d"; SWIFT_OBJC_BRIDGING_HEADER = "Potatso/Potatso-Bridge-Header.h"; TARGETED_DEVICE_FAMILY = "1,2"; USER_HEADER_SEARCH_PATHS = ""; @@ -3361,7 +3361,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.touchingapp.potatso; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE = ""; + PROVISIONING_PROFILE = "419c75b8-4b16-43ab-84ac-e4490e1db11d"; SWIFT_OBJC_BRIDGING_HEADER = "Potatso/Potatso-Bridge-Header.h"; TARGETED_DEVICE_FAMILY = "1,2"; USER_HEADER_SEARCH_PATHS = ""; diff --git a/Potatso/Sync/ICloudSyncService.swift b/Potatso/Sync/ICloudSyncService.swift index 304922f..9568671 100644 --- a/Potatso/Sync/ICloudSyncService.swift +++ b/Potatso/Sync/ICloudSyncService.swift @@ -21,8 +21,12 @@ class ICloudSyncService: SyncServiceProtocol { func setup(completion: (ErrorType? -> Void)?) { let setupOp = ICloudSetupOperation(completion: completion) + let subscribeOp = BlockOperation { + self.subscribe() + } + subscribeOp.addDependency(setupOp) operationQueue.addOperation(setupOp) - subscribe() + operationQueue.addOperation(subscribeOp) } func sync(manually: Bool = false) { @@ -31,10 +35,15 @@ class ICloudSyncService: SyncServiceProtocol { _ = try? DBUtils.markAll(syncd: false) } let setupOp = ICloudSetupOperation(completion: nil) + let subscribeOp = BlockOperation { + self.subscribe() + } let syncOp = SyncOperation(zoneID: potatsoZoneId, syncType: SyncType.FetchCloudChangesAndThenPushLocalChanges) { print("<<<<<<<<< sync completed") } - + + subscribeOp.addDependency(setupOp) + operationQueue.addOperation(subscribeOp) operationQueue.addOperation(setupOp) operationQueue.addOperation(syncOp) } @@ -42,7 +51,7 @@ class ICloudSyncService: SyncServiceProtocol { func subscribe() { let subscription = CKSubscription(zoneID: potatsoZoneId, subscriptionID: potatsoSubscriptionId, options: CKSubscriptionOptions(rawValue: 0)) let info = CKNotificationInfo() - info.alertBody = "Potatso iCloud updated" + info.shouldSendContentAvailable = true subscription.notificationInfo = info potatsoDB.saveSubscription(subscription) { (sub, error) in diff --git a/PotatsoLibrary/Manager.swift b/PotatsoLibrary/Manager.swift index 9aab268..82f8b27 100644 --- a/PotatsoLibrary/Manager.swift +++ b/PotatsoLibrary/Manager.swift @@ -132,9 +132,9 @@ public class Manager { } let toURL = Potatso.sharedUrl().URLByAppendingPathComponent("GeoLite2-Country.mmdb") if NSFileManager.defaultManager().fileExistsAtPath(fromURL.path!) { -// if NSFileManager.defaultManager().fileExistsAtPath(toURL.path!) { -// try NSFileManager.defaultManager().removeItemAtURL(toURL) -// } + if NSFileManager.defaultManager().fileExistsAtPath(toURL.path!) { + try NSFileManager.defaultManager().removeItemAtURL(toURL) + } try NSFileManager.defaultManager().copyItemAtURL(fromURL, toURL: toURL) } }