How to show Array of Dictionaries in a SwiftUI List? - swift

After a few months of learning Swift by migrating parts of a legacy ObjC app, I'm looking forward to starting a new app in pure Swift - I'm hoping that working with pure Swift base classes will lead to less unwrapping and other bridging shenanigans.
However, very quickly I've found myself facing similar problems.
I want to read some JSON from a web service, and show in in a list implemented with SwiftUI - should be simple, right?
The data (actually read from the Twitter API) comes in, and I deserialise it,
do {
if let results = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.allowFragments ) as? [String: Any] {
print(results)
if let followers = results["users"] as? [[String: Any]] {
print (followers.count)
print("followers class \(type(of: followers))")
} else {
print(results["errors"])
}
}
} catch let error as NSError {
print(error.localizedDescription)
}
You'll see I print the class of followers, and this shows,
followers class Array<Dictionary<String, Any>>
...a nice array of Dictionaries, using Swift base classes. That would seem a reasonable data structure to present via a List in SwiftUI. However, it doesn't seem to be so simple, as the data elements need to be Identifiable (fair enough) but I don't have a struct for each element, and I don't want the overhead of processing the array into an array of structs carrying identifiers.
A bit of research, and it seems there's a solution available, as I can initialise List with an Array, something like the following,
var body: some View {
List (twitter.followers!, id: \.self) { follower in // <<< Compilation error
Text("\(follower["name"])")
}
}
However, that code gives the compilation error on the flagged line,
Protocol type 'Any' cannot conform to 'Hashable' because only concrete
types can conform to protocols
I think the issue is that the compiler sees followers as 'Any', rather than an Array, but why?
btw, I've seen the answer to this question, but it seems the List initialiser should be a more elegant solution, if I can get it to work...

You have to create a struct for follower/user and declare it as Decodable and Identifiable.
Then you will be able to use JSONDecoder to read the data from the struct into an array for you.
As a result your twitter.followers will be an array of identifiable objects and you will be able to use it in ForEach().

Related

Swift Tests: Spying on `Encoder`?

I have some Swift models that encode data to json. For example:
struct ExampleModel: Encodable {
var myComputedProperty: Bool { dependentModel.first(where: { $0.hasTrueProperty}) }
enum CodingKeys: String, CodingKey {
case firstKey = "first_key"
case secondKey = "second_key"
case myComputedProperty = "my_computed_property"
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(firstKey, forKey: .firstKey)
try container.encode(myComputedProperty, forKey: .myComputedProperty)
}
}
The encoded data is sent to an API, and cross-platform system tests in this case are logistically tricky, so it's important I write tests that ensure the encoded data is as expected. All I'm looking to do is ensure container.encode receives expected keys and values.
I'm somewhat new to Swift, and trying to override the container, its dependencies & generics, and its .encode method is taking me down a rabbit-hole of rewriting half Swift's encoding foundation. In short: the spy I'm writing is too complex to be useful.
Despite lack of Google/StackOverflow results, I'm guessing spying on encoders is common (?), and that there's an easier way to confirm container.encode receives expected values. But the way swift's Encoder functionality is written is making it hard for me to do so without rewriting half the encoder. Anyone have boilerplate code or an example of effectively spying on container.encode?
I have never done tests spying on encoder, although I have plenty of tests checking correction of encoding/decoding.
There are multiple ways to do it:
You can get something like SwiftyJSON and inspect encoded JSON
You can use OHHTTPStubs to mock sending a request to API, and examine the request the way it's sent (this allows to examine not only JSON, but headers as well)
But the most basic way to test is encoding and then decoding your data structure, and comparing the structures. They should be identical.
How it's done:
Create a Decodable extension for your struct (or if your struct is Codable, then you already ave this). It can be added directly in the test class / test target if you don't need it in the production code:
extension ExampleModel: Decodable {
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
firstKey = try container.decode(<...>.self, forKey: .firstKey)
// etc
}
}
Add Equatable extension for the struct:
extension ExampleModel: Equatable {
// Rely on synthesized comparison, or create your own
}
Add a test:
let given = ExampleModel(firstKey: ..., ...)
let whenEncoded = try JSONEncoder().encode(given)
let whenDecoded = try JSONDecoder().decode(ExampleModel.self, from: whenEncoded)
// Then
XCTAssertEqual(given, whenDecoded)
Couple of notes:
It's very unusual to encode computed property. It's not prohibited, but it breaks the immutability of the property you are about to send to API: what if it changes after you called encode, but before it was sent to API? Better solution is to make a let property in the struct, but have an option to create a struct with this value given with appropriate calculation (e.g. init for ExampleModel could be passing the dependentModel, and the property would be calculated in the init once)
If you still choose to have a calculated property, you obviously cannot decode into it. So in that case you will need to preserve the decoded property in some class variable to compare it separately.

