Swift map Algolia hit to Model - ios

I need to map the JSON object from Algolia to a Model.
Here's my ViewModel:
import Foundation
import AlgoliaSearchClient
class AlgoliaViewModel: ObservableObject {
#Published var idList: [MySearchModel] = []
func search(text: String, index: String) {
let client = SearchClient(appID: "XXX", apiKey: "XXX")
let index = client.index(withName: IndexName(rawValue: index))
let query = Query(text)
index.search(query: query) { result in
if case .success(let response) = result {
print("Response: \(response)")
do {
let hits: Array = response.hits
DispatchQueue.main.async {
self.idList = hits.map({
MySearchModel(searchValue: $0.objectID.rawValue)
})
print(self.idList)
}
}
catch {
print("JSONSerialization error:", error)
}
}
}
}
}
For the moment, I just have a Model with the objectID as searchValue.
How can I access to all other attributes of my Object and map them to a Model?

import Foundation
import AlgoliaSearchClient
class AlgoliaViewModel: ObservableObject {
#Published var list: [MySearchModel] = []
func search(text: String, index: String) {
let client = SearchClient(appID: "XXX", apiKey: "XXX")
let index = client.index(withName: IndexName(rawValue: index))
let query = Query(text)
index.search(query: query) { result in
if case .success(let response) = result {
print("Response: \(response)")
do {
let hits: Array = response.hits
DispatchQueue.main.async {
self.list = hits.map({MySearchModel.init})
print(self.list)
}
}
catch {
print("JSONSerialization error:", error)
}
}
}
}
}

Related

How to show and hide loader on Network layer in swiftUI?

I have a Global Singleton network class which handles the response of the API. I want to handle the loader from this class I don't want to manage from each ViewModel individually. I just want to toggle variabe "isLoading" in the ContentView from my global Network class "APIService" But no idea how would I do that?
class APIService {
static let shared = APIService()
private let timeInterval = 60
func makeApiTypeRequest<T: Codable>(
url: String,
param: [String: Any]? = nil,
methodType: HttpMethodType,
expecting: T.Type,
passToken: Bool = true,
completion: #escaping (Result<T, Error>)->Void) {
guard let url = URL(string: url) else {
return
}
var request = URLRequest(url: url, cachePolicy: .reloadIgnoringCacheData, timeoutInterval: TimeInterval(timeInterval))
if let param = param {
let finalData = try? JSONSerialization.data(withJSONObject: param)
request.httpBody = finalData
}
request.httpMethod = methodType.rawValue //"post"
request.addValue("application/json", forHTTPHeaderField: "content-type")
if passToken {
let data = KeychainHelper.standard.read(service: Constant.tokenKey)
if let data = data {
let accessToken = String(data: data, encoding: .utf8)
if let accessToken, accessToken != "" {
let headers = ["Content-Type": "application/json", "Authorization": "Bearer " + accessToken]
request.allHTTPHeaderFields = headers
}
}
}
URLSession.shared.dataTask(with: request) { (data,response,error) in
do {
if let error = error {
completion(.failure(error))
return
}
if let data = data {
if let httpStatus = response as? HTTPURLResponse {
switch httpStatus.statusCode {
case 400:
completion(.failure(CustomError.forbidden))
case 200:
let respObj = try JSONDecoder().decode(T.self, from: data)
completion(.success(respObj))
case 500:
// token expired
DispatchQueue.main.async {
NotificationCenter.default.post(name: Notification.Name("test.token.expired"), object: nil)
}
completion(.failure(CustomError.tokenExpired))
default:
completion(.failure(CustomError.unknown))
}
}
} else {
completion(.failure(CustomError.unknown))
print("no data found")
}
} catch(let error) {
completion(.failure(CustomError.unknown))
print("Error A123 \(error.localizedDescription)")
}
}.resume()
}
}
ContenView
struct ContentView: View {
#EnvironmentObject var session: SessionManager
#State var showMenu: Bool = false
#State var isLoading: Bool = false
#State var isAppInstalled: Bool = false
let pub = NotificationCenter.default
.publisher(for: NSNotification.Name("test.token.expired"))
var body: some View {
return NavigationView {
ZStack {
if isAppInstalled {
if session.currentUserState == .loggedIn {
GeometryReader { geometry in
TabContainer(showMenu: $showMenu)
.frame(width: geometry.size.width, height: geometry.size.height)
if self.showMenu {
MenuView()
.frame(width: geometry.size.width/2)
}
}
} else {
LoginView(showMenu: $showMenu)
}
if isLoading {
Loading()
.ignoresSafeArea()
}
} else {
GettingStartedView(isInstalled: $isAppInstalled)
}
}
.onAppear {
if GettingStartedPersistance.shared.isAppAlreadyInstalled() {
isAppInstalled = true
}
}
.onReceive(pub) { (output) in
self.session.signout()
}
}//.allowsHitTesting(!self.isLoading)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Sample ViewModel
class LoginViewModel: ObservableObject {
#Published var loginResponse:LoginResponseModel?
#Published var isNavigatedToDashBaord = false
#Published var errorMessage = ""
#Published var isErrorPresented = false
private func validateInputFields(email: String, password: String)->Bool {
if email.isEmpty {
errorOccured(err: "Please enter email")
return false
}
if password.isEmpty {
errorOccured(err: "Please enter password")
return false
}
if !Utils.shared.isValidEmail(strToValidate: email) {
errorOccured(err: "Please enter a valid email")
return false
}
if password.count < 5 {
errorOccured(err: "Password must be atleast 5 characters long")
return false
}
return true
}
func login(email: String, password: String, completion: #escaping (()->())) {
if !validateInputFields(email: email, password: password) {
completion()
return
}
var param = [String: Any]()
param["username"] = email
param["password"] = password
APIService.shared.makeApiTypeRequest(url: APIURLConstant.loginUrl, param: param, methodType: .post, expecting: LoginResponseModel.self, passToken: false) { result in
switch result {
case .success(let loginData):
if loginData.authToken != "" && loginData.authToken != nil {
DispatchQueue.main.async {
self.saveUserToken(token: loginData.authToken ?? "")
self.loginResponse = loginData
self.successlogIn()
completion()
}
} else {
self.errorOccured(err: "Login failled")
completion()
}
case .failure( _):
self.errorOccured(err: "Login failled")
completion()
}
}
}
private func errorOccured(err: String) {
DispatchQueue.main.async {
self.errorMessage = err
self.isErrorPresented = true
}
}
private func successlogIn() {
DispatchQueue.main.async {
self.errorMessage = ""
self.isErrorPresented = false
self.isNavigatedToDashBaord = true
}
}
private func saveUserToken(token: String) {
let accessToken = Data(token.utf8)
KeychainHelper.standard.save(accessToken, service: Constant.tokenKey)
}
}

How can i use this new get method for other models?

class Network {
func getingData(completion : #escaping ([Model]) -> ()) async {
guard let url = URL(string: "https://jsonplaceholder.typicode.com/posts") else { return }
do {
let (data, _) = try await URLSession.shared.data(from: url)
if let posts = try? JSONDecoder().decode([Model].self, from: data) {
completion(posts)
}
}
catch {
print("error")
}
}
}
You could try something like this approach, where getData works with Decodable, as was mentioned in the previous answer.
In this particular example an array of Decodable.
struct Post: Decodable, Identifiable {
let userId: Int
let id: Int
let title: String
let body: String
var comments: [Comment]?
}
struct Comment: Decodable, Identifiable {
let postId: Int
let id: Int
let name: String
let email: String
let body: String
}
struct ContentView: View {
let client = Network()
#State var posts: [Post] = []
var body: some View {
List {
ForEach(posts, id: \.id) { post in
Text(post.title)
}
}
.task {
posts = await client.getData(from: "https://jsonplaceholder.typicode.com/posts")
// all comments from the first post
let comments: [Comment] = await client.getData(from: "https://jsonplaceholder.typicode.com/posts/\(posts[0].id)/comments")
print("\n---> comments: \(comments)")
}
}
}
class Network {
func getData<T: Decodable>(from urlString: String) async -> [T] {
guard let url = URL(string: urlString) else {
print(URLError(.badURL))
return [] // <-- todo, deal with errors
}
do {
let (data, response) = try await URLSession.shared.data(for: URLRequest(url: url))
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
print(URLError(.badServerResponse))
return [] // <-- todo, deal with errors
}
let results = try JSONDecoder().decode([T].self, from: data)
return results
}
catch {
return [] // <-- todo, deal with errors
}
}
}
Is it what you're looking for?
import Foundation
class Network {
func getingData<Model: Decodable>(completion : #escaping ([Model]) -> ()) async {
guard let url = URL(string: "https://jsonplaceholder.typicode.com/posts") else {
return
}
do {
let (data, _) = try await URLSession.shared.data(from: url)
if let posts = try? JSONDecoder().decode([Model].self, from: data) {
completion(posts)
}
} catch {
print("error")
}
}
}
If so, you only need to declare the Model type as generic. The only thing you need Model to conform is Decodable (the requirement of the JSONDecoder().decode([Model].self, from: data) call).

JSON Decoding Not Populating Table View

I am trying to parse data from the website movieDatabase.com However there's some issue decoding the data to json and populating my table view.I am not sure why this is happening. Please I need help spotting out the problem. Here's my code. https://github.com/lexypaul13/Movie-Browser/tree/main/Movie-Browser
struct Movies: Codable {
let overview:String?
let original_title: String?
let poster_path:String
}
struct ApiResponse:Codable, Hashable {
let page:Int
let shows:[Movies]
enum CodingKeys:String, CodingKey {
case page = "page"
case shows = "results"
}
}
class NetworkManger{
enum EndPoint{
case showList
}
static let shared = NetworkManger()
private let baseURL : String
private var apiKeyPathCompononent :String
private init(){
self.baseURL = "https://api.themoviedb.org/3/movie/now_playing?"
self.apiKeyPathCompononent = "api_key=a07e22bc18f5cb106bfe4cc1f83ad8ed"
}
private var jsonDecoder:JSONDecoder = {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
return decoder
}()
func get<T:Decodable>(_ endPoints: EndPoint, urlString: String, completed:#escaping(Result<T?,ErroMessage>)->Void){
guard let url = urlBuilder(endPoint: endPoints) else {
completed(.failure(.invalidURL))
return
}
let task = URLSession.shared.dataTask(with: url){ data, response, error in
if let _ = error {
completed(.failure(.unableToComplete))
return
}
guard let response = response as? HTTPURLResponse, response.statusCode==200 else {
print(ErroMessage.invalidResponse.rawValue)
completed(.failure(.invalidResponse))
return
}
guard let data = data else{
completed(.failure(.invalidData))
return
}
do{
let apiResponse = try self.jsonDecoder.decode([T].self, from: data)
DispatchQueue.main.async {
completed(.success(apiResponse as? T))
}
} catch{
print(ErroMessage.invalidData.rawValue)
}
}
task.resume()
}
private func urlBuilder(endPoint:EndPoint )->URL?{
switch endPoint {
case .showList:
return URL(string: baseURL + apiKeyPathCompononent )
}
}
func getMovies(){
NetworkManger.shared.get(.showList, urlString: "") { [weak self] (result: Result<[Movies]?,ErroMessage> ) in
guard let self = self else { return }
switch result{
case .success(let movies):
self.movies = movies ?? []
DispatchQueue.main.async {self.tableView.reloadData()}
case .failure(let error):
print(error.localizedDescription)
}
}
}
}
The root object returned from the api is your ApiResult struct. This contains an array of movies (which you have mapped to the shows property of the ApiResult)
You need to change the getMovies function so that the right generic type can be inferred and the json decoder can do the right thing
func getMovies(){
NetworkManger.shared.get(.showList, urlString: "") { [weak self] (result: Result<ApiResult,ErroMessage> ) in
guard let self = self else { return }
switch result{
case .success(let apiResult):
self.movies = apiResult.shows
DispatchQueue.main.async {self.tableView.reloadData()}
case .failure(let error):
print(error.localizedDescription)
}
}
}

Load and save struct into UserDefaults

I am trying to save the user in UserDefaults from a struct that fetches the data from an API when a user logs in successfully.
Here is my Webservice class:
import Foundation
import UIKit
struct Resource<T: Codable> {
let url : URL
let httpMethod = HTTPMethod.post
var body : Data? = nil
}
extension Resource {
init(url: URL) {
self.url = url
}
}
enum HTTPMethod : String {
case get = "GET"
case post = "POST"
}
enum NetworkingError: Error {
case domainError
case badResponse
case encodingError
case decodingError
}
class Webservice {
func load<T>(resource: Resource<T>, caller: UIViewController ,completion: #escaping (Result<T, NetworkingError>) -> Void) {
var request = URLRequest(url: resource.url)
request.httpMethod = resource.httpMethod.rawValue
request.httpBody = resource.body
request.addValue("application/JSON", forHTTPHeaderField: "Content-Type")
URLSession.shared.dataTask(with: request) { (data, response, error) in
guard let data = data, error == nil else {
return completion(.failure(.domainError))
}
let json = try? JSONSerialization.jsonObject(with: data, options: [])
print(json)
do {
let result = try JSONDecoder().decode(T.self, from: data)
//save to UserDefaults
UserDefaults.standard.set(PropertyListEncoder().encode(T), forKey: "user")\\ here I am getting error that T.type does not conform to Encodable protocol.
completion(.success(result))
}catch {
do {
if let result = try? JSONDecoder().decode(LoginErrorResponse.self, from: data){
print(result.errors.msg)
DispatchQueue.main.async {
let alert = AlertService().alert(message: "\(result.errors.msg[0])")
caller.present(alert, animated: true)
}
completion(.failure(.decodingError))
}
if let result = try? JSONDecoder().decode(SignUpErrorResponse.self, from: data){
print(result.errors.msg)
DispatchQueue.main.async {
let alert = AlertService().alert(message: "\(result.errors.msg)")
caller.present(alert, animated: true)
}
completion(.failure(.decodingError))
}
}
}
}.resume()
}
}
Here is my model class:
import Foundation
struct User: Encodable, Decodable {
let name: String
let email: String
let password: String
let first_name: String
let last_name: String
}
extension User {
static var all : Resource<User> = {
guard let url = URL(string: "http://orderahead.gagzweblab.xyz:3001/login") else {
fatalError("url is incorrect")
}
return Resource<User>(url: url)
}()
static func create(vm : UserViewModel) -> Resource<UserResponseModel?> {
let user = User(vm)
guard let url = URL(string: "http://orderahead.gagzweblab.xyz:3001/register") else {
fatalError("url is incorrect")
}
guard let data = try? JSONEncoder().encode(user) else {
fatalError("error encoding user")
}
var resource = Resource<UserResponseModel?>(url: url)
resource.body = data
return resource
}
}
extension User {
init?(_ vm: UserViewModel) {
let email = vm.email
let password = vm.password
let first_name = vm.first_name
let last_name = vm.last_name
let name = vm.name
self.password = password
self.email = email
self.first_name = first_name
self.last_name = last_name
self.name = name
}
}
And here is my view model:
import Foundation
struct UserViewModel : Codable {
let user : User
}
extension UserViewModel {
var name : String {
return self.user.name
}
var email : String {
return self.user.email
}
var password : String {
self.user.password
}
var first_name: String {
self.user.first_name
}
var last_name: String {
self.user.last_name
}
}
This is how I am calling it:
let login = LoginUser(email: email, password: password)
let vm = UserViewModel(loginUser: login)
Webservice().load(resource: User.create(vm: vm), caller: self) { (result) in
My model and view model conform to Codable as well as my Resource is Codable too.
What is the reason of the error that T.type does not conform to protocol Encodable? How to resolve it?
Is this approach to send and receive data appropriate?
You didn't specify that T should be Encodable for load(resource:... function of class Webservice:
Change this:
class Webservice {
func load<T>(resource: Resource<T>, caller: UIViewController ,completion: #escaping (Result<T, NetworkingError>) -> Void) {
To this:
class Webservice {
func load<T: Encodable>(resource: Resource<T>, caller: UIViewController ,completion: #escaping (Result<T, NetworkingError>) -> Void) {
And also you need to encode value, not generic type here:
UserDefaults.standard.set(PropertyListEncoder().encode(T.self), forKey: "user")
should be
UserDefaults.standard.set(try PropertyListEncoder().encode(result), forKey: "user")
But another question is: Why do you encode from JSON and then encode it to PropertyList? Why not save JSON data in UserDefaults?
may be it will work for you.
extension UserDefaults {
func save<T: Codable>(_ object: T, forKey key: String) {
let encoder = JSONEncoder()
if let encodedObject = try? encoder.encode(object) {
UserDefaults.standard.set(encodedObject, forKey: key)
UserDefaults.standard.synchronize()
}
}
func getObject<T: Codable>(forKey key: String) -> T? {
if let object = UserDefaults.standard.object(forKey: key) as? Data {
let decoder = JSONDecoder()
if let decodedObject = try? decoder.decode(T.self, from: object) {
return decodedObject
}
}
return nil
}}
this is how to store
func setCoableInUser<T: Codable>(_ object:T, key: String)-> Void{
UserDefaults.standard.save(object, forKey: key)}

Deserialize a JSON array to a Swift array of objects

I am new to Swift, and am not able to figure out how to deserialize a JSON array to an array of Swift objects. I'm able to deserialize a single JSON user to a Swift user object fine, but just not sure how to do it with a JSON array of users.
Here is my User.swift class:
class User {
var id: Int
var firstName: String?
var lastName: String?
var email: String
var password: String?
init (){
id = 0
email = ""
}
init(user: NSDictionary) {
id = (user["id"] as? Int)!
email = (user["email"] as? String)!
if let firstName = user["first_name"] {
self.firstName = firstName as? String
}
if let lastName = user["last_name"] {
self.lastName = lastName as? String
}
if let password = user["password"] {
self.password = password as? String
}
}
}
Here's the class where I'm trying to deserialize the JSON:
//single user works.
Alamofire.request(.GET, muURL/user)
.responseJSON { response in
if let user = response.result.value {
var swiftUser = User(user: user as! NSDictionary)
}
}
//array of users -- not sure how to do it. Do I need to loop?
Alamofire.request(.GET, muURL/users)
.responseJSON { response in
if let users = response.result.value {
var swiftUsers = //how to get [swiftUsers]?
}
}
The best approach is the use Generic Response Object Serialization provided by Alamofire here is an example :
1) Add the extension in your API Manager or on a separate file
public protocol ResponseObjectSerializable {
init?(response: NSHTTPURLResponse, representation: AnyObject)
}
extension Request {
public func responseObject<T: ResponseObjectSerializable>(completionHandler: Response<T, NSError> -> Void) -> Self {
let responseSerializer = ResponseSerializer<T, NSError> { request, response, data, error in
guard error == nil else { return .Failure(error!) }
let JSONResponseSerializer = Request.JSONResponseSerializer(options: .AllowFragments)
let result = JSONResponseSerializer.serializeResponse(request, response, data, error)
switch result {
case .Success(let value):
if let
response = response,
responseObject = T(response: response, representation: value)
{
return .Success(responseObject)
} else {
let failureReason = "JSON could not be serialized into response object: \(value)"
let error = Error.errorWithCode(.JSONSerializationFailed, failureReason: failureReason)
return .Failure(error)
}
case .Failure(let error):
return .Failure(error)
}
}
return response(responseSerializer: responseSerializer, completionHandler: completionHandler)
}
}
public protocol ResponseCollectionSerializable {
static func collection(response response: NSHTTPURLResponse, representation: AnyObject) -> [Self]
}
extension Alamofire.Request {
public func responseCollection<T: ResponseCollectionSerializable>(completionHandler: Response<[T], NSError> -> Void) -> Self {
let responseSerializer = ResponseSerializer<[T], NSError> { request, response, data, error in
guard error == nil else { return .Failure(error!) }
let JSONSerializer = Request.JSONResponseSerializer(options: .AllowFragments)
let result = JSONSerializer.serializeResponse(request, response, data, error)
switch result {
case .Success(let value):
if let response = response {
return .Success(T.collection(response: response, representation: value))
} else {
let failureReason = "Response collection could not be serialized due to nil response"
let error = Error.errorWithCode(.JSONSerializationFailed, failureReason: failureReason)
return .Failure(error)
}
case .Failure(let error):
return .Failure(error)
}
}
return response(responseSerializer: responseSerializer, completionHandler: completionHandler)
}
}
2) update your model object like this:
final class User: ResponseObjectSerializable, ResponseCollectionSerializable {
let username: String
let name: String
init?(response: NSHTTPURLResponse, representation: AnyObject) {
self.username = response.URL!.lastPathComponent!
self.name = representation.valueForKeyPath("name") as! String
}
static func collection(response response: NSHTTPURLResponse, representation: AnyObject) -> [User] {
var users: [User] = []
if let representation = representation as? [[String: AnyObject]] {
for userRepresentation in representation {
if let user = User(response: response, representation: userRepresentation) {
users.append(user)
}
}
}
return users
}
}
3) then you can use it like that :
Alamofire.request(.GET, "http://example.com/users")
.responseCollection { (response: Response<[User], NSError>) in
debugPrint(response)
}
Source: Generic Response Object Serialization
Useful Link: Alamofire JSON Serialization of Objects and Collections
Since you are using Alamofire to make your requests why don't you give a chance to Hearst-DD ObjectMapper it has an Alamofire extension AlamofireObjectMapper. I think it'll save you time!
I would loop through them then add each user to an array (preferably a property of the VC and not an instance variable) but here is an example.
Alamofire.request(.GET, "YourURL/users")
.responseJSON { response in
if let users = response.result.value {
for user in users {
var swiftUser = User(user: user as! NSDictionary)
//should ideally be a property of the VC
var userArray : [User]
userArray.append(swiftUser)
}
}
}
You could also try EVReflection https://github.com/evermeer/EVReflection
It's even more simple, i.e. to parse JSON (code snippet taken from EVReflection link):
let json:String = "{
\"id\": 24,
\"name\": \"Bob Jefferson\",
\"friends\": [{
\"id\": 29,
\"name\":
\"Jen Jackson\"}]}"
you can use this class:
class User: EVObject {
var id: Int = 0
var name: String = ""
var friends: [User]? = []
}
in this way:
let user = User(json: json)

Resources