SwiftUI: When I unlock apps it takes me back to the list - core-data

I have some problem with my application. When I lock it while I am in the content view and then unlock it, it takes me back to the list. I would like the view I was in before locking to still be visible after unlocking. I have tried to get this effect somehow but not successfully. Please, give me a hint.
What is more, if I click the heart and the item is marked as favorite, it also moves me back to the list. Here I would also like to remain in the view after selecting the heart. How can I eliminate this?

Here is a sample usage using Apple's sample code since you didn't provide any entity info
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
animation: .default)
private var items: FetchedResults<Item>
//SceneStorage to preserve the selected item by the user
#SceneStorage("ContentView.selection") var selection: String?
var body: some View {
NavigationView {
List {
ForEach(items) { item in
//Use the NavigationLink that uses selection
NavigationLink(tag: item.objectID.description, selection: $selection,
destination: {
//to edit/ observe the item pass it to an #ObservedObject
EditItemView2(item: item)
}, label: {
VStack{
Text(item.timestamp!, formatter: itemFormatter)
}
})
Button("delete", action: {
withAnimation(.easeOut(duration: 2)){
try? viewContext.delete(item)
}
})
}.onDelete(perform: deleteItems)
}
}
.toolbar {
#if os(iOS)
ToolbarItem(placement: .navigationBarTrailing) {
EditButton()
}
#endif
ToolbarItem {
Button(action: addItem) {
Label("Add Item", systemImage: "plus")
}
}
}
Text("Select an item")
}
private func addItem() {
withAnimation {
let newItem = Item(context: viewContext)
newItem.timestamp = Date()
do {
try viewContext.save()
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
private func deleteItems(offsets: IndexSet) {
withAnimation(.easeOut(duration: 2)) {
offsets.map { items[$0] }.forEach(viewContext.delete)
do {
try viewContext.save()
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
}
private let itemFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .medium
return formatter
}()
struct EditItemView2: View{
//This observes the item and allows changes to the object
#ObservedObject var item: Item
var body: some View{
DatePicker("timestamp", selection: $item.timestamp.bound)
}
}
This page talks all about it
https://developer.apple.com/documentation/uikit/view_controllers/restoring_your_app_s_state_with_swiftui

If your view is the first to create your UserSettings object, I recommend that you use #StateObject instead of #ObservedObject—and especially if that object is the only owner of the view and is not being observed by any other view.

Related

SwiftUI CoreData - How to update the fetch request and the list

I am creating an application in SwiftUI using CoreData and I have a problem. In application you can add song to favorites and it will be added to list (FavoriteSongsView). Until the song is added to favorites everything is fine. In DetailView I click the button and the "heart.fill" icon and the song is added to the list. However, if I click on the icon again to un-favorite the song, it does not disappear from the list. I fought with it a little bit but without any effect. Could you please point out the cause of the problem?
List of favorite songs:
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(
entity: Song.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \Song.number, ascending: true)],
predicate: NSPredicate(format: "favorite <> 'false'")
) var songs: FetchedResults<Song>
var body: some View {
NavigationView{
VStack{
List {
ForEach(songs, id:\.self){ song in
NavigationLink(destination: DetailView(song: song, isSelected: song.favorite)) {
HStack{
Text("\(song.number). ") .font(.headline) + Text(song.title ?? "No title")
}
}
}
}
}
.listStyle(InsetListStyle())
.navigationTitle("Favorite")
}
}
}
Detailed view:
struct DetailView: View {
#State var song : Song
#State var isSelected: Bool
#State var wrongNumber: Bool = false
var body: some View {
VStack{
Text(song.content!)
.padding()
Spacer()
}
.navigationBarTitle("\(song.number). \(song.title ?? "No title")", displayMode: .inline)
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
HStack{
Button(action: {
song.favorite.toggle()
PersistenceController.shared.save()
isSelected=song.favorite
}) {
Image(systemName: "heart.fill")
.foregroundColor(isSelected ? .red : .blue)
}
Button(action: {
alert()
}) {
Image(systemName: "1.magnifyingglass")
}
NavigationLink("DetailView", destination: DetailView(song: song, isSelected: isSelected))
.frame(width: 0, height: 0)
.hidden()
}
}
}
}
}
Use this NSPredicate
NSPredicate(format: "favorite = %d", false)

