iOS devices are getting better, have more cores but how can we get benefit out of it while we are parsing JSON?
Currently,
I am using JSONDecoder() for JSON Parsing. Is there way we can do it faster? Maybe using multiple threads parsing in parts etc.
Any hints/pointers will be appreciated.
import Foundation
let filePath = Bundle.main.path(forResource: "json", ofType: "json")
struct Vehicle: Codable {
private let color: String
private let tires: [Tire]
private let number: String
}
struct Tire: Codable {
private let company: String
private let isNew: Bool
}
func parseData(_ data: Data) {
let decoder = JSONDecoder()
try? decoder.decode([Vehicle].self, from: data)
}
func modifiedParsing(_ data: Data) {
}
let data = try String(contentsOfFile: filePath!).data(using: .utf8)
let date = Date()
let start = date.timeIntervalSince1970
//parseData(data!)
let end = Date().timeIntervalSince1970
print("Time Taken \(end-start)")
/*
Initial Times: [3.728722095489502, 3.5913820266723633, 3.5568389892578125, 3.534559965133667, 3.506725311279297]
After Changes
*/
I wanted to make JSON Parsing faster.
For anyone who is looking for a good solution please refer: https://github.com/bwhiteley/JSONShootout
Marshal is faster than codable.
Related
New to IOS development and JSON stuff. I have a struct for Recipe which includes things like name, ingredients, instructions, etc. I have an array of Recipes. When my app is first run, I read data from a JSON file into the array of recipes so the app isn't empty at first. Throughout the app I append to the array of recipes. How would I go about writing the array back to the file everytime the array is changed? Here is some of the code and things I have tried.
Recipe Struct:
struct Recipe : Codable, Identifiable {
var id: String
var name: String
var ingredients: [String]
var instructions: [String]
var category: String
var imageName: String
}
Reading from JSON into recipe array:
import UIKit
import SwiftUI
var recipeData: [Recipe] = loadJson("recipeData.json")
func loadJson<T: Decodable>(_ filename: String) -> T {
let data: Data
guard let file = Bundle.main.url(forResource: filename,withExtension: nil)
else {
fatalError("\(filename) not found.")
}
do {
data = try Data(contentsOf: file)
} catch {
fatalError("Could not load \(filename): \(error)")
}
do {
return try JSONDecoder().decode(T.self, from: data)
} catch {
fatalError("Unable to parse \(filename): \(error)")
}
}
My attempt to write back to a json file once array is changed(appended to):
func writeJson(){
var jsonArray = [String]()
if let documentDirectory = FileManager.default.urls(for: .documentDirectory,
in: .userDomainMask).first {
let pathWithFilename = documentDirectory.appendingPathComponent("test.json")
for recipe in recipeData{
do{
let encodedData = try JSONEncoder().encode(recipe)
let jsonString = String(data: encodedData, encoding: .utf8)
print(jsonString!)
jsonArray.append(jsonString!)
try jsonString!.write(to: pathWithFilename,
atomically: true,
encoding: .utf8)
}catch{
print(error)
}
}
}
}
This all builds successfully but nothing is written to my tests.json file. I am very new so any help would be appreciated. Thank you.
try jsonString!.write(to: pathWithFilename,
atomically: true,
encoding: .utf8)
This method erases the existing file and replaces it with the data in question. You do this in a loop, so you're going to overwrite the file many times, always with a single recipe. My expectation is that this would always leave just the last recipe in the file.
I believe what you meant to do is:
// Encode all the recipes, not one at a time.
let encodedData = try JSONEncoder().encode(recipeData)
// Write them. There's no reason to convert to a string
encodedData.write(to: pathWithFilename, options: [.atomic])
As a beginner, this is probably fine. A more professional approach would likely spread this data over multiple files, use a database, or Core Data. But for small projects with only a few data items, writing a single JSON file is fine.
Recently, I was dealing with persistence on iOS with NSKeyedArchiver and NSKeyedUnarchiver. But I got some problem.
Here is the code:
import Foundation
class Model {
static let instance = Model()
static let DocumentsDirectory = FileManager().urls(for: .documentDirectory, in: .userDomainMask).first!
static let TestArchiveURL = DocumentsDirectory.appendingPathComponent("t")
static func getInstance() -> Model {
return instance
}
var str = "String"
var date = Date()
func save() {
let coder = NSKeyedArchiver(requiringSecureCoding: false)
coder.encode(str, forKey: ModelPropertyKey.str)
coder.encode(date, forKey: ModelPropertyKey.date)
do {
try coder.encodedData.write(to: Model.TestArchiveURL)
} catch {
print("error write to file: " + error.localizedDescription)
}
do {
let data = try Data(contentsOf: Model.TestArchiveURL)
let decoder = try NSKeyedUnarchiver(forReadingFrom: data)
let strGot = decoder.decodeObject(forKey: ModelPropertyKey.str) as? String
let dateGot = decoder.decodeObject(forKey: ModelPropertyKey.date) as? Date
print("\(String(describing: strGot)) \(String(describing: dateGot))")
} catch {
print("Error read from file")
}
}
}
I encode data with encode(:forKey:) and gain persistence with encodedData.write(to:). Then I try to read the data from disk with NSKeyedUnarchiver(forReadingFrom:) and decodeObject(forKey:). But this strategy works for String and fails for Date. The print output:
Optional("String") nil
I wonder why this happened.
You need to specify the object type you are trying to decode and since you are decoding Date object you need to understand that NSKeyedArchiver and NSKeyedUnarchiver will bridge your Date Swift object to NSDate Objective-C object for compatibility between swift and obj-c because NSKeyedArchiver uses NSCoding and Date does not conform to NSCoding
To do that change your decoding to be like this
let dateGot = decoder.decodeObject(of: NSDate.self, forKey: ModelPropertyKey.date)! as Date
However I would suggest that you use the swift Codable protocol with JSONEncoder and JSONDecoder they have a really good dateDecodingStrategy which allows for specifying your custom date formatter when encoding and decoding and they also support decoding Date objects directly without bridging to obj-c NSDate that's good when your trying to send your object as a json payload or decoding it from a network call
here is an example
let encoder = JSONEncoder()
let formatter = DateFormatter()
formatter.dateStyle = .full
formatter.timeStyle = .short
encoder.dateEncodingStrategy = .formatted(formatter)
let data = try? encoder.encode(Date())
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(formatter)
let newDate = try? decoder.decode(Date.self, from: data!)
I would like to load a JSON file and convert it to Data as part of my unit tests.
This is so I may assert how my service handles the response, however I do not want to fill each test case with massive json blocks. I was hoping to keep the file in the same directory as the test case, however trying to run the test throws an exception as the file cannot be found.
func test_ViewDidLoad_CallsContentService() {
let contentExpectation = expectation(description: "FetchContentEntry")
let httpClient = HTTPClient()
let response = createURLResponse(forUrl: "https://foo.bar", withStatusCode: 200)
httpClient.session = MockURLSession(data: mockContentData, urlResponse: response, error: nil)
.....
}
My file is referenced like
extension ContentSceneTests {
.........
var mockContentData: Data {
let data = try! Data(contentsOf: URL(fileURLWithPath: "./ContentResponse.json"), options: .alwaysMapped)
return data
}
}
The files are next to each other, eg
ContentSceneTests.swift
ContentResponse.json
I do something similar in my tests with an extension on XCTestCase.
You should be able to use
var mockContentData: Data {
return getData(name: "ContentResponse")
}
func getData(name: String, withExtension: String = "json") -> Data {
let bundle = Bundle(for: type(of: self))
let fileUrl = bundle.url(forResource: name, withExtension: withExtension)
let data = try! Data(contentsOf: fileUrl!)
return data
}
I am looking to access a string that is located inside of a JSON array that is located inside of another array. I am accessing the JSON API using JSONDecoder. I am receiving errors when trying the various methods that I have used in the past when using JSON arrays.
Here is the code:
var country = [Results]()
struct Rating: Codable {
let results: [Results]
}
struct Results: Codable {
let iso_3166_1: String
let release_dates: [Release_Dates]
}
struct Release_Dates: Codable {
let certification: String
}
func loadRating() {
let id = filmId
let apiKey = ""
let url = URL(string: "https://api.themoviedb.org/3/movie/\(id)/release_dates?api_key=\(apiKey)")
let request = URLRequest(
url: url! as URL,
cachePolicy: URLRequest.CachePolicy.reloadIgnoringLocalCacheData,
timeoutInterval: 10 )
let session = URLSession (
configuration: URLSessionConfiguration.default,
delegate: nil,
delegateQueue: OperationQueue.main
)
let task = session.dataTask(with: request, completionHandler: { (dataOrNil, response, error) in
if let data = dataOrNil {
do { let rates = try! JSONDecoder().decode(Rating.self, from: data)
self.country = rates.results
let us = self.country.filter({ $0.iso_3166_1.contains("US") })
print(us)
}
}
})
task.resume()
}
us prints to console
[Film.DetailsView.Results(iso_3166_1: "US", release_dates: [Film.DetailsView.Release_Dates(certification: "PG-13")])]
I am trying to access the certification string.
What would be the correct method used to achieve this?
us is an array of Results.
To get the first certification use this:
print(us.first!.release_dates.first!. certification)
I am force unwrapping for brevity, you should properly do it with optional binding or the guard statement.
Pretty straightforward, the result of filter is an array and certification is in the array release_dates
let us = self.country.filter({ $0.iso_3166_1.contains("US") })
for item in us {
for releaseDate in item.release_dates {
print(releaseDate.certification)
}
}
Please name your struct member names lowerCamelCased by mapping the keys with CodingKeys or with the convertFromSnakeCase strategy.
If there is only one US item, use first
if let us = self.country.first({ $0.iso_3166_1.contains("US") }) {
for releaseDate in us.release_dates {
print(releaseDate.certification)
}
}
Is it possible to return multiple JSON files from a Content Blocker Extension? In my UI users enable / disable different filters and each filter is represented by a separate file. I currently have (which only loads one despite iterating through multiple):
func beginRequestWithExtensionContext(context: NSExtensionContext) {
var items = Array <NSExtensionItem>()
let resources = ["a", "b", "c"]
for resource in resources {
let url = NSBundle.mainBundle().URLForResource(resource, withExtension: "json")
if let attachment = NSItemProvider(contentsOfURL: url) {
let item = NSExtensionItem()
item.attachments = [attachment]
items.append(item)
}
}
context.completeRequestReturningItems(items, completionHandler: nil)
}
I've tried doing multiple items and a single item with multiple attachments. If it isn't possible to have separate files, any way to combine multiple (or generate programmatically)?
It is possible to have multiple JSON files and use it for the Content Blocker extension.
1) Throws SFContentBlockerErrorDomain when you pass multiple extension items to completeRequestReturningItems method.
2) Can't attach multiple attachments to NSExtension. The comment on the source code says, the attachment is not meant to be an array of alternate data formats/types, but instead a collection to include in a social media post for example. These items are always typed NSItemProvider. I reckon you wouldn't be able to add multiple JSON data as attachments, since they are not a series of attachments to create a message.
My Solution (Verified it works):
NSItemProvider can be initialised with item (NSData) and typeIdentifier.
let aData = NSData(contentsOfURL: NSBundle.mainBundle().URLForResource("a", withExtension: "json")!)
let bData = NSData(contentsOfURL: NSBundle.mainBundle().URLForResource("b", withExtension: "json")!)
aJSON = `convert aData to JSON`
bJSON = `convert bData to JSON`
combinedJSON = `aJSON + bJSON`
combinedData = 'convert combinedJSON to NSData'
let attachment = NSItemProvider(item: combinedData, typeIdentifier: kUTTypeJSON as String)
Now you could create the extension with the attachment, combinedData as per your preferences.
For those curious I ended up adding code to dynamically generate a JSON file (persisted to disk). From other answers it seems like the step of saving could be avoided by returning an NSData representation of the file instead - although that attempt failed for me. Here's my snippet:
import UIKit
import MobileCoreServices
class ActionRequestHandler: NSObject, NSExtensionRequestHandling {
func beginRequestWithExtensionContext(context: NSExtensionContext) {
let item = NSExtensionItem()
let items = [item]
let url = buildJSONFileURL()
if let attachment = NSItemProvider(contentsOfURL: url) { item.attachments = [attachment] }
context.completeRequestReturningItems(items, completionHandler: nil)
}
func buildJSONFileURL() -> NSURL {
let directories = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)
let directory = directories[0]
let path = directory.stringByAppendingFormat("/block.json")
let selector = [...] // Dynamically Generated
let dictionary = [[
"action": [ "type": "css-display-none", "selector": selector ],
"trigger": [ "url-filter": ".*" ]
]]
let data = try! NSJSONSerialization.dataWithJSONObject(dictionary, options: NSJSONWritingOptions.PrettyPrinted)
let text = NSString(data: data, encoding: NSASCIIStringEncoding)!
try! text.writeToFile(path, atomically: true, encoding: NSASCIIStringEncoding)
return NSURL(fileURLWithPath: path)
}
}
You can combine two JSON rule files in to one file and use that file.
import UIKit
import MobileCoreServices
class ContentBlockerRequestHandler: NSObject, NSExtensionRequestHandling {
func beginRequest(with context: NSExtensionContext) {
let sharedContainerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "you app group identifier")
let sourceURLRules = sharedContainerURL?.appendingPathComponent("Rules1.json")
let sourceURLRules2 = sharedContainerURL?.appendingPathComponent("Rules2.json")
do {
let jsonDecoder = JSONDecoder()
let dataFormRules1 = try Data(contentsOf: sourceURLRules1!, options: .mappedIfSafe)// Rule is Decode able Swift class
let rulesArray1 = try? jsonDecoder.decode(Array<Rule>.self,from: dataFormRules1)
let dataFormRules2 = try Data(contentsOf: sourceURLRules2!, options: .mappedIfSafe)
let rulesArray2 = try? jsonDecoder.decode(Array<Rule>.self,from: dataFormRules2)
saveCombinedRuleFile(ruleList: rulesArray1! + rulesArray2!)
} catch {
//handle error condition
}
let sourceURLCombinedRule = sharedContainerURL?.appendingPathComponent("CombinedRule.json")
let combinedRuleAttachment = NSItemProvider(contentsOf: sourceURLCombinedRule)
let item = NSExtensionItem()
item.attachments = [combinedRuleAttachment]
context.completeRequest(returningItems: [item], completionHandler: nil)
}
func saveCombinedRuleFile(ruleList:[Rule]) {
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(ruleList) {
let sharedContainerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "you app group identifier")
if let json = String(data: encoded, encoding: .utf8) {
print(json)
}
if let destinationURL = sharedContainerURL?.appendingPathComponent("CombinedRule.json") {
do {
try encoded.write(to: destinationURL)
} catch {
print ("catchtry")
}
}
}
}
}