What is the recommended approach in RxSwift to implement RAC tryMap-like functionality?
The following code is how I do mapping of json objects to an internal response wrapper class. If the response fails to comply with certain conditions, nil will be returned, which turns into an Error Event(tryMap implementation).
extension RACSignal{
func mapToAPIResponse() -> RACSignal{
return tryMap({ (object) -> AnyObject! in
if let data = object as? [String:AnyObject]{
//Some Logic
return data["key"]
}
return nil
})
}
}
How should this be implemented in RxSwift?
Updated-Possible Solution
I came up with following solution for Rx-Swift. Open for better solutions.
extension Observable{
func mapToAPIResponse() -> Observable<APIResponse>{
return map({ (object) in
guard let dictionary = object as? [String:AnyObject] else{
//APIResponseError.InvalidResponseFormat is defined in other class.
throw APIResponseError.InvalidResponseFormat
}
let response = APIResponse()
//Complete API Response
return response
})
}
My conclusion is to use throw inside a map to handle errors.
Your solution is correct, that's why map operator in RxSwift is annotated with throws. Release notes of RxSwift 2 explicitly state this:
Adds support for Swift 2.0 error handling try/do/catch.
You can now just write
API.fetchData(URL)
.map { rawData in
if invalidData(rawData) {
throw myParsingError
}
...
return parsedData
}
Even in `RxCocoa
There is a great way to implement network layer with these set of PODs
RxSwift + Moya/RxSwift + Moya-ObjectMapper/RxSwift
Finally your code for model will looks like
import ObjectMapper
final class Product: Mappable {
var id: String?
var categoryId: String?
var name: String?
func mapping(map: Map) {
id <- map["id"]
categoryId <- map["category_id"]
name <- map["name"]
}
}
And for service
final class ProductService {
class func productWithId(id: String, categoryId: String) -> Observable < Product > {
return networkStubbedProvider
.request(.Product(id, categoryId))
.filterSuccessfulStatusAndRedirectCodes()
.mapObject(Product)
}
}
Related
I have a mapper as follows
protocol MapperProtocol {
associatedtype T
associatedtype U
func map(item: T) -> U?
}
And I want to inject it to a class as follows
protocol ParserProtocol {
associatedtype T
associatedtype U
func parse(from: T) -> U?
}
class TrackParser: ParserProtocol {
typealias T = String
typealias U = Track
private let mapper: MapperProtocol
init(mapper: MapperProtocol) {
self.mapper = mapper
}
func parse(from path: String) -> Track? {
guard let data = try? Data(contentsOf: URL(filePath: path)) else { return nil }
return mapper.map(item: data)
}
}
TrackParser will be initialised from somewhere else so it doesn't need to know the concrete type of the mapper.
When I want to implement it that way I get the following error.
Any ideas how to fix it?
Notice that the parse implementation requires that the mapper has T == Data and U == Track, but you haven't specified those constraint anywhere in TrackParser.
We can make T and U the primary associated types of MapperProtocol, so that the same-type requirements can be specified very easily as MapperProtocol<Data, Track>.
protocol MapperProtocol<T, U> { // Notice the "<T, U>"
associatedtype T
associatedtype U
func map(item: T) -> U?
}
Also, starting from Swift 5.7, existential types are required to be prefixed with "any", so you would write:
private let mapper: any MapperProtocol<Data, Track>
init(mapper: any MapperProtocol<Data, Track>) {
self.mapper = mapper
}
I'm using ReamSwift to store data in my iOS application and I have troubles to figure out, why sometimes objects are duplicated in my application.
I use an UUID as primary key on every object. This property is defined in a base class and all the others are subclasses of that.
I use the update approach with a dictionary. So new objects are created and existing should be updated.
I use a central class which holds the Realm object and handles the update. This method could be called from multiple different threads.
This is my base class
import RealmSwift
open class RealmObject : Object {
static let ID_ATTRIBUTE : String = "id"
#objc dynamic var id : String = ""
override public static func primaryKey() -> String? {
return "id"
}
}
Central class managing swift.
This class holds the Realm Object
public var realm: Realm {
var tempRealm: Realm?
do {
tempRealm = try Realm(configuration: configuration)
}
catch let err {
print("------ERROR----- initializing realm\n \(err)")
}
if let tempRealm = tempRealm{
return tempRealm
}
return self.realm
}
This is the update method in the class. As a fallback if the id property is not set, it will be set as it is the primary key
func update<T: RealmObject>(_ obj : T, values : [String:Any?] ) -> T? {
var vals = values
do {
var res : T?
try realm.write {
if let idattr = vals[T.ID_ATTRIBUTE] as? String {
if(idattr.isEmpty) {
vals[T.ID_ATTRIBUTE] = UUID().uuidString
}
} else {
vals[T.ID_ATTRIBUTE] = UUID().uuidString
}
res = realm.create(T.self, value: vals, update: .modified)
}
return res
} catch {
return nil
}
}
Could calling the update method cause in any case the duplication as I set the primary key of the object? The problem is, I cannot reproduce to find the problem, i just encounter the issue from time to time and in the field from users.
One interesting thing, when a copy is deleted, also the other object will be deleted.
This is the method which deletes objects by id and type
func delete<T: RealmObject>(_ id : String, _ type : T.Type) -> Bool {
do {
let obj = get(id, T.self)
if let obj = obj {
try realm.write {
realm.delete(obj)
}
return true
} else {
return false
}
} catch {
return false
}
}
So I'm trying to make sense of how to use Realm, Moya, and ObjectMapper.
I use Moya to make requests to my API. I use Realm to keep the returned data in a local database. And I use ObjectMapper to map the JSON objects to correct Realm variable.
However, I've come to an issue now where I'm not sure how to decode the JSON response in order to put it through the mapper.
Here is my Moya code:
provider.request(.signIn(email: email, password: password)) { result in
switch result {
case let .success(response):
do {
// Get the response data
let data = try JSONDecoder().decode(MyResponse.self, from: response.data)
// Get the response status code
let statusCode = response.statusCode
// Check the status code
if (statusCode == 200) {
// Do stuff
}
} catch {
print(error)
}
case let .failure(error):
print(error)
break
}
}
The error happens on this line:
In argument type 'MyResponse.Type', 'MyResponse' does not conform to expected type 'Decodable'
The MyResponse class looks like this:
class MyResponse: Object, Mappable {
#objc dynamic var success = false
#objc dynamic var data: MyResponseData? = nil
required convenience init?(map: Map) {
self.init()
}
func mapping(map: Map) {
}
}
I understand why I'm getting that error, I just don't know the correct way to solving it. Am I missing something in the documentation of one of the above frameworks? Am I doing this totally wrong? How should I fix my line of code?
I've tried #Kamran's solution, but I got the error:
Argument labels '(JSON:)' do not match any available overloads
On the line:
let myResponse = MyResponse(JSON: json)
You are getting that error because you are decoding using Swift JSONDecoder which requires you to implement Codable which wraps Encodable and Decodable (JSON <-> YourObject).
If you're using Swift 4 you can use Codable instead of relying on 3rd party libraries.
MyResponse would become:
class MyResponse: Codable {
let success: Bool
let data: MyResponseData?
}
And MyResponseData should implement Codable too.
After this you should be able to do:
do {
let data = try JSONDecoder().decode(MyResponse.self, from: response.data)
} catch let error {
// handle error
}
I am using ObjectMapper library to map my JSON object to Swift object. The traditional method of library is working fine for me like below code.
tmpArray1 = Mapper<UserModel>().mapArray(JSONArray: result1)
tmpArray2 = Mapper<CompanyModel>().mapArray(JSONArray: result2)
Now, I want to create a generic method to return dynamic object according to argument which i pass in that function. I want somewhat like below.
tmpArray1 = WrapperClass.shared.getModelObject(objType: UserModel, data: Any)
tmpArray2 = WrapperClass.shared.getModelObject(objType: CompanyModel, data: Any)
Here is my WrapperClass.swift class code:
open class WrapperClass: NSObject {
static let shared = WrapperClass()
open func getModelObject(objType: Mappable, data: Any) -> Any? {
// Need Help Here
return <dynamic object>
}
}
I don't know my approach is 100% right but i want somewhat like whatever object type i pass in the argument of the function i want same object type in return with mapped with ObjectMapper object. I am using Swift 4.0 version.
You can find the ObjectMapper
here.
Update:
I have tried below thing but it will not work, show an error
func getModelObject<T: Mappable>(modelType: T.Type, data: Any) -> [T]? {
if data is Array<Any> {
return Mapper<modelType>().mapArray(JSONArray: data as! [[String: Any]])
//ERROR: Use of undeclared type 'modelType'
}
return nil
}
You can achieve that by combination of generics and Type. It allows you to instantiate the mappable object with generic T (no Mapper<...> here):
func getModelObject<T: Mappable>(objType: T.Type, data: Any) -> T? {
if let data = data as? [String: Any] {
return T(JSON: data)
} else if let data = data as? String {
return T(JSONString: data)
}
return nil
}
Example of usage:
class User: Mappable {
var name: String!
var age: Int!
required init?(map: Map) {}
func mapping(map: Map) {
name <- map["name"]
age <- map["age"]
}
}
let json = "{\"name\":\"Bob\",\"age\":100}"
if let user = WrapperClass.shared.getModelObject(objType: User.self, data: json) {
print(user.name, user.age)
}
Answer with Mapper<...>:
func getModelObject<T: Mappable>(data: Any) -> [T]? {
if let data = data as? [[String: Any]] {
return Mapper<T>().mapArray(JSONArray: data)
}
return nil
}
Let me explain the issue with example.
I have two protocols named - Mappable(available on Git) and Responsable(I created that which conforms to the Mappable protocol)
protocol Responsable: Mappable {
static func getResponse(map: Map) -> Self
}
Approach 1
then I have struct 'NSIResponse' which is generic and conforms to Mappable and generic T is Resposable type
struct NSIResponse<T>: Mappable where T: Responsable {
mutating func mapping(map: Map) {
response = T.getResponse(map: map)
}
}
After that I created the struct of User Object which conforms to the Responsable protocol.
struct User: Responsable {
var id: Int ?
mutating func mapping(map: Map) {
id <- map["id"]
}
static func getResponse(map: Map) -> User {
guard let response = Mapper <User>().map(JSONObject: map["response"].currentValue)
else {
return User()
}
return response
}
}
Approach 2
Well, I created getResponse or Responsable because I don't want to use more lines in mapping method in NSIResponse like following
mutating func mapping(map: Map) {
switch T.self {
case is User.Type: response = Mapper<User>().map(JSONObject: map["response"].currentValue) as? T
case is Array<User>.Type: response = Mapper<User>().mapArray(JSONObject: map["response"].currentValue) as? T
default: response <- map["response"]
}
}
I dont want to use previous approach because if I do then I have to write every single two lines of code for every class. Result, function length will increase. Therefore, I created the T.getResponse(map: map) method.
Now Issue which I am facing
let jsonResponse = response.result.value as? [String: AnyObject]
let nsiResponse = NSIResponse<User>(JSON: jsonResponse) // WORKING
let nsiResponse1 = NSIResponse<[User]>(JSON: jsonResponse) // NOT WORKING and getting Type '[User]' does not conform to protocol Responsable
However, its working fine in case of Approach 2..
I would be very appreciated if you could help me with Approach 1.
I hope you understood my question.
The reason for this error is because the type Array does not conform to Responsable, only User does. In approach 2, you cover that case so it works.
What you have to do is extend the Array type, so it can conform to Responsable:
extension Array: Responsable {
mutating func mapping(map: Map) {
// Logic
}
static func getResponse(map: Map) -> User {
// Logic
}
}