I wanted to add a description to my Data instances so I know what they're meant to be used for.
Eg: "This data is for (user.name)'s profile picture" or "This data is a encoded User instance" etc.
I was going through the properties available to Data instances and saw there was a description and debugDescription property. I tried to set the values for those properties but it seems like I can't since they are get-only properties. Is there any other way to add a description to a Data instance?
Edit:
Wrapping a Data instance as recommended below is a great solution but if there's a way to achieve the same without using a wrapper please let me know.
You can create a light-weight wrapper like following -
import Foundation
struct DescriptiveData: CustomStringConvertible, CustomDebugStringConvertible {
let data: Data
let customDescription: String?
init(data: Data, customDescription: String? = nil) {
self.data = data
self.customDescription = customDescription
}
var description: String { customDescription ?? data.description }
var debugDescription: String { description }
}
Usage
let data = DescriptiveData(data: Data(), customDescription: "data for profile picture")
print(data)
// data for profile picture
Related
I was wondering if an object which conforms to Codable can ever cause an error while in the process of being encoded or decoded. I feel as though conforming to the protocol ensures that an error can never be thrown.
For example if I have a struct called Book:
struct Book: Codable, Identifiable {
#DocumentID var id: String?
var name: String
}
Can this struct ever fail to be encoded or decoded?
For example if I do:
try! docRef.setData(from: book) { error in _ }
I don't think this can ever throw an error synchronously if Book conforms to Codable.
Similarly,
try! docRef.data(as: Book.self)
should never throw because, again, Book conforms to Codable
If I'm wrong and Book may fail while being Encoded or Decoded please explain under what conditions/circumstances that might happen.
Thanks in advance!
Encoding:
For JSONEncoder the cases where encoding fails are documented here: https://developer.apple.com/documentation/foundation/jsonencoder/2895034-encode.
Afaik assuming all properties and contained data structures are composed of simple types like Int, String, Array, Dictionary it will never fail because these values can be always represented as JSON. But you might have a type like Float that throws an Error when the value cannot be represented as JSON.
Decoding:
Any invalid JSON input like missing fields or syntax errors will throw an error. I recommend always handling these and making sure they don't go unnoticed / the user gets at least a "sorry, something went wrong, we're looking into it" experience.
As you asked this question in the context of Firestore, I'd like to add some more details in addition to what Ralf mentioned in his answer.
When decoding a Firestore document, there are a number of situations that might in a mapping error, which is why you should be prepared to handle them.
For example:
Your struct might expect a field (i.e. it is non-optional), but the field is missing in the Firestore document. This might happen when you've got other apps (Android, web, or even a previous version of your iOS app) that write to the same document, but use a previous version of the schema
One (or more) of the attributes on the document might have a different data type than your struct.
The following code snippet contains extensive error handling for these and other cases (please be aware that you need to adjust how to handle those errors to your individual situation - you might or might not want to display a user-visible error message, for example). The code is taken from my article Mapping Firestore Data in Swift - The Comprehensive Guide.
class MappingSimpleTypesViewModel: ObservableObject {
#Published var book: Book = .empty
#Published var errorMessage: String?
private var db = Firestore.firestore()
func fetchAndMap() {
fetchBook(documentId: "hitchhiker")
}
func fetchAndMapNonExisting() {
fetchBook(documentId: "does-not-exist")
}
func fetchAndTryMappingInvalidData() {
fetchBook(documentId: "invalid-data")
}
private func fetchBook(documentId: String) {
let docRef = db.collection("books").document(documentId)
docRef.getDocument { document, error in
if let error = error as NSError? {
self.errorMessage = "Error getting document: \(error.localizedDescription)"
}
else {
let result = Result { try document?.data(as: Book.self) }
switch result {
case .success(let book):
if let book = book {
// A Book value was successfully initialized from the DocumentSnapshot.
self.book = book
self.errorMessage = nil
}
else {
// A nil value was successfully initialized from the DocumentSnapshot,
// or the DocumentSnapshot was nil.
self.errorMessage = "Document doesn't exist."
}
case .failure(let error):
// A Book value could not be initialized from the DocumentSnapshot.
switch error {
case DecodingError.typeMismatch(let type, let context):
self.errorMessage = "\(error.localizedDescription): \(context.debugDescription)"
case DecodingError.valueNotFound(let type, let context):
self.errorMessage = "\(error.localizedDescription): \(context.debugDescription)"
case DecodingError.keyNotFound(let key, let context):
self.errorMessage = "\(error.localizedDescription): \(context.debugDescription)"
case DecodingError.dataCorrupted(let key):
self.errorMessage = "\(error.localizedDescription): \(key)"
default:
self.errorMessage = "Error decoding document: \(error.localizedDescription)"
}
}
}
}
}
}
If you're curious about how Firestore implements the Codable protocol, head over to the source code. For example, searching for DecodableError will show how exactly which error conditions might occur, and why.
I have a bunch of Codables in my app that are mostly used for JSON, but sometimes I need to upload images along with them, so I use a multipart request. To do that, I've built up a lot of machinery to handle the boundaries, carriage returns, et cetera but it requires manually adding each key value pair to the list of parts. I feel like the Encodable/Decodable protocols were made to obsolesce exactly that kind of thing.
With a JSONEncoder all I have to do is say encoder.encode(myCodable) and it spits out a Data representation of the JSON for that Codable, respecting the various coding keys and values.
Is there some sort of MultipartEncoder that I could use in a similar way? If not, could you point me to some resources for creating a custom Encoder?
Here's an example of what I currently do. Say we have a Codable called Post:
class Post: Codable {
var userId: Int?
var title: String?
var text: String?
var headerImage: Data?
}
I have built some classes called MultipartComponent (which holds a given key/value pair) and MultipartFormData (which holds an array of MultipartComponents that I use to create an InputStream out of those components to assign to URLRequest.httpBodyStream). So to turn Post into an InputStream I have to make a MultipartComponent from each of it's properties, put those into an array, put that array into a MultipartFormData, and then get an InputStream from that:
extension Post {
func multipartData() -> MultipartFormData {
var parts = [MultipartComponent]()
if let userId = userId {
parts.append(MultipartComponent(data: "\(userId)".data(using: .utf8) ?? Data(), name: "post[user_id]", fileName: nil, contentType:"text"))
}
if let title = title {
parts.append(MultipartComponent(data: title.data(using: .utf8) ?? Data(), name: "post[title]", fileName: nil, contentType:"text"))
}
if let text = text {
parts.append(MultipartComponent(data: text.data(using: .utf8) ?? Data(), name: "post[text]", fileName: nil, contentType:"text"))
}
if let headerImage = headerImage {
parts.append(MultipartComponent(data: headerImage, name: "post[header_image]", fileName: "headerImage.jpg", contentType:"text"))
}
return MultipartFormData(parts: parts, boundary: "--boundary-\(Date().timeIntervalSince1970)-boundary--")
}
Once I've written this extension, I can create a URLRequest for it like so:
var urlRequest = URLRequest(url: url)
urlRequest.httpBodyStream = post.multipartData().makeInputStream()
...
This works... fine. Problem is, as you can imagine, writing that extension gets tedious if there are more than a handful of properties, not to mention the pain of adding or removing properties. What I want is to create a custom encoder (or use an existing one) that will allow me to take any existing Codable and encode it as a multipart form instead of JSON. Is there something that I could use to do something like multipartEncoder.encode(post) to get the data representation or input stream for the Codable?
Currently our backend has added a dict on channel object as part of the extra data, it looks something like this:
{
// channel stuff from Stream
"extra_data": {
"custom dict": {
"custom field": "custom value"
}
}
}
However it seems that we cannot access to that dict from the iOS client since the channel.extraData type is a ChannelExtraDataCodable which only has two properties: name and imageURL.
Is there a way to access to this custom stuff from the client side?
Thanks in advance.
You need to define your own structure conforming to ChannelExtraDataCodable and set Channel.extraDataType to it.
Example:
struct MyChannelExtraData: ChannelExtraDataCodable {
var name: String?
var imageURL: URL?
var customDict: [String: String]
}
// Before you initialize the client
Channel.extraDataType = MyChannelExtraData.self
For more information on this, you can check the Stream Chat's documentation about custom extra data on iOS.
I want to set a model array max capacity if the user has not logged in even JSON response contains more records.
public class AlertsInbox: Codable {
public var messages: [Messages]?
public init(messages: [Messages]) {
self.messages = messages
if !GlobalVar.isLoggedIn,
let capacity = GlobalVar.messageCapacity {
self.messages?.reserveCapacity(capacity)
}
}
}
Decode json response:
let responseMessage = try JSONDecoder().decode(AlertsInbox.self, from: data)
Using reserveCapacity in init of model but it gets overridden with the number of records received in response.
How to restrict while decoding?
Thanks
You should not conditionally strip/drop out elements from an array in the Type's initializer. This consideration is against the responsibility role of Model object. Instead it's a job for the controller object.
But you can have an access point in the model type. Let's say, you have a property messages in your AlertsInbox object. You can have another access point with another property, let's say, limitedMessages that will be a computed property.
Look at the below code. I intentionally changed the type's semantics from Class to Struct. But you are free to use as your need.
struct AlertsInbox: Codable {
let messages: [Message]
var limitedMessages: [Message] {
// here you will use the value of your limit
// if the limit exceeds the size of the array, whole array will be returned
return Array(messages.prefix(5))
}
struct Message: Codable {
let title: String
}
}
Now you will use this, just like any other property, when you need the stripped out version of your actual messages. Like alertInbox.limitedMessages instead of the messages property.
But if you are not satisfied with the above and wanna stick to your plan, you can do that as well. See:
struct AlertsInbox: Codable {
let messages: [Message]
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let messages = try container.decode([Message].self, forKey: .messages)
// the idea of limiting the element numbers is same as the above
self.messages = Array(messages.prefix(5))
}
struct Message: Codable {
let title: String
}
}
OK, first, I know that there is no such thing as AnyRealmObject.
But I have a need to have something the behaves just like a Realm List, with the exception that any kind of Realm Object can be added to the list -- they don't all have to be the same type.
Currently, I have something like this:
enter code here
class Family: Object {
var pets: List<Pet>
}
class Pet: Object {
var dog: Dog?
var cat: Cat?
var rabbit: Rabbit?
}
Currently, if I wanted to add in, say, Bird, I'd have to modify the Pet object. I don't want to keep modifying that class.
What I really want to do is this:
class Family: Object {
var pets: List<Object>
}
Or, maybe, define a Pet protocol, that must be an Object, and have var pets: List<Pet>
The point is, I want a databag that can contain any Realm Object that I pass into it. The only requirement for the databag is that the objects must be Realm Objects.
Now, since Realm doesn't allow for this, how could I do this, anyway? I was thinking of creating something like a Realm ObjectReference class:
class ObjectReference: Object {
var className: String
var primaryKeyValue: String
public init(with object: Object) {
className = ???
primaryKeyValue = ???
}
public func object() -> Object? {
guard let realm = realm else { return nil }
var type = ???
var primaryKey: AnyObject = ???
return realm.object(ofType: type, forPrimaryKey: primaryKey)(
}
}
The stuff with the ??? is what I'm asking about. If there's a better way of doing this I'm all ears. I think my approach is ok, I just don't know how to fill in the blanks, here.
(I'm assuming that you are writing an application, and that the context of the code samples and problem you provided is in terms of application code, not creating a library.)
Your approach seems to be a decent one given Realm's current limitations; I can't think of anything better off the top of my head. You can use NSClassFromString() to turn your className string into a Swift metaclass object you can use with the object(ofType:...) API:
public func object() -> Object? {
let applicationName = // (application name goes here)
guard let realm = realm else { return nil }
guard let type = NSClassFromString("\(applicationName).\(className)") as? Object.Type else {
print("Error: \(className) isn't the name of a Realm class.")
return nil
}
var primaryKey: String = primaryKeyValue
return realm.object(ofType: type, forPrimaryKey: primaryKey)(
}
My recommendation is that you keep things simple and use strings exclusively as primary keys. If you really need to be able to use arbitrary types as primary keys you can take a look at our dynamic API for ideas as to how to extract the primary key value for a given object. (Note that although this API is technically a public API we don't generally offer support for it nor do we encourage its use except when the typed APIs are inadequate.)
In the future, we hope to offer enhanced support for subclassing and polymorphism. Depending on how this feature is designed, it might allow us to introduce APIs to allow subclasses of a parent object type to be inserted into a list (although that poses its own problems).
This may not be a complete answer but could provide some direction. If I am reading the question correctly (with comments) the objective is to have a more generic object that can be the base class for other objects.
While that's not directly doable - i.e. An NSObject is the base for NSView, NSString etc, how about this...
Let's define some Realm objects
class BookClass: Object {
#objc dynamic var author = ""
}
class CardClass: Object {
#objc dynamic var team = ""
}
class MugClass: Object {
#objc dynamic var liters = ""
}
and then a base realm object called Inventory Item Class that will represent them
class InvItemClass: Object {
#objc dynamic var name = ""
#objc dynamic var image = ""
#objc dynamic var itemType = ""
#objc dynamic var book: BookClass?
#objc dynamic var mug: MugClass?
#objc dynamic var card: CardClass?
}
then assume we want to store some books along with our mugs and cards (from the comments)
let book2001 = BookClass()
book2001.author = "Clarke"
let bookIRobot = BookClass()
bookIRobot.author = "Asimov"
let item0 = InvItemClass()
item0.name = "2001: A Space Odyssey"
item0.image = "Pic of Hal"
item0.itemType = "Book"
item0.book = book2001
let item1 = InvItemClass()
item1.name = "I, Robot"
item1.image = "Robot image"
item1.itemType = "Book"
item1.book = bookIRobot
do {
let realm = try Realm()
try! realm.write {
realm.add(item0)
realm.add(item1)
}
} catch let error as NSError {
print(error.localizedDescription)
}
From here, we can load all of the Inventory Item Objects as one set of objects (per the question) and take action depending on their type; for example, if want to load all items and print out just the ones that are books.
do {
let realm = try Realm()
let items = realm.objects(InvItemClass.self)
for item in items {
switch item.itemType {
case "Book":
let book = item.book
print(book?.author as! String)
case "Mug":
return
default:
return
}
}
} catch let error as NSError {
print(error.localizedDescription)
}
As it stands there isn't a generic 'one realm object fits all' solution, but this answer provides some level of generic-ness where a lot of different object types could be accessed via one main base object.