SwiftUI and dynamic NSSortDescriptors in #FetchRequest - core-data

I have a list with items that contain a title and a date. User can set what to sort on (title or date).
However I can't figure out how to change the NSSortDescriptor dynamically.
import SwiftUI
import CoreData
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \Test.title, ascending: true)], animation: .default) private var items: FetchedResults<Test>
#State private var sortType: Int = 0
#State private var sortDescriptor: NSSortDescriptor = NSSortDescriptor(keyPath: \Test.title, ascending: true)
var body: some View {
Picker(selection: $sortType, label: Text("Sort")) {
Text("Title").tag(0)
Text("Date").tag(1)
}
.pickerStyle(SegmentedPickerStyle())
.onChange(of: sortType) { value in
sortType = value
if sortType == 0 {
sortDescriptor = NSSortDescriptor(keyPath: \Test.title, ascending: true)
} else {
sortDescriptor = NSSortDescriptor(keyPath: \Test.date, ascending: true)
}
}
List {
ForEach(items) { item in
let dateString = itemFormatter.string(from: item.date!)
HStack {
Text(item.title!)
Spacer()
Text(dateString)
}
}
}
.onAppear(perform: {
if items.isEmpty {
let newEntry1 = Test(context: self.viewContext)
newEntry1.title = "Apple"
newEntry1.date = Date(timeIntervalSince1970: 197200800)
let newEntry2 = Test(context: self.viewContext)
newEntry2.title = "Microsoft"
newEntry2.date = Date(timeIntervalSince1970: 168429600)
let newEntry3 = Test(context: self.viewContext)
newEntry3.title = "Google"
newEntry3.date = Date(timeIntervalSince1970: 904903200)
let newEntry4 = Test(context: self.viewContext)
newEntry4.title = "Amazon"
newEntry4.date = Date(timeIntervalSince1970: 773402400)
if self.viewContext.hasChanges {
try? self.viewContext.save()
}
}
})
}
}
private let itemFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .none
return formatter
}()
When I change
#FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \Test.title, ascending: true)], animation: .default) private var items: FetchedResults<Test>
to
#FetchRequest(sortDescriptors: [sortDescriptor], animation: .default) private var items: FetchedResults<Test>
the error "Cannot use instance member 'sortDescriptor' within property initializer; property initializers run before 'self' is available" appears.
I also tried to store the NSSortDescriptor in a UserDefault and create an init that creates it's own FetchRequest.. still no dynamic sorting...
Anyone a pointer where to look to solve this problem?
Whole project found here: https://github.com/l1ghthouse/FRDSD

Solved! Thanks to Asperi pointing to this QA: https://stackoverflow.com/a/59345830/12299030
import SwiftUI
import CoreData
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#AppStorage("firstLaunch") var firstLaunch: Bool = true
#State var sortDescriptor: NSSortDescriptor = NSSortDescriptor(keyPath: \Test.title, ascending: true)
#State private var sortType: Int = 0
var body: some View {
Picker(selection: $sortType, label: Text("Sort")) {
Text("Title").tag(0)
Text("Date").tag(1)
}
.pickerStyle(SegmentedPickerStyle())
.onChange(of: sortType) { value in
sortType = value
if sortType == 0 {
sortDescriptor = NSSortDescriptor(keyPath: \Test.title, ascending: true)
} else {
sortDescriptor = NSSortDescriptor(keyPath: \Test.date, ascending: true)
}
}
ListView(sortDescripter: sortDescriptor)
.onAppear(perform: {
if firstLaunch == true {
let newEntry1 = Test(context: self.viewContext)
newEntry1.title = "Apple"
newEntry1.date = Date(timeIntervalSince1970: 197200800)
let newEntry2 = Test(context: self.viewContext)
newEntry2.title = "Microsoft"
newEntry2.date = Date(timeIntervalSince1970: 168429600)
let newEntry3 = Test(context: self.viewContext)
newEntry3.title = "Google"
newEntry3.date = Date(timeIntervalSince1970: 904903200)
let newEntry4 = Test(context: self.viewContext)
newEntry4.title = "Amazon"
newEntry4.date = Date(timeIntervalSince1970: 773402400)
if self.viewContext.hasChanges {
try? self.viewContext.save()
}
firstLaunch = false
}
})
}
}
struct ListView: View {
#FetchRequest var items: FetchedResults<Test>
#Environment(\.managedObjectContext) var viewContext
init(sortDescripter: NSSortDescriptor) {
let request: NSFetchRequest<Test> = Test.fetchRequest()
request.sortDescriptors = [sortDescripter]
_items = FetchRequest<Test>(fetchRequest: request)
}
var body: some View {
List {
ForEach(items) { item in
let dateString = itemFormatter.string(from: item.date!)
HStack {
Text(item.title!)
Spacer()
Text(dateString)
}
}
}
}
}
private let itemFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .none
return formatter
}()