Programmatic Navigation Weird Behaviour with CoreData

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
}

SwiftUI handling SpriteKit scene recreation when data changes

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
}
}
}

OnAppear calls unexpectedly when Keyboard Appears in SwiftUI

I am experiencing very odd behavior in SwiftUI 2.0 and iOS14.
When the keyboard appears on the screen, the OnAppear method of other tab's view called automatically.
However, this works fine Xcode 11.7
Here is the issue in action.
Here is the code which produces the above error.
struct ContentView: View {
var body: some View {
TabView {
DemoView(screenName: "Home")
.tabItem {
Image.init(systemName: "star.fill")
Text("Home")
}
DemoView(screenName: "Result")
.tabItem {
Image.init(systemName: "star.fill")
Text("Result")
}
DemoView(screenName: "More")
.tabItem {
Image.init(systemName: "star.fill")
Text("More")
}
}
}
}
struct DemoView:View {
#State var text:String = ""
var screenName:String
var body: some View {
VStack{
Text(screenName)
.font(.title)
TextField("Buggy Keyboard Issue", text: $text)
.textFieldStyle(RoundedBorderTextFieldStyle())
Text("Issue : When keyboard appears, onAppear of other 2 tabs call automatically.")
.font(.footnote)
}
.padding()
.onAppear(perform: {
debugPrint("OnAppear of : \(screenName)")
})
}
}
This seems to be a bug of SwiftUI 2.0 but not sure.
Any help will be appreciated.
Thanks
Having the same issue myself, I think this is a bug or something like that, however I came up with a solution maybe a workaround until apple will fix it.
The thing that I did is basically I used a LazyVStack, and this seems to be working perfectly.
LazyVStack {
VStack{
Text(screenName)
.font(.title)
TextField("Buggy Keyboard Issue", text: $text)
.textFieldStyle(RoundedBorderTextFieldStyle())
Text("Issue : When keyboard appears, onAppear of other 2 tabs call automatically.")
.font(.footnote)
}
.padding()
.onAppear(perform: {
debugPrint("OnAppear of : \(screenName)")
})
}
Now the OnAppear method of other tab's view it is not called automatically when the keyboard appear.
Just implemented the following workaround:
struct ContentView: View {
var body: some View {
TabView(selection: $selectedTab) {
TabContentView(tag: 0, selectedTag: selectedTab) {
Text("Some tab content")
}
.tabItem {
Text("First tab")
}
TabContentView(tag: 0, selectedTag: selectedTab) {
Text("Another tab content")
}
.tabItem {
Text("Second tab")
}
}
}
#State private var selectedTab: Int = 0
}
private struct TabContentView<Content: View, Tag: Hashable>: View {
init(tag: Tag, selectedTag: Tag, #ViewBuilder content: #escaping () -> Content) {
self.tag = tag
self.selectedTag = selectedTag
self.content = content
}
var body: some View {
Group {
if tag == selectedTag {
content()
.frame(maxWidth: .infinity, maxHeight: .infinity)
} else {
Color.clear
}
}
.tag(tag)
}
private let tag: Tag
private let selectedTag: Tag
private let content: () -> Content
}
Not sure if it's stable enough but keyboard appearance doesn't trigger onAppear on tabs content anymore.
To avoid reloading your view try with on the TabView
.ignoresSafeArea(.keyboard, edges: .bottom)
It only works on iOS 14

How to deal with master/detail CoreData between SwiftUI views