How to sort an array of objects by a string value in the object properties?

I have been learning swift recently (I have used js and java before), and I ran into this problem where I need to sort an array of objects by a string value in the object. I have this list and I want to add a sort feature, where it automatically sorts the list by the date of the logged dates (It is kind of like a reading log). So here is how I built the array behind the log:
struct LogItem: Hashable, Codable {
var date: String
var time: String
var message: String
}
And in the LogView struct I have:
#AppStorage("logs") var logs: [LogItem] = []
The message property is kind of notes on the log btw. So what I want to do is sort this array by the date property, which is in the "d/mm/yyyy" format. I have looked at a few solutions but couldn't figure out how to sort it in alphabetical order if it is in a object.
If someone could please help that would be great, also some of the posts I have seen only seem to work in a swift playground and not in an actual swiftui file, I get the cannot use instance member within property initializer error, property initializers run before 'self' is avaliable. So it will have to work in an actual app.
You can sort an array of custom objects like this. Keep in mind that this method sorts the strings alphabetically.
logs.sort(by: {$0.date < $1.date})
Switch the < to a > to reverse the sort order.
... some of the posts I have seen only seem to work in a swift playground and not in an actual swiftui file, I get the cannot use instance member within property initializer error, property initializers run before 'self' is avaliable. So it will have to work in an actual app.
Your problem here is you are putting the code in the wrong place. It should be in the onAppear method, placed after your outermost view:
.onAppear {
logs.sort(by: {$0.date < $1.date})
}
For example:
var body: some View {
NavigationView { // <- Just an example
...
}
.onAppear {
// appear stuff here
}
}

Get all attribute names of Core Data entity; Swift

Is there a more efficient way to retrieve all the names/titles of attributes of a NSManagedObject than this:
func getAllAttributeTitles(_ myStatSheet:StatSheet) -> Array<String> {
let dictAttributes = myStatSheet.entity.attributesByName
var arrAttributeTitles:Array<String> = []
for (key, _) in dictAttributes {
arrAttributeTitles.append(key)
}
return arrAttributeTitles
}
As I mentioned, what you've got is the right way to do it. There are other ways but I wasn't at a Mac earlier and couldn't try them out.
A more "Swift-y" way to get the array would be something like
let arrAttributeTitles = myStatSheet.entity.attributesByName.enumerated().map { $0.element.key }
This won't be any more efficient, since it's really doing the same things, but it might be more what you were thinking of when you asked. It's still getting attributesByName and iterating over the result to get strings naming the attributes.
It might be worth noting that the argument type on your method could be NSManagedObject instead of StatSheet, since the code will work for any managed object of any entity type.

Can Codable APIs be used to decode data encoded with NSKeyedArchiver.archivedData?

