I have several codable structs and I'd like to create a universal protocol to code them to CKRecord for CloudKit and decode back.
I have an extension for Encodable to create a dictionary:
extension Encodable {
var dictionary: [String: Any] {
return (try? JSONSerialization.jsonObject(with: JSONEncoder().encode(self), options: .allowFragments)) as? [String: Any] ?? [:]
}
}
Then in a protocol extension, I create the record as a property and I try to create a CKAsset if the type is Data.
var ckEncoded: CKRecord? {
// Convert self.id to CKRecord.name (CKRecordID)
guard let idString = self.id?.uuidString else { return nil }
let record = CKRecord(recordType: Self.entityType.rawValue,
recordID: CKRecordID(recordName: idString))
self.dictionary.forEach {
if let data = $0.value as? Data {
if let asset: CKAsset = try? ckAsset(from: data, id: idString) { record[$0.key] = asset }
} else {
record[$0.key] = $0.value as? CKRecordValue
}
}
return record
}
To decode:
func decode(_ ckRecord: CKRecord) throws {
let keyIntersection = Set(self.dtoEncoded.dictionary.keys).intersection(ckRecord.allKeys())
var dictionary: [String: Any?] = [:]
keyIntersection.forEach {
if let asset = ckRecord[$0] as? CKAsset {
dictionary[$0] = try? self.data(from: asset)
} else {
dictionary[$0] = ckRecord[$0]
}
}
guard let data = try? JSONSerialization.data(withJSONObject: dictionary) else { throw Errors.LocalData.isCorrupted }
guard let dto = try? JSONDecoder().decode(self.DTO, from: data) else { throw Errors.LocalData.isCorrupted }
do { try decode(dto) }
catch { throw error }
}
Everything works forth and back except the Data type. It can't be recognized from the dictionary. So, I can't convert it to CKAsset. Thank you in advance.
I have also found there is no clean support for this by Apple so far.
My solution has been to manually encode/decode: On my Codable subclass I added two methods:
/// Returns CKRecord
func ckRecord() -> CKRecord {
let record = CKRecord(recordType: "MyClassType")
record["title"] = title as CKRecordValue
record["color"] = color as CKRecordValue
return record
}
init(withRecord record: CKRecord) {
title = record["title"] as? String ?? ""
color = record["color"] as? String ?? kDefaultColor
}
Another solution for more complex cases is use some 3rd party lib, one I came across was: https://github.com/insidegui/CloudKitCodable
So I had this problem as well, and wasn't happy with any of the solutions. Then I found this, its somewhat helpful, doesn't handle partial decodes very well though https://github.com/ggirotto/NestedCloudkitCodable
Related
I am trying to decode the array IGNotification into data that was successfully encoded into a dictionary. Below is my compactMap that should call my Decodable extension, however it looks like my extension is not being called or that the dictionary is not being serialized. Can someone help me as to why this is?
getNotifications function:
public func getNotifications(
completion: #escaping ([IGNotification]) -> Void
) {
guard let username = UserDefaults.standard.string(forKey: "username") else {
completion([])
return
}
let ref = firestoreDatabase.collection("users").document(username).collection("notifications")
ref.getDocuments { snapshot, error in
guard let notifications = snapshot?.documents.compactMap({
IGNotification(with: $0.data())
}),
error == nil else {
completion([])
return
}
completion(notifications)
print(notifications)
}
}
Decodable extension:
extension Decodable {
/// Create model with dictionary
/// - Parameter dictionary: Firestore data
init?(with dictionary: [String: Any]) {
guard let data = try? JSONSerialization.data(
withJSONObject: dictionary,
options: .prettyPrinted)
else {
return nil
}
guard let result = try? JSONDecoder().decode(
Self.self,
from: data
) else {
return nil
}
self = result
}
}
For my project I want to save data I'm getting from an API in realm and then display it in a table view.
The JSON will look like this:
{"books":[{"author":"Chinua Achebe", "title":"Things Fall Apart","imageLink":"http://books.google.com/books/content?id=plk_nwEACAAJ&printsec=frontcover&img=1&zoom=5&source=gbs_api"}]}
I've tried a few different things, but I can't figure out how to decode and store the JSON properly. I have used this function before for parsing the JSON, but when I add the realm Code I'm getting errors.
My function for fetching the JSON is:
func fetchArticle(){
let urlRequest = URLRequest(url: URL(string: "https:/mocki.io/v1/89aa9fe9-fdba-463f-99b3-5d8b6bc1d32e")!)
let task = URLSession.shared.dataTask(with: urlRequest) { (data,response,error) in
if error != nil {
print(error)
return
}
self.books = [Books]()
do {
let json = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) as! [String: AnyObject]
if let booksFromJson = json["books"] as? [[String : AnyObject]]{
for bookFromJson in booksFromJson {
let book = Books()
if let title = bookFromJson["title"] as? String, let author = bookFromJson["author"] as? String, let imageLink = bookFromJson["imageLink"] as? String {
book.author = author
book.title = title
book.imageLink = imageLink
}
self.books?.append(book)
let realm = try! Realm()
for books in bookFromJson {
try! realm.write {
realm.add(books, update: .all)
}
}
DispatchQueue.main.async {
self.tableview.reloadData()
}
} catch let error {
print(error)
}
}
task.resume()
}
}
}
This is my Struct:
class Books: Object, Decodable {
#objc dynamic var author: String?
#objc dynamic var imageLink: String?
#objc dynamic var title: String?
convenience init(author: String, imageLink: String, title: String) {
self.init()
self.author = author
self.imageLink = imageLink
self.title = title
}
override static func primaryKey() -> String? {
return "author"
}
private enum CodingKeys: String, CodingKey {
case author
case imageLink
case title
}
}
This are the errors im getting in the func:
Invalid conversion from throwing function of type '(Data?, URLResponse?, Error?) throws -> Void' to non-throwing function type '(Data?, URLResponse?, Error?) -> Void'
'let' declarations cannot be computed properties
There are probably a 100 different ways to map your json to a realm object but let's keep it simple. First I assume your incoming json may be several books so it would look like this
let jsonStringWithKey = """
{
"books":
[{
"author":"Chinua Achebe",
"title":"Things Fall Apart",
"imageLink":"someLink"
},
{
"author":"another author",
"title":"book title",
"imageLink":"another link"
}]
}
"""
So encode it as data
guard let jsonDataWithKey = jsonStringWithKey.data(using: .utf8) else { return }
Then, using JSONSerialization, map it to an array. Keeping in mind the top level object is "books" and AnyObject will be all of the child data
do {
if let json = try JSONSerialization.jsonObject(with: jsonDataWithKey) as? [String: AnyObject] {
if let bookArray = json["books"] as? [[String:AnyObject]] {
for eachBook in bookArray {
let book = Book(withBookDict: eachBook)
try! realm.write {
realm.add(book)
}
}
}
}
} catch {
print("Error deserializing JSON: \(error)")
}
and the Realm object is
class Book: Object, Codable {
#objc dynamic var author = ""
#objc dynamic var title = ""
#objc dynamic var imageLink = ""
convenience init(withBookDict: [String: Any]) {
self.init()
self.author = withBookDict["author"] as? String ?? "No Author"
self.title = withBookDict["title"] as? String ?? "No Title"
self.imageLink = withBookDict["imageLink"] as? String ?? "No link"
}
}
Again, there are a LOT of different ways of handling this so this is kind of the basics that can be expanded on.
As a suggestion, Realm Results are live-updating objects that also have corresponding events. So a neat thing you can do is to make a results object your tableView datasource and add an observer to it.
Results objects work very much like an array.
As books are added, updated or deleted from realm, the results object will reflect those changes and an event will be fired for each one - that makes keeping your tableView updated very simple.
So in your viewController
class ViewController: NSViewController {
var bookResults: Results<PersonClass>? = nil
#IBOutlet weak var bookTableView: NSTableView!
var bookToken: NotificationToken?
self.bookResults = realm.objects(Book.self)
and then
override func viewDidLoad() {
super.viewDidLoad()
self.bookToken = self.bookResults!.observe { changes in
//update the tableView when bookResults change
You have an error in terms of where you've placed your catch statement -- it should be in line with the do { } block. This might be easier to see if you format/indent your code (Ctrl-i in Xcode).
func fetchArticle(){
let urlRequest = URLRequest(url: URL(string: "https:/mocki.io/v1/89aa9fe9-fdba-463f-99b3-5d8b6bc1d32e")!)
let task = URLSession.shared.dataTask(with: urlRequest) { (data,response,error) in
if let error = error {
print(error)
return
}
self.books = [Books]()
do {
let json = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) as! [String: AnyObject]
if let booksFromJson = json["books"] as? [[String : AnyObject]]{
for bookFromJson in booksFromJson {
let book = Books()
if let title = bookFromJson["title"] as? String, let author = bookFromJson["author"] as? String, let imageLink = bookFromJson["imageLink"] as? String {
book.author = author
book.title = title
book.imageLink = imageLink
}
self.books?.append(book)
let realm = try! Realm()
for books in bookFromJson {
try! realm.write {
realm.add(books, update: .all)
}
}
DispatchQueue.main.async {
self.tableview.reloadData()
}
}
}
} catch { //<-- no need for `let error`
print(error)
}
}
task.resume() //<-- Moved outside the declaration
}
I set up the WKScriptMessageHandler function userContentController(WKUserContentController, didReceive: WKScriptMessage) to handle JavaScript messages sent to the native app. I know ahead of time that the message body will always come back with the same fields. How do I convert the WKScriptMessage.body, which is declared as Any to a struct?
What about safe type casting to, for example, dictionary?
let body = WKScriptMessage.body
guard let dictionary = body as? [String: String] else { return }
Or as an option, you can send body as json string and serialise it using codable.
struct SomeStruct: Codable {
let id: String
}
guard let bodyString = WKScriptMessage.body as? String,
let bodyData = bodyString.data(using: .utf8) else { fatalError() }
let bodyStruct = try? JSONDecoder().decode(SomeStruct.self, from: bodyData)
In SwiftUI message.body is String object. You can convert the body in dictionary like this:
if let bodyString = message.body as? String {
let data = Data(bodyString.utf8)
do {
if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
guard let body = json["body"] as? [String: Any] else {
return
}
//use body object
}
} catch let error as NSError {
print("Failed to load: \(error.localizedDescription)")
}
}
Your defined struct
struct EventType:Codable{
let status: Int!
let message: String!
}
WKScriptMessageHandler protocol method
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
do {
let jsonData = try JSONSerialization.data(withJSONObject: message.body)
let eventType = try JSONDecoder().decode(EventType.self, from: jsonData)
} catch {
print("fatalError")
}
}
if let jsonObj = jsonObj as? [String: Any],
let weatherDictionary = jsonObj["weather"] as? [String: Any],
let weather = weatherDictionary["description", default: "clear sky"] as?
NSDictionary {
print("weather")
DispatchQueue.main.async {
self.conditionsLabel.text = "\(weather)"
}
}
// to display weather conditions in "name" from Open Weather
"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01n"}]
//No errors, but code is not printing or displaying in App.
I'm not sure how to help with your exact question unless you can provide some more code for context. However,
You might try using the built-in decoding that comes with Swift 4. Check it out here. Basically, you make a class that models the response object, like this:
struct Weather: Decodable {
var id: Int
var main: String
var description: String
var icon: String
}
Then decode it like so:
let decoder = JSONDecoder()
let weather = try decoder.decode(Weather.self, from: jsonObj)
And it magically decodes into the data you need! Let me know if that doesn't work, and comment if you have more code context for your problem that I can help with.
I put the complete demo here to show how to send a HTTP request and parse the JSON response.
Note, Configure ATS if you use HTTP request, rather than HTTPS request.
The demo URL is "http://samples.openweathermap.org/data/2.5/forecast?q=M%C3%BCnchen,DE&appid=b6907d289e10d714a6e88b30761fae22".
The JSON format is as below, and the demo shows how to get the city name.
{
cod: "200",
message: 0.0032,
cnt: 36,
list: [...],
city: {
id: 6940463,
name: "Altstadt",
coord: {
lat: 48.137,
lon: 11.5752
},
country: "none"
}
}
The complete demo is as below. It shows how to use URLSessionDataTask and JSONSerialization.
class WeatherManager {
static func sendRequest() {
guard let url = URL(string: "http://samples.openweathermap.org/data/2.5/forecast?q=M%C3%BCnchen,DE&appid=b6907d289e10d714a6e88b30761fae22") else {
return
}
// init dataTask
let dataTask = URLSession.shared.dataTask(with: url) { (data, response, error) in
let name = WeatherManager.cityName(fromWeatherData: data)
print(name ?? "")
}
// send the request
dataTask.resume()
}
private static func cityName(fromWeatherData data: Data?) -> String? {
guard let data = data else {
print("data is nil")
return nil
}
do {
// convert Data to JSON object
let jsonObject = try JSONSerialization.jsonObject(with: data, options: [])
print(jsonObject)
if let jsonObject = jsonObject as? [String: Any],
let cityDic = jsonObject["city"] as? [String: Any],
let name = cityDic["name"] as? String {
return name
} else {
return nil
}
} catch {
print("failed to get json object")
return nil
}
}
}
I try to save an array of [String: Any] to user default, and for some situations it works, but others do not. I use the following to save to the default:
static func savingQueueToDisk(){
let queueDict = App.delegate?.queue?.map({ (track) -> [String: Any] in
return track.dict
})
if let queueDict = queueDict{
UserDefaults.standard.set(queueDict, forKey: App.UserDefaultKey.queue)
UserDefaults.standard.synchronize()
}
}
Queue is an array of Track, which is defined as follows:
class Track {
var dict: [String: Any]!
init(dict: [String: Any]) {
self.dict = dict
}
var album: Album?{
guard let albumDict = self.dict[AlbumKey] as? [String: Any] else{
return nil
}
return Album(dict: albumDict)
}
var artists: [Artist]?{
guard let artistsDict = self.dict[ArtistsKey] as? [[String: Any]] else{
return nil
}
let artists = artistsDict.map { (artistdict) -> Artist in
return Artist(dict: artistdict)
}
return artists
}
var id: String!{
return self.dict[IdKey] as! String
}
var name: String?{
return self.dict[NameKey] as? String
}
var uri: String?{
return self.dict[URIKey] as? String
}
}
I got different output when retrieving from the same API
Crashing output:
http://www.jsoneditoronline.org/?id=cb45af75a79aff64995e01e5efc0e7b6
Valid output:
http://www.jsoneditoronline.org/?id=0939823a4ac261bd4cb088663c092b20
It turns out it's not safe to just store an array of [String: Any] to the user defaults directly, and it might break based on the data it contains, and hence complaining about can't set none-property-list to user defaults. I solve this by first convert the array of [String: Any] to Data using JSONSerializationand now it can be saved correctly.
Solution:
//saving current queue in the app delegate to disk
static func savingQueueToDisk(){
if let queue = App.delegate?.queue{
let queueDict = queue.map({ (track) -> [String: Any] in
return track.dict
})
if let data = try? JSONSerialization.data(withJSONObject: queueDict, options: []){
UserDefaults.standard.set(data, forKey: App.UserDefaultKey.queue)
UserDefaults.standard.synchronize()
}else{
print("data invalid")
}
}
}
//retriving queue form disk
static func retrivingQueueFromDisk() -> [Track]?{
if let queueData = UserDefaults.standard.value(forKey: App.UserDefaultKey.queue) as? Data{
guard let jsonObject = try? JSONSerialization.jsonObject(with: queueData, options: []) else{
return nil
}
guard let queueDicts = jsonObject as? [[String: Any]] else{
return nil
}
let tracks = queueDicts.map({ (trackDict) -> Track in
return Track(dict: trackDict)
})
return tracks
}
return nil
}