Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add SwiftUI query example to playgrounds #181

Merged
merged 4 commits into from
Jul 2, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
//: [Previous](@previous)

//: For this page, make sure your build target is set to ParseSwift (iOS) and targeting
//: an iPhone, iPod, or iPad. Also be sure your `Playground Settings`
//: in the `File Inspector` is `Platform = iOS`. This is because
//: SwiftUI in macOS Playgrounds doesn't seem to build correctly
//: Be sure to switch your target and `Playground Settings` back to
//: macOS after leaving this page.

#if canImport(SwiftUI)
import PlaygroundSupport
import Foundation
import ParseSwift
import SwiftUI
#if canImport(Combine)
import Combine
#endif

PlaygroundPage.current.needsIndefiniteExecution = true

initializeParse()

//: Create your own value typed ParseObject.
struct GameScore: ParseObject, Identifiable {

//: Conform to Identifiable for iOS13+
var id: String { // swiftlint:disable:this identifier_name
if let objectId = self.objectId {
return objectId
} else {
return UUID().uuidString
}
}

//: These are required for any Object.
var objectId: String?
var createdAt: Date?
var updatedAt: Date?
var ACL: ParseACL?

//: Your own properties.
var score: Int = 0
var location: ParseGeoPoint?
var name: String?

//: Custom initializer.
init(name: String, score: Int) {
self.name = name
self.score = score
}
}

//: To use queries with SwiftUI

//: Create a custom view model that queries GameScore's.
class ViewModel: ObservableObject {
@Published var objects = [GameScore]()
@Published var error: ParseError?

private var subscriptions = Set<AnyCancellable>()

init() {
fetchScores()
}

func fetchScores() {
let query = GameScore.query("score" > 2)
.order([.descending("score")])
let publisher = query
.findPublisher()
.sink(receiveCompletion: { result in
switch result {
case .failure(let error):
// Publish error.
self.error = error
case .finished:
print("Successfully queried data")
}
},
receiveValue: {
// Publish found objects
self.objects = $0
print("Found \(self.objects.count), objects: \(self.objects)")
})
publisher.store(in: &subscriptions)
}
}

//: Create a SwiftUI view.
struct ContentView: View {

//: A view model in SwiftUI
@ObservedObject var viewModel = ViewModel()

var body: some View {
NavigationView {
if let error = viewModel.error {
Text(error.debugDescription)
} else {
//: Warning - List seems to only work in Playgrounds Xcode 13+.
List(viewModel.objects, id: \.objectId) { object in
VStack(alignment: .leading) {
Text("Score: \(object.score)")
.font(.headline)
if let createdAt = object.createdAt {
Text("\(createdAt.description)")
}
}
}
}
Spacer()
}
}
}

PlaygroundPage.current.setLiveView(ContentView())
#endif