I'm converting a codebase from using NSCoding to using Codable. I have run into an issue when trying to restore data encoded with NSCoding. I have an object that was encoded with the code:
let encodedUser = NSKeyedArchiver.archivedData(withRootObject: user)
let userDefaults = UserDefaults.standard
userDefaults.set(encodedUser, forKey: userKey)
userDefaults.synchronize()
It was previously being decoded with the code:
if let encodedUser = UserDefaults.standard.data(forKey: userKey) {
if let user = NSKeyedUnarchiver.unarchiveObject(with: encodedUser) as? User {
\\ do stuff
}
}
After updating the User object to be Codable, I attempted to update the archive/unarchive code to use PropertyListDecoder because I saw that archivedData(withRootObject:) returns data formatted as NSPropertyListBinaryFormat_v1_0. When running this code:
if let encodedUser = UserDefaults.standard.data(forKey: userKey) {
let user = try! PropertyListDecoder().decode(User.self, from: encodedUser)
\\ do stuff
}
I get a keyNotFound error for the first key I'm looking for, and breakpointing in my init(from: Decoder), I can see that the container has no keys. I also tried to use PropertyListSerialization.propertyList(from:options:format) to see if I could pass that result into PropertyListDecoder, but that function gave me an NSCFDictionary, which is structured in a really strange way but does appear to contain all the data I'm looking for.
So is it possible to decode an object using Codable APIs if it was encoded with NSKeyedArchiver.archivedData(withRootObject:)? How can I achieve this?
I decided that the answer is almost certainly no, you cannot use Codable to decode what NSCoding encoded. I wanted to get rid of all the NSCoding cruft in my codebase, and settled on a migration path where I have both protocols implemented in the critical data models to convert stored data from NSCoding to Codable formats, and I will remove NSCoding in the future when enough of my users have updated.

Retrieving NSOrderedSet from Core Data and casting it to entity managedObjectSubclasss

Im making a Fitness app to learn Core data, and I have found that I need to let the user store every performed workout as a WorkoutLog item, and in there, there should be a series of ExerciseLogs which represents performances of that exercise (it contains each lift and also a reference to the actual exercise design).
Problem is that after a while i realize that i need to have these ordered, so that the next time i want to show the user their workout, the order that the exercisese were performed should be the same.
So I checked "ordered" in the top right of the image there, and now my code is in dire need of an update. I have tried to read as much as I could about working with NSOrderedSet and how to fetch them from core data and then manipulate them, but I havent really found much of use to me. (I have no experice in objective-c)
For example my code that used to be:
static func deleteWorkoutLog(_ workoutLogToDelete: WorkoutLog) {
guard let exerciseLogsToDelete = workoutLogToDelete.loggedExercises as? Set<ExerciseLog> else {
print("error unwrapping logged exercises in deleteWorkoutLog")
return
}
I get the error: .../DatabaseFacade.swift:84:77: Cast from 'NSOrderedSet?' to unrelated type 'Set' always fails
So what ive learned about sets and core data no longer seems applicable.
Im far from an expert in programming, but im very eager to learn how to get access to the loggedExercises instances.
TLDR; Is there a way to cast NSOrderedSet to something I can work with? How do we usually work with NSManagedSets from core data? Do we cast them to Arrays or MutableSets? I would very much appreciate an example or two on how to get started with retrieving and using these ordered sets!
Thanks
For anyone else wondering how to get started with orderedSets in core data:
After setting my the WorkoutLog.loggedExercises "to-many" relationship to be ordered, I managed to access them through the mutableOrderedSetValue function like this:
static func deleteWorkoutLog(_ workoutLogToDelete: WorkoutLog) {
let orderedExerciseLogs: NSMutableOrderedSet = workoutLogToDelete.mutableOrderedSetValue(forKey: "loggedExercises")
let exerciseLogsToDelete = orderedExerciseLogs.array
for exerciseLog in exerciseLogsToDelete {
guard let exerciseLog = exerciseLog as? ExerciseLog else {
return
}
Works great so far.
And to rearrange the NSOrderedSet I ended up doing something like this:
// Swap the order of the orderedSet
if let orderedExerciseLogs: NSOrderedSet = dataSourceWorkoutLog.loggedExercises {
var exerciseLogsAsArray = orderedExerciseLogs.array as! [ExerciseLog]
let temp = exerciseLogsAsArray[indexA]
exerciseLogsAsArray[indexA] = exerciseLogsAsArray[indexB]
exerciseLogsAsArray[indexB] = temp
let exerciseLogsAsOrderedeSet = NSOrderedSet(array: exerciseLogsAsArray)
dataSourceWorkoutLog.loggedExercises = exerciseLogsAsOrderedeSet
}