Is viewContext different between Swift and SwiftUI - swift

EDIT: I've added a rewording of my question at the bottom.
So I have an app that I've been working on for a long time. I used it to teach myself Xcode / Swift and now that I have a Mac again, I'm trying to learn SwiftUI as well by remaking the app completely as a new project.
I have an Entity in CoreData that only ever contains 1 record, which has all of the attributes of the settings I'm tracking.
Before, I was able to make an extension to my Entity, Ent_Settings that would allow me to always get that specific record back.
class var returnSettings: Ent_Settings {
//this is inside of extension Ent_Settings
let moc = PersistenceController.shared.container.viewContext
var settings: Ent_Settings?
let fetchRequest: NSFetchRequest<Ent_Settings> = Ent_Settings.fetchRequest()
do {
let results = try moc.fetch(fetchRequest)
if results.count == 0 {
Ent_Settings.createSettingsEntity()
do {
let results = try moc.fetch(fetchRequest)
settings = results.first!
} catch {
error.tryError(tryMessage: "Failed performing a fetch to get the Settings object after it was created.", loc: "Ent_Settings Extension")
}
} else {
settings = results.first!
}
} catch {
error.tryError(tryMessage: "Fetching Settings Object", loc: "Ent_Settings Extension")
}
return settings!
}
It took me a while to figure out how to get the moc now that there is no AppDelegate, but you can see I figured out in the code and that seems to work great.
However, when I go to a View that I made in SwiftUI, I'm getting weird results.
In the Simulator, I can run my app and see the changes. Leave the view and come back and see the changes are saved. However, if I rebuild and run the app it loses all data. So it seems it's not remembering things after all. Basically, it's volatile memory instead of persistent memory. But if I put another function in the extension of Ent_Settings to update that attribute and then build and run again, it will persistently save.
I'm wondering if it has something to do with how the context is managed, perhaps it's not seeing the changes since it is a different "listener"?
Here is what I'm doing to make the changes in the View. (I tried removing the extra stuff that just made it harder to see)
import SwiftUI
struct Settings_CheckedView: View {
#Environment(\.managedObjectContext) private var viewContext
#State private var picker1 = 0
var body: some View {
Form {
Section(header: Text("..."),
footer: Text("...")) {
VStack {
HStack { ... }
Picker("", selection: $picker1) {
Text("Off").tag(0)
Text("Immediately").tag(1)
Text("Delay").tag(2)
}
.pickerStyle(SegmentedPickerStyle())
.onChange(of: picker1, perform: { value in
settings.autoDeleteSegment = Int16(value)
viewContext.trySave("Updating auto-delete segment value in Settings",
loc: "Ent_Settings Extension")
})
}
}
...
...
.onAppear(perform: {
settings = Ent_Settings.returnSettings
self.picker1 = Int(settings.autoDeleteSegment)
self.picker2 = Int(settings.deleteDelaySegment)
})
}
}
And here is the code that I'm using for the trySave() function I have on the viewContext
extension NSManagedObjectContext {
func trySave(_ tryMessage: String, loc: String) {
let context = self
if context.hasChanges {
do {
try self.save()
} catch {
print("Attempted: \(tryMessage) -in \(loc)")
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
}
This code doesn't seem to actually save anything to the Database.
I also tried adding this code into Settings_CheckedView to see if that would make a difference, but I got even weird results.
#FetchRequest(sortDescriptors:[])
private var settingsResult: FetchedResults<Ent_Settings>
But that returns zero results even though I know the Ent_Settings has 1 record. So that makes me wonder if I am actually creating two databases with the way I'm doing it.
I'm really new to SwiftUI and CoreData seems to be one of those things that is hard to get information about. Hoping someone can point out where I'm going wrong. I'd love to be able to make CoreData changes in both Swift and SwiftUI.
Thank you for any help you can give and let me know if you need anything else added in.
EDITED: To try to reword question
So ultimately, I'm wanting to do some stuff with CoreData inside of the extension for that entity. My database has a lot of aspects to it, but I also made an entity in there to store settings information and since that one isn't related to anything else, I figured that would be a good starting point in learning SwiftUI. I have everything working in UIKit, but I'm specifically trying to learn SwiftUI as well.
From how I understand SwiftUI, it is meant to replace the storyboard and make the lifecycle stuff much easier. So classes I have that deal with CoreData should still do that external to SwiftUI.
As you can see above in my Settings_CheckedView view above, I'm referencing that single settings record from an extension in Ent_Settings. Basically, everything inside of that extension takes care of returning that single record of Settings, checking if that record exists and creating it if it doesn't (first time running app basically)
Ideally, I'd like to keep the functionality inside the extension of Ent_Settings, but my problem is I can't get the correct instance of the moc to persistently save it.
#main
struct MyApp: App {
let persistenceController = PersistenceController.shared
var body: some Scene {
WindowGroup {
MainMenuView().onAppear(perform: {
if Ent_Lists.hasAnyList(persistenceController.container.viewContext) == false {
Ent_Lists.createAnyList(persistenceController.container.viewContext)
}
if Ent_Section.hasBlankSection(persistenceController.container.viewContext) == false {
Ent_Section.createBlankSection(persistenceController.container.viewContext)
}
//self.settings = Ent_Settings.returnSettings(persistenceController.container.viewContext)
})
//ContentView()
//.environment(\.managedObjectContext, persistenceController.container.viewContext)
}
}
}
I'm pretty certain that let persistenceController = PersistenceController.shared is initializing the persistent controller used throughout the app. Either creating it or retrieving it.
So, I tried this first inside of Ent_Settings:
let moc = PersistenceController.shared.container.viewContext
but I think that this might be creating a new PersistenceController outside the one made inside of MyApp
I also tried let moc = EZ_Lists_SwiftUI.PersistenceController.shared.container.viewContext but I'm pretty sure that also makes a new instance given I can only access the upper case PersistenceController and not the lower case one.
I also tried just passing the moc from the view like this: class func createSettingsEntity(_ moc: NSManagedObjectContext) { but I get an error about the moc being nil, so I'm guessing the moc can't be sent by reference from a Struct.
Thread 1: "+entityForName: nil is not a legal NSPersistentStoreCoordinator for searching for entity name 'Ent_Settings'"
And just to be clear, I'm adding this to the view: #Environment(\.managedObjectContext) private var viewContext adding this #State property to the View: #State private var settings: Ent_Settings! and setting it within .onAppear with this: self.settings = Ent_Settings.returnSettings(self.viewContext)
So what I'm really looking for is how I access the moc from within that extension of Ent_Settings. In my app that was purely UIKit and Storyboards I used this: let moc = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext but as there is no longer an AppDelegate, I cannot use that anymore. What replaces this to get moc when you are doing SwiftUI but working outside of the Views, like in a class or something like I am? Thank you. Hopefully this extra information helps explain my issue.

If you are only using coredata to store settings, I would look into UserDefaults and if that matches your use case.
https://developer.apple.com/documentation/foundation/userdefaults
If you for some reason need to use CoreData, I would recommend enabling CloudKit, as CloudKit will allow you to actually view what data is saved via a web console.
Also, SwiftUI does not create an AppDelegate for you, you are still able to just create your own AppDelegate.swift file as you would with UIKit.
Just define the #UIApplicationDelegateAdaptor to your custom AppDelegate.swift file in your top most struct (the one with #main) before the struct declaration.
#main
struct MainAppView: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

Okay, a bit of egg on my face. I finally figured out why I was having so much trouble. When I was making my app, I commented out everything related to the apple provided ContextView and commented out too much.
//ContentView()
//.environment(\.managedObjectContext, persistenceController.container.viewContext)
I lost the part where the environment object was getting setup. After putting that back in I'm able to save to the moc now.
In my Entity class extension I'm accessing the moc this way:
let moc = MyApp.PersistenceController.shared.container.viewContext
Thank you for everyone who looked and tried to help. Turns out I was shooting myself in the foot.

Related

SwiftUI FileDocument using Class instead of Struct

I've started a document-base macOS app in SwiftUI and am using a FileDocument (not a Reference FileDocument) as the type of document. In every tutorial I've seen, even in Apple's own WWDC video discussing it (https://developer.apple.com/wwdc20/10039), a struct is always used to define the FileDocument.
My question is: is there an issue with using a class in the struct defining the document. Doing so doesn't result in any Xcode warnings but I wanted to be sure I'm not creating any issues for my app before going down this path.
Below is some example code for what I'm talking about: declaring TestProjectData as a class for use within the DocumentDataAsClassInsteadOfStructDocument - struct as a FileDocument?
public class TestProjectData: Codable{
var anotherString: String
init(){
anotherString = "Hello world!"
}
}
struct DocumentDataAsClassInsteadOfStructDocument: FileDocument, Codable {
var project: TestProjectData
init() {
project = TestProjectData()
}
static var readableContentTypes: [UTType] { [.exampleText] }
init(configuration: ReadConfiguration) throws {
guard let data = configuration.file.regularFileContents,
let _ = String(data: data, encoding: .utf8)
else {
throw CocoaError(.fileReadCorruptFile)
}
let fileContents = try JSONDecoder().decode(Self.self, from: data)
self = fileContents
}
func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
let data = try JSONEncoder().encode(self)
return .init(regularFileWithContents: data)
}
}
It appears that yes, we need to use a struct for documents. See this post for a thorough example of the issues you can run into if you use a class instead of a struct.
SwiftUI View doesn't update
SwiftUI's architecture is all about using value types for speed and consistency. E.g. on a state change we create all the View structs and then SwiftUI diffs and uses the result to init/update/deinit UIView objects.
I believe the same thing happens with FileDocument. The struct is diffed on a change and the difference is used to init/update/deinit a UIDocument object.
If you init object vars inside these structs then basically it is a memory leak because a new object will be init every time the struct is created which is every time something changes. Also chances are you'll end up using the wrong instance of the object because there will be so many. You can see this type of problem surface when blocks are used inside body, the callback usually happens on an older version of the View struct, which isn't a problem when everything is value types but it is a big problem if referencing old objects.
Try to stick to value types in SwiftUI if you can, if you use objects you'll run into all kinds of headaches. And I don't think ReferenceFileDocument even works yet - I seem to remember it needs some kind of undo manager workaround.

SwiftUI #Published and main thread

Could someone explain why I get this warning: Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates.
I'm know that if I wrap the changes in DispatchQueue.main.async the problem goes away. Why does it happen with some view modals and not others? I thought that since the variable has #Published it's automatically a publisher on main thread?
class VM: ObservableObject {
private let contactsRepo = ContactsCollection()
#Published var mutuals: [String]?
func fetch() {
contactsRepo.findMutuals(uid: uid, otherUid: other_uid, limit: 4) { [weak self] mutuals in
guard let self = self else { return }
if mutuals != nil {
self.mutualsWithHost = mutuals // warning...
} else {
self.mutualsWithHost = []
}
}
}
}
Evidently, contactsRepo.findMutuals can call its completion handler on a background thread. You need to ward that off by getting back onto the main thread.
The #Published property wrapper creates a publisher of the declared type, nothing more. The documentation may be able to provide further clarity.
As for it happening on some viewModels and not others, we wouldn't be able to tell here as we don't have the code. However it's always best practice to use DispatchQueue.main.async block or .receive(on: DispatchQueue.main) modifier for combine as you've already figured out when updating your UI.
The chances are your other viewModel is already using the main thread or the properties on the viewModel aren't being used to update the UI, again without the code we'll never be sure.

SwiftUI with complex MVVM (Repository + Nested ObservedObject)

Explanation
I am still in the process of learning to utilize SwiftUI patterns in the most optimal way. But most SwiftUI MVVM implementation examples I find are very simplistic. They usually have one database class and then 1-2 viewmodels that take data from there and then you have views.
In my app, I have a SQLite DB, Firebase and different areas of content. So I have a few separate model-vm-view paths. In the Android equivalent of my app, I used a pattern like this:
View - ViewModel - Repository - Database
This way I can separate DB logic like all SQL queries in the repository classes and have the VM handle only view related logic. So the whole thing looks something like this:
In Android this works fine, because I just pass through the LiveData object to the view. But when trying this pattern in SwiftUI, I kind of hit a wall:
It doesn't work / I don't know how to correctly connect the Published objects of both
The idea of "chaining" or nesting ObservableObjects seems to be frowned upon:
This article about Nested Observable Objects in SwiftUI:
I’ve seen this pattern described as “nested observable objects”, and it’s a subtle quirk of SwiftUI and how the Combine ObservableObject protocol works that can be surprising. You can work around this, and get your view updating with some tweaks to the top level object, but I’m not sure that I’d suggest this as a good practice. When you hit this pattern, it’s a good time to step back and look at the bigger picture.
So it seems like one is being pushed towards using the simpler pattern of:
View - ViewModel - Database Repository
Without the repository in-between. But this seems annoying to me, it would make my viewmodel classes bloated and would mix UI/business code with SQL queries.
My Code
So this is a simplified version of my code to demonstrate the problem:
Repository:
class SA_Repository: ObservableObject {
#Published var selfAffirmations: [SelfAffirmation]?
private var dbQueue: DatabaseQueue?
init() {
do {
dbQueue = Database.sharedInstance.dbQueue
fetchSelfAffirmations()
// Etc. other SQL code
} catch {
print(error.localizedDescription)
}
}
private func fetchSelfAffirmations() {
let saObservation = ValueObservation.tracking { db in
try SelfAffirmation.fetchAll(db)
}
if let unwrappedDbQueue = dbQueue {
let _ = saObservation.start(
in: unwrappedDbQueue,
scheduling: .immediate,
onError: {error in print(error.localizedDescription)},
onChange: {selfAffirmations in
print("change in SA table noticed")
self.selfAffirmations = selfAffirmations
})
}
}
public func updateSA() {...}
public func insertSA() {...}
// Etc.
}
ViewModel:
class SA_ViewModel: ObservableObject {
#ObservedObject private var saRepository = SA_Repository()
#Published var selfAffirmations: [SelfAffirmation] = []
init() {
selfAffirmations = saRepository.selfAffirmations ?? []
}
public func updateSA() {...}
public func insertSA() {...}
// + all the Firebase stuff later on
}
View:
struct SA_View: View {
#ObservedObject var saViewModel = SA_ViewModel()
var body: some View {
NavigationView {
List(saViewModel.selfAffirmations, id: \.id) { selfAffirmation in
SA_ListitemView(content: selfAffirmation.content,
editedValueCallback: { newString in
saViewModel.updateSA(id: selfAffirmation.id, newContent: newString)
})
}
}
}
}
Attempts
Obviously the way I did it here is wrong, because it clones the data from repo to vm once with selfAffirmations = saRepository.selfAffirmations ?? [] but then it never updates when I edit the entries from the view, only on app restart.
I tried $selfAffirmations = saRepository.$selfAffirmations to just transfer the binding. But the repo one is an optional, so I'd need to make the vm selfAffirmations an optional too, which would then mean handling unnecessary logic in the view code. And not sure if it would even work at all.
I tried to do it manually with Combine but this way seemed to not be recommended and fragile. Plus it also didn't work:
selfAffirmations = saRepository.selfAffirmations ?? []
cancellable = saRepository.$selfAffirmations.sink(
receiveValue: { [weak self] repoSelfAffirmations in
self?.selfAffirmations = repoSelfAffirmations ?? []
}
)
Question
Overall I would just need some way to pass through the data from the repo to the view, but have the vm be in the middle as a separator. I read about the PassthroughSubject in Combine, which sounds like it would be fitting, but I'm not sure if I am just misunderstanding some concepts here.
Now I am not sure if my architecture concepts are wrong/unfitting, or if I just don't understand enough about Combine publishers yet to make this work.
Any advice would be appreciated.
After getting some input from the comments, I figured out a clean way.
The problem for me was understanding how to make a property of a class publish its values. Because the comments suggested that property wrappers like #ObservedObject was a frontend/SwiftUI only thing, making me assume that everything related was limited to that too, like #Published.
So I was looking for something like selfAffirmations.makePublisher {...}, something that would make my property a subscribable value emitter. I found that arrays naturally come with a .publisher property, but this one seems to only emit the values once and never again.
Eventually I figured out that #Published can be used without #ObservableObject and still work properly! It turns any property into a published property.
So now my setup looks like this:
Repository (using GRDB.swift btw):
class SA_Repository {
private var dbQueue: DatabaseQueue?
#Published var selfAffirmations: [SelfAffirmation]?
// Set of cancellables so they live as long as needed and get deinitialiazed with the class end
var subscriptions = Array<DatabaseCancellable>()
init() {
dbQueue = Database.sharedInstance.dbQueue
fetchSelfAffirmations()
}
private func fetchSelfAffirmations() {
// DB code....
}
}
And viewmodel:
class SA_ViewModel: ObservableObject {
private var saRepository = SA_Repository()
#Published var selfAffirmations: [SelfAffirmation] = []
// Set of cancellables to keep them running
var subscriptions = Set<AnyCancellable>()
init() {
saRepository.$selfAffirmations
.sink{ [weak self] repoSelfAffirmations in
self?.selfAffirmations = repoSelfAffirmations ?? []
}
.store(in: &subscriptions)
}
}

What's best practice for programmatic movement a NavigationView in SwiftUI

I'm working on an app that needs to open on the users last used view even if the app is completly killed by the user or ios.
As a result I'm holding last view used in UserDefaults and automatically moving the user through each view in the stack until they reach their destination.
The code on each view is as follows:
#Binding var redirectionID: Int
VStack() {
List {
NavigationLink(destination: testView(data: data, moc: moc), tag: data.id, selection:
$redirectionId) {
DataRow(data: data)
}
}
}.onAppear() {
redirectionID = userData.lastActiveView
}
Is there a better / standard way to achieve this? This works reasonably on iOS 14.* but doesn't work very well on iOS 13.* On iOS 13.* The redirection regularly doesnt reach its destination page and non of the preceeding views in the stack seem to be created. Pressing back etc results in a crash.
Any help / advice would be greatly appreciated.
This sounds like the perfect use of if SceneStorage
"You use SceneStorage when you need automatic state restoration of the value. SceneStorage works very similar to State, except its initial value is restored by the system if it was previously saved, and the value is· shared with other SceneStorage variables in the same scene."
#SceneStorage("ContentView.selectedProduct") private var selectedProduct: String?
#SceneStorage("DetailView.selectedTab") private var selectedTab = Tabs.detail
It is only available in iOS 14+ though so something manual would have to be implemented. Maybe something in CoreData. An object that would have variables for each important state variable. It would work like an ObservedObject ViewModel with persistence.
Also. you can try...
"An NSUserActivity object captures the app’s state at the current moment in time. For example, include information about the data the app is currently displaying. The system saves the provided object and returns it to the app the next time it launches. The sample creates a new NSUserActivity object when the user closes the app or the app enters the background."
Here is some sample code that summarizes how to bring it all together. It isn't a minimum reproducible example because it is a part of the larger project called "Restoring Your App's State with SwiftUI" from Apple. But it gives a pretty good picture on how to implement it.
struct ContentView: View {
// The data model for storing all the products.
#EnvironmentObject var productsModel: ProductsModel
// Used for detecting when this scene is backgrounded and isn't currently visible.
#Environment(\.scenePhase) private var scenePhase
// The currently selected product, if any.
#SceneStorage("ContentView.selectedProduct") private var selectedProduct: String?
let columns = Array(repeating: GridItem(.adaptive(minimum: 94, maximum: 120)), count: 3)
var body: some View {
NavigationView {
ScrollView {
LazyVGrid(columns: columns) {
ForEach(productsModel.products) { product in
NavigationLink(destination: DetailView(product: product, selectedProductID: $selectedProduct),
tag: product.id.uuidString,
selection: $selectedProduct) {
StackItemView(itemName: product.name, imageName: product.imageName)
}
.padding(8)
.buttonStyle(PlainButtonStyle())
.onDrag {
/** Register the product user activity as part of the drag provider which
will create a new scene when dropped to the left or right of the iPad screen.
*/
let userActivity = NSUserActivity(activityType: DetailView.productUserActivityType)
let localizedString = NSLocalizedString("DroppedProductTitle", comment: "Activity title with product name")
userActivity.title = String(format: localizedString, product.name)
userActivity.targetContentIdentifier = product.id.uuidString
try? userActivity.setTypedPayload(product)
return NSItemProvider(object: userActivity)
}
}
}
.padding()
}
.navigationTitle("ProductsTitle")
}
.navigationViewStyle(StackNavigationViewStyle())
.onContinueUserActivity(DetailView.productUserActivityType) { userActivity in
if let product = try? userActivity.typedPayload(Product.self) {
selectedProduct = product.id.uuidString
}
}
.onChange(of: scenePhase) { newScenePhase in
if newScenePhase == .background {
// Make sure to save any unsaved changes to the products model.
productsModel.save()
}
}
}
}

SwiftUI not being updated with manual publish

I have a class, a “clock face” with regular updates; it should display an array of metrics that change over time.
Because I’d like the clock to also be displayed in a widget, I’ve found that I had to put the class into a framework (perhaps there’s another way, but I’m too far down the road now). This appears to have caused a problem with SwiftUI and observable objects.
In my View I have:
#ObservedObject var clockFace: myClock
In the clock face I have:
class myClock: ObservableObject, Identifiable {
var id: Int
#Publish public var metric:[metricObject] = []
....
// at some point the array is mutated and the display updates
}
I don’t know if Identifiable is needed but it’s doesn’t make any difference to the outcome. The public is demanded by the compiler, but it’s always been like that anyway.
With these lines I get a runtime error as the app starts:
objc[31175] no class for metaclass
So I took off the #Published and changed to a manual update:
public var metric:[metricObject] = [] {
didSet {
self.objectWillChange.send()`
}
}
And now I get a display and by setting a breakpoint I can see the send() is being called at regular intervals. But the display won’t update unless I add/remove from the array. I’m guessing the computed variables (which make up the bulk of the metricObject change isn’t being seen by SwiftUI. I’ve subsequently tried adding a “dummy” Int to the myClock class and setting that to a random value to trying to trigger a manual refresh via a send() on it’s didSet with no luck.
So how can I force a periodic redraw of the display?
What is MetricObject and can you make it a struct so you get Equatable for free?
When I do this with an Int it works:
class PeriodicUpdater: ObservableObject {
#Published var time = 0
var subscriptions = Set<AnyCancellable>()
init() {
Timer
.publish(every: 1, on: .main, in: .default)
.autoconnect()
.sink(receiveValue: { _ in
self.time = self.time + 1
})
.store(in: &subscriptions)
}
}
struct ContentView: View {
#ObservedObject var updater = PeriodicUpdater()
var body: some View {
Text("\(self.updater.time)")
}
}
So it's taken a while but I've finally got it working. The problem seemed to be two-fold.
I had a class defined in my framework which controls the SwiftUI file. This class is sub-classed in both the main app and the widget.
Firstly I couldn't use #Published in the main class within the framework. That seemed to cause the error:
objc[31175] no class for metaclass
So I used #JoshHomman's idea of an iVar that's periodically updated but that didn't quite work for me. With my SwiftUI file, I had:
struct FRMWRKShape: Shape {
func drawShape(in rect: CGRect) -> Path {
// draw and return a shape
}
}
struct ContentView: View {
#ObservedObject var updater = PeriodicUpdater()
var body: some View {
FRMWRKShape()
//....
FRMWRKShape() //slightly different parameters are passed in
}
}
The ContentView was executed every second as I wanted, however the FRMWRKShape code was called but not executed(?!) - except on first starting up - so the view doesn't update. When I changed to something far less D.R.Y. such as:
struct ContentView: View {
#ObservedObject var updater = PeriodicUpdater()
var body: some View {
Path { path in
// same code as was in FRMWRKShape()
}
//....
Path { path in
// same code as was in FRMWRKShape()
// but slightly different parameters
}
}
}
Magically, the View was updated as I wanted it to be. I don't know if this is expected behaviour, perhaps someone can say whether I should file a Radar....