This question already has answers here:
How to use a #FetchRequest with the new searchable modifier in SwiftUI?
(3 answers)
Closed last month.
I have a view with a segmented picker. This view displays results of a FetchRequest.
I'd like the predicate linked to this request to change when the picker value change.
Here's what I tried but I can't get around the fact that #FetchRequest cannot use a #State var since they're not both created at init().
#FetchRequest(sortDescriptors: [
SortDescriptor(\.date, order: .reverse)
], predicate: NSPredicate(format: "date >= %#", selectedTimeFrame.getDate() as NSDate)) var sessions: FetchedResults<Session>
#State private var selectedTimeFrame = TimeFrame.thisWeek
This give me this error which I understand but I can't think of another way to make this work:
Cannot use instance member 'selectedTimeFrame' within property
initializer; property initializers run before 'self' is available
You can handle this by setting an initial predicate for your FetchRequest, e.g.
#FetchRequest(sortDescriptors: [
SortDescriptor(\.date, order: .reverse)
], predicate: NSPredicate(format: "date >= %#", TimeFrame.thisWeek as NSDate)) var sessions: FetchedResults<Session>
#State private var selectedTimeFrame = TimeFrame.thisWeek
and then in your view, update the predicate when the State changes, e.g.
.onChange(of: selectedTimeFrame) { selectedTimeFrame
sessions.nsPreciate = NSPredicate(format: "date >= %#", selectedTimeFrame.getDate() as NSDate))
}
Related
Context
I have two different realms that a user can switch between at any point they decide; in-memory or on-disk.
I store this Realm configuration on an observable object which publishes the change when required:
class RealmServiceConfig: ObservableObject {
static var config = CurrentValueSubject<Realm.Configuration?, Never>(nil)
}
I have a class that stores realm filters/sorts as #Published variables that a user can change at any point.
class Store: ObservableObject
{
static var shared = Store()
#Published var realmFilter: NSPredicate? = nil
#Published var realmSortKeyPath: String = "fieldOnMyRealmModel"
#Published var realmSortKeyPathAscending: Bool = false
}
I have two views:
ListView - This shows the live results of Objects in the current Realm.
As the filter/sort changes, the filteredObjects is recalculated thanks to the #Published values, but, it still points to the same config.
//import SwiftUI
//import Combine
//import RealmSwift
struct ListView: View
{
#ObservedObject var store = Store.shared
#ObservedResults(MyRealmModel.self, configuration: RealmServiceConfig.config.value) var unfilteredObjects
var body: some View {
let filteredObjects = unfilteredObjects
.filter(store.realmFilter ?? NSPredicate(value: true))
.sorted(by: [SortDescriptor(keyPath: "\(store.realmSortKeyPath)", ascending: store.realmSortKeyPathAscending)])
return VStack(spacing: 0) {
ScrollView {
VStack {
ForEach(filteredObjects) { object in
Text(object.name)
}
}
}
}
}
}
SettingsView - The user can change their Realm configuration between in-memory and on-disk here which updates the aforementioned RealmServiceConfig.config value
When navigating to SettingsView, the ListView is moved to the background, it is not removed from the view hierarchy.
Question
When a user changes the realm and therefore updates the RealmServiceConfig.config value, is it possible to force reload #ObservedResults with the latest configuration?
Based on the current setup, even though my configuration parameter points to the published value, this is never applied.
Workaround
I have implemented this on a model class that stores the Results<Object> in a #Published variable which allows me to react and update the query based on the config changes and pass the results through to the views.
It would just be cleaner to have this all contained in the SwiftUI view where it's needed.
Maybe this is not available yet/works against how #ObservedResults is meant to be used?
I have a VM that is implemented as follows:
LoginViewModel
class LoginViewModel: ObservableObject {
var username: String = ""
var password: String = ""
}
In my ContentView, I use the VM as shown below:
#StateObject private var loginVM = LoginViewModel()
var body: some View {
NavigationView {
Form {
TextField("User name", text: $loginVM.username)
TextField("Password", text: $loginVM.password)
Every time I type something in the TextField it shows the following message in the output window:
Binding<String> action tried to update multiple times per frame.
Binding<String> action tried to update multiple times per frame.
Binding<String> action tried to update multiple times per frame.
It is a message and not an error.
If I decorate my username and password properties with #Published then the message goes away but the body is rendered each time I type in the TextField.
Any ideas what is going on and whether I should use #Published or not. I don't think I will gain anything from putting the #Published attribute since this is a one-way binding and I don't want to display anything on the view once the username changes.
If I decorate my username and password properties with #Published then the message goes away
This is the correct solution. You need to use #Published on those properties because that is how SwiftUI gets notified when the properties change.
the body is rendered each time I type in the TextField
That is fine. Your body method is not expensive to compute.
I don't think I will gain anything from putting the #Published attribute since this is a one-way binding
You cannot be sure SwiftUI will work correctly (now or in future releases) if you don't use #Published. SwiftUI expects to be notified when the value of a Binding changes, even when a built-in SwiftUI component like TextField causes the change.
For the simple case - the state is kept in the same view or in a ModelSupport class, consists of strings or other primitive types, and there's only one of each, #Published will work fine.
I got this error with a model class containing an array of structs and using a List, and every time you type inside a TextField inside a list (or every time you select an item in a list), the view gets refreshed, and the error gets triggered.
I am thus using a DelayedTextField:
struct DelayedTextField: View {
var title: String = ""
#Binding var text: String
#State private var tempText: String = ""
var body: some View {
TextField(title, text: $tempText, onEditingChanged: { editing in
if !editing {
$text.wrappedValue = tempText
}
})
.onAppear {
tempText = text
}
}
}
and the binding update error is no more.
I am trying out this quick start for SwiftUI and Combine in order to try and understand how to connect my Realm database to Combine.
The example observes a RealmSwift.List and keeps a table populated with its data. This is is a linked list to a child class. I'm wondering how to observe a Results collection so I can keep track of any changes to an entire Realm class.
For example, let's say I have a Workspace class:
class Workspace: Object, ObjectKeyIdentifiable{
#objc dynamic var id = UUID().uuidString
#objc dynamic var name = ""
#objc dynamic var archived = false
}
In the state object, I can set up a Results<Workspace> variable like this:
class AppState: ObservableObject {
#Published var workspaces: Results<Workspace>?
var cancellables = Set<AnyCancellable>()
init(){
let realmPublisher = PassthroughSubject<Realm, Error>()
realmPublisher
.sink(receiveCompletion: { _ in }, receiveValue: { realm in
//Get the Results
self.workspaces = realm.objects(Workspace.self)
})
.store(in: &cancellables)
realmPublisher.send(try! Realm())
return
}
}
But when it comes time to observe the object, I can't because Results isn't an object (I assume).
struct ContentView: App {
#ObservedObject var state = AppState()
var view: some View {
ItemsView(workspaces: state.workspaces!)
}
var body: some Scene {
WindowGroup {
view.environmentObject(state)
}
}
}
struct ItemsView: View {
#ObservedObject var workspaces: Results<Workspace> //<!-- Error
var body: some View {
//...
}
}
Xcode gives a syntax error on the workspaces property:
Property type 'Results' does not match that of the 'wrappedValue' property of its wrapper type 'ObservedObject'
Is it possible to observe a set of Results just like we can have a notification listener on a collection of Results?
Technically, you could hook up a sink to state.workspaces (state.$workspaces.sink()), but in this case, I think you're overcomplicating the problem.
You already have an #ObservableObject in your ContentView (AppState) that is managing the results for you. So, change ItemsView to just take this as a parameter:
var workspaces: Results<Workspace>?
It doesn't need to be an #ObservedObject -- either way, whether it's getting observed in that view or it's parent view, it's going to get re-rendered. It does have to be optional here, since it's an optional value on your AppState, unless you want to keep passing it with the force unwrap (!), but that's generally a bad idea, since it'll crash if it ever is in fact nil.
Also, above, in your Realm code, make sure it's matching the tutorial that you were following. For example, you have Publisher.sink which should really be realmPublisher.sink
You are correct, Results is a struct, and therefore cannot be covered by #StateObject or #ObservedObject. Your workaround is suitable for now.
Once https://github.com/realm/realm-cocoa/pull/7045 is released, you will be able to use one of the new Realm property wrappers to embed your Results into the view directly. At the time of this posting, that would be #FetchRealmResults, but that is subject to change.
I try to pass data from an #environmentObject to a #State object in the TopLevel
struct ContentView: View {
#EnvironmentObject var countRecognizer: themeCounter
#State var theme: themeModel = themeData[countRecognizer.themeCount]
#State var hideBar = true
var body: some View {
ZStack {
videoCard(theme: theme)
.statusBar(hidden: true)
Text("\(self.countRecognizer.themeCount)")
if hideBar == true {
}
}
But I am getting this error: "Cannot use instance member within property initializer; property initializers run before 'self' is available"
the themeData Array should get the Int from the environment Object.
How can I fix this problem?
do your
theme: themeModel = themeData[countRecognizer.themeCount]
in
.onAppear(...)
You cannot use countRecognizer directly from the initial value of another property, and there is no easy fix.
I suggest you to look into refactoring your #State var theme property into a #Published var theme inside the themeCounter ObservableObject. Apple tutorials will help you: https://developer.apple.com/tutorials/swiftui/tutorials
As an aside: DON'T NAME TYPES WITH A LOWERCASE.
themeModel should be ThemeModel
themeCounter should be ThemeCounter
videoCard should be VideoCard
(This is my first SwiftUI project; please be kind if this is a stupid question.)
I have a collection of objects which are displayed in a Picker. The picker selection is $selectedIndex, where
#State private var selectedIndex: Int = 0
I also have a
#State private var opts: OptsStruct = OptsStruct()
where elements of the OptsStruct structure are bound to SwiftUI views. The value of opts needs to change when the selectedIndex changes, because the opts property is the option shown in and selected by the Picker. (Also, I want to save the current value of selectedIndex in UserDefaults.) The problem is that I don't understand how to express these actions in SwiftUI.
I tried
#State private var selectedIndex: Int = 0 {
mutating didSet {
// save selectedIndex to UserDefaults
opts = f(selectedIndex)
}
but this causes a Segmentation Fault.
Where is the 'correct' place to put this logic. (And in general, can someone suggest some reading on how to connect changes to SwiftUI #States with general business logic.)
Thanks,
Rick
The idea of a #State variable is for it to be the single source of truth (wikipedia). This means that one variable should be the only thing that contains the "state" of your picker. In this case, I suggest using this:
$opts.selectionIndex
as the Binding for your picker. selectionIndex would then be a Int property of your OptsStruct type.