Error only when the app is launched in an iPhone. Thread 1: EXC_BAD_ACCESS (code=1, address=0x4052c00000000000) - swift

I am writing a program that uses data from coredata and creates a graph out of it. When I run it, it crashes with the following error Thread 1: EXC_BAD_ACCESS (code=1, address=0x4052c00000000000). The code starts from ContentView as follows
struct ContentView: View {
#State private var name = ""
#State private var score = ""
var reviewItem: ReviewItem
#State private var displayNewView = false
var body: some View {
NavigationView{
VStack {
NavigationLink(destination: ReviewView(reviewItem: ReviewItem()), isActive: self.$displayNewView) { EmptyView() }
Form {
TextField("Name", text: $name)
TextField("Score", text: $score)
}
.toolbar {
Button("add") {
reviewItem.saveReviewData(name: name, score: Int32(score)!)
self.displayNewView = true
}
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(reviewItem: ReviewItem())
}
}
This is what the code for reviewView looks like
struct GradeData: Identifiable {
var id = UUID()
var averageGrade: Int32
var condition: String
}
struct ReviewView: View {
var gradeVShours: [GradeData]
init(reviewItem: ReviewItem) {
gradeVShours = [
GradeData(averageGrade: reviewItem.getScore(scoreType: "high"), condition: "Well"), GradeData(averageGrade: reviewItem.getScore(scoreType: "low"), condition: "Not Well")]
}
var body: some View {
if #available(iOS 16.0, *) {
Chart {
ForEach(self.gradeVShours) { shape in
BarMark (
x: .value("Total Grade", shape.averageGrade),
y: .value("Grade Condition", shape.condition)
)
}
}
} else {
Text("Upgrade to ios version 16.0 or higher")
}
}
}
This is what the code for ReviewItem looks like
class ReviewItem: NSObject, ObservableObject {
let coreDM: DataController = DataController.shared
override init() {
super.init()
}
func getListScore(scoreType: String) -> [Int32] {
let score = coreDM.getScoreFromCoreData(scoreType: scoreType)
return score
}
func getScore(scoreType: String) -> Int32 {
var listScore = getListScore(scoreType: scoreType)
let totalCount = scoreType.count
let totalSum = listScore.reduce(0, +)
return totalSum/Int32(totalCount)
}
func saveReviewData(name: String, score: Int32) {
coreDM.saveToCoreData(name: name, score: score)
}
}
This is what the code in DataController looks like. It is used to fetch the data from coredata. And this is where the error is thrown after the app crashes. I have noted the line where the error is thrown.
class DataController: ObservableObject {
static var shared = DataController()
let container = NSPersistentContainer(name: "StudentData")
init() {
container.loadPersistentStores(completionHandler: { description, error in
if let error = error {
print("Core data failed to laod: \(error.localizedDescription)")
}
})
}
func getScoreFromCoreData(scoreType: String) -> [Int32] {
// let filter: NSPredicate
var scoreData: [GradeTable]
var listScore: [Int32] = []
let fetchRequest: NSFetchRequest<GradeTable> = GradeTable.fetchRequest()
fetchRequest.returnsObjectsAsFaults = false
if scoreType == "high" {
let filter = NSPredicate(format: "score >= %#", 85) //Error thrown here
fetchRequest.predicate = filter
}
else {
let filter = NSPredicate(format: "score <= %#", 85)
fetchRequest.predicate = filter
}
do {
scoreData = try container.viewContext.fetch(fetchRequest)
} catch let error {
print("ERROR while fetching data from db array \(error.localizedDescription)")
return []
}
for item in scoreData {
listScore.append(item.score)
}
return listScore
}
func saveToCoreData(name: String, score: Int32) {
let gradeTable = GradeTable(context: container.viewContext)
gradeTable.uuid = UUID()
gradeTable.name = name
gradeTable.score = score
do {
try container.viewContext.save()
print("Saved")
} catch let error {
print("Error: \(error.localizedDescription)")
}
}
}
Note that StudentData has an entity named GradeTable which has attributes named score, name and uuid which is of data type Int32, string and UUID. The app launches fine and works well in simulator, but it crashes with the following error when I launch it in my phone.
I can't figure out what's going on. Using breakpoints didn't help much. Will you please tell me what's going on and how to fix it?

The problem is that your DataController instance will load the persistent store in an asynchronous operation, while the view immediately creates the fetch request before the persistent store is loaded completely.
You would want to move the initialization way before the view creation, such as in the SceneDelegate.

If you are having this issue, make sure to check the data type of the filter in Nspredicate. In my case, I was using an integer as a filter, and it was the reason behind the error. Using a string instead of an integer fixed it.
This doesn't work because the filter(85) is an integer.
let filter = NSPredicate(format: "score >= %#", 85) //Error thrown here
This works because the filter("85") is a string.
let filter = NSPredicate(format: "score >= %#", "85")

Related

How to change the value of a var with a TextField SwiftUI

I was trying to make a weather api call, the api call needs to have a location. The location that I pass is a variable, but now I want to change the location value based on a TextField's input.
I made the apiKey shorter just for safety measures. There's more code, but it's not relevant.
I just need to know how to change the city variable that is on the WeatherClass using the TextField that is in the cityTextField View.
Thanks.
class WeatherClass: ObservableObject {
#Published var weatherAddress: String = ""
#Published var weatherDays: [WeatherDays] = []
var city: String = ""
func fetch() {
let location = city
let apiKey = "AP8LUYMSTHZ"
let url = URL(string: "https://weather.visualcrossing.com/VisualCrossingWebServices/rest/services/timeline/\(location)?key=\(apiKey)")!
URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data else { return }
if let weather = try? JSONDecoder().decode(WeatherData.self, from: data) {
DispatchQueue.main.async {
self.weatherAddress = weather.resolvedAddress
self.weatherDays = weather.days
}
} else {
print("City?")
}
}.resume()
}//----------------------------------- End of fetch()
}
struct WeatherData: Decodable {
let resolvedAddress: String
let days: [WeatherDays]
}
struct WeatherDays: Hashable, Decodable {
let datetime: String
let tempmax: Double
let tempmin: Double
let description: String
}
struct cityTextField: View {
#State var city: String = ""
var body: some View {
TextField("Search city", text: $city).frame(height:30).multilineTextAlignment(.center).background().cornerRadius(25).padding(.horizontal)
}
}
I already watched a lot of tutorials for similar things buts none of them really helped me.
Try this approach using minor modifications to
func fetch(_ city: String){...} to fetch the weather for the city in your
TextField using .onSubmit{...}
struct ContentView: View {
#StateObject var weatherModel = WeatherClass()
var body: some View {
VStack {
cityTextField(weatherModel: weatherModel)
}
}
}
struct cityTextField: View {
#ObservedObject var weatherModel: WeatherClass // <-- here
#State var city: String = ""
var body: some View {
TextField("Search city", text: $city)
.frame(height:30)
.multilineTextAlignment(.center)
.background()
.cornerRadius(25)
.padding(.horizontal)
.onSubmit {
weatherModel.fetch(city) // <-- here
}
}
}
class WeatherClass: ObservableObject {
#Published var weatherAddress: String = ""
#Published var weatherDays: [WeatherDays] = []
func fetch(_ city: String) { // <-- here
let apiKey = "AP8LUYMSTHZ"
// -- here
let url = URL(string: "https://weather.visualcrossing.com/VisualCrossingWebServices/rest/services/timeline/\(city)?key=\(apiKey)")!
URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data else { return }
if let weather = try? JSONDecoder().decode(WeatherData.self, from: data) {
DispatchQueue.main.async {
self.weatherAddress = weather.resolvedAddress
self.weatherDays = weather.days
}
} else {
print("City?")
}
}.resume()
}
}
Alternatively, as suggested by synapticloop, you could use this approach:
struct cityTextField: View {
#ObservedObject var weatherModel: WeatherClass // <-- here
var body: some View {
TextField("Search city", text: $weatherModel.city) // <-- here
.frame(height:30)
.multilineTextAlignment(.center)
.background()
.cornerRadius(25)
.padding(.horizontal)
.onSubmit {
weatherModel.fetch() // <-- here
}
}
}
class WeatherClass: ObservableObject {
#Published var weatherAddress: String = ""
#Published var weatherDays: [WeatherDays] = []
#Published var city: String = "" // <-- here
func fetch() {
let apiKey = "AP8LUYMSTHZ"
// -- here
let url = URL(string: "https://weather.visualcrossing.com/VisualCrossingWebServices/rest/services/timeline/\(city)?key=\(apiKey)")!
URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data else { return }
if let weather = try? JSONDecoder().decode(WeatherData.self, from: data) {
DispatchQueue.main.async {
self.weatherAddress = weather.resolvedAddress
self.weatherDays = weather.days
}
} else {
print("City?")
}
}.resume()
}
}

