I'm having trouble figuring out how to dynamically update Core Data's FetchRequest's NSSortDescriptor in SwiftUI. I'm not sure if setting it in .onAppear is the correct way which it doesn't as I'm seeing a strange rearranging of my list. I'm using an AppStorage variable to store my sorting then I set the NSSortDescriptor .onAppear, and save when state changes. Also I detect any changes in my Picker selection and apply it to my FetchRequest configuration's sort descriptor like Apple describe's here. If I remove the FetchRequest's animation I don't see the strange rearranging but I also don't get any animations which doesn't look nice. I'm really unsure how to solve this or if I'm even doing this right.
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
// Set default sort descriptor as empty because it's set in onAppear
#FetchRequest(
sortDescriptors: [],
animation: .default) // or set to .none
private var items: FetchedResults<Entity>
#AppStorage("sortby") var sortby: SortBy = .title
var body: some View {
NavigationView {
List {
ForEach(items) { item in
Text(item.title)
}
}
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
ForEach(sort.allCases) { sort in
Button(sort.title, action: {
sortby = sort
})
}
}
}
// When sort changes set the new value here
.onChange(of: sortby, perform: { value in
items.nsSortDescriptors = //NSSortDescriptor here
})
// Set initial sort descriptor here
.onAppear(perform: {
items.nsSortDescriptors = //NSSortDescriptor here
})
}
}
}
FetchRequest's animation is set to default and rearranging.
FetchRequest's animation is set to none, it works but then no animations become available when sorting.
Related
I have a list of items stored in CoreData and an add button on the top right which adds an item to the list and navigates to the ItemView programmatically.
When there is no item in the list and I press the add button the created item slides in as expected but the NavigationLink doesn't navigate to its ItemView automatically. Furthermore, the NavigationLink doesn't even work when tapping on it.
Only when pressing the add button again - which slides in the second item - do the NavigationLinks start to work as expected.
Steps to reproduce:
Create a new Single View App and click CoreData and Hosted in CloudKit. There should be an Item Entity stored in CoreData already.
Replace the ContentView.swift with this:
import SwiftUI
import CoreData
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#State private var selection: Date?
#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(
"Item at \(item.timestamp!, formatter: itemFormatter)",
destination: ItemView(item: item),
tag: item.timestamp!,
selection: $selection
)
}
.onDelete(perform: deleteItems)
}
.listStyle(InsetGroupedListStyle())
.navigationTitle("Items")
.toolbar {
Button(action: addItem) {
Label("Add Item", systemImage: "plus")
}
}
}
}
private func addItem() {
withAnimation {
let newItem = Item(context: viewContext)
newItem.timestamp = Date()
do {
try viewContext.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
selection = newItem.timestamp
}
}
private func deleteItems(offsets: IndexSet) {
withAnimation {
offsets.map { items[$0] }.forEach(viewContext.delete)
do {
try viewContext.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
}
struct ItemView: View {
let item: Item
var body: some View {
Text("Item at \(item.timestamp!, formatter: itemFormatter)")
}
}
private let itemFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .medium
return formatter
}()
Click on the add button on the top right. Then try to click on the NavigationLink. It won't navigate. Only when pressing the add button again will it work as expected.
I update the selection asynchronously after 0.5 seconds and the bug disappears. Not sure if this is a reliable solution but from the user's perspective it is intuitive to first see the item slide in and navigating after.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
selection = newItem.timestamp
}
I have a SwiftUI view that is connected to a CoreData model. I also have a SpriteKit scene that changes data in my model. So every time I manipulate my data from my SKScene in CoreData my scene gets reinitialised which is an unwanted behaviour in my case.
How can I get the updated model in my SpriteView without the SKScene being recreated?
My code looks like this:
struct TamagotchiListView: View {
#Environment(\.managedObjectContext)
var context: NSManagedObjectContext
#FetchRequest(fetchRequest: TamagotchiModel.getFetchRequest())
var tamagotchis: FetchedResults<TamagotchiModel>
var body: some View {
VStack {
List {
ForEach(tamagotchis, id: \.self) { (tamagotchi: TamagotchiModel) in
NavigationLink(destination: SpriteKitView(scene: SpriteKitScene(model: tamagotchi))) {
HStack {
Image(systemName: "gamecontroller")
.padding(.trailing, 5)
VStack(alignment: .leading) {
Text(tamagotchi.name)
.font(.headline)
Spacer()
Text(tamagotchi.birthDate, style: .date)
}
Spacer()
}
}
}
}
}
}
I managed to work around my problem by creating a view model that manages the SpriteKit scene creation if needed.
class TamagotchiViewModel {
private var spriteKitScenes: [SpriteKitScene] = []
func scene(for tamagotchi: TamagotchiModel) -> SpriteKitScene {
if let scene = spriteKitScenes.first(where: { $0.tamagotchi?.tamagotchiModel.id == tamagotchi.id}) {
return scene
} else {
let newScene = SpriteKitScene(model: tamagotchi)
spriteKitScenes.append(newScene)
return newScene
}
}
}
I am trying to select database items with a predicate before displaying a view and need to pass a property, but I'm in a catch-22 situation since the property may not be initialized, yielding the message: Cannot use instance member 'subject' within property initializer; property initializers run before 'self' is available
struct ShowInfo: View
{
#State var subject: Subject
#Environment(\.managedObjectContext) var moc
#FetchRequest(entity: Info.entity(), sortDescriptors: [NSSortDescriptor(key: "title", ascending: true)],
predicate: NSPredicate(format: "subject.title == %#", $subject.title)
) var infos: FetchedResults<Info>
#State var size = UIScreen.main.bounds.width / 3
var body: some View
{
List
{
Text("Info").font(.system(size: 24)).foregroundColor(Color.green)
ForEach(infos, id: \.infoid)
{ info in
ZStack
{
if info.subject == self.subject.title
{
NavigationLink(destination: EditInfoView(info: info))
{
HStack
{
Text(info.title!).frame(width: 150, height: 40).background(Color.blue).foregroundColor(Color.white).cornerRadius(10)
Text(info.value!)
}
}
}
}
}.onDelete(perform: deleteInfo)
}
}
}
The answer found at SwiftUI View and #FetchRequest predicate with variable that can change is essentially correct, but I discovered that the order in which things appear inside the struct matters to avoid a slew of errors.
I modified my code as follows...
struct ShowInfo: View
{
init (subject: Subject)
{
self.subject = subject
self.infoRequest = FetchRequest<Info>(entity: Info.entity(), sortDescriptors: [NSSortDescriptor(key: "title", ascending: true)],predicate: NSPredicate(format: "subject.title == %#", subject.title!))
}
#Environment(\.managedObjectContext) var moc
var subject: Subject
var infoRequest: FetchRequest<Info>
var infos: FetchedResults<Info>{infoRequest.wrappedValue}
This cleared up all the errors and allowed the app to work correctly. When the init() was after the variable declarations, I got the same error as before as well as other errors. The compiler seemed to be getting confused about what had already been initialized and what had not. I'm not sure why the example shown for question 57871088 worked for others and mine required rearranging the declarations.
I thought the above answer solved my problem, and it did in that case, but in another view the same methodology yielded errors claiming that I hadn't initialized all the properties within the init. After much experimenting, I discovered all those errors went away if I commented out the #Environment statement. Of course that generated errors for the undefined variable moc, so I commented that code out to see what happened, then the original errors reappeared. It seems clear that the compiler is getting confused. Does anyone know a way around that issue?
Here's the code I'm working with now:
struct EditSubjectView: View
{
init(subject: Subject)
{
self.subject = subject
self.formRequest = FetchRequest(entity: Field.entity(), sortDescriptors: [NSSortDescriptor(key: "sequence", ascending: true)], predicate: NSPredicate(format: "subjectid == %#", subject.subjectid!.description))
}
#Binding var subject: Subject
var formRequest: FetchRequest<Field>
var fields : FetchedResults<Field>{formRequest.wrappedValue}
#Environment(\.managedObjectContext) var moc
var body: some View
{
return VStack
{
HStack
{
Text("Subject Title").padding(.leading)
//Spacer()
}
TextField(subject.title!, text: Binding($subject.title)!)
.padding(.all)
.background(Color(red: 239.0/255.0, green: 243.0/255.0, blue: 244.0/255.0, opacity: 1.0))
.cornerRadius(15)
Spacer()
Button("Save")
{
do
{
try self.moc.save()
}
catch
{
print(error.localizedDescription)
}
}.frame(width: 150, height: 30).background(Color.red).foregroundColor(Color.white).cornerRadius(15.0)
}.padding()
}
}
Back in objective-c, I could fetch a sectioned list of objects from Core Data using something like this:
self.fetchedResultsController = [[NSFetchedResultsController alloc]
initWithFetchRequest:fetchRequest
managedObjectContext:managedObjectContext
sectionNameKeyPath:#"ispurchased"
cacheName:nil];
And then the NSFetchedResultsController would automatically give me the data in sections and rows.
I'm experimenting with SwiftUI for the first time and I'm trying to figure out how to achieve a sectioned List like that. I've found lots of examples that use some canned array data that is pre-structured in the sectioned fashion, but I can't figure out how to do a sectioned FetchRequest nor how to integrate that with the List.
struct EasyModeList: View {
#FetchRequest(
sortDescriptors: [
NSSortDescriptor(keyPath: \EasyMode.ispurchased, ascending: true),
NSSortDescriptor(keyPath: \EasyMode.ispurchasable, ascending: false),
NSSortDescriptor(keyPath: \EasyMode.name, ascending: true),
],
animation: .default)
var easymodes: FetchedResults<EasyMode>
#Environment(\.managedObjectContext)
var viewContext
var body: some View {
List {
ForEach(self.easymodes, id: \.self) { easymode in
NavigationLink(
destination: DetailView(easymode: easymode)
) {
VStack {
Text("\(easymode.name!)")
}
}
}
}
}
}
Does SwiftUI easily support those kinds of sectioned lists? Is there a different paradigm that I should be shifting my brain to?
I am also looking for a proper solution to this. For now I'll share what I've tried. My sections are by string titles, but I'll adapt it for your data.
#FetchRequest(...) var managedEasyModes: FetchedResults<EasyMode>
private var easyModes: [String: [EasyMode]] {
Dictionary(grouping: managedEasyModes) { easymode in
easymode.ispurchased ? "Purchased" : "Not Purchased"
}
}
private var sections: [String] {
easyModes.keys.sorted(by: >)
}
var body: some View {
ForEach(sections, id: \.self) { section in
Section(header: Text(section)) {
ForEach(easyModes[section]!) { easyMode in
NavigationLink(destination: EasyModeView(easyMode: easyMode)) {
EasyModeRowView(easyMode: easyMode)
}
}
}
}
}
I created a UIHostingController to put a SwiftUI view in my app. The root view contains a VStack, and inside that some HStacks and Pickers. The picker pushes the view wider than the screen by 8 points. It looks bad - text is slightly off screen. Usually I would fix this with auto layout constraints that pin the left and right edges of the view to the superview, with priority 1000 (required). What is the equivalent in SwiftUI?
Some boiled down code that triggers the problem
struct EditSegmentView: View {
#State var seconds: Int = 10
var body: some View {
VStack {
HStack {
Text("Pause after intro")
Spacer()
Text("10 seconds")
}
Picker(selection: $seconds, label: Text("Seconds")) {
ForEach(0..<60) { n in
Text("\(n) sec").tag(n)
}
}
}
}
}
That is displayed from a UIViewController, like this:
let editView = EditSegmentView()
let vc = UIHostingController(rootView: editView)
vc.modalPresentationStyle = .fullScreen
present(vc, animated: true)
I can't see anything obviously wrong with the code provided. You could try using a GeometryReader to force the VStack to be the width of the available space. Also add some padding(), because otherwise your Text will start right at the edge
var body: some View {
GeometryReader { proxy in
VStack {
//...
} .padding()
.frame(width: proxy.size.width)
}
}
Are there any clues as to why the view is expanding beyond the edges when you debug its view hierarchy?
This is a little late, but I had the same issue and fixed by making sure the outer VStack had it's alignment set to .leading. The inner Vstack also has alignment: leading and then added a .padding() to the inner VStack