I'm dealing with issue how to pass parameter selected in master view to CoreData predicate in detail view. I have this master view
struct ContentView: View {
#State private var selectedCountry: Country?
#State private var showSetting = false
#FetchRequest(entity: Country.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \Country.cntryName, ascending: true)]
) var countries: FetchedResults<Country>
var body: some View {
NavigationView {
VStack {
Form {
Picker("Pick a country", selection: $selectedCountry) {
ForEach(countries, id: \.self) { country in
Text(country.cntryName ?? "Error").tag(country as Country?)
}
}
if selectedCountry != nil {
Years(cntryName: selectedCountry?.cntryName! ?? "")
}
}
}
.navigationBarTitle("UNECE Data")
.navigationBarItems(trailing: Button("Settings", action: {
self.showSetting.toggle()
}))
}
.sheet(isPresented: $showSetting) {
SettingsView(showSetting: self.$showSetting)
}
}
}
where I use Picker to select country name (from CoreData entity Country and its attribute cntryName) and pass it as String value to Years view which is coded like this
struct Years: View {
var cntryName: String
#State private var selectedDataRow: Data?
#State private var result: NSFetchRequestResult
#FetchRequest(entity: Data.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \Data.dataYear, ascending: true)],
predicate: NSPredicate(format: "dataCountry == %#", "UK"), animation: .default
) var data: FetchedResults<Data>
var body: some View {
Picker("Year", selection: $selectedDataRow) {
ForEach(data, id: \.self) { dataRow in
Text(dataRow.dataYear ?? "N/A")
}
}
.pickerStyle(WheelPickerStyle())
.frame(width: CGFloat(UIScreen.main.bounds.width), height: CGFloat(100))
.clipped()
.onAppear() {
let request = NSFetchRequest<Data>(entityName: "Data")
request.sortDescriptors = [NSSortDescriptor(key: "dataYear", ascending: true)]
request.predicate = NSPredicate(format: "dataCountry == %#", self.cntryName)
do {
self.result = try context.fetch(request) as! NSFetchRequestResult
print(self.result)
} catch let error {
print(error)
}
}
}
}
It works fine with #FetchRequest and FetchedResults stored in var data but I'm wondering how to build predicate here based on passed country name. To overcome this I considered to use onAppear section and classic NSFetchRequest and NSFetchRequestResult which causes compiler error "'Years.Type' is not convertible to '(String, NSFetchRequestResult, FetchRequest) -> Years'" in the line
Years(cntryName: selectedCountry?.cntryName! ?? "")
of ContentView struct. Error disappear if I comment the line
#State private var result: NSFetchRequestResult
in Years struct but it obviously causes another error. So I'm lost in circle. What`s recommended practice here, please?
Thanks.
Finally I found the way thanks to this post SwiftUI use relationship predicate with struct parameter in FetchRequest
struct Years: View {
var request: FetchRequest<Data>
var result: FetchedResults<Data> {
request.wrappedValue
}
#State private var selectedDataRow: Data?
init(cntryName: String) {
self.request = FetchRequest(entity: Data.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \Data.dataYear, ascending: true)],
predicate: NSPredicate(format: "dataCountry == %#", cntryName), animation: .default)
}
var body: some View {
VStack {
Picker("Year", selection: $selectedDataRow) {
ForEach(result, id: \.self) { dataRow in
Text(dataRow.dataYear ?? "N/A").tag(dataRow as Data?)
}
}
.pickerStyle(WheelPickerStyle())
.frame(width: CGFloat(UIScreen.main.bounds.width), height: CGFloat(100))
.clipped()
VStack(alignment: .leading, spacing: 10) {
HStack {
Text("Total polutation: ")
.alignmentGuide(.leading) { dimension in
10
}
if selectedDataRow != nil {
Text(String(describing: selectedDataRow!.dataTotalPopulation))
} else {
Text("N/A")
}
}
}}
}
}

Resources