Swift Codable protocol with Strings and UIImages - swift

I am trying to save data via a codable protocol. It is an array of structures with structs inside. There are strings, images and bool values and I am thinking it is one of these data types that does not conform to the protocol.
Here is a pic of all the data I must save:
original
struct Checklist {
var name: String
var image: UIImage?
var items: [Item] = []
}
struct Item {
var nameOfNote: String
var remind: Bool
//var date: Date
}
struct alldata: Codable {
var checklists: [Checklist] = []
}
I have tried to include the protocol in all the structs but it also produced an error. Here's a pic:
tried a solution picture

JSON in Swift 4 is fun (repeat this at 2am in the morning ;)).
One of the first things I always do is consult a good blog, like Ultimate Guide to JSON Parsing with Swift 4. While it might not answer the "direct" question, it will provide a lot of useful information.
Another thing to do, is consult the Apple documentation. In this, a quick look at the documentation for UIImage will point out that it does not conform to Codable, so that's a problem.
Another issue is, JSON doesn't support binary data, it's a text based solution.
This means that you need to figure out away to convert the binary image data to text. Lucky for us, people have already thought about this and the most common mechanism is to use Base 64 encoding.
A quick google search will turn up any number of solutions for encoding/decoding a UIImage to/from base 64, for example Convert between UIImage and Base64 string
Now, we just need to customise the encoding and decoding process.
The first thing is to make sure all the other fields/objects/data we use are also conform to Codable...
struct Item: Codable {
var nameOfNote: String
var remind: Bool
//var date: Date
}
Then we can set out customising the encoding and decoding process for the image, maybe something like...
struct Checklist: Codable {
enum CodingKeys: String, CodingKey {
case image
case name
case items
}
var name: String
var image: UIImage?
var items: [Item] = []
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
items = try container.decode([Item].self, forKey: .items)
if let text = try container.decodeIfPresent(String.self, forKey: .image) {
if let data = Data(base64Encoded: text) {
image = UIImage(data: data)
}
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
if let image = image, let data = image.pngData() {
try container.encode(data, forKey: .image)
}
try container.encode(name, forKey: .name)
try container.encode(items, forKey: .items)
}
}
nb: This is the basic process I've used for a couple of projects, I've not tested the above myself, but it should present the basic idea you need to get started

Related

Xcode warning: Immutable property will not be decoded because it is declared with an initial value which cannot be overwritten

Running Xcode 12, my Swift 5 Xcode project now has warnings whenever a Decodable or Codable type declares a let constant with an initial value.
struct ExampleItem: Decodable {
let number: Int = 42 // warning
}
Immutable property will not be decoded because it is declared with an initial value which cannot be overwritten
Xcode suggests changing the let to a var:
Fix: Make the property mutable instead
var number: Int = 42
It also suggests the fix:
Fix: Set the initial value via the initializer or explicitly define a CodingKeys enum including a 'title' case to silence this warning
What is the purpose of this new warning? Should it be heeded, or ignored? Can this type of warning be silenced?
Should Xcode's fix be implemented? Or is there a better solution?
Noah's explanation is correct. It’s a common source of bugs and it's not immediately obvious what’s happening due to the “magical” behaviour of Codable synthesis, which is why I added this warning to the compiler, since it brings your attention to the fact that the property won't be decoded and makes you explicitly call it out if that's the expected behaviour.
As the fix-it explains, you have a couple of options if you want to silence this warning - which one you choose depends on the exact behaviour you want:
Pass the initial value via an init:
struct ExampleItem: Decodable {
let number: Int
init(number: Int = 42) {
self.number = number
}
}
This will allow number to be decoded, but you can also pass around instances of ExampleItem where the default value is used.
You can also use it directly inside init instead, during decoding:
struct ExampleItem: Decodable {
let number: Int
private enum CodingKeys: String, CodingKey {
case number
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
number = try container.decodeIfPresent(Int.self, forKey: .number) ?? 42
}
}
This will allow number to be decoded, but use 42 as the default value if the decoding fails.
Make the property a var, although you can also make it a private(set) var:
struct ExampleItem: Decodable {
var number: Int = 42
}
Making it a var will allow number to be decoded, but it will also allow callers to modify it. By marking it as private(set) var instead, you can disallow this if you want.
Define an explicit CodingKeys enum:
struct ExampleItem: Decodable {
let number: Int = 42
private enum CodingKeys: CodingKey {}
}
This will prevent number from being decoded. Since the enum has no cases, this makes it clear to the compiler that there are no properties that you want to decode.
This warning appears because immutable properties with initial values don't participate in decoding - after all, they're immutable and they have an initial value, which means that initial value will never be changed.
For example, consider this code:
struct Model: Decodable {
let value: String = "1"
}
let json = """
{"value": "2"}
"""
let decoder = JSONDecoder()
let model = try! decoder.decode(Model.self, from: json.data(using: .utf8)!)
print(model)
This will actually print Model(value: "1"), even though the json we gave it had value as "2".
In fact, you don't even need to provide the value in the data you're decoding, since it has an initial value anyway!
let json = """
{}
"""
let decoder = JSONDecoder()
let model = try! decoder.decode(Model.self, from: json.data(using: .utf8)!)
print(model) // prints "Model(value: "1")"
Changing the value to a var means it will decode correctly:
struct VarModel: Decodable {
var value: String = "1"
}
let json = """
{"value": "2"}
"""
let varModel = try! decoder.decode(VarModel.self, from: json.data(using: .utf8)!)
print(varModel) // "VarModel(value: "2")"
If you're seeing this error, it means your code has never correctly parsed the property in question when decoding. If you change it to a var, the property will be parsed correctly, which might be what you want - however, you should make sure that the data you're decoding always has that key set. For example, this will throw an exception (and crash since we're using try!):
let json = """
{}
"""
let decoder = JSONDecoder()
struct VarModel: Decodable {
var value: String = "1"
}
let varModel = try! decoder.decode(VarModel.self, from: json.data(using: .utf8)!)
In conclusion, Xcode's suggestion is probably viable in many cases, but you should evaluate on a case by case basis whether changing the property to a var will break your app's functionality.
If you want the property to always return the hard-coded initial value (which is what's happening right now), consider making it a computed property or a lazy var.
Solution: define an explicit CodingKeys enum to prevent id from decoded.
For example,
struct Course: Identifiable, Decodable {
let id = UUID()
let name: String
private enum CodingKeys: String, CodingKey {
case name
}
init(name: String) { self.name = name }
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let name = try container.decodeIfPresent(String.self, forKey: .name)
self.name = name ?? "default-name"
}
}
The suggested workarounds by #SuyashSrijan suppresses the warning but may also lead to further developer errors.
I've written an alternative work around here:
public struct IdentifierWrapper<T>: Identifiable {
public let id = UUID()
public let value: T
}
Usage:
struct Model: Codable, Identifiable {
public let name: String
}
let wrapper = IdentifierWrapper(value: Model(name: "ptrkstr"))

How to turn a standard model class/struct into observable properties?

I am using ReactiveKit with their Bond extension, and I can't really figure out how to do something that feels kind of basic.
Let's say I have a User model in my app. Something like this.
class User: Codable {
var id: String
var firstName: String?
var avatar: String?
}
The content comes from a remote API, by making it confirm to Codable everything works nice and easy.
But let's also say for example that I would like a bidirectional binding from a model property to some UI state; that is not possible since none of my properties confirm to the BindableProtocol. Or if I want to observe changes to my model's properties, this is not possible either of course.
So my question is: how do I turn my model properties in actual observable Properties, without breaking the existing User model and behavior? For example, it still needs to be Codable. Do I make a second ObservableUser class, and then have didSet in all properties on the User model to write changes to the ObservableUser? And something similar on the ObservableUser to write changes back to the User? That seems horrible and hopefully not the recommenced way forward.
You don't need to create another object to make model properties observable. But there is definitely an overhead of implementing the decoder & encoder methods and coding keys enum as below which is also a standard practice in many cases,
class User: Codable {
var id: String = ""
var firstName = Observable<String?>(nil)
var avatar = Observable<String?>(nil)
private enum CodingKeys: String, CodingKey {
case id, firstName, avatar
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(String.self, forKey: .id)
self.firstName.value = try container.decode(String.self, forKey: .firstName)
self.avatar.value = try container.decode(String.self, forKey: .avatar)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.id, forKey: .id)
try container.encode(self.firstName.value, forKey: .firstName)
try container.encode(self.avatar.value, forKey: .avatar)
}
}
Now you are able to bind any UIView element.
Normally, with reactive approach, ViewModel is where you create bindable properties that provide value from your model to view and update model property by keeping the model invisible by the view.

Trying to make a class codable in Swift but it is not the correct format?

I am creating a class that conforms to codable.
I have this:
import Foundation
class Attribute : Decodable {
var number: Int16
var label: String?
var comments: String?
init(number:Int16, label:String?, comments:String?) {
self.number = number
self.label = label
self.comments = comments
}
// Everything from here on is generated for you by the compiler
required init(from decoder: Decoder) throws {
let keyedContainer = try decoder.container(keyedBy: CodingKeys.self)
number = try keyedContainer.decode(Int16.self, forKey: .number)
label = try keyedContainer.decode(String.self, forKey: .label)
comments = try keyedContainer.decode(String.self, forKey: .comments)
}
enum CodingKeys: String, CodingKey {
case number
case label
case comments
}
}
extension Attribute: Encodable {
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(number, forKey: .number)
try container.encode(label, forKey: .label)
try container.encode(comments, forKey: .comments)
}
}
This is apparently fine.
I create an instance of Attribute and encode it using:
let newAttribute = Attribute.init(number:value.number, label:value.label, comments:value.shortcut)
Then I create an array of these attributes and encode that array using
let array = try JSONEncoder().encode(array)
This will encode the array of Attribute to Data.
Then I try to convert the Data object back to the array of Attribute using this:
let array = try JSONDecoder().decode(Attribute.self, from: data) as! Array<Attribute>
First error I get is this:
Cast from 'Attribute' to unrelated type 'Array< Attribute>' always fails
If I remove the cast part I catch this error when the decode tries...
Optional("The data isn’t in the correct format.")
Any ideas?
You need to pass in the array to decode, don't pass in the array element type, then try to force-cast that to an array, that doesn't make any sense. YourType and Array<YourType> are two different and completely unrelated types, so you cannot cast one to the other and you need to use the specific type when calling JSONDecoder.decode(_:from:).
let array = try JSONDecoder().decode([Attribute].self, from: data)
Btw as already pointed out in your previous question, there is no need to manually write the init(from:) and encode(to:) methods or the CodingKeys enum since for your simple type, the compiler can auto-synthesise all of those for you. Also, if you used a struct instead of class, you'd also get the member wise initialiser for free.

Error using Codable for NSAttributedString

I'm trying to implement Codable for a class which contains a NSAttributedString, but I get errors at compile time:
try container.encode(str, forKey: .str)
error ambiguous reference to member 'encode(_:forKey:)'
and
str = try container.decode(NSMutableAttributedString.self, forKey: .str)
error: No 'decode' candidates produce the expected contextual type 'NSAttributedString'
I can get around it by using the NSData from the string, but thought this should work I would have thought
class Text : Codable {
var str : NSAttributedString
enum CodingKeys: String, CodingKey {
case str
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(str, forKey: .str). <-- error ambiguous reference to member 'encode(_:forKey:)'
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
str = try container.decode(NSMutableAttributedString.self, forKey: .str) <-- error: No 'decode' candidates produce the expected contextual type 'NSAttributedString'
}
}
NSAttributedString doesn’t conform to Codable, so you can’t do this directly.
If you just want to store the data you can implement a simple wrapper around your attributed string that conforms to Codable and use that in your data class. This is rather easy since you can convert the attributed string to Data using data(from:documentAttributes) when encoding. When decoding you first read the data and then initialize your attributed string using init(data:options:documentAttributes:). It supports various data formats including HTML, RTF and Microsoft Word.
Resist the temptation to add a Codable conformance to NSAttributedString in an extension. This will cause trouble when Apple adds this conformance or you add another library that does this.
If, on the other hand, you need this to communicate with a server or some other program, you need to exactly match the required data format. If you only need a plain string you probably should not use NSAttributedString at all. If it is some other format like markdown you could implement a wrapper that does the necessary transforms.
If you want to decode&encode the textual content only and convert it back from/to NS(Mutual)AttributedString you can try something like that:
class Text : Codable {
var str : NSMutableAttributedString?
enum CodingKeys: String, CodingKey {
case str
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try? container.encode(str?.string, forKey: .str)
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if let content = try? container.decode(String.self, forKey: .str){
str = NSMutableAttributedString(string: content)
// ADD any attributes here if required...
}
}
}

How can I decode when I don't know the type, with class inheritance?

I have a base class Action, which is an Operation. It has a bunch of crufty Operation stuff in it (KVO and all that). The base class itself doesn't actually need to encode/decode anything.
class Action : Operation, Codable {
var _executing = false
...
}
I have a bunch of Action sub-classes, like DropboxUploadAction, which are directly instantiated with an Input struct they define:
let actionInput = DropboxUploadAction.Input.init(...)
ActionManager.shared.run(DropboxUploadAction.init(actionInput, data: binaryData), completionBlock: nil)
Here's what the subclasses look like:
class DropboxUploadAction : Action {
struct Input : Codable {
var guid: String
var eventName: String
var fileURL: URL?
var filenameOnDropbox: String
var share: Bool
}
struct Output : Codable {
var sharedFileLink: String?
var dropboxPath: String?
}
var input: Input
var output: Output
...
required init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
input = try values.decode(Input.self, forKey: .input)
output = try values.decode(Output.self, forKey: .output)
let superDecoder = try values.superDecoder()
try super.init(from: superDecoder)
}
fileprivate enum CodingKeys: String, CodingKey {
case input
case output
}
override func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(input, forKey: .input)
try container.encode(output, forKey: .output)
try super.encode(to: container.superEncoder())
}
}
When some situations occur such as a loss of internet connectivity, these classes need to be serialized to disk for later. That's fine, because at the time I have references to them and can encode them with JSONEncoder().encode(action), no problem.
But later when I want to deserialize them, I need to specify the type of the class and I don't know what it is. I have some data and I know it can be decoded to a class that inherits from Action, but I don't know which subclass it is. I'm loathe to encode that in the filename. Is there some way to decode it as the base class Action, then in the decode() method of Action, somehow detect the proper class and redirect?
In the past I've used NSKeyedUnarchiver.setClass() to handle this. But I don't know how to do that with Swift 4's Codable, and I understand that NSCoding is deprecated now so I shouldn't use NSKeyedUnarchiver anymore...
If it helps: I have a struct Types : OptionSet, Codable which each subclass returns, so I don't have to use the name of the class as its identity.
Thanks for any help!
Uhhh NSCoding isn't deprecated. We still use it when instantiating UIViewControllers from storyboard via init(coder:).
Also, if you still don't want to use NSCoding, you can just store the Input, Output and Types to a struct and serialize that to disk instead.
struct SerializedAction {
let input: Input
let output: Output
let type: Type
}
When needed, you can decode that and decide the correct Action to initialize with your input/output via the type property.
class DropboxAction: Action {
...
init(input: Input, output: Output) {
...
}
}
You don't necessarily need to encode the entire Action object.