Sharing Data with AppGroup - swift

I want to share one variable from my UIKit File to my Widget Extension created with SwiftUI.
I followed this here. Please look at the answer from J Arango.
But i dont understand the last part there.
I have to use import MySharedObjects.
So I did this:
import MySharedObject
struct testing {
let mySharedObject = MySharedObject(name: "My Name", lastName: "My Last Name")
do {
let data = try JSONEncoder().encode(mySharedObject)
/// Make sure to use your "App Group" container suite name when saving and retrieving the object from UserDefaults
let container = UserDefaults(suiteName:"group.com.widgetTest.widgetContainer")
container?.setValue(data, forKey: "sharedObject")
/// Used to let the widget extension to reload the timeline
WidgetCenter.shared.reloadAllTimelines()
} catch {
print("Unable to encode WidgetDay: \(error.localizedDescription)")
}
}
But I get the following errors.
Extra argument at position #1, #2 in call
Missing argument for parameter from call
insert from : <#Decoder#>
expected declaration where I use the do part.

Save data to UserDefaults in your main App:
UserDefaults(suiteName: <your_app_group>)!.set("test", forKey: "test")
Read data from UserDefaults in your Widget:
let testStr = UserDefaults(suiteName: <your_app_group>)!.string(forKey: "test")
If you want to save other types see:
How can I use UserDefaults in Swift?

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.

Can I Fetch Data from API while using a variable to change the url target? [SwiftUI]

