I want use ObjectMapper to parsing Json string within a singleton situation. Example code :
class User: Mappable {
var username: String?
var signature: String?
//Singleton
static let shared = User()
private init() {}
//Mappable functions
required init?(map: Map) {}
func mapping(map: Map) {
username <- map["username"]
signature <- map["signature"]
}
//Update userInfo after network request
func getUserInfo() {
//Network things
...
//Example data
let data = [
"username": "Eason",
"signature": "I love U"
]
//Some thing like this to update userInfo
Mapper<User>().map(data)
}
}
So, what is the right way to use ObjectMapper in singleton situation?
I prefer the following option.
1) UserManager (singleton):
class UserManager: NSObject {
static let shared = UserManager()
var profile: UserProfile?
private override init() {}
func loadUserProfile(completion: #escaping () -> ()) {
RestClient.shared.getUserProfile() { [weak self] (profile, error) in
guard let `self` = self else { return }
self.profile = profile as? UserProfile
completion()
}
}
}
2) User model:
class UserProfile: Mappable {
var username: String?
var signature: String?
required init?() {}
required init?(map: Map) {}
func mapping(map: Map) {
username <- map ["username"]
signature <- map ["signature"]
}
}
3) RestClient
typealias IdResponseBlock = (_ swiftObj: Any?, _ error: Error?) -> Void
class RestClient: NSObject {
static let shared = RestClient()
// Additional class where sending and receiving information from the server occurs
private var http = HttpService()
let baseUrl = ApiSettings.shared.kServerBaseURL
override init() {
super.init()
}
func parseData<P: BaseMappable>(object: Any, modelCls: P.Type, response: (IdResponseBlock)) {
if object is NSArray {
let result = Mapper<P>().mapArray(JSONObject: object)
return response(result, nil)
}
if object is NSDictionary {
let model: P = Mapper<P>().map(JSONObject: object)!
return response(model, nil)
}
}
//MARK: - API
//MARK: - User
func getUserProfile(resp: #escaping IdResponseBlock) {
let url = baseUrl + Requests.profile
http.queryBy(url, method: .get, queue: .background, resp: { (response, error) in
if let err = error {
return resp(nil, err)
}
guard let data = response else {
return resp(nil, error)
}
let jsonData = JSON(data)["data"]
self.parseData(object: jsonData.rawValue,
modelCls: UserProfile.self,
response: resp)
})
}
}
Related
I'm very new to swiftUI and have been working through the landscapes app tutorial.
I have been trying to switch the data source from a bundled JSON file to a remote JSON source but have so far been lost on how to integrate what I've learnt about the URLSession with the tutorials load code.
Apple's code:
final class ModelData: ObservableObject {
#Published var landmarks: [Landmark] = load("landmarkData.json")
// #Published var landmarks: [Landmark] = apiCall.getLocations(locations)
}
func load<T: Decodable>(_ filename: String) -> T {
let data: Data
guard let file = Bundle.main.url(forResource: filename, withExtension: nil)
else {
fatalError("Couldn't find \(filename) in main bundle.")
}
do {
data = try Data(contentsOf: file)
} catch {
fatalError("Couldn't load \(filename) from main bundle:\n\(error)")
}
do {
let decoder = JSONDecoder()
return try decoder.decode(T.self, from: data)
} catch {
fatalError("Couldn't parse \(filename) as \(T.self):\n\(error)")
}
}
What I have to load from the remote source:
struct Location: Codable, Identifiable {
let id = UUID()
let country: String
let name: String
}
class apiCall {
func getLocations(completion:#escaping ([Location]) -> ()) {
guard let url = URL(string: "https://overseer.cyou/heritage/heritageData.json") else { return }
URLSession.shared.dataTask(with: url) { (data, _, _) in
let locations = try! JSONDecoder().decode([Location].self, from: data!)
print(locations)
DispatchQueue.main.async {
completion(locations)
}
}
.resume()
}
}
Can anyone show me how I go about doing this, ideally from a complete beginners point of view?
// framework support
import SwiftUI
import Combine
// List view setup
struct LocationsView: View {
#ObservedObject var viewModel = LocationModel()
var body: some View {
List(viewModel.locations) { location in
HStack {
VStack(alignment: .leading) {
Text(location.name)
.font(.headline)
Text(location.country)
.font(.subheadline)
}
}
}
}
}
// Location model
struct Location: Codable, Identifiable {
var id = UUID()
let country: String
let name: String
let locationId: Int = 0
enum CodingKeys: String, CodingKey {
case locationId = "id"
case country
case name
}
}
// Location view model class
class LocationModel: ObservableObject {
#Published var locations: [Location] = []
var cancellationToken: AnyCancellable?
init() {
getLocations()
}
}
extension LocationModel {
func getLocations() {
cancellationToken = self.request("https://overseer.cyou/heritage/heritageData.json")?
.mapError({ (error) -> Error in
print(error)
return error
})
.sink(receiveCompletion: { _ in },
receiveValue: {
self.locations = $0
})
}
// API request
private func request(_ path: String) -> AnyPublisher<[Location], Error>? {
guard let url = URL(string: path)
else { return nil }
let request = URLRequest(url: url)
return apiCall.run(request)
.map(\.value)
.eraseToAnyPublisher()
}
}
// API setup
struct apiCall {
struct Response<T> {
let value: T
let response: URLResponse
}
static func run<T: Decodable>(_ request: URLRequest) -> AnyPublisher<Response<T>, Error> {
return URLSession.shared
.dataTaskPublisher(for: request)
.tryMap { result -> Response<T> in
let value = try JSONDecoder().decode(T.self, from: result.data)
return Response(value: value, response: result.response)
}
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
}
I am parsing a JSON on my own, after seeing a couple of tutorials, I try to adapt an OMBD API, but Xcode is throwing me this error Thread 1: EXC_BAD_ACCESS (code=2, address=0x7ffee7ca9f68), how do I debug this, below is what I've done so far.
import UIKit
import SnapKit
class ViewController: UIViewController, MovieManagerDelegate {
lazy var titleLabel: UILabel = {
UILabel()
}()
var movieManager = MovieManager()
override func viewDidLoad() {
super.viewDidLoad()
movieManager.delegate = self
movieManager.performMovieRequest(urlRequest: movieManager.movieUrl)
viewHierarchy()
constraitsMaker()
additionalComponents()
}
func viewHierarchy() {
view.addSubview(titleLabel)
}
func constraitsMaker() {
titleLabel.snp.makeConstraints { (maker) in
maker.center.leading.trailing.equalToSuperview()
}
}
func additionalComponents() {
titleLabel.textColor = .black
}
func didUpdateTitle(movie: MovieModel) {
DispatchQueue.main.async {
self.titleLabel.text = movie.movieTitle
}
}
}
import Foundation
protocol MovieManagerDelegate {
func didUpdateTitle(movie: MovieModel)
}
struct MovieManager {
let viewController = ViewController()
let movieUrl = "https://www.omdbapi.com/?i=tt3896198&apikey=b6531970"
let posterUrl = "https://m.media-amazon.com/images/M/MV5BNjM0NTc0NzItM2FlYS00YzEwLWE0YmUtNTA2ZWIzODc2OTgxXkEyXkFqcGdeQXVyNTgwNzIyNzg#._V1_SX300.jpg"
var delegate: MovieManagerDelegate?
func performMovieRequest(urlRequest: String) {
if let url = URL(string: movieUrl) {
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { (data, response, error) in
if error != nil {
print(error!)
return
}
if let safeData = data {
let movie = self.parseJSON(movieData: safeData)
self.delegate?.didUpdateTitle(movie: movie!)
}
}
task.resume()
}
}
//parse json function
func parseJSON(movieData: Data) -> MovieModel? {// with data as a parameter
let decoder = JSONDecoder()
//do try catch to handle errors of decoded json
do {
let decodedData = try decoder.decode(MovieData.self, from: movieData)//call the data for decode json. movie data comes from the parameter of the function, that is subclassed as data object
let title = decodedData.Title
let movie = MovieModel(movieTitle: title)
return movie
} catch {
print(error)
return nil
}
}
import Foundation
struct MovieData: Codable {
let Title: String
let Year: String
let Rated: String
let Writer: String
let Released: String
let Runtime: String
let Genre: String
let Director: String
let Actors: String
let Plot: String
let Language: String
let Country: String
let Awards: String
let Poster: URL
let Ratings: [Ratings]
let Metascore: String
let `Type`: String
let DVD: String
let BoxOffice: String
let Production: String
}
struct Ratings: Codable {
let Source: String
let Value: String
}
import Foundation
struct MovieModel {
let movieTitle: String
}
MovieManager makes an asynchronous call when downloading data meaning the code after the call to performMovieRequest is executed before the data is downloaded and the label has been initialised. It should work fine to move the calls in viewDidLoad to the delegate method
func didUpdateTitle(movie: MovieModel) {
viewHierarchy()
constraitsMaker()
additionalComponents()
DispatchQueue.main.async {
self.titleLabel.text = movie.movieTitle
}
}
If you are calling the delegate method after that as well you might want to have a boolean property to verify the 3 methods from viewDidLoad doesn't get called again
I try to map a string response with object mapper by my result base.
this is result class :
import ObjectMapper
class Result< T : Mappable > : Mappable {
var data: T?
var status: String?
var message: String?
var error: String?
init?(data: T?, status: String?, error: String?){
self.data = data
self.status = status
self.error = error
}
required init?(map: Map){
}
func mapping(map: Map) {
data <- map["data"]
status <- map["status"]
message <- map["message"]
error <- map["error"]
}
}
and also this is my network class:
import Foundation
import ObjectMapper
final class Network<T:Mappable>{
init() {
}
open func requestItem(_ router: BaseRouter, completionHandler: #escaping (Any?, Error?) -> Void) {
APIClient.Instance.requestJSON(router) { (response, error) in
if let error = error {
completionHandler(nil, APIError(code:ErrorCode.NetworkFailed, message:error.localizedDescription))
}
else if let json = response {
var convertedString : String?
do {
let data1 = try JSONSerialization.data(withJSONObject: json, options: JSONSerialization.WritingOptions.prettyPrinted)
convertedString = String(data: data1, encoding: String.Encoding.utf8)
print(convertedString!.description)
} catch let myJSONError {
print(myJSONError)
}
let result : Result<T>? = Mapper<Result<T>>().map(JSONString: convertedString!)
if let success = result?.status, success == "success" {
completionHandler(result?.data, nil)
}
else {
completionHandler(nil, APIError(code:ErrorCode.HandledInternalError, message:(result?.error)!))
}
}
else {
completionHandler(nil, APIError(code:ErrorCode.EmptyJSONException, message:"Empty JSON Exception"))
}
}
}
}
the response is :
{
"status" : "success",
"data" : "01CPSE6AQXVK554MTGENETKW24"
}
I try to map it but because of String is not a mappable class, I can not do. map["data"] variable should assign only string, not another complex class. Is there anyone can help me about this problem?
Finally error is :
Finally I can relies that what the wrong is with this code.
extension String : Mappable {
}
That's all.
I am using postgreSQL database and vapor, fluent for relation.I am trying to make extension ´request´ for ´model´(dataPointStorage) to be in Json formate. i get the error (Argument labels '(node:)' do not match any available overloads).
code.
dataPointController.swift
import Vapor
import HTTP
final class dataPointController: ResourceRepresentable{
func create(request: Request) throws -> ResponseRepresentable{
var dataPoints = try request.dataPoints()
try dataPoints.save()
return dataPoints as! ResponseRepresentable
}
func show(request: Request, dataPoints: dataPointStorage) throws -> ResponseRepresentable {
return dataPoints as! ResponseRepresentable
}
func update(request: Request, dataPoints: dataPointStorage) throws -> ResponseRepresentable {
let new = try request.dataPoints()
var dataPoints = dataPoints
dataPoints.name = new.name
dataPoints.displayName = new.displayName
dataPoints.content = new.content
try dataPoints.save()
return dataPoints as! ResponseRepresentable
}
func delete(request: Request, dataPoints: dataPointStorage) throws -> ResponseRepresentable {
try dataPoints.delete()
return JSON([:])
}
func index(request: Request) throws -> ResponseRepresentable{
return try JSON(node:dataPointStorage.all())
}
func makeResource() -> Resource<dataPointStorage> {
return Resource(
index: index,
store: create,
show: show,
destroy: delete
)
}
}
extension Request{
func dataPoints() throws -> dataPointStorage {
guard let json = json else { throw Abort.badRequest }
**return try dataPointStorage(node: json)** 'error(Argument labels '(node:)' do not match any available overloads)'
}
}
model.swift
import Foundation
import Vapor
import FluentProvider
import PostgreSQLProvider
final class dataPointStorage: Model, JSONRepresentable, NodeRepresentable {
// var _data: dataPointProtocol?
// var _data:[dataPointProtocol] = []
let storage = Storage()
var id: Node?
var name:String
var displayName:String
public var content: String!
init(node:Node ,content: String, displayName:String, name:String) {
self.id = nil
self.content = content
self.displayName = displayName
self.name = name
}
struct Keys {
static let id = "id"
static let content = "content"
}
func forDataBase() {
let array:[berichtDataPoint] = [intDataPoint(), boolDataPoint(), doubleDataPoint()]
let _ = array[0] as! intDataPoint
let _ = array[1] as! doubleDataPoint
for point in array {
switch point {
case is intDataPoint:
print("int")
case is doubleDataPoint:
print("double")
case is boolDataPoint:
print("bool")
default:
print("error")
}
}
}
func makeRow() throws -> Row {
var row = Row()
try row.set("id", id)
try row.set("displayName", displayName)
try row.set("name", name)
return row
}
init(row: Row) throws {
content = try row.get("content")
displayName = try row.get("displayName")
name = try row.get("name")
}
func makeNode(context: Context) throws -> Node {
return try Node(node: [
"id": id,
"content": content,
"displayName": displayName,
"name": name
])
}
}
extension dataPointStorage: Preparation {
static func prepare(_ database: Database) throws {
try database.create(self) { dataPointStorage in
dataPointStorage.id()
dataPointStorage.string("displayName")
dataPointStorage.string("name")
dataPointStorage.string("content")
}
}
static func revert(_ database: Database) throws {
try database.delete(dataPointStorage)
}
}
extension dataPointStorage: JSONConvertible {
convenience init(json: JSON) throws {
try self.init(
**content: json.get(dataPointStorage.content)**
error(Instance member 'content' cannot be used on type 'dataPointStorage')
)
}
func makeJSON() throws -> JSON {
var json = JSON()
try json.set("name", name)
try json.set("displaName", displayName)
try json.set("content", Content)
return json
}
Also in Model the i am using extension dataPointStorage: JSONConvertible ,in which convenience init is not getting the content while i already defined the var content as public,
I dont know where i am doing mistake.
On your model, you have to create an initializer with only Node argument. I think your model need to update to following code:
import Vapor
import FluentProvider
final class dataPointStorage: Model, JSONRepresentable, NodeRepresentable {
// var _data: dataPointProtocol?
// var _data:[dataPointProtocol] = []
let storage = Storage()
var id: Node?
var name: String
var displayName: String
public var content: String!
init(node: Node) {
self.content = try node.get(dataPointStorage.Keys.content)
self.displayName = try node.get(dataPointStorage.Keys.displayName)
self.name = try node.get(dataPointStorage.Keys.displayName)
}
struct Keys {
static let id = "id"
static let content = "content"
static let displayName = "displayName"
static let name = "name"
}
//...
}
Hope this help.
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)
}
}
}