How to Make JSON Data Persistent for Offline Use (Swift 4)

How do I make JSON data persistent for offline use (Swift 4)

I would recommend taking a look at Realm if you ever need local storage. It is an alternative to Core Data, but is simpler to use:
https://realm.io/docs/swift/latest/

For this task specifically, I wouldn't save to CoreData or Realm, but directly to the app directory, and save the version and location of the file in the UserDefaults.

1) (UPDATED ANSWER)

//Download the file from web and save it to the local directory on the device
let hostedJSONFile = "http://tourofhonor.com/BonusData.json"
let jsonURL = URL(string: hostedJSONFile)
let defaults = UserDefaults.standard
let itemName = "myJSONFromWeb"
do {
let directory = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
let fileURL = directory.appendingPathComponent(itemName)
let jsonData = try Data(contentsOf: jsonURL!)
try jsonData.write(to: fileURL, options: .atomic)
//Save the location of your JSON file to UserDefaults
defaults.set(fileURL, forKey: "pathForJSON")
} catch {
print(error)
}

2) Save the version of your JSON (if it has a version coming from the web) to UserDefaults:

defaults.set(1, forKey: "jsonVersion")

3) When you launch your app, you want to verify the current jsonVersion to the version you saved in document directory:

let currentVersion = //...get the current version from your JSON file from web.
let existingVersion = defaults.integer(forKey: "jsonVersion")
if currentVersion != existingVersion {
//Download the newest JSON and save it again to documents directory for future use. Also, use it now for your needs.
} else {
//Retrieve the existing JSON from documents directory
let fileUrl = defaults.url(forKey: "pathForJSON")
do {
let jsonData2 = try Data(contentsOf: fileUrl!, options: [])
let myJson = try JSONSerialization.jsonObject(with: jsonData2, options: .mutableContainers)
//...do thing with you JSON file
print("My JSON: ", myJson)
} catch {
print(error)
}

Reading local JSON file and using it to populate UITableView

First of all, you are parsing JSON values incorrectly. You need to first understand your JSON format. You go to your JSON file link, and analyze it. If it starts with a "{", then it is a Dictionary, if it starts with a "[", then it is an Array. In your case, it is a Dictionary, then there come the keys which are Strings ("meta", "bonuses"). So, we know our keys are Strings. Next, we look at our values. For "meta" we have a Dictionary of String : String; for "bonuses" we have an Array of Dictionaries.
So, our JSON format is [String : Any], or it can be written Dictionary<String, Any>.

Next step, is accessing those values in the Dictionary.

func updateJSONFile() {
print("updateJSONFile Method Called")
let hostedJSONFile = "http://tourofhonor.com/BonusData.json"
let jsonURL = URL(string: hostedJSONFile)
let itemName = "BonusData.json"
let defaults = UserDefaults.standard
do {
let directory = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor:nil, create:false)
let fileURL = directory.appendingPathComponent(itemName)
let jsonData = try Data(contentsOf: jsonURL!)
let jsonFile = try JSONSerialization.jsonObject(with: jsonData, options: .mutableContainers) as? [String : Any]
let metaData = jsonFile!["meta"] as! [String : Any]
let jsonVersion = metaData["version"]
print("JSON VERSION ", jsonVersion!)
try jsonData.write(to: fileURL, options: .atomic)
defaults.set(fileURL, forKey: "pathForJSON") //Save the location of your JSON file to UserDefaults
defaults.set(jsonVersion, forKey: "jsonVersion") //Save the version of your JSON file to UserDefaults
DispatchQueue.main.async {
//reload table in the main queue
self.tableView.reloadData()
}
} catch {
print(error)
}
}

Then, when you access your locally saved file, again, you have to parse the JSON to check the versions:

func checkJSON() {
//MARK: Check for updated JSON file
let defaults = UserDefaults.standard
let hostedJSONFile = "http://tourofhonor.com/BonusData.json"
let jsonURL = URL(string: hostedJSONFile)
var hostedJSONVersion = ""
let jsonData = try! Data(contentsOf: jsonURL!)
let jsonFile = try! JSONSerialization.jsonObject(with: jsonData, options: .mutableContainers) as! [String : Any]
let metaData = jsonFile["meta"] as! [String : Any]
hostedJSONVersion = metaData["version"] as! String
let localJSONVersion = defaults.string(forKey: "jsonVersion")
if localJSONVersion != hostedJSONVersion {
print("\(localJSONVersion) : \(hostedJSONVersion)")
updateJSONFile()
} else {
//Retrieve the existing JSON from documents directory
print("\(localJSONVersion) : \(hostedJSONVersion)")
print("Local JSON is still the latest version")
let fileUrl = defaults.url(forKey: "pathForJSON")
do {
let localJSONFileData = try Data(contentsOf: fileUrl!, options: [])
let myJson = try JSONSerialization.jsonObject(with: localJSONFileData, options: .mutableContainers) as! [String : Any]
//Use my downloaded JSON file to do stuff

DispatchQueue.main.async {
//reload table in the main queue
self.tableView.reloadData()
}
} catch {
print(error)
}
}
}

Don't forget to allow arbitrary loads in your Info.plist file, because your JSON file is hosted on a website without https.
Allow Arbitrary Loads

What's the best way to store data offline and update it when app goes online

  • You are not checking failure condition.
  • if you are offline, Alamofire goes to failure case.

check below code will help you.

        case .success(_):
if let data = response.result.value{
let json1 = data as! NSDictionary
UserDefaults.standard.set(NSKeyedArchiver.archivedData(withRootObject: json1), forKey: "JsonFeatured")

let jsond = UserDefaults.standard.object(forKey: "JsonFeatured") as? NSData

let json = NSKeyedUnarchiver.unarchiveObject(with: jsond! as Data) as! NSDictionary

//self.CollectionArray = json.object(forKey: "banners") as! NSArray
}

case .failure(_):
if (UserDefaults.standard.object(forKey: "JsonFeatured") != nil){
if let savedData = NSKeyedUnarchiver.unarchiveObject(with: UserDefaults.standard.object(forKey: "JsonFeatured") as! Data)
{
let json = NSKeyedUnarchiver.unarchiveObject(with: savedData as! Data) as! NSDictionary

self.CollectionArray = json.object(forKey: "banners") as! NSArray

}
}
break

}

Best practice for storing temporary data in swift

You should not store data to UserDefaults, user defaults is just a key value based file which is mostly used to store some data about user, for example :

doNotShowAdds : true
isUserLogedIn :true.

You should not use keychain either since keychain is encrypted container where u store critic data about user, for example : password.

You don't need to use database

My opinion is that you should not use global variables, mainly I try to make viewController data specific for itself, the data should not be accessed from anywhere in your project.
When to use global variables in Swift

Mostly I make service protocols with implementations for specific view controllers.
Anyways, each controller should have its own part of data for example

class MyViewControllerOne {
private var data :Array<DataExampleOne>;
}

class MyViewControllerTwo {
private var data :Array<DataExampleTwo>;
}

Once you load data from your API, the data will be there, until you close your app.

You can always make another class which will contain this data and maybe predicates for cleaner approach for example :

protocol MyViewControllerOneService {
func search() -> Array<DataExampleOne>;
}

class MyViewControllerOneServiceImpl :MyViewControllerService {
public var data :Array<DataExampleOne>;

public func search(searchString :String) -> Array<DataExampleOne>{
let predicate = NSPredicate() ...
let filteredArray = self.data.filter { evaluate...}
return filteredArray;
}
}

I use similar approach, instead of saving data in my service classes (business logic), I got repositories which I use to get data from database. Since you don't have any database this approach is okay.

and then instead of

class MyViewControllerOne {
private var data :Array<DataExampleOne>;
}

you can use

class MyViewController {
private var myViewControllerService :MyViewControllerOneServiceImpl;
}

Hope it helps,
Best regards!

How to save huge nested array book locally (in json file) in swiftUI/swift from server?

Here is some code to download the book data from your server at:
"http://vadtaldhambooks.com/api/v1/get_book?book_id=58"
, and store it into a local file "testbookfile",
as a array of books. The code takes into consideration some of your previous questions/answers
and shows how to decode into an array of books the unusual json from the api, where the book
content txContent is a string (where it should be an array of book content).

Run the code with fetchData() in the DataModel, then click the "Save to file button" to save the data into a
local file called "testbookfile". Then comment out the fetchData() and use the loadFromFile() instead.

Since the call to the server is "http" instead of the required "https", you need to temporarily set your Info.plist
with:

<dict>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
</dict>

The code:

import SwiftUI

@main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}

// holds the books and functions to fetch and save them
class DataModel: ObservableObject, @unchecked Sendable {
@Published var books: [Book] = []
@Published var loading = false

let fileName = "testbookfile"
let urlString = "http://vadtaldhambooks.com/api/v1/get_book?book_id=58"

init() {
fetchData() // <-- to fetch the data from the server at urlString
// loadFromFile() // <-- get the data from local file, after it has been saved
}

func fetchData() {
Task {
let res: BookResponse? = await fetch()
if let response = res {
DispatchQueue.main.async {
self.books = response.books
}
}
}
}

// todo deal with errors
private func fetch<T: Decodable>() async -> T? {
if let url = URL(string: urlString) {
do {
let (data, _) = try await URLSession.shared.data(for: URLRequest(url: url))
let results = try JSONDecoder().decode(T.self, from: data)
return results
}
catch {
print("error: \(error)")
}
}
return nil
}

func loadFromFile() {
do {
let furl = try FileManager.default
.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
.appendingPathComponent(fileName)
.appendingPathExtension("json")
let data = try Data(contentsOf: furl)
let theBooks = try JSONDecoder().decode([Book].self, from: data)
DispatchQueue.main.async {
self.books = theBooks
}
} catch {
print("error: \(error)")
}
}

func saveToFile() {
do {
let furl = try FileManager.default
.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
.appendingPathComponent(fileName)
.appendingPathExtension("json")
print("---> writing file to: \(furl)")
let data = try JSONEncoder().encode(books)
try data.write(to: furl)
} catch {
print("---> error saveToFile: \(error)")
}
}

}

extension View {

func attString(_ str: String) -> AttributedString {
attributedString(from: str, font: Font.system(size: 22))
}

func attributedString(from str: String, font: Font) -> AttributedString {
if let theData = str.data(using: .utf16) {
do {
let theString = try NSAttributedString(data: theData, options: [.documentType: NSAttributedString.DocumentType.html], documentAttributes: nil)
var attaString = AttributedString(theString)
attaString.font = font
attaString.backgroundColor = .yellow
return attaString
} catch {
print("\(error)")
}
}
return AttributedString(str)
}

}

struct BookView: View {
@State var bookContent: BookContent

var body: some View {
ScrollView {
Text(attString(bookContent.title))
}.background(Color.yellow)
}

}

struct ContentView: View {
@StateObject var dataModel = DataModel()

var body: some View {
NavigationView {
VStack(alignment: .leading, spacing: 22) {
ForEach(dataModel.books) { book in
ScrollView {
LazyVStack(alignment: .leading, spacing: 22) {
Section(header: Text(book.title).font(.largeTitle).fontWeight(.heavy).foregroundColor(.yellow).padding(10)) {
ForEach(book.content) { bookContent in
OutlineGroup(bookContent.child ?? [], children: \.child) { item in
NavigationLink(destination: BookView(bookContent: item)) {
Text(bookContent.title).fontWeight(.heavy)
}.padding(10)
}
}
}.onAppear { dataModel.loading = false }
}
}
}
}
// remove the toolbar, after the data has been saved to file
.toolbar {
ToolbarItem(placement: .primaryAction) {
VStack {
if dataModel.loading {
ProgressView("Loading data …")
} else {
Button("Save to file") {
dataModel.saveToFile()
}
.padding(20)
.buttonStyle(BorderedButtonStyle())
}
}.padding(.top, 20)
}
}
.navigationTitle("Book list")
}.navigationViewStyle(.stack)
.onAppear { dataModel.loading = true }
}

}

struct BookResponse: Codable, Sendable {
var message: String = ""
var totalRecord: Int = 0
var totalPage: Int = 0
var nextPage: String = ""
var books: [Book] = []

enum CodingKeys: String, CodingKey {
case message
case totalRecord
case totalPage
case nextPage
case books = "data"
}
}

struct Book: Identifiable, Codable, Sendable {
let id = UUID().uuidString
var bookId: Int
var title: String
var content: [BookContent] = []
var coverImage: String

enum CodingKeys: String, CodingKey {
case bookId = "iBookId"
case title = "vTitle"
case content = "txContent"
case coverImage = "txCoverImage"
}

init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
bookId = try values.decode(Int.self, forKey: .bookId)
title = try values.decode(String.self, forKey: .title)
coverImage = try values.decode(String.self, forKey: .coverImage)
// decode the book content, when it comes as a String
if let theString = try? values.decode(String.self, forKey: .content) {
if let data = theString.data(using: .utf16) {
do {
content = try JSONDecoder().decode([BookContent].self, from: data)
} catch {
print("----> Book content decoding error: \(error)")
}
}
} else {
content = try values.decode([BookContent].self, forKey: .content)
}
}

}

struct BookContent: Identifiable, Codable, Sendable {
let id = UUID().uuidString
var title, type: String
var child: [BookContent]?
}

saving JSON response locally in database Objective C

Thanks guys! but I tried creating database using FMDB(since it was already integrated) and it creates new answer table properly but am having trouble with saving the above dictionary as its having dynamic Key- value pairing.

iOS non-persistent data structures

If you are not interested in persisting the edits (CRUD) for the given information, creating a model to be a representation (template) of your data would be a good choise, for instance:

// of course determining what's the data type of each property is up to you,
// or even giving them an initial value...
struct MyModel {
var country: String?
var tollNumber: String?
var TollFreeNumber: String?
var TollNumber2: String?
var ISOCode: String?
}

let containerArray = [MyModel(country: "country", tollNumber: "tollNumber", TollFreeNumber: "TollFreeNumber", TollNumber2: "TollNumber2", ISOCode: "ISOCode"),
MyModel(country: "country", tollNumber: "tollNumber", TollFreeNumber: "TollFreeNumber", TollNumber2: "TollNumber2", ISOCode: "ISOCode"),
MyModel(country: "country", tollNumber: "tollNumber", TollFreeNumber: "TollFreeNumber", TollNumber2: "TollNumber2", ISOCode: "ISOCode"), ...]

If you are required to read the given data from a file, I think that .plist file would be a good choice, it is easy to work with from the end-users perspective, also check this Q&A.

Although reading data directly from a struct instance -as mentioned in the first approach- should be better (speed wise), the benefit of working with a .plist file might be the ease of editing, all you have to do is replacing the updated file and that's it!

Remark: if we are talking about a small amount of data, speed issue won't be notable -almost- at all.

Also: if you are looking for a mechanism to persist data into your application, you might want to check this Q&A.

Hope this helped.

can not convert json to struct object

You are making a very common mistake.

You are ignoring the root object which is a dictionary and causes the error.

struct Root: Decodable {
let status : String
let result: [UserData]
}

struct UserData: Decodable {
let avatar: String
let city: String
let contribution: Int
let country: String
let friendOfCount: Int
let handle: String
let lastOnlineTimeSeconds: Int
let maxRank: String
let maxRating: Int
let organization: String
let rank: String
let rating: Int
let registrationTimeSeconds: Int
let titlePhoto: String
}

Forget JSONSerialization and use only JSONDecoder

And it's not a good idea to return meaningless enumerated errors. Use Error and return the real error.

You get the array with dataresponse.result

struct DataRequest { // name structs always with starting capital letter
let requestUrl: URL

init(){
self.requestUrl = URL(string: "https://codeforces.com/api/user.info?handles=abhijeet_ar")!
}
func getData(completionHandler: @escaping(Result<Root, Error>) -> Void) {

URLSession.shared.dataTask(with: self.requestUrl) { (data,response, error) in
guard let data = data else {
completionHandler(.failure(error!))
print("-------bye-bye--------")
return
}
do {
print("-------entered--------")
let dataresponse = try JSONDecoder().decode(Root.self, from: data)
completionHandler(.success(dataresponse))
}
catch {
completionHandler(.failure(error))
}

}.resume()
}
}

And consider that if status is not "OK" the JSON response could be different.

How to make offline database for my app?

I think what are you asking for is a mechanism to persist data into your application. There are several approaches to achieve this. Although it could be too broad to provide detailed answer, you might want to check as options:

Databases:

  • Core Data:

Core Data is an object graph and persistence framework provided by
Apple in the macOS and iOS operating systems. It was introduced in Mac
OS X 10.4 Tiger and iOS with iPhone SDK 3.0. It allows data
organized by the relational entity–attribute model to be serialized
into XML, binary, or SQLite stores. The data can be manipulated using
higher level objects representing entities and their relationships.
Core Data manages the serialized version, providing object lifecycle
and object graph management, including persistence. Core Data
interfaces directly with SQLite, insulating the developer from the
underlying SQL.

Wikipedia Resource.

Programming Guide.

  • SQLite -with a wrapper such as SQLite.swift-

SQLite is a relational database management system contained in a C
programming library. In contrast to many other database management
systems, SQLite is not a client–server database engine. Rather, it is
embedded into the end program.

Wikipedia resource.

  • Realm:

Realm is an open-source object database management system, initially
for mobile (Android/iOS), also available for platforms such as Xamarin
or React Native, and others, including desktop applications (Windows),
and is licensed under the Apache License.

Wikipedia resource.



Other Alternatives:

  • UserDefaults:

The UserDefaults class provides a programmatic interface for
interacting with the defaults system. The defaults system allows an
app to customize its behavior to match a user’s preferences. For
example, you can allow users to specify their preferred units of
measurement or media playback speed. Apps store these preferences by
assigning values to a set of parameters in a user’s defaults database.
The parameters are referred to as defaults because they’re commonly
used to determine an app’s default state at startup or the way it acts
by default.

  • Saving Data to plist files:

In the macOS, iOS, NeXTSTEP, and GNUstep programming frameworks,
property list files are files that store serialized objects. Property
list files use the filename extension .plist, and thus are often
referred to as p-list files.

Property list files are often used to store a user's settings. They
are also used to store information about bundles and applications, a
task served by the resource fork in the old Mac OS.

  • Saving Data to json files:

In computing, JavaScript Object Notation or JSON is an open-standard
file format that uses human-readable text to transmit data objects
consisting of attribute–value pairs and array data types (or any other
serializable value). It is a very common data format used for
asynchronous browser–server communication, including as a replacement
for XML in some AJAX-style systems.

Wikipedia resource.



Related Topics



Leave a reply



Submit