MacOS swiftUI table select a single item

I followed the example from here: SwiftUI 3 MacOs Table single selection and double click open sheet
But it's not working well for me.
I have a structure like this:
struct Response: Codable {
var items: [Repository]
}
struct Repository: Codable, Identifiable {
var number = UUID()
var id: Int = 0
let name: String
let updated_at: String
let owner: Owner
let stargazers_count: Int
let forks_count: Int
let language: String?
let description: String?
struct Owner: Codable {
let login: String
}
enum CodingKeys: String, CodingKey {
case name, updated_at, owner, stargazers_count, forks_count, language, description
}
}
class Api : ObservableObject{
func loadData(query: String = "javascript", completion:#escaping ([Repository]) -> ()) {
guard let url = URL(string: "https://api.github.com/search/repositories?q=\(query)") else {
print("Invalid url...")
return
}
URLSession.shared.dataTask(with: url) { data, response, error in
if let decodedResponse = try? JSONDecoder().decode(Response.self, from: data!) {
var id = 0
let results = decodedResponse.items.map { (repo: Repository) -> Repository in
var copyRepo = repo
copyRepo.id = id
id += 1
return copyRepo
}
print(results)
DispatchQueue.main.async {
completion(results)
}
}
}.resume()
}
}
...
#State var repositories = [Repository]()
#State private var sortOrder = [KeyPathComparator(\Repository.name)]
#State private var selectedRepository: Repository.ID?
public var body: some View {
if(selectedRepository != nil)//null??
Text(repositories[selectedRepository!].name)
Table(repositories, selection: $selectedRepository, sortOrder: $sortOrder) {
TableColumn("Name") {
Text($0.name)
.frame(
maxWidth: .infinity,
maxHeight: .infinity,
alignment: .leading
)
.contentShape(Rectangle())
.contextMenu {
Button(action: {}) { Text("Action") }
}
/*.gesture(TapGesture(count: 2).onEnded {
print("pr", selectedRepository )
})*/
}
TableColumn("Last updated"){
Text($0.updated_at)
}
TableColumn("Owner", value: \.owner.login)
}
.onAppear() {
Api().loadData { (repositories) in
self.repositories = repositories
self.isLoading = false
}
}
.onChange(of: sortOrder) {
print($0)
repositories.sort(using: $0)
}
}
I would like to make sure that when the user clicks on an element of the table, then activates the selection to show the data of the selected element in a Text field, I have tried to have the name printed but it is not working.
It gives me the following error after starting the app: Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value
Can you give me a hand?
May be some missing character :
if(selectedRepository != nil)//null?? <- missing '{'
Text(repositories[selectedRepository!].name)
Also be writing like :
if let selectedRepository = selectedRepository {
Text(repositories[selectedRepository!].name)
...
}
Even if not necessary I always prefer to initialise properties (It is more an habit that something useful) :
#State private var selectedRepository: Repository.ID? = nil

