Hi i am able to get response from server for array list. But i dont know how to parse that response in object array [Feed] and send it on on completion handler. My Code is as follows:
class FeedsService {
private var feedsEndPoint: String = "https://jsonplaceholder.typicode.com/posts"
public func getFeeds(completion: ([Feed]) -> Void) {
Alamofire.request(feedsEndPoint, method: .get)
.validate(statusCode: 200..<300)
.validate(contentType: ["application/json"])
.responseJSON{ response in
print("Response \(response.result.value)")
}
}
}
Feed Model is as follows:
class Feed {
private var title: String
private var body: String
init(title:String, body: String) {
self.title = title
self.body = body
}
public func getTitle() -> String {
return self.title
}
public func getBody() -> String {
return self.body;
}
}
I want to parse this as Feed Array and sent it on completion callback.
I am using Alamofire rest library for loading data from rest server.
You can try this:
class FeedsService {
private var feedsEndPoint: String = "https://jsonplaceholder.typicode.com/posts"
public func getFeeds(completion: #escaping ([Feed]) -> Void) {
Alamofire.request(feedsEndPoint, method: .get)
.validate(statusCode: 200..<300)
.validate(contentType: ["application/json"])
.responseJSON{ response in
print("Response \(response.result.value)")
var feeds = [Feed]()
if let jsonArray = response.result.value as? [[String: Any]] {
for json in jsonArray {
if let feed = Feed(json: json) {
feeds.append(feed)
}
}
completion(feeds)
} else {
// handle error
}
}
}
}
Also, update your Feed class:
class Feed {
private var title: String
private var body: String
init(title:String, body: String) {
self.title = title
self.body = body
}
init?(json: [String: Any]) {
guard let title = json["title"] as? String,
let body = json["body"] as? String else {
return nil
}
self.title = title
self.body = body
}
public func getTitle() -> String {
return self.title
}
public func getBody() -> String {
return self.body;
}
}
Just change "title" and "body" to whatever is the appropriate key in your json response.
As El Tomato is pointing out that Feed init is not working, this is a test code that can be tried out in a playground:
let testFeedJson = ["title": "Test Title", "body" : "Test Body"]
if let testFeed = Feed(json: testFeedJson) {
print(testFeed.getTitle())
print(testFeed.getBody())
}
Related
How to delete and update structure type array in Core Data iOS Sswift? I am saving to core data like this. I need to delete and update selected cell containing values
let projectsInfo = NSEntityDescription.insertNewObject(forEntityName:"ItemsInfo", into: delegate.persistentContainer.viewContext) as! ItemsInfo
let auditArray:[String:[lendingData]] = ["allcreditData":SaveWitnessData.shared.LendingDataArray]
let jsonData = try! JSONEncoder().encode(auditArray) projectsInfo.values = jsonData
delegate.saveContext()
And my struct is like
struct lendingData : Codable {
let userName : String
let amount : String
let date : String
let type : String
var witnessDetails : [witnessData]
}
Option 1.
Using a class and NSSecureCoding is the best way of doing this. The most flexible.
///To See the whole thing in action you have to follow a few steps
///Step 1. Create an new SwiftUI project with CoreData
///Step 2. Copy all the code in Option 1 into a `.swift` file
///Step 3. Go to the `Persistence.swift` file
/// Place these 2 lines
/// `WitnessDataTransformer.register()`
/// `LendingDataTransformer.register()`
/// Just under `container = NSPersistentCloudKitContainer(name: "YourAppName")
///Step 4. Go to the CoreData model
/// Select the `Item` Entity
/// Add a `lendingData` attribute of type `Transformable`
/// Update the `Transformer` and `Custom Class` in the `Data Model Inspector` as shown
///Step 5. You should see the View on Canvas in this point
Photo for Step 4
Code
import SwiftUI
//struct and class should start with an uppercase
//You need secureCoding not codable
//You have to change to class because NSSecurecoding is not available for a struct -https://developer.apple.com/documentation/foundation/nssecurecoding
public class LendingData : NSObject, Identifiable, ObservableObject{
public let id: String
#Published var userName : String
#Published var amount : String
#Published var date : String
#Published var type : String
//WitnessData needs to conform to secure coding as well
#Published var witnessDetails : [WitnessData]
static func sample() -> LendingData {
LendingData(id: UUID().uuidString, userName: "sample name", amount: "10.00", date: "\(Date())", type: "sample type", witnessDetails: [WitnessData.sample(), WitnessData.sample()])
}
static func blank() -> LendingData {
LendingData(id: UUID().uuidString, userName: "", amount: "", date: "", type: "", witnessDetails: [])
}
public enum CodingKeys: String, CodingKey {
case id
case userName
case amount
case date
case type
case witnessDetails
}
public init(id: String, userName : String, amount : String, date : String, type : String, witnessDetails : [WitnessData]) {
self.id = id
self.userName = userName
self.amount = amount
self.date = date
self.type = type
self.witnessDetails = witnessDetails
}
public required init?(coder: NSCoder) {
id = coder.decodeObject(forKey: CodingKeys.id.rawValue) as! String
userName = coder.decodeObject(forKey: CodingKeys.userName.rawValue) as! String
amount = coder.decodeObject(forKey: CodingKeys.amount.rawValue) as! String
date = coder.decodeObject(forKey: CodingKeys.date.rawValue) as! String
type = coder.decodeObject(forKey: CodingKeys.type.rawValue) as! String
witnessDetails = coder.decodeArrayOfObjects(ofClass: WitnessData.self, forKey: CodingKeys.witnessDetails.rawValue) ?? []
}
}
extension LendingData: NSSecureCoding{
public static var supportsSecureCoding: Bool{
return true
}
public func encode(with coder: NSCoder) {
coder.encode(id, forKey: CodingKeys.id.rawValue)
coder.encode(userName, forKey: CodingKeys.userName.rawValue)
coder.encode(amount, forKey: CodingKeys.amount.rawValue)
coder.encode(date, forKey: CodingKeys.date.rawValue)
coder.encode(type, forKey: CodingKeys.type.rawValue)
coder.encode(witnessDetails, forKey: CodingKeys.witnessDetails.rawValue)
}
}
///MUST CALL LendingDataTransformer.register() right after creating the Persistent Container before setup and loading store
#objc(LendingDataTransformer)
public final class LendingDataTransformer: NSSecureUnarchiveFromDataTransformer {
public static let name = NSValueTransformerName(rawValue: String(describing: LendingDataTransformer.self))
public override static var allowedTopLevelClasses: [AnyClass] {
return [LendingData.self, NSString.self, NSArray.self, WitnessData.self]
}
//Register before CoreData setup starts
#objc dynamic
public static func register() {
let transformer = LendingDataTransformer()
ValueTransformer.setValueTransformer(transformer, forName: name)
}
}
//You have to change to class because NSSecurecoding is not available for a struct -https://developer.apple.com/documentation/foundation/nssecurecoding
public class WitnessData: NSObject, Identifiable, ObservableObject{
public let id: String
//This is just a sample since you did not provide the struct
//Add your variables to
// the class,
// the CodingKeys,
// init?(coder: NSCoder),
// encode(with coder: NSCoder), and
// init(id: String, name : String).
// Just follow the pattern.
#Published var name: String
static func sample() -> WitnessData{
WitnessData(id: UUID().uuidString, name: UUID().uuidString)
}
static func blank() -> WitnessData{
WitnessData(id: UUID().uuidString, name: "")
}
public enum CodingKeys: String, CodingKey {
case id
case name
}
public init(id: String, name : String) {
self.id = id
self.name = name
}
public required init?(coder: NSCoder) {
id = coder.decodeObject(forKey: CodingKeys.id.rawValue) as? String ?? ""
name = coder.decodeObject(forKey: CodingKeys.name.rawValue) as? String ?? ""
}
}
extension WitnessData: NSSecureCoding{
public static var supportsSecureCoding: Bool{
return true
}
public func encode(with coder: NSCoder) {
coder.encode(id, forKey: CodingKeys.id.rawValue)
coder.encode(name, forKey: CodingKeys.name.rawValue)
}
}
///MUST CALL WitnessDataTransformer.register() right after creating the Persistent Container before setup and loading store
#objc(WitnessDataTransformer)
public final class WitnessDataTransformer: NSSecureUnarchiveFromDataTransformer {
public static let name = NSValueTransformerName(rawValue: String(describing: WitnessDataTransformer.self))
public override static var allowedTopLevelClasses: [AnyClass] {
return [WitnessData.self, NSString.self, NSArray.self]
}
//Register before CoreData setup starts
#objc dynamic
public static func register() {
let transformer = WitnessDataTransformer()
ValueTransformer.setValueTransformer(transformer, forName: name)
}
}
The below SwiftUI code works for both option 1 or option 2
///This is just a sample View
struct LendingDataView: View {
//You will need the original ObservableObject if you want to be able to show changes
//SwiftUI depends on being told that there are chagnes so it can reload Views
#ObservedObject var item: Item
var body: some View {
if item.lendingData != nil{
List{
TextField("username",text: $item.lendingData.bound.userName)
TextField("amount",text: $item.lendingData.bound.amount)
TextField("date",text: $item.lendingData.bound.date)
TextField("type",text: $item.lendingData.bound.type)
Section(content: {
ForEach($item.lendingData.bound.witnessDetails, content: { $witness in
HStack{
TextField("name",text: $witness.name)
Spacer()
//For deleting by object
Image(systemName: "trash")
.foregroundColor(.red)
.onTapGesture {
let idx = item.lendingData!.witnessDetails.firstIndex(where: {
$0.id == witness.id
})
if idx != nil{
item.lendingData!.witnessDetails.remove(at: idx!)
}
//Because you are so far down the line you have to tell the ObservableObject there is a change
//If you dont you won't see the new items until something happens to trigger a refresh
//item.objectWillChange.send()
item.objectWillChange.send()
}
}
})
//For deleting by index
.onDelete(perform: { indexSet in
for idx in indexSet{
item.lendingData!.witnessDetails.remove(at: idx)
}
})
}, header: {
HStack{
Text("Witness Data")
Button(action: {
item.lendingData!.witnessDetails.append(WitnessData.blank())
//Because you are so far down the line you have to tell the ObservableObject there is a change
//If you dont you won't see the new items until something happens to trigger a refresh
item.objectWillChange.send()
}, label: {
Image(systemName: "plus")
})
}
})
}
}else{
VStack{
Text("no lending data")
Button(action: {
item.lendingData = LendingData.blank()
}, label: {
Image(systemName: "plus")
})
}
}
}
}
//Standard Preview
struct LendingDataView_Previews: PreviewProvider {
//Use the preview container
static let context = PersistenceController.preview.container.viewContext
static var sampleItem = Item(context: context)
static var previews: some View {
LendingDataView(item: sampleItem)
}
}
extension Optional where Wrapped == LendingData {
var _bound: LendingData? {
get {
return self
}
set {
self = newValue
}
}
var bound: LendingData {
get {
return _bound ?? LendingData.blank()
}
set {
_bound = newValue
}
}
}
Like I said at the start class is the safest way but you can use the struct.
Option 2
Just add an an attribute named lendingDataJSON of Type String? INSTEAD of the lendingData of type Transformable
struct LendingData : Codable, Identifiable{
let id: String
var userName : String
var amount : String
var date : String
var type : String
var witnessDetails : [WitnessData]
static func sample() -> LendingData {
LendingData(id: UUID().uuidString, userName: "sample name", amount: "10.00", date: "\(Date())", type: "sample type", witnessDetails: [WitnessData.sample(), WitnessData.sample()])
}
static func blank() -> LendingData {
LendingData(id: UUID().uuidString, userName: "", amount: "", date: "", type: "", witnessDetails: [])
}
}
struct WitnessData: Codable, Identifiable{
let id: String
var name: String
static func sample() -> WitnessData{
WitnessData( id: UUID().uuidString, name: UUID().uuidString)
}
static func blank() -> WitnessData{
WitnessData( id: UUID().uuidString, name: "")
}
}
//The App's CoreData Model will need an attibute
// named lendingDataJSON of Type String
extension Item{
//This computed property should be the only way that the app alters the LendingData
//If you use the lendingDataJSON directly you can corrupt all of it
var lendingData: LendingData?{
get{
let decoder = JSONDecoder()
if let obj = try? decoder.decode(LendingData.self, from: self.lendingDataJSON?.data(using: .utf8) ?? Data()) {
return obj
}else{
return nil
}
}
set{
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
if let encoded = try? encoder.encode(newValue) {
self.lendingDataJSON = String(data: encoded, encoding: .utf8) ?? ""
}
}
}
}
All the View code will work the same with the class option or with the struct option
var array: Array<Int>? = nil
var arrayType: Any.Type = type(of: array)
print("type:\(arrayType)")
I got printed:
type:Optional<Array<Int>>
then, how can i get the type Int from arrayType?
You may not understand why I did this. The actual situation is:
I have a bean, like this:
protocol Initializable {
init()
}
class MyBean1: Initializable {
required init() {}
var property1: Int?
var property2: String?
}
class MyBean2: Initializable {
required init() {}
var beans: Array<MyBean1>?
}
I have json data like this:
{beans:[{"property1":1,"property2":"string1"},{"property1":2,"property2":"string2"}]}
I want automatic create object and set the values.
I use the Runtime(A Swift Runtime library for viewing type info, and the dynamic getting and setting of properties.) Framework to mirror and set properties.
I extend the TypeInfo:
extension TypeInfo {
func properties() -> [String] {
var properties: [String] = []
for property in self.properties {
properties.append(property.name)
}
return properties
}
}
I want create object from json string:
static func fromJson<Result: Initializable>(json: String, type: Result.Type) -> Result {
var result: Initializable = Result.init()
if let dictionary = Dictionary<String, Any>.from(json: json) {
if let info: TypeInfo = try? typeInfo(of: type(of: result)) {
let properties = info.properties()
for (key, value) in dictionary {
if properties.contains(key) {
if let property: PropertyInfo = try? info.property(named: key) {
print("\(key):\(property.type)")
}
}
}
}
}
return result as! Result
}
I got print:
beans:Optional<Array<MyBean1>>
the property.type type is Any.Type.
I want get the type MyBean1 and create MyBean1's object from property.type.
Now i use Bundle.main.classNamed to get the generic type.
TypeName got from substring
var typeName = "\(property.type)" // Optional<Array<MyBean1>>
if let range = typeName.range(of: "Optional<") {
typeName = typeName.replacingCharacters(in: range, with: "")
typeName = String(typeName[typeName.startIndex..<(typeName.index(before: typeName.endIndex))])
}
print("typeName:\(typeName)") // Array<MyBean1>
if let range = typeName.range(of: "Array<") {
typeName = typeName.replacingCharacters(in: range, with: "")
typeName = String(typeName[typeName.startIndex..<(typeName.index(before: typeName.endIndex))])
print("typeName:\(typeName)") // MyBean1
if let namespace = Bundle.main.infoDictionary?["CFBundleExecutable"] as? String {
let clazz: AnyClass? = Bundle.main.classNamed("\(namespace).\(typeName)")
if let initableType = clazz as? Initializable.Type {
var beans = Array<Any>()
for item in array {
var bean = initableType.init()
// set bean properties
beans.append(bean)
}
}
}
}
I am a newbie to Swift and SwiftUI.
I have a RecipeLinkFetcher() which picks up a json file dynamically and populates an array of recipe objects.
I am trying to filter the array of RecipeLink objects based on the category.
The RecipeLink is defined as
struct RecipeLink: Codable, Identifiable
{
public var id: Int
public var category: String
public var title: String
public var permalink: String
public var excerpt: String
public var image: String
enum CodingKeys: String, CodingKey {
case id = "id"
case category = "category"
case title = "title"
case permalink = "permalink"
case excerpt = "excerpt"
case image = "featured_image"
}
}
In my view I am using the below code to populate the variable fetcher which contains an array of RecipeLink
#ObservedObject var fetcher = RecipeLinkFetcher()
var categoryName: String = "Featured"
And I am trying to filter it to a new variable using the below, however this does not work
var recipes = fetcher.filter($0.category == categoryName)
Here is my RecipeLinkFetcher
import Foundation
public class RecipeLinkFetcher: ObservableObject {
#Published var recipeLink:[RecipeLink] = [RecipeLink]()
init(){
load()
}
func load() {
let url = URL(string: "http://localhost/wordpress2/wp-content/uploads/json-export.json")!
URLSession.shared.dataTask(with: url) {(data,response,error) in
do {
if let d = data {
let decodedLists = try JSONDecoder().decode([RecipeLink].self, from: d)
DispatchQueue.main.async {
self.recipeLink = decodedLists
}
}else {
print("No Data")
}
} catch {
print ("Error")
}
}.resume()
}
}
struct RecipeLink: Codable, Identifiable {
public var id: Int
public var category: String
public var title: String
public var permalink: String
public var excerpt: String
public var image: String
enum CodingKeys: String, CodingKey {
case id = "id"
case category = "category"
case title = "title"
case permalink = "permalink"
case excerpt = "excerpt"
case image = "featured_image"
}
}
This is my UIView with the line marked with ** where I am trying to filter the array/list
struct CategoryRow: View {
#ObservedObject var fetcher = RecipeLinkFetcher()
var categoryName: String
**var filteredList = $fetcher.filter({$0.category==categoryName})**
var body: some View {
VStack() {
Text(self.categoryName.htmlUnescape())
.font(.headline)
.padding(.leading, 15)
.padding(.top, 5)
// ScrollView(.horizontal, showsIndicators: false) {
List(fetcher.recipeLink){ recipe in
if(recipe.category.htmlUnescape() == self.categoryName){
HStack(alignment: .top, spacing: 0 ){
CategoryItem(imageURL: recipe.image, recipeTitle: recipe.title)
}
}
}
}
}
}
Move your filtered array to an #Published on the ObseravbleObject so your view rebuilds whenever it changes. Make the category name a CurrentValueSubject on the observedObject and combine it with your network results to reactively derive the filtered array. ie when ever you change the .value of categoryName or get a new value for recipeLink, the filteredRecipeLinks will be automatically recalculated and since its a #Published property your view will automatically rebuild itself.
public class RecipeLinkFetcher: ObservableObject {
#Published var recipeLink:[RecipeLink] = [RecipeLink]()
#Published var filteredRecipeLinks:[RecipeLink] = []
var categoryName = CurrentValueSubject<String, Never>("")
private var subscriptions = Set<AnyCancellable>()
init(){
load()
Publishers
.CombineLatest($recipeLink, categoryName)
.map { combined -> [RecipeLink] in
let (links, name) = combined
guard !name.isEmpty else { return links } // Show all when search term is empty
return links.filter { $0.category == categoryName }
}
.assign(to: \.recipeLink, on: self)
.store(in: &subscriptions)
}
...
}
I would separate fetched model and presented filtered recipes, as in below approach (scratchy)
#State private var filteredList = [RecipeLink]() // to be presented
var body: some View {
VStack() {
Text(self.categoryName.htmlUnescape())
.font(.headline)
.padding(.leading, 15)
.padding(.top, 5)
List(filteredList) { recipe in // list only filtered
HStack(alignment: .top, spacing: 0 ){
CategoryItem(imageURL: recipe.image, recipeTitle: recipe.title)
}
.onReceive(fetcher.$recipeLink) { _ in // once model updated refilter
self.filteredList = self.fetcher.recipeLink.filter
{$0.category == self.categoryName}
}
}
}
I currently have an array of custom objects
[GenrePosters]
which is defined like so:
public struct GenrePosters: Decodable, Equatable{
public let poster : String
public init? (json: JSON) {
guard let poster: String = "poster_path" <~~ json
else {return nil}
self.poster = poster
}
public static func ==(lhs: GenrePosters, rhs: GenrePosters) -> Bool {
return lhs.poster == rhs.poster
}
When printed to console it looks like this:
[MyMovieGuide.GenrePosters(poster:
"/e1mjopzAS2KNsvpbpahQ1a6SkSn.jpg"), MyMovieGuide.GenrePosters(poster:
"/jjBgi2r5cRt36xF6iNUEhzscEcb.jpg"), MyMovieGuide.GenrePosters(poster:
"/tIKFBxBZhSXpIITiiB5Ws8VGXjt.jpg")]
I'm trying to convert the array of GenrePosters to an array of strings with only the poster values that like this:
[
"/e1mjopzAS2KNsvpbpahQ1a6SkSn.jpg"
"/jjBgi2r5cRt36xF6iNUEhzscEcb.jpg"
"/tIKFBxBZhSXpIITiiB5Ws8VGXjt.jpg"]
Any help will be appreciated!
You should be able to do this using map(_:) method:
let posters = posterList.map {$0.poster}
public struct GenrePosters: Decodable, Equatable{
public let poster : String
public init? (json: JSON) {
guard let poster: String = "poster_path" <~~ json
else {return nil}
self.poster = poster
}
public static func ==(lhs: GenrePosters, rhs: GenrePosters) -> Bool {
return lhs.poster == rhs.poster
}
}
let genrePostersArray = [GenrePosters(poster: "one"), GenrePosters(poster: "two"), GenrePosters(poster: "three")]
let genrePostersStringArray = genrePostersArray.flatMap { $0.poster }
print(genrePostersStringArray) // ["one", "two", "three"]
class PostFOrData {
let url = NSURL( string: "http://210.61.209.194:8088/SmarttvWebServiceTopmsoApi/GetReadlist")
var picUrl = NSURL(string : "http://210.61.209.194:8088/SmarttvMedia/img/epi00001.png")
var responseString : NSString = ""
func forData() -> NSString {
let request = NSMutableURLRequest( URL: url!)
request.HTTPMethod = "POST"
var s : NSString = ""
let postString : String = "uid=59"
request.HTTPBody = postString.dataUsingEncoding(NSUTF8StringEncoding)
let task = NSURLSession.sharedSession().dataTaskWithRequest(request) {
data, response, error in
if error != nil {
println("error=\(error)")
return
} else {
println("response = \(response!)")
self.responseString = NSString(data: data, encoding: NSUTF8StringEncoding)!
println("responseString = \(self.responseString)")
}
}
// I want to return NSString here, but I always get nothing
return self.responseString
}
}
Anyone know how to get the data from task?
You can't return data directly from an asynchronous task.
The solution with Swift 2 is to make a completion handler like this:
class PostFOrData {
// the completion closure signature is (NSString) -> ()
func forData(completion: (NSString) -> ()) {
if let url = NSURL(string: "http://210.61.209.194:8088/SmarttvWebServiceTopmsoApi/GetReadlist") {
let request = NSMutableURLRequest( URL: url)
request.HTTPMethod = "POST"
let postString : String = "uid=59"
request.HTTPBody = postString.dataUsingEncoding(NSUTF8StringEncoding)
let task = NSURLSession.sharedSession().dataTaskWithRequest(request) {
data, response, error in
if let data = data,
jsonString = NSString(data: data, encoding: NSUTF8StringEncoding)
where error == nil {
completion(jsonString)
} else {
print("error=\(error!.localizedDescription)")
}
}
task.resume()
}
}
}
let pfd = PostFOrData()
// you call the method with a trailing closure
pfd.forData { jsonString in
// and here you get the "returned" value from the asynchronous task
print(jsonString)
}
That way, the completion is only called when the asynchronous task is completed. It is a way to "return" the data without actually using return.
Swift 3 version
class PostFOrData {
// the completion closure signature is (String) -> ()
func forData(completion: #escaping (String) -> ()) {
if let url = URL(string: "http://210.61.209.194:8088/SmarttvWebServiceTopmsoApi/GetReadlist") {
var request = URLRequest(url: url)
request.httpMethod = "POST"
let postString : String = "uid=59"
request.httpBody = postString.data(using: String.Encoding.utf8)
let task = URLSession.shared.dataTask(with: request) {
data, response, error in
if let data = data, let jsonString = String(data: data, encoding: String.Encoding.utf8), error == nil {
completion(jsonString)
} else {
print("error=\(error!.localizedDescription)")
}
}
task.resume()
}
}
}
let pfd = PostFOrData()
// you call the method with a trailing closure
pfd.forData { jsonString in
// and here you get the "returned" value from the asynchronous task
print(jsonString)
}
There are some very generic requirements that would like every good API Manager to satisfy: will implement a protocol-oriented API Client.
APIClient Initial Interface
protocol APIClient {
func send(_ request: APIRequest,
completion: #escaping (APIResponse?, Error?) -> Void)
}
protocol APIRequest: Encodable {
var resourceName: String { get }
}
protocol APIResponse: Decodable {
}
Now Please check complete api structure
// ******* This is API Call Class *****
public typealias ResultCallback<Value> = (Result<Value, Error>) -> Void
/// Implementation of a generic-based API client
public class APIClient {
private let baseEndpointUrl = URL(string: "irl")!
private let session = URLSession(configuration: .default)
public init() {
}
/// Sends a request to servers, calling the completion method when finished
public func send<T: APIRequest>(_ request: T, completion: #escaping ResultCallback<DataContainer<T.Response>>) {
let endpoint = self.endpoint(for: request)
let task = session.dataTask(with: URLRequest(url: endpoint)) { data, response, error in
if let data = data {
do {
// Decode the top level response, and look up the decoded response to see
// if it's a success or a failure
let apiResponse = try JSONDecoder().decode(APIResponse<T.Response>.self, from: data)
if let dataContainer = apiResponse.data {
completion(.success(dataContainer))
} else if let message = apiResponse.message {
completion(.failure(APIError.server(message: message)))
} else {
completion(.failure(APIError.decoding))
}
} catch {
completion(.failure(error))
}
} else if let error = error {
completion(.failure(error))
}
}
task.resume()
}
/// Encodes a URL based on the given request
/// Everything needed for a public request to api servers is encoded directly in this URL
private func endpoint<T: APIRequest>(for request: T) -> URL {
guard let baseUrl = URL(string: request.resourceName, relativeTo: baseEndpointUrl) else {
fatalError("Bad resourceName: \(request.resourceName)")
}
var components = URLComponents(url: baseUrl, resolvingAgainstBaseURL: true)!
// Common query items needed for all api requests
let timestamp = "\(Date().timeIntervalSince1970)"
let hash = "\(timestamp)"
let commonQueryItems = [
URLQueryItem(name: "ts", value: timestamp),
URLQueryItem(name: "hash", value: hash),
URLQueryItem(name: "apikey", value: "")
]
// Custom query items needed for this specific request
let customQueryItems: [URLQueryItem]
do {
customQueryItems = try URLQueryItemEncoder.encode(request)
} catch {
fatalError("Wrong parameters: \(error)")
}
components.queryItems = commonQueryItems + customQueryItems
// Construct the final URL with all the previous data
return components.url!
}
}
// ****** API Request Encodable Protocol *****
public protocol APIRequest: Encodable {
/// Response (will be wrapped with a DataContainer)
associatedtype Response: Decodable
/// Endpoint for this request (the last part of the URL)
var resourceName: String { get }
}
// ****** This Results type Data Container Struct ******
public struct DataContainer<Results: Decodable>: Decodable {
public let offset: Int
public let limit: Int
public let total: Int
public let count: Int
public let results: Results
}
// ***** API Errro Enum ****
public enum APIError: Error {
case encoding
case decoding
case server(message: String)
}
// ****** API Response Struct ******
public struct APIResponse<Response: Decodable>: Decodable {
/// Whether it was ok or not
public let status: String?
/// Message that usually gives more information about some error
public let message: String?
/// Requested data
public let data: DataContainer<Response>?
}
// ***** URL Query Encoder OR JSON Encoder *****
enum URLQueryItemEncoder {
static func encode<T: Encodable>(_ encodable: T) throws -> [URLQueryItem] {
let parametersData = try JSONEncoder().encode(encodable)
let parameters = try JSONDecoder().decode([String: HTTPParam].self, from: parametersData)
return parameters.map { URLQueryItem(name: $0, value: $1.description) }
}
}
// ****** HTTP Pamater Conversion Enum *****
enum HTTPParam: CustomStringConvertible, Decodable {
case string(String)
case bool(Bool)
case int(Int)
case double(Double)
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let string = try? container.decode(String.self) {
self = .string(string)
} else if let bool = try? container.decode(Bool.self) {
self = .bool(bool)
} else if let int = try? container.decode(Int.self) {
self = .int(int)
} else if let double = try? container.decode(Double.self) {
self = .double(double)
} else {
throw APIError.decoding
}
}
var description: String {
switch self {
case .string(let string):
return string
case .bool(let bool):
return String(describing: bool)
case .int(let int):
return String(describing: int)
case .double(let double):
return String(describing: double)
}
}
}
/// **** This is your API Request Endpoint Method in Struct *****
public struct GetCharacters: APIRequest {
public typealias Response = [MyCharacter]
public var resourceName: String {
return "characters"
}
// Parameters
public let name: String?
public let nameStartsWith: String?
public let limit: Int?
public let offset: Int?
// Note that nil parameters will not be used
public init(name: String? = nil,
nameStartsWith: String? = nil,
limit: Int? = nil,
offset: Int? = nil) {
self.name = name
self.nameStartsWith = nameStartsWith
self.limit = limit
self.offset = offset
}
}
// *** This is Model for Above Api endpoint method ****
public struct MyCharacter: Decodable {
public let id: Int
public let name: String?
public let description: String?
}
// ***** These below line you used to call any api call in your controller or view model ****
func viewDidLoad() {
let apiClient = APIClient()
// A simple request with no parameters
apiClient.send(GetCharacters()) { response in
response.map { dataContainer in
print(dataContainer.results)
}
}
}