Here are two useful extension functions for decoding Json data into structs or objects of any type. The first one is an extension function of Bundle for decoding any JSON file stored in your local bundle. The second one is an extension function of URLSession for decoding any JSON data from remote URL.
1. Decode JSON from file stored in bundle
As said before, creating views in loop is easy with the ForEach and List in SwiftUI, therefore, for better maintainability or flexibility of change, we should have our struct model decodable from JSON files stored in our local bundle for creating list of views.
extension Bundle {
func decodeJson <T:Decodable> (_ type : T.Type , fileName : String,
dateDecodingStrategy: JSONDecoder.DateDecodingStrategy = .deferredToDate,
keyDecodingStrategy: JSONDecoder.KeyDecodingStrategy = .useDefaultKeys) -> T {
guard let url = self.url(forResource: fileName, withExtension: nil) else {
fatalError("Failed to load file ")
}
do {
let jsonData = try Data(contentsOf: url)
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = dateDecodingStrategy
decoder.keyDecodingStrategy = keyDecodingStrategy
let result = try decoder.decode(type, from: jsonData)
return result
}
catch {
fatalError("err:\(error)")
}
}
}
The above is a generic extension function of Bundle, which decodes any JSON file stored in your local bundle into any type specified by the placeholder T.
The Menus.json file:
To decode the above Menus.json stored in local bundle directory into an array of MenuItem, you just need to do as follows:
let menuItems = Bundle.main.decodeJson(
[MenuItem].self,"Menus.json")
If the specified JSON file is not found in your local bundle, your app will be forced to crash by fatalError and please make sure you have your JSON files added in your Copy Bundle Resources as below:
And also it’ll force crash your app with invalid json format.
The example below decodes Menus.json into array of MenuItems and used to create views in loop:
let menuItems = Bundle.main.decodeJson([MenuItem].self,
fileName: "Menus.json")
VStack (alignment: .leading, spacing: 20){
ForEach(menuItems) { item in
HStack(spacing: 20) {
Image(systemName: item.imageName)
.frame(width: 30, height: 30)
Text (item.name)
}
}
}
And the result shown on Xcode preview is as below:
2. Decode JSON file from remote URL
Here we have the extension function of URLSession, which is the same generic function that decodes the given urlString into any type specified by the placeholder type T.
The function’s first parameter is the placeholder type T, which you specify the type that needs to decode into. The second parameter is the urlString. Since URLSession works asynchrounously, the third parameter is a completion handler, which gives you a Result – an enum with a success and failure case, which are the decoded result and error message respectively.
The enum Result:
public enum Result<Success, Failure: Error> {
case success(Success)
case failure(Failure)
}
The extension function of URLSession:
extension URLSession {
func decodeJson <T:Decodable> (_ type : T.Type , urlString : String ,
completion: @escaping (Result<T, Error>)->Void,
dateDecodingStrategy: JSONDecoder.DateDecodingStrategy = .deferredToDate,
keyDecodingStrategy: JSONDecoder.KeyDecodingStrategy = .useDefaultKeys) {
guard let url = URL(string: urlString) else {
return
}
let task = self.dataTask(with: url, completionHandler: { data, response, error in
if let error = error {
completion(.failure(error))
}
else {
do {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = dateDecodingStrategy
decoder.keyDecodingStrategy = keyDecodingStrategy
let results = try decoder.decode(type, from: data!)
completion(.success(results))
}
catch {
completion(.failure(error))
}
}
})
task.resume()
}
}
To load and decode JSON file from a remote URL into array of MenuItem, we can just do as follows:
URLSession.shared.decodeJson([MenuItem].self,
urlString: "https://techchee.com/somejsons/Menus.json",
completion: { result in
switch(result){
// switch case on success
case .success(let menuItems) :
// handle the returned
// array of MenuItem
...
// case on failure
case .failure(let err) :
// handle the error
print("error :\(err)")
}
})
Let’s look at a more complete SwiftUI example:
A quick explanation of our example. It basically has a ZStack which the main view presenting a List view of MenuItem. And a ProgressView is shown only when the @State variable showLoading is true. Please note that ProgressView is only available since iOS 14.
@State private var menuItems : [MenuItem] = []
@State private var showLoading : Bool = false
@State private var showErrorAlert = false
@State private var errorMessage : String = ""
var body: some View {
ZStack {
List (menuItems) { item in
// HStack to display each MenuItem
...
}
// onAppear is used to load JSON from remote URL
.onAppear{
// set showLoading to true to show the ProgressView
self.showLoading = true
// Use the extension function to load from remote URL
URLSession.shared.decodeJson([MenuItem].self,
urlString: "https://techchee.com/somejsons/Menus.json",
completion: { result in
// handle case of success and failure here
}
)
}
// show an alert with the errorMessage, if showErrorAlert is set true
.alert(isPresented: $showErrorAlert) {
Alert(title: Text("Error!"),
message: Text(self.errorMessage), dismissButton: .default(Text("OK")))
}
// show a spinning ProgressView
// if showLoading is true
if $showLoading.wrappedValue {
ProgressView()
.scaleEffect(2, anchor: .center)
}
}
In the above, we make use of the onAppear() of the List view to start loading remote JSON and decode it by the URLSession.shared decodeJson(…).
In the completion handler, we handle the cases of result by assigning the @State menuItems while on success or setting the @State showErrorAlert to true and assigning the @State errorMessage while on error, as follows:
List (menuItems) { item in ...}
.onAppear{
if self.menuItems.count == 0 {
self.showLoading = true
URLSession.shared.decodeJson([MenuItem].self,
urlString: "https://techchee.com/somejsons/Menus.json",
completion: { result in
switch(result){
case .success(let mItems) :
self.menuItems = mItems
case .failure(let err) :
self.showErrorAlert = true
self.errorMessage = err.localizedDescription
}
self.showLoading = false // hide ProgressView
}
)
}
}
The result is as below when fetching of the remote JSON file was successful.
So when we change the URL with a file name that does NOT exist on the server, then it’ll show you an alert of the error message :
Please note that we don’t specifically handle any http response code, thus a file not found page sent by the server will just produce an error of invalid JSON format.
The complete source code for this tutorial can be found on GitHub.
3 thoughts on “Useful extension functions to decode Json from Bundle and remote URL with SwiftUI examples”