Preview in Canvas stops working as soon as I use an object passed by another view in SwiftUI

I have an app that uses Core Data, everything works fine when I compile to the simulator or a physical device, the issue is that for some reason the Preview in Canvas doesn't work. In the code below I'm passing an Item from ItemsView to ItemView through a NavigationLink, the issue starts as soon as I use the passed item anywhere in ItemView. Again, the issue is only in Canvas. I know it's not a big deal since it compiles fine but I got used to seeing the preview when designing the interface.
Error message:
ItemsApp crashed due to an uncaught exception NSInvalidArgumentException. Reason: - [Item name]: unrecognized selector sent to instance 0x600001d6c080.: The preview process appears to have crashed.
Items View: Preview works fine.
import SwiftUI
struct ItemsView: View {
#ObservedObject var coreDataViewModel:CoreDataViewModel
var body: some View {
NavigationView{
VStack{
List {
ForEach(coreDataViewModel.items) { item in
HStack{
VStack(alignment:.leading){
Text(item.name ?? "")
Text(item.price ?? "")
}
NavigationLink(destination: ItemView(coreDataViewModel: coreDataViewModel, selectedItem: item)){
}
}
}
}
}
}
}
}
Item View: Preview doesn't work. The issue starts when I call Text(selectedItem.name ?? "--")
import SwiftUI
struct ItemView: View {
#ObservedObject var coreDataViewModel: CoreDataViewModel
#State var selectedItem: Item
var body: some View {
VStack{
HStack{
Text(selectedItem.name ?? "--") // this causes the issue
}
}
.onAppear{
Text(selectedItem.name ?? "--") // this causes the issue
}
}
}
struct ItemView_Previews: PreviewProvider {
static var previews: some View {
ItemView(coreDataViewModel: CoreDataViewModel(), selectedItem: Item())
}
}
Any idea what could be wrong?
Am I passing the item correctly?
Thanks
EDIT:
Corrected view name from ServicesView to ItemView in NavigationLink and Previews. Also added the error message.
EDIT:
Added CoreDataManager and CoreDataViewModel
CoreDataManager
class CoreDataManager{
static let instance = CoreDataManager()
let container: NSPersistentContainer
let context: NSManagedObjectContext
init(){
container = NSPersistentContainer(name: "CoreDataContainer")
container.loadPersistentStores { (description, error) in
if let error = error{
print("Error loading Core Data. \(error)")
}
}
context = container.viewContext
}
func save(){
do{
try context.save()
}catch let error{
print("Error saving Core Data. \(error.localizedDescription)")
}
}
}
CoreDataViewModel
class CoreDataViewModel: ObservableObject{
let manager = CoreDataManager.instance
#Published var items: [Item] = []
init(){
getItems()
}
func addItem(name: String, price: String){
let item = Item(context: manager.context)
item.name = name
item.price = price
save()
getItems()
}
func getItems(){
let request = NSFetchRequest<Item>(entityName: "Item")
let sort = NSSortDescriptor(keyPath: \Item.name, ascending: true)
request.sortDescriptors = [sort]
do{
items = try manager.context.fetch(request)
}catch let error{
print("Error fetching businesses. \(error.localizedDescription)")
}
}
func save(){
self.manager.save()
}
}
Here are the steps to follow:
In your Persistance struct declare a variable preview with your preview Items:
static var preview: PersistenceController = {
let result = PersistenceController(inMemory: true)
let viewContext = result.container.viewContext
let newItem = Item(context: viewContext)
newItem.yourProperty = yourValue
do {
try viewContext.save()
} catch {
// error handling
}
return result
}()
Create item from your viewContext and pass it to preview:
struct YourView_Previews: PreviewProvider {
static var previews: some View {
let context = PersistenceController.preview.container.viewContext
let request: NSFetchRequest<Item> = Item.fetchRequest()
let fetchedItem = (try! context.fetch(request).first)!
YourView(item: fetchedItem)
}
}
Here is Persistence struct created by Xcode at the moment of the project initialization:
import CoreData
struct PersistenceController {
static let shared = PersistenceController()
static var preview: PersistenceController = {
let result = PersistenceController(inMemory: true)
let viewContext = result.container.viewContext
let item = Item(context: viewContext)
item.property = yourProperty
do {
try viewContext.save()
} catch {
}
return result
}()
let container: NSPersistentContainer
init(inMemory: Bool = false) {
container = NSPersistentContainer(name: "TestCD")
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
}
}