Related

My struct values cannot be used in function -> Cannot convert value of type 'Int' to expected argument type 'Range<Int>'

Im getting an error when using my Question struct in my function checkAnswer
I want to use the value in questions[index].answer but it error says
Cannot convert value of type 'Int' to expected argument type 'Range' and Value of type 'ArraySlice' has no member 'answer'
What did I do wrong on that part?
import SwiftUI
struct Question {
var question: String
var answer: Int
}
struct ContentView: View {
#State private var isGameRunning = false
let multiplyTableRange = Range(2...12)
#State private var selectedTable = 2
#State private var selectedNumOfQuestions = 5 //min of 5
#State private var variantsForCountOfQuestions = [5, 10, 20]
#State private var questions = [Question]()
#State private var currentQuestionIndex: Int = 1
#State private var answerInput: Int = 0
#State private var score = 0
#State private var isGameDone = false
#State private var gameCompleteMessage = ""
var body: some View {
if isGameRunning{
Group{
VStack{
Text("Your Score \(score)")
Text("\(questions[currentQuestionIndex].question)")
TextField("Enter your answer", value: $answerInput, formatter: NumberFormatter())
//error here
Button("Enter") {
checkAnswer(userAnswer: answerInput ?? 0 , answer: questions[currentQuestionIndex].answer)
}
.alert(isPresented: $isGameDone) {
Alert(title: Text(gameCompleteMessage), message: Text("Restart game"), primaryButton: .destructive(Text("Okay")) {
isGameRunning = false
answerInput = 0
score = 0
gameCompleteMessage = ""
currentQuestionIndex = 1
}, secondaryButton: .cancel())
}
}
}
}else{
Group{
VStack{
Text("Pick multiplication table to practice")
Picker("Pick multiplication table to practice", selection: $selectedTable){
ForEach(multiplyTableRange, id: \.self){
Text("\($0)")
}
} //eof picker
.labelsHidden()
.pickerStyle(SegmentedPickerStyle())
Text("How many questions?")
Picker("", selection: $selectedNumOfQuestions){
ForEach(variantsForCountOfQuestions, id: \.self){
Text("\($0)")
}
}//eofPicker
.labelsHidden()
.pickerStyle(SegmentedPickerStyle())
Button("Start"){
generateQuestions()
isGameRunning.toggle()
}
}
}
}
} //eof body
func generateQuestions(){
questions.removeAll()
for question in 1...selectedNumOfQuestions {
let x = Question(question: ("\(selectedTable) X \(question)"), answer: selectedTable * question)
questions.append(x)
}
}
func checkAnswer(userAnswer: Int, answer: Int) {
if userAnswer == answer {
score += 1
}
if currentQuestionIndex < selectedNumOfQuestions - 1 {
currentQuestionIndex += 1
} else {
gameCompleteMessage = "Your score is \(score)"
isGameDone = true
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

SwiftUI show loading view while core data is being loaded

How I can show loading view while core data is being loaded?.
Currently my app's core data store some many images in Binary Data. So when I switch to another tab showing data stored in core data, app lags 1.5 seconds.
So here are two things I have tried:
first I tried to minimize amount of data being loaded from core data using downsample function:
func downsample(imageAt imageURL: Data, to pointSize: CGSize, scale: CGFloat = UIScreen.main.scale) -> UIImage? {
// Create an CGImageSource that represent an image
//CGImageSourceCreateWithData(_ data: CFData, _ options: CFDictionary?)
let imageSourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary
guard let imageSource = CGImageSourceCreateWithData(imageURL as CFData, imageSourceOptions) else {
return nil
}
// Calculate the desired dimension
let maxDimensionInPixels = max(pointSize.width, pointSize.height) * scale
// Perform downsampling
let downsampleOptions = [
kCGImageSourceCreateThumbnailFromImageAlways: true,
kCGImageSourceShouldCacheImmediately: true,
kCGImageSourceCreateThumbnailWithTransform: true,
kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels
] as CFDictionary
guard let downsampledImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, downsampleOptions) else {
return nil
}
// Return the downsampled image as UIImage
return UIImage(cgImage: downsampledImage)
}
let small = downsample(imageAt: data, to: size)
Image(uiImage: small!)
But there were no difference in lagging time.
So i tried this:
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \ToDoItem2.createdAt, ascending: false)])
var toDoItems: FetchedResults<ToDoItem>
var body: some View {
VStack{
if toDoItems.isEmpty {
LoadingView()
} else {
List {
ForEach(toDoItems) { item in
ToDoItemView(item: item)
}
}
}
}
}
}
Tried to detect no loaded state as toDoItems.isEmpty but it doesn't work
Would be there anyway to show loading view while core data is being loaded?
Thanks
you could try using NSAsynchronousFetchRequest, something like this approach (does not have to be exactly like this (untested) code):
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#State private var toDoItems: [ToDoItem] = []
#State var isLoading = true
var body: some View {
VStack {
if isLoading {
LoadingView()
} else {
List {
ForEach(toDoItems) { item in
ToDoItemView(item: item)
}
}
}
}
.onAppear {
isLoading = true
let fetchRequest: NSFetchRequest<ToDoItem> = ToDoItem.fetchRequest()
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \ToDoItem.createdAt, ascending: false)]
let asyncFetchRequest = NSAsynchronousFetchRequest(fetchRequest: fetchRequest) { fetchResult -> Void in
if let resutls = fetchResult.finalResult {
self.toDoItems = resutls
}
self.isLoading = false
}
do {
_ = try viewContext.execute(asyncFetchRequest)
} catch {
print("error: \(error)")
}
}
}
}

