Skip to content

Commit

Permalink
Use ParseLiveQuery Subscription as a SwiftUI view model (#65)
Browse files Browse the repository at this point in the history
* Enable LiveQuery Subscription as a SwiftUI VM

* Add changes to changelog

* update changelog

* Don't use Subscription on Linux

* fix jazzy script

* Update Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift

Co-authored-by: Tom Fox <[email protected]>

Co-authored-by: Tom Fox <[email protected]>
  • Loading branch information
cbaker6 and TomWFox authored Jan 25, 2021
1 parent 7474bcb commit 39f20d2
Show file tree
Hide file tree
Showing 9 changed files with 656 additions and 45 deletions.
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
# Parse-Swift Changelog

### main
[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/1.0.2...main)
[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/1.1.0...main)
* _Contributing to this repo? Add info about your change here to be included in next release_

### 1.1.0
[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/1.0.2...1.1.0)

__New features__
- Enable `ParseFile` for Linux ([#64](https://github.com/parse-community/Parse-Swift/pull/64)), thanks to [jt9897253](https://github.com/jt9897253).
- Use a `ParseLiveQuery` subscription as a SwiftUI view model ([#65](https://github.com/parse-community/Parse-Swift/pull/65)), thanks to [Corey Baker](https://github.com/cbaker6).
- Idempotency support ([#62](https://github.com/parse-community/Parse-Swift/pull/62)), thanks to [Corey Baker](https://github.com/cbaker6).

### 1.0.2
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@ struct GameScore: ParseObject {
//: Create a query just as you normally would.
var query = GameScore.query("score" > 9)

//: This is how you subscribe your created query
let subscription = query.subscribe!
//: This is how you subscribe to your created query using callbacks.
//: Note that if you want to use subscriptions with SwiftUI, you should
//: use `let subscription = query.subscribe` instead.
let subscription = query.subscribeCallback!

//: This is how you receive notifications about the success
//: of your subscription.
Expand Down Expand Up @@ -86,7 +88,7 @@ var query2 = GameScore.query("score" > 50)
query2.fields("score")

//: Subscribe to your new query.
let subscription2 = query2.subscribe!
let subscription2 = query2.subscribeCallback!

//: As before, setup your subscription and event handlers.
subscription2.handleSubscribe { subscribedQuery, isNew in
Expand Down
2 changes: 1 addition & 1 deletion ParseSwift.playground/contents.xcplayground
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@
<page name='13 - Operations'/>
<page name='14 - Config'/>
</pages>
</playground>
</playground>
2 changes: 1 addition & 1 deletion ParseSwift.podspec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = "ParseSwift"
s.version = "1.0.2"
s.version = "1.1.0"
s.summary = "Parse Pure Swift SDK"
s.homepage = "https://github.com/parse-community/Parse-Swift"
s.authors = {
Expand Down
16 changes: 8 additions & 8 deletions ParseSwift.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -2045,7 +2045,7 @@
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
MARKETING_VERSION = 1.0.2;
MARKETING_VERSION = 1.1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.parse.ParseSwift;
PRODUCT_NAME = ParseSwift;
SKIP_INSTALL = YES;
Expand All @@ -2067,7 +2067,7 @@
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
MARKETING_VERSION = 1.0.2;
MARKETING_VERSION = 1.1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.parse.ParseSwift;
PRODUCT_NAME = ParseSwift;
SKIP_INSTALL = YES;
Expand Down Expand Up @@ -2131,7 +2131,7 @@
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks";
MACOSX_DEPLOYMENT_TARGET = 10.13;
MARKETING_VERSION = 1.0.2;
MARKETING_VERSION = 1.1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.parse.ParseSwift;
PRODUCT_NAME = ParseSwift;
SDKROOT = macosx;
Expand All @@ -2155,7 +2155,7 @@
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks";
MACOSX_DEPLOYMENT_TARGET = 10.13;
MARKETING_VERSION = 1.0.2;
MARKETING_VERSION = 1.1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.parse.ParseSwift;
PRODUCT_NAME = ParseSwift;
SDKROOT = macosx;
Expand Down Expand Up @@ -2300,7 +2300,7 @@
INFOPLIST_FILE = "ParseSwift-watchOS/Info.plist";
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
MARKETING_VERSION = 1.0.2;
MARKETING_VERSION = 1.1.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.parse.ParseSwift-watchOS";
Expand Down Expand Up @@ -2328,7 +2328,7 @@
INFOPLIST_FILE = "ParseSwift-watchOS/Info.plist";
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
MARKETING_VERSION = 1.0.2;
MARKETING_VERSION = 1.1.0;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.parse.ParseSwift-watchOS";
PRODUCT_NAME = ParseSwift;
Expand All @@ -2354,7 +2354,7 @@
INFOPLIST_FILE = "ParseSwift-tvOS/Info.plist";
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
MARKETING_VERSION = 1.0.2;
MARKETING_VERSION = 1.1.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.parse.ParseSwift-tvOS";
Expand All @@ -2381,7 +2381,7 @@
INFOPLIST_FILE = "ParseSwift-tvOS/Info.plist";
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
MARKETING_VERSION = 1.0.2;
MARKETING_VERSION = 1.1.0;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.parse.ParseSwift-tvOS";
PRODUCT_NAME = ParseSwift;
Expand Down
2 changes: 1 addition & 1 deletion Scripts/jazzy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ bundle exec jazzy \
--author_url http://parseplatform.org \
--github_url https://github.com/parse-community/Parse-Swift \
--root-url http://parseplatform.org/Parse-Swift/api/ \
--module-version 1.0.2 \
--module-version 1.1.0 \
--theme fullwidth \
--skip-undocumented \
--output ./docs/api \
Expand Down
34 changes: 31 additions & 3 deletions Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift
Original file line number Diff line number Diff line change
Expand Up @@ -619,6 +619,10 @@ extension ParseLiveQuery {
try subscribe(Subscription(query: query))
}

func subscribe<T>(_ query: Query<T>) throws -> SubscriptionCallback<T> {
try subscribe(SubscriptionCallback(query: query))
}

func subscribe<T>(_ handler: T) throws -> T where T: ParseSubscription {

let requestId = requestIdGenerator()
Expand Down Expand Up @@ -689,27 +693,33 @@ extension ParseLiveQuery {
// MARK: ParseLiveQuery - Subscribe
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
public extension Query {
#if !os(Linux)
/**
Registers the query for live updates, using the default subscription handler,
and the default `ParseLiveQuery` client.
and the default `ParseLiveQuery` client. Suitable for `ObjectObserved`
as the subscription can be used as a SwiftUI publisher. Meaning it can serve
indepedently as a ViewModel in MVVM.
*/
var subscribe: Subscription<ResultType>? {
try? ParseLiveQuery.client?.subscribe(self)
}

/**
Registers the query for live updates, using the default subscription handler,
and a specific `ParseLiveQuery` client.
and a specific `ParseLiveQuery` client. Suitable for `ObjectObserved`
as the subscription can be used as a SwiftUI publisher. Meaning it can serve
indepedently as a ViewModel in MVVM.
- parameter client: A specific client.
- returns: The subscription that has just been registered
*/
func subscribe(_ client: ParseLiveQuery) throws -> Subscription<ResultType> {
try client.subscribe(Subscription(query: self))
}
#endif

/**
Registers a query for live updates, using a custom subscription handler.
- parameter handler: A custom subscription handler.
- parameter handler: A custom subscription handler.
- returns: Your subscription handler, for easy chaining.
*/
static func subscribe<T: ParseSubscription>(_ handler: T) throws -> T {
Expand All @@ -729,6 +739,24 @@ public extension Query {
static func subscribe<T: ParseSubscription>(_ handler: T, client: ParseLiveQuery) throws -> T {
try client.subscribe(handler)
}

/**
Registers the query for live updates, using the default subscription handler,
and the default `ParseLiveQuery` client.
*/
var subscribeCallback: SubscriptionCallback<ResultType>? {
try? ParseLiveQuery.client?.subscribe(self)
}

/**
Registers the query for live updates, using the default subscription handler,
and a specific `ParseLiveQuery` client.
- parameter client: A specific client.
- returns: The subscription that has just been registered.
*/
func subscribeCallback(_ client: ParseLiveQuery) throws -> SubscriptionCallback<ResultType> {
try client.subscribe(SubscriptionCallback(query: self))
}
}

// MARK: ParseLiveQuery - Unsubscribe
Expand Down
91 changes: 84 additions & 7 deletions Sources/ParseSwift/LiveQuery/Subscription.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,85 @@ private func == <T>(lhs: Event<T>, rhs: Event<T>) -> Bool {
}
}

#if !os(Linux)
/**
A default implementation of the `ParseSubscription` protocol, using closures for callbacks.
A default implementation of the `ParseSubscription` protocol. Suitable for `ObjectObserved`
as the subscription can be used as a SwiftUI publisher. Meaning it can serve
indepedently as a ViewModel in MVVM.
*/
open class Subscription<T: ParseObject>: ParseSubscription {
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
open class Subscription<T: ParseObject>: ParseSubscription, ObservableObject {
//The query subscribed to.
public var query: Query<T>
//The ParseObject
public typealias Object = T

/// Notifies there's a new event related to a specific query.
private (set) var event: (query: Query<T>, event: Event<T>)? {
willSet {
if newValue != nil {
subscribed = nil
unsubscribed = nil
objectWillChange.send()
}
}
}

/// Notifies when a subscription request has been fulfilled and if it is new.
private (set) var subscribed: (query: Query<T>, isNew: Bool)? {
willSet {
if newValue != nil {
unsubscribed = nil
event = nil
objectWillChange.send()
}
}
}

/// Notifies when an unsubscribe request has been fulfilled.
private (set) var unsubscribed: Query<T>? {
willSet {
if newValue != nil {
subscribed = nil
event = nil
objectWillChange.send()
}
}
}

/**
Creates a new subscription that can be used to handle updates.
*/
public init(query: Query<T>) {
self.query = query
self.subscribed = nil
self.event = nil
self.unsubscribed = nil
}

open func didReceive(_ eventData: Data) throws {
// Need to decode the event with respect to the `ParseObject`.
let eventMessage = try ParseCoding.jsonDecoder().decode(EventResponse<T>.self, from: eventData)
guard let event = Event(event: eventMessage) else {
throw ParseError(code: .unknownError, message: "ParseLiveQuery Error: couldn't create event.")
}
self.event = (query, event)
}

open func didSubscribe(_ new: Bool) {
self.subscribed = (query, new)
}

open func didUnsubscribe() {
self.unsubscribed = query
}
}
#endif

/**
A default implementation of the `ParseSubscription` protocol using closures for callbacks.
*/
open class SubscriptionCallback<T: ParseObject>: ParseSubscription {
//The query subscribed to.
public var query: Query<T>
//The ParseObject
Expand All @@ -80,7 +155,8 @@ open class Subscription<T: ParseObject>: ParseSubscription {
- parameter handler: The callback to register.
- returns: The same subscription, for easy chaining.
*/
@discardableResult open func handleEvent(_ handler: @escaping (Query<T>, Event<T>) -> Void) -> Subscription {
@discardableResult open func handleEvent(_ handler: @escaping (Query<T>,
Event<T>) -> Void) -> SubscriptionCallback {
eventHandlers.append(handler)
return self
}
Expand All @@ -90,7 +166,8 @@ open class Subscription<T: ParseObject>: ParseSubscription {
- parameter handler: The callback to register.
- returns: The same subscription, for easy chaining.
*/
@discardableResult open func handleSubscribe(_ handler: @escaping (Query<T>, Bool) -> Void) -> Subscription {
@discardableResult open func handleSubscribe(_ handler: @escaping (Query<T>,
Bool) -> Void) -> SubscriptionCallback {
subscribeHandlers.append(handler)
return self
}
Expand All @@ -100,7 +177,7 @@ open class Subscription<T: ParseObject>: ParseSubscription {
- parameter handler: The callback to register.
- returns: The same subscription, for easy chaining.
*/
@discardableResult open func handleUnsubscribe(_ handler: @escaping (Query<T>) -> Void) -> Subscription {
@discardableResult open func handleUnsubscribe(_ handler: @escaping (Query<T>) -> Void) -> SubscriptionCallback {
unsubscribeHandlers.append(handler)
return self
}
Expand All @@ -123,7 +200,7 @@ open class Subscription<T: ParseObject>: ParseSubscription {
}
}

extension Subscription {
extension SubscriptionCallback {

/**
Register a callback for when an event occurs of a specific type
Expand All @@ -136,7 +213,7 @@ extension Subscription {
- returns: The same subscription, for easy chaining.
*/
@discardableResult public func handle(_ eventType: @escaping (T) -> Event<T>,
_ handler: @escaping (Query<T>, T) -> Void) -> Subscription {
_ handler: @escaping (Query<T>, T) -> Void) -> SubscriptionCallback {
return handleEvent { query, event in
switch event {
case .entered(let obj) where eventType(obj) == event: handler(query, obj)
Expand Down
Loading

0 comments on commit 39f20d2

Please sign in to comment.