| import Foundation |
| |
| /// CodableVariable wraps a `Codable` instance to make it compatible with @objc methods. |
| /// |
| /// `Codable` documentation: https://developer.apple.com/documentation/swift/codable |
| /// |
| /// The Swift compiler doesn't allow developers to declare an @objc method for eDO as below: |
| /// |
| /// ``` |
| /// @objc public FooClass : NSObject { |
| /// @objc public func callBar(value: Int?) // Compile Error! |
| /// } |
| /// ``` |
| /// |
| /// The same issue also applies to other auto-synthesized Codable types, including classes, structs |
| /// and enums (Swift 5.5+). This is because Optional<Int> is a pure Swift type that cannot be |
| /// represented in Objective-C land. As a workaround, developers can declare a new method in the |
| /// class extension as below: |
| /// |
| /// ``` |
| /// extension FooClass { |
| /// @objc public func callBar(codedValue: CodableVariable) throws { |
| /// self.callBar(value: try codedValue.unwrap(Int?.self)) |
| /// } |
| /// } |
| /// ``` |
| /// |
| /// The new method is compatible with @objc and forwards the coded variables to the original method. |
| /// So developers can use the new method in the remote call: |
| /// |
| /// ``` |
| /// var value : Int? |
| /// // Do something... |
| /// try remoteFooInstance.callBar(codedValue: try CodableVariable.wrap(value)) |
| /// ``` |
| @objc(EDOCodableVariable) |
| public class CodableVariable: NSObject, NSSecureCoding, Codable { |
| |
| /// Error thrown by `CodableVariable` during the decoding. |
| public enum DecodingError: Error, LocalizedError { |
| /// The type of encoded data doesn't match the caller's expecting type. |
| /// - expectedType: The type expected by the decoding method caller. |
| /// - actualType: The type of the encoded data. |
| case typeUnmatched(expectedType: String, actualType: String) |
| |
| public var errorDescription: String? { |
| switch self { |
| case let .typeUnmatched(expectedType, actualType): |
| return "Expecting to decode \(expectedType) but the codable variable is \(actualType)." |
| } |
| } |
| } |
| |
| internal static let typeKey = "EDOTypeKey" |
| internal let data: Data |
| internal let type: String |
| |
| /// Creates `CodableVariable` instance with a `Codable` instance. |
| /// |
| /// - Parameter parameter: The Codable instance to be wrapped. |
| /// - Returns: A `CodableVariable` instance. |
| /// - Throws: Errors propagated from JSONEncoder when encoding `parameter`. |
| public static func wrap<T: Encodable>(_ parameter: T) throws -> CodableVariable { |
| let encoder = JSONEncoder() |
| return CodableVariable(data: try encoder.encode(parameter), type: String(describing: T.self)) |
| } |
| |
| internal init(data: Data, type: String) { |
| self.data = data |
| self.type = type |
| } |
| |
| /// Decodes the Codable instance. |
| /// |
| /// - Parameter type: The expected type of the decoded instance. |
| /// - Returns: The decoded instance of `type`. |
| /// - Throws: `CodableVariable.DecodingError` if decoding fails. |
| public func unwrap<T: Decodable>() throws -> T { |
| guard self.type == String(describing: T.self) else { |
| throw DecodingError.typeUnmatched( |
| expectedType: self.type, |
| actualType: String(describing: T.self)) |
| } |
| let decoder = JSONDecoder() |
| return try decoder.decode(T.self, from: data) |
| } |
| |
| // MARK - NSSecureCoding |
| |
| public required init?(coder: NSCoder) { |
| guard let data = coder.decodeData(), |
| let type = coder.decodeObject(forKey: CodableVariable.typeKey) as? String |
| else { |
| return nil |
| } |
| self.data = data |
| self.type = type |
| } |
| |
| public func encode(with coder: NSCoder) { |
| coder.encode(data) |
| coder.encode(type, forKey: CodableVariable.typeKey) |
| } |
| |
| @objc public override var edo_isEDOValueType: Bool { return true } |
| |
| @objc public static var supportsSecureCoding: Bool { return true } |
| } |
| |
| /// Extends Encodable to easily produce `CodableVariable` from the instance. |
| extension Encodable { |
| /// Produces a `CodableVariable` instance from `self`. |
| public var eDOCodableVariable: CodableVariable { |
| // try! is used here because`CodableVariable.wrap` only throws programmer errors when |
| // JSONEncoder fails to encode a `Encodable` type. |
| return try! CodableVariable.wrap(self) |
| } |
| } |
| |
| extension Array where Element: AnyObject { |
| // Workaround to interact with a Swift array obtained via eDO to avoid the type mismatch failure |
| // when bridging between NSArray and Swift Array. |
| public var localArray: Self { |
| (self as [AnyObject]).map { unsafeBitCast($0, to: Element.self) } |
| } |
| } |