FirestoreQuery.swift 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. /*
  2. * Copyright 2021 Google LLC
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. import SwiftUI
  17. import FirebaseFirestore
  18. /// The strategy to use when an error occurs during mapping Firestore documents
  19. /// to the target type of `FirestoreQuery`.
  20. ///
  21. public enum DecodingFailureStrategy {
  22. /// Ignore any errors that occur when mapping Firestore documents.
  23. case ignore
  24. /// Raise an error when mapping a Firestore document fails.
  25. case raise
  26. }
  27. /// A property wrapper that listens to a Firestore collection.
  28. ///
  29. /// In the following example, `FirestoreQuery` will fetch all documents from the
  30. /// `fruits` collection, filtering only documents whose `isFavourite` attribute
  31. /// is equal to `true`, map members of result set to the `Fruit` type, and make
  32. /// them available via the wrapped value `fruits`.
  33. ///
  34. /// struct ContentView: View {
  35. /// @FirestoreQuery(
  36. /// collectionPath: "fruits",
  37. /// predicates: [.whereField("isFavourite", isEqualTo: true)]
  38. /// ) var fruits: [Fruit]
  39. ///
  40. /// var body: some View {
  41. /// List(fruits) { fruit in
  42. /// Text(fruit.name)
  43. /// }
  44. /// }
  45. /// }
  46. ///
  47. /// `FirestoreQuery` also supports returning a `Result` type. The `.success` case
  48. /// returns an array of elements, whereas the `.failure` case returns an error
  49. /// in case mapping the Firestore docments wasn't successful:
  50. ///
  51. /// struct ContentView: View {
  52. /// @FirestoreQuery(
  53. /// collectionPath: "fruits",
  54. /// predicates: [.whereField("isFavourite", isEqualTo: true)]
  55. /// ) var fruitResults: Result<[Fruit], Error>
  56. ///
  57. /// var body: some View {
  58. /// if case let .success(fruits) = fruitResults {
  59. /// List(fruits) { fruit in
  60. /// Text(fruit.name)
  61. /// }
  62. /// } else if case let .failure(error) = fruitResults {
  63. /// Text("Couldn't map data: \(error.localizedDescription)")
  64. /// }
  65. /// }
  66. ///
  67. /// Alternatively, the _projected value_ of the property wrapper provides access to
  68. /// the `error` as well. This allows you to display a list of all successfully mapped
  69. /// documents, as well as an error message with details about the documents that couldn't
  70. /// be mapped successfully (e.g. because of a field name mismatch).
  71. ///
  72. /// struct ContentView: View {
  73. /// @FirestoreQuery(
  74. /// collectionPath: "mappingFailure",
  75. /// decodingFailureStrategy: .ignore
  76. /// ) private var fruits: [Fruit]
  77. ///
  78. /// var body: some View {
  79. /// VStack(alignment: .leading) {
  80. /// List(fruits) { fruit in
  81. /// Text(fruit.name)
  82. /// }
  83. /// if $fruits.error != nil {
  84. /// HStack {
  85. /// Text("There was an error")
  86. /// .foregroundColor(Color(UIColor.systemBackground))
  87. /// Spacer()
  88. /// }
  89. /// .padding(30)
  90. /// .background(Color.red)
  91. /// }
  92. /// }
  93. /// }
  94. /// }
  95. ///
  96. /// Internally, `@FirestoreQuery` sets up a snapshot listener and publishes
  97. /// any incoming changes via an `@StateObject`.
  98. ///
  99. /// The projected value of this property wrapper provides access to a
  100. /// configuration object of type `FirestoreQueryConfiguration` which can be used
  101. /// to modify the query criteria. Changing the filter predicates results in the
  102. /// underlying snapshot listener being unregistered and a new one registered.
  103. ///
  104. /// Button("Show only Apples and Oranges") {
  105. /// $fruits.predicates = [.whereField("name", isIn: ["Apple", "Orange]]
  106. /// }
  107. ///
  108. /// This property wrapper does not support updating the `wrappedValue`, i.e.
  109. /// you need to use Firestore's other APIs to add, delete, or modify documents.
  110. @available(iOS 14.0, macOS 11.0, macCatalyst 14.0, tvOS 14.0, watchOS 7.0, *)
  111. @propertyWrapper
  112. public struct FirestoreQuery<T>: DynamicProperty {
  113. @StateObject private var firestoreQueryObservable: FirestoreQueryObservable<T>
  114. /// The query's configurable properties.
  115. public struct Configuration {
  116. /// The query's collection path.
  117. public var path: String
  118. /// The query's predicates.
  119. public var predicates: [QueryPredicate]
  120. // The strategy to use in case there was a problem during the decoding phase.
  121. public var decodingFailureStrategy: DecodingFailureStrategy = .raise
  122. /// If any errors occurred, they will be exposed here as well.
  123. public var error: Error?
  124. }
  125. /// The results of the query.
  126. ///
  127. /// This property returns an empty collection when there are no matching results.
  128. public var wrappedValue: T {
  129. firestoreQueryObservable.items
  130. }
  131. /// A binding to the request's mutable configuration properties
  132. public var projectedValue: Configuration {
  133. get {
  134. firestoreQueryObservable.configuration
  135. }
  136. nonmutating set {
  137. firestoreQueryObservable.objectWillChange.send()
  138. firestoreQueryObservable.configuration = newValue
  139. }
  140. }
  141. /// Creates an instance by defining a query based on the parameters.
  142. /// - Parameters:
  143. /// - collectionPath: The path to the Firestore collection to query.
  144. /// - predicates: An optional array of `QueryPredicate`s that defines a
  145. /// filter for the fetched results.
  146. /// - decodingFailureStrategy: The strategy to use when there is a failure
  147. /// during the decoding phase. Defaults to `DecodingFailureStrategy.raise`.
  148. public init<U: Decodable>(collectionPath: String, predicates: [QueryPredicate] = [],
  149. decodingFailureStrategy: DecodingFailureStrategy = .raise)
  150. where T == [U] {
  151. let configuration = Configuration(
  152. path: collectionPath,
  153. predicates: predicates,
  154. decodingFailureStrategy: decodingFailureStrategy
  155. )
  156. _firestoreQueryObservable =
  157. StateObject(wrappedValue: FirestoreQueryObservable<T>(configuration: configuration))
  158. }
  159. /// Creates an instance by defining a query based on the parameters.
  160. /// - Parameters:
  161. /// - collectionPath: The path to the Firestore collection to query.
  162. /// - predicates: An optional array of `QueryPredicate`s that defines a
  163. /// filter for the fetched results.
  164. /// - decodingFailureStrategy: The strategy to use when there is a failure
  165. /// during the decoding phase. Defaults to `DecodingFailureStrategy.raise`.
  166. public init<U: Decodable>(collectionPath: String, predicates: [QueryPredicate] = [],
  167. decodingFailureStrategy: DecodingFailureStrategy = .raise)
  168. where T == Result<[U], Error> {
  169. let configuration = Configuration(
  170. path: collectionPath,
  171. predicates: predicates,
  172. decodingFailureStrategy: decodingFailureStrategy
  173. )
  174. _firestoreQueryObservable =
  175. StateObject(wrappedValue: FirestoreQueryObservable<T>(configuration: configuration))
  176. }
  177. }