Как добавить инициализацию (Swift4) в протокол Decodable

Я пытаюсь создать расширение Codable, способное инициализировать объект Decodable (Swift 4) только с помощью строки json. Итак, что должно работать:

struct MyObject: Decodable {
   var title: String?
}

let myObject = MyObject(json: "{\"title\":\"The title\"}")

Я думаю, это означает, что я должен создать init, который вызывает self.init с помощью декодера. Вот код, который я придумал:

public init?(json: String) throws {
    guard let decoder: Decoder = GetDecoder.decode(json: json)?.decoder else { return }
    try self.init(from: decoder) // Who what should we do to get it so that we can call this?
}

Этот код может получить декодер, но я получаю ошибку компилятора при вызове init. Я получаю следующую ошибку:

'self' используется перед вызовом self.init

Означает ли это, что нет возможности добавить инициализацию в протокол Decodable?

Полный исходный код см. В расширении для кодирования на github < / а>

обновление: после отладки приведенного ниже решения из @appzYourLive я обнаружил, что у меня конфликт с инициализаторами init(json: в Decodable и Array. Я только что опубликовал новую версию расширения на GitHub. Я также добавил решение как новый ответ на этот вопрос.


person Edwin Vermeer    schedule 30.06.2017    source источник


Ответы (3)


Возможный обходной путь

DecodableFromString

Возможное решение - определение другого протокола

protocol DecodableFromString: Decodable { }

с собственным инициализатором

extension DecodableFromString {
    init?(from json: String) throws {
        guard let data = try json.data(using: .utf8) else { return nil }
        guard let value = try? JSONDecoder().decode(Self.self, from: data) else { return nil }
        self = value
    }
}

Соответствие DecodableFromString

Теперь вам нужно согласовать ваш тип с DecodableFromString

struct Person:Codable, DecodableFromString {
    let firstName: String
    let lastName: String
}

Результат

И, наконец, с учетом JSON

let json =  """
            {
            "firstName": "Luke",
            "lastName": "Skywalker"
            }
            """

вы можете построить свою ценность

if let luke = try? Person(from: json) {
    print(luke)
}

Человек (имя: «Люк», фамилия: «Скайуокер»)

person Luca Angeletti    schedule 02.07.2017
comment
ха! ... Сначала я был озадачен, почему это сработало, но почему я не мог добавить это расширение в сам протокол Decodable. Оказывается, мой init (json в протоколе Decodable противоречил тому, который был в протоколе Array. Думаю, теперь он работает как расширение Decodable ... Я еще немного поиграю с ним и дам вам знать результаты. .. - person Edwin Vermeer; 02.07.2017
comment
Поскольку ваш ответ заставил меня задуматься о своей реализации и создал исправление, я думаю, вы заслуживаете 50 баллов. Я пометил свой ответ как правильный, так как именно он у меня получился. - person Edwin Vermeer; 03.07.2017

Вот что можно сделать:

extension Decodable {

    init?(jsonString: String) throws {
        guard let jsonData = jsonString.data(using: .utf8) else { return nil }
        self = try JSONDecoder().decode(Self.self, from: jsonData)
    }

}

См .: https://bugs.swift.org/browse/SR-5356.

[UPD] Проблема исправлена ​​в XCode 9 beta 3 (подробности см. по ссылке выше).

person 0x416e746f6e    schedule 02.07.2017
comment
Это не работает на моем Xcode 9: error: incorrect argument label in call (have 'jsonString:', expected 'from:') - person Luca Angeletti; 02.07.2017
comment
Вы правы, это работает, если Тип явно объявлен соответствующим do Decodable (вместо Codable). Хороший ответ! - person Luca Angeletti; 02.07.2017
comment
Странно .. Codable - это просто typeAlias ​​для DeCodable и Encodable. Это указано так: 'public typealias Codable = Decodable & Encodable' В моем модульном тесте (см. GitHub) он работает с Codable. Просто я использую init без вопросительного знака. Поэтому вместо того, чтобы вернуть nil, я выдаю ошибку. - person Edwin Vermeer; 02.07.2017
comment
хм ... Кому мне поставить 50 баллов ... Я смог решить свою проблему после ответа @appzYourLife, но ваш ответ лучше. Но мое окончательное решение - использовать исправный инициализатор, чтобы обойти быструю ошибку ... - person Edwin Vermeer; 03.07.2017

Моя первоначальная проблема была вызвана двумя причинами:

  • Конфликт между инициализацией с идентичной подписью, которую я добавил в качестве расширения к массиву, который также является кодируемым, когда его внутренние объекты являются кодируемыми.
  • Ошибка быстрого компилятора, которая вызывает проблемы при использовании отказавшего инициализатора. См. https://bugs.swift.org/browse/SR-5356.

Итак, поскольку есть проблема с использованием отказавшего инициализатора, я в итоге получил:

public extension Decodable {
    init(json: String) throws {
        guard let data = json.data(using: .utf8) else { throw CodingError.RuntimeError("cannot create data from string") }
        try self.init(data: data, keyPath: keyPath)
    }

    init(data: Data) throws {
        self = try JSONDecoder().decode(Self.self, from: data)
    }
}

enum CodingError : Error {
    case RuntimeError(String)
}

Я также сделал вариант, в котором вы можете использовать keyPath для перехода к определенному разделу:

public extension Decodable {
    init(json: String, keyPath: String? = nil) throws {
        guard let data = json.data(using: .utf8) else { throw CodingError.RuntimeError("cannot create data from string") }
        try self.init(data: data, keyPath: keyPath)
    }

    init(data: Data, keyPath: String? = nil) throws {
        if let keyPath = keyPath {
            let topLevel = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.mutableContainers)
            guard let nestedJson = (topLevel as AnyObject).value(forKeyPath: keyPath) else { throw CodingError.RuntimeError("Cannot decode data to object")  }
            let nestedData = try JSONSerialization.data(withJSONObject: nestedJson)
            self = try JSONDecoder().decode(Self.self, from: nestedData)
            return
        }
        self = try JSONDecoder().decode(Self.self, from: data)
    }
}

Вы можете найти полный код моих расширений Decodable и Encodable ниже. Это также в моем подспецификации GitHub. С этим расширением вы можете использовать такой код:

struct YourCodableObject : Codable {
    var naam: String?
    var id: Int?
}

let json = yourEncodableObjectInstance.toJsonString()
let data = yourEncodableObjectInstance.toJsonData()
let newObject = try? YourCodableObject(json: json)
let newObject2 = try? YourCodableObject(data: data)
let objectArray = try? [YourCodableObject](json: json)
let objectArray2 = try? [YourCodableObject](data: data)
let newJson = objectArray.toJsonString()
let innerObject = try? TestCodable(json: "{\"user\":{\"id\":1,\"naam\":\"Edwin\"}}", keyPath: "user")
try initialObject.saveToDocuments("myFile.dat")
let readObject = try? TestCodable(fileNameInDocuments: "myFile.dat")
try objectArray.saveToDocuments("myFile2.dat")
let objectArray3 = try? [TestCodable](fileNameInDocuments: "myFile2.dat")

И вот 2 расширения:

//
//  Codable.swift
//  Stuff
//
//  Created by Edwin Vermeer on 28/06/2017.
//  Copyright © 2017 EVICT BV. All rights reserved.
//

enum CodingError : Error {
    case RuntimeError(String)
}

public extension Encodable {
    /**
     Convert this object to json data

     - parameter outputFormatting: The formatting of the output JSON data (compact or pritty printed)
     - parameter dateEncodinStrategy: how do you want to format the date
     - parameter dataEncodingStrategy: what kind of encoding. base64 is the default

     - returns: The json data
     */
    public func toJsonData(outputFormatting: JSONEncoder.OutputFormatting = .compact, dateEncodingStrategy: JSONEncoder.DateEncodingStrategy = .deferredToDate, dataEncodingStrategy: JSONEncoder.DataEncodingStrategy = .base64Encode) -> Data? {
        let encoder = JSONEncoder()
        encoder.outputFormatting = outputFormatting
        encoder.dateEncodingStrategy = dateEncodingStrategy
        encoder.dataEncodingStrategy = dataEncodingStrategy
        return try? encoder.encode(self)
    }

    /**
     Convert this object to a json string

     - parameter outputFormatting: The formatting of the output JSON data (compact or pritty printed)
     - parameter dateEncodinStrategy: how do you want to format the date
     - parameter dataEncodingStrategy: what kind of encoding. base64 is the default

     - returns: The json string
     */
    public func toJsonString(outputFormatting: JSONEncoder.OutputFormatting = .compact, dateEncodingStrategy: JSONEncoder.DateEncodingStrategy = .deferredToDate, dataEncodingStrategy: JSONEncoder.DataEncodingStrategy = .base64Encode) -> String? {
        let data = self.toJsonData(outputFormatting: outputFormatting, dateEncodingStrategy: dateEncodingStrategy, dataEncodingStrategy: dataEncodingStrategy)
        return data == nil ? nil : String(data: data!, encoding: .utf8)
    }


    /**
     Save this object to a file in the temp directory

     - parameter fileName: The filename

     - returns: Nothing
     */
    public func saveTo(_ fileURL: URL) throws {
        guard let data = self.toJsonData() else { throw CodingError.RuntimeError("cannot create data from object")}
        try data.write(to: fileURL, options: .atomic)
    }


    /**
     Save this object to a file in the temp directory

     - parameter fileName: The filename

     - returns: Nothing
     */
    public func saveToTemp(_ fileName: String) throws {
        let fileURL = try FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: false).appendingPathComponent(fileName)
        try self.saveTo(fileURL)
    }



    #if os(tvOS)
    // Save to documents folder is not supported on tvOS
    #else
    /**
     Save this object to a file in the documents directory

     - parameter fileName: The filename

     - returns: true if successfull
     */
    public func saveToDocuments(_ fileName: String) throws {
        let fileURL = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false).appendingPathComponent(fileName)
        try self.saveTo(fileURL)
    }
    #endif
}