//: [Next](@next)
3 changes: 2 additions & 1 deletion ParseSwift.playground/contents.xcplayground
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@
<page name='14 - Config'/>
<page name='15 - Custom ObjectId'/>
<page name='16 - Analytics'/>
<page name='17 - SwiftUI - Finding Objects'/>
</pages>
</playground>
</playground>
8 changes: 8 additions & 0 deletions ParseSwift.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,9 @@
708D035325215F9B00646C70 /* Deletable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 708D035125215F9B00646C70 /* Deletable.swift */; };
708D035425215F9B00646C70 /* Deletable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 708D035125215F9B00646C70 /* Deletable.swift */; };
708D035525215F9B00646C70 /* Deletable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 708D035125215F9B00646C70 /* Deletable.swift */; };
709B40C1268F999000ED2EAC /* IOS13Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 709B40C0268F999000ED2EAC /* IOS13Tests.swift */; };
709B40C2268F999000ED2EAC /* IOS13Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 709B40C0268F999000ED2EAC /* IOS13Tests.swift */; };
709B40C3268F999000ED2EAC /* IOS13Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 709B40C0268F999000ED2EAC /* IOS13Tests.swift */; };
709B98352556EC7400507778 /* ParseSwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 912C9BD824D3011F009947C3 /* ParseSwift.framework */; };
709B984B2556ECAA00507778 /* MockURLProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 911DB13224C494390027F3C7 /* MockURLProtocol.swift */; };
709B984C2556ECAA00507778 /* APICommandTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 911DB12D24C4837E0027F3C7 /* APICommandTests.swift */; };
Expand Down Expand Up @@ -658,6 +661,7 @@
707A3C1025B0A8E8000D215C /* ParseAnonymous.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseAnonymous.swift; sourceTree = "<group>"; };
707A3C1F25B14BCF000D215C /* ParseApple.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseApple.swift; sourceTree = "<group>"; };
708D035125215F9B00646C70 /* Deletable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Deletable.swift; sourceTree = "<group>"; };
709B40C0268F999000ED2EAC /* IOS13Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IOS13Tests.swift; sourceTree = "<group>"; };
709B98302556EC7400507778 /* ParseSwiftTeststvOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ParseSwiftTeststvOS.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
709B98342556EC7400507778 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
70A2D81E25B36A7D001BEB7D /* ParseAuthenticationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseAuthenticationTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -871,6 +875,7 @@
7003957525A0EE770052CB31 /* BatchUtilsTests.swift */,
705726ED2592C91C00F0ADD5 /* HashTests.swift */,
70DFEA892618E77800F8EB4B /* InitializeSDKTests.swift */,
709B40C0268F999000ED2EAC /* IOS13Tests.swift */,
4AA8076E1F794C1C008CD551 /* KeychainStoreTests.swift */,
9194657724F16E330070296B /* ParseACLTests.swift */,
70170A4D2656EBA50070C905 /* ParseAnalyticsTests.swift */,
Expand Down Expand Up @@ -1790,6 +1795,7 @@
70CE1D892545BF730018D572 /* ParsePointerTests.swift in Sources */,
89899D772603CF66002E2043 /* ParseFacebookTests.swift in Sources */,
70386A4625D99C8B0048EC1B /* ParseLDAPTests.swift in Sources */,
709B40C1268F999000ED2EAC /* IOS13Tests.swift in Sources */,
911DB12E24C4837E0027F3C7 /* APICommandTests.swift in Sources */,
70732C5A2606CCAD000CAB81 /* ParseObjectCustomObjectIdTests.swift in Sources */,
911DB12C24C3F7720027F3C7 /* MockURLResponse.swift in Sources */,
Expand Down Expand Up @@ -1955,6 +1961,7 @@
709B98532556ECAA00507778 /* ParsePointerTests.swift in Sources */,
89899D822603CF67002E2043 /* ParseFacebookTests.swift in Sources */,
70386A4825D99C8B0048EC1B /* ParseLDAPTests.swift in Sources */,
709B40C3268F999000ED2EAC /* IOS13Tests.swift in Sources */,
709B984C2556ECAA00507778 /* APICommandTests.swift in Sources */,
70732C5C2606CCAD000CAB81 /* ParseObjectCustomObjectIdTests.swift in Sources */,
709B984D2556ECAA00507778 /* AnyDecodableTests.swift in Sources */,
Expand Down Expand Up @@ -2018,6 +2025,7 @@
70F2E2B7254F283000B2EA5C /* ParsePointerTests.swift in Sources */,
89899D812603CF67002E2043 /* ParseFacebookTests.swift in Sources */,
70386A4725D99C8B0048EC1B /* ParseLDAPTests.swift in Sources */,
709B40C2268F999000ED2EAC /* IOS13Tests.swift in Sources */,
70F2E2B5254F283000B2EA5C /* ParseEncoderExtraTests.swift in Sources */,
70732C5B2606CCAD000CAB81 /* ParseObjectCustomObjectIdTests.swift in Sources */,
70F2E2C2254F283000B2EA5C /* APICommandTests.swift in Sources */,
Expand Down
4 changes: 2 additions & 2 deletions Sources/ParseSwift/Coding/ParseEncoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,9 @@ public struct ParseEncoder {
switch self {

case .object:
return Set(["createdAt", "updatedAt", "objectId", "className", "emailVerified"])
return Set(["createdAt", "updatedAt", "objectId", "className", "emailVerified", "id"])
case .customObjectId:
return Set(["createdAt", "updatedAt", "className", "emailVerified"])
return Set(["createdAt", "updatedAt", "className", "emailVerified", "id"])
case .cloud:
return Set(["functionJobName"])
case .none:
Expand Down
2 changes: 1 addition & 1 deletion Sources/ParseSwift/LiveQuery/SubscriptionCallback.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ open class SubscriptionCallback<T: ParseObject>: ParseSubscription {
}

/**
Register a callback for when a client succesfully subscribes to a query.
Register a callback for when a client successfully subscribes to a query.
- parameter handler: The callback to register.
- returns: The same subscription, for easy chaining.
*/
Expand Down
2 changes: 1 addition & 1 deletion Sources/ParseSwift/Objects/ParseObject.swift
Original file line number Diff line number Diff line change
Expand Up @@ -786,4 +786,4 @@ extension ParseObject {
internal func deleteCommand() throws -> API.NonParseBodyCommand<NoBody, NoBody> {
try API.NonParseBodyCommand<NoBody, NoBody>.deleteCommand(self)
}
}// swiftlint:disable:this file_length
} // swiftlint:disable:this file_length
145 changes: 145 additions & 0 deletions Tests/ParseSwiftTests/IOS13Tests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
//
// IOS13Tests.swift
// ParseSwift
//
// Created by Corey Baker on 7/2/21.
// Copyright © 2021 Parse Community. All rights reserved.
//

import Foundation
import XCTest
@testable import ParseSwift

@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *)
class IOS13Tests: XCTestCase { // swiftlint:disable:this type_body_length
struct Level: ParseObject {
var objectId: String?

var createdAt: Date?

var updatedAt: Date?

var ACL: ParseACL?

var name = "First"
}

struct GameScore: ParseObject, Identifiable {

// Comform to Identifiable
var id: String { // swiftlint:disable:this identifier_name
if let objectId = self.objectId {
return objectId
} else {
return UUID().uuidString
}
}

//: Those are required for Object
var objectId: String?
var createdAt: Date?
var updatedAt: Date?
var ACL: ParseACL?

//: Your own properties
var score: Int?
var player: String?
var level: Level?
var levels: [Level]?

//custom initializers
init (objectId: String?) {
self.objectId = objectId
}
init(score: Int) {
self.score = score
self.player = "Jen"
}
init(score: Int, name: String) {
self.score = score
self.player = name
}
}

override func setUpWithError() throws {
try super.setUpWithError()
guard let url = URL(string: "http://localhost:1337/1") else {
XCTFail("Should create valid URL")
return
}
ParseSwift.initialize(applicationId: "applicationId",
clientKey: "clientKey",
masterKey: "masterKey",
serverURL: url,
testing: true)
}

override func tearDownWithError() throws {
try super.tearDownWithError()
MockURLProtocol.removeAll()
#if !os(Linux) && !os(Android)
try KeychainStore.shared.deleteAll()
#endif
try ParseStorage.shared.deleteAll()

guard let fileManager = ParseFileManager(),
let defaultDirectoryPath = fileManager.defaultDataDirectoryPath else {
throw ParseError(code: .unknownError, message: "Should have initialized file manage")
}

let directory2 = defaultDirectoryPath
.appendingPathComponent(ParseConstants.fileDownloadsDirectory, isDirectory: true)
let expectation2 = XCTestExpectation(description: "Delete files2")
fileManager.removeDirectoryContents(directory2) { _ in
expectation2.fulfill()
}
wait(for: [expectation2], timeout: 20.0)
}

#if !os(Linux) && !os(Android)
func testSaveCommand() throws {
let score = GameScore(score: 10)
let className = score.className

let command = try score.saveCommand()
XCTAssertNotNil(command)
XCTAssertEqual(command.path.urlComponent, "/classes/\(className)")
XCTAssertEqual(command.method, API.Method.POST)
XCTAssertNil(command.params)
XCTAssertNotNil(command.data)

let expected = "GameScore ({\"score\":10,\"player\":\"Jen\"})"
let decoded = score.debugDescription
XCTAssertEqual(decoded, expected)
}

func testUpdateCommand() throws {
var score = GameScore(score: 10)
let className = score.className
let objectId = "yarr"
score.objectId = objectId
score.createdAt = Date()
score.updatedAt = score.createdAt

let command = try score.saveCommand()
XCTAssertNotNil(command)
XCTAssertEqual(command.path.urlComponent, "/classes/\(className)/\(objectId)")
XCTAssertEqual(command.method, API.Method.PUT)
XCTAssertNil(command.params)
XCTAssertNotNil(command.data)

guard let body = command.body else {
XCTFail("Should be able to unwrap")
return
}

let expected = "{\"score\":10,\"player\":\"Jen\"}"
let encoded = try ParseCoding.parseEncoder()
.encode(body, collectChildren: false,
objectsSavedBeforeThisOne: nil,
filesSavedBeforeThisOne: nil).encoded
let decoded = try XCTUnwrap(String(data: encoded, encoding: .utf8))
XCTAssertEqual(decoded, expected)
}
#endif
}