SwiftUI: Deleting an item from a ForEach results in Index Out of Range

I've built a horizontal scrolling ForEach UI within my ContentView component that displays an array of custom objects (struct form). When I try to delete an item, I get an "Fatal Error: Index out of range" error.
The issue is when I delete an item, the actual array itself updates, but specific AssetView (component below) component is not updating and so it ultimately iterates to an index that no longer exists. Any idea what the issue can be? Below is my code:
ContentView
struct ContentView: View {
#ObservedObject var assetStore: AssetStore
var body: some View {
ScrollView (.horizontal) {
ForEach(assetStore.assets.indices, id: \.self) { index in
AssetView(
asset: $assetStore.assets[index],
assetStore: assetStore,
smallSize: geo.size.height <= 667
)
.padding(.bottom)
}
}
}
}
AssetView
struct AssetView: View {
#Binding var asset: Asset
#ObservedObject var assetStore: AssetStore
var smallSize: Bool
#State var editAsset: Bool = false
var body: some View {
VStack(alignment: .center, spacing: smallSize ? -10 : 0) {
HStack {
TagText(tagName: asset.name)
.onTapGesture {
editAsset.toggle()
}
Spacer()
DisplayCurrentValues(
forCurrentValueText: asset.getCurrentValueString,
forCurrentValueLabel: "Current Value"
)
.onTapGesture {
editAsset.toggle()
}
}
DisplayStepper(asset: $asset, title: "YoY Growth", type: .growth)
DisplayStepper(asset: $asset, title: "Recurring Deposit", type: .recurring)
}
.sheet(isPresented: $editAsset, content: {
EditAsset(assetStore: assetStore, currentValue: String(asset.currentValue), name: asset.name, asset: $asset)
})
}
}
AssetStore
This is where I read/write all of the asset objects to my App's Documents folder.
class AssetStore: ObservableObject {
let assetsJSONURL = URL(fileURLWithPath: "Assets",
relativeTo: FileManager.documentsDirectoryURL).appendingPathExtension("json")
#Published var assets: [Asset] = [] {
didSet {
saveJSONAssets()
}
}
init() {
print(assetsJSONURL)
loadJSONAssets()
}
private func loadJSONAssets() {
guard FileManager.default.fileExists(atPath: assetsJSONURL.path) else {
return
}
let decoder = JSONDecoder()
do {
let assetsData = try Data(contentsOf: assetsJSONURL)
assets = try decoder.decode([Asset].self, from: assetsData)
} catch let error {
print(error)
}
}
private func saveJSONAssets() {
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
do {
let assetsData = try encoder.encode(assets)
try assetsData.write(to: assetsJSONURL, options: .atomicWrite)
} catch let error {
print(error)
}
}
public func deleteAsset(atIndex id: Asset) {
let index = assets.firstIndex(where: { $0.id == id.id})
assets.remove(at: index!)
}
}
Asset Object
struct Asset: Identifiable, Codable, Hashable {
// currently set for 10 years
let id = UUID()
enum Frequency: String, Codable, CaseIterable {
case month = "Month"
case year = "Year"
case none = "None"
}
let years: Int
var name: String
var currentValue: Int
var growth: Int
var recurringDeposit: Int
var recurringFrequency: Frequency
enum CodingKeys: CodingKey {
case id
case years
case name
case currentValue
case growth
case recurringDeposit
case recurringFrequency
}
init(
name: String = "AssetName",
currentValue: Int = 1000,
growth: Int = 10,
years: Int = 10,
recurringDeposit: Int = 100,
recurringFrequency: Frequency = .month
) {
self.name = name
self.currentValue = currentValue
self.growth = growth
self.recurringDeposit = recurringDeposit
self.recurringFrequency = recurringFrequency
self.years = years
}
}
I think it's because of your ForEach relying on the indices, rather than the Assets themselves. But, if you get rid of the indices, you'll have to write a new binding for the Asset. Here's what I think it could look like:
ForEach(assetStore.assets, id: \.id) { asset in
AssetView(
asset: assetStore.bindingForId(id: asset.id),
assetStore: assetStore,
smallSize: geo.size.height <= 667
)
.padding(.bottom)
}
And then in your AssetStore:
func bindingForId(id: UUID) -> Binding<Asset> {
Binding<Asset> { () -> Asset in
self.assets.first(where: { $0.id == id }) ?? Asset()
} set: { (newValue) in
self.assets = self.assets.map { asset in
if asset.id == id {
return newValue
} else {
return asset
}
}
}
}

