SwiftUI.swift 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585
  1. ////////////////////////////////////////////////////////////////////////////
  2. //
  3. // Copyright 2021 Realm Inc.
  4. //
  5. // Licensed under the Apache License, Version 2.0 (the "License");
  6. // you may not use this file except in compliance with the License.
  7. // You may obtain a copy of the License at
  8. //
  9. // http://www.apache.org/licenses/LICENSE-2.0
  10. //
  11. // Unless required by applicable law or agreed to in writing, software
  12. // distributed under the License is distributed on an "AS IS" BASIS,
  13. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. // See the License for the specific language governing permissions and
  15. // limitations under the License.
  16. //
  17. ////////////////////////////////////////////////////////////////////////////
  18. import Foundation
  19. #if canImport(SwiftUI) && canImport(Combine) && swift(>=5.3.1) && (REALM_HAVE_COMBINE || !SWIFT_PACKAGE)
  20. import SwiftUI
  21. import Combine
  22. import Realm
  23. import Realm.Private
  24. private func safeWrite<Value>(_ value: Value, _ block: (Value) -> Void) where Value: ThreadConfined {
  25. let thawed = value.realm == nil ? value : value.thaw() ?? value
  26. var didStartWrite = false
  27. if thawed.realm?.isInWriteTransaction == false {
  28. didStartWrite = true
  29. thawed.realm?.beginWrite()
  30. }
  31. block(thawed)
  32. if didStartWrite {
  33. try! thawed.realm?.commitWrite()
  34. }
  35. }
  36. @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
  37. private func createBinding<T: ThreadConfined, V>(_ value: T,
  38. forKeyPath keyPath: ReferenceWritableKeyPath<T, V>) -> Binding<V> {
  39. guard let value = value.isFrozen ? value.thaw() : value else {
  40. throwRealmException("Could not bind value")
  41. }
  42. // store last known value outside of the binding so that we can reference it if the parent
  43. // is invalidated
  44. var lastValue = value[keyPath: keyPath]
  45. return Binding(get: {
  46. guard !value.isInvalidated else {
  47. return lastValue
  48. }
  49. lastValue = value[keyPath: keyPath]
  50. if let value = lastValue as? ListBase & ThreadConfined, !value.isInvalidated && value.realm != nil {
  51. return value.freeze() as! V
  52. }
  53. return lastValue
  54. },
  55. set: { newValue in
  56. guard !value.isInvalidated else {
  57. return
  58. }
  59. safeWrite(value) { value in
  60. value[keyPath: keyPath] = newValue
  61. }
  62. })
  63. }
  64. // MARK: SwiftUIKVO
  65. @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
  66. internal final class SwiftUIKVO: NSObject {
  67. /// Objects must have observers removed before being added to a realm.
  68. /// They are stored here so that if they are appended through the Bound Property
  69. /// system, they can be de-observed before hand.
  70. fileprivate static var observedObjects = [NSObject: SwiftUIKVO.Subscription]()
  71. @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
  72. struct Subscription: Combine.Subscription {
  73. let observer: NSObject
  74. let value: NSObject
  75. let keyPaths: [String]
  76. var combineIdentifier: CombineIdentifier {
  77. CombineIdentifier(value)
  78. }
  79. func request(_ demand: Subscribers.Demand) {
  80. }
  81. func cancel() {
  82. guard SwiftUIKVO.observedObjects.keys.contains(value) else {
  83. return
  84. }
  85. keyPaths.forEach {
  86. value.removeObserver(observer, forKeyPath: $0)
  87. }
  88. SwiftUIKVO.observedObjects.removeValue(forKey: value)
  89. }
  90. }
  91. private let receive: () -> Void
  92. override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
  93. receive()
  94. }
  95. init<S>(subscriber: S) where S: Subscriber, S.Input == Void {
  96. receive = { _ = subscriber.receive() }
  97. super.init()
  98. }
  99. }
  100. // MARK: - ObservableStorage
  101. @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
  102. private final class ObservableStoragePublisher<ObjectType>: Publisher where ObjectType: ThreadConfined & RealmSubscribable {
  103. public typealias Output = Void
  104. public typealias Failure = Never
  105. private var subscribers = [AnySubscriber<Void, Never>]()
  106. private let value: ObjectType
  107. init(_ value: ObjectType) {
  108. self.value = value
  109. }
  110. func send() {
  111. subscribers.forEach {
  112. _ = $0.receive()
  113. }
  114. }
  115. public func receive<S>(subscriber: S) where S: Subscriber, Failure == S.Failure, Output == S.Input {
  116. subscribers.append(AnySubscriber(subscriber))
  117. if value.realm != nil && !value.isInvalidated, let value = value.thaw() {
  118. // if the value is managed
  119. let token = value._observe(subscriber)
  120. subscriber.receive(subscription: ObservationSubscription(token: token))
  121. } else if let value = value as? ObjectBase, !value.isInvalidated {
  122. // else if the value is unmanaged
  123. let schema = ObjectSchema(RLMObjectBaseObjectSchema(value)!)
  124. let kvo = SwiftUIKVO(subscriber: subscriber)
  125. var keyPaths = [String]()
  126. for property in schema.properties {
  127. keyPaths.append(property.name)
  128. value.addObserver(kvo, forKeyPath: property.name, options: .initial, context: nil)
  129. }
  130. let subscription = SwiftUIKVO.Subscription(observer: kvo, value: value, keyPaths: keyPaths)
  131. subscriber.receive(subscription: subscription)
  132. SwiftUIKVO.observedObjects[value] = subscription
  133. }
  134. }
  135. }
  136. @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
  137. private class ObservableStorage<ObservedType>: ObservableObject where ObservedType: RealmSubscribable & ThreadConfined & Equatable {
  138. @Published var value: ObservedType {
  139. willSet {
  140. if newValue != value {
  141. objectWillChange.send()
  142. self.objectWillChange = ObservableStoragePublisher(newValue)
  143. }
  144. }
  145. }
  146. var objectWillChange: ObservableStoragePublisher<ObservedType>
  147. init(_ value: ObservedType) {
  148. self.value = value.realm != nil && !value.isInvalidated ? value.thaw() ?? value : value
  149. self.objectWillChange = ObservableStoragePublisher(value)
  150. }
  151. }
  152. // MARK: - StateRealmObject
  153. /// A property wrapper type that instantiates an observable object.
  154. ///
  155. /// Create a state realm object in a ``SwiftUI/View``, ``SwiftUI/App``, or
  156. /// ``SwiftUI/Scene`` by applying the `@StateRealmObject` attribute to a property
  157. /// declaration and providing an initial value that conforms to the
  158. /// <doc://com.apple.documentation/documentation/Combine/ObservableObject>
  159. /// protocol:
  160. ///
  161. /// @StateRealmObject var model = DataModel()
  162. ///
  163. /// SwiftUI creates a new instance of the object only once for each instance of
  164. /// the structure that declares the object. When published properties of the
  165. /// observable realm object change, SwiftUI updates the parts of any view that depend
  166. /// on those properties. If unmanaged, the property will be read from the object itself,
  167. /// otherwise, it will be read from the underlying Realm. Changes to the value will update
  168. /// the view asynchronously:
  169. ///
  170. /// Text(model.title) // Updates the view any time `title` changes.
  171. ///
  172. /// You can pass the state object into a property that has the
  173. /// ``SwiftUI/ObservedRealmObject`` attribute.
  174. ///
  175. /// Get a ``SwiftUI/Binding`` to one of the state object's properties using the
  176. /// `$` operator. Use a binding when you want to create a two-way connection to
  177. /// one of the object's properties. For example, you can let a
  178. /// ``SwiftUI/Toggle`` control a Boolean value called `isEnabled` stored in the
  179. /// model:
  180. ///
  181. /// Toggle("Enabled", isOn: $model.isEnabled)
  182. ///
  183. /// This will write the modified `isEnabled` property to the `model` object's Realm.
  184. @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
  185. @propertyWrapper public struct StateRealmObject<T: RealmSubscribable & ThreadConfined & Equatable>: DynamicProperty {
  186. @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
  187. @StateObject private var storage: ObservableStorage<T>
  188. private let defaultValue: T
  189. /// :nodoc:
  190. public var wrappedValue: T {
  191. get {
  192. if storage.value.realm == nil {
  193. // if unmanaged return the unmanaged value
  194. return storage.value
  195. } else if storage.value.isInvalidated {
  196. // if invalidated, return the default value
  197. return defaultValue
  198. }
  199. // else return the frozen value. the frozen value
  200. // will be consumed by SwiftUI, which requires
  201. // the ability to cache and diff objects and collections
  202. // during some timeframe. The ObjectType is frozen so that
  203. // SwiftUI can cache state. other access points will thaw
  204. // the ObjectType
  205. return storage.value.freeze()
  206. }
  207. nonmutating set {
  208. storage.value = newValue
  209. }
  210. }
  211. /// :nodoc:
  212. public var projectedValue: Binding<T> {
  213. Binding(get: {
  214. if self.storage.value.isInvalidated {
  215. return self.defaultValue
  216. }
  217. return self.storage.value
  218. }, set: { newValue in
  219. self.storage.value = newValue
  220. })
  221. }
  222. /**
  223. Initialize a RealmState struct for a given thread confined type.
  224. - parameter wrappedValue The List reference to wrap and observe.
  225. */
  226. @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
  227. public init<Value>(wrappedValue: T) where T == List<Value> {
  228. self._storage = StateObject(wrappedValue: ObservableStorage(wrappedValue))
  229. defaultValue = T()
  230. }
  231. /**
  232. Initialize a RealmState struct for a given thread confined type.
  233. - parameter wrappedValue The ObjectBase reference to wrap and observe.
  234. */
  235. @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
  236. public init(wrappedValue: T) where T: ObjectKeyIdentifiable {
  237. self._storage = StateObject(wrappedValue: ObservableStorage(wrappedValue))
  238. defaultValue = T()
  239. }
  240. }
  241. // MARK: ObservedResults
  242. /// A property wrapper type that retrieves results from a Realm.
  243. ///
  244. /// The results use the realm configuration provided by
  245. /// the environment value `EnvironmentValues/realmConfiguration`.
  246. @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
  247. @propertyWrapper public struct ObservedResults<ResultType>: DynamicProperty, BoundCollection where ResultType: Object & ObjectKeyIdentifiable {
  248. private class Storage: ObservableStorage<Results<ResultType>> {
  249. private func didSet() {
  250. /// A base value to reset the state of the query if a user reassigns the `filter` or `sortDescriptor`
  251. value = try! Realm(configuration: configuration ?? Realm.Configuration.defaultConfiguration).objects(ResultType.self)
  252. if let sortDescriptor = sortDescriptor {
  253. value = value.sorted(byKeyPath: sortDescriptor.keyPath, ascending: sortDescriptor.ascending)
  254. }
  255. if let filter = filter {
  256. value = value.filter(filter)
  257. }
  258. }
  259. var sortDescriptor: SortDescriptor? {
  260. didSet {
  261. didSet()
  262. }
  263. }
  264. var filter: NSPredicate? {
  265. didSet {
  266. didSet()
  267. }
  268. }
  269. var configuration: Realm.Configuration? {
  270. didSet {
  271. didSet()
  272. }
  273. }
  274. }
  275. @Environment(\.realmConfiguration) var configuration
  276. @ObservedObject private var storage = Storage(Results(RLMResults.emptyDetached()))
  277. /// :nodoc:
  278. @State public var filter: NSPredicate? {
  279. willSet {
  280. storage.filter = newValue
  281. }
  282. }
  283. /// :nodoc:
  284. @State public var sortDescriptor: SortDescriptor? {
  285. willSet {
  286. storage.sortDescriptor = newValue
  287. }
  288. }
  289. /// :nodoc:
  290. public var wrappedValue: Results<ResultType> {
  291. storage.configuration != nil ? storage.value.freeze() : storage.value
  292. }
  293. /// :nodoc:
  294. public var projectedValue: Self {
  295. return self
  296. }
  297. /// :nodoc:
  298. public init(_ type: ResultType.Type,
  299. configuration: Realm.Configuration? = nil,
  300. filter: NSPredicate? = nil,
  301. sortDescriptor: SortDescriptor? = nil) {
  302. self.storage.configuration = configuration
  303. self.filter = filter
  304. self.sortDescriptor = sortDescriptor
  305. }
  306. public mutating func update() {
  307. // When the view updates, it will inject the @Environment
  308. // into the propertyWrapper
  309. if storage.configuration == nil || storage.configuration != configuration {
  310. storage.configuration = configuration
  311. }
  312. }
  313. }
  314. // MARK: ObservedRealmObject
  315. /// A property wrapper type that subscribes to an observable Realm `Object` or `List` and
  316. /// invalidates a view whenever the observable object changes.
  317. @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
  318. @propertyWrapper public struct ObservedRealmObject<ObjectType>: DynamicProperty where ObjectType: RealmSubscribable & ThreadConfined & ObservableObject & Equatable {
  319. /// A wrapper of the underlying observable object that can create bindings to
  320. /// its properties using dynamic member lookup.
  321. @dynamicMemberLookup @frozen public struct Wrapper {
  322. /// :nodoc:
  323. public var wrappedValue: ObjectType
  324. /// Returns a binding to the resulting value of a given key path.
  325. ///
  326. /// - Parameter keyPath : A key path to a specific resulting value.
  327. ///
  328. /// - Returns: A new binding.
  329. public subscript<Subject>(dynamicMember keyPath: ReferenceWritableKeyPath<ObjectType, Subject>) -> Binding<Subject> {
  330. createBinding(wrappedValue, forKeyPath: keyPath)
  331. }
  332. }
  333. /// The object to observe.
  334. @ObservedObject private var storage: ObservableStorage<ObjectType>
  335. /// A default value to avoid invalidated access.
  336. private let defaultValue: ObjectType
  337. /// :nodoc:
  338. public var wrappedValue: ObjectType {
  339. get {
  340. if storage.value.realm == nil {
  341. // if unmanaged return the unmanaged value
  342. return storage.value
  343. } else if storage.value.isInvalidated {
  344. // if invalidated, return the default value
  345. return defaultValue
  346. }
  347. // else return the frozen value. the frozen value
  348. // will be consumed by SwiftUI, which requires
  349. // the ability to cache and diff objects and collections
  350. // during some timeframe. The ObjectType is frozen so that
  351. // SwiftUI can cache state. other access points will thaw
  352. // the ObjectType
  353. return storage.value.freeze()
  354. }
  355. set {
  356. storage.value = newValue
  357. }
  358. }
  359. /// :nodoc:
  360. public var projectedValue: Wrapper {
  361. return Wrapper(wrappedValue: storage.value.isInvalidated ? defaultValue : storage.value)
  362. }
  363. /**
  364. Initialize a RealmState struct for a given thread confined type.
  365. - parameter wrappedValue The RealmSubscribable value to wrap and observe.
  366. */
  367. public init(wrappedValue: ObjectType) where ObjectType: ObjectKeyIdentifiable {
  368. _storage = ObservedObject(wrappedValue: ObservableStorage(wrappedValue))
  369. defaultValue = ObjectType()
  370. }
  371. /**
  372. Initialize a RealmState struct for a given thread confined type.
  373. - parameter wrappedValue The RealmSubscribable value to wrap and observe.
  374. */
  375. public init<V>(wrappedValue: ObjectType) where ObjectType == List<V> {
  376. _storage = ObservedObject(wrappedValue: ObservableStorage(wrappedValue))
  377. defaultValue = List()
  378. }
  379. }
  380. @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
  381. extension Binding where Value: ObjectBase & ThreadConfined {
  382. /// :nodoc:
  383. public subscript<V>(dynamicMember member: ReferenceWritableKeyPath<Value, V>) -> Binding<V> where V: _ManagedPropertyType {
  384. createBinding(wrappedValue, forKeyPath: member)
  385. }
  386. }
  387. /// :nodoc:
  388. @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
  389. public protocol BoundCollection {
  390. /// :nodoc:
  391. associatedtype Value
  392. /// :nodoc:
  393. var wrappedValue: Value { get }
  394. }
  395. @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
  396. public extension BoundCollection where Value: RealmCollection {
  397. /// :nodoc:
  398. typealias Element = Value.Element
  399. /// :nodoc:
  400. typealias Index = Value.Index
  401. /// :nodoc:
  402. typealias Indices = Value.Indices
  403. /// :nodoc:
  404. func remove<V>(at index: Index) where Value == List<V> {
  405. safeWrite(self.wrappedValue) { list in
  406. list.remove(at: index)
  407. }
  408. }
  409. /// :nodoc:
  410. func remove<V>(_ object: V) where Value == Results<V>, V: ObjectBase & ThreadConfined {
  411. guard let thawed = object.thaw(),
  412. let index = wrappedValue.thaw()?.index(of: thawed) else {
  413. return
  414. }
  415. safeWrite(self.wrappedValue) { results in
  416. results.realm?.delete(results[index])
  417. }
  418. }
  419. /// :nodoc:
  420. func remove<V>(atOffsets offsets: IndexSet) where Value == Results<V>, V: ObjectBase {
  421. safeWrite(self.wrappedValue) { results in
  422. results.realm?.delete(Array(offsets.map { results[$0] }))
  423. }
  424. }
  425. /// :nodoc:
  426. func remove<V>(atOffsets offsets: IndexSet) where Value == List<V> {
  427. safeWrite(self.wrappedValue) { list in
  428. list.remove(atOffsets: offsets)
  429. }
  430. }
  431. /// :nodoc:
  432. func move<V>(fromOffsets offsets: IndexSet, toOffset destination: Int) where Value == List<V> {
  433. safeWrite(self.wrappedValue) { list in
  434. list.move(fromOffsets: offsets, toOffset: destination)
  435. }
  436. }
  437. /// :nodoc:
  438. func append<V>(_ value: Value.Element) where Value == List<V>, Value.Element: RealmCollectionValue {
  439. safeWrite(self.wrappedValue) { list in
  440. list.append(value)
  441. }
  442. }
  443. /// :nodoc:
  444. func append<V>(_ value: Value.Element) where Value == List<V>, Value.Element: ObjectBase & ThreadConfined {
  445. // if the value is unmanaged but the list is managed, we are adding this value to the realm
  446. if value.realm == nil && self.wrappedValue.realm != nil {
  447. SwiftUIKVO.observedObjects[value]?.cancel()
  448. }
  449. safeWrite(self.wrappedValue) { list in
  450. list.append(value)
  451. }
  452. }
  453. /// :nodoc:
  454. func append<V>(_ value: Value.Element) where Value == Results<V>, V: Object {
  455. if value.realm == nil && self.wrappedValue.realm != nil {
  456. SwiftUIKVO.observedObjects[value]?.cancel()
  457. }
  458. safeWrite(self.wrappedValue) { results in
  459. results.realm?.add(value)
  460. }
  461. }
  462. }
  463. @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
  464. extension Binding: BoundCollection where Value: RealmCollection {
  465. }
  466. @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
  467. extension Binding where Value: ObjectKeyIdentifiable & ThreadConfined {
  468. /// :nodoc:
  469. public func delete() {
  470. safeWrite(wrappedValue) { object in
  471. object.realm?.delete(self.wrappedValue)
  472. }
  473. }
  474. }
  475. @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
  476. extension ObservedRealmObject.Wrapper where ObjectType: ObjectBase {
  477. /// :nodoc:
  478. public func delete() {
  479. safeWrite(wrappedValue) { object in
  480. object.realm?.delete(self.wrappedValue)
  481. }
  482. }
  483. }
  484. @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
  485. extension ThreadConfined where Self: ObjectBase {
  486. /**
  487. Create a `Binding` for a given property, allowing for
  488. automatically transacted reads and writes behind the scenes.
  489. This is a convenience method for SwiftUI views (e.g., TextField, DatePicker)
  490. that require a `Binding` to be passed in. SwiftUI will automatically read/write
  491. from the binding.
  492. - parameter keyPath The key path to the member property.
  493. - returns A `Binding` to the member property.
  494. */
  495. public func bind<V: _ManagedPropertyType>(_ keyPath: ReferenceWritableKeyPath<Self, V>) -> Binding<V> {
  496. createBinding(self.realm != nil ? self.thaw() ?? self : self, forKeyPath: keyPath)
  497. }
  498. }
  499. private struct RealmEnvironmentKey: EnvironmentKey {
  500. static let defaultValue = Realm.Configuration.defaultConfiguration
  501. }
  502. @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
  503. extension EnvironmentValues {
  504. /// The current `Realm.Configuration` that the view should use.
  505. public var realmConfiguration: Realm.Configuration {
  506. get {
  507. return self[RealmEnvironmentKey]
  508. }
  509. set {
  510. self[RealmEnvironmentKey] = newValue
  511. }
  512. }
  513. /// The current `Realm` that the view should use.
  514. public var realm: Realm {
  515. get {
  516. return try! Realm(configuration: self[RealmEnvironmentKey])
  517. }
  518. set {
  519. self[RealmEnvironmentKey] = newValue.configuration
  520. }
  521. }
  522. }
  523. @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
  524. extension SwiftUIKVO {
  525. static func removeObservers(object: NSObject) {
  526. if let subscription = SwiftUIKVO.observedObjects[object] {
  527. subscription.cancel()
  528. }
  529. }
  530. }
  531. #else
  532. internal final class SwiftUIKVO {
  533. static func removeObservers(object: NSObject) {
  534. // noop
  535. }
  536. }
  537. #endif