public extension Decodable {
    /**
     Create an instance of this type from a json string

     - parameter json: The json string
     - parameter keyPath: for if you want something else than the root object
     */
    init(json: String, keyPath: String? = nil) throws {
        guard let data = json.data(using: .utf8) else { throw CodingError.RuntimeError("cannot create data from string") }
        try self.init(data: data, keyPath: keyPath)
    }

    /**
     Create an instance of this type from a json string

     - parameter data: The json data
     - parameter keyPath: for if you want something else than the root object
     */
    init(data: Data, keyPath: String? = nil) throws {
        if let keyPath = keyPath {
            let topLevel = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.mutableContainers)
            guard let nestedJson = (topLevel as AnyObject).value(forKeyPath: keyPath) else { throw CodingError.RuntimeError("Cannot decode data to object")  }
            let nestedData = try JSONSerialization.data(withJSONObject: nestedJson)
            let value = try JSONDecoder().decode(Self.self, from: nestedData)
            self = value
            return
        }
        self = try JSONDecoder().decode(Self.self, from: data)
    }

    /**
     Initialize this object from an archived file from an URL

     - parameter fileNameInTemp: The filename
     */
    public init(fileURL: URL) throws {
        let data = try Data(contentsOf: fileURL)
        try self.init(data: data)
    }

    /**
     Initialize this object from an archived file from the temp directory

     - parameter fileNameInTemp: The filename
     */
    public init(fileNameInTemp: String) throws {
        let fileURL = try FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: false).appendingPathComponent(fileNameInTemp)
        try self.init(fileURL: fileURL)
    }

    /**
     Initialize this object from an archived file from the documents directory

     - parameter fileNameInDocuments: The filename
     */
    public init(fileNameInDocuments: String) throws {
        let fileURL = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false).appendingPathComponent(fileNameInDocuments)
        try self.init(fileURL: fileURL)
    }
}
person Edwin Vermeer    schedule 02.07.2017