2
votes

This has been bugging me. I wished to use Moya + ReactiveCocoa and json-swift.

It does not seem easy without rewriting some of these frameworks as at one point it seems I need to return an AnyObject and yet created some data which conforms to Equatable and hence doesn't allow this.

Here is a toy Playground-ready example to demonstrate an error message

struct JSValue: Equatable {
    let value:String
}

func ==(lhs: JSValue, rhs: JSValue) -> Bool {
    return (lhs.value == rhs.value)
}

var jsv = JSValue(value: "abc")
var anyValue = jsv as AnyObject

The last line errors with this "Cannot downcast from JSValue to non-@objc protocol type 'AnyObject'".

I would normally just avoid casting to AnyObject but I have been trying to use an implementation of JSValue from json-swift and repeat the pattern described inside Moya's implementation of ReactiveCocoa seen with the usage of tryMap here. The signature of the callback required by tryMap in the Moya + RACSignal extension expects a return of an AnyObject.

For those that would prefer to see an example than understand the context, here is what I mean:

import Foundation

import Moya
import ReactiveCocoa

import JSONLib

/*
let MoyaErrorDomain = "Moya"

public enum MoyaErrorCode: Int {
    case ImageMapping = 0
    case JSONMapping
    case StringMapping
    case StatusCode
    case Data
}
*/

public typealias JSParsingResult = (value: JSValue?, error: Error?)

/// Extension for processing raw NSData generated by network access.
public extension RACSignal {

    /// Maps data received from the signal into a literal JSON type. If the conversion fails, the signal errors.
    public func mapLiteralJSON() -> RACSignal {
        return tryMap({ (object, error) -> AnyObject! in
            var json:AnyObject?
            if let response = object as? MoyaResponse {
                let parsingResult = JSON.parse(response.data)
                json = parsingResult.value! as AnyObject // The same error message as described previously.
            }

            // Note: ignore that I am not handling errors yet...
            /*
            if json == nil && error != nil && error.memory == nil {
                var userInfo: [NSObject : AnyObject]?
                if object != nil {
                    userInfo = ["data": object]
                }

                error.memory = NSError(domain: MoyaErrorDomain, code: MoyaErrorCode.JSONMapping.toRaw(), userInfo: userInfo)
            }
            */

            return json
        })
    }
}

Are there any quick ways around this error message, or is my only choice to use a (1) different method of parsing JSON which does not conform to Equatable, or (2) rewrite Moya to use the other version of ReactiveCocoa in which tryMap does not return AnyObject, or (3) not using ReactiveCocoa.

It would be really nice to fix this properly by avoiding AnyObject, but at this point I probably lack both the Swift and ReactiveCocoa skills to do so...

On a non-technical level, what are ways around this? Would it be wise to decrease my exposure to unfinished tech?

3
What if you make it a class instead of a struct? - Arno van Lieshout
The problem is that I do not control that piece of code. The struct that conforms to Equatable comes from json-swift an abstraction to handle JSON better in Swift. - olive
Definitely you cannot downcast the struct to 'AnyObject', however it's possible to cast your struct to 'Any' type. - tikhop
It works with a class because AnyObject can represent an instance of any class type. (see The Swift Programming Language). In this line: let parsingResult = JSON.parse(response.data);, what is the type that JSON.parse.value! returns? and why do you want to cast this to AnyObject? - Arno van Lieshout
Related, because an answer provides a good explanation of downcast mechanics: Double question mark, How does it work?. - jww

3 Answers

2
votes

You have hit this problem because you are mixing libraries that were written for Objective-C with those that were written for Swift.

Objective-C id maps to AnyObject, which can be a reference to any class.

With Swift, if you want a variable that can reference anything you need to use Any.

Furthermore, with Swift you are encouraged to use structs, where value-type semantic makes sense, for performance reasons. This will no doubt result in structs being used quite frequently within Swift libraries. As a result you can expect significant compatibility issues with Swift libraries and Objective-C libraries.

1
votes

My temporary solution until the Swift version of ReactiveCocoa is released was this:

public class WrappedStruct<T> {

    let data:T?

    init(data: T?) {
        self.data = data
    }

}

I am able to pass that class around as an AnyObject. It's quite a hacky thing to have to do, but it is at least easy to understand.

-1
votes

Yeah, I know that it's desirable to use structs instead of proper classes in Swift, but I've found the following to silence the Playground error.

class JSValue: Equatable {
    let value:String

    init(value: String) {
        self.value = value
    }
}

func ==(lhs: JSValue, rhs: JSValue) -> Bool {
    return (lhs.value == rhs.value)
}

var jsv = JSValue(value: "abc")
var anyValue = jsv as AnyObject

Again, not ideal, but until ReactiveCocoa is fully Swift-ified, it may be your only option. I had to do something similar with the MoyaResponse class.