Refresh a view when the children of an object are changed in SwiftUI

I am working on a CoreData application with two entities MyList and MyListItem. MyList can have many MyListItem (one to many). When the app is launched, I can see all the lists. I can tap on a list to go to the list items. On that screen, I tap a button to add an item to the selected list. After, adding the item when I go back to the all lists screen I cannot see the number of items reflected in the count. The reason is that MyListsView is not rendered again since the number of lists have not changed.
The complete code is shown below:
import SwiftUI
import CoreData
extension MyList {
static var all: NSFetchRequest<MyList> {
let request = MyList.fetchRequest()
request.sortDescriptors = []
return request
}
}
struct DetailView: View {
#Environment(\.managedObjectContext) var viewContext
let myList: MyList
var body: some View {
VStack {
Text("Detail View")
Button("Add List Item") {
let myListP = viewContext.object(with: myList.objectID) as! MyList
let myListItem = MyListItem(context: viewContext)
myListItem.name = randomString()
myListItem.myList = myListP
try? viewContext.save()
}
}
}
func randomString(length: Int = 8) -> String {
let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
return String((0..<length).map{ _ in letters.randomElement()! })
}
}
class ViewModel: NSObject, ObservableObject {
#Published var myLists: [MyList] = []
private var fetchedResultsController: NSFetchedResultsController<MyList>
private(set) var context: NSManagedObjectContext
override init() {
self.context = CoreDataManager.shared.context
fetchedResultsController = NSFetchedResultsController(fetchRequest: MyList.all, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
super.init()
fetchedResultsController.delegate = self
do {
try fetchedResultsController.performFetch()
guard let myLists = fetchedResultsController.fetchedObjects else { return }
self.myLists = myLists
} catch {
print(error)
}
}
}
extension ViewModel: NSFetchedResultsControllerDelegate {
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
guard let myLists = controller.fetchedObjects as? [MyList] else { return }
self.myLists = myLists
}
}
struct MyListsView: View {
let myLists: [MyList]
var body: some View {
List(myLists) { myList in
NavigationLink {
DetailView(myList: myList)
} label: {
HStack {
Text(myList.name ?? "")
Spacer()
Text("\((myList.items ?? []).count)")
}
}
}
}
}
struct ContentView: View {
#StateObject private var vm = ViewModel()
#Environment(\.managedObjectContext) var viewContext
var body: some View {
NavigationView {
VStack {
// when adding an item to the list the MyListView view is
// not re-rendered
MyListsView(myLists: vm.myLists)
Button("Change List") {
}
}
}
}
func randomString(length: Int = 8) -> String {
let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
return String((0..<length).map{ _ in letters.randomElement()! })
}
}
Inside ContentView there is a view called "MyListsView". That view is not rendered when the items are added. Since, according to that view nothing changed since the number of lists are still the same.
How do you solve this problem?
UPDATE:
What happens if I add one more level of views like for ListCellView as shown below:
struct MyListCellView: View {
#StateObject var vm: ListCellViewModel
init(vm: ListCellViewModel) {
_vm = StateObject(wrappedValue: vm)
}
var body: some View {
HStack {
Text(vm.name)
Spacer()
Text("\((vm.items).count)")
}
}
}
#MainActor
class ListCellViewModel: ObservableObject {
let myList: MyList
init(myList: MyList) {
self.myList = myList
self.name = myList.name ?? ""
self.items = myList.items!.allObjects as! [MyListItem]
print(self.items.count)
}
#Published var name: String = ""
#Published var items: [MyListItem] = []
}
struct MyListsView: View {
#StateObject var vm: ViewModel
init(vm: ViewModel) {
_vm = StateObject(wrappedValue: vm)
}
var body: some View {
let _ = Self._printChanges()
List(vm.myLists) { myList in
NavigationLink {
DetailView(myList: myList)
} label: {
MyListCellView(vm: ListCellViewModel(myList: myList))
}
}
}
}
Now the count is again not being updated.
Your ViewModel is an ObserveableObject, but you are not observing it in MyListsView. When you initialized MyListsView, you set a let constant. Of course that won't update. Do this instead:
struct MyListsView: View {
#ObservedObject var vm: ViewModel
init(viewModel: ViewModel) {
self.vm = viewModel
}
var body: some View {
List(vm.myLists) { myList in
NavigationLink {
DetailView(myList: myList)
} label: {
HStack {
Text(myList.name ?? "")
Spacer()
Text("\((myList.items ?? []).count)")
}
}
}
}
}
Now the #Published in ViewModel will cause MyListView to change when it does, and that includes adding a related entity.
We don't need MVVM in SwiftUI, the View data structs already fill that role and property wrappers make them behave like objects giving best of both worlds. In your case use the #FetchRequest property wrapper for the list and #ObservedObject for the detail and body will be called on any changes to the model data. Examine the code in the app template in Xcode with Core Data checked. It looks like this:
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
animation: .default)
private var items: FetchedResults<Item>
var body: some View {
NavigationView {
List {
ForEach(items) { item in
NavigationLink {
DetailView(item: item)
} label: {
Text(item.timestamp!, formatter: itemFormatter)
}
}
.onDelete(perform: deleteItems)
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
EditButton()
}
ToolbarItem {
Button(action: addItem) {
Label("Add Item", systemImage: "plus")
}
}
}
Text("Select an item")
}
}
...
struct DetailView: View {
#ObservedObject var item: Item
var body: some View {
Text("Item at \(item.timestamp!, formatter: itemFormatter)")

SwiftUI + Core Data - updating an object (Detail -> DetailEdit)

Goal: update a core data object with SwiftUI: DetailView -> EditDetail -> DetailView (updated).
Problem: code bellow works, but creates a new object, instead of updating existing one.
import SwiftUI
struct DetailView: View {
var order = Order()
#State var showOrderEdit = false
var body: some View {
Form{
Text(order.tableNumber)
Text(order.pizzaType)
}
.navigationTitle(order.pizzaType)
.toolbar {
ToolbarItem(placement: .primaryAction) {
//edit button
Button(action: {
showOrderEdit = true
}, label: {
Text("Edit")
})
.sheet(isPresented: $showOrderEdit) {
OrderEdit(order: order)
}
}
}
}
}
import SwiftUI
struct DetailEdit: View {
#State var tableNumber = ""
#Environment(\.managedObjectContext) private var viewContext
#Environment (\.presentationMode) var presentationMode
var order = Order()
var body: some View {
NavigationView {
Form {
TextField("table number", text: $tableNumber)
//update button
Button(action: {
updateOrder(order: order)
}) {
Text("Update")
.foregroundColor(.blue)
}
}
//passing data item detail -> item edit
.onAppear {
self.tableNumber = self.order.tableNumber
}
.navigationTitle("Edit Order")
}
}
func updateOrder(order: Order) {
let newtableNumber = tableNumber
viewContext.performAndWait {
order.tableNumber = newtableNumber
try? viewContext.save()
}
}
You create new Order object in each view, so it is stored as new one into database. Instead you need to inject CoreData object from parent view (which shows DetailView) as observed object,
struct DetailView: View {
#ObservedObject var order: Order // << here !!
// .. other code
and
struct DetailEdit: View {
#State var tableNumber = ""
#Environment(\.managedObjectContext) private var viewContext
#Environment (\.presentationMode) var presentationMode
#ObservedObject var order: Order // << here !!
// ... other code
in such approach you will work with same instance of Order in both views and they will be updated because observe that instance for modifications.

fetch core data string and place in a label (Swift4)

I am trying to call 2 different core data strings and place them each on separate labels. Right now I am getting the error Cannot invoke initializer for type 'init(_:)' with an argument list of type '([NSManagedObject])'. This error is coming from j1.text = String(itemsName). I added both view controllers for saving and displaying.
import UIKit
import CoreData
class ViewController: UIViewController {
#IBOutlet var j1 : UITextField!
#IBOutlet var j2 : UITextField!
#IBAction func save(){
let appD = UIApplication.shared.delegate as! AppDelegate
let context = appD.persistentContainer.viewContext
let entity = NSEntityDescription.entity(forEntityName: "Team", in : context)!
let theTitle = NSManagedObject(entity: entity, insertInto: context)
theTitle.setValue(j1.text, forKey: "score")
theTitle.setValue(j2.text, forKey: "alba")
do {
try context.save()
}
catch {
print("Tom Corley")
}
}}
class twoVC: UIViewController {
#IBOutlet var j1 : UILabel!
#IBOutlet var j2 : UILabel!
var itemsName : [NSManagedObject] = []
var itemsName2 : [NSManagedObject] = []
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let appD = UIApplication.shared.delegate as! AppDelegate
let context = appD.persistentContainer.viewContext
let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "Team")
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "score", ascending: true)]
let fetchRequest2 = NSFetchRequest<NSManagedObject>(entityName: "Team")
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "alba", ascending: true)]
do {
itemsName = try context.fetch(fetchRequest)
itemsName2 = try context.fetch(fetchRequest2)
if let score = itemsName[0].value(forKey: "score") {
j1.text = (score as! String)
}
if let alba = itemsName2[0].value(forKey: "alba") {
j2.text = (alba as? String)
}
}catch {
print("Ashley Tisdale")
}
}}
Loop over the result from the fetch and append to a string that is then used as value for the label, this goes inside the do{...} where you do the fetch today. Note that I am only using one fetch request here.
itemsName = try context.fetch(fetchRequest)
var mergedScore: String = ""
var mergedAlba: String = ""
for item in itemsName {
if let score = item.value(forKey: "score") as? String {
mergedScore.append(score)
mergedScore.append(" ") //separator
}
if let alba = item.value(forKey: "alba") as? String {
mergedScore.append(alba)
mergedScore.append(" ") //separator
}
}
j1.text = mergedScore
j2.text = mergedAlba
Try this one it's Working for me Swift 4 I think You need to store the value as int which are used as sortDescriptor.
func FetchManagedObjectFromDatabaseForStoreData(Entity :NSEntityDescription) ->
[NSManagedObject]
{
let fetchRequest = NSFetchRequest<NSFetchRequestResult>()
// Add Sort Descriptor
let sortDescriptor = NSSortDescriptor(key: "order", ascending: true)
let sortDescriptor1 = NSSortDescriptor(key: "is_favourite", ascending: false)
fetchRequest.sortDescriptors = [sortDescriptor,sortDescriptor1]
// Create Entity Description
fetchRequest.entity = Entity
let result : [NSManagedObject] = []
// Execute Fetch Request
do{
let result = try appDelegate.managedObjectContext.fetch(fetchRequest) as! [NSManagedObject]
if result.count > 0
{
return result
}
else
{
// return result
}
}
catch{
let fetchError = error as NSError
print(fetchError)
}
return result
}
For Fetch Data
// Create Entity Description
let entityDescription = NSEntityDescription.entity(forEntityName: "Your Entity Name Here", in: appDel.managedObjectContext)
let DataObject = FetchManagedObjectFromDatabaseForStoreData(Entity: entityDescription!)
//Convert Array of NSManagedObject into array of [String:AnyObject]
for item in DataObject{
let keys = Array(item.entity.attributesByName.keys)
// Here is your result
print((item.dictionaryWithValues(forKeys: keys) as NSDictionary).value(forKey: "id") as Any) // And so On Whatewer you Fetch
}

Resources