diff --git a/ParseSwift.playground/Pages/1 - Your first Object.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/1 - Your first Object.xcplaygroundpage/Contents.swift index e935e3c25..92b8c6e48 100644 --- a/ParseSwift.playground/Pages/1 - Your first Object.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/1 - Your first Object.xcplaygroundpage/Contents.swift @@ -40,6 +40,10 @@ struct GameScore: ParseObject { //: Your own properties. var score: Int = 0 + //: Optional custom properties need to be marked with @NullableProperty or + // setting properties to `nil` won't propagate to server + @NullableProperty var gameEndDate: Date? + /*: It's recommended the developer adds the emptyObject computed property or similar. Gets an empty version of the respective object. This can be used when you only need to update a @@ -76,10 +80,10 @@ struct GameData: ParseObject { var ACL: ParseACL? //: Your own properties. - var polygon: ParsePolygon? + @NullableProperty var polygon: ParsePolygon? //: `ParseBytes` needs to be a part of the original schema //: or else you will need your masterKey to force an upgrade. - var bytes: ParseBytes? + @NullableProperty var bytes: ParseBytes? } //: It's recommended to place custom initializers in an extension diff --git a/ParseSwift.playground/Pages/11 - LiveQuery.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/11 - LiveQuery.xcplaygroundpage/Contents.swift index 115050a78..d652ce782 100644 --- a/ParseSwift.playground/Pages/11 - LiveQuery.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/11 - LiveQuery.xcplaygroundpage/Contents.swift @@ -19,8 +19,8 @@ struct GameScore: ParseObject { //: Your own properties. var score: Int = 0 - var location: ParseGeoPoint? - var name: String? + @NullableProperty var location: ParseGeoPoint? + @NullableProperty var name: String? } //: It's recommended to place custom initializers in an extension diff --git a/ParseSwift.playground/Pages/12 - Roles and Relations.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/12 - Roles and Relations.xcplaygroundpage/Contents.swift index ec480e99f..868866b1d 100644 --- a/ParseSwift.playground/Pages/12 - Roles and Relations.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/12 - Roles and Relations.xcplaygroundpage/Contents.swift @@ -28,7 +28,7 @@ struct User: ParseUser { var authData: [String: [String: String]?]? //: Your custom keys. - var customKey: String? + @NullableProperty var customKey: String? } struct Role: ParseRole { diff --git a/ParseSwift.playground/Pages/14 - Config.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/14 - Config.xcplaygroundpage/Contents.swift index e79bb5423..7733b1ca2 100644 --- a/ParseSwift.playground/Pages/14 - Config.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/14 - Config.xcplaygroundpage/Contents.swift @@ -18,8 +18,8 @@ struct Config: ParseConfig { //: If your server Config has any parameters their names and types should //: match your ParseCondig properties: - var welcomeMessage: String? - var winningNumber: Int? + @NullableProperty var welcomeMessage: String? + @NullableProperty var winningNumber: Int? } /*: Go to your Parse Dashboard and click `Config->Create a parameter`: diff --git a/ParseSwift.playground/Pages/17 - SwiftUI - Finding Objects.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/17 - SwiftUI - Finding Objects.xcplaygroundpage/Contents.swift index 83b89fa58..c27a31649 100644 --- a/ParseSwift.playground/Pages/17 - SwiftUI - Finding Objects.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/17 - SwiftUI - Finding Objects.xcplaygroundpage/Contents.swift @@ -28,9 +28,9 @@ struct GameScore: ParseObject { //: Your own properties. var score: Int = 0 - var location: ParseGeoPoint? - var name: String? - var myFiles: [ParseFile]? + @NullableProperty var location: ParseGeoPoint? + @NullableProperty var name: String? + @NullableProperty var myFiles: [ParseFile]? } //: It's recommended to place custom initializers in an extension diff --git a/ParseSwift.playground/Pages/18 - SwiftUI - Finding Objects With Custom ViewModel.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/18 - SwiftUI - Finding Objects With Custom ViewModel.xcplaygroundpage/Contents.swift index a7367d2f8..c0c685a9c 100644 --- a/ParseSwift.playground/Pages/18 - SwiftUI - Finding Objects With Custom ViewModel.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/18 - SwiftUI - Finding Objects With Custom ViewModel.xcplaygroundpage/Contents.swift @@ -29,8 +29,8 @@ struct GameScore: ParseObject { //: Your own properties. var score: Int = 0 - var location: ParseGeoPoint? - var name: String? + @NullableProperty var location: ParseGeoPoint? + @NullableProperty var name: String? } //: It's recommended to place custom initializers in an extension diff --git a/ParseSwift.playground/Pages/19 - SwiftUI - LiveQuery.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/19 - SwiftUI - LiveQuery.xcplaygroundpage/Contents.swift index ab24881d9..ed6b0d2d9 100644 --- a/ParseSwift.playground/Pages/19 - SwiftUI - LiveQuery.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/19 - SwiftUI - LiveQuery.xcplaygroundpage/Contents.swift @@ -27,8 +27,8 @@ struct GameScore: ParseObject { //: Your own properties. var score: Int = 0 - var location: ParseGeoPoint? - var name: String? + @NullableProperty var location: ParseGeoPoint? + @NullableProperty var name: String? } //: It's recommended to place custom initializers in an extension diff --git a/ParseSwift.playground/Pages/2 - Finding Objects.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/2 - Finding Objects.xcplaygroundpage/Contents.swift index 3a6dcdacc..f4502caea 100644 --- a/ParseSwift.playground/Pages/2 - Finding Objects.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/2 - Finding Objects.xcplaygroundpage/Contents.swift @@ -20,9 +20,9 @@ struct GameScore: ParseObject { var ACL: ParseACL? //: Your own properties. - var score: Int? - var timeStamp: Date? = Date() - var oldScore: Int? + @NullableProperty var score: Int? + @NullableProperty var timeStamp: Date? = Date() + @NullableProperty var oldScore: Int? } var score = GameScore() diff --git a/ParseSwift.playground/Pages/3 - User - Sign Up.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/3 - User - Sign Up.xcplaygroundpage/Contents.swift index 0227b2a47..b9f1436f9 100644 --- a/ParseSwift.playground/Pages/3 - User - Sign Up.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/3 - User - Sign Up.xcplaygroundpage/Contents.swift @@ -28,7 +28,7 @@ struct User: ParseUser { var authData: [String: [String: String]?]? //: Your custom keys. - var customKey: String? + @NullableProperty var customKey: String? } /*: Sign up user asynchronously - Performs work on background diff --git a/ParseSwift.playground/Pages/4 - User - Continued.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/4 - User - Continued.xcplaygroundpage/Contents.swift index b554f0c62..7fd9c6167 100644 --- a/ParseSwift.playground/Pages/4 - User - Continued.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/4 - User - Continued.xcplaygroundpage/Contents.swift @@ -28,10 +28,10 @@ struct User: ParseUser { var authData: [String: [String: String]?]? //: Your custom keys. - var customKey: String? - var score: GameScore? - var targetScore: GameScore? - var allScores: [GameScore]? + @NullableProperty var customKey: String? + @NullableProperty var score: GameScore? + @NullableProperty var targetScore: GameScore? + @NullableProperty var allScores: [GameScore]? /*: It's recommended the developer adds the emptyObject computed property or similar. @@ -68,7 +68,7 @@ struct GameScore: ParseObject { var ACL: ParseACL? //: Your own properties. - var score: Int? = 0 + @NullableProperty var score: Int? = 0 } //: It's recommended to place custom initializers in an extension diff --git a/ParseSwift.playground/Pages/6 - Installation.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/6 - Installation.xcplaygroundpage/Contents.swift index 035eb198b..816159289 100644 --- a/ParseSwift.playground/Pages/6 - Installation.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/6 - Installation.xcplaygroundpage/Contents.swift @@ -34,7 +34,7 @@ struct Installation: ParseInstallation { var localeIdentifier: String? //: Your custom keys - var customKey: String? + @NullableProperty var customKey: String? /*: It's recommended the developer adds the emptyObject computed property or similar. diff --git a/ParseSwift.playground/Pages/7 - GeoPoint.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/7 - GeoPoint.xcplaygroundpage/Contents.swift index be7f9da72..0ef6dbf84 100644 --- a/ParseSwift.playground/Pages/7 - GeoPoint.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/7 - GeoPoint.xcplaygroundpage/Contents.swift @@ -23,7 +23,7 @@ struct GameScore: ParseObject { var location: ParseGeoPoint? //: Your own properties - var score: Int? + @NullableProperty var score: Int? } //: It's recommended to place custom initializers in an extension diff --git a/ParseSwift.playground/Pages/8 - Pointers.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/8 - Pointers.xcplaygroundpage/Contents.swift index 30cd238fc..129c8f6dc 100644 --- a/ParseSwift.playground/Pages/8 - Pointers.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/8 - Pointers.xcplaygroundpage/Contents.swift @@ -23,7 +23,7 @@ struct Book: ParseObject { var relatedBook: Pointer? //: Your own properties. - var title: String? + @NullableProperty var title: String? } //: It's recommended to place custom initializers in an extension @@ -45,7 +45,7 @@ struct Author: ParseObject { //: Your own properties. var name: String var book: Book - var otherBooks: [Book]? + @NullableProperty var otherBooks: [Book]? init() { self.name = "hello" diff --git a/ParseSwift.playground/Pages/9 - Files.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/9 - Files.xcplaygroundpage/Contents.swift index d1231d133..709373697 100644 --- a/ParseSwift.playground/Pages/9 - Files.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/9 - Files.xcplaygroundpage/Contents.swift @@ -23,8 +23,8 @@ struct GameScore: ParseObject { //: Your own properties. var score: Int = 0 - var profilePicture: ParseFile? - var myData: ParseFile? + @NullableProperty var profilePicture: ParseFile? + @NullableProperty var myData: ParseFile? } //: It's recommended to place custom initializers in an extension diff --git a/ParseSwift.xcodeproj/project.pbxproj b/ParseSwift.xcodeproj/project.pbxproj index d4bcbe907..84587b92d 100644 --- a/ParseSwift.xcodeproj/project.pbxproj +++ b/ParseSwift.xcodeproj/project.pbxproj @@ -587,6 +587,10 @@ 91F346C3269B88F7005727B6 /* ParseCloudViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91F346C2269B88F7005727B6 /* ParseCloudViewModelTests.swift */; }; 91F346C4269B88F7005727B6 /* ParseCloudViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91F346C2269B88F7005727B6 /* ParseCloudViewModelTests.swift */; }; 91F346C5269B88F7005727B6 /* ParseCloudViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91F346C2269B88F7005727B6 /* ParseCloudViewModelTests.swift */; }; + 98A7D7BC272AF07300F5F190 /* NullableProperty.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98A7D7BB272AF07300F5F190 /* NullableProperty.swift */; }; + 98A7D7BD272AF07300F5F190 /* NullableProperty.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98A7D7BB272AF07300F5F190 /* NullableProperty.swift */; }; + 98A7D7BE272AF07300F5F190 /* NullableProperty.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98A7D7BB272AF07300F5F190 /* NullableProperty.swift */; }; + 98A7D7BF272AF07300F5F190 /* NullableProperty.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98A7D7BB272AF07300F5F190 /* NullableProperty.swift */; }; F971F4F624DE381A006CB79B /* ParseEncoderExtraTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F971F4F524DE381A006CB79B /* ParseEncoderExtraTests.swift */; }; F97B45CE24D9C6F200F4A88B /* ParseCoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B45B424D9C6F200F4A88B /* ParseCoding.swift */; }; F97B45CF24D9C6F200F4A88B /* ParseCoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B45B424D9C6F200F4A88B /* ParseCoding.swift */; }; @@ -961,6 +965,7 @@ 91F346B8269B766C005727B6 /* CloudViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudViewModel.swift; sourceTree = ""; }; 91F346BD269B77B5005727B6 /* CloudObservable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudObservable.swift; sourceTree = ""; }; 91F346C2269B88F7005727B6 /* ParseCloudViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseCloudViewModelTests.swift; sourceTree = ""; }; + 98A7D7BB272AF07300F5F190 /* NullableProperty.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NullableProperty.swift; sourceTree = ""; }; F971F4F524DE381A006CB79B /* ParseEncoderExtraTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseEncoderExtraTests.swift; sourceTree = ""; }; F97B45B424D9C6F200F4A88B /* ParseCoding.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParseCoding.swift; sourceTree = ""; }; F97B45B524D9C6F200F4A88B /* AnyDecodable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnyDecodable.swift; sourceTree = ""; }; @@ -1480,6 +1485,7 @@ F97B45B824D9C6F200F4A88B /* AnyCodable.swift */, F97B45B524D9C6F200F4A88B /* AnyDecodable.swift */, F97B45B924D9C6F200F4A88B /* AnyEncodable.swift */, + 98A7D7BB272AF07300F5F190 /* NullableProperty.swift */, F97B45B724D9C6F200F4A88B /* Extensions.swift */, F97B45B424D9C6F200F4A88B /* ParseCoding.swift */, F97B45B624D9C6F200F4A88B /* ParseEncoder.swift */, @@ -2089,6 +2095,7 @@ 70C5509225B4A99100B5DBC2 /* AddRelation.swift in Sources */, 708D035225215F9B00646C70 /* Deletable.swift in Sources */, F97B466424D9C88600F4A88B /* SecureStorage.swift in Sources */, + 98A7D7BC272AF07300F5F190 /* NullableProperty.swift in Sources */, 7004C22025B63C7A005E0AD9 /* ParseRelation.swift in Sources */, 7003959525A10DFC0052CB31 /* Messages.swift in Sources */, 703B091126BD992E005A112F /* ParseOperation+async.swift in Sources */, @@ -2298,6 +2305,7 @@ 70C5509325B4A99100B5DBC2 /* AddRelation.swift in Sources */, 708D035325215F9B00646C70 /* Deletable.swift in Sources */, F97B466524D9C88600F4A88B /* SecureStorage.swift in Sources */, + 98A7D7BD272AF07300F5F190 /* NullableProperty.swift in Sources */, 7004C22125B63C7A005E0AD9 /* ParseRelation.swift in Sources */, 7003959625A10DFC0052CB31 /* Messages.swift in Sources */, 703B091226BD992E005A112F /* ParseOperation+async.swift in Sources */, @@ -2602,6 +2610,7 @@ 70C5509525B4A99100B5DBC2 /* AddRelation.swift in Sources */, 708D035525215F9B00646C70 /* Deletable.swift in Sources */, 70110D55250680140091CC1D /* ParseConstants.swift in Sources */, + 98A7D7BF272AF07300F5F190 /* NullableProperty.swift in Sources */, 7004C22325B63C7A005E0AD9 /* ParseRelation.swift in Sources */, 7003959825A10DFC0052CB31 /* Messages.swift in Sources */, 703B091426BD992E005A112F /* ParseOperation+async.swift in Sources */, @@ -2725,6 +2734,7 @@ 70C5509425B4A99100B5DBC2 /* AddRelation.swift in Sources */, 708D035425215F9B00646C70 /* Deletable.swift in Sources */, 70110D54250680140091CC1D /* ParseConstants.swift in Sources */, + 98A7D7BE272AF07300F5F190 /* NullableProperty.swift in Sources */, 7004C22225B63C7A005E0AD9 /* ParseRelation.swift in Sources */, 7003959725A10DFC0052CB31 /* Messages.swift in Sources */, 703B091326BD992E005A112F /* ParseOperation+async.swift in Sources */, diff --git a/Sources/ParseSwift/Coding/NullableProperty.swift b/Sources/ParseSwift/Coding/NullableProperty.swift new file mode 100644 index 000000000..bad4b818f --- /dev/null +++ b/Sources/ParseSwift/Coding/NullableProperty.swift @@ -0,0 +1,64 @@ +// +// NullableProperty.swift +// +// Created by Steven Grosmark on 6/10/20. +// Source: https://github.com/g-mark/NullCodable +// + +import Foundation + +/// Property wrapper that encodes `nil` optional values as a delete operation +/// when encoded using `JSONEncoder`. +/// +/// For example, adding `@NullableProperty` like this: +/// ```swift +/// struct Test: Codable { +/// @NullableProperty var name: String? = nil +/// } +/// ``` +/// +@propertyWrapper +public struct NullableProperty { + + public var wrappedValue: Wrapped? + + public init(wrappedValue: Wrapped?) { + self.wrappedValue = wrappedValue + } +} + +extension NullableProperty: Encodable where Wrapped: Encodable { + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch wrappedValue { + case .some(let value): + try container.encode(value) + + case .none: + try container.encode(Delete()) + } + } +} + +extension NullableProperty: Decodable where Wrapped: Decodable { + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + if !container.decodeNil() { + wrappedValue = try container.decode(Wrapped.self) + } + } +} + +extension NullableProperty: Hashable where Wrapped: Hashable { } +extension NullableProperty: Equatable where Wrapped: Equatable { } + +extension KeyedDecodingContainer { + + public func decode(_ type: NullableProperty.Type, forKey key: KeyedDecodingContainer.Key) + throws -> NullableProperty where Wrapped: Decodable { + return try decodeIfPresent(NullableProperty.self, forKey: key) ?? + NullableProperty(wrappedValue: nil) + } +}