How to display Realm Results in SwiftUI List?

I have been able to save data in a Realm database, but have been unable to show the results in a SwiftUI List.
I know I have the data and have no problem printing the results in the console.
Is there a way to convert Realm Result into a format that can be displayed on a SwiftUI List?
import SwiftUI
import RealmSwift
import Combine
class Dog: Object {
#objc dynamic var name = ""
#objc dynamic var age = 0
override static func primaryKey() -> String? {
return "name"
}
}
class SaveDog {
func saveDog(name: String, age: String) {
let dog = Dog()
dog.age = Int(age)!
dog.name = name
// Get the default Realm
let realm = try! Realm()
print(Realm.Configuration.defaultConfiguration.fileURL!)
// Persist your data easily
try! realm.write {
realm.add(dog)
}
print(dog)
}
}
class RealmResults: BindableObject {
let didChange = PassthroughSubject<Void, Never>()
func getRealmResults() -> String{
let realm = try! Realm()
var results = realm.objects(Dog.self) { didSet
{didChange.send(())}}
print(results)
return results.first!.name
}
}
struct dogRow: View {
var dog = Dog()
var body: some View {
HStack {
Text(dog.name)
Text("\(dog.age)")
}
}
}
struct ContentView : View {
#State var dogName: String = ""
#State var dogAge: String = ""
let saveDog = SaveDog()
#ObjectBinding var savedResults = RealmResults()
let realm = try! Realm()
let dogs = Dog()
var body: some View {
VStack {
Text("Hello World")
TextField($dogName)
TextField($dogAge)
Button(action: {
self.saveDog.saveDog(name: self.dogName,
age:self.dogAge)
// self.savedResults.getRealmResults()
}) {
Text("Save")
}
//insert list here to show realm data
List(0 ..< 5) {
item in
Text(self.savedResults.getRealmResults())
} //Displays the same thing 5 times
}
}
}
#if DEBUG
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
ContentView()
}
}
#endif
Some of the code probably may not make sense because I was attempting several approaches to see if anything would work.
This line, for example, will display the result in the List View.
return results.first!.name
If I just return results, nothing displays in the List Text View.
As I have commented below I will attempt the ForEach approach when I have time. That looks promising.
The data that you pass in List or a ForEach must conform to the Identifiable protocol.
Either you adopt it in your Realm models or you use .identified(by:) method.
Even with that, the View won't reload if the data changes.
You could wrap Results and make it a BindableObject, so the view can detect the changes and reload itself:
class BindableResults<Element>: ObservableObject where Element: RealmSwift.RealmCollectionValue {
var results: Results<Element>
private var token: NotificationToken!
init(results: Results<Element>) {
self.results = results
lateInit()
}
func lateInit() {
token = results.observe { [weak self] _ in
self?.objectWillChange.send()
}
}
deinit {
token.invalidate()
}
}
And use it like:
struct ContentView : View {
#ObservedObject var dogs = BindableResults(results: try! Realm().objects(Dog.self))
var body: some View {
List(dogs.results.identified(by: \.name)) { dog in
DogRow(dog: dog)
}
}
}
This is the most straight forward way of doing it:
struct ContentView: View {
#State private var dog: Results<Dog> = try! Realm(configuration: Realm.Configuration(schemaVersion: 1)).objects(Dog.self)
var body: some View {
ForEach(dog, id: \.name) { i in
Text(String((i.name)!))
}
}
}
...That's it, and it works!
I have created a generic solution to display and add/delete for any Results<T>. By default, Results<T> is "live". SwiftUI sends changes to View when the #Published property WILL update. When a RealmCollectionChange<Results<T>> notification is received, Results<T> has already updated; Therefore, a fatalError will occur on deletion due to index out of range. Instead, I use a "live" Results<T> for tracking changes and a "frozen" Results<T> for use with the View. A full working example, including how to use a generic View with RealmViewModel<T> (shown below), can be found here: SwiftUI+Realm. The enum Status is used to display a ProgressView, "No records found", etc., when applicable, as shown in the project. Also, note that the "frozen" Results<T> is used when needing a count or single object. When deleting, the IndexSet by onDelete is going to return a position from the "frozen" Results<T> so it checks that the object still existing in the "live" Results<T>.
class RealmViewModel<T: RealmSwift.Object>: ObservableObject, Verbose where T: Identifiable {
typealias Element = T
enum Status {
// Display ProgressView
case fetching
// Display "No records found."
case empty
// Display results
case results
// Display error
case error(Swift.Error)
enum _Error: String, Swift.Error {
case fetchNotCalled = "System Error."
}
}
init() {
fetch()
}
deinit {
token?.invalidate()
}
#Published private(set) var status: Status = .error(Status._Error.fetchNotCalled)
// Frozen results: Used for View
#Published private(set) var results: Results<Element>?
// Live results: Used for NotificationToken
private var __results: Results<Element>?
private var token: NotificationToken?
private func notification(_ change: RealmCollectionChange<Results<Element>>) {
switch change {
case .error(let error):
verbose(error)
self.__results = nil
self.results = nil
self.token = nil
self.status = .error(error)
case .initial(let results):
verbose("count:", results.count)
//self.results = results.freeze()
//self.status = results.count == 0 ? .empty : .results
case .update(let results, let deletes, let inserts, let updates):
verbose("results:", results.count, "deletes:", deletes, "inserts:", inserts, "updates:", updates)
self.results = results.freeze()
self.status = results.count == 0 ? .empty : .results
}
}
var count: Int { results?.count ?? 0 }
subscript(_ i: Int) -> Element? { results?[i] }
func fetch() {
status = .fetching
//Realm.asyncOpen(callback: asyncOpen(_:_:))
do {
let realm = try Realm()
let results = realm.objects(Element.self).sorted(byKeyPath: "id")
self.__results = results
self.results = results.freeze()
self.token = self.__results?.observe(notification)
status = results.count == 0 ? .empty : .results
} catch {
verbose(error)
self.__results = nil
self.results = nil
self.token = nil
status = .error(error)
}
}
func insert(_ data: Element) throws {
let realm = try Realm()
try realm.write({
realm.add(data)
})
}
func delete(at offsets: IndexSet) throws {
let realm = try Realm()
try realm.write({
offsets.forEach { (i) in
guard let id = results?[i].id else { return }
guard let data = __results?.first(where: { $0.id == id }) else { return }
realm.delete(data)
}
})
}
}
Here is another option using the new Realm frozen() collections. While this is early days the UI is updated automatically when 'assets' get added to the database. In this example they are being added from an NSOperation thread, which should be a background thread.
In this example the sidebar lists different property groups based on the distinct values in the database - note that you may wish to implement this in a more robust manner - but as a quick POC this works fine. See image below.
struct CategoryBrowserView: View {
#ObservedObject var assets: RealmSwift.List<Asset> = FileController.shared.assets
#ObservedObject var model = ModelController.shared
#State private var searchTerm: String = ""
#State var isEventsShowing: Bool = false
#State var isProjectsShowing: Bool = false
#State var isLocationsShowing: Bool = false
var projects: Results<Asset> {
return assets.sorted(byKeyPath: "project").distinct(by: ["project"])
}
var events: Results<Asset> {
return assets.sorted(byKeyPath: "event").distinct(by: ["event"])
}
var locations: Results<Asset> {
return assets.sorted(byKeyPath: "location").distinct(by: ["location"])
}
#State var status: Bool = false
var body: some View {
VStack(alignment: .leading) {
ScrollView {
VStack(alignment: .leading) {
// Projects
DisclosureGroup(isExpanded: $isProjectsShowing) {
VStack(alignment:.trailing, spacing: 4) {
ForEach(filteredProjectsCollection().freeze()) { asset in
HStack {
Text(asset.project)
Spacer()
Image(systemName: self.model.selectedProjects.contains(asset.project) ? "checkmark.square" : "square")
.resizable()
.frame(width: 17, height: 17)
.onTapGesture { self.model.addProject(project: asset.project) }
}
}
}.frame(maxWidth:.infinity)
.padding(.leading, 20)
} label: {
HStack(alignment:.center) {
Image(systemName: "person.2")
Text("Projects").font(.system(.title3))
Spacer()
}.padding([.top, .bottom], 8).foregroundColor(.secondary)
}
// Events
DisclosureGroup(isExpanded: $isEventsShowing) {
VStack(alignment:.trailing, spacing: 4) {
ForEach(filteredEventsCollection().freeze()) { asset in
HStack {
Text(asset.event)
Spacer()
Image(systemName: self.model.selectedEvents.contains(asset.event) ? "checkmark.square" : "square")
.resizable()
.frame(width: 17, height: 17)
.onTapGesture { self.model.addEvent(event: asset.event) }
}
}
}.frame(maxWidth:.infinity)
.padding(.leading, 20)
} label: {
HStack(alignment:.center) {
Image(systemName: "calendar")
Text("Events").font(.system(.title3))
Spacer()
}.padding([.top, .bottom], 8).foregroundColor(.secondary)
}
// Locations
DisclosureGroup(isExpanded: $isLocationsShowing) {
VStack(alignment:.trailing, spacing: 4) {
ForEach(filteredLocationCollection().freeze()) { asset in
HStack {
Text(asset.location)
Spacer()
Image(systemName: self.model.selectedLocations.contains(asset.location) ? "checkmark.square" : "square")
.resizable()
.frame(width: 17, height: 17)
.onTapGesture { self.model.addLocation(location: asset.location) }
}
}
}.frame(maxWidth:.infinity)
.padding(.leading, 20)
} label: {
HStack(alignment:.center) {
Image(systemName: "flag")
Text("Locations").font(.system(.title3))
Spacer()
}.padding([.top, .bottom], 8).foregroundColor(.secondary)
}
}.padding(.all, 10)
.background(Color(NSColor.controlBackgroundColor))
}
SearchBar(text: self.$searchTerm)
.frame(height: 30, alignment: .leading)
}
}
func filteredProjectsCollection() -> AnyRealmCollection<Asset> {
if self.searchTerm.isEmpty {
return AnyRealmCollection(self.projects)
} else {
return AnyRealmCollection(self.projects.filter("project CONTAINS[c] %# || event CONTAINS[c] %# || location CONTAINS[c] %# || tags CONTAINS[c] %#", searchTerm, searchTerm, searchTerm, searchTerm))
}
}
func filteredEventsCollection() -> AnyRealmCollection<Asset> {
if self.searchTerm.isEmpty {
return AnyRealmCollection(self.events)
} else {
return AnyRealmCollection(self.events.filter("project CONTAINS[c] %# || event CONTAINS[c] %# || location CONTAINS[c] %# || tags CONTAINS[c] %#", searchTerm, searchTerm, searchTerm, searchTerm))
}
}
func filteredLocationCollection() -> AnyRealmCollection<Asset> {
if self.searchTerm.isEmpty {
return AnyRealmCollection(self.locations)
} else {
return AnyRealmCollection(self.locations.filter("project CONTAINS[c] %# || event CONTAINS[c] %# || location CONTAINS[c] %# || tags CONTAINS[c] %#", searchTerm, searchTerm, searchTerm, searchTerm))
}
}
func filteredCollection() -> AnyRealmCollection<Asset> {
if self.searchTerm.isEmpty {
return AnyRealmCollection(self.assets)
} else {
return AnyRealmCollection(self.assets.filter("project CONTAINS[c] %# || event CONTAINS[c] %# || location CONTAINS[c] %# || tags CONTAINS[c] %#", searchTerm, searchTerm, searchTerm, searchTerm))
}
}
func delete(at offsets: IndexSet) {
if let realm = assets.realm {
try! realm.write {
realm.delete(assets[offsets.first!])
}
} else {
assets.remove(at: offsets.first!)
}
}
}
struct CategoryBrowserView_Previews: PreviewProvider {
static var previews: some View {
CategoryBrowserView()
}
}
struct CheckboxToggleStyle: ToggleStyle {
func makeBody(configuration: Configuration) -> some View {
return HStack {
configuration.label
Spacer()
Image(systemName: configuration.isOn ? "checkmark.square" : "square")
.resizable()
.frame(width: 22, height: 22)
.onTapGesture { configuration.isOn.toggle() }
}
}
}