////////////////////////////////////////////////////////////////////////////
//
// Copyright 2021 Realm Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
////////////////////////////////////////////////////////////////////////////
import Foundation
import SwiftUI
import Combine
import Realm
import Realm.Private
private func write<Value>(_ value: Value, _ block: (Value) -> Void) where Value: ThreadConfined {
let thawed = value.realm == nil ? value : value.thaw() ?? value
if let realm = thawed.realm, !realm.isInWriteTransaction {
try! realm.write {
block(thawed)
}
} else {
block(thawed)
}
}
private func thawObjectIfFrozen<Value>(_ value: Value) -> Value where Value: ObjectBase & ThreadConfined {
return value.realm == nil ? value : value.thaw() ?? value
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
private func createBinding<T: ThreadConfined, V>(
_ value: T,
forKeyPath keyPath: ReferenceWritableKeyPath<T, V>) -> Binding<V> {
guard let value = value.isFrozen ? value.thaw() : value else {
throwRealmException("Could not bind value")
}
// store last known value outside of the binding so that we can reference it if the parent
// is invalidated
var lastValue = value[keyPath: keyPath]
return Binding(get: {
guard !value.isInvalidated else { return lastValue }
lastValue = value[keyPath: keyPath]
return lastValue
}, set: { newValue in
guard !value.isInvalidated else { return }
write(value) { value in
value[keyPath: keyPath] = newValue
}
})
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
private func createCollectionBinding<T: ThreadConfined, V: RLMSwiftCollectionBase & ThreadConfined>(
_ value: T,
forKeyPath keyPath: ReferenceWritableKeyPath<T, V>) -> Binding<V> {
guard let value = value.isFrozen ? value.thaw() : value else {
throwRealmException("Could not bind value")
}
var lastValue = value[keyPath: keyPath]
return Binding(get: {
guard !value.isInvalidated else { return lastValue }
lastValue = value[keyPath: keyPath]
if lastValue.realm != nil {
lastValue = lastValue.freeze()
}
return lastValue
}, set: { newValue in
guard !value.isInvalidated else { return }
write(value) { value in
value[keyPath: keyPath] = newValue
}
})
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
private func createEquatableBinding<T: ThreadConfined, V: Equatable>(
_ value: T,
forKeyPath keyPath: ReferenceWritableKeyPath<T, V>) -> Binding<V> {
guard let value = value.isFrozen ? value.thaw() : value else {
throwRealmException("Could not bind value")
}
var lastValue = value[keyPath: keyPath]
return Binding(get: {
guard !value.isInvalidated else { return lastValue }
lastValue = value[keyPath: keyPath]
return lastValue
}, set: { newValue in
guard !value.isInvalidated else { return }
guard value[keyPath: keyPath] != newValue else { return }
write(value) { value in
value[keyPath: keyPath] = newValue
}
})
}
// MARK: SwiftUIKVO
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
@objc(RLMSwiftUIKVO) internal final class SwiftUIKVO: NSObject {
/// Objects must have observers removed before being added to a realm.
/// They are stored here so that if they are appended through the Bound Property
/// system, they can be de-observed before hand.
@Unchecked
fileprivate static var observedObjects = [NSObject: SwiftUIKVO.Subscription]()
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
struct Subscription: Combine.Subscription {
let observer: NSObject
let value: NSObject
let keyPaths: [String]
var combineIdentifier: CombineIdentifier {
CombineIdentifier(value)
}
func request(_ demand: Subscribers.Demand) {
}
func cancel() {
removeObservers()
SwiftUIKVO.observedObjects.removeValue(forKey: value)
}
fileprivate func removeObservers() {
guard SwiftUIKVO.observedObjects.keys.contains(value) else {
return
}
keyPaths.forEach {
value.removeObserver(observer, forKeyPath: $0)
}
}
fileprivate func addObservers() {
guard SwiftUIKVO.observedObjects.keys.contains(value) else {
return
}
keyPaths.forEach {
value.addObserver(observer, forKeyPath: $0, options: .init(), context: nil)
}
}
}
private let receive: () -> Void
override func observeValue(forKeyPath keyPath: String?,
of object: Any?,
change: [NSKeyValueChangeKey: Any]?,
context: UnsafeMutableRawPointer?) {
receive()
}
init<S>(subscriber: S) where S: Subscriber, S.Input == Void {
receive = { _ = subscriber.receive() }
super.init()
}
}
// MARK: - ObservableStorage
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
private final class ObservableStoragePublisher<ObjectType>: Publisher where ObjectType: ThreadConfined & RealmSubscribable {
public typealias Output = Void
public typealias Failure = Never
var subscribers = [AnySubscriber<Void, Never>]()
private var value: ObjectType
private let keyPaths: [String]?
private let unwrappedValue: ObjectBase?
init(_ value: ObjectType, _ keyPaths: [String]? = nil) {
self.value = value
self.keyPaths = keyPaths
self.unwrappedValue = nil
}
init(_ value: ObjectType, _ keyPaths: [String]? = nil) where ObjectType: ObjectBase {
self.value = value
self.keyPaths = keyPaths
self.unwrappedValue = value
}
init(_ value: ObjectType, _ keyPaths: [String]? = nil) where ObjectType: ProjectionObservable {
self.value = value
self.keyPaths = keyPaths
self.unwrappedValue = value.rootObject
}
// Refresh the publisher with a managed object.
func update(value: ObjectType) {
self.value = value
}
func send() {
subscribers.forEach {
_ = $0.receive()
}
}
public func receive<S>(subscriber: S) where S: Subscriber, Failure == S.Failure, Output == S.Input {
subscribers.append(AnySubscriber(subscriber))
if value.realm != nil && !value.isInvalidated, let value = value.thaw() {
// This path is for cases where the object is already managed. If an
// unmanaged object becomes managed it will continue to use KVO.
let token = value._observe(keyPaths, subscriber)
subscriber.receive(subscription: ObservationSubscription(token: token))
} else if let value = unwrappedValue, !value.isInvalidated {
// else if the value is unmanaged
let schema = ObjectSchema(RLMObjectBaseObjectSchema(value)!)
let kvo = SwiftUIKVO(subscriber: subscriber)
var keyPaths = [String]()
for property in schema.properties {
keyPaths.append(property.name)
value.addObserver(kvo, forKeyPath: property.name, options: .init(), context: nil)
}
let subscription = SwiftUIKVO.Subscription(observer: kvo, value: value, keyPaths: keyPaths)
subscriber.receive(subscription: subscription)
SwiftUIKVO.observedObjects[value] = subscription
}
}
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
private class ObservableStorage<ObservedType>: ObservableObject where ObservedType: RealmSubscribable & ThreadConfined & Equatable {
@Published var value: ObservedType {
willSet {
if newValue != value {
objectWillChange.send()
objectWillChange.update(value: newValue)
objectWillChange.subscribers.forEach {
$0.receive(subscription: ObservationSubscription(token: newValue._observe(keyPaths, $0)))
}
}
}
}
let objectWillChange: ObservableStoragePublisher<ObservedType>
let keyPaths: [String]?
init(_ value: ObservedType, _ keyPaths: [String]? = nil) {
self.value = value.realm != nil && !value.isInvalidated ? value.thaw() ?? value : value
self.objectWillChange = ObservableStoragePublisher(value, keyPaths)
self.keyPaths = keyPaths
}
init(_ value: ObservedType, _ keyPaths: [String]? = nil) where ObservedType: ObjectBase {
self.value = value.realm != nil && !value.isInvalidated ? value.thaw() ?? value : value
self.objectWillChange = ObservableStoragePublisher(value, keyPaths)
self.keyPaths = keyPaths
}
init(_ value: ObservedType, _ keyPaths: [String]? = nil) where ObservedType: ProjectionObservable {
self.value = value.realm != nil && !value.isInvalidated ? value.thaw() ?? value : value
self.objectWillChange = ObservableStoragePublisher(value, keyPaths)
self.keyPaths = keyPaths
}
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
private class ObservableResultsStorage<T>: ObservableStorage<T> where T: RealmSubscribable & ThreadConfined & Equatable {
private var setupHasRun = false
func didSet() {
if setupHasRun {
updateValue()
}
}
func updateValue() {
// Implemented in subclasses
fatalError()
}
func setupValue() {
guard !setupHasRun else { return }
updateValue()
setupHasRun = true
}
var sortDescriptor: SortDescriptor? {
didSet {
didSet()
}
}
var filter: NSPredicate? {
didSet {
didSet()
}
}
var configuration: Realm.Configuration? {
didSet {
didSet()
}
}
var searchFilter: NSPredicate? {
didSet {
didSet()
}
}
private var searchString: String = ""
fileprivate func searchText<U: ObjectBase>(_ text: String, on keyPath: KeyPath<U, String>) {
guard text != searchString else { return }
if text.isEmpty {
searchFilter = nil
} else {
searchFilter = Query<U>()[dynamicMember: keyPath].contains(text).predicate
}
searchString = text
}
}
// MARK: - StateRealmObject
/// A property wrapper type that instantiates an observable object.
///
/// Create a state realm object in a ``SwiftUI/View``, ``SwiftUI/App``, or
/// ``SwiftUI/Scene`` by applying the `@StateRealmObject` attribute to a property
/// declaration and providing an initial value that conforms to the
/// <doc://com.apple.documentation/documentation/Combine/ObservableObject>
/// protocol:
///
/// @StateRealmObject var model = DataModel()
///
/// SwiftUI creates a new instance of the object only once for each instance of
/// the structure that declares the object. When published properties of the
/// observable realm object change, SwiftUI updates the parts of any view that depend
/// on those properties. If unmanaged, the property will be read from the object itself,
/// otherwise, it will be read from the underlying Realm. Changes to the value will update
/// the view asynchronously:
///
/// Text(model.title) // Updates the view any time `title` changes.
///
/// You can pass the state object into a property that has the
/// ``SwiftUI/ObservedRealmObject`` attribute.
///
/// Get a ``SwiftUI/Binding`` to one of the state object's properties using the
/// `$` operator. Use a binding when you want to create a two-way connection to
/// one of the object's properties. For example, you can let a
/// ``SwiftUI/Toggle`` control a Boolean value called `isEnabled` stored in the
/// model:
///
/// Toggle("Enabled", isOn: $model.isEnabled)
///
/// This will write the modified `isEnabled` property to the `model` object's Realm.
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
@propertyWrapper public struct StateRealmObject<T: RealmSubscribable & ThreadConfined & Equatable>: DynamicProperty {
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
@StateObject private var storage: ObservableStorage<T>
private let defaultValue: T
/// :nodoc:
public var wrappedValue: T {
get {
let value = storage.value
if value.realm == nil {
// if unmanaged return the unmanaged value
return value
} else if value.isInvalidated {
// if invalidated, return the default value
return defaultValue
}
// else return the frozen value. the frozen value
// will be consumed by SwiftUI, which requires
// the ability to cache and diff objects and collections
// during some timeframe. The ObjectType is frozen so that
// SwiftUI can cache state. other access points will thaw
// the ObjectType
return value.freeze()
}
nonmutating set {
storage.value = newValue
}
}
/// :nodoc:
public var projectedValue: Binding<T> {
Binding(get: {
let value = self.storage.value
if value.isInvalidated {
return self.defaultValue
}
return value
}, set: { newValue in
self.storage.value = newValue
})
}
/**
Initialize a RealmState struct for a given thread confined type.
- parameter wrappedValue The List reference to wrap and observe.
*/
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
public init<Value>(wrappedValue: T) where T == List<Value> {
self._storage = StateObject(wrappedValue: ObservableStorage(wrappedValue))
defaultValue = T()
}
/**
Initialize a RealmState struct for a given thread confined type.
- parameter wrappedValue The MutableSet reference to wrap and observe.
*/
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
public init<Value>(wrappedValue: T) where T == MutableSet<Value> {
self._storage = StateObject(wrappedValue: ObservableStorage(wrappedValue))
defaultValue = T()
}
/**
Initialize a RealmState struct for a given thread confined type.
- parameter wrappedValue The Map reference to wrap and observe.
*/
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
public init<Key, Value>(wrappedValue: T) where T == Map<Key, Value> {
self._storage = StateObject(wrappedValue: ObservableStorage(wrappedValue))
defaultValue = T()
}
/**
Initialize a RealmState struct for a given thread confined type.
- parameter wrappedValue The ObjectBase reference to wrap and observe.
*/
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
public init(wrappedValue: T) where T: ObjectBase & Identifiable {
self._storage = StateObject(wrappedValue: ObservableStorage(wrappedValue))
defaultValue = T()
}
/**
Initialize a RealmState struct for a given Projection type.
- parameter wrappedValue The Projection reference to wrap and observe.
*/
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
public init(wrappedValue: T) where T: ProjectionObservable {
self._storage = StateObject(wrappedValue: ObservableStorage(wrappedValue))
defaultValue = T(projecting: T.Root())
}
/// :nodoc:
public var _publisher: some Publisher {
self.storage.objectWillChange
}
}
// MARK: ObservedResults
/**
A type which can be used with @ObservedResults propperty wrapper. Children class of Realm Object or Projection.
It's made to specialize the init methods of ObservedResults.
*/
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
public protocol _ObservedResultsValue: RealmCollectionValue { }
/// :nodoc:
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
extension Object: _ObservedResultsValue { }
/// :nodoc:
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
extension Projection: _ObservedResultsValue { }
/// A property wrapper type that represents the results of a query on a realm.
///
/// The results use the realm configuration provided by
/// the environment value `EnvironmentValues/realmConfiguration`.
///
/// Unlike non-SwiftUI results collections, the ObservedResults is mutable. Writes to an ObservedResults collection implicitly
/// perform a write transaction. If you add an object to the ObservedResults that the associated query would filter out, the object
/// is added to the realm but not included in the ObservedResults.
///
/// Given `@ObservedResults var v` in SwiftUI, `$v` refers to a `BoundCollection`.
///
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
@propertyWrapper public struct ObservedResults<ResultType>: DynamicProperty, BoundCollection where ResultType: _ObservedResultsValue & RealmFetchable & KeypathSortable & Identifiable {
public typealias Element = ResultType
private class Storage: ObservableResultsStorage<Results<ResultType>> {
override func updateValue() {
let realm = try! Realm(configuration: configuration ?? Realm.Configuration.defaultConfiguration)
var value = realm.objects(ResultType.self)
if let sortDescriptor = sortDescriptor {
value = value.sorted(byKeyPath: sortDescriptor.keyPath, ascending: sortDescriptor.ascending)
}
let filters = [searchFilter, filter].compactMap { $0 }
if !filters.isEmpty {
let compoundFilter = NSCompoundPredicate(andPredicateWithSubpredicates: filters)
value = value.filter(compoundFilter)
}
self.value = value
}
}
@Environment(\.realmConfiguration) var configuration
@ObservedObject private var storage: Storage
fileprivate func searchText<T: ObjectBase>(_ text: String, on keyPath: KeyPath<T, String>) {
storage.searchText(text, on: keyPath)
}
/// Stores an NSPredicate used for filtering the Results. This is mutually exclusive
/// to the `where` parameter.
@State public var filter: NSPredicate? {
willSet {
storage.filter = newValue
}
}
/// Stores a type safe query used for filtering the Results. This is mutually exclusive
/// to the `filter` parameter.
@State public var `where`: ((Query<ResultType>) -> Query<Bool>)? {
willSet {
storage.filter = newValue?(Query()).predicate
}
}
/// :nodoc:
@State public var sortDescriptor: SortDescriptor? {
willSet {
storage.sortDescriptor = newValue
}
}
/// :nodoc:
public var wrappedValue: Results<ResultType> {
storage.setupValue()
return storage.configuration != nil ? storage.value.freeze() : storage.value
}
/// :nodoc:
public var projectedValue: Self {
return self
}
/**
Initialize a `ObservedResults` struct for a given `Projection` type.
- parameter type: Observed type
- parameter configuration: The `Realm.Configuration` used when creating the Realm,
user's sync configuration for the given partition value will be set as the `syncConfiguration`,
if empty the configuration is set to the `defaultConfiguration`
- parameter filter: Observations will be made only for passing objects.
If no filter given - all objects will be observed
- parameter keyPaths: Only properties contained in the key paths array will be observed.
If `nil`, notifications will be delivered for any property change on the object.
String key paths which do not correspond to a valid a property will throw an exception.
- parameter sortDescriptor: A sequence of `SortDescriptor`s to sort by
*/
public init<ObjectType: ObjectBase>(_ type: ResultType.Type,
configuration: Realm.Configuration? = nil,
filter: NSPredicate? = nil,
keyPaths: [String]? = nil,
sortDescriptor: SortDescriptor? = nil) where ResultType: Projection<ObjectType>, ObjectType: ThreadConfined {
let results = Results<ResultType>(RLMResults<ResultType>.emptyDetached())
self.storage = Storage(results, keyPaths)
self.storage.configuration = configuration
self.filter = filter
self.sortDescriptor = sortDescriptor
}
/**
Initialize a `ObservedResults` struct for a given `Object` or `EmbeddedObject` type.
- parameter type: Observed type
- parameter configuration: The `Realm.Configuration` used when creating the Realm,
user's sync configuration for the given partition value will be set as the `syncConfiguration`,
if empty the configuration is set to the `defaultConfiguration`
- parameter filter: Observations will be made only for passing objects.
If no filter given - all objects will be observed
- parameter keyPaths: Only properties contained in the key paths array will be observed.
If `nil`, notifications will be delivered for any property change on the object.
String key paths which do not correspond to a valid a property will throw an exception.
- parameter sortDescriptor: A sequence of `SortDescriptor`s to sort by
*/
public init(_ type: ResultType.Type,
configuration: Realm.Configuration? = nil,
filter: NSPredicate? = nil,
keyPaths: [String]? = nil,
sortDescriptor: SortDescriptor? = nil) where ResultType: Object {
self.storage = Storage(Results(RLMResults<ResultType>.emptyDetached()), keyPaths)
self.storage.configuration = configuration
self.filter = filter
self.sortDescriptor = sortDescriptor
}
/**
Initialize a `ObservedResults` struct for a given `Object` or `EmbeddedObject` type.
- parameter type: Observed type
- parameter configuration: The `Realm.Configuration` used when creating the Realm,
user's sync configuration for the given partition value will be set as the `syncConfiguration`,
if empty the configuration is set to the `defaultConfiguration`
- parameter where: Observations will be made only for passing objects.
If no type safe query is given - all objects will be observed
- parameter keyPaths: Only properties contained in the key paths array will be observed.
If `nil`, notifications will be delivered for any property change on the object.
String key paths which do not correspond to a valid a property will throw an exception.
- parameter sortDescriptor: A sequence of `SortDescriptor`s to sort by
*/
public init(_ type: ResultType.Type,
configuration: Realm.Configuration? = nil,
where: ((Query<ResultType>) -> Query<Bool>)? = nil,
keyPaths: [String]? = nil,
sortDescriptor: SortDescriptor? = nil) where ResultType: Object {
self.storage = Storage(Results(RLMResults<ResultType>.emptyDetached()), keyPaths)
self.storage.configuration = configuration
self.where = `where`
self.sortDescriptor = sortDescriptor
}
/// :nodoc:
public init(_ type: ResultType.Type,
keyPaths: [String]? = nil,
configuration: Realm.Configuration? = nil,
sortDescriptor: SortDescriptor? = nil) where ResultType: Object {
self.storage = Storage(Results(RLMResults<ResultType>.emptyDetached()), keyPaths)
self.storage.configuration = configuration
self.sortDescriptor = sortDescriptor
}
nonisolated public func update() {
assumeOnMainActorExecutor {
// When the view updates, it will inject the @Environment
// into the propertyWrapper
if storage.configuration == nil {
storage.configuration = configuration
}
}
}
}
/// A property wrapper type that represents a sectioned results collection.
///
/// The sectioned results use the realm configuration provided by
/// the environment value `EnvironmentValues/realmConfiguration`
/// if `configuration` is not set in the initializer.
///
///
/// Given `@ObservedSectionedResults var v` in SwiftUI, `$v` refers to a `BoundCollection`.
///
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
@propertyWrapper public struct ObservedSectionedResults<Key: _Persistable & Hashable, ResultType>: DynamicProperty, BoundCollection where ResultType: _ObservedResultsValue & RealmFetchable & KeypathSortable & Identifiable {
public typealias Element = ResultType
private class Storage: ObservableResultsStorage<SectionedResults<Key, ResultType>> {
override func updateValue() {
let realm = try! Realm(configuration: configuration ?? Realm.Configuration.defaultConfiguration)
var results = realm.objects(ResultType.self)
let filters = [searchFilter, filter].compactMap { $0 }
if !filters.isEmpty {
let compoundFilter = NSCompoundPredicate(andPredicateWithSubpredicates: filters)
results = results.filter(compoundFilter)
}
if let keyPathString = keyPathString, sortDescriptors.isEmpty {
sortDescriptors.append(.init(keyPath: keyPathString, ascending: true))
}
value = results.sectioned(sortDescriptors: sortDescriptors, sectionBlock)
}
var sortDescriptors: [SortDescriptor] = [] {
didSet {
didSet()
}
}
var sectionBlock: ((ResultType) -> Key)
var keyPathString: String?
init(_ value: Results<ResultType>,
sectionBlock: @escaping ((ResultType) -> Key),
sortDescriptors: [SortDescriptor],
keyPathString: String? = nil,
keyPaths: [String]? = nil) {
self.sectionBlock = sectionBlock
self.sortDescriptors = sortDescriptors
if let keyPathString = keyPathString {
self.keyPathString = keyPathString
self.sortDescriptors.append(.init(keyPath: keyPathString, ascending: true))
}
if self.sortDescriptors.isEmpty {
throwRealmException("sortDescriptors must not be empty when sectioning ObservedSectionedResults with `sectionBlock`")
}
super.init(value.sectioned(sortDescriptors: self.sortDescriptors, self.sectionBlock), keyPaths)
}
}
@Environment(\.realmConfiguration) var configuration
@ObservedObject private var storage: Storage
/// :nodoc:
fileprivate func searchText<T: ObjectBase>(_ text: String, on keyPath: KeyPath<T, String>) {
storage.searchText(text, on: keyPath)
}
/// Stores an NSPredicate used for filtering the SectionedResults. This is mutually exclusive
/// to the `where` parameter.
@State public var filter: NSPredicate? {
willSet {
storage.filter = newValue
}
}
/// Stores a type safe query used for filtering the SectionedResults. This is mutually exclusive
/// to the `filter` parameter.
@State public var `where`: ((Query<ResultType>) -> Query<Bool>)? {
willSet {
storage.filter = newValue?(Query()).predicate
}
}
/// :nodoc:
@State public var sortDescriptors: [SortDescriptor] = [] {
willSet {
storage.sortDescriptors = newValue
}
}
/// :nodoc:
public var wrappedValue: SectionedResults<Key, ResultType> {
storage.setupValue()
return storage.value
}
/// :nodoc:
public var projectedValue: Self {
return self
}
private init(type: ResultType.Type,
sectionBlock: @escaping ((ResultType) -> Key),
sortDescriptors: [SortDescriptor] = [],
filter: NSPredicate? = nil,
where: ((Query<ResultType>) -> Query<Bool>)? = nil,
keyPaths: [String]? = nil,
keyPathString: String? = nil,
configuration: Realm.Configuration? = nil) where ResultType: AnyObject {
let results = Results<ResultType>(RLMResults<ResultType>.emptyDetached())
self.storage = Storage(results,
sectionBlock: sectionBlock,
sortDescriptors: sortDescriptors,
keyPathString: keyPathString,
keyPaths: keyPaths)
self.storage.configuration = configuration
if let filter = filter {
self.filter = filter
} else if let `where` = `where` {
self.where = `where`
}
self.sortDescriptors = sortDescriptors
}
/**
Initialize a `ObservedSectionedResults` struct for a given `Projection` type.
- parameter type: Observed type
- parameter sectionKeyPath: The keyPath that will produce the key for each section.
For every unique value retrieved from the keyPath a section key will be generated.
- parameter sortDescriptors: A sequence of `SortDescriptor`s to sort by.
- parameter filter: Observations will be made only for passing objects.
If no filter given - all objects will be observed
- parameter keyPaths: Only properties contained in the key paths array will be observed.
If `nil`, notifications will be delivered for any property change on the object.
String key paths which do not correspond to a valid a property will throw an exception.
- parameter configuration: The `Realm.Configuration` used when creating the Realm.
If empty the configuration is set to the `defaultConfiguration`
- note: The primary sort descriptor must be responsible for determining the section key.
*/
public init<ObjectType: ObjectBase>(_ type: ResultType.Type,
sectionKeyPath: KeyPath<ResultType, Key>,
sortDescriptors: [SortDescriptor] = [],
filter: NSPredicate? = nil,
keyPaths: [String]? = nil,
configuration: Realm.Configuration? = nil) where ResultType: Projection<ObjectType>, ObjectType: ThreadConfined {
self.init(type: type,
sectionBlock: { (obj: ResultType) in obj[keyPath: sectionKeyPath] },
sortDescriptors: sortDescriptors,
filter: filter,
keyPaths: keyPaths,
keyPathString: _name(for: sectionKeyPath),
configuration: configuration)
}
/**
Initialize a `ObservedSectionedResults` struct for a given `Projection` type.
- parameter type: Observed type
- parameter sectionBlock: A callback which returns the section key for each object in the collection.
- parameter sortDescriptors: A sequence of `SortDescriptor`s to sort by.
- parameter filter: Observations will be made only for passing objects.
If no filter given - all objects will be observed
- parameter keyPaths: Only properties contained in the key paths array will be observed.
If `nil`, notifications will be delivered for any property change on the object.
String key paths which do not correspond to a valid a property will throw an exception.
- parameter configuration: The `Realm.Configuration` used when creating the Realm.
If empty the configuration is set to the `defaultConfiguration`
- note: The primary sort descriptor must be responsible for determining the section key.
*/
public init<ObjectType: ObjectBase>(_ type: ResultType.Type,
sectionBlock: @escaping ((ResultType) -> Key),
sortDescriptors: [SortDescriptor] = [],
filter: NSPredicate? = nil,
keyPaths: [String]? = nil,
configuration: Realm.Configuration? = nil) where ResultType: Projection<ObjectType>, ObjectType: ThreadConfined {
self.init(type: type,
sectionBlock: sectionBlock,
sortDescriptors: sortDescriptors,
filter: filter,
keyPaths: keyPaths,
configuration: configuration)
}
/**
Initialize a `ObservedSectionedResults` struct for a given `Object` or `EmbeddedObject` type.
- parameter type: Observed type
- parameter sectionKeyPath: The keyPath that will produce the key for each section.
For every unique value retrieved from the keyPath a section key will be generated.
- parameter sortDescriptors: A sequence of `SortDescriptor`s to sort by.
- parameter filter: Observations will be made only for passing objects.
If no filter given - all objects will be observed
- parameter keyPaths: Only properties contained in the key paths array will be observed.
If `nil`, notifications will be delivered for any property change on the object.
String key paths which do not correspond to a valid a property will throw an exception.
- parameter configuration: The `Realm.Configuration` used when creating the Realm.
If empty the configuration is set to the `defaultConfiguration`
- note: The primary sort descriptor must be responsible for determining the section key.
*/
public init(_ type: ResultType.Type,
sectionKeyPath: KeyPath<ResultType, Key>,
sortDescriptors: [SortDescriptor] = [],
filter: NSPredicate? = nil,
keyPaths: [String]? = nil,
configuration: Realm.Configuration? = nil) where ResultType: Object {
self.init(type: type,
sectionBlock: { (obj: ResultType) in obj[keyPath: sectionKeyPath] },
sortDescriptors: sortDescriptors,
filter: filter,
keyPaths: keyPaths,
keyPathString: _name(for: sectionKeyPath),
configuration: configuration)
}
/**
Initialize a `ObservedSectionedResults` struct for a given `Object` or `EmbeddedObject` type.
- parameter type: Observed type
- parameter sectionBlock: A callback which returns the section key for each object in the collection.
- parameter sortDescriptors: A sequence of `SortDescriptor`s to sort by.
- parameter filter: Observations will be made only for passing objects.
If no filter given - all objects will be observed
- parameter keyPaths: Only properties contained in the key paths array will be observed.
If `nil`, notifications will be delivered for any property change on the object.
String key paths which do not correspond to a valid a property will throw an exception.
- parameter configuration: The `Realm.Configuration` used when creating the Realm.
If empty the configuration is set to the `defaultConfiguration`
- note: The primary sort descriptor must be responsible for determining the section key.
*/
public init(_ type: ResultType.Type,
sectionBlock: @escaping ((ResultType) -> Key),
sortDescriptors: [SortDescriptor] = [],
filter: NSPredicate? = nil,
keyPaths: [String]? = nil,
configuration: Realm.Configuration? = nil) where ResultType: Object {
self.init(type: type,
sectionBlock: sectionBlock,
sortDescriptors: sortDescriptors,
filter: filter,
keyPaths: keyPaths,
configuration: configuration)
}
/**
Initialize a `ObservedSectionedResults` struct for a given `Object` or `EmbeddedObject` type.
- parameter type: Observed type
- parameter sectionBlock: A callback which returns the section key for each object in the collection.
- parameter sortDescriptors: A sequence of `SortDescriptor`s to sort by.
- parameter where: Observations will be made only for passing objects.
If no type safe query is given - all objects will be observed.
- parameter keyPaths: Only properties contained in the key paths array will be observed.
If `nil`, notifications will be delivered for any property change on the object.
String key paths which do not correspond to a valid a property will throw an exception.
- parameter configuration: The `Realm.Configuration` used when creating the Realm.
If empty the configuration is set to the `defaultConfiguration`
- note: The primary sort descriptor must be responsible for determining the section key.
*/
public init(_ type: ResultType.Type,
sectionBlock: @escaping ((ResultType) -> Key),
sortDescriptors: [SortDescriptor] = [],
where: ((Query<ResultType>) -> Query<Bool>)? = nil,
keyPaths: [String]? = nil,
configuration: Realm.Configuration? = nil) where ResultType: Object {
self.init(type: type,
sectionBlock: sectionBlock,
sortDescriptors: sortDescriptors,
where: `where`,
keyPaths: keyPaths,
configuration: configuration)
}
/**
Initialize a `ObservedSectionedResults` struct for a given `Object` or `EmbeddedObject` type.
- parameter type: Observed type
- parameter sectionKeyPath: The keyPath that will produce the key for each section.
For every unique value retrieved from the keyPath a section key will be generated.
- parameter sortDescriptors: A sequence of `SortDescriptor`s to sort by.
- parameter where: Observations will be made only for passing objects.
If no type safe query is given - all objects will be observed.
- parameter keyPaths: Only properties contained in the key paths array will be observed.
If `nil`, notifications will be delivered for any property change on the object.
String key paths which do not correspond to a valid a property will throw an exception.
- parameter configuration: The `Realm.Configuration` used when creating the Realm.
If empty the configuration is set to the `defaultConfiguration`
- note: The primary sort descriptor must be responsible for determining the section key.
*/
public init(_ type: ResultType.Type,
sectionKeyPath: KeyPath<ResultType, Key>,
sortDescriptors: [SortDescriptor] = [],
where: ((Query<ResultType>) -> Query<Bool>)? = nil,
keyPaths: [String]? = nil,
configuration: Realm.Configuration? = nil) where ResultType: Object {
self.init(type: type,
sectionBlock: { (obj: ResultType) in obj[keyPath: sectionKeyPath] },
sortDescriptors: sortDescriptors,
where: `where`,
keyPaths: keyPaths,
keyPathString: _name(for: sectionKeyPath),
configuration: configuration)
}
/**
Initialize a `ObservedSectionedResults` struct for a given `Object` or `EmbeddedObject` type.
- parameter type: Observed type
- parameter sectionKeyPath: The keyPath that will produce the key for each section.
For every unique value retrieved from the keyPath a section key will be generated.
- parameter sortDescriptors: A sequence of `SortDescriptor`s to sort by.
- parameter keyPaths: Only properties contained in the key paths array will be observed.
If `nil`, notifications will be delivered for any property change on the object.
String key paths which do not correspond to a valid a property will throw an exception.
- parameter configuration: The `Realm.Configuration` used when creating the Realm.
If empty the configuration is set to the `defaultConfiguration`
- note: The primary sort descriptor must be responsible for determining the section key.
*/
public init(_ type: ResultType.Type,
sectionKeyPath: KeyPath<ResultType, Key>,
sortDescriptors: [SortDescriptor] = [],
keyPaths: [String]? = nil,
configuration: Realm.Configuration? = nil) where ResultType: Object {
self.init(type: type,
sectionBlock: { (obj: ResultType) in obj[keyPath: sectionKeyPath] },
sortDescriptors: sortDescriptors,
keyPaths: keyPaths,
keyPathString: _name(for: sectionKeyPath),
configuration: configuration)
}
/**
Initialize a `ObservedSectionedResults` struct for a given `Object` or `EmbeddedObject` type.
- parameter type: Observed type
- parameter sectionBlock: A callback which returns the section key for each object in the collection.
- parameter sortDescriptors: A sequence of `SortDescriptor`s to sort by.
- parameter keyPaths: Only properties contained in the key paths array will be observed.
If `nil`, notifications will be delivered for any property change on the object.
String key paths which do not correspond to a valid a property will throw an exception.
- parameter configuration: The `Realm.Configuration` used when creating the Realm.
If empty the configuration is set to the `defaultConfiguration`
- note: The primary sort descriptor must be responsible for determining the section key.
*/
public init(_ type: ResultType.Type,
sectionBlock: @escaping ((ResultType) -> Key),
sortDescriptors: [SortDescriptor],
keyPaths: [String]? = nil,
configuration: Realm.Configuration? = nil) where ResultType: Object {
self.init(type: type,
sectionBlock: sectionBlock,
sortDescriptors: sortDescriptors,
keyPaths: keyPaths,
configuration: configuration)
}
nonisolated public func update() {
assumeOnMainActorExecutor {
// When the view updates, it will inject the @Environment
// into the propertyWrapper
if storage.configuration == nil {
storage.configuration = configuration
}
}
}
}
// MARK: ObservedRealmObject
/// A property wrapper type that subscribes to an observable Realm `Object` or `List` and
/// invalidates a view whenever the observable object changes.
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
@propertyWrapper public struct ObservedRealmObject<ObjectType>: DynamicProperty where ObjectType: RealmSubscribable & ThreadConfined & ObservableObject & Equatable {
/// A wrapper of the underlying observable object that can create bindings to
/// its properties using dynamic member lookup.
@dynamicMemberLookup @frozen public struct Wrapper {
/// :nodoc:
public var wrappedValue: ObjectType
/// Returns a binding to the resulting value of a given key path.
///
/// - Parameter keyPath : A key path to a specific resulting value.
/// - Returns: A new binding.
public subscript<Subject>(dynamicMember keyPath: ReferenceWritableKeyPath<ObjectType, Subject>) -> Binding<Subject> {
createBinding(wrappedValue, forKeyPath: keyPath)
}
/// Returns a binding to the resulting equatable value of a given key path.
///
/// This binding's set() will only perform a write if the new value is different from the existing value.
///
/// - Parameter keyPath : A key path to a specific resulting value.
/// - Returns: A new binding.
public subscript<Subject: Equatable>(dynamicMember keyPath: ReferenceWritableKeyPath<ObjectType, Subject>) -> Binding<Subject> {
createEquatableBinding(wrappedValue, forKeyPath: keyPath)
}
/// Returns a binding to the resulting collection value of a given key path.
///
/// - Parameter keyPath : A key path to a specific resulting value.
/// - Returns: A new binding.
public subscript<Subject: RLMSwiftCollectionBase & ThreadConfined>(dynamicMember keyPath: ReferenceWritableKeyPath<ObjectType, Subject>) -> Binding<Subject> {
createCollectionBinding(wrappedValue, forKeyPath: keyPath)
}
}
/// The object to observe.
@ObservedObject private var storage: ObservableStorage<ObjectType>
/// A default value to avoid invalidated access.
private let defaultValue: ObjectType
/// :nodoc:
public var wrappedValue: ObjectType {
get {
if storage.value.realm == nil {
// if unmanaged return the unmanaged value
return storage.value
} else if storage.value.isInvalidated {
// if invalidated, return the default value
return defaultValue
}
// else return the frozen value. the frozen value
// will be consumed by SwiftUI, which requires
// the ability to cache and diff objects and collections
// during some timeframe. The ObjectType is frozen so that
// SwiftUI can cache state. other access points will thaw
// the ObjectType
return storage.value.freeze()
}
set {
storage.value = newValue
}
}
/// :nodoc:
public var projectedValue: Wrapper {
return Wrapper(wrappedValue: storage.value.isInvalidated ? defaultValue : storage.value)
}
/**
Initialize a RealmState struct for a given thread confined type.
- parameter wrappedValue The RealmSubscribable value to wrap and observe.
*/
public init(wrappedValue: ObjectType) where ObjectType: ObjectBase & Identifiable {
_storage = ObservedObject(wrappedValue: ObservableStorage(wrappedValue))
defaultValue = ObjectType()
}
/**
Initialize a RealmState struct for a given thread confined type.
- parameter wrappedValue The RealmSubscribable value to wrap and observe.
*/
public init<V>(wrappedValue: ObjectType) where ObjectType == List<V> {
_storage = ObservedObject(wrappedValue: ObservableStorage(wrappedValue))
defaultValue = List()
}
/**
Initialize a RealmState struct for a given thread confined type.
- parameter wrappedValue The RealmSubscribable value to wrap and observe.
*/
public init(wrappedValue: ObjectType) where ObjectType: ProjectionObservable {
_storage = ObservedObject(wrappedValue: ObservableStorage(wrappedValue))
defaultValue = ObjectType(projecting: ObjectType.Root())
}
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
extension Binding where Value: ObjectBase & ThreadConfined {
/// :nodoc:
public subscript<V>(dynamicMember member: ReferenceWritableKeyPath<Value, V>) -> Binding<V> where V: _Persistable {
createBinding(wrappedValue, forKeyPath: member)
}
/// :nodoc:
public subscript<V>(dynamicMember member: ReferenceWritableKeyPath<Value, V>) -> Binding<V> where V: _Persistable & RLMSwiftCollectionBase & ThreadConfined {
createCollectionBinding(wrappedValue, forKeyPath: member)
}
/// :nodoc:
public subscript<V>(dynamicMember member: ReferenceWritableKeyPath<Value, V>) -> Binding<V> where V: _Persistable & Equatable {
createEquatableBinding(wrappedValue, forKeyPath: member)
}
}
// MARK: - BoundCollection
/// :nodoc:
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
@preconcurrency @MainActor
public protocol BoundCollection {
/// :nodoc:
associatedtype Value
/// :nodoc:
associatedtype Element: RealmCollectionValue
/// :nodoc:
var wrappedValue: Value { get }
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
extension BoundCollection {
private func write(_ block: (Value) -> Void) where Value: ThreadConfined {
RealmSwift.write(wrappedValue, block)
}
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
public extension BoundCollection where Value: RealmCollection {
/// :nodoc:
typealias Element = Value.Element
/// :nodoc:
typealias Index = Value.Index
/// :nodoc:
typealias Indices = Value.Indices
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
public extension BoundCollection where Value == List<Element> {
/// :nodoc:
func remove(at index: Index) {
write { list in
list.remove(at: index)
}
}
/// :nodoc:
func remove(atOffsets offsets: IndexSet) {
write { list in
list.remove(atOffsets: offsets)
}
}
/// :nodoc:
func move(fromOffsets offsets: IndexSet, toOffset destination: Int) {
write { list in
list.move(fromOffsets: offsets, toOffset: destination)
}
}
/// :nodoc:
func append(_ value: Value.Element) {
write { list in
list.append(value)
}
}
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
public extension BoundCollection where Value == List<Element>, Element: ObjectBase & ThreadConfined {
/// :nodoc:
func append(_ value: Value.Element) {
write { list in
if value.realm == nil && list.realm != nil {
SwiftUIKVO.observedObjects[value]?.cancel()
}
list.append(thawObjectIfFrozen(value))
}
}
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
public extension BoundCollection where Value == Results<Element>, Element: ObjectBase & ThreadConfined {
/// :nodoc:
func remove(_ object: Value.Element) {
guard let thawed = object.thaw() else { return }
write { results in
if results.index(of: thawed) != nil {
results.realm?.delete(thawed)
}
}
}
/// :nodoc:
func remove(atOffsets offsets: IndexSet) {
write { results in
results.realm?.delete(Array(offsets.map { results[$0] }))
}
}
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
public extension BoundCollection where Value == MutableSet<Element> {
/// :nodoc:
func remove(_ element: Value.Element) {
write { mutableSet in
mutableSet.remove(element)
}
}
/// :nodoc:
func insert(_ value: Value.Element) {
write { mutableSet in
mutableSet.insert(value)
}
}
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
public extension BoundCollection where Value == MutableSet<Element>, Element: ObjectBase & ThreadConfined {
/// :nodoc:
func remove(_ object: Value.Element) {
write { mutableSet in
mutableSet.remove(thawObjectIfFrozen(object))
}
}
/// :nodoc:
func insert(_ value: Value.Element) {
write { mutableSet in
if value.realm == nil && mutableSet.realm != nil {
SwiftUIKVO.observedObjects[value]?.cancel()
}
mutableSet.insert(thawObjectIfFrozen(value))
}
}
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
public extension BoundCollection where Value == Results<Element>, Element: Object {
/// :nodoc:
func append(_ value: Value.Element) {
write { results in
if value.realm == nil && results.realm != nil {
SwiftUIKVO.observedObjects[value]?.cancel()
}
results.realm?.add(thawObjectIfFrozen(value))
}
}
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
public extension BoundCollection where Value == Results<Element>, Element: ProjectionObservable & ThreadConfined, Element.Root: Object {
/// :nodoc:
func append(_ value: Value.Element) {
write { results in
if value.realm == nil && results.realm != nil {
SwiftUIKVO.observedObjects[value.rootObject]?.cancel()
}
results.realm?.add(thawObjectIfFrozen(value.rootObject))
}
}
}
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
extension Binding: BoundCollection where Value: RealmCollection {
/// :nodoc:
public typealias Element = Value.Element
/// :nodoc:
public typealias Index = Value.Index
/// :nodoc:
public typealias Indices = Value.Indices
}
// MARK: - BoundMap
/// :nodoc:
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
public protocol BoundMap {
/// :nodoc:
associatedtype Value: RealmKeyedCollection
/// :nodoc:
var wrappedValue: Value { get }
}
/// :nodoc:
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
public extension BoundMap {
// The compiler will not allow us to assign values by subscript as the binding is a get-only
// property. To get around this we need an explicit `set` method.
/// :nodoc:
subscript( key: Value.Key) -> Value.Value? {
self.wrappedValue[key]
}
/// :nodoc:
func set(object: Value.Value?, for key: Value.Key) {
write(self.wrappedValue) { map in
var m = map
m[key] = object
}
}
}
/// :nodoc:
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
public extension BoundMap where Value.Value: ObjectBase & ThreadConfined {
/// :nodoc:
func set(object: Value.Value?, for key: Value.Key) {
// If the value is `nil` remove it from the map.
guard let value = object else {
write(self.wrappedValue) { map in
map.removeObject(for: key)
}
return
}
// if the value is unmanaged but the map is managed, we are adding this value to the realm
if value.realm == nil && self.wrappedValue.realm != nil {
SwiftUIKVO.observedObjects[value]?.cancel()
}
write(self.wrappedValue) { map in
var m = map
m[key] = thawObjectIfFrozen(value)
}
}
}
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
extension Binding: BoundMap where Value: RealmKeyedCollection {
}
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
extension Binding where Value: Object {
/// :nodoc:
public func delete() {
write(wrappedValue) { object in
object.realm?.delete(thawObjectIfFrozen(self.wrappedValue))
}
}
}
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
extension Binding where Value: ProjectionObservable, Value.Root: ThreadConfined {
/// :nodoc:
public func delete() {
write(wrappedValue.rootObject) { object in
object.realm?.delete(thawObjectIfFrozen(object))
}
}
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
extension ThreadConfined where Self: ProjectionObservable {
/**
Create a `Binding` for a given property, allowing for
automatically transacted reads and writes behind the scenes.
This is a convenience method for SwiftUI views (e.g., TextField, DatePicker)
that require a `Binding` to be passed in. SwiftUI will automatically read/write
from the binding.
- parameter keyPath The key path to the member property.
- returns A `Binding` to the member property.
*/
public func bind<V: _Persistable & Equatable>(_ keyPath: ReferenceWritableKeyPath<Self, V>) -> Binding<V> {
createEquatableBinding(self, forKeyPath: keyPath)
}
/// :nodoc:
public func bind<V: _Persistable & RLMSwiftCollectionBase & ThreadConfined>(_ keyPath: ReferenceWritableKeyPath<Self, V>) -> Binding<V> {
createCollectionBinding(self, forKeyPath: keyPath)
}
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
extension ObservedRealmObject.Wrapper where ObjectType: ObjectBase {
/// :nodoc:
public func delete() {
write(wrappedValue) { object in
object.realm?.delete(self.wrappedValue)
}
}
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
extension ThreadConfined where Self: ObjectBase {
/**
Create a `Binding` for a given property, allowing for
automatically transacted reads and writes behind the scenes.
This is a convenience method for SwiftUI views (e.g., TextField, DatePicker)
that require a `Binding` to be passed in. SwiftUI will automatically read/write
from the binding.
- parameter keyPath The key path to the member property.
- returns A `Binding` to the member property.
*/
public func bind<V: _Persistable & Equatable>(_ keyPath: ReferenceWritableKeyPath<Self, V>) -> Binding<V> {
createEquatableBinding(self, forKeyPath: keyPath)
}
/// :nodoc:
public func bind<V: _Persistable & RLMSwiftCollectionBase & ThreadConfined>(_ keyPath: ReferenceWritableKeyPath<Self, V>) -> Binding<V> {
createCollectionBinding(self, forKeyPath: keyPath)
}
}
private struct RealmEnvironmentKey: EnvironmentKey {
static let defaultValue = Realm.Configuration.defaultConfiguration
}
private struct PartitionValueEnvironmentKey: EnvironmentKey {
static let defaultValue: PartitionValue? = nil
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
extension EnvironmentValues {
/// The current `Realm.Configuration` that the view should use.
public var realmConfiguration: Realm.Configuration {
get {
return self[RealmEnvironmentKey.self]
}
set {
self[RealmEnvironmentKey.self] = newValue
}
}
/// The current `Realm` that the view should use.
public var realm: Realm {
get {
return try! Realm(configuration: self[RealmEnvironmentKey.self])
}
set {
self[RealmEnvironmentKey.self] = newValue.configuration
}
}
/// The current `PartitionValue` that the view should use.
public var partitionValue: PartitionValue? {
get {
return self[PartitionValueEnvironmentKey.self]
}
set {
self[PartitionValueEnvironmentKey.self] = newValue
}
}
}
/**
An enum representing different states from `AsyncOpen` and `AutoOpen` process
*/
public enum AsyncOpenState {
/// Starting the Realm.asyncOpen process.
case connecting
/// Waiting for a user to be logged in before executing Realm.asyncOpen.
case waitingForUser
/// The Realm has been opened and is ready for use. For AsyncOpen this means that the Realm has been fully downloaded, but for AutoOpen the existing local file may have been used if the device is offline.
case open(Realm)
/// The Realm is currently being downloaded from the server.
case progress(Progress)
/// Opening the Realm failed.
case error(Error)
}
private enum AsyncOpenKind {
case asyncOpen
case autoOpen
}
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
private class ObservableAsyncOpenStorage: ObservableObject {
private var asyncOpenKind: AsyncOpenKind
private var app: App
var configuration: Realm.Configuration?
var partitionValue: AnyBSON?
// Tracks User State for App for Multi-User Support
enum AppState {
case loggedIn(User)
case loggedOut
}
private var appState: AppState = .loggedOut
// Cancellables
private var appCancellable = [AnyCancellable]()
private var asyncOpenCancellable = [AnyCancellable]()
@Published fileprivate var asyncOpenState: AsyncOpenState
init(asyncOpenKind: AsyncOpenKind, app: App, configuration: Realm.Configuration?, partitionValue: AnyBSON?) {
self.asyncOpenKind = asyncOpenKind
self.app = app
self.configuration = configuration
self.partitionValue = partitionValue
// Initialising the state value depending on the user status, before first rendering.
if let user = app.currentUser {
appState = .loggedIn(user)
asyncOpenState = .connecting
} else {
asyncOpenState = .waitingForUser
}
}
var setupHasRun = false
func setup() {
guard !setupHasRun else { return }
initAsyncOpen()
setupHasRun = true
}
private func initAsyncOpen() {
if case .loggedIn(let user) = appState {
// we only open the realm on initialisation if there is a user logged.
asyncOpenForUser(user)
}
// we observe the changes in the app state to check for user changes,
// we store an internal state, so we could react to those changes (user login, user change, logout).
app.objectWillChange.sink { [weak self] app in
guard let self = self else { return }
switch self.appState {
case .loggedIn(let user):
if let newUser = app.currentUser,
user != newUser {
self.appState = .loggedIn(newUser)
self.asyncOpenState = .connecting
self.asyncOpenForUser(user)
} else if app.currentUser == nil {
self.asyncOpenState = .waitingForUser
self.appState = .loggedOut
}
case .loggedOut:
if let user = app.currentUser {
self.appState = .loggedIn(user)
self.asyncOpenState = .connecting
self.asyncOpenForUser(user)
}
}
}.store(in: &appCancellable)
}
private func asyncOpenForUser(_ user: User) {
// Set the `syncConfiguration` depending if there is partition value (pbs) or not (flx).
var config: Realm.Configuration
if let partitionValue = partitionValue {
config = user.configuration(partitionValue: partitionValue, cancelAsyncOpenOnNonFatalErrors: true)
} else {
config = user.flexibleSyncConfiguration(cancelAsyncOpenOnNonFatalErrors: true)
}
// Use the user configuration by default or set configuration with the current user `syncConfiguration`'s.
if var configuration = configuration {
// We want to throw if the configuration doesn't contain a `SyncConfiguration`
guard configuration.syncConfiguration != nil else {
throwRealmException("The used configuration was not configured with sync.")
}
let userSyncConfig = config.syncConfiguration
configuration.syncConfiguration = userSyncConfig
config = configuration
}
// Cancel any current subscriptions to asyncOpen if there is one
cancelAsyncOpen()
Realm.asyncOpen(configuration: config)
.onProgressNotification { asyncProgress in
// Do not change state to progress if the realm file is already opened or there is an error
switch self.asyncOpenState {
case .connecting, .waitingForUser, .progress:
let progress = Progress(totalUnitCount: Int64(asyncProgress.transferredBytes))
progress.completedUnitCount = Int64(asyncProgress.transferredBytes)
self.asyncOpenState = .progress(progress)
default: break
}
}
.sink { completion in
if case .failure(let error) = completion {
switch self.asyncOpenKind {
case .asyncOpen:
self.asyncOpenState = .error(error)
case .autoOpen:
if let realm = try? Realm(configuration: config) {
self.asyncOpenState = .open(realm)
} else {
self.asyncOpenState = .error(error)
}
}
}
} receiveValue: { realm in
self.asyncOpenState = .open(realm)
}.store(in: &self.asyncOpenCancellable)
}
fileprivate func update(_ partitionValue: PartitionValue?, _ configuration: Realm.Configuration) {
if let partitionValue = partitionValue {
let bsonValue = AnyBSON(partitionValue: partitionValue)
if self.partitionValue != bsonValue {
self.partitionValue = bsonValue
}
}
// We don't want to use the `defaultConfiguration` from the environment, we only want to use this environment value in @AsyncOpen if is not the default one
if configuration != .defaultConfiguration, self.configuration != configuration {
if let partitionValue = configuration.syncConfiguration?.partitionValue {
self.partitionValue = partitionValue
}
self.configuration = configuration
}
}
private func cancelAsyncOpen() {
asyncOpenCancellable.forEach { $0.cancel() }
asyncOpenCancellable = []
}
func cancel() {
cancelAsyncOpen()
appCancellable.forEach { $0.cancel() }
appCancellable = []
}
// MARK: - AutoOpen & AsyncOpen Helper
class func configureApp(appId: String? = nil, timeout: UInt? = nil) -> App {
var app: App
if let appId = appId {
app = App(id: appId)
} else {
// Check if there is a singular cached app
let cachedApps = RLMApp.allApps()
if cachedApps.count > 1 {
throwRealmException("Cannot AsyncOpen the Realm because more than one appId was found. When using multiple Apps you must explicitly pass an appId to indicate which to use.")
}
guard let cachedApp = cachedApps.first else {
throwRealmException("Cannot AsyncOpen the Realm because no appId was found. You must either explicitly pass an appId or initialize an App before displaying your View.")
}
app = cachedApp
}
// Setup timeout if needed
if let timeout {
app.syncManager.timeoutOptions = SyncTimeoutOptions(connectTimeout: timeout)
}
return app
}
}
// MARK: - AsyncOpen
/// A property wrapper type that initiates a `Realm.asyncOpen()` for the current user which asynchronously open a Realm,
/// and notifies states for the given process
///
/// Add AsyncOpen to your ``SwiftUI/View`` or ``SwiftUI/App``, after a user is already logged in,
/// or if a user is going to be logged in
///
/// @AsyncOpen(appId: "app_id", partitionValue: <partition_value>) var asyncOpen
///
/// This will immediately initiates a `Realm.asyncOpen()` operation which will perform all work needed to get the Realm to
/// a usable state. (see Realm.asyncOpen() documentation)
///
/// This property wrapper will publish states of the current `Realm.asyncOpen()` process like progress, errors and an opened realm,
/// which can be used to update the view
///
/// struct AsyncOpenView: View {
/// @AsyncOpen(appId: "app_id", partitionValue: <partition_value>) var asyncOpen
///
/// var body: some View {
/// switch asyncOpen {
/// case .notOpen:
/// ProgressView()
/// case .open(let realm):
/// ListView()
/// .environment(\.realm, realm)
/// case .error(_):
/// ErrorView()
/// case .progress(let progress):
/// ProgressView(progress)
/// }
/// }
/// }
///
/// This opened `realm` can be later injected to the view as an environment value which will be used by our property wrappers
/// to populate the view with data from the opened realm
///
/// ListView()
/// .environment(\.realm, realm)
///
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
@propertyWrapper public struct AsyncOpen: DynamicProperty {
@Environment(\.realmConfiguration) var configuration
@Environment(\.partitionValue) var partitionValue
@ObservedObject private var storage: ObservableAsyncOpenStorage
/**
A Publisher for `AsyncOpenState`, emits a state each time the asyncOpen state changes.
*/
public var projectedValue: Published<AsyncOpenState>.Publisher {
storage.$asyncOpenState
}
/// :nodoc:
public var wrappedValue: AsyncOpenState {
storage.setup()
return storage.asyncOpenState
}
/**
This will cancel any notification from the property wrapper states
*/
public func cancel() {
storage.cancel()
}
/**
Initialize the property wrapper
- parameter appId: The unique identifier of your Realm app, if empty or `nil` will try to retrieve latest singular cached app.
- parameter partitionValue: The `BSON` value the Realm is partitioned on.
- parameter configuration: The `Realm.Configuration` used when creating the Realm,
user's sync configuration for the given partition value will be set as the `syncConfiguration`,
if empty the user configuration will be used.
- parameter timeout: The maximum number of milliseconds to allow for a connection to
become fully established., if empty or `nil` no connection timeout is set.
*/
public init<Partition>(appId: String? = nil,
partitionValue: Partition,
configuration: Realm.Configuration? = nil,
timeout: UInt? = nil) where Partition: BSON {
let app = ObservableAsyncOpenStorage.configureApp(appId: appId, timeout: timeout)
// Store property wrapper values on the storage
storage = ObservableAsyncOpenStorage(asyncOpenKind: .asyncOpen, app: app, configuration: configuration, partitionValue: AnyBSON(partitionValue))
}
/**
Initialize the property wrapper for a flexible sync configuration.
- parameter appId: The unique identifier of your Realm app, if empty or `nil` will try to retrieve latest singular cached app.
- parameter configuration: The `Realm.Configuration` used when creating the Realm,
user's sync configuration for the given partition value will be set as the `syncConfiguration`,
if empty the user configuration will be used.
- parameter timeout: The maximum number of milliseconds to allow for a connection to
become fully established., if empty or `nil` no connection timeout is set.
*/
public init(appId: String? = nil,
configuration: Realm.Configuration? = nil,
timeout: UInt? = nil) {
let app = ObservableAsyncOpenStorage.configureApp(appId: appId, timeout: timeout)
// Store property wrapper values on the storage
storage = ObservableAsyncOpenStorage(asyncOpenKind: .asyncOpen, app: app, configuration: configuration, partitionValue: nil)
}
nonisolated public func update() {
assumeOnMainActorExecutor {
storage.update(partitionValue, configuration)
}
}
}
// MARK: - AutoOpen
/// `AutoOpen` will try once to asynchronously open a Realm, but in case of no internet connection will return an opened realm
/// for the given appId and partitionValue which can be used within our view.
/// Add AutoOpen to your ``SwiftUI/View`` or ``SwiftUI/App``, after a user is already logged in
/// or if a user is going to be logged in
///
/// @AutoOpen(appId: "app_id", partitionValue: <partition_value>, timeout: 4000) var autoOpen
///
/// This will immediately initiates a `Realm.asyncOpen()` operation which will perform all work needed to get the Realm to
/// a usable state. (see Realm.asyncOpen() documentation)
///
/// This property wrapper will publish states of the current `Realm.asyncOpen()` process like progress, errors and an opened realm,
/// which can be used to update the view
///
/// struct AutoOpenView: View {
/// @AutoOpen(appId: "app_id", partitionValue: <partition_value>) var autoOpen
///
/// var body: some View {
/// switch autoOpen {
/// case .notOpen:
/// ProgressView()
/// case .open(let realm):
/// ListView()
/// .environment(\.realm, realm)
/// case .error(_):
/// ErrorView()
/// case .progress(let progress):
/// ProgressView(progress)
/// }
/// }
/// }
///
/// This opened `realm` can be later injected to the view as an environment value which will be used by our property wrappers
/// to populate the view with data from the opened realm
///
/// ListView()
/// .environment(\.realm, realm)
///
/// This property wrapper behaves similar as `AsyncOpen`, and in terms of declaration and use is completely identical,
/// but with the difference of a offline-first approach.
///
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
@propertyWrapper public struct AutoOpen: DynamicProperty {
@Environment(\.realmConfiguration) var configuration
@Environment(\.partitionValue) var partitionValue
@ObservedObject private var storage: ObservableAsyncOpenStorage
/**
A Publisher for `AsyncOpenState`, emits a state each time the asyncOpen state changes.
*/
public var projectedValue: Published<AsyncOpenState>.Publisher {
storage.$asyncOpenState
}
/// :nodoc:
public var wrappedValue: AsyncOpenState {
storage.setup()
return storage.asyncOpenState
}
/**
This will cancel any notification from the property wrapper states
*/
public func cancel() {
storage.cancel()
}
/**
Initialize the property wrapper
- parameter appId: The unique identifier of your Realm app, if empty or `nil` will try to retrieve latest singular cached app.
- parameter partitionValue: The `BSON` value the Realm is partitioned on.
- parameter configuration: The `Realm.Configuration` used when creating the Realm,
user's sync configuration for the given partition value will be set as the `syncConfiguration`,
if empty the user configuration will be used.
- parameter timeout: The maximum number of milliseconds to allow for a connection to
become fully established, if empty or `nil` no connection timeout is set.
*/
public init<Partition>(appId: String? = nil,
partitionValue: Partition,
configuration: Realm.Configuration? = nil,
timeout: UInt? = nil) where Partition: BSON {
let app = ObservableAsyncOpenStorage.configureApp(appId: appId, timeout: timeout)
// Store property wrapper values on the storage
storage = ObservableAsyncOpenStorage(asyncOpenKind: .autoOpen, app: app, configuration: configuration, partitionValue: AnyBSON(partitionValue))
}
/**
Initialize the property wrapper for a flexible sync configuration.
- parameter appId: The unique identifier of your Realm app, if empty or `nil` will try to retrieve latest singular cached app.
- parameter configuration: The `Realm.Configuration` used when creating the Realm,
user's sync configuration for the given partition value will be set as the `syncConfiguration`,
if empty the user configuration will be used.
- parameter timeout: The maximum number of milliseconds to allow for a connection to
become fully established., if empty or `nil` no connection timeout is set.
*/
public init(appId: String? = nil,
configuration: Realm.Configuration? = nil,
timeout: UInt? = nil) {
let app = ObservableAsyncOpenStorage.configureApp(appId: appId, timeout: timeout)
// Store property wrapper values on the storage
storage = ObservableAsyncOpenStorage(asyncOpenKind: .autoOpen, app: app, configuration: configuration, partitionValue: nil)
}
nonisolated public func update() {
assumeOnMainActorExecutor {
storage.update(partitionValue, configuration)
}
}
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
extension SwiftUIKVO {
@objc(removeObserversFromObject:) static func removeObservers(object: NSObject) -> Bool {
if let subscription = SwiftUIKVO.observedObjects[object] {
subscription.removeObservers()
return true
} else {
return false
}
}
@objc(addObserversToObject:) static func addObservers(object: NSObject) {
if let subscription = SwiftUIKVO.observedObjects[object] {
subscription.addObservers()
}
}
}
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
extension View {
/// Marks this view as searchable, which configures the display of a search field.
/// You can provide a collection and a key path to be filtered using the search
/// field string provided by the searchable component, this will result in the collection
/// querying for all items containing the search field string for the given key path.
///
/// @State var searchString: String
/// @ObservedResults(Reminder.self) var reminders
///
/// List {
/// ForEach(reminders) { reminder in
/// ReminderRowView(reminder: reminder)
/// }
/// }
/// .searchable(text: $searchFilter,
/// collection: $reminders,
/// keyPath: \.name) {
/// ForEach(reminders) { remindersFiltered in
/// Text(remindersFiltered.name).searchCompletion(remindersFiltered.name)
/// }
/// }
///
/**
- Note: See ``SwiftUI/View/searchable(text:placement:prompt)``
<https://developer.apple.com/documentation/swiftui/form/searchable(text:placement:prompt:)-6royb>
for more information on searchable view modifier.
- parameter text: The text to display and edit in the search field.
- parameter collection: The collection to be filtered.
- parameter keyPath: The key path to the property which will be used to filter
the collection, only key paths with `String` type are allowed.
- parameter placement: The preferred placement of the search field within the
containing view hierarchy.
- parameter prompt: A `Text` representing the prompt of the search field
which provides users with guidance on what to search for.
*/
public func searchable<T: ObjectBase>(text: Binding<String>, collection: ObservedResults<T>, keyPath: KeyPath<T, String>,
placement: SearchFieldPlacement = .automatic, prompt: Text? = nil) -> some View {
filterCollection(collection, for: text.wrappedValue, on: keyPath)
return searchable(text: text, placement: placement, prompt: prompt)
}
/// Marks this view as searchable, which configures the display of a search field.
/// You can provide a collection and a key path to be filtered using the search
/// field string provided by the searchable component, this will result in the collection
/// querying for all items containing the search field string for the given key path.
///
/// @State var searchString: String
/// @ObservedResults(Reminder.self) var reminders
///
/// List {
/// ForEach(reminders) { reminder in
/// ReminderRowView(reminder: reminder)
/// }
/// }
/// .searchable(text: $searchFilter,
/// collection: $reminders,
/// keyPath: \.name) {
/// ForEach(reminders) { remindersFiltered in
/// Text(remindersFiltered.name).searchCompletion(remindersFiltered.name)
/// }
/// }
///
/**
- Note: See ``SwiftUI/View/searchable(text:placement:prompt)``
<https://developer.apple.com/documentation/swiftui/form/searchable(text:placement:prompt:)-2ed8t>
for more information on searchable view modifier.
- parameter text: The text to display and edit in the search field.
- parameter collection: The collection to be filtered.
- parameter keyPath: The key path to the property which will be used to filter
the collection.
- parameter placement: The preferred placement of the search field within the
containing view hierarchy.
- parameter prompt: The key for the localized prompt of the search field
which provides users with guidance on what to search for.
*/
public func searchable<T: ObjectBase>(text: Binding<String>, collection: ObservedResults<T>,
keyPath: KeyPath<T, String>, placement: SearchFieldPlacement = .automatic,
prompt: LocalizedStringKey) -> some View {
filterCollection(collection, for: text.wrappedValue, on: keyPath)
return searchable(text: text, placement: placement, prompt: prompt)
}
/// Marks this view as searchable, which configures the display of a search field.
/// You can provide a collection and a key path to be filtered using the search
/// field string provided by the searchable component, this will result in the collection
/// querying for all items containing the search field string for the given key path.
///
/// @State var searchString: String
/// @ObservedResults(Reminder.self) var reminders
///
/// List {
/// ForEach(reminders) { reminder in
/// ReminderRowView(reminder: reminder)
/// }
/// }
/// .searchable(text: $searchFilter,
/// collection: $reminders,
/// keyPath: \.name) {
/// ForEach(reminders) { remindersFiltered in
/// Text(remindersFiltered.name).searchCompletion(remindersFiltered.name)
/// }
/// }
///
/**
- Note: See ``SwiftUI/View/searchable(text:placement:prompt)``
<https://developer.apple.com/documentation/swiftui/form/searchable(text:placement:prompt:)-58egp>
for more information on searchable view modifier.
- parameter text: The text to display and edit in the search field.
- parameter collection: The collection to be filtered.
- parameter keyPath: The key path to the property which will be used to filter
the collection.
- parameter placement: The preferred placement of the search field within the
containing view hierarchy.
- parameter prompt: A string representing the prompt of the search field
which provides users with guidance on what to search for.
*/
public func searchable<T: ObjectBase, S>(text: Binding<String>, collection: ObservedResults<T>, keyPath: KeyPath<T, String>,
placement: SearchFieldPlacement = .automatic, prompt: S) -> some View where S: StringProtocol {
filterCollection(collection, for: text.wrappedValue, on: keyPath)
return searchable(text: text, placement: placement, prompt: prompt)
}
/// Marks this view as searchable, which configures the display of a search field.
/// You can provide a collection and a key path to be filtered using the search
/// field string provided by the searchable component, this will result in the collection
/// querying for all items containing the search field string for the given key path.
///
/// @State var searchString: String
/// @ObservedResults(Reminder.self) var reminders
///
/// List {
/// ForEach(reminders) { reminder in
/// ReminderRowView(reminder: reminder)
/// }
/// }
/// .searchable(text: $searchFilter,
/// collection: $reminders,
/// keyPath: \.name) {
/// ForEach(reminders) { remindersFiltered in
/// Text(remindersFiltered.name).searchCompletion(remindersFiltered.name)
/// }
/// }
///
/**
- Note: See ``SwiftUI/View/searchable(text:placement:prompt:suggestions)``
<https://developer.apple.com/documentation/swiftui/form/searchable(text:placement:prompt:suggestions:)-94bdu>
for more information on searchable view modifier.
- parameter text: The text to display and edit in the search field.
- parameter collection: The collection to be filtered.
- parameter keyPath: The key path to the property which will be used to filter
the collection.
- parameter placement: The preferred placement of the search field within the
containing view hierarchy.
- parameter prompt: A `Text` representing the prompt of the search field
which provides users with guidance on what to search for.
- parameter suggestions: A view builder that produces content that
populates a list of suggestions.
*/
public func searchable<T: ObjectBase, S>(text: Binding<String>, collection: ObservedResults<T>, keyPath: KeyPath<T, String>,
placement: SearchFieldPlacement = .automatic, prompt: Text? = nil, @ViewBuilder suggestions: () -> S)
-> some View where S: View {
filterCollection(collection, for: text.wrappedValue, on: keyPath)
return searchable(text: text,
placement: placement,
prompt: prompt,
suggestions: suggestions)
}
/// Marks this view as searchable, which configures the display of a search field.
/// You can provide a collection and a key path to be filtered using the search
/// field string provided by the searchable component, this will result in the collection
/// querying for all items containing the search field string for the given key path.
///
/// @State var searchString: String
/// @ObservedResults(Reminder.self) var reminders
///
/// List {
/// ForEach(reminders) { reminder in
/// ReminderRowView(reminder: reminder)
/// }
/// }
/// .searchable(text: $searchFilter,
/// collection: $reminders,
/// keyPath: \.name) {
/// ForEach(reminders) { remindersFiltered in
/// Text(remindersFiltered.name).searchCompletion(remindersFiltered.name)
/// }
/// }
///
/**
- Note: See ``SwiftUI/View/searchable(text:placement:prompt:suggestions)``
<https://developer.apple.com/documentation/swiftui/form/searchable(text:placement:prompt:suggestions:)-1mw1m>
for more information on searchable view modifier.
- parameter text: The text to display and edit in the search field.
- parameter collection: The collection to be filtered.
- parameter keyPath: The key path to the property which will be used to filter
the collection.
- parameter placement: The preferred placement of the search field within the
containing view hierarchy.
- parameter prompt: The key for the localized prompt of the search field
which provides users with guidance on what to search for.
- parameter suggestions: A view builder that produces content that
populates a list of suggestions.
*/
public func searchable<T: ObjectBase, S>(text: Binding<String>, collection: ObservedResults<T>, keyPath: KeyPath<T, String>,
placement: SearchFieldPlacement = .automatic, prompt: LocalizedStringKey, @ViewBuilder suggestions: () -> S)
-> some View where S: View {
filterCollection(collection, for: text.wrappedValue, on: keyPath)
return searchable(text: text,
placement: placement,
prompt: prompt,
suggestions: suggestions)
}
/// Marks this view as searchable, which configures the display of a search field.
/// You can provide a collection and a key path to be filtered using the search
/// field string provided by the searchable component, this will result in the collection
/// querying for all items containing the search field string for the given key path.
///
/// @State var searchString: String
/// @ObservedResults(Reminder.self) var reminders
///
/// List {
/// ForEach(reminders) { reminder in
/// ReminderRowView(reminder: reminder)
/// }
/// }
/// .searchable(text: $searchFilter,
/// collection: $reminders,
/// keyPath: \.name) {
/// ForEach(reminders) { remindersFiltered in
/// Text(remindersFiltered.name).searchCompletion(remindersFiltered.name)
/// }
/// }
///
/**
- Note: See ``SwiftUI/View/searchable(text:placement:prompt:suggestions)``
<https://developer.apple.com/documentation/swiftui/form/searchable(text:placement:prompt:suggestions:)-6h6qo>
for more information on searchable view modifier.
- parameter text: The text to display and edit in the search field.
- parameter collection: The collection to be filtered.
- parameter keyPath: The key path to the property which will be used to filter
the collection.
- parameter placement: The preferred placement of the search field within the
containing view hierarchy.
- parameter prompt: A string representing the prompt of the search field
which provides users with guidance on what to search for.
- parameter suggestions: A view builder that produces content that
populates a list of suggestions.
*/
public func searchable<T: ObjectBase, V, S>(text: Binding<String>, collection: ObservedResults<T>, keyPath: KeyPath<T, String>,
placement: SearchFieldPlacement = .automatic, prompt: S, @ViewBuilder suggestions: () -> V)
-> some View where V: View, S: StringProtocol {
filterCollection(collection, for: text.wrappedValue, on: keyPath)
return searchable(text: text,
placement: placement,
prompt: prompt,
suggestions: suggestions)
}
private func filterCollection<T: ObjectBase>(_ collection: ObservedResults<T>, for text: String, on keyPath: KeyPath<T, String>) {
assumeOnMainActorExecutor {
collection.searchText(text, on: keyPath)
}
}
/// Marks this view as searchable, which configures the display of a search field.
/// You can provide a collection and a key path to be filtered using the search
/// field string provided by the searchable component, this will result in the collection
/// querying for all items containing the search field string for the given key path.
///
/// @State var searchString: String
/// @ObservedSectionedResults(Reminder.self) var reminders
///
/// List {
/// ForEach(reminders) { reminderSection in
/// Section(reminderSection.key) {
/// ForEach(reminderSection) { object in
/// ReminderRowView(reminder: object)
/// }
/// }
/// }
/// }
/// .searchable(text: $searchFilter,
/// collection: $reminders,
/// keyPath: \.name) {
/// ForEach(reminders) { remindersFiltered in
/// Text(remindersFiltered.name).searchCompletion(remindersFiltered.name)
/// }
/// }
///
/**
- Note: See ``SwiftUI/View/searchable(text:placement:prompt)``
<https://developer.apple.com/documentation/swiftui/form/searchable(text:placement:prompt:)-6royb>
for more information on searchable view modifier.
- parameter text: The text to display and edit in the search field.
- parameter collection: The collection to be filtered.
- parameter keyPath: The key path to the property which will be used to filter
the collection, only key paths with `String` type are allowed.
- parameter placement: The preferred placement of the search field within the
containing view hierarchy.
- parameter prompt: A `Text` representing the prompt of the search field
which provides users with guidance on what to search for.
*/
public func searchable<Key, T: ObjectBase>(text: Binding<String>, collection: ObservedSectionedResults<Key, T>, keyPath: KeyPath<T, String>,
placement: SearchFieldPlacement = .automatic, prompt: Text? = nil) -> some View {
filterCollection(collection, for: text.wrappedValue, on: keyPath)
return searchable(text: text, placement: placement, prompt: prompt)
}
/// Marks this view as searchable, which configures the display of a search field.
/// You can provide a collection and a key path to be filtered using the search
/// field string provided by the searchable component, this will result in the collection
/// querying for all items containing the search field string for the given key path.
///
/// @State var searchString: String
/// @ObservedResults(Reminder.self) var reminders
///
/// List {
/// ForEach(reminders) { reminderSection in
/// Section(reminderSection.key) {
/// ForEach(reminderSection) { object in
/// ReminderRowView(reminder: object)
/// }
/// }
/// }
/// }
/// .searchable(text: $searchFilter,
/// collection: $reminders,
/// keyPath: \.name) {
/// ForEach(reminders) { remindersFiltered in
/// Text(remindersFiltered.name).searchCompletion(remindersFiltered.name)
/// }
/// }
///
/**
- Note: See ``SwiftUI/View/searchable(text:placement:prompt)``
<https://developer.apple.com/documentation/swiftui/form/searchable(text:placement:prompt:)-2ed8t>
for more information on searchable view modifier.
- parameter text: The text to display and edit in the search field.
- parameter collection: The collection to be filtered.
- parameter keyPath: The key path to the property which will be used to filter
the collection.
- parameter placement: The preferred placement of the search field within the
containing view hierarchy.
- parameter prompt: The key for the localized prompt of the search field
which provides users with guidance on what to search for.
*/
public func searchable<Key, T: ObjectBase>(text: Binding<String>, collection: ObservedSectionedResults<Key, T>,
keyPath: KeyPath<T, String>, placement: SearchFieldPlacement = .automatic,
prompt: LocalizedStringKey) -> some View {
filterCollection(collection, for: text.wrappedValue, on: keyPath)
return searchable(text: text, placement: placement, prompt: prompt)
}
/// Marks this view as searchable, which configures the display of a search field.
/// You can provide a collection and a key path to be filtered using the search
/// field string provided by the searchable component, this will result in the collection
/// querying for all items containing the search field string for the given key path.
///
/// @State var searchString: String
/// @ObservedResults(Reminder.self) var reminders
///
/// List {
/// ForEach(reminders) { reminderSection in
/// Section(reminderSection.key) {
/// ForEach(reminderSection) { object in
/// ReminderRowView(reminder: object)
/// }
/// }
/// }
/// }
/// .searchable(text: $searchFilter,
/// collection: $reminders,
/// keyPath: \.name) {
/// ForEach(reminders) { remindersFiltered in
/// Text(remindersFiltered.name).searchCompletion(remindersFiltered.name)
/// }
/// }
///
/**
- Note: See ``SwiftUI/View/searchable(text:placement:prompt)``
<https://developer.apple.com/documentation/swiftui/form/searchable(text:placement:prompt:)-58egp>
for more information on searchable view modifier.
- parameter text: The text to display and edit in the search field.
- parameter collection: The collection to be filtered.
- parameter keyPath: The key path to the property which will be used to filter
the collection.
- parameter placement: The preferred placement of the search field within the
containing view hierarchy.
- parameter prompt: A string representing the prompt of the search field
which provides users with guidance on what to search for.
*/
public func searchable<Key, T: ObjectBase, S>(text: Binding<String>, collection: ObservedSectionedResults<Key, T>, keyPath: KeyPath<T, String>,
placement: SearchFieldPlacement = .automatic, prompt: S) -> some View where S: StringProtocol {
filterCollection(collection, for: text.wrappedValue, on: keyPath)
return searchable(text: text, placement: placement, prompt: prompt)
}
/// Marks this view as searchable, which configures the display of a search field.
/// You can provide a collection and a key path to be filtered using the search
/// field string provided by the searchable component, this will result in the collection
/// querying for all items containing the search field string for the given key path.
///
/// @State var searchString: String
/// @ObservedResults(Reminder.self) var reminders
///
/// List {
/// ForEach(reminders) { reminderSection in
/// Section(reminderSection.key) {
/// ForEach(reminderSection) { object in
/// ReminderRowView(reminder: object)
/// }
/// }
/// }
/// }
/// .searchable(text: $searchFilter,
/// collection: $reminders,
/// keyPath: \.name) {
/// ForEach(reminders) { remindersFiltered in
/// Text(remindersFiltered.name).searchCompletion(remindersFiltered.name)
/// }
/// }
///
/**
- Note: See ``SwiftUI/View/searchable(text:placement:prompt:suggestions)``
<https://developer.apple.com/documentation/swiftui/form/searchable(text:placement:prompt:suggestions:)-94bdu>
for more information on searchable view modifier.
- parameter text: The text to display and edit in the search field.
- parameter collection: The collection to be filtered.
- parameter keyPath: The key path to the property which will be used to filter
the collection.
- parameter placement: The preferred placement of the search field within the
containing view hierarchy.
- parameter prompt: A `Text` representing the prompt of the search field
which provides users with guidance on what to search for.
- parameter suggestions: A view builder that produces content that
populates a list of suggestions.
*/
public func searchable<Key, T: ObjectBase, S>(text: Binding<String>, collection: ObservedSectionedResults<Key, T>, keyPath: KeyPath<T, String>,
placement: SearchFieldPlacement = .automatic, prompt: Text? = nil, @ViewBuilder suggestions: () -> S)
-> some View where S: View {
filterCollection(collection, for: text.wrappedValue, on: keyPath)
return searchable(text: text,
placement: placement,
prompt: prompt,
suggestions: suggestions)
}
/// Marks this view as searchable, which configures the display of a search field.
/// You can provide a collection and a key path to be filtered using the search
/// field string provided by the searchable component, this will result in the collection
/// querying for all items containing the search field string for the given key path.
///
/// @State var searchString: String
/// @ObservedResults(Reminder.self) var reminders
///
/// List {
/// ForEach(reminders) { reminderSection in
/// Section(reminderSection.key) {
/// ForEach(reminderSection) { object in
/// ReminderRowView(reminder: object)
/// }
/// }
/// }
/// }
/// .searchable(text: $searchFilter,
/// collection: $reminders,
/// keyPath: \.name) {
/// ForEach(reminders) { remindersFiltered in
/// Text(remindersFiltered.name).searchCompletion(remindersFiltered.name)
/// }
/// }
///
/**
- Note: See ``SwiftUI/View/searchable(text:placement:prompt:suggestions)``
<https://developer.apple.com/documentation/swiftui/form/searchable(text:placement:prompt:suggestions:)-1mw1m>
for more information on searchable view modifier.
- parameter text: The text to display and edit in the search field.
- parameter collection: The collection to be filtered.
- parameter keyPath: The key path to the property which will be used to filter
the collection.
- parameter placement: The preferred placement of the search field within the
containing view hierarchy.
- parameter prompt: The key for the localized prompt of the search field
which provides users with guidance on what to search for.
- parameter suggestions: A view builder that produces content that
populates a list of suggestions.
*/
public func searchable<Key, T: ObjectBase, S>(text: Binding<String>, collection: ObservedSectionedResults<Key, T>, keyPath: KeyPath<T, String>,
placement: SearchFieldPlacement = .automatic, prompt: LocalizedStringKey, @ViewBuilder suggestions: () -> S)
-> some View where S: View {
filterCollection(collection, for: text.wrappedValue, on: keyPath)
return searchable(text: text,
placement: placement,
prompt: prompt,
suggestions: suggestions)
}
/// Marks this view as searchable, which configures the display of a search field.
/// You can provide a collection and a key path to be filtered using the search
/// field string provided by the searchable component, this will result in the collection
/// querying for all items containing the search field string for the given key path.
///
/// @State var searchString: String
/// @ObservedResults(Reminder.self) var reminders
///
/// List {
/// ForEach(reminders) { reminderSection in
/// Section(reminderSection.key) {
/// ForEach(reminderSection) { object in
/// ReminderRowView(reminder: object)
/// }
/// }
/// }
/// }
/// .searchable(text: $searchFilter,
/// collection: $reminders,
/// keyPath: \.name) {
/// ForEach(reminders) { remindersFiltered in
/// Text(remindersFiltered.name).searchCompletion(remindersFiltered.name)
/// }
/// }
///
/**
- Note: See ``SwiftUI/View/searchable(text:placement:prompt:suggestions)``
<https://developer.apple.com/documentation/swiftui/form/searchable(text:placement:prompt:suggestions:)-6h6qo>
for more information on searchable view modifier.
- parameter text: The text to display and edit in the search field.
- parameter collection: The collection to be filtered.
- parameter keyPath: The key path to the property which will be used to filter
the collection.
- parameter placement: The preferred placement of the search field within the
containing view hierarchy.
- parameter prompt: A string representing the prompt of the search field
which provides users with guidance on what to search for.
- parameter suggestions: A view builder that produces content that
populates a list of suggestions.
*/
public func searchable<Key, T: ObjectBase, V, S>(text: Binding<String>, collection: ObservedSectionedResults<Key, T>, keyPath: KeyPath<T, String>,
placement: SearchFieldPlacement = .automatic, prompt: S, @ViewBuilder suggestions: () -> V)
-> some View where V: View, S: StringProtocol {
filterCollection(collection, for: text.wrappedValue, on: keyPath)
return searchable(text: text,
placement: placement,
prompt: prompt,
suggestions: suggestions)
}
private func filterCollection<Key, T: ObjectBase>(_ collection: ObservedSectionedResults<Key, T>, for text: String, on keyPath: KeyPath<T, String>) {
assumeOnMainActorExecutor {
collection.searchText(text, on: keyPath)
}
}
}