I have a question. I am trying to make a API call to fetch data for a specific item. To do so, I just need to modify 1 parameter of the URL that is making the call.
This is the call on the item page to fetch the data. I feel like I should be able to pass the item # like this. But im having trouble passing it correctly.
struct ProductPage: View {
#ObservedObject var relatedsectionNetwork = RelatedSectionAPI()
...
...
.onAppear() {
relatedsectionNetwork.fetchData(prID: "1824085")
print("relatedSection Loaded")
}
This is the actual class and function that I am using to make the call. You can see what my thought process was here: create a var that could be used to change the prID within the URL.
class RelatedSectionAPI: ObservableObject {
var prID : String = ""
func fetchData() {
print("fetchData3 -start 􀋂")
if let url = URL(string:"https://www.*****.com/****/******?***=***=&***=&***=***&pr_id=\(prID)&***") {
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { data, response, error in
if error == nil {
let decoder = JSONDecoder()
I cut this part short to not confuse anyone.
If any one can please help me out, or point me in the right direction, it would be greatly appreciated!
Thank you
Add the parameter to the function call func fetchData(prID: String = ""). BTW you should use #StateObject vs #ObservedObject when you initialize an ObservableObject in a View

Swift Core Data - storing an array of custom types

I am trying to create a data model which mirrors a view model that I use to handle an API call, the idea being that I will be able to store all the necessary data in core data and then access it when the user is offline, effectively giving the app offline functionality.
However, there is one entity which I need to store which is an array of a custom class that I have in the app:
[OrderSheet]
This is a struct defined as follows:
struct OrderSheet {
let order: SheetClass // codable class
let sheet: Sheet // codable struct
init(fuelOrder: SheetClass, sheet: Sheet) {
self.order = order
self.sheet = sheet
}
}
How can I create an entity that would be capable of storing the above?
One simple way would be to have an entity that holds only one Data field (Binary Data in xcdatamodel settings), which would be the orderSheet itself.
Before going with this solution, I'd like to mention that, one down side of this approach is; if later in the future, any of the models inside OrderSheet changes, you won't be able to retrieve already stored objects as conversion will fail. One way of overcoming this issue would be declaring everything inside OrderSheet and sub models as Optional. But if it is not so crucial, meaning, if not being able to read old models on user's device after an app update is okay, (maybe they will be replaced with new networking call) then you can go with not marking properties as optional either.
Lets imagine you create an entity named OrderSheetManaged with one field as I mentioned like following:
import Foundation
import CoreData
#objc(Entity)
public class OrderSheetManaged: NSManagedObject {
}
extension OrderSheetManaged {
#nonobjc public class func fetchRequest() -> NSFetchRequest<OrderSheetManaged> {
return NSFetchRequest<OrderSheetManaged>(entityName: "OrderSheetManaged")
}
#NSManaged public var orderSheet: Data?
}
I will write some code for NSManagedObjectContext, which is not directly related to your question, you should make research on how to initialise a core data stack and a managed context from it if you are not familiar with that since it is crucial.
I also do some force unwrapping for simplicity, make sure to not force unwrap where not needed in production code.
Now whenever you have an actual OrderSheet object (it is orderSheet in my example below), that was parsed before, you are going to convert it to Data and persist it with new Core Data model as following:
// unrelated to question, it should already be initialised from core data stack, I just init with
// concurrency type to make compiler happy, dont do this before further research.
let yourManagedContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
let entityDescription = NSEntityDescription.entity(forEntityName: "OrderSheetManaged",
in: yourManagedContext)
let dataForCoreData = try! JSONEncoder().encode(orderSheet)
let managedOrderSheet = NSManagedObject(entity: entityDescription!, insertInto: yourManagedContext)
managedOrderSheet.setValue(dataForCoreData, forKey: "orderSheet")
Now we have persisted your object as Data inside a wrapper core data model (OrderSheetManaged)
Let's see now how we can fetch these models from our core data and convert it back to OrderSheet model:
// when you fetch it
var orderSheets = [OrderSheet]()
let request = NSFetchRequest<NSFetchRequestResult>(entityName: "OrderSheetManaged")
var coreDataObjects: [NSManagedObject]!
do {
coreDataObjects = try yourManagedContext.fetch(request) as? [NSManagedObject]
for coreDataObject in coreDataObjects {
if let orderSheetData = coreDataObject.value(forKey: "orderSheet") as? Data {
let orderSheet = try! JSONDecoder().decode(OrderSheet.self, from: orderSheetData)
orderSheets.append(orderSheet)
}
}
} catch {
error
}
Now you will have all your stored order sheets inside orderSheets array.
You can also write some utility methods to easily modify core data models by converting orderSheet data inside of them to OrderSheet first and then again converting it back to Data after modifying and then persisting again with setValue.

SwiftUI: List does not update automatically after deleting all Core Data Entity entries

I know SwiftUI uses state-driven rendering. So I was assuming, when I delete Core Data Entity entries, that my List with Core Data elements gets refreshed immediately.
I use this code, which gets my Entity cleaned succesfully:
func deleteAll()
{
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = ToDoItem.fetchRequest()
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
let persistentContainer = (UIApplication.shared.delegate as! AppDelegate).persistentContainer
do {
try persistentContainer.viewContext.execute(deleteRequest)
} catch let error as NSError {
print(error)
}
}
To get the List in my View visually empty I have to leave the View afterwards (for example with " self.presentationMode.wrappedValue.dismiss()") and open it again. As if the values are still stored somewhere in the memory or something.
This is of course not user-friendly and I am sure I just oversee something that refreshes the List immediately.
Maybe someone can help.
The reason is that execute (as described in details below - pay attention on first sentence) does not affect managed objects context, so all fetched objects remains in context and UI represents what is really presented by context.
So in general, after this bulk operation you need to inform back to that code (not provided here) force sync and refetch everything.
API interface declaration
// Method to pass a request to the store without affecting the contents of the managed object context.
// Will return an NSPersistentStoreResult which may contain additional information about the result of the action
// (ie a batch update result may contain the object IDs of the objects that were modified during the update).
// A request may succeed in some stores and fail in others. In this case, the error will contain information
// about each individual store failure.
// Will always reject NSSaveChangesRequests.
#available(iOS 8.0, *)
open func execute(_ request: NSPersistentStoreRequest) throws -> NSPersistentStoreResult
For example it might be the following approach (scratchy)
// somewhere in View declaration
#State private var refreshingID = UUID()
...
// somewhere in presenting fetch results
ForEach(fetchedResults) { item in
...
}.id(refreshingID) // < unique id of fetched results
...
// somewhere in bulk delete
try context.save() // < better to save everything pending
try context.execute(deleteRequest)
context.reset() // < reset context
self.refreshingID = UUID() // < force refresh
No need to force a refresh, this is IMO not a clean solution.
As you correctly mentioned in your question, there are still elements in memory. The solution is to update your in-memory objects after the execution with mergeChanges.
This blog post explains the solution in detail under "Updating in-memory objects".
There, the author provides an extension to NSBatchDeleteRequest as follows
extension NSManagedObjectContext {
/// Executes the given `NSBatchDeleteRequest` and directly merges the changes to bring the given managed object context up to date.
///
/// - Parameter batchDeleteRequest: The `NSBatchDeleteRequest` to execute.
/// - Throws: An error if anything went wrong executing the batch deletion.
public func executeAndMergeChanges(using batchDeleteRequest: NSBatchDeleteRequest) throws {
batchDeleteRequest.resultType = .resultTypeObjectIDs
let result = try execute(batchDeleteRequest) as? NSBatchDeleteResult
let changes: [AnyHashable: Any] = [NSDeletedObjectsKey: result?.result as? [NSManagedObjectID] ?? []]
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [self])
}
}
Here is an update to your code on how to call it:
func deleteAll() {
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = ToDoItem.fetchRequest()
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
let persistentContainer = (UIApplication.shared.delegate as! AppDelegate).persistentContainer
do {
try persistentContainer.viewContext.executeAndMergeChanges(deleteRequest)
} catch let error as NSError {
print(error)
}
}
Some more info also here under this link: Core Data NSBatchDeleteRequest appears to leave objects in context.

Subclassing UserDefaults

tldr; why do we always use UserDefaults.standard instead of subclassing UserDefaults to make something that more precisely fits our needs?
Has anyone out there subclassed UserDefaults before? Or is that considered bad practice?
Say, for example, that we make a ColorDefaults subclass of UserDefaults. When the app, the ColorDefaults object is instantiated, and that object loads all its own data. And the loaded data can then by sent to an appropriate object via delegation, or made universally available via a singleton.
My running theory is that UserDefaults is only meant to store relatively amounts of data, so having to use a singleton enforces that idea.
Bottom line: do we use UserDefaults.standard because:
subclassing is frowned upon
we're supposed to avoid saving too much data to UserDefaults in general
there's just not much value in subclassing anyway?
pretty much anything else.
Your ColorDefaults should not be a subclass of UserDefaults. It should be a plain struct or class with computed properties that are backed by UserDefaults.
Here is an example using static properties but you could refactor this to use a singleton class instead.
struct ColorDefaults {
static var someDefault: String {
get {
return UserDefaults.standard.string(forKey: "someKey") ?? "some initial value"
}
set {
UserDefaults.standard.set(newValue, forKey: "someKey")
}
}
}
let someVal = ColorDefaults.someDefault // read
ColorDefaults.someDefault = "hello" // write
This would also be useful if one of your defaults was more complicated and needed to be encoded/decoded for UserDefaults. The logic goes in here and not all over your app.
Note that such a class should only be used to store small bits of preferences, not full blown app data.
User defaults are a system of storage on file. There is little sense in subclassing unless you want to change some of its logic. But you can create multiple suits like UserDefaults(suiteName: String). What do you expect you would do with subclassing? You could simply just globally define let myDefaults = UserDefaults(suiteName: String) and use it anywhere. I guess you could use methods like
class MyDefaults: UserDefaults {
func saveName(_ name: String) {
setValue(name, forKey: "name_key")
}
}
But then again it might make more sense to just create an extension
extension UserDefaults {
func saveName(_ name: String) {
setValue(name, forKey: "name_key")
}
}
Or make it a bit more complex:
extension UserDefaults {
struct User {
static let defaults = UserDefaults(suiteName: "User")
static func saveName(_ name: String) {
defaults.setValue(name, forKey: "name")
}
}
struct General {
static let defaults = UserDefaults.standard
static func saveLastOpened(date: Date) {
defaults.setValue(date, forKey: "last_opened")
}
}
}
But all of these have one fatal flow: Now you are dependent on using user defaults within the app. At some point you may find the need to rather save these data in some other form like a local JSON file synced with iCloud. I guess UserDefaults.User could be modified to do so but would be very ugly. What we want is not UserDefaults.User.saveName("My name") but User.saveName("My name"). From the interface perspective we do not care where this user name is saved and if a new system is introduced to save these data we don't want the change in interface.
In other words, imagine you are using UserDefaults.User.saveName on 100 places in your application and now want to use another system for saving user name. You will now need to change your code on 100 places to use AnotherSystem.User.saveName while if you simply use User.saveName the interface is still valid.
So the bottom line is there is no sense in (extensively) modifying, extending or subclassing UserDefaults because it is better creating a system that wraps UserDefaults and may later be changed to any other system.
Seems you are looking for something like this
class ColorDefaults : NSObject
{
/// Save Data
class func saveDataInDefaultForKey(_ key: String, _ data: Any){
UserDefaults.standard.set(data, forKey: key)
}
/// Retrieve data
class func retrieveDataFromDefaultsWithKey(_ key: String) -> Any {
return UserDefaults.standard.value(forKey: key) as Any
}
}
Save and get data:
/// Save Data
ColorDefaults.saveDataInDefaultForKey("myArray", myArray)
ColorDefaults.saveDataInDefaultForKey("myString", myString)
/// Get Data
if let valueString = ColorDefaults.retrieveDataFromDefaultsWithKey("myString") as? String {
print("Saved Value String: \(valueString)")
}
else {
print("Error retrieving myString")
}
if let valueArray = ColorDefaults.retrieveDataFromDefaultsWithKey("myArray") as? [String] {
print("Saved Value Array: \(valueArray)")
}
else{
print("Error retrieving myArray")
}
Output: