How to Read The Property Values of a JSON Error Object Using Combine in Swift

How do I read the property values of a JSON error object using Combine in Swift?

I am not sure from where you will be getting your data as in JSON response there is no Key for data. Before writing below code my understanding was that you want to check error and statusCode from the mentioned JSON response and then move forward with your business logic. The below code is to give you a vague idea of how we can do that.

    enum CustomError: Error {

case custom(_ error: String)
case unknownStatusCode
case errorOccurred
}

let url = URL(string: "http://abc336699.com/create")

func load() -> AnyPublisher<Data,CustomError> {
URLSession.shared.dataTaskPublisher(for: url!)
.map(\.data)
.tryMap { (data) -> Data in
let genericModel = try? JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: AnyObject]

if let statusCode = genericModel?["statusCode"] as? String {
switch statusCode {
case "200":
guard let data = genericModel?["message"] as? Data else {
throw CustomError.custom("Parsing error")
}
return data
default:
if let error = genericModel?["error"] as? String {
throw CustomError.custom(error)
} else {
throw CustomError.unknownError
}
}
}
throw CustomError.errorOccurred
}
.decode(type: YourCustomDecodableModel.self, decoder: JSONDecoder())
.mapError({ $0 as? CustomError ?? CustomError.errorOccurred })
.eraseToAnyPublisher()
}

Reading in a JSON File Using Swift

Follow the below code :

if let path = NSBundle.mainBundle().pathForResource("test", ofType: "json")
{
if let jsonData = NSData(contentsOfFile: path, options: .DataReadingMappedIfSafe, error: nil)
{
if let jsonResult: NSDictionary = NSJSONSerialization.JSONObjectWithData(jsonData, options: NSJSONReadingOptions.MutableContainers, error: nil) as? NSDictionary
{
if let persons : NSArray = jsonResult["person"] as? NSArray
{
// Do stuff
}
}
}
}

The array "persons" will contain all data for key person. Iterate throughs to fetch it.

Swift 4.0:

if let path = Bundle.main.path(forResource: "test", ofType: "json") {
do {
let data = try Data(contentsOf: URL(fileURLWithPath: path), options: .mappedIfSafe)
let jsonResult = try JSONSerialization.jsonObject(with: data, options: .mutableLeaves)
if let jsonResult = jsonResult as? Dictionary<String, AnyObject>, let person = jsonResult["person"] as? [Any] {
// do stuff
}
} catch {
// handle error
}
}

How to Implement API for both Success and Failure Response with Combine Swift

I beg for your patience. I think I understand the problem, but I'm having a hard time lining it up to the code you've given. Your fetchFunction is particularly odd and I don't understand what your protocol is trying to accomplish.

Let me start with the problem statement and explore a solution. I'll do it step-by-step so this will be a long response. The tl;dr is a Playground at the end.

I have an API and if my data is valid the API will give the correct response, for which I need to decode with the respective struct in swift.
If my data is wrong the API will fail and it will produce an error response which is a different struct.

So we need two structs. One for success and one for failure. I'll make some up:

struct SuccessfulResult : Decodable {
let interestingText : String
}

struct FailedResult : Decodable {
let errorCode : Int
let failureReason : String
}

Based on that, request to the network can:

  • Return success data to decode into SuccessfulResult
  • Return failure data to decode into FailedResult
  • Fail because of a low-level error (e.g. The network is unreachable).

Let's create a type for "The network worked just fine and gave me either success data or failure data":

enum NetworkData {
case success(Data)
case failure(Data)
}

I'll use Error for low-level errors.

With those types an API request can be represented as a publisher of the type AnyPublisher<NetworkData, Error>

But that's not what you asked for. You want to parse the data into SuccessfulResult or FailedResult. This also raises the possibility that JSON parsing fails which I will also sweep under the rug of a generic Error.

We need a data type to represent the parsed variant of NetworkData:

enum ParsedNetworkData {
case success(SuccessfulResult)
case failure(FailedResult)
}

Which means the real Network request type you've asked for is a publisher of the type AnyPublisher<ParsedNetworkData,Error>

We can write a function to transform a Data bearing network request, AnyPublisher<NetworkData,Error>, into an AnyPublisher<ParsedNetworkData,Error>.

One way to write that function is:

func transformRawNetworkRequest(_ networkRequest: AnyPublisher<NetworkData,Error>) -> AnyPublisher<ParsedNetworkData, Error> {

let decoder = JSONDecoder()
return networkRequest
.tryMap { networkData -> ParsedNetworkData in
switch(networkData) {
case .success(let successData):
return ParsedNetworkData.success(try decoder.decode(SuccessfulResult.self, from: successData))
case .failure(let failureData):
return ParsedNetworkData.failure(try decoder.decode(FailedResult.self, from: failureData))
}
}
.eraseToAnyPublisher()
}

To exercise the code we can write a function to create a fake network request and add some code that tries things out. Putting it all together into a playground you get:

import Foundation
import Combine

struct SuccessfulResult : Decodable {
let interestingText : String
}

struct FailedResult : Decodable {
let errorCode : Int
let failureReason : String
}

enum NetworkData {
case success(Data)
case failure(Data)
}

enum ParsedNetworkData {
case success(SuccessfulResult)
case failure(FailedResult)
}

func transformRawNetworkRequest(_ networkRequest: AnyPublisher<NetworkData,Error>) -> AnyPublisher<ParsedNetworkData, Error> {

let decoder = JSONDecoder()
return networkRequest
.tryMap { networkData -> ParsedNetworkData in
switch(networkData) {
case .success(let successData):
return ParsedNetworkData.success(try decoder.decode(SuccessfulResult.self, from: successData))
case .failure(let failureData):
return ParsedNetworkData.failure(try decoder.decode(FailedResult.self, from: failureData))
}
}
.eraseToAnyPublisher()
}

func fakeNetworkRequest(shouldSucceed: Bool) -> AnyPublisher<NetworkData,Error> {
let successfulBody = """
{ "interestingText" : "This is interesting!" }
""".data(using: .utf8)!

let failedBody = """
{
"errorCode" : -4242,
"failureReason" : "Bogus! Stuff went wrong."
}
""".data(using: .utf8)!

return Future<NetworkData,Error> { fulfill in
let delay = Set(stride(from: 100, to: 600, by: 100)).randomElement()!

DispatchQueue.global(qos: .background).asyncAfter(
deadline: .now() + .milliseconds(delay)) {
if(shouldSucceed) {
fulfill(.success(NetworkData.success(successfulBody)))
} else {
fulfill(.success(NetworkData.failure(failedBody)))
}
}
}.eraseToAnyPublisher()
}

var subscriptions = Set<AnyCancellable>()
let successfulRequest = transformRawNetworkRequest(fakeNetworkRequest(shouldSucceed: true))
successfulRequest
.sink(receiveCompletion:{ debugPrint($0) },
receiveValue:{ debugPrint("Success Result \($0)") })
.store(in: &subscriptions)

let failedRequest = transformRawNetworkRequest(fakeNetworkRequest(shouldSucceed: false))
failedRequest
.sink(receiveCompletion:{ debugPrint($0) },
receiveValue:{ debugPrint("Failure Result \($0)") })
.store(in: &subscriptions)

How to decode a property with type of JSON dictionary in Swift [45] decodable protocol

With some inspiration from this gist I found, I wrote some extensions for UnkeyedDecodingContainer and KeyedDecodingContainer. You can find a link to my gist here. By using this code you can now decode any Array<Any> or Dictionary<String, Any> with the familiar syntax:

let dictionary: [String: Any] = try container.decode([String: Any].self, forKey: key)

or

let array: [Any] = try container.decode([Any].self, forKey: key)

Edit: there is one caveat I have found which is decoding an array of dictionaries [[String: Any]] The required syntax is as follows. You'll likely want to throw an error instead of force casting:

let items: [[String: Any]] = try container.decode(Array<Any>.self, forKey: .items) as! [[String: Any]]

EDIT 2: If you simply want to convert an entire file to a dictionary, you are better off sticking with api from JSONSerialization as I have not figured out a way to extend JSONDecoder itself to directly decode a dictionary.

guard let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else {
// appropriate error handling
return
}

The extensions

// Inspired by https://gist.github.com/mbuchetics/c9bc6c22033014aa0c550d3b4324411a

struct JSONCodingKeys: CodingKey {
var stringValue: String

init?(stringValue: String) {
self.stringValue = stringValue
}

var intValue: Int?

init?(intValue: Int) {
self.init(stringValue: "\(intValue)")
self.intValue = intValue
}
}

extension KeyedDecodingContainer {

func decode(_ type: Dictionary<String, Any>.Type, forKey key: K) throws -> Dictionary<String, Any> {
let container = try self.nestedContainer(keyedBy: JSONCodingKeys.self, forKey: key)
return try container.decode(type)
}

func decodeIfPresent(_ type: Dictionary<String, Any>.Type, forKey key: K) throws -> Dictionary<String, Any>? {
guard contains(key) else {
return nil
}
guard try decodeNil(forKey: key) == false else {
return nil
}
return try decode(type, forKey: key)
}

func decode(_ type: Array<Any>.Type, forKey key: K) throws -> Array<Any> {
var container = try self.nestedUnkeyedContainer(forKey: key)
return try container.decode(type)
}

func decodeIfPresent(_ type: Array<Any>.Type, forKey key: K) throws -> Array<Any>? {
guard contains(key) else {
return nil
}
guard try decodeNil(forKey: key) == false else {
return nil
}
return try decode(type, forKey: key)
}

func decode(_ type: Dictionary<String, Any>.Type) throws -> Dictionary<String, Any> {
var dictionary = Dictionary<String, Any>()

for key in allKeys {
if let boolValue = try? decode(Bool.self, forKey: key) {
dictionary[key.stringValue] = boolValue
} else if let stringValue = try? decode(String.self, forKey: key) {
dictionary[key.stringValue] = stringValue
} else if let intValue = try? decode(Int.self, forKey: key) {
dictionary[key.stringValue] = intValue
} else if let doubleValue = try? decode(Double.self, forKey: key) {
dictionary[key.stringValue] = doubleValue
} else if let nestedDictionary = try? decode(Dictionary<String, Any>.self, forKey: key) {
dictionary[key.stringValue] = nestedDictionary
} else if let nestedArray = try? decode(Array<Any>.self, forKey: key) {
dictionary[key.stringValue] = nestedArray
}
}
return dictionary
}
}

extension UnkeyedDecodingContainer {

mutating func decode(_ type: Array<Any>.Type) throws -> Array<Any> {
var array: [Any] = []
while isAtEnd == false {
// See if the current value in the JSON array is `null` first and prevent infite recursion with nested arrays.
if try decodeNil() {
continue
} else if let value = try? decode(Bool.self) {
array.append(value)
} else if let value = try? decode(Double.self) {
array.append(value)
} else if let value = try? decode(String.self) {
array.append(value)
} else if let nestedDictionary = try? decode(Dictionary<String, Any>.self) {
array.append(nestedDictionary)
} else if let nestedArray = try? decode(Array<Any>.self) {
array.append(nestedArray)
}
}
return array
}

mutating func decode(_ type: Dictionary<String, Any>.Type) throws -> Dictionary<String, Any> {

let nestedContainer = try self.nestedContainer(keyedBy: JSONCodingKeys.self)
return try nestedContainer.decode(type)
}
}

Swift Combine: handle no data before decode without an error

I would suggest creating a separate Publisher for handling the specific endpoint which doesn't return any Data. You can use a tryMap to check the HTTP status code and throw an error in case it's not in the accepted range. If you don't care about the result, only that there was a successful response, you can map to a Void. If you care about the result (or the status code), you can map to that too.

extension URLSession.DataTaskPublisher {
func emptyBodyResponsePublisher() -> AnyPublisher<Void, CustomError> {
tryMap { _, response in
guard let httpResponse = response as? HTTPURLResponse else { throw CustomError.nonHTTPResponse }
let statusCode = httpResponse.statusCode
guard (200..<300).contains(statusCode) else { throw CustomError.incorrectStatusCode(statusCode) }
return Void()
}.mapError { CustomError.network($0) }
.eraseToAnyPublisher()
}
}

xx is get-only property - How to Assign decoded Json Data to class (nested view model) Swift

I am posting my answer after getting the right clue from Joakim & Ptit's comments. As per their advice, I have separated (data) model and view model for the watch app.

It was a little time-consuming, but I managed to update the app design, and it enabled me to store transferred data into appropriate view model class.

See below code extract for future reference.

WatchDayProgram Struct (Data Model)

import Foundation
import Combine

struct WatchDayProgram: Codable {

var watchDayProgramSequence: Int = 0
var exerciseVM: [WatchExercise]

enum CodingKeys: String, CodingKey {

case dayProgramSeq
case exerciseVM

}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)

watchDayProgramSequence = try container.decode(Int.self, forKey: .dayProgramSeq)
exerciseVM = try container.decode([WatchExercise].self, forKey: .exerciseVM)

do {
watchDayProgramSequence = try container.decode(Int.self, forKey: .dayProgramSeq)
} catch DecodingError.typeMismatch {
do {
watchDayProgramSequence = try Int(container.decode(String.self, forKey: .dayProgramSeq)) ?? 0
}
}

}

func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)

try container.encode(watchDayProgramSequence, forKey: .dayProgramSeq)
try container.encode(exerciseVM, forKey: .exerciseVM)

}//encode function ends

}

WatchDayProgViewModel

import Foundation
import Combine

class ReceivedDayProgViewModel: ObservableObject {

@Published var workoutModel = WorkoutModel()
@Published var watchDayProgramSequence: Int = 0
@Published var exerciseVM: [WatchExerciseViewModel] = []


init() {
//initiliazed empty view model via this function, so empty view model class can be loaded onto Watch app when it starts while awaiting for the file transfer to be complete
}

}

ContentView

import SwiftUI
import WatchKit
import Combine
import Communicator

struct ContentView: View {
@State var selectedTab = 0

@StateObject var receivedDayProgramVM = ReceivedDayProgViewModel()

var body: some View {

NavigationView {

//Show WatchLoadingView, if dayprogramVM is being fetched
if receivedDayProgramVM.exerciseVM.isEmpty == true {

WatchLoadingView()
.onAppear {

Blob.observe { Blob in

if Blob.identifier == "dayProgramData" {

let ReceivedDayProgVMData = Blob.content
print("WatchConnectivity blob setData printing \(ReceivedDayProgVMData)")

let decoder = JSONDecoder()
let ReceivedDayProgViewModel = try! decoder.decode(WatchDayProgram.self, from: ReceivedDayProgVMData)


for (i, exercise) in ReceivedDayProgViewModel.exerciseVM.enumerated() {

DispatchQueue.main.async {
receivedDayProgramVM.exerciseVM.append(WatchExerciseViewModel(exercise: exercise))
}


} //Blob for "dayProgramData" code ends


} //Blob observation code ends
} else {

TabView(selection: $selectedTab) {

WatchControlView().id(0)
SetInformationView().id(1)
SetRestDetailView().id(2)
//Check if WatchControlView can be vertically scrollable
NowPlayingView().id(3)
}
.environmentObject(receivedDayProgramVM)
}

}
}
}

Unable to get the response from URL using combine with SwiftUI

First of all, your error comes from the fact you want to return AnyPublisher<LoginModel, Error> but you map your response as .decode(type: LoginResponse.self, decoder: JSONDecoder()) which doesn't match your json response.

In the second time, I would use a Basic Authorization as a body of your URL request as it is to send user credentials with a password, which must be protected. Do you have access to the server side? How is the backend handling this post request?
Is it with Authorization or Content-Type? I would put the two solutions, try to find the one that is set in the server side.

Your LoginModel must match your json response. I noticed their was expertToken missing:

struct LoginModel: Codable {

let success: Bool
let token: String
let expertToken: String
let message: String

enum CodingKeys: String, CodingKey {
case success
case token
case expertToken = "expert-token"
case message
}
}

So I would create the LoginService class this way:

final class LoginService {

/// The request your use when the button is pressed.
func logIn(username: String, password: String) -> AnyPublisher<LoginModel, Error> {

let url = URL(string: "http://your.api.endpoints/")!
let body = logInBody(username: username, password: password)
let urlRequest = basicAuthRequestSetup(url: url, body: body)

return URLSession.shared
.dataTaskPublisher(for: urlRequest)
.receive(on: DispatchQueue.main)
.tryMap { try self.validate($0.data, $0.response) }
.decode(
type: LoginModel.self,
decoder: JSONDecoder())
.eraseToAnyPublisher()
}

/// The body for a basic authorization with encoded credentials.
func logInBody(username: String, password: String) -> String {

let body = String(format: "%@:%@",
username,
password)

guard let bodyData = body.data(using: .utf8) else { return String() }

let encodedBody = bodyData.base64EncodedString()
return encodedBody
}

/// The authorization setup
func basicAuthRequestSetup(url: URL, body: String) -> URLRequest {

var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = "POST"
urlRequest.setValue("Basic \(body)",
forHTTPHeaderField: "Authorization")

return urlRequest
}

/// Validation of the Data and the response.
/// You can handle response with status code for more precision.
func validate(_ data: Data, _ response: URLResponse) throws -> Data {
guard let httpResponse = response as? HTTPURLResponse else {
throw NetworkError.unknown
}
guard (200..<300).contains(httpResponse.statusCode) else {
throw networkRequestError(from: httpResponse.statusCode)
}
return data
}

/// Handle the status code errors to populate to user.
func networkRequestError(from statusCode: Int) -> Error {
switch statusCode {
case 401:
return NetworkError.unauthorized
default:
return NetworkError.unknown
}
}

/// Define your different Error here that can come back from
/// your backend.
enum NetworkError: Error, Equatable {

case unauthorized
case unknown
}
}

So if you use a simple Content-Type, your body would be this one below. Replace from the code above logInBody(username:password:) -> String and basicAuthRequestSetup(url:body:) -> URLRequest

/// Classic body for content type.
/// Keys must match the one in your server side.
func contentTypeBody(username: String, password: String) -> [String: Any] {
[
"username": username,
"password": password
] as [String: Any]
}

/// Classic Content-Type but not secure. To avoid when having
/// passwords.
func contentTypeRequestSetup(url: URL,
body: [String: Any]) -> URLRequest {

var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = "POST"
urlRequest.setValue("application/json",
forHTTPHeaderField: "Content-Type")
urlRequest.httpBody = try? JSONSerialization.data(withJSONObject: body)

return urlRequest
}

I would then create a ViewModel to handle the logic that will be passed in your View.

final class OnboardingViewModel: ObservableObject {

var logInService = LoginService()

var subscriptions = Set<AnyCancellable>()

func logIn() {
logInService.logIn(username: "Shubhank", password: "1234")
.sink(receiveCompletion: { completion in
print(completion) },
receiveValue: { data in
print(data.expertToken) }) // This is your response
.store(in: &subscriptions)
}
}

And now, in your ContentView, you can pass the view model login action inside the button:

struct ContentView: View {

@ObservedObject var viewModel = OnboardingViewModel()

var body: some View {
Button(action: { viewModel.logIn() }) {
Text("Log In")
}
}
}

Fatal error when publishing UserDefaults with Combine

As you found out you can not observe your custom types directly, but you could add a possibility to observe the data change and decode that data to your custom type in your View model:

extension UserDefaults{
// Make it private(set) so you cannot use this from the outside and set arbitary data by accident
@objc dynamic private(set) var observableRatedProductsData: Data? {
get {
UserDefaults.standard.data(forKey: "ratedProducts")
}
set { UserDefaults.standard.set(newValue, forKey: "ratedProducts") }
}

var ratedProducts: [Product]{
get{
guard let data = UserDefaults.standard.data(forKey: "ratedProducts") else { return [] }
return (try? PropertyListDecoder().decode([Product].self, from: data)) ?? []
} set{
// set the custom objects array through your observable data property.
observableRatedProductsData = try? PropertyListEncoder().encode(newValue)
}
}
}

and the observer in your init:

UserDefaults.standard.publisher(for: \.observableRatedProductsData)
.map{ data -> [Product] in
// check data and decode it
guard let data = data else { return [] }
return (try? PropertyListDecoder().decode([Product].self, from: data)) ?? []
}
.receive(on: RunLoop.main) // Needed if changes come from background
.assign(to: &$ratedProducts) // assign it directly


Related Topics



Leave a reply



Submit