From df6851f664b05f99b9b07c0bb91a66798b005430 Mon Sep 17 00:00:00 2001 From: Richard Ross Date: Fri, 18 Mar 2016 19:45:59 -0700 Subject: [PATCH] =?UTF-8?q?(=E3=81=A3=CB=98=E2=96=BD=CB=98)=E3=81=A3=20?= =?UTF-8?q?=E2=98=81=EF=B8=8F=E2=98=81=EF=B8=8F=E2=98=81=EF=B8=8F=20?= =?UTF-8?q?=E2=8A=82(=E2=97=95=E3=80=82=E2=97=95=E2=8A=82)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 104 ++++ .swiftlint.yml | 9 + .travis.yml | 43 ++ CONTRIBUTING.md | 61 +++ .../LiveQueryDemo.xcodeproj/project.pbxproj | 324 ++++++++++++ .../xcschemes/LiveQueryDemo.xcscheme | 91 ++++ Examples/LiveQueryDemo/Message.swift | 22 + Examples/LiveQueryDemo/Room.swift | 18 + Examples/LiveQueryDemo/main.swift | 130 +++++ LICENSE | 30 ++ PATENTS | 33 ++ ParseLiveQuery.podspec | 22 + .../contents.xcworkspacedata | 21 + .../xcshareddata/WorkspaceSettings.xcsettings | 8 + Podfile | 29 + Podfile.lock | 21 + .../ParseLiveQuery.xcodeproj/project.pbxproj | 495 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcschemes/ParseLiveQuery-OSX.xcscheme | 80 +++ .../xcschemes/ParseLiveQuery-iOS.xcscheme | 80 +++ Sources/ParseLiveQuery/Client.swift | 227 ++++++++ Sources/ParseLiveQuery/Info.plist | 24 + .../Internal/BoltsHelpers.swift | 45 ++ .../Internal/ClientPrivate.swift | 263 ++++++++++ Sources/ParseLiveQuery/Internal/Errors.swift | 51 ++ .../ParseLiveQuery/Internal/Operation.swift | 99 ++++ .../Internal/QueryEncoder.swift | 29 + Sources/ParseLiveQuery/ObjCCompat.swift | 340 ++++++++++++ .../ParseLiveQuery/PFQuery+Subscribe.swift | 26 + Sources/ParseLiveQuery/Subscription.swift | 250 +++++++++ 30 files changed, 2982 insertions(+) create mode 100644 .gitignore create mode 100644 .swiftlint.yml create mode 100644 .travis.yml create mode 100644 CONTRIBUTING.md create mode 100644 Examples/LiveQueryDemo.xcodeproj/project.pbxproj create mode 100644 Examples/LiveQueryDemo.xcodeproj/xcshareddata/xcschemes/LiveQueryDemo.xcscheme create mode 100644 Examples/LiveQueryDemo/Message.swift create mode 100644 Examples/LiveQueryDemo/Room.swift create mode 100644 Examples/LiveQueryDemo/main.swift create mode 100644 LICENSE create mode 100644 PATENTS create mode 100644 ParseLiveQuery.podspec create mode 100644 ParseLiveQuery.xcworkspace/contents.xcworkspacedata create mode 100644 ParseLiveQuery.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 Podfile create mode 100644 Podfile.lock create mode 100644 Sources/ParseLiveQuery.xcodeproj/project.pbxproj create mode 100644 Sources/ParseLiveQuery.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 Sources/ParseLiveQuery.xcodeproj/xcshareddata/xcschemes/ParseLiveQuery-OSX.xcscheme create mode 100644 Sources/ParseLiveQuery.xcodeproj/xcshareddata/xcschemes/ParseLiveQuery-iOS.xcscheme create mode 100644 Sources/ParseLiveQuery/Client.swift create mode 100644 Sources/ParseLiveQuery/Info.plist create mode 100644 Sources/ParseLiveQuery/Internal/BoltsHelpers.swift create mode 100644 Sources/ParseLiveQuery/Internal/ClientPrivate.swift create mode 100644 Sources/ParseLiveQuery/Internal/Errors.swift create mode 100644 Sources/ParseLiveQuery/Internal/Operation.swift create mode 100644 Sources/ParseLiveQuery/Internal/QueryEncoder.swift create mode 100644 Sources/ParseLiveQuery/ObjCCompat.swift create mode 100644 Sources/ParseLiveQuery/PFQuery+Subscribe.swift create mode 100644 Sources/ParseLiveQuery/Subscription.swift diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..e02fd3eb --- /dev/null +++ b/.gitignore @@ -0,0 +1,104 @@ +# Created by https://www.gitignore.io/api/osx,objective-c,xcode + +### OSX ### +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + + +### Objective-C ### +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## Build generated +build/ +DerivedData + +## Various settings +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata + +## Other +*.xccheckout +*.moved-aside +*.xcuserstate +*.xcscmblueprint + +## Obj-C/Swift specific +*.hmap +*.ipa + +# CocoaPods +# +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# http://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# +Pods/ + +# Carthage +# +# Add this line if you want to avoid checking in source code from Carthage dependencies. +# Carthage/Checkouts + +Carthage/Build + +### Objective-C Patch ### +*.xcscmblueprint + + +### Xcode ### +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## Build generated +build/ +DerivedData + +## Various settings +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata + +## Other +*.xccheckout +*.moved-aside +*.xcuserstate + diff --git a/.swiftlint.yml b/.swiftlint.yml new file mode 100644 index 00000000..a57c8e03 --- /dev/null +++ b/.swiftlint.yml @@ -0,0 +1,9 @@ +line_length: 140 +file_length: 1000 +type_body_length: 500 +opt_in_rules: + - empty_count +disabled_rules: + - cyclomatic_complexity +excluded: + - Pods diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..845f8554 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,43 @@ +branches: + only: + - master +language: objective-c +os: osx +osx_image: xcode7.2 +cache: + - cocoapods +env: + matrix: + - TEST_TYPE=iOS + - TEST_TYPE=OSX + - TEST_TYPE=OSXDemo + - TEST_TYPE=CocoaPods +before_install: +- | + gem install cocoapods --pre --quiet --no-ri --no-rdoc + pod setup --silent + pod repo update --silent + + if [ "$TEST_TYPE" = iOS ] || [ "$TEST_TYPE" = OSX ] || [ "$TEST_TYPE" = OSXDemo ]; then + pod install + gem install xcpretty -N --no-ri --no-rdoc + fi +script: +- | + if [ "$TEST_TYPE" = iOS ]; then + set -o pipefail + xcodebuild build -workspace ParseLiveQuery.xcworkspace -sdk iphonesimulator -scheme ParseLiveQuery-iOS -configuration Debug -destination "platform=iOS Simulator,name=iPhone 4s" -destination "platform=iOS Simulator,name=iPhone 6 Plus" GCC_INSTRUMENT_PROGRAM_FLOW_ARCS=YES GCC_GENERATE_TEST_COVERAGE_FILES=YES | xcpretty -c + elif [ "$TEST_TYPE" = OSX ]; then + set -o pipefail + xcodebuild build -workspace ParseLiveQuery.xcworkspace -sdk macosx -scheme ParseLiveQuery-OSX -configuration Debug GCC_INSTRUMENT_PROGRAM_FLOW_ARCS=YES GCC_GENERATE_TEST_COVERAGE_FILES=YES | xcpretty -c + elif [ "$TEST_TYPE" = OSXDemo ]; then + set -o pipefail + xcodebuild build -workspace ParseLiveQuery.xcworkspace -sdk macosx -scheme LiveQueryDemo -configuration Debug GCC_INSTRUMENT_PROGRAM_FLOW_ARCS=YES GCC_GENERATE_TEST_COVERAGE_FILES=YES | xcpretty -c + elif [ "$TEST_TYPE" = CocoaPods ]; then + pod lib lint ParseLiveQuery.podspec + fi +after_success: +- | + if [ "$TEST_TYPE" = iOS ] || [ "$TEST_TYPE" = OSX ]; then + bash <(curl -s https://codecov.io/bash) + fi diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..ac63ae68 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,61 @@ +# Contributing to Parse LiveQueryClient for iOS/OS X +We want to make contributing to this project as easy and transparent as possible. + +## Code of Conduct +Facebook has adopted a Code of Conduct that we expect project participants to adhere to. Please read [the full text](https://code.facebook.com/codeofconduct) so that you can understand what actions will and will not be tolerated. + +## Our Development Process +Most of our work will be done in public directly on GitHub. There may be changes done through our internal source control, but it will be rare and only as needed. + +### `master` is unsafe +Our goal is to keep `master` stable, but there may be changes that your application may not be compatible with. We'll do our best to publicize any breaking changes, but try to use our specific releases in any production environment. + +### Pull Requests +We actively welcome your pull requests. When we get one, we'll run some Parse-specific integration tests on it first. From here, we'll need to get a core member to sign off on the changes and then merge the pull request. For API changes we may need to fix internal uses, which could cause some delay. We'll do our best to provide updates and feedback throughout the process. + +1. Fork the repo and create your branch from `master`. +4. Add unit tests for any new code you add. +3. If you've changed APIs, update the documentation. +4. Ensure the test suite passes. +5. Make sure your code lints. +6. If you haven't already, complete the Contributor License Agreement ("CLA"). + +### Contributor License Agreement ("CLA") +In order to accept your pull request, we need you to submit a CLA. You only need to do this once to work on any of Facebook's open source projects. + +Complete your CLA here: + +## Bugs +Although we try to keep developing on Parse easy, you still may run into some issues. General questions should be asked on [Google Groups][google-group], technical questions should be asked on [Stack Overflow][stack-overflow], and for everything else we'll be using GitHub issues. + +### Known Issues +We use GitHub issues to track public bugs. We will keep a close eye on this and try to make it clear when we have an internal fix in progress. Before filing a new issue, try to make sure your problem doesn't already exist. + +### Reporting New Issues + +Details are key. The more information you provide us the easier it'll be for us to debug and the faster you'll receive a fix. Some examples of useful tidbits: + +* A description. What did you expect to happen and what actually happened? Why do you think that was wrong? +* A simple unit test that fails. You can submit a pull request with your failing unit test so that our CI verifies that the test fails. +* What version does this reproduce on? What version did it last work on? +* [Stacktrace or GTFO][stacktrace-or-gtfo]. In all honesty, full stacktraces with line numbers make a happy developer. +* Anything else you find relevant. + + +### Security Bugs +Facebook has a [bounty program](https://www.facebook.com/whitehat/) for the safe disclosure of security bugs. In those cases, please go through the process outlined on that page and do not file a public issue. + +## Style Guide +We're still working on providing a code style for your IDE and getting a linter on GitHub, but for now try to keep the following: + +* Most importantly, match the existing code style as much as possible. +* Try to keep lines under 120 characters, if possible. + +## License +By contributing to Parse Live Query, you agree that your contributions will be licensed under its license. + + [google-group]: https://groups.google.com/forum/#!forum/parse-developers + [stack-overflow]: http://stackoverflow.com/tags/parse.com + [rest-api]: https://www.parse.com/docs/rest/guide + [parse-api-console]: http://blog.parse.com/announcements/introducing-the-parse-api-console/ + [stacktrace-or-gtfo]: http://i.imgur.com/jacoj.jpg diff --git a/Examples/LiveQueryDemo.xcodeproj/project.pbxproj b/Examples/LiveQueryDemo.xcodeproj/project.pbxproj new file mode 100644 index 00000000..f4ec89cf --- /dev/null +++ b/Examples/LiveQueryDemo.xcodeproj/project.pbxproj @@ -0,0 +1,324 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 7F64F07377979A28C88AF3AE /* Pods_LiveQueryDemo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E6A6F02FD57E57309877DA38 /* Pods_LiveQueryDemo.framework */; }; + F59F85B01C9BB48200566A29 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = F59F85AF1C9BB48200566A29 /* main.swift */; }; + F59F85B81C9BB4B600566A29 /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = F59F85B61C9BB4B600566A29 /* Message.swift */; }; + F59F85B91C9BB4B600566A29 /* Room.swift in Sources */ = {isa = PBXBuildFile; fileRef = F59F85B71C9BB4B600566A29 /* Room.swift */; }; + F59F85BD1C9BB66C00566A29 /* ParseLiveQuery.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F59F85BC1C9BB66C00566A29 /* ParseLiveQuery.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + F59F85AA1C9BB48200566A29 /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = /usr/share/man/man1/; + dstSubfolderSpec = 0; + files = ( + ); + runOnlyForDeploymentPostprocessing = 1; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 3AC9312CEDA0007F8EAA9880 /* Pods-LiveQueryDemo.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-LiveQueryDemo.debug.xcconfig"; path = "../Pods/Target Support Files/Pods-LiveQueryDemo/Pods-LiveQueryDemo.debug.xcconfig"; sourceTree = ""; }; + 497772719B97C861F0896BFC /* Pods-LiveQueryDemo.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-LiveQueryDemo.release.xcconfig"; path = "../Pods/Target Support Files/Pods-LiveQueryDemo/Pods-LiveQueryDemo.release.xcconfig"; sourceTree = ""; }; + E6A6F02FD57E57309877DA38 /* Pods_LiveQueryDemo.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_LiveQueryDemo.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + F59F85AC1C9BB48200566A29 /* LiveQueryDemo */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = LiveQueryDemo; sourceTree = BUILT_PRODUCTS_DIR; }; + F59F85AF1C9BB48200566A29 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; + F59F85B61C9BB4B600566A29 /* Message.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Message.swift; sourceTree = ""; }; + F59F85B71C9BB4B600566A29 /* Room.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Room.swift; sourceTree = ""; }; + F59F85BC1C9BB66C00566A29 /* ParseLiveQuery.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ParseLiveQuery.framework; path = "../../../../Library/Developer/Xcode/DerivedData/ParseLiveQuery-dstaosuetnlsavabgvfvntbqxpxo/Build/Products/Debug/ParseLiveQuery.framework"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + F59F85A91C9BB48200566A29 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + F59F85BD1C9BB66C00566A29 /* ParseLiveQuery.framework in Frameworks */, + 7F64F07377979A28C88AF3AE /* Pods_LiveQueryDemo.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 2E2DAD338FCB65EC95CB7AA9 /* Frameworks */ = { + isa = PBXGroup; + children = ( + F59F85BC1C9BB66C00566A29 /* ParseLiveQuery.framework */, + E6A6F02FD57E57309877DA38 /* Pods_LiveQueryDemo.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + B4367C9D9257525F9D28B542 /* Pods */ = { + isa = PBXGroup; + children = ( + 3AC9312CEDA0007F8EAA9880 /* Pods-LiveQueryDemo.debug.xcconfig */, + 497772719B97C861F0896BFC /* Pods-LiveQueryDemo.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; + F59F85A31C9BB48200566A29 = { + isa = PBXGroup; + children = ( + F59F85AE1C9BB48200566A29 /* LiveQueryDemo */, + F59F85AD1C9BB48200566A29 /* Products */, + B4367C9D9257525F9D28B542 /* Pods */, + 2E2DAD338FCB65EC95CB7AA9 /* Frameworks */, + ); + indentWidth = 4; + sourceTree = ""; + tabWidth = 4; + }; + F59F85AD1C9BB48200566A29 /* Products */ = { + isa = PBXGroup; + children = ( + F59F85AC1C9BB48200566A29 /* LiveQueryDemo */, + ); + name = Products; + sourceTree = ""; + }; + F59F85AE1C9BB48200566A29 /* LiveQueryDemo */ = { + isa = PBXGroup; + children = ( + F59F85B61C9BB4B600566A29 /* Message.swift */, + F59F85B71C9BB4B600566A29 /* Room.swift */, + F59F85AF1C9BB48200566A29 /* main.swift */, + ); + path = LiveQueryDemo; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + F59F85AB1C9BB48200566A29 /* LiveQueryDemo */ = { + isa = PBXNativeTarget; + buildConfigurationList = F59F85B31C9BB48200566A29 /* Build configuration list for PBXNativeTarget "LiveQueryDemo" */; + buildPhases = ( + 15BD0AAA808A79D4D9BF001B /* πŸ“¦ Check Pods Manifest.lock */, + F59F85A81C9BB48200566A29 /* Sources */, + F59F85A91C9BB48200566A29 /* Frameworks */, + F59F85AA1C9BB48200566A29 /* CopyFiles */, + 61A2B4349CA47E883546F289 /* πŸ“¦ Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = LiveQueryDemo; + productName = LiveQueryDemo; + productReference = F59F85AC1C9BB48200566A29 /* LiveQueryDemo */; + productType = "com.apple.product-type.tool"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + F59F85A41C9BB48200566A29 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0720; + LastUpgradeCheck = 0720; + ORGANIZATIONNAME = Parse; + TargetAttributes = { + F59F85AB1C9BB48200566A29 = { + CreatedOnToolsVersion = 7.2.1; + }; + }; + }; + buildConfigurationList = F59F85A71C9BB48200566A29 /* Build configuration list for PBXProject "LiveQueryDemo" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = F59F85A31C9BB48200566A29; + productRefGroup = F59F85AD1C9BB48200566A29 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + F59F85AB1C9BB48200566A29 /* LiveQueryDemo */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXShellScriptBuildPhase section */ + 15BD0AAA808A79D4D9BF001B /* πŸ“¦ Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "πŸ“¦ Check Pods Manifest.lock"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [[ $? != 0 ]] ; then\n cat << EOM\nerror: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\nEOM\n exit 1\nfi\n"; + showEnvVarsInLog = 0; + }; + 61A2B4349CA47E883546F289 /* πŸ“¦ Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "πŸ“¦ Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/../Pods/Target Support Files/Pods-LiveQueryDemo/Pods-LiveQueryDemo-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + F59F85A81C9BB48200566A29 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F59F85B01C9BB48200566A29 /* main.swift in Sources */, + F59F85B81C9BB4B600566A29 /* Message.swift in Sources */, + F59F85B91C9BB4B600566A29 /* Room.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + F59F85B11C9BB48200566A29 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + F59F85B21C9BB48200566A29 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + }; + name = Release; + }; + F59F85B41C9BB48200566A29 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 3AC9312CEDA0007F8EAA9880 /* Pods-LiveQueryDemo.debug.xcconfig */; + buildSettings = { + EMBEDDED_CONTENT_CONTAINS_SWIFT = NO; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + F59F85B51C9BB48200566A29 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 497772719B97C861F0896BFC /* Pods-LiveQueryDemo.release.xcconfig */; + buildSettings = { + EMBEDDED_CONTENT_CONTAINS_SWIFT = NO; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + F59F85A71C9BB48200566A29 /* Build configuration list for PBXProject "LiveQueryDemo" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F59F85B11C9BB48200566A29 /* Debug */, + F59F85B21C9BB48200566A29 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + F59F85B31C9BB48200566A29 /* Build configuration list for PBXNativeTarget "LiveQueryDemo" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F59F85B41C9BB48200566A29 /* Debug */, + F59F85B51C9BB48200566A29 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = F59F85A41C9BB48200566A29 /* Project object */; +} diff --git a/Examples/LiveQueryDemo.xcodeproj/xcshareddata/xcschemes/LiveQueryDemo.xcscheme b/Examples/LiveQueryDemo.xcodeproj/xcshareddata/xcschemes/LiveQueryDemo.xcscheme new file mode 100644 index 00000000..cc7527f6 --- /dev/null +++ b/Examples/LiveQueryDemo.xcodeproj/xcshareddata/xcschemes/LiveQueryDemo.xcscheme @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Examples/LiveQueryDemo/Message.swift b/Examples/LiveQueryDemo/Message.swift new file mode 100644 index 00000000..9dbce57f --- /dev/null +++ b/Examples/LiveQueryDemo/Message.swift @@ -0,0 +1,22 @@ +// +// Message.swift +// ParseLiveQuery +// +// Created by Richard Ross III on 10/27/15. +// Copyright Β© 2015 parse. All rights reserved. +// + +import Foundation +import Parse + +class Message: PFObject, PFSubclassing { + @NSManaged var author: PFUser? + @NSManaged var authorName: String? + @NSManaged var message: String? + @NSManaged var room: PFObject? + @NSManaged var roomName: String? + + class func parseClassName() -> String { + return "Message" + } +} diff --git a/Examples/LiveQueryDemo/Room.swift b/Examples/LiveQueryDemo/Room.swift new file mode 100644 index 00000000..f4aee715 --- /dev/null +++ b/Examples/LiveQueryDemo/Room.swift @@ -0,0 +1,18 @@ +// +// Room.swift +// ParseLiveQuery +// +// Created by Richard Ross III on 10/27/15. +// Copyright Β© 2015 parse. All rights reserved. +// + +import Foundation +import Parse + +class Room: PFObject, PFSubclassing { + @NSManaged var name: String? + + static func parseClassName() -> String { + return "Room" + } +} diff --git a/Examples/LiveQueryDemo/main.swift b/Examples/LiveQueryDemo/main.swift new file mode 100644 index 00000000..63b53f1c --- /dev/null +++ b/Examples/LiveQueryDemo/main.swift @@ -0,0 +1,130 @@ +// +// main.swift +// ParseLiveQuery +// +// Created by Richard Ross III on 10/22/15. +// Copyright Β© 2015 parse. All rights reserved. +// + +import Foundation +import Parse +import ParseLiveQuery + +Message.registerSubclass() +Room.registerSubclass() + +Parse.initializeWithConfiguration(ParseClientConfiguration { + $0.applicationId = "myAppId" + $0.server = "http://localhost:1337/parse" + }) + +let liveQueryClient = ParseLiveQuery.Client(server: "http://localhost:1337") + +class ChatRoomManager { + private var currentChatRoom: Room? + private var subscription: Subscription? + + var connected: Bool { return currentChatRoom != nil } + var messagesQuery: PFQuery { + return (Message.query()? + .whereKey("room_name", equalTo: currentChatRoom!.name!) + .orderByAscending("createdAt"))! + } + + func connectToChatRoom(room: String) { + if connected { + disconnectFromChatRoom() + } + + Room.query()?.whereKey("name", equalTo: room).getFirstObjectInBackground().continueWithBlock { task in + self.currentChatRoom = task.result as? Room + print("Connected to room \(self.currentChatRoom?.name ?? "null")") + + self.printPriorMessages() + self.subscribeToUpdates() + + return nil + } + } + + func disconnectFromChatRoom() { + liveQueryClient.unsubscribe(messagesQuery, handler: subscription!) + } + + func sendMessage(msg: String) { + let message = Message() + message.author = PFUser.currentUser() + message.authorName = message.author?.username + message.message = msg + message.room = currentChatRoom + message.roomName = currentChatRoom?.name + + message.saveInBackground() + } + + func printPriorMessages() { + messagesQuery.findObjectsInBackground().continueWithBlock() { task in + (task.result as? [Message])?.forEach(self.printMessage) + + return nil + } + } + + func subscribeToUpdates() { + subscription = liveQueryClient + .subscribe(messagesQuery) + .handle(Event.Created) { _, message in + self.printMessage(message) + } + } + + private func printMessage(message: Message) { + let createdAt = message.createdAt ?? NSDate() + + print("\(createdAt) \(message.authorName ?? "unknown"): \(message.message ?? "")") + } +} + +class InputManager { + let stdinChannel = dispatch_io_create(DISPATCH_IO_STREAM, STDIN_FILENO, dispatch_get_main_queue()) { _ in } + let chatManager: ChatRoomManager + + init(chatManager: ChatRoomManager) { + self.chatManager = chatManager + + dispatch_io_set_low_water(stdinChannel, 1) + dispatch_io_read(stdinChannel, 0, Int.max, dispatch_get_main_queue(), handleInput) + } + + private func handleInput(done: Bool, data: dispatch_data_t?, error: Int32) { + guard + let data = data as? NSData, + let inputString = String(data: data, encoding: NSUTF8StringEncoding)? + .stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSet()) else { + return + } + + if chatManager.connected { + chatManager.sendMessage(inputString) + } else { + chatManager.connectToChatRoom(inputString) + } + } +} + +print("Enter username: ") + +let username = readLine()! +let password = "Enter password for \(username): ".withCString { + String(UTF8String: getpass($0))! +} + +let chatManager = ChatRoomManager() +let inputManager = InputManager(chatManager: chatManager) + +PFUser.logInWithUsernameInBackground(username, password: password).continueWithBlock { task in + print("Enter chat room to connect to: ") + return nil +} + +dispatch_main() diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..144e07c0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,30 @@ +BSD License + +For Parse LiveQueryClient for iOS/OS X software + +Copyright (c) 2016-present, Parse, LLC. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name Parse nor the names of its contributors may be used to + endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/PATENTS b/PATENTS new file mode 100644 index 00000000..a5ce30c6 --- /dev/null +++ b/PATENTS @@ -0,0 +1,33 @@ +Additional Grant of Patent Rights Version 2 + +"Software" means the Parse LiveQueryClient for iOS/OS X software distributed by Parse, LLC. + +Parse, LLC. ("Parse") hereby grants to each recipient of the Software +("you") a perpetual, worldwide, royalty-free, non-exclusive, irrevocable +(subject to the termination provision below) license under any Necessary +Claims, to make, have made, use, sell, offer to sell, import, and otherwise +transfer the Software. For avoidance of doubt, no license is granted under +Parse’s rights in any patent claims that are infringed by (i) modifications +to the Software made by you or any third party or (ii) the Software in +combination with any software or other technology. + +The license granted hereunder will terminate, automatically and without notice, +if you (or any of your subsidiaries, corporate affiliates or agents) initiate +directly or indirectly, or take a direct financial interest in, any Patent +Assertion: (i) against Parse or any of its subsidiaries or corporate +affiliates, (ii) against any party if such Patent Assertion arises in whole or +in part from any software, technology, product or service of Parse or any of +its subsidiaries or corporate affiliates, or (iii) against any party relating +to the Software. Notwithstanding the foregoing, if Parse or any of its +subsidiaries or corporate affiliates files a lawsuit alleging patent +infringement against you in the first instance, and you respond by filing a +patent infringement counterclaim in that lawsuit against that party that is +unrelated to the Software, the license granted hereunder will not terminate +under section (i) of this paragraph due to such counterclaim. + +A "Necessary Claim" is a claim of a patent owned by Parse that is +necessarily infringed by the Software standing alone. + +A "Patent Assertion" is any lawsuit or other action alleging direct, indirect, +or contributory infringement or inducement to infringe any patent, including a +cross-claim or counterclaim. diff --git a/ParseLiveQuery.podspec b/ParseLiveQuery.podspec new file mode 100644 index 00000000..055fb777 --- /dev/null +++ b/ParseLiveQuery.podspec @@ -0,0 +1,22 @@ +Pod::Spec.new do |s| + s.name = 'ParseLiveQuery' + s.version = '1.0.0' + s.license = { :type => 'BSD' } + s.summary = 'Allows for subscriptions to queries in conjunction with parse-server.' + s.homepage = 'https://github.com/ParsePlatform/parse-server' + s.authors = { 'Richard Ross' => 'richardross@fb.com', 'Nikita Lutsenko' => 'nlutsenko@me.com' } + + s.source = { :git => 'https://github.com/ParsePlatform/LiveQueryClient-iOS-OSX.git', :tag => s.version.to_s } + + s.requires_arc = true + + s.ios.deployment_target = '8.0' + s.osx.deployment_target = '10.9' + + s.source_files = 'Sources/ParseLiveQuery/**/*.swift' + s.module_name = 'ParseLiveQuery' + + s.dependency 'Parse' + s.dependency 'Bolts-Swift' + s.dependency 'SocketRocket' +end diff --git a/ParseLiveQuery.xcworkspace/contents.xcworkspacedata b/ParseLiveQuery.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..0d6f1882 --- /dev/null +++ b/ParseLiveQuery.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,21 @@ + + + + + + + + + + + + + diff --git a/ParseLiveQuery.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ParseLiveQuery.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 00000000..08de0be8 --- /dev/null +++ b/ParseLiveQuery.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded + + + diff --git a/Podfile b/Podfile new file mode 100644 index 00000000..fb86badb --- /dev/null +++ b/Podfile @@ -0,0 +1,29 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '8.0' +# Uncomment this line if you're using Swift +use_frameworks! +workspace 'ParseLiveQuery.xcworkspace' + +target 'ParseLiveQuery OSX' do + project 'Sources/ParseLiveQuery.xcodeproj' + platform :osx, '10.10' + pod 'Parse' + pod 'Bolts-Swift' + pod 'SocketRocket' +end + +target 'ParseLiveQuery iOS' do + project 'Sources/ParseLiveQuery.xcodeproj' + platform :ios, '8.0' + pod 'Parse' + pod 'Bolts-Swift' + pod 'SocketRocket' +end + +target 'LiveQueryDemo' do + project 'Examples/LiveQueryDemo.xcodeproj' + platform :osx, '10.10' + pod 'Parse' + pod 'Bolts-Swift' + pod 'SocketRocket' +end diff --git a/Podfile.lock b/Podfile.lock new file mode 100644 index 00000000..2e17473d --- /dev/null +++ b/Podfile.lock @@ -0,0 +1,21 @@ +PODS: + - Bolts-Swift (1.0.0) + - Bolts/Tasks (1.6.0) + - Parse (1.12.0): + - Bolts/Tasks (~> 1.5) + - SocketRocket (0.5.0) + +DEPENDENCIES: + - Bolts-Swift + - Parse + - SocketRocket + +SPEC CHECKSUMS: + Bolts: f52a250053bb517ca874523c3913776359ab3def + Bolts-Swift: 1b20170f1edf3568ec62142ea16ecdc369deced7 + Parse: de2c52a9a1421b91ae7594ab8ce191afd184f19b + SocketRocket: 2c51efccd2d73c99a923407ca4b06e7e9da95dbf + +PODFILE CHECKSUM: 4982c01d12e5aa39b03d538361ee31261706147f + +COCOAPODS: 1.0.0.beta.6 diff --git a/Sources/ParseLiveQuery.xcodeproj/project.pbxproj b/Sources/ParseLiveQuery.xcodeproj/project.pbxproj new file mode 100644 index 00000000..1134c293 --- /dev/null +++ b/Sources/ParseLiveQuery.xcodeproj/project.pbxproj @@ -0,0 +1,495 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 629DC3BE90DA87A7857677D2 /* Pods_ParseLiveQuery_iOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BE2643D85A7565FC20EE144C /* Pods_ParseLiveQuery_iOS.framework */; }; + DE7126BDB27E5DDB1C21490A /* Pods_ParseLiveQuery_OSX.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AF5A55E51D52E372CD28FF08 /* Pods_ParseLiveQuery_OSX.framework */; }; + F534A5B21BDAFE0200CBD11A /* Subscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = F534A5B11BDAFE0200CBD11A /* Subscription.swift */; }; + F534A5B41BDB09CE00CBD11A /* Operation.swift in Sources */ = {isa = PBXBuildFile; fileRef = F534A5B31BDB09CE00CBD11A /* Operation.swift */; }; + F54D58B61C8E33D9009F8D6C /* ObjCCompat.swift in Sources */ = {isa = PBXBuildFile; fileRef = F54D58B51C8E33D9009F8D6C /* ObjCCompat.swift */; }; + F54D58B81C8E3446009F8D6C /* ClientPrivate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F54D58B71C8E3446009F8D6C /* ClientPrivate.swift */; }; + F54D58BA1C8E345F009F8D6C /* BoltsHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = F54D58B91C8E345F009F8D6C /* BoltsHelpers.swift */; }; + F59CA92F1C8E496200329737 /* Errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = F59CA92E1C8E496200329737 /* Errors.swift */; }; + F5A88F4A1C9B6EBA002F0E0D /* PFQuery+Subscribe.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5A88F491C9B6EBA002F0E0D /* PFQuery+Subscribe.swift */; }; + F5A88F4E1C9B7341002F0E0D /* QueryEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5591BA91BD97BB70072F966 /* QueryEncoder.swift */; }; + F5A88F4F1C9B7341002F0E0D /* Operation.swift in Sources */ = {isa = PBXBuildFile; fileRef = F534A5B31BDB09CE00CBD11A /* Operation.swift */; }; + F5A88F501C9B7341002F0E0D /* ClientPrivate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F54D58B71C8E3446009F8D6C /* ClientPrivate.swift */; }; + F5A88F511C9B7341002F0E0D /* BoltsHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = F54D58B91C8E345F009F8D6C /* BoltsHelpers.swift */; }; + F5A88F521C9B7341002F0E0D /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5591BA31BD720E10072F966 /* Client.swift */; }; + F5A88F531C9B7341002F0E0D /* Subscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = F534A5B11BDAFE0200CBD11A /* Subscription.swift */; }; + F5A88F541C9B7341002F0E0D /* ObjCCompat.swift in Sources */ = {isa = PBXBuildFile; fileRef = F54D58B51C8E33D9009F8D6C /* ObjCCompat.swift */; }; + F5A88F551C9B7341002F0E0D /* Errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = F59CA92E1C8E496200329737 /* Errors.swift */; }; + F5A88F561C9B7341002F0E0D /* PFQuery+Subscribe.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5A88F491C9B6EBA002F0E0D /* PFQuery+Subscribe.swift */; }; + F5D965351BD99DA200C3AAFC /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5591BA31BD720E10072F966 /* Client.swift */; }; + F5D965381BD99DA200C3AAFC /* QueryEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5591BA91BD97BB70072F966 /* QueryEncoder.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 11F6DFE2732DB0DE49976BA5 /* Pods-ParseLiveQuery OSX.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ParseLiveQuery OSX.release.xcconfig"; path = "../Pods/Target Support Files/Pods-ParseLiveQuery OSX/Pods-ParseLiveQuery OSX.release.xcconfig"; sourceTree = ""; }; + 6062D7994653A4F07D1358B9 /* Pods-ParseLiveQuery iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ParseLiveQuery iOS.debug.xcconfig"; path = "../Pods/Target Support Files/Pods-ParseLiveQuery iOS/Pods-ParseLiveQuery iOS.debug.xcconfig"; sourceTree = ""; }; + 8445DD921B87567C1E6A6042 /* Pods-ParseLiveQuery OSX.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ParseLiveQuery OSX.debug.xcconfig"; path = "../Pods/Target Support Files/Pods-ParseLiveQuery OSX/Pods-ParseLiveQuery OSX.debug.xcconfig"; sourceTree = ""; }; + AF5A55E51D52E372CD28FF08 /* Pods_ParseLiveQuery_OSX.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ParseLiveQuery_OSX.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + BE2643D85A7565FC20EE144C /* Pods_ParseLiveQuery_iOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ParseLiveQuery_iOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + E4BFFC7C2B6C5374CA5F7440 /* Pods-ParseLiveQuery iOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ParseLiveQuery iOS.release.xcconfig"; path = "../Pods/Target Support Files/Pods-ParseLiveQuery iOS/Pods-ParseLiveQuery iOS.release.xcconfig"; sourceTree = ""; }; + F5256FD31BD71F9A0052FB8A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + F534A5B11BDAFE0200CBD11A /* Subscription.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Subscription.swift; sourceTree = ""; }; + F534A5B31BDB09CE00CBD11A /* Operation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Operation.swift; sourceTree = ""; }; + F54D58B51C8E33D9009F8D6C /* ObjCCompat.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjCCompat.swift; sourceTree = ""; }; + F54D58B71C8E3446009F8D6C /* ClientPrivate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClientPrivate.swift; sourceTree = ""; }; + F54D58B91C8E345F009F8D6C /* BoltsHelpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BoltsHelpers.swift; sourceTree = ""; }; + F5591BA31BD720E10072F966 /* Client.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Client.swift; sourceTree = ""; }; + F5591BA91BD97BB70072F966 /* QueryEncoder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QueryEncoder.swift; sourceTree = ""; }; + F5903CEA1BD999C500C3EFFE /* ParseLiveQuery.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ParseLiveQuery.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + F59CA92E1C8E496200329737 /* Errors.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Errors.swift; path = Internal/Errors.swift; sourceTree = ""; }; + F5A88F491C9B6EBA002F0E0D /* PFQuery+Subscribe.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "PFQuery+Subscribe.swift"; sourceTree = ""; }; + F5A9BFCA1BE0248D00E78326 /* ParseLiveQuery.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ParseLiveQuery.framework; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + F5903CE61BD999C500C3EFFE /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + DE7126BDB27E5DDB1C21490A /* Pods_ParseLiveQuery_OSX.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F5A9BFC01BE0248D00E78326 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 629DC3BE90DA87A7857677D2 /* Pods_ParseLiveQuery_iOS.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 0BD13CAB7945A6C1A2A7B613 /* Frameworks */ = { + isa = PBXGroup; + children = ( + AF5A55E51D52E372CD28FF08 /* Pods_ParseLiveQuery_OSX.framework */, + BE2643D85A7565FC20EE144C /* Pods_ParseLiveQuery_iOS.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 173CC277BE04603C70FB4E43 /* Pods */ = { + isa = PBXGroup; + children = ( + 8445DD921B87567C1E6A6042 /* Pods-ParseLiveQuery OSX.debug.xcconfig */, + 11F6DFE2732DB0DE49976BA5 /* Pods-ParseLiveQuery OSX.release.xcconfig */, + 6062D7994653A4F07D1358B9 /* Pods-ParseLiveQuery iOS.debug.xcconfig */, + E4BFFC7C2B6C5374CA5F7440 /* Pods-ParseLiveQuery iOS.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; + F5256FC41BD71F9A0052FB8A = { + isa = PBXGroup; + children = ( + F5256FD01BD71F9A0052FB8A /* ParseLiveQuery */, + F5256FCF1BD71F9A0052FB8A /* Products */, + 173CC277BE04603C70FB4E43 /* Pods */, + 0BD13CAB7945A6C1A2A7B613 /* Frameworks */, + ); + indentWidth = 4; + sourceTree = ""; + tabWidth = 4; + }; + F5256FCF1BD71F9A0052FB8A /* Products */ = { + isa = PBXGroup; + children = ( + F5903CEA1BD999C500C3EFFE /* ParseLiveQuery.framework */, + F5A9BFCA1BE0248D00E78326 /* ParseLiveQuery.framework */, + ); + name = Products; + sourceTree = ""; + }; + F5256FD01BD71F9A0052FB8A /* ParseLiveQuery */ = { + isa = PBXGroup; + children = ( + F5256FD31BD71F9A0052FB8A /* Info.plist */, + F5DC9D381BD9BAFC00E8FF07 /* Internal */, + F5591BA31BD720E10072F966 /* Client.swift */, + F534A5B11BDAFE0200CBD11A /* Subscription.swift */, + F54D58B51C8E33D9009F8D6C /* ObjCCompat.swift */, + F59CA92E1C8E496200329737 /* Errors.swift */, + F5A88F491C9B6EBA002F0E0D /* PFQuery+Subscribe.swift */, + ); + path = ParseLiveQuery; + sourceTree = ""; + }; + F5DC9D381BD9BAFC00E8FF07 /* Internal */ = { + isa = PBXGroup; + children = ( + F5591BA91BD97BB70072F966 /* QueryEncoder.swift */, + F534A5B31BDB09CE00CBD11A /* Operation.swift */, + F54D58B71C8E3446009F8D6C /* ClientPrivate.swift */, + F54D58B91C8E345F009F8D6C /* BoltsHelpers.swift */, + ); + path = Internal; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + F5903CE71BD999C500C3EFFE /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F5A9BFC31BE0248D00E78326 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + F5903CE91BD999C500C3EFFE /* ParseLiveQuery-OSX */ = { + isa = PBXNativeTarget; + buildConfigurationList = F5903CEF1BD999C500C3EFFE /* Build configuration list for PBXNativeTarget "ParseLiveQuery-OSX" */; + buildPhases = ( + 432EB76C64066D923373DC45 /* πŸ“¦ Check Pods Manifest.lock */, + F5903CE51BD999C500C3EFFE /* Sources */, + F5903CE61BD999C500C3EFFE /* Frameworks */, + F5903CE71BD999C500C3EFFE /* Headers */, + F5903CE81BD999C500C3EFFE /* Resources */, + 458C6F69E79D3F67465FC4DB /* πŸ“¦ Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "ParseLiveQuery-OSX"; + productName = ParseLiveQuery; + productReference = F5903CEA1BD999C500C3EFFE /* ParseLiveQuery.framework */; + productType = "com.apple.product-type.framework"; + }; + F5A9BFB61BE0248D00E78326 /* ParseLiveQuery-iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = F5A9BFC71BE0248D00E78326 /* Build configuration list for PBXNativeTarget "ParseLiveQuery-iOS" */; + buildPhases = ( + 0779A1A9A8C094A6EC98CD51 /* πŸ“¦ Check Pods Manifest.lock */, + F5A9BFBA1BE0248D00E78326 /* Sources */, + F5A9BFC01BE0248D00E78326 /* Frameworks */, + F5A9BFC31BE0248D00E78326 /* Headers */, + F5A9BFC51BE0248D00E78326 /* Resources */, + 89E3C0C052352147C9B80227 /* πŸ“¦ Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "ParseLiveQuery-iOS"; + productName = ParseLiveQuery; + productReference = F5A9BFCA1BE0248D00E78326 /* ParseLiveQuery.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + F5256FC51BD71F9A0052FB8A /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0710; + ORGANIZATIONNAME = Parse; + TargetAttributes = { + F5903CE91BD999C500C3EFFE = { + CreatedOnToolsVersion = 7.1; + }; + }; + }; + buildConfigurationList = F5256FC81BD71F9A0052FB8A /* Build configuration list for PBXProject "ParseLiveQuery" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = F5256FC41BD71F9A0052FB8A; + productRefGroup = F5256FCF1BD71F9A0052FB8A /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + F5A9BFB61BE0248D00E78326 /* ParseLiveQuery-iOS */, + F5903CE91BD999C500C3EFFE /* ParseLiveQuery-OSX */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + F5903CE81BD999C500C3EFFE /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F5A9BFC51BE0248D00E78326 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 0779A1A9A8C094A6EC98CD51 /* πŸ“¦ Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "πŸ“¦ Check Pods Manifest.lock"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [[ $? != 0 ]] ; then\n cat << EOM\nerror: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\nEOM\n exit 1\nfi\n"; + showEnvVarsInLog = 0; + }; + 432EB76C64066D923373DC45 /* πŸ“¦ Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "πŸ“¦ Check Pods Manifest.lock"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [[ $? != 0 ]] ; then\n cat << EOM\nerror: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\nEOM\n exit 1\nfi\n"; + showEnvVarsInLog = 0; + }; + 458C6F69E79D3F67465FC4DB /* πŸ“¦ Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "πŸ“¦ Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/../Pods/Target Support Files/Pods-ParseLiveQuery OSX/Pods-ParseLiveQuery OSX-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + 89E3C0C052352147C9B80227 /* πŸ“¦ Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "πŸ“¦ Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/../Pods/Target Support Files/Pods-ParseLiveQuery iOS/Pods-ParseLiveQuery iOS-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + F5903CE51BD999C500C3EFFE /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F54D58B81C8E3446009F8D6C /* ClientPrivate.swift in Sources */, + F5D965351BD99DA200C3AAFC /* Client.swift in Sources */, + F54D58B61C8E33D9009F8D6C /* ObjCCompat.swift in Sources */, + F54D58BA1C8E345F009F8D6C /* BoltsHelpers.swift in Sources */, + F5D965381BD99DA200C3AAFC /* QueryEncoder.swift in Sources */, + F534A5B21BDAFE0200CBD11A /* Subscription.swift in Sources */, + F59CA92F1C8E496200329737 /* Errors.swift in Sources */, + F534A5B41BDB09CE00CBD11A /* Operation.swift in Sources */, + F5A88F4A1C9B6EBA002F0E0D /* PFQuery+Subscribe.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F5A9BFBA1BE0248D00E78326 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F5A88F541C9B7341002F0E0D /* ObjCCompat.swift in Sources */, + F5A88F531C9B7341002F0E0D /* Subscription.swift in Sources */, + F5A88F511C9B7341002F0E0D /* BoltsHelpers.swift in Sources */, + F5A88F501C9B7341002F0E0D /* ClientPrivate.swift in Sources */, + F5A88F551C9B7341002F0E0D /* Errors.swift in Sources */, + F5A88F4E1C9B7341002F0E0D /* QueryEncoder.swift in Sources */, + F5A88F521C9B7341002F0E0D /* Client.swift in Sources */, + F5A88F4F1C9B7341002F0E0D /* Operation.swift in Sources */, + F5A88F561C9B7341002F0E0D /* PFQuery+Subscribe.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + F5256FE01BD71F9A0052FB8A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ENABLE_TESTABILITY = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MACOSX_DEPLOYMENT_TARGET = 10.10; + ONLY_ACTIVE_ARCH = YES; + }; + name = Debug; + }; + F5256FE11BD71F9A0052FB8A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MACOSX_DEPLOYMENT_TARGET = 10.10; + }; + name = Release; + }; + F5903CF01BD999C500C3EFFE /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 8445DD921B87567C1E6A6042 /* Pods-ParseLiveQuery OSX.debug.xcconfig */; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGNING_REQUIRED = NO; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + INFOPLIST_FILE = ParseLiveQuery/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.9; + PRODUCT_BUNDLE_IDENTIFIER = com.parse.livequery.osx; + PRODUCT_NAME = ParseLiveQuery; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + F5903CF11BD999C500C3EFFE /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 11F6DFE2732DB0DE49976BA5 /* Pods-ParseLiveQuery OSX.release.xcconfig */; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGNING_REQUIRED = NO; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + INFOPLIST_FILE = ParseLiveQuery/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.9; + PRODUCT_BUNDLE_IDENTIFIER = com.parse.livequery.osx; + PRODUCT_NAME = ParseLiveQuery; + SDKROOT = macosx; + SKIP_INSTALL = YES; + }; + name = Release; + }; + F5A9BFC81BE0248D00E78326 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 6062D7994653A4F07D1358B9 /* Pods-ParseLiveQuery iOS.debug.xcconfig */; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGNING_REQUIRED = NO; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + INFOPLIST_FILE = ParseLiveQuery/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.parse.livequery.ios; + PRODUCT_NAME = ParseLiveQuery; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + F5A9BFC91BE0248D00E78326 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = E4BFFC7C2B6C5374CA5F7440 /* Pods-ParseLiveQuery iOS.release.xcconfig */; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGNING_REQUIRED = NO; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + INFOPLIST_FILE = ParseLiveQuery/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.parse.livequery.ios; + PRODUCT_NAME = ParseLiveQuery; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + F5256FC81BD71F9A0052FB8A /* Build configuration list for PBXProject "ParseLiveQuery" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F5256FE01BD71F9A0052FB8A /* Debug */, + F5256FE11BD71F9A0052FB8A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + F5903CEF1BD999C500C3EFFE /* Build configuration list for PBXNativeTarget "ParseLiveQuery-OSX" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F5903CF01BD999C500C3EFFE /* Debug */, + F5903CF11BD999C500C3EFFE /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + F5A9BFC71BE0248D00E78326 /* Build configuration list for PBXNativeTarget "ParseLiveQuery-iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F5A9BFC81BE0248D00E78326 /* Debug */, + F5A9BFC91BE0248D00E78326 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = F5256FC51BD71F9A0052FB8A /* Project object */; +} diff --git a/Sources/ParseLiveQuery.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Sources/ParseLiveQuery.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..71ce1917 --- /dev/null +++ b/Sources/ParseLiveQuery.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Sources/ParseLiveQuery.xcodeproj/xcshareddata/xcschemes/ParseLiveQuery-OSX.xcscheme b/Sources/ParseLiveQuery.xcodeproj/xcshareddata/xcschemes/ParseLiveQuery-OSX.xcscheme new file mode 100644 index 00000000..66b221d0 --- /dev/null +++ b/Sources/ParseLiveQuery.xcodeproj/xcshareddata/xcschemes/ParseLiveQuery-OSX.xcscheme @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Sources/ParseLiveQuery.xcodeproj/xcshareddata/xcschemes/ParseLiveQuery-iOS.xcscheme b/Sources/ParseLiveQuery.xcodeproj/xcshareddata/xcschemes/ParseLiveQuery-iOS.xcscheme new file mode 100644 index 00000000..5e918dad --- /dev/null +++ b/Sources/ParseLiveQuery.xcodeproj/xcshareddata/xcschemes/ParseLiveQuery-iOS.xcscheme @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Sources/ParseLiveQuery/Client.swift b/Sources/ParseLiveQuery/Client.swift new file mode 100644 index 00000000..b9704684 --- /dev/null +++ b/Sources/ParseLiveQuery/Client.swift @@ -0,0 +1,227 @@ +/** + * Copyright (c) 2016-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +import Foundation +import Parse +import BoltsSwift +import SocketRocket + +/** + This is the 'advanced' view of live query subscriptions. It allows you to customize your subscriptions + to a live query server, have connections to multiple servers, cleanly handle disconnect and reconnect. + */ +@objc(PFLiveQueryClient) +public class Client: NSObject { + internal let host: NSURL + internal let applicationId: String + internal let clientKey: String? + + internal var socket: SRWebSocket? + internal var disconnected = false + + // This allows us to easily plug in another request ID generation scheme, or more easily change the request id type + // if needed (technically this could be a string). + internal let requestIdGenerator: () -> RequestId + internal var subscriptions = [SubscriptionRecord]() + + internal let queue = dispatch_queue_create("com.parse.livequery", DISPATCH_QUEUE_SERIAL) + + /** + Creates a Client which automatically attempts to connect to the custom parse-server URL set + in Parse.currentConfiguration + */ + public override convenience init() { + self.init(server: Parse.currentConfiguration().server) + } + + /** + Creates a client which will connect to a specific server with an optional application id and client key + + - parameter server: The server to connect to + - parameter applicationId: The application id to use + - parameter clientKey: The client key to use + */ + public init(server: String, applicationId: String? = nil, clientKey: String? = nil) { + guard let components = NSURLComponents(string: server) else { + fatalError("Server should be a valid URL.") + } + components.scheme = "ws" + components.path = "/LQ" + + // Simple incrementing generator - can't use ++, that operator is deprecated! + var currentRequestId = 0 + requestIdGenerator = { + currentRequestId += 1 + return RequestId(value: currentRequestId) + } + + self.applicationId = applicationId ?? Parse.currentConfiguration().applicationId! + self.clientKey = clientKey ?? Parse.currentConfiguration().clientKey + + self.host = components.URL! + } +} + +extension Client { + // Swift is lame and doesn't allow storage to directly be in extensions. + // So we create an inner struct to wrap it up. + private class Storage { + static var onceToken: dispatch_once_t = 0 + static var sharedStorage: Storage! + static var shared: Storage { + dispatch_once(&onceToken) { + sharedStorage = Storage() + } + return sharedStorage + } + + let queue: dispatch_queue_t = dispatch_queue_create("com.parse.livequery.client.storage", DISPATCH_QUEUE_SERIAL) + var client: Client? + } + + /// Gets or sets shared live query client to be used for default subscriptions + @objc(sharedClient) + public static var shared: Client! { + get { + let storage = Storage.shared + var client: Client? + dispatch_sync(storage.queue) { + client = storage.client + if client == nil { + let configuration = Parse.currentConfiguration() + client = Client( + server: configuration.server, + applicationId: configuration.applicationId, + clientKey: configuration.clientKey + ) + storage.client = client + } + } + return client + } + set { + let storage = Storage.shared + dispatch_sync(storage.queue) { + storage.client = newValue + } + } + } +} + +extension Client { + /** + Registers a query for live updates, using the default subscription handler + + - parameter query: The query to register for updates. + - parameter subclassType: The subclass of PFObject to be used as the type of the Subscription. + This parameter can be automatically inferred from context most of the time + + - returns: The subscription that has just been registered + */ + public func subscribe( + query: PFQuery, + subclassType: T.Type = T.self + ) -> Subscription { + return subscribe(query, handler: Subscription()) + } + + /** + Registers a query for live updates, using a custom subscription handler + + - parameter query: The query to register for updates. + - parameter handler: A custom subscription handler. + + - returns: Your subscription handler, for easy chaining. + */ + public func subscribe( + query: PFQuery, + handler: T + ) -> T { + let subscriptionRecord = SubscriptionRecord( + query: query, + requestId: requestIdGenerator(), + handler: handler + ) + subscriptions.append(subscriptionRecord) + + if socket == nil { + if !disconnected { + reconnect() + } + } else { + sendOperationAsync(.Subscribe(requestId: subscriptionRecord.requestId, query: query)) + } + + return handler + } + + /** + Unsubscribes all current subscriptions for a given query. + + - parameter query: The query to unsubscribe from. + */ + @objc(unsubscribeFromQuery:) + public func unsubscribe(query: PFQuery) { + unsubscribe { $0.query == query } + } + + /** + Unsubscribes from a specific query-handler pair. + + - parameter query: The query to unsubscribe from. + - parameter handler: The specific handler to unsubscribe from. + */ + public func unsubscribe(query: PFQuery, handler: T) { + unsubscribe { $0.query == query && $0.subscriptionHandler === handler } + } + + internal func unsubscribe(@noescape matching matcher: SubscriptionRecord -> Bool) { + subscriptions.filter { + matcher($0) + }.forEach { + sendOperationAsync(.Unsubscribe(requestId: $0.requestId)) + } + } +} + +extension Client { + /** + Reconnects this client to the server. + + This will disconnect and resubscribe all existing subscriptions. This is not required to be called the first time + you use the client, and should usually only be called when an error occurs. + */ + public func reconnect() { + socket?.close() + socket = { + let socket = SRWebSocket(URL: host) + socket.delegate = self + socket.setDelegateDispatchQueue(queue) + socket.open() + + return socket + }() + } + + /** + Explicitly disconnects this client from the server. + + This does not remove any subscriptions - if you `reconnect()` your existing subscriptions will be restored. + Use this if you wish to dispose of the live query client. + */ + public func disconnect() { + guard let socket = socket + else { + return + } + socket.close() + self.socket = nil + disconnected = true + } +} diff --git a/Sources/ParseLiveQuery/Info.plist b/Sources/ParseLiveQuery/Info.plist new file mode 100644 index 00000000..ba72822e --- /dev/null +++ b/Sources/ParseLiveQuery/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + + diff --git a/Sources/ParseLiveQuery/Internal/BoltsHelpers.swift b/Sources/ParseLiveQuery/Internal/BoltsHelpers.swift new file mode 100644 index 00000000..001e2d10 --- /dev/null +++ b/Sources/ParseLiveQuery/Internal/BoltsHelpers.swift @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2016-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +import Bolts +import BoltsSwift + +internal let unknownDomain = "unknown" + +internal func objcTask(task: Task) -> BFTask { + let taskCompletionSource = BFTaskCompletionSource() + task.continueWith { task in + if task.cancelled { + taskCompletionSource.trySetCancelled() + } else if task.faulted { + let error = task.error as? NSError ?? NSError(domain: unknownDomain, code: -1, userInfo: nil) + taskCompletionSource.trySetError(error) + } else { + taskCompletionSource.trySetResult(task.result) + } + } + return taskCompletionSource.task +} + +internal func swiftTask(task: BFTask) -> Task { + let taskCompletionSource = TaskCompletionSource() + task.continueWithBlock { task in + if task.cancelled { + taskCompletionSource.tryCancel() + } else if let error = task.error where task.faulted { + taskCompletionSource.trySetError(error) + } else if let result = task.result { + taskCompletionSource.trySetResult(result) + } else { + fatalError("Unknown task state") + } + return nil + } + return taskCompletionSource.task +} diff --git a/Sources/ParseLiveQuery/Internal/ClientPrivate.swift b/Sources/ParseLiveQuery/Internal/ClientPrivate.swift new file mode 100644 index 00000000..f937ddd0 --- /dev/null +++ b/Sources/ParseLiveQuery/Internal/ClientPrivate.swift @@ -0,0 +1,263 @@ +/** + * Copyright (c) 2016-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +import Foundation +import Parse +import SocketRocket +import BoltsSwift + +private func parseObject(objectDictionary: [String:AnyObject]) throws -> T { + guard let parseClassName = objectDictionary["className"] as? String else { + throw LiveQueryErrors.InvalidJSONError(json: objectDictionary, expectedKey: "parseClassName") + } + guard let objectId = objectDictionary["objectId"] as? String else { + throw LiveQueryErrors.InvalidJSONError(json: objectDictionary, expectedKey: "objectId") + } + + let parseObject = T(withoutDataWithClassName: parseClassName, objectId: objectId) + objectDictionary.filter { key, _ in + key != "parseClassName" && key != "objectId" + }.forEach { key, value in + parseObject[key] = value + } + return parseObject +} + +// --------------- +// MARK: Subscriptions +// --------------- + +internal extension Client { + internal class SubscriptionRecord { + weak var subscriptionHandler: AnyObject? + + // HandlerClosure captures the generic type info passed into the constructor of SubscriptionRecord, + // and 'unwraps' it so that it can be used with just a 'PFObject' instance. + // Technically, this should be a compiler no-op, as no witness tables should be used as 'PFObject' currently inherits from NSObject. + // Should we make PFObject ever a native swift class without the backing Objective-C runtime however, + // this becomes extremely important to have, and makes a ton more sense than just unsafeBitCast-ing everywhere. + var eventHandlerClosure: (Event, Client) -> Void + var errorHandlerClosure: (ErrorType, Client) -> Void + var subscribeHandlerClosure: Client -> Void + var unsubscribeHandlerClosure: Client -> Void + + let query: PFQuery + let requestId: RequestId + + init(query: PFQuery, requestId: RequestId, handler: T) { + self.query = query + self.requestId = requestId + + subscriptionHandler = handler + + // This is needed because swift requires 'handlerClosure' to be fully initialized before we setup the + // capture list for the closure. + eventHandlerClosure = { _, _ in } + errorHandlerClosure = { _, _ in } + subscribeHandlerClosure = { _ in } + unsubscribeHandlerClosure = { _ in } + + eventHandlerClosure = { [weak self] event, client in + guard let handler = self?.subscriptionHandler as? T else { + return + } + + handler.didReceive(Event(event: event), forQuery: query, inClient: client) + } + + errorHandlerClosure = { [weak self] error, client in + guard let handler = self?.subscriptionHandler as? T else { + return + } + + handler.didEncounter(error, forQuery: query, inClient: client) + } + + subscribeHandlerClosure = { [weak self] client in + guard let handler = self?.subscriptionHandler as? T else { + return + } + + handler.didSubscribe(toQuery: query, inClient: client) + } + + unsubscribeHandlerClosure = { [weak self] client in + guard let handler = self?.subscriptionHandler as? T else { + return + } + + handler.didUnsubscribe(fromQuery: query, inClient: client) + } + } + } + + // An opaque placeholder structed used to ensure that we type-safely create request IDs and don't shoot ourself in + // the foot with array indexes. + internal struct RequestId: Equatable { + internal let value: Int + + internal init(value: Int) { + self.value = value + } + } +} + +func == (first: Client.RequestId, second: Client.RequestId) -> Bool { + return first.value == second.value +} + +// --------------- +// MARK: Web Socket +// --------------- + +extension Client: SRWebSocketDelegate { + public func webSocketDidOpen(webSocket: SRWebSocket!) { + // TODO: Add support for session token and user authetication. + self.sendOperationAsync(.Connect(applicationId: applicationId, sessionToken: "")) + } + + public func webSocket(webSocket: SRWebSocket!, didFailWithError error: NSError!) { + print("Error: \(error)") + + if !disconnected { + reconnect() + } + } + + public func webSocket(webSocket: SRWebSocket!, didCloseWithCode code: Int, reason: String!, wasClean: Bool) { + print("code: \(code) reason: \(reason)") + + // TODO: Better retry logic, unless `disconnect()` was explicitly called + if !disconnected { + reconnect() + } + } + + public func webSocket(webSocket: SRWebSocket!, didReceiveMessage message: AnyObject?) { + guard let messageString = message as? String else { + fatalError("Socket got into inconsistent state and received \(message) instead.") + } + handleOperationAsync(messageString).continueWith { task in + if let error = task.error { + print("Error: \(error)") + } + } + } +} + +// ------------------- +// MARK: Operations +// ------------------- + +internal extension Event { + init(serverResponse: ServerResponse, inout requestId: Client.RequestId) throws { + switch serverResponse { + case .Enter(let reqId, let object): + requestId = reqId + self = .Entered(try parseObject(object)) + + case .Leave(let reqId, let object): + requestId = reqId + self = .Left(try parseObject(object)) + + case .Create(let reqId, let object): + requestId = reqId + self = .Created(try parseObject(object)) + + case .Update(let reqId, let object): + requestId = reqId + self = .Updated(try parseObject(object)) + + case .Delete(let reqId, let object): + requestId = reqId + self = .Deleted(try parseObject(object)) + + default: fatalError("Invalid state reached") + } + } +} + +internal extension Client { + private func subscriptionRecord(requestId: RequestId) -> SubscriptionRecord? { + guard + let recordIndex = self.subscriptions.indexOf({ $0.requestId == requestId }), + let record: SubscriptionRecord = self.subscriptions[recordIndex] + where record.subscriptionHandler != nil else { + return nil + } + + return record + } + + internal func sendOperationAsync(operation: ClientOperation) -> Task { + return Task(.Queue(queue)) { + let jsonEncoded = operation.JSONObjectRepresentation + let jsonData = try NSJSONSerialization.dataWithJSONObject(jsonEncoded, options: NSJSONWritingOptions(rawValue: 0)) + let jsonString = String(data: jsonData, encoding: NSUTF8StringEncoding) + + self.socket?.send(jsonString) + } + } + + internal func handleOperationAsync(string: String) -> Task { + return Task(.Queue(queue)) { + guard + let jsonData = string.dataUsingEncoding(NSUTF8StringEncoding), + let jsonDecoded = try NSJSONSerialization.JSONObjectWithData(jsonData, options: NSJSONReadingOptions(rawValue: 0)) + as? [String:AnyObject], + let response: ServerResponse = try ServerResponse(json: jsonDecoded) + else { + throw LiveQueryErrors.InvalidResponseError(response: string) + } + + switch response { + case .Connected: + self.subscriptions.forEach { + self.sendOperationAsync(.Subscribe(requestId: $0.requestId, query: $0.query)) + } + + case .Redirect: + // TODO: Handle redirect. + break + + case .Subscribed(let requestId): + self.subscriptionRecord(requestId)?.subscribeHandlerClosure(self) + + case .Unsubscribed(let requestId): + guard + let recordIndex = self.subscriptions.indexOf({ $0.requestId == requestId }), + let record: SubscriptionRecord = self.subscriptions[recordIndex] else { + break + } + + record.unsubscribeHandlerClosure(self) + self.subscriptions.removeAtIndex(recordIndex) + + case .Create, .Delete, .Enter, .Leave, .Update: + guard + var requestId: RequestId = RequestId(value: 0), + let event: Event = try Event(serverResponse: response, requestId: &requestId), + let record = self.subscriptionRecord(requestId) + else { + break + } + + record.eventHandlerClosure(event, self) + + case .Error(let requestId, let code, let error, let reconnect): + let error = LiveQueryErrors.ServerReportedError(code: code, error: error, reconnect: reconnect) + if let requestId = requestId { + self.subscriptionRecord(requestId)?.errorHandlerClosure(error, self) + } else { + throw error + } + } + } + } +} diff --git a/Sources/ParseLiveQuery/Internal/Errors.swift b/Sources/ParseLiveQuery/Internal/Errors.swift new file mode 100644 index 00000000..f54340a9 --- /dev/null +++ b/Sources/ParseLiveQuery/Internal/Errors.swift @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2016-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +/** + Namespace struct for all errors reported by the Live Query SDK. + */ +public struct LiveQueryErrors { + private init() {} + + /** + An error that is reported when the server returns a response that cannot be parsed. + */ + public struct InvalidResponseError: ErrorType { + /// Response string of the error. + public let response: String + } + + /** + An error that is reported when the server does not accept a query we've sent to it. + */ + public struct InvalidQueryError: ErrorType { + } + + /** + An error that is reported when the server returns valid JSON, but it doesn't match the format we expect. + */ + public struct InvalidJSONError: ErrorType { + /// JSON used for matching. + public let json: [String:AnyObject] + /// Key that was expected to match. + public let expectedKey: String + } + + /** + An error that is reported when the live query server encounters an internal error. + */ + public struct ServerReportedError: ErrorType { + /// Error code reported by the server. + public let code: Int + /// String error reported by the server. + public let error: String + /// Boolean value representing whether a client should reconnect. + public let reconnect: Bool + } +} diff --git a/Sources/ParseLiveQuery/Internal/Operation.swift b/Sources/ParseLiveQuery/Internal/Operation.swift new file mode 100644 index 00000000..e6d1b125 --- /dev/null +++ b/Sources/ParseLiveQuery/Internal/Operation.swift @@ -0,0 +1,99 @@ +/** + * Copyright (c) 2016-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +import Foundation +import Parse + +internal enum ClientOperation { + case Connect(applicationId: String, sessionToken: String) + case Subscribe(requestId: Client.RequestId, query: PFQuery) + case Unsubscribe(requestId: Client.RequestId) + + var JSONObjectRepresentation: [String : AnyObject] { + switch self { + case .Connect(let applicationId, let sessionToken): + return [ "op": "connect", "applicationId": applicationId, "sessionToken": sessionToken ] + + case .Subscribe(let requestId, let query): + return [ "op": "subscribe", "requestId": requestId.value, "query": Dictionary(query: query) ] + + case .Unsubscribe(let requestId): + return [ "op": "unsubscribe", "requestId": requestId.value ] + } + } +} + +internal enum ServerResponse { + case Redirect(url: String) + case Connected() + + case Subscribed(requestId: Client.RequestId) + case Unsubscribed(requestId: Client.RequestId) + + case Enter(requestId: Client.RequestId, object: [String : AnyObject]) + case Leave(requestId: Client.RequestId, object: [String : AnyObject]) + case Update(requestId: Client.RequestId, object: [String : AnyObject]) + case Create(requestId: Client.RequestId, object: [String : AnyObject]) + case Delete(requestId: Client.RequestId, object: [String : AnyObject]) + + case Error(requestId: Client.RequestId?, code: Int, error: String, reconnect: Bool) + + internal init(json: [String : AnyObject]) throws { + func jsonValue(json: [String:AnyObject], _ key: String) throws -> T { + guard let value = json[key] as? T + else { + throw LiveQueryErrors.InvalidJSONError(json: json, expectedKey: key) + } + return value + } + + func jsonRequestId(json: [String:AnyObject]) throws -> Client.RequestId { + let requestId: Int = try jsonValue(json, "requestId") + return Client.RequestId(value: requestId) + } + + func subscriptionEvent( + json: [String:AnyObject], + _ eventType: (Client.RequestId, [String : AnyObject]) -> ServerResponse + ) throws -> ServerResponse { + return eventType(try jsonRequestId(json), try jsonValue(json, "object")) + } + + let rawOperation: String = try jsonValue(json, "op") + switch rawOperation { + case "connected": + self = .Connected() + + case "redirect": + self = .Redirect(url: try jsonValue(json, "url")) + + case "subscribed": + self = .Subscribed(requestId: try jsonRequestId(json)) + case "unsubscribed": + self = .Unsubscribed(requestId: try jsonRequestId(json)) + + case "enter": self = try subscriptionEvent(json, ServerResponse.Enter) + case "leave": self = try subscriptionEvent(json, ServerResponse.Leave) + case "update": self = try subscriptionEvent(json, ServerResponse.Update) + case "create": self = try subscriptionEvent(json, ServerResponse.Create) + case "delete": self = try subscriptionEvent(json, ServerResponse.Delete) + + case "error": + self = .Error( + requestId: try? jsonRequestId(json), + code: try jsonValue(json, "code"), + error: try jsonValue(json, "error"), + reconnect: try jsonValue(json, "reconnect") + ) + + default: + throw LiveQueryErrors.InvalidJSONError(json: json, expectedKey: "op") + } + } +} diff --git a/Sources/ParseLiveQuery/Internal/QueryEncoder.swift b/Sources/ParseLiveQuery/Internal/QueryEncoder.swift new file mode 100644 index 00000000..ac5a63e0 --- /dev/null +++ b/Sources/ParseLiveQuery/Internal/QueryEncoder.swift @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2016-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +import Foundation +import Parse + +/** + NOTE: This is super hacky, and we need a better answer for this. + */ +internal extension Dictionary where Key: StringLiteralConvertible, Value: AnyObject { + internal init(query: PFQuery) { + self.init() + + let queryState = query.valueForKey("state") + if let className = queryState?.valueForKey("parseClassName") { + self["className"] = className as? Value + } + + if let conditions: [String:AnyObject] = queryState?.valueForKey("conditions") as? [String:AnyObject] { + self["where"] = conditions as? Value + } + } +} diff --git a/Sources/ParseLiveQuery/ObjCCompat.swift b/Sources/ParseLiveQuery/ObjCCompat.swift new file mode 100644 index 00000000..a9966bb9 --- /dev/null +++ b/Sources/ParseLiveQuery/ObjCCompat.swift @@ -0,0 +1,340 @@ +/** + * Copyright (c) 2016-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +import Foundation +import Parse +import BoltsSwift + +/** + This protocol describes the interface for handling events from a live query client. + + You can use this protocol on any custom class of yours, instead of Subscription, if it fits your use case better. + */ +@objc(PFLiveQuerySubscriptionHandling) +public protocol ObjCCompat_SubscriptionHandling { + /** + Tells the handler that an event has been received from the live query server. + + - parameter query: The query that the event occurred on. + - parameter event: The event that has been recieved from the server. + - parameter client: The live query client which received this event. + */ + @objc(liveQuery:didRecieveEvent:inClient:) + optional func didRecieveEvent(query: PFQuery, event: ObjCCompat.Event, client: Client) + + /** + Tells the handler that an error has been received from the live query server. + + - parameter query: The query that the error occurred on. + - parameter error: The error that the server has encountered. + - parameter client: The live query client which received this error. + */ + @objc(liveQuery:didEncounterError:inClient:) + optional func didRecieveError(query: PFQuery, error: NSError, client: Client) + + /** + Tells the handler that a query has been successfully registered with the server. + + - note: This may be invoked multiple times if the client disconnects/reconnects. + + - parameter query: The query that has been subscribed. + - parameter client: The live query client which subscribed this query. + */ + @objc(liveQuery:didSubscribeInClient:) + optional func didSubscribe(query: PFQuery, client: Client) + + /** + Tells the handler that a query has been successfully deregistered from the server. + + - note: This is not called unless `unregister()` is explicitly called. + + - parameter query: The query that has been unsubscribed. + - parameter client: The live query client which unsubscribed this query. + */ + @objc(liveQuery:didUnsubscribeInClient:) + optional func didUnsubscribe(query: PFQuery, client: Client) +} + +/** + This struct wraps up all of our Objective-C compatibility layer. You should never need to touch this if you're using Swift. + */ +public struct ObjCCompat { + private init() { } + + /** + A type of an update event on a specific object from the live query server. + */ + @objc + public enum PFLiveQueryEventType: Int { + /// The object has been updated, and is now included in the query. + case Entered + /// The object has been updated, and is no longer included in the query. + case Left + /// The object has been created, and is a part of the query. + case Created + /// The object has been updated, and is still a part of the query. + case Updated + /// The object has been deleted, and is no longer included in the query. + case Deleted + } + + /** + Represents an update on a specific object from the live query server. + */ + @objc(PFLiveQueryEvent) + public class Event: NSObject { + /// Type of the event. + public let type: PFLiveQueryEventType + /// Object this event is for. + public let object: PFObject + + init(event: ParseLiveQuery.Event) { + (type, object) = { + switch event { + case .Entered(let object): return (.Entered, object) + case .Left(let object): return (.Left, object) + case .Created(let object): return (.Created, object) + case .Updated(let object): return (.Updated, object) + case .Deleted(let object): return (.Deleted, object) + } + }() + } + + init(type: PFLiveQueryEventType, object: PFObject) { + self.type = type + self.object = object + } + } + + /** + A default implementation of the SubscriptionHandling protocol, using blocks for callbacks. + */ + @objc(PFLiveQuerySubscription) + public class Subscription: NSObject { + public typealias SubscribeHandler = @convention(block) PFQuery -> Void + public typealias ErrorHandler = @convention(block) (PFQuery, NSError) -> Void + public typealias EventHandler = @convention(block) (PFQuery, Event) -> Void + public typealias ObjectHandler = @convention(block) (PFQuery, PFObject) -> Void + + internal var subscribeHandlers = [SubscribeHandler]() + internal var unsubscribeHandlers = [SubscribeHandler]() + internal var errorHandlers = [ErrorHandler]() + internal var eventHandlers = [EventHandler]() + + /** + Register a callback for when a client succesfully subscribes to a query. + + - parameter handler: The callback to register. + + - returns: The same subscription, for easy chaining. + */ + public func addSubscribeHandler(handler: SubscribeHandler) -> Subscription { + subscribeHandlers.append(handler) + return self + } + + /** + Register a callback for when a query has been unsubscribed. + + - parameter handler: The callback to register. + + - returns: The same subscription, for easy chaining. + */ + public func addUnsubscribeHandler(handler: SubscribeHandler) -> Subscription { + unsubscribeHandlers.append(handler) + return self + } + + /** + Register a callback for when an error occurs. + + - parameter handler: The callback to register. + + - returns: The same subscription, for easy chaining. + */ + public func addErrorHandler(handler: ErrorHandler) -> Subscription { + errorHandlers.append(handler) + return self + } + + /** + Register a callback for when an event occurs. + + - parameter handler: The callback to register. + + - returns: The same subscription, for easy chaining. + */ + public func addEventHandler(handler: EventHandler) -> Subscription { + eventHandlers.append(handler) + return self + } + + /** + Register a callback for when an object enters a query. + + - parameter handler: The callback to register. + + - returns: The same subscription, for easy chaining. + */ + public func addEnterHandler(handler: ObjectHandler) -> Subscription { + return addEventHandler { $1.type == .Entered ? handler($0, $1.object) : () } + } + + /** + Register a callback for when an object leaves a query. + + - parameter handler: The callback to register. + + - returns: The same subscription, for easy chaining. + */ + public func addLeaveHandler(handler: ObjectHandler) -> Subscription { + return addEventHandler { $1.type == .Left ? handler($0, $1.object) : () } + } + + /** + Register a callback for when an object that matches the query is created. + + - parameter handler: The callback to register. + + - returns: The same subscription, for easy chaining. + */ + public func addCreateHandler(handler: ObjectHandler) -> Subscription { + return addEventHandler { $1.type == .Created ? handler($0, $1.object) : () } + } + + /** + Register a callback for when an object that matches the query is updated. + + - parameter handler: The callback to register. + + - returns: The same subscription, for easy chaining. + */ + public func addUpdateHandler(handler: ObjectHandler) -> Subscription { + return addEventHandler { $1.type == .Updated ? handler($0, $1.object) : () } + } + + /** + Register a callback for when an object that matches the query is deleted. + + - parameter handler: The callback to register. + + - returns: The same subscription, for easy chaining. + */ + public func addDeleteHandler(handler: ObjectHandler) -> Subscription { + return addEventHandler { $1.type == .Deleted ? handler($0, $1.object) : () } + } + } +} + +extension ObjCCompat.Subscription: ObjCCompat_SubscriptionHandling { + func didRecieveEvent(query: PFQuery, event: ObjCCompat.Event, client: Client) { + eventHandlers.forEach { $0(query, event) } + } + + func didRecieveError(query: PFQuery, error: NSError, client: Client) { + errorHandlers.forEach { $0(query, error) } + } + + func didSubscribe(query: PFQuery, client: Client) { + subscribeHandlers.forEach { $0(query) } + } + + func didUnsubscribe(query: PFQuery, client: Client) { + unsubscribeHandlers.forEach { $0(query) } + } +} + +extension Client { + private class HandlerConverter: SubscriptionHandling { + typealias PFObjectSubclass = PFObject + + private let handler: ObjCCompat_SubscriptionHandling + + init(handler: ObjCCompat_SubscriptionHandling) { + self.handler = handler + } + + private func didReceive(event: Event, forQuery query: PFQuery, inClient client: Client) { + handler.didRecieveEvent?(query, event: ObjCCompat.Event(event: event), client: client) + } + + private func didEncounter(error: ErrorType, forQuery query: PFQuery, inClient client: Client) { + handler.didRecieveError?(query, error: error as NSError, client: client) + } + + private func didSubscribe(toQuery query: PFQuery, inClient client: Client) { + handler.didSubscribe?(query, client: client) + } + + private func didUnsubscribe(fromQuery query: PFQuery, inClient client: Client) { + handler.didUnsubscribe?(query, client: client) + } + } + + /** + Registers a query for live updates, using a custom subscription handler. + + - parameter query: The query to register for updates. + - parameter handler: A custom subscription handler. + + - returns: The subscription that has just been registered. + */ + @objc(subscribeToQuery:withHandler:) + public func _PF_objc_subscribe( + query: PFQuery, handler: ObjCCompat_SubscriptionHandling + ) -> ObjCCompat_SubscriptionHandling { + let swiftHandler = HandlerConverter(handler: handler) + subscribe(query, handler: swiftHandler) + return handler + } + + /** + Registers a query for live updates, using the default subscription handler. + + - parameter query: The query to register for updates. + + - returns: The subscription that has just been registered. + */ + @objc(subscribeToQuery:) + public func _PF_objc_subscribe(query: PFQuery) -> ObjCCompat.Subscription { + let subscription = ObjCCompat.Subscription() + _PF_objc_subscribe(query, handler: subscription) + return subscription + } + + /** + Unsubscribes a specific handler from a query. + + - parameter query: The query to unsubscribe from. + - parameter handler: The specific handler to unsubscribe from. + */ + @objc(unsubscribeFromQuery:withHandler:) + public func _PF_objc_unsubscribe(query: PFQuery, subscriptionHandler: ObjCCompat_SubscriptionHandling) { + unsubscribe { record in + guard let handler = record.subscriptionHandler as? HandlerConverter + else { + return false + } + return record.query == query && handler.handler === subscriptionHandler + } + } +} + +extension PFQuery { + /** + Register this PFQuery for updates with Live Queries. + This uses the shared live query client, and creates a default subscription handler for you. + + - returns: The created subscription for observing. + */ + @objc(subscribe) + public func _PF_objc_subscribe() -> ObjCCompat.Subscription { + return Client.shared._PF_objc_subscribe(self) + } +} diff --git a/Sources/ParseLiveQuery/PFQuery+Subscribe.swift b/Sources/ParseLiveQuery/PFQuery+Subscribe.swift new file mode 100644 index 00000000..b209ddfb --- /dev/null +++ b/Sources/ParseLiveQuery/PFQuery+Subscribe.swift @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2016-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +import Foundation +import Parse + +extension PFQuery { + /** + Register this PFQuery for updates with Live Queries. + This uses the shared live query client, and creates a default subscription handler for you. + + - parameter subclassType: The type of the subscription to register for. + This can usually be inferred from the context and rarely should be set. + + - returns: The created subscription for observing. + */ + public func subscribe(subclassType: T.Type = T.self) -> Subscription { + return Client.shared.subscribe(self) + } +} diff --git a/Sources/ParseLiveQuery/Subscription.swift b/Sources/ParseLiveQuery/Subscription.swift new file mode 100644 index 00000000..b52f6903 --- /dev/null +++ b/Sources/ParseLiveQuery/Subscription.swift @@ -0,0 +1,250 @@ +/** + * Copyright (c) 2016-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +import Foundation +import Parse +import BoltsSwift + +/** + This protocol describes the interface for handling events from a liveQuery client. + + You can use this protocol on any custom class of yours, instead of Subscription, if it fits your use case better. + */ +public protocol SubscriptionHandling: AnyObject { + /// The type of the PFObject subclass that this handler uses. + typealias PFObjectSubclass: PFObject + + /** + Tells the handler that an event has been received from the live query server. + + - parameter event: The event that has been recieved from the server. + - parameter query: The query that the event occurred on. + - parameter client: The live query client which received this event. + */ + func didReceive(event: Event, forQuery query: PFQuery, inClient client: Client) + + /** + Tells the handler that an error has been received from the live query server. + + - parameter error: The error that the server has encountered. + - parameter query: The query that the error occurred on. + - parameter client: The live query client which received this error. + */ + func didEncounter(error: ErrorType, forQuery query: PFQuery, inClient client: Client) + + /** + Tells the handler that a query has been successfully registered with the server. + + - note: This may be invoked multiple times if the client disconnects/reconnects. + + - parameter query: The query that has been subscribed. + - parameter client: The live query client which subscribed this query. + */ + func didSubscribe(toQuery query: PFQuery, inClient client: Client) + + /** + Tells the handler that a query has been successfully deregistered from the server. + + - note: This is not called unless `unregister()` is explicitly called. + + - parameter query: The query that has been unsubscribed. + - parameter client: The live query client which unsubscribed this query. + */ + func didUnsubscribe(fromQuery query: PFQuery, inClient client: Client) +} + +/** + Represents an update on a specific object from the live query server. + + - Entered: The object has been updated, and is now included in the query. + - Left: The object has been updated, and is no longer included in the query. + - Created: The object has been created, and is a part of the query. + - Updated: The object has been updated, and is still a part of the query. + - Deleted: The object has been deleted, and is no longer included in the query. + */ +public enum Event { + /// The object has been updated, and is now included in the query + case Entered(T) + + /// The object has been updated, and is no longer included in the query + case Left(T) + + /// The object has been created, and is a part of the query + case Created(T) + + /// The object has been updated, and is still a part of the query + case Updated(T) + + /// The object has been deleted, and is no longer included in the query + case Deleted(T) + + internal init(event: Event) { + switch event { + case .Entered(let value as T): self = .Entered(value) + case .Left(let value as T): self = .Left(value) + case .Created(let value as T): self = .Created(value) + case .Updated(let value as T): self = .Updated(value) + case .Deleted(let value as T): self = .Deleted(value) + default: fatalError() + } + } +} + +private func == (lhs: Event, rhs: Event) -> Bool { + switch (lhs, rhs) { + case (.Entered(let obj1), .Entered(let obj2)): return obj1 == obj2 + case (.Left(let obj1), .Left(let obj2)): return obj1 == obj2 + case (.Created(let obj1), .Created(let obj2)): return obj1 == obj2 + case (.Updated(let obj1), .Updated(let obj2)): return obj1 == obj2 + case (.Deleted(let obj1), .Deleted(let obj2)): return obj1 == obj2 + default: return false + } +} + +/** + A default implementation of the SubscriptionHandling protocol, using closures for callbacks. + */ +public class Subscription: SubscriptionHandling { + private var errorHandlers: [(PFQuery, ErrorType) -> Void] = [] + private var eventHandlers: [(PFQuery, Event) -> Void] = [] + private var subscribeHandlers: [PFQuery -> Void] = [] + private var unsubscribeHandlers: [PFQuery -> Void] = [] + + /** + Creates a new subscription that can be used to handle updates. + */ + public init() { + } + + /** + Register a callback for when an error occurs. + + - parameter handler: The callback to register. + + - returns: The same subscription, for easy chaining + */ + public func handleError(handler: (PFQuery, ErrorType) -> Void) -> Subscription { + errorHandlers.append(handler) + return self + } + + /** + Register a callback for when an event occurs. + + - parameter handler: The callback to register. + + - returns: The same subscription, for easy chaining. + */ + public func handleEvent(handler: (PFQuery, Event) -> Void) -> Subscription { + eventHandlers.append(handler) + return self + } + + /** + Register a callback for when a client succesfully subscribes to a query. + + - parameter handler: The callback to register. + + - returns: The same subscription, for easy chaining. + */ + public func handleSubscribe(handler: PFQuery -> Void) -> Subscription { + subscribeHandlers.append(handler) + return self + } + + /** + Register a callback for when a query has been unsubscribed. + + - parameter handler: The callback to register. + + - returns: The same subscription, for easy chaining. + */ + public func handleUnsubscribe(handler: PFQuery -> Void) -> Subscription { + unsubscribeHandlers.append(handler) + return self + } + + // --------------- + // MARK: SubscriptionHandling + // TODO: Move to extension once swift compiler is less crashy + // --------------- + public typealias PFObjectSubclass = T + + public func didReceive(event: Event, forQuery query: PFQuery, inClient client: Client) { + eventHandlers.forEach { $0(query, event) } + } + + public func didEncounter(error: ErrorType, forQuery query: PFQuery, inClient client: Client) { + errorHandlers.forEach { $0(query, error) } + } + + public func didSubscribe(toQuery query: PFQuery, inClient client: Client) { + subscribeHandlers.forEach { $0(query) } + } + + public func didUnsubscribe(fromQuery query: PFQuery, inClient client: Client) { + unsubscribeHandlers.forEach { $0(query) } + } +} + +extension Subscription { + /** + Register a callback for when an error occcurs of a specific type + + Example: + + subscription.handle(LiveQueryErrors.InvalidJSONError.self) { query, error in + print(error) + } + + - parameter errorType: The error type to register for + - parameter handler: The callback to register + + - returns: The same subscription, for easy chaining + */ + public func handle( + errorType: E.Type = E.self, + _ handler: (PFQuery, E) -> Void + ) -> Subscription { + errorHandlers.append { query, error in + if let error = error as? E { + handler(query, error) + } + } + return self + } + + /** + Register a callback for when an event occurs of a specific type + + Example: + + subscription.handle(Event.Created) { query, object in + // Called whenever an object is creaated + } + + - parameter eventType: The event type to handle. You should pass one of the enum cases in `Event` + - parameter handler: The callback to register + + - returns: The same subscription, for easy chaining + + */ + public func handle(eventType: T -> Event, _ handler: (PFQuery, T) -> Void) -> Subscription { + return handleEvent { query, event in + switch event { + case .Entered(let obj) where eventType(obj) == event: handler(query, obj) + case .Left(let obj) where eventType(obj) == event: handler(query, obj) + case .Created(let obj) where eventType(obj) == event: handler(query, obj) + case .Updated(let obj) where eventType(obj) == event: handler(query, obj) + case .Deleted(let obj) where eventType(obj) == event: handler(query, obj) + default: return + } + } + } +}