Adding property observer to Decodable protocol in Swift 4 - swift

I am retrieving a URL from a JSON parse, and upon doing that I would like to convert that URL into a UIImage. Here is my current data struct:
struct Book: Decodable {
let author: String
let artworkURL: URL
let genres: [Genre]
let name: String
let releaseDate: String
}
I tried to do:
struct Book: Decodable {
let author: String
var bookArtwork: UIImage
let artworkURL: URL {
didSet {
do {
if let imageData: Data = try Data(contentsOf: artworkURL) {
bookArtwork = UIImage(data: imageData)!
}
} catch {
print(error)
}
}
let genres: [Genre]
let name: String
let releaseDate: String
}
But that does not conform to protocol 'Decodable'
Anyone else have a solution?

Below I will enumerate a few ways of handling this sort of scenario, but the right answer is that you simply should not be retrieving images during the parsing of the JSON. And you definitely shouldn't be doing this synchronously (which is what Data(contentsOf:) does).
Instead, you should should only retrieve the images as they're needed by the UI. And you want to retrieve images into something that can be purged in low memory scenarios, responding to the .UIApplicationDidReceiveMemoryWarning system notification. Bottom line, images can take a surprising amount of memory if you're not careful and you almost always want to decouple the images from the model objects themselves.
But, if you're absolutely determined to incorporate the image into the Book object (which, again, I'd advise against), you can either:
You can make the bookArtwork a lazy variable:
struct Book: Decodable {
let author: String
lazy var bookArtwork: UIImage = {
let data = try! Data(contentsOf: artworkURL)
return UIImage(data: data)!
}()
let artworkURL: URL
let genres: [Genre]
let name: String
let releaseDate: String
}
This is horrible pattern in this case (again, because we should never do synchronous network call), but it illustrates the idea of a lazy variable. You sometimes do this sort of pattern with computed properties, too.
Just as bad (for the same reason, that synchronous network calls are evil), you can also implement a custom init method:
struct Book: Decodable {
let author: String
let bookArtwork: UIImage
let artworkURL: URL
let genres: [Genre]
let name: String
let releaseDate: String
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
author = try values.decode(String.self, forKey: .author)
artworkURL = try values.decode(URL.self, forKey: .artworkURL)
genres = try values.decode([Genre].self, forKey: .genres)
name = try values.decode(String.self, forKey: .name)
releaseDate = try values.decode(String.self, forKey: .releaseDate)
// special processing for artworkURL
let data = try Data(contentsOf: artworkURL)
bookArtwork = UIImage(data: data)!
}
enum CodingKeys: String, CodingKey {
case author, artworkURL, genres, name, releaseDate
}
}
In fact, this is even worse than the prior pattern, because at least with the first option, the synchronous network calls are deferred until when you reference the UIImage property.
But for more information on this general pattern see Encode and Decode Manually in Encoding and Decoding Custom Types.
I provide these two above patterns, as examples of how you can have some property not part of the JSON, but initialized in some other manner. But because you are using Data(contentsOf:), neither of these are really advisable. But they're good patterns to be aware of in case the property in question didn't require some time consuming synchronous task, like you do here.
In this case, I think it's simplest to just provide a method to retrieve the image asynchronously when you need it:
Eliminate the synchronous network call altogether and just provide an asynchronous method to retrieve the image:
struct Book: Decodable {
let author: String
let artworkURL: URL
let genres: [Genre]
let name: String
let releaseDate: String
func retrieveImage(completionHandler: #escaping (UIImage?, Error?) -> Void) {
let task = URLSession.shared.dataTask(with: artworkURL) { data, _, error in
guard let data = data, error == nil else {
completionHandler(nil, error)
return
}
completionHandler(UIImage(data: data), nil)
}
task.resume()
}
}
Then, when you need the image for your UI, you can retrieve it lazily:
book.retrieveImage { image, error in
DispatchQueue.main.async { image, error in
cell.imageView.image = image
}
}
Those are a few approaches you can adopt.

Related

Swift Decodable - How to decode nested JSON that has been base64 encoded

I am attempting to decode a JSON response from a third-party API which contains nested/child JSON that has been base64 encoded.
Contrived Example JSON
{
"id": 1234,
"attributes": "eyAibmFtZSI6ICJzb21lLXZhbHVlIiB9",
}
PS "eyAibmFtZSI6ICJzb21lLXZhbHVlIiB9" is { 'name': 'some-value' } base64 encoded.
I have some code that is able to decode this at present but unfortunately I have to reinstanciate an additional JSONDecoder() inside of the init in order to do so, and this is not cool...
Contrived Example Code
struct Attributes: Decodable {
let name: String
}
struct Model: Decodable {
let id: Int64
let attributes: Attributes
private enum CodingKeys: String, CodingKey {
case id
case attributes
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(Int64.self, forKey: .id)
let encodedAttributesString = try container.decode(String.self, forKey: .attributes)
guard let attributesData = Data(base64Encoded: encodedAttributesString) else {
fatalError()
}
// HERE IS WHERE I NEED HELP
self.attributes = try JSONDecoder().decode(Attributes.self, from: attributesData)
}
}
Is there anyway to achieve the decoding without instanciating the additional JSONDecoder?
PS: I have no control over the response format and it cannot be changed.
If attributes contains only one key value pair this is the simple solution.
It decodes the base64 encoded string directly as Data – this is possible with the .base64 data decoding strategy – and deserializes it with traditional JSONSerialization. The value is assigned to a member name in the Model struct.
If the base64 encoded string cannot be decoded a DecodingError will be thrown
let jsonString = """
{
"id": 1234,
"attributes": "eyAibmFtZSI6ICJzb21lLXZhbHVlIiB9",
}
"""
struct Model: Decodable {
let id: Int64
let name: String
private enum CodingKeys: String, CodingKey {
case id, attributes
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(Int64.self, forKey: .id)
let attributeData = try container.decode(Data.self, forKey: .attributes)
guard let attributes = try JSONSerialization.jsonObject(with: attributeData) as? [String:String],
let attributeName = attributes["name"] else { throw DecodingError.dataCorruptedError(forKey: .attributes, in: container, debugDescription: "Attributes isn't eiter a dicionary or has no key name") }
self.name = attributeName
}
}
let data = Data(jsonString.utf8)
do {
let decoder = JSONDecoder()
decoder.dataDecodingStrategy = .base64
let result = try decoder.decode(Model.self, from: data)
print(result)
} catch {
print(error)
}
I find the question interesting, so here is a possible solution which would be to give the main decoder an additional one in its userInfo:
extension CodingUserInfoKey {
static let additionalDecoder = CodingUserInfoKey(rawValue: "AdditionalDecoder")!
}
var decoder = JSONDecoder()
let additionalDecoder = JSONDecoder() //here you can put the same one, you can add different options, same ones, etc.
decoder.userInfo = [CodingUserInfoKey.additionalDecoder: additionalDecoder]
Because the main method we use from JSONDecoder() is func decode<T>(_ type: T.Type, from data: Data) throws -> T where T : Decodable and I wanted to keep it as such, I created a protocol:
protocol BasicDecoder {
func decode<T>(_ type: T.Type, from data: Data) throws -> T where T : Decodable
}
extension JSONDecoder: BasicDecoder {}
And I made JSONDecoder respects it (and since it already does...)
Now, to play a little and check what could be done, I created a custom one, in the idea of having like you said a XML Decoder, it's basic, and it's just for the fun (ie: do no replicate this at home ^^):
struct CustomWithJSONSerialization: BasicDecoder {
func decode<T>(_ type: T.Type, from data: Data) throws -> T where T : Decodable {
guard let dict = try JSONSerialization.jsonObject(with: data) as? [String: Any] else { fatalError() }
return Attributes(name: dict["name"] as! String) as! T
}
}
So, init(from:):
guard let attributesData = Data(base64Encoded: encodedAttributesString) else { fatalError() }
guard let additionalDecoder = decoder.userInfo[.additionalDecoder] as? BasicDecoder else { fatalError() }
self.attributes = try additionalDecoder.decode(Attributes.self, from: attributesData)
Let's try it now!
var decoder = JSONDecoder()
let additionalDecoder = JSONDecoder()
decoder.userInfo = [CodingUserInfoKey.additionalDecoder: additionalDecoder]
var decoder2 = JSONDecoder()
let additionalDecoder2 = CustomWithJSONSerialization()
decoder2.userInfo = [CodingUserInfoKey.additionalDecoder: additionalDecoder]
let jsonStr = """
{
"id": 1234,
"attributes": "eyAibmFtZSI6ICJzb21lLXZhbHVlIiB9",
}
"""
let jsonData = jsonStr.data(using: .utf8)!
do {
let value = try decoder.decode(Model.self, from: jsonData)
print("1: \(value)")
let value2 = try decoder2.decode(Model.self, from: jsonData)
print("2: \(value2)")
}
catch {
print("Error: \(error)")
}
Output:
$> 1: Model(id: 1234, attributes: Quick.Attributes(name: "some-value"))
$> 2: Model(id: 1234, attributes: Quick.Attributes(name: "some-value"))
After reading this interesting post, I came up with a reusable solution.
You can create a new NestedJSONDecodable protocol which gets also the JSONDecoder in it's initializer:
protocol NestedJSONDecodable: Decodable {
init(from decoder: Decoder, using nestedDecoder: JSONDecoder) throws
}
Implement the decoder extraction technique (from the aforementioned post) together with a new decode(_:from:) function for decoding NestedJSONDecodable types:
protocol DecoderExtractable {
func decoder(for data: Data) throws -> Decoder
}
extension JSONDecoder: DecoderExtractable {
struct DecoderExtractor: Decodable {
let decoder: Decoder
init(from decoder: Decoder) throws {
self.decoder = decoder
}
}
func decoder(for data: Data) throws -> Decoder {
return try decode(DecoderExtractor.self, from: data).decoder
}
func decode<T: NestedJSONDecodable>(_ type: T.Type, from data: Data) throws -> T {
return try T(from: try decoder(for: data), using: self)
}
}
And change your Model struct to conform to NestedJSONDecodable protocol instead of Decodable:
struct Model: NestedJSONDecodable {
let id: Int64
let attributes: Attributes
private enum CodingKeys: String, CodingKey {
case id
case attributes
}
init(from decoder: Decoder, using nestedDecoder: JSONDecoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(Int64.self, forKey: .id)
let attributesData = try container.decode(Data.self, forKey: .attributes)
self.attributes = try nestedDecoder.decode(Attributes.self, from: attributesData)
}
}
The rest of your code will remain the same.
You could create a single decoder as a static property of Model, configure it once, and use it for all your Model decoding needs, both externally and internally.
Unsolicited thought:
Honestly, I would only recommend doing that if you're seeing a measurable loss of CPU time or crazy heap growth from the allocation of additional JSONDecoders… they're not heavyweight objects, less than 128 bytes unless there's some trickery I don't understand (which is pretty common though tbh):
let decoder = JSONDecoder()
malloc_size(Unmanaged.passRetained(decoder).toOpaque()) // 128

generic swift object serialization?

Is there a generic way to serialize/deserialize objects for iOS? I was using the following code, and the system functions I was calling were deprecated in iOS 12:
func object(forKey:String) -> Any? {
if let data = get(BLOB_COL, forKey) {
return NSKeyedUnarchiver.unarchiveObject(with: data as! Data)
}
return nil
}
func set(_ object:Any, forKey: String) {
let data = NSKeyedArchiver.archivedData(withRootObject: object)
updateOrInsert(forKey, BLOB_COL, data)
}
It looks like the new versions of these functions requires knowledge of the object classes, and different signatures for unarchiving different collection types... is there a simple way to handle this generically?
If you would like to use NSKeyedArchiver/NSKeyedUnarchiver:
When archiving change:
NSKeyedArchiver.archivedData...
to
NSKeyedArchiver.archivedData(withRootObject: object, requiringSecureCoding: false)
And when unarchiving change
NSKeyedUnarchiver.unarchiveObject...
to
NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data)
Codable protocol generic approach
If you would like to use Codable to encode and decode (serialize) your Data you can extend Encodable protocol and create a method to encode any object that conforms to it:
extension Encodable {
func data(using encoder: JSONEncoder = JSONEncoder()) throws -> Data { try encoder.encode(self) }
}
And to decode the encoded data you can extend Data and create a generic method that decode any object that conforms to Decodable:
extension Data {
func object<T: Decodable>(using decoder: JSONDecoder = JSONDecoder()) throws -> T {
try decoder.decode(T.self, from: self)
}
// you can also create a string property to convert the JSON data to String
var string: String? { String(data: self, encoding: .utf8) }
}
Usage:
struct Person: Codable {
let name: String
let age: Int
}
do {
// encoding
let person = Person(name: "Joe", age: 10)
var people = [Person]()
people.append(person)
let data = try people.data()
print(data.string ?? "") // [{"name":"Joe","age":10}]
// decoding
let loadedPeople: [Person] = try data.object()
loadedPeople.forEach({print( $0.name, $0.age)}) // Joe 10
} catch {
print(error)
}
As Leo says, stop using NSKeyedArchiver and use the Codable protocol. That's the modern, Swifty way to serialize/deserialize your objects.

Decode url with space in it from decodable model

I have the following structure and response parsing fails because url (https://example.url.com/test a) has a space in it. How can I escape it with %20 value in deserialization layer and keep it as URL type?
struct Response: Decodable {
let url: URL
enum CodingKeys: String, CodingKey {
case url
}
init(from decoder: Decoder) throws {
self.url = try decoder.container(keyedBy: CodingKeys.self).decode(URL.self, forKey: .url)
}
}
let string = "{\"url\":\"https://example.url.com/test a\"}"
let responseModel = try? JSONDecoder().decode(Response.self, from: string.data(using: .utf8)!)
print(responseModel?.url)
I don't think that's possible with a JSONDecoder customisation or in the serialization layer as you've mentioned in the post. The best you can achieve would be to do this:
struct Response: Decodable {
let urlString: String
var url: URL {
URL(string: urlString.replacingOccurrences(of: " ", with: "%20"))!
}
enum CodingKeys: String, CodingKey {
case urlString = "url"
}
}
Note: You don't need init(decoder:) if you don't have a custom implementation. Also don't need the CodingKeys enum if the property names are the same as the string keys (In your case url key is redundant).
As Rob has already mentioned it would be better to fix the issue at the backend instead of fixing the decoding part. If you can not fix it at the server side you can decote your url as a string, percent escape it and then use the resulting string to initialize your url property:
struct Response: Codable {
let url: URL
}
extension Response {
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let string = try container.decode(String.self, forKey: .url)
guard
let percentEncoded = string
.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed),
let url = URL(string: percentEncoded)
else {
throw DecodingError.dataCorruptedError(forKey: .url,
in: container,
debugDescription: "Invalid url string: \(string)")
}
self.url = url
}
}
Playground testing
let jsonString = #"{"url":"https://example.url.com/test a"}"#
do {
let responseModel = try JSONDecoder().decode(Response.self, from: Data(jsonString.utf8))
print(responseModel.url)
} catch { print(error) }
This will print
https://example.url.com/test%20a
You cannot decode url as an URL, as in decode(URL.self...), because it is not an URL. Change your url property to String, decode this value as String.self, and deal with turning the String into a URL later in the process.

Swift 4 Codable - API provides sometimes an Int sometimes a String

I have Codables running now. But the API has some String entries that can sometimes have an Int value of 0 if they are empty. I was searching here and found this: Swift 4 Codable - Bool or String values But I'm not able to get it running
My struct
struct check : Codable {
let test : Int
let rating : String?
}
Rating is most of the time something like "1Star". But if there is no rating I get 0 as Int back.
I'm parsing the data like this:
enum Result<Value> {
case success(Value)
case failure(Error)
}
func checkStar(for userId: Int, completion: ((Result<check>) -> Void)?) {
var urlComponents = URLComponents()
urlComponents.scheme = "https"
urlComponents.host = "xyz.com"
urlComponents.path = "/api/stars"
let userIdItem = URLQueryItem(name: "userId", value: "\(userId)")
urlComponents.queryItems = [userIdItem]
guard let url = urlComponents.url else { fatalError("Could not create URL from components") }
var request = URLRequest(url: url)
request.httpMethod = "GET"
let config = URLSessionConfiguration.default
config.httpAdditionalHeaders = [
"Authorization": "Bearer \(keytoken)"
]
let session = URLSession(configuration: config)
let task = session.dataTask(with: request) { (responseData, response, responseError) in
DispatchQueue.main.async {
if let error = responseError {
completion?(.failure(error))
} else if let jsonData = responseData {
// Now we have jsonData, Data representation of the JSON returned to us
// from our URLRequest...
// Create an instance of JSONDecoder to decode the JSON data to our
// Codable struct
let decoder = JSONDecoder()
do {
// We would use Post.self for JSON representing a single Post
// object, and [Post].self for JSON representing an array of
// Post objects
let posts = try decoder.decode(check.self, from: jsonData)
completion?(.success(posts))
} catch {
completion?(.failure(error))
}
} else {
let error = NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey : "Data was not retrieved from request"]) as Error
completion?(.failure(error))
}
}
}
task.resume()
}
Loading it:
func loadStars() {
checkStar(for: 1) { (result) in
switch result {
case .success(let goo):
dump(goo)
case .failure(let error):
fatalError(error.localizedDescription)
}
}
}
I hope someone can help me there, cause I'm not completely sure how this parsing, etc. works.
you may implement your own decode init method, get each class property from decode container, during this section, make your logic dealing with wether "rating" is an Int or String, sign all required class properties at last.
here is a simple demo i made:
class Demo: Decodable {
var test = 0
var rating: String?
enum CodingKeys: String, CodingKey {
case test
case rating
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let test = try container.decode(Int.self, forKey: .test)
let ratingString = try? container.decode(String.self, forKey: .rating)
let ratingInt = try? container.decode(Int.self, forKey: .rating)
self.rating = ratingString ?? (ratingInt == 0 ? "rating is nil or 0" : "rating is integer but not 0")
self.test = test
}
}
let jsonDecoder = JSONDecoder()
let result = try! jsonDecoder.decode(Demo.self, from: YOUR-JSON-DATA)
if rating API's value is normal string, you will get it as you wish.
if rating API's value is 0, rating will equal to "rating is nil or 0"
if rating API's value is other integers, rating will be "rating is integer but not 0"
you may modify decoded "rating" result, that should be easy.
hope this could give you a little help. :)
for more info: Apple's encoding and decoding doc

Making API call using swift

I am a TOTAL NOOB when it comes to iOS coding.
Im trying to learn how to make an API call to "http://de-coding-test.s3.amazonaws.com/books.json"
however, since I'm a total noob, all tutorials i find make no sense as to how to do it.
All i want to learn is how I can get the JSON data from the web and input it into a UITableViewCell
I have looked through about 3 dozen tutorials, and nothing makes sense.
Any help is appreciated.
Let's going by step:
1) The framework that you're going to use to make api call is NSURLSession (or some library like Alomofire, etc).
An example to make an api call:
func getBooksData(){
let url = "http://de-coding-test.s3.amazonaws.com/books.json"
(NSURLSession.sharedSession().dataTaskWithURL(NSURL(string: url)!) { (data:NSData?, response:NSURLResponse?, error:NSError?) -> Void in
//Here we're converting the JSON to an NSArray
if let jsonData = (try? NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions.MutableLeaves)) as? NSArray{
//Create a local variable to save the data that we receive
var results:[Book] = []
for bookDict in jsonData where bookDict is NSDictionary{
//Create book objects and add to the array of results
let book = Book.objectWithDictionary(bookDict as! NSDictionary)
results.append(book)
}
dispatch_async(dispatch_get_main_queue(), { () -> Void in
//Make a call to the UI on the main queue
self.books = results
self.tblBooks.reloadData()
})
}
}).resume()
}
Book Entity:
class Book{
var title:String
init(title:String){
self.title = title
}
class func objectWithDictionary(dict:NSDictionary)->Book{
var title = ""
if let titleTmp = dict.objectForKey("title") as? String{
title = titleTmp
}
return Book(title: title)
}
}
Note: In practice, you will check error and status code of response, and also you can extract the code of making api call to another class (or service layer).One option, using the pattern of DataMapper, you can create a class Manager by Entities (In this example for book like BookManager) and you can make something like this (You can abstract even more, creating a general api, that receive a url and return an AnyObject from the transformation of the JSON, and from there process inside your manager):
class BookManager{
let sharedInstance:BookManager = BookManager()
private init(){}
func getBookData(success:([Book])->Void,failure:(String)->Void){
let url = "http://de-coding-test.s3.amazonaws.com/books.json"
(NSURLSession.sharedSession().dataTaskWithURL(NSURL(string: url)!) { (data:NSData?, response:NSURLResponse?, error:NSError?) -> Void in
if error != nil{
failure(error!.localizedDescription)
}else{
if let jsonData = (try? NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions.MutableLeaves)) as? NSArray{
var results:[Book] = []
for bookDict in jsonData where bookDict is NSDictionary{
let book = Book.objectWithDictionary(bookDict as! NSDictionary)
results.append(book)
}
success(results)
}else{
failure("Error Format")
}
}
}).resume()
}
}