Skip to content

Commit 5b03511

Browse files
authored
Add SwiftUI query example to playgrounds (#181)
* Add SwiftUI query example to playgrounds * Skip encoding id in ParseEncoder * Add comment about List not working in Xcode < 13 * Remove skipping id in function and job
1 parent 5ee216f commit 5b03511

File tree

7 files changed

+278
-5
lines changed

7 files changed

+278
-5
lines changed
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
//: [Previous](@previous)
2+
3+
//: For this page, make sure your build target is set to ParseSwift (iOS) and targeting
4+
//: an iPhone, iPod, or iPad. Also be sure your `Playground Settings`
5+
//: in the `File Inspector` is `Platform = iOS`. This is because
6+
//: SwiftUI in macOS Playgrounds doesn't seem to build correctly
7+
//: Be sure to switch your target and `Playground Settings` back to
8+
//: macOS after leaving this page.
9+
10+
#if canImport(SwiftUI)
11+
import PlaygroundSupport
12+
import Foundation
13+
import ParseSwift
14+
import SwiftUI
15+
#if canImport(Combine)
16+
import Combine
17+
#endif
18+
19+
PlaygroundPage.current.needsIndefiniteExecution = true
20+
21+
initializeParse()
22+
23+
//: Create your own value typed ParseObject.
24+
struct GameScore: ParseObject, Identifiable {
25+
26+
//: Conform to Identifiable for iOS13+
27+
var id: String { // swiftlint:disable:this identifier_name
28+
if let objectId = self.objectId {
29+
return objectId
30+
} else {
31+
return UUID().uuidString
32+
}
33+
}
34+
35+
//: These are required for any Object.
36+
var objectId: String?
37+
var createdAt: Date?
38+
var updatedAt: Date?
39+
var ACL: ParseACL?
40+
41+
//: Your own properties.
42+
var score: Int = 0
43+
var location: ParseGeoPoint?
44+
var name: String?
45+
46+
//: Custom initializer.
47+
init(name: String, score: Int) {
48+
self.name = name
49+
self.score = score
50+
}
51+
}
52+
53+
//: To use queries with SwiftUI
54+
55+
//: Create a custom view model that queries GameScore's.
56+
class ViewModel: ObservableObject {
57+
@Published var objects = [GameScore]()
58+
@Published var error: ParseError?
59+
60+
private var subscriptions = Set<AnyCancellable>()
61+
62+
init() {
63+
fetchScores()
64+
}
65+
66+
func fetchScores() {
67+
let query = GameScore.query("score" > 2)
68+
.order([.descending("score")])
69+
let publisher = query
70+
.findPublisher()
71+
.sink(receiveCompletion: { result in
72+
switch result {
73+
case .failure(let error):
74+
// Publish error.
75+
self.error = error
76+
case .finished:
77+
print("Successfully queried data")
78+
}
79+
},
80+
receiveValue: {
81+
// Publish found objects
82+
self.objects = $0
83+
print("Found \(self.objects.count), objects: \(self.objects)")
84+
})
85+
publisher.store(in: &subscriptions)
86+
}
87+
}
88+
89+
//: Create a SwiftUI view.
90+
struct ContentView: View {
91+
92+
//: A view model in SwiftUI
93+
@ObservedObject var viewModel = ViewModel()
94+
95+
var body: some View {
96+
NavigationView {
97+
if let error = viewModel.error {
98+
Text(error.debugDescription)
99+
} else {
100+
//: Warning - List seems to only work in Playgrounds Xcode 13+.
101+
List(viewModel.objects, id: \.objectId) { object in
102+
VStack(alignment: .leading) {
103+
Text("Score: \(object.score)")
104+
.font(.headline)
105+
if let createdAt = object.createdAt {
106+
Text("\(createdAt.description)")
107+
}
108+
}
109+
}
110+
}
111+
Spacer()
112+
}
113+
}
114+
}
115+
116+
PlaygroundPage.current.setLiveView(ContentView())
117+
#endif
118+
119+
//: [Next](@next)

ParseSwift.playground/contents.xcplayground

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,6 @@
1717
<page name='14 - Config'/>
1818
<page name='15 - Custom ObjectId'/>
1919
<page name='16 - Analytics'/>
20+
<page name='17 - SwiftUI - Finding Objects'/>
2021
</pages>
21-
</playground>
22+
</playground>

ParseSwift.xcodeproj/project.pbxproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,9 @@
230230
708D035325215F9B00646C70 /* Deletable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 708D035125215F9B00646C70 /* Deletable.swift */; };
231231
708D035425215F9B00646C70 /* Deletable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 708D035125215F9B00646C70 /* Deletable.swift */; };
232232
708D035525215F9B00646C70 /* Deletable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 708D035125215F9B00646C70 /* Deletable.swift */; };
233+
709B40C1268F999000ED2EAC /* IOS13Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 709B40C0268F999000ED2EAC /* IOS13Tests.swift */; };
234+
709B40C2268F999000ED2EAC /* IOS13Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 709B40C0268F999000ED2EAC /* IOS13Tests.swift */; };
235+
709B40C3268F999000ED2EAC /* IOS13Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 709B40C0268F999000ED2EAC /* IOS13Tests.swift */; };
233236
709B98352556EC7400507778 /* ParseSwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 912C9BD824D3011F009947C3 /* ParseSwift.framework */; };
234237
709B984B2556ECAA00507778 /* MockURLProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 911DB13224C494390027F3C7 /* MockURLProtocol.swift */; };
235238
709B984C2556ECAA00507778 /* APICommandTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 911DB12D24C4837E0027F3C7 /* APICommandTests.swift */; };
@@ -658,6 +661,7 @@
658661
707A3C1025B0A8E8000D215C /* ParseAnonymous.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseAnonymous.swift; sourceTree = "<group>"; };
659662
707A3C1F25B14BCF000D215C /* ParseApple.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseApple.swift; sourceTree = "<group>"; };
660663
708D035125215F9B00646C70 /* Deletable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Deletable.swift; sourceTree = "<group>"; };
664+
709B40C0268F999000ED2EAC /* IOS13Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IOS13Tests.swift; sourceTree = "<group>"; };
661665
709B98302556EC7400507778 /* ParseSwiftTeststvOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ParseSwiftTeststvOS.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
662666
709B98342556EC7400507778 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
663667
70A2D81E25B36A7D001BEB7D /* ParseAuthenticationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseAuthenticationTests.swift; sourceTree = "<group>"; };
@@ -871,6 +875,7 @@
871875
7003957525A0EE770052CB31 /* BatchUtilsTests.swift */,
872876
705726ED2592C91C00F0ADD5 /* HashTests.swift */,
873877
70DFEA892618E77800F8EB4B /* InitializeSDKTests.swift */,
878+
709B40C0268F999000ED2EAC /* IOS13Tests.swift */,
874879
4AA8076E1F794C1C008CD551 /* KeychainStoreTests.swift */,
875880
9194657724F16E330070296B /* ParseACLTests.swift */,
876881
70170A4D2656EBA50070C905 /* ParseAnalyticsTests.swift */,
@@ -1790,6 +1795,7 @@
17901795
70CE1D892545BF730018D572 /* ParsePointerTests.swift in Sources */,
17911796
89899D772603CF66002E2043 /* ParseFacebookTests.swift in Sources */,
17921797
70386A4625D99C8B0048EC1B /* ParseLDAPTests.swift in Sources */,
1798+
709B40C1268F999000ED2EAC /* IOS13Tests.swift in Sources */,
17931799
911DB12E24C4837E0027F3C7 /* APICommandTests.swift in Sources */,
17941800
70732C5A2606CCAD000CAB81 /* ParseObjectCustomObjectIdTests.swift in Sources */,
17951801
911DB12C24C3F7720027F3C7 /* MockURLResponse.swift in Sources */,
@@ -1955,6 +1961,7 @@
19551961
709B98532556ECAA00507778 /* ParsePointerTests.swift in Sources */,
19561962
89899D822603CF67002E2043 /* ParseFacebookTests.swift in Sources */,
19571963
70386A4825D99C8B0048EC1B /* ParseLDAPTests.swift in Sources */,
1964+
709B40C3268F999000ED2EAC /* IOS13Tests.swift in Sources */,
19581965
709B984C2556ECAA00507778 /* APICommandTests.swift in Sources */,
19591966
70732C5C2606CCAD000CAB81 /* ParseObjectCustomObjectIdTests.swift in Sources */,
19601967
709B984D2556ECAA00507778 /* AnyDecodableTests.swift in Sources */,
@@ -2018,6 +2025,7 @@
20182025
70F2E2B7254F283000B2EA5C /* ParsePointerTests.swift in Sources */,
20192026
89899D812603CF67002E2043 /* ParseFacebookTests.swift in Sources */,
20202027
70386A4725D99C8B0048EC1B /* ParseLDAPTests.swift in Sources */,
2028+
709B40C2268F999000ED2EAC /* IOS13Tests.swift in Sources */,
20212029
70F2E2B5254F283000B2EA5C /* ParseEncoderExtraTests.swift in Sources */,
20222030
70732C5B2606CCAD000CAB81 /* ParseObjectCustomObjectIdTests.swift in Sources */,
20232031
70F2E2C2254F283000B2EA5C /* APICommandTests.swift in Sources */,

Sources/ParseSwift/Coding/ParseEncoder.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,9 @@ public struct ParseEncoder {
7272
switch self {
7373

7474
case .object:
75-
return Set(["createdAt", "updatedAt", "objectId", "className", "emailVerified"])
75+
return Set(["createdAt", "updatedAt", "objectId", "className", "emailVerified", "id"])
7676
case .customObjectId:
77-
return Set(["createdAt", "updatedAt", "className", "emailVerified"])
77+
return Set(["createdAt", "updatedAt", "className", "emailVerified", "id"])
7878
case .cloud:
7979
return Set(["functionJobName"])
8080
case .none:

Sources/ParseSwift/LiveQuery/SubscriptionCallback.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ open class SubscriptionCallback<T: ParseObject>: ParseSubscription {
3939
}
4040

4141
/**
42-
Register a callback for when a client succesfully subscribes to a query.
42+
Register a callback for when a client successfully subscribes to a query.
4343
- parameter handler: The callback to register.
4444
- returns: The same subscription, for easy chaining.
4545
*/

Sources/ParseSwift/Objects/ParseObject.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -786,4 +786,4 @@ extension ParseObject {
786786
internal func deleteCommand() throws -> API.NonParseBodyCommand<NoBody, NoBody> {
787787
try API.NonParseBodyCommand<NoBody, NoBody>.deleteCommand(self)
788788
}
789-
}// swiftlint:disable:this file_length
789+
} // swiftlint:disable:this file_length
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
//
2+
// IOS13Tests.swift
3+
// ParseSwift
4+
//
5+
// Created by Corey Baker on 7/2/21.
6+
// Copyright © 2021 Parse Community. All rights reserved.
7+
//
8+
9+
import Foundation
10+
import XCTest
11+
@testable import ParseSwift
12+
13+
@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *)
14+
class IOS13Tests: XCTestCase { // swiftlint:disable:this type_body_length
15+
struct Level: ParseObject {
16+
var objectId: String?
17+
18+
var createdAt: Date?
19+
20+
var updatedAt: Date?
21+
22+
var ACL: ParseACL?
23+
24+
var name = "First"
25+
}
26+
27+
struct GameScore: ParseObject, Identifiable {
28+
29+
// Comform to Identifiable
30+
var id: String { // swiftlint:disable:this identifier_name
31+
if let objectId = self.objectId {
32+
return objectId
33+
} else {
34+
return UUID().uuidString
35+
}
36+
}
37+
38+
//: Those are required for Object
39+
var objectId: String?
40+
var createdAt: Date?
41+
var updatedAt: Date?
42+
var ACL: ParseACL?
43+
44+
//: Your own properties
45+
var score: Int?
46+
var player: String?
47+
var level: Level?
48+
var levels: [Level]?
49+
50+
//custom initializers
51+
init (objectId: String?) {
52+
self.objectId = objectId
53+
}
54+
init(score: Int) {
55+
self.score = score
56+
self.player = "Jen"
57+
}
58+
init(score: Int, name: String) {
59+
self.score = score
60+
self.player = name
61+
}
62+
}
63+
64+
override func setUpWithError() throws {
65+
try super.setUpWithError()
66+
guard let url = URL(string: "http://localhost:1337/1") else {
67+
XCTFail("Should create valid URL")
68+
return
69+
}
70+
ParseSwift.initialize(applicationId: "applicationId",
71+
clientKey: "clientKey",
72+
masterKey: "masterKey",
73+
serverURL: url,
74+
testing: true)
75+
}
76+
77+
override func tearDownWithError() throws {
78+
try super.tearDownWithError()
79+
MockURLProtocol.removeAll()
80+
#if !os(Linux) && !os(Android)
81+
try KeychainStore.shared.deleteAll()
82+
#endif
83+
try ParseStorage.shared.deleteAll()
84+
85+
guard let fileManager = ParseFileManager(),
86+
let defaultDirectoryPath = fileManager.defaultDataDirectoryPath else {
87+
throw ParseError(code: .unknownError, message: "Should have initialized file manage")
88+
}
89+
90+
let directory2 = defaultDirectoryPath
91+
.appendingPathComponent(ParseConstants.fileDownloadsDirectory, isDirectory: true)
92+
let expectation2 = XCTestExpectation(description: "Delete files2")
93+
fileManager.removeDirectoryContents(directory2) { _ in
94+
expectation2.fulfill()
95+
}
96+
wait(for: [expectation2], timeout: 20.0)
97+
}
98+
99+
#if !os(Linux) && !os(Android)
100+
func testSaveCommand() throws {
101+
let score = GameScore(score: 10)
102+
let className = score.className
103+
104+
let command = try score.saveCommand()
105+
XCTAssertNotNil(command)
106+
XCTAssertEqual(command.path.urlComponent, "/classes/\(className)")
107+
XCTAssertEqual(command.method, API.Method.POST)
108+
XCTAssertNil(command.params)
109+
XCTAssertNotNil(command.data)
110+
111+
let expected = "GameScore ({\"score\":10,\"player\":\"Jen\"})"
112+
let decoded = score.debugDescription
113+
XCTAssertEqual(decoded, expected)
114+
}
115+
116+
func testUpdateCommand() throws {
117+
var score = GameScore(score: 10)
118+
let className = score.className
119+
let objectId = "yarr"
120+
score.objectId = objectId
121+
score.createdAt = Date()
122+
score.updatedAt = score.createdAt
123+
124+
let command = try score.saveCommand()
125+
XCTAssertNotNil(command)
126+
XCTAssertEqual(command.path.urlComponent, "/classes/\(className)/\(objectId)")
127+
XCTAssertEqual(command.method, API.Method.PUT)
128+
XCTAssertNil(command.params)
129+
XCTAssertNotNil(command.data)
130+
131+
guard let body = command.body else {
132+
XCTFail("Should be able to unwrap")
133+
return
134+
}
135+
136+
let expected = "{\"score\":10,\"player\":\"Jen\"}"
137+
let encoded = try ParseCoding.parseEncoder()
138+
.encode(body, collectChildren: false,
139+
objectsSavedBeforeThisOne: nil,
140+
filesSavedBeforeThisOne: nil).encoded
141+
let decoded = try XCTUnwrap(String(data: encoded, encoding: .utf8))
142+
XCTAssertEqual(decoded, expected)
143+
}
144+
#endif
145+
}

0 commit comments

Comments
 (0)