web-dev-qa-db-ja.com

Swift 3でJSONを正しく解析する

私はJSONレスポンスを取得して結果を変数に格納しようとしています。 GMバージョンのXcode 8がリリースされるまで、私は以前のリリースのSwiftでこのコードのバージョンを動作させました。私はStackOverflowに関する同様の投稿をいくつか見てみました。 Swift 2 Parsing JSON - 'AnyObject'型の値に添え字を付けることはできません そして Swift 3のJSON Parsing

ただし、そこで伝えられたアイデアはこのシナリオには当てはまりません。

Swift 3でJSONレスポンスを正しく解析する方法は? Swift 3でのJSONの読み方に何か変更がありましたか?

問題のコードは次のとおりです(これは遊び場で実行できます)。

import Cocoa

let url = "https://api.forecast.io/forecast/apiKey/37.5673776,122.048951"

if let url = NSURL(string: url) {
    if let data = try? Data(contentsOf: url as URL) {
        do {
            let parsedData = try JSONSerialization.jsonObject(with: data as Data, options: .allowFragments)

        //Store response in NSDictionary for easy access
        let dict = parsedData as? NSDictionary

        let currentConditions = "\(dict!["currently"]!)"

        //This produces an error, Type 'Any' has no subscript members
        let currentTemperatureF = ("\(dict!["currently"]!["temperature"]!!)" as NSString).doubleValue

            //Display all current conditions from API
            print(currentConditions)

            //Output the current temperature in Fahrenheit
            print(currentTemperatureF)

        }
        //else throw an error detailing what went wrong
        catch let error as NSError {
            print("Details of JSON parsing error:\n \(error)")
        }
    }
}

編集: これはprint(currentConditions)の後のAPI呼び出しの結果のサンプルです。

["icon": partly-cloudy-night, "precipProbability": 0, "pressure": 1015.39, "humidity": 0.75, "precipIntensity": 0, "windSpeed": 6.04, "summary": Partly Cloudy, "ozone": 321.13, "temperature": 49.45, "dewPoint": 41.75, "apparentTemperature": 47, "windBearing": 332, "cloudCover": 0.28, "time": 1480846460]
113
user2563039

まず、リモートURLからデータを同期的にロードしないでくださいURLSessionなどの非同期メソッドを常に使用してください。

「Any」には下付き文字メンバーがありません

これは、コンパイラが中間オブジェクトのタイプを認識していないため(たとえば、["currently"]!["temperature"]currently)、NSDictionaryなどのFoundationコレクションタイプを使用しているため、コンパイラがそのタイプをまったく認識していないためです。

さらに、Swift 3では、添え字付きオブジェクトallのタイプについてコンパイラーに通知する必要があります。

JSONシリアル化の結果を実際の型にキャストする必要があります。

このコードはURLSessionおよびexclusivelySwiftネイティブ型を使用します

let urlString = "https://api.forecast.io/forecast/apiKey/37.5673776,122.048951"

let url = URL(string: urlString)
URLSession.shared.dataTask(with:url!) { (data, response, error) in
  if error != nil {
    print(error)
  } else {
    do {

      let parsedData = try JSONSerialization.jsonObject(with: data!) as! [String:Any]
      let currentConditions = parsedData["currently"] as! [String:Any]

      print(currentConditions)

      let currentTemperatureF = currentConditions["temperature"] as! Double
      print(currentTemperatureF)
    } catch let error as NSError {
      print(error)
    }
  }

}.resume()

currentConditionsのすべてのキー/値のペアを出力するには、次のように書くことができます

 let currentConditions = parsedData["currently"] as! [String:Any]

  for (key, value) in currentConditions {
    print("\(key) - \(value) ")
  }

jsonObject(with dataに関する注意::

多くの(すべてのように思われる)チュートリアルでは、Swiftではまったく意味のない.mutableContainersまたは.mutableLeavesオプションを推奨しています。 2つのオプションは、結果をNSMutable...オブジェクトに割り当てるための従来のObjective-Cオプションです。 Swiftでは、variableはデフォルトで変更可能であり、これらのオプションのいずれかを渡し、結果をlet定数に割り当ててもまったく効果がありません。さらに、ほとんどの実装は、とにかくデシリアライズされたJSONを変更しません。

Swiftで役立つ唯一の(まれな)オプションは.allowFragmentsであり、JSONルートオブジェクトが値タイプ(StringNumberBool、またはnull)の場合は必須ですコレクションの種類(arrayまたはdictionary)。ただし、通常はオプションなしを意味するoptionsパラメーターを省略します。

================================================== =========================

JSONを解析するための一般的な考慮事項

JSONは、適切に配置されたテキスト形式です。 JSON文字列を読み取るのは非常に簡単です。 文字列を注意深く読んでください。 2つのコレクションタイプと4つの値タイプの6つの異なるタイプのみがあります。


コレクションのタイプは

  • 配列-JSON:角括弧内のオブジェクト[]-Swift:[Any]ですが、ほとんどの場合[[String:Any]]
  • Dictionary-JSON:中括弧内のオブジェクト{}-Swift:[String:Any]

値のタイプは

  • String-JSON:二重引用符で囲まれた値"Foo"、さらには"123"または"false" – Swift:String
  • Number-JSON:二重引用符で囲まれた数値not123または123.0 – Swift:IntまたはDouble
  • Bool-JSON:trueまたはfalsenot二重引用符で囲む– Swift:trueまたはfalse
  • null-JSON:null – Swift:NSNull

JSON仕様によると、辞書のすべてのキーはStringである必要があります。


基本的には、オプションのバインディングを使用してオプションを安全にアンラップすることが常に推奨されます

ルートオブジェクトが辞書({})の場合、型を[String:Any]にキャストします

if let parsedData = try JSONSerialization.jsonObject(with: data!) as? [String:Any] { ...

OneOfSupportedJSONTypesは上記のJSONコレクションまたは値タイプのいずれかです)

if let foo = parsedData["foo"] as? OneOfSupportedJSONTypes {
    print(foo)
} 

ルートオブジェクトが配列([])の場合、型を[[String:Any]]にキャストします

if let parsedData = try JSONSerialization.jsonObject(with: data!) as? [[String:Any]] { ...

そして、配列で繰り返します

for item in parsedData {
    print(item)
}

特定のインデックスでアイテムが必要な場合は、インデックスが存在するかどうかも確認してください

if let parsedData = try JSONSerialization.jsonObject(with: data!) as? [[String:Any]], parsedData.count > 2,
   let item = parsedData[2] as? OneOfSupportedJSONTypes {
      print(item)
    }
}

まれに、JSONがコレクション型ではなく単なる値型の1つである場合、.allowFragmentsオプションを渡して、結果を適切な値型にキャストする必要があります

if let parsedData = try JSONSerialization.jsonObject(with: data!, options: .allowFragments) as? String { ...

AppleはSwiftブログで包括的な記事を公開しています: Working with JSON in Swift


================================================== =========================

Swift 4+では、Codableプロトコルにより、JSONを構造体/クラスに直接解析するより便利な方法が提供されます。

たとえば、質問内の特定のJSONサンプル(わずかに変更)

let jsonString = """
{"icon": "partly-cloudy-night", "precipProbability": 0, "pressure": 1015.39, "humidity": 0.75, "precip_intensity": 0, "wind_speed": 6.04, "summary": "Partly Cloudy", "ozone": 321.13, "temperature": 49.45, "dew_point": 41.75, "apparent_temperature": 47, "wind_bearing": 332, "cloud_cover": 0.28, "time": 1480846460}
"""

struct Weatherにデコードできます。 Swiftタイプは上記と同じです。追加のオプションがいくつかあります。

  • URLを表す文字列は、URLとして直接デコードできます。
  • time整数は、Date.secondsSince1970を使用してdateDecodingStrategyとしてデコードできます。
  • snaked_cased JSONキーは、keyDecodingStrategy.convertFromSnakeCasecamelCaseに変換できます

struct Weather: Decodable {
    let icon, summary: String
    let pressure: Double, humidity, windSpeed : Double
    let ozone, temperature, dewPoint, cloudCover: Double
    let precipProbability, precipIntensity, apparentTemperature, windBearing : Int
    let time: Date
}

let data = Data(jsonString.utf8)
do {
    let decoder = JSONDecoder()
    decoder.dateDecodingStrategy = .secondsSince1970
    decoder.keyDecodingStrategy = .convertFromSnakeCase
    let result = try decoder.decode(Weather.self, from: data)
    print(result)
} catch {
    print(error)
}

その他のコード化可能なソース:

163
vadian

Swift 3のXcode 8 Beta 6で起こった大きな変化は、idがAnyではなくAnyObjectとしてインポートされるようになったことです。

これは、parsedData[Any:Any]型の辞書として返されることを意味します。デバッガを使用しないと、NSDictionaryへのキャストが何をするのか正確にはわかりませんが、表示されるエラーはdict!["currently"]!の型がAnyであるためです

それで、あなたはこれをどのように解決しますか?あなたがそれを参照した方法から、私はdict!["currently"]!が辞書であると思います、そして、あなたは多くのオプションがあります:

まず、あなたはこのようなことをすることができます:

let currentConditionsDictionary: [String: AnyObject] = dict!["currently"]! as! [String: AnyObject]  

これはあなたが値を問い合わせることができる辞書オブジェクトをあなたに与えるでしょう、そしてあなたはこのようにあなたの体温を得ることができます:

let currentTemperatureF = currentConditionsDictionary["temperature"] as! Double

あるいは、あなたが望むなら、あなたはそれをインラインで行うことができます:

let currentTemperatureF = (dict!["currently"]! as! [String: AnyObject])["temperature"]! as! Double

うまくいけば、これが役立ちます、私はそれをテストするためのサンプルアプリを書く時間がなかったと思います。

最後の注意事項:最も簡単なことは、JSONペイロードを最初の[String: AnyObject]にキャストすることです。

let parsedData = try JSONSerialization.jsonObject(with: data as Data, options: .allowFragments) as! Dictionary<String, AnyObject>
12
discorevilo
let str = "{\"names\": [\"Bob\", \"Tim\", \"Tina\"]}"

let data = str.data(using: String.Encoding.utf8, allowLossyConversion: false)!

do {
    let json = try JSONSerialization.jsonObject(with: data, options: []) as! [String: AnyObject]
    if let names = json["names"] as? [String] 
{
        print(names)
}
} catch let error as NSError {
    print("Failed to load: \(error.localizedDescription)")
}
6
BhuShan PaWar

この目的のために quicktype を構築しました。サンプルのJSONを貼り付けるだけで、quicktypeがAPIデータのこの型階層を生成します。

struct Forecast {
    let hourly: Hourly
    let daily: Daily
    let currently: Currently
    let flags: Flags
    let longitude: Double
    let latitude: Double
    let offset: Int
    let timezone: String
}

struct Hourly {
    let icon: String
    let data: [Currently]
    let summary: String
}

struct Daily {
    let icon: String
    let data: [Datum]
    let summary: String
}

struct Datum {
    let precipIntensityMax: Double
    let apparentTemperatureMinTime: Int
    let apparentTemperatureLowTime: Int
    let apparentTemperatureHighTime: Int
    let apparentTemperatureHigh: Double
    let apparentTemperatureLow: Double
    let apparentTemperatureMaxTime: Int
    let apparentTemperatureMax: Double
    let apparentTemperatureMin: Double
    let icon: String
    let dewPoint: Double
    let cloudCover: Double
    let humidity: Double
    let ozone: Double
    let moonPhase: Double
    let precipIntensity: Double
    let temperatureHigh: Double
    let pressure: Double
    let precipProbability: Double
    let precipIntensityMaxTime: Int
    let precipType: String?
    let sunriseTime: Int
    let summary: String
    let sunsetTime: Int
    let temperatureMax: Double
    let time: Int
    let temperatureLow: Double
    let temperatureHighTime: Int
    let temperatureLowTime: Int
    let temperatureMin: Double
    let temperatureMaxTime: Int
    let temperatureMinTime: Int
    let uvIndexTime: Int
    let windGust: Double
    let uvIndex: Int
    let windBearing: Int
    let windGustTime: Int
    let windSpeed: Double
}

struct Currently {
    let precipProbability: Double
    let humidity: Double
    let cloudCover: Double
    let apparentTemperature: Double
    let dewPoint: Double
    let ozone: Double
    let icon: String
    let precipIntensity: Double
    let temperature: Double
    let pressure: Double
    let precipType: String?
    let summary: String
    let uvIndex: Int
    let windGust: Double
    let time: Int
    let windBearing: Int
    let windSpeed: Double
}

struct Flags {
    let sources: [String]
    let isdStations: [String]
    let units: String
}

また、JSONSerialization.jsonObjectの戻り値をForecastに変換するための依存性のないマーシャリングコードも生成します。これには、厳密に型指定されたForecast値をすばやく解析してそのフィールドにアクセスできる便利なコンストラクタが含まれます。

let forecast = Forecast.from(json: jsonString)!
print(forecast.daily.data[0].windGustTime)

あなたはnpmからnpm i -g quicktypeを使ってquicktypeをインストールすることができますまたは あなたの遊び場に貼り付けるために完全な生成コードを得るために Web UIを使用してください - /。

4
David Siegel

更新あとで、この投稿のおかげでisConnectToNetwork-Function Swiftとのインターネット接続を確認します

私はそれのために追加のメソッドを書きました:

import SystemConfiguration

func loadingJSON(_ link:String, postString:String, completionHandler: @escaping (_ JSONObject: AnyObject) -> ()) {
    if(isConnectedToNetwork() == false){
       completionHandler("-1" as AnyObject)
       return
    }

    let request = NSMutableURLRequest(url: URL(string: link)!)
    request.httpMethod = "POST"
    request.httpBody = postString.data(using: String.Encoding.utf8)

    let task = URLSession.shared.dataTask(with: request as URLRequest) { data, response, error in
    guard error == nil && data != nil else {                                                          // check for fundamental networking error
        print("error=\(error)")
        return
        }

    if let httpStatus = response as? HTTPURLResponse , httpStatus.statusCode != 200 {           // check for http errors
        print("statusCode should be 200, but is \(httpStatus.statusCode)")
        print("response = \(response)")
    }


    //JSON successfull
    do {

        let parseJSON = try JSONSerialization.jsonObject(with: data!, options: .allowFragments)

        DispatchQueue.main.async(execute: {
            completionHandler(parseJSON as AnyObject)
        });


    } catch let error as NSError {
        print("Failed to load: \(error.localizedDescription)")

    }
  }
  task.resume()
}


func isConnectedToNetwork() -> Bool {

    var zeroAddress = sockaddr_in(sin_len: 0, sin_family: 0, sin_port: 0, sin_addr: in_addr(s_addr: 0), sin_zero: (0, 0, 0, 0, 0, 0, 0, 0))
    zeroAddress.sin_len = UInt8(MemoryLayout.size(ofValue: zeroAddress))
    zeroAddress.sin_family = sa_family_t(AF_INET)

    let defaultRouteReachability = withUnsafePointer(to: &zeroAddress) {
        $0.withMemoryRebound(to: sockaddr.self, capacity: 1) {zeroSockAddress in
            SCNetworkReachabilityCreateWithAddress(nil, zeroSockAddress)
        }
    }

    var flags: SCNetworkReachabilityFlags = SCNetworkReachabilityFlags(rawValue: 0)
    if SCNetworkReachabilityGetFlags(defaultRouteReachability!, &flags) == false {
        return false
    }

    let isReachable = (flags.rawValue & UInt32(kSCNetworkFlagsReachable)) != 0
    let needsConnection = (flags.rawValue & UInt32(kSCNetworkFlagsConnectionRequired)) != 0
    let ret = (isReachable && !needsConnection)

    return ret

}

だから今、あなたは簡単にあなたが欲しいところはどこでもあなたのアプリでこれを呼び出すことができます

loadingJSON("yourDomain.com/login.php", postString:"email=\(userEmail!)&password=\(password!)") {
            parseJSON in

            if(String(describing: parseJSON) == "-1"){
                print("No Internet")
            } else {

                if let loginSuccessfull = parseJSON["loginSuccessfull"] as? Bool {
                    //... do stuff
                }
  }
3
Marco Weber

問題はAPIインタラクションメソッドにあります。JSONの構文解析はSyntaxでのみ変更されます。主な問題はデータの取得方法にあります。あなたが使用しているのはデータを取得する同期的な方法です。どのような場合でもこれはうまくいきません。あなたが使うべきものはデータをフェッチするための非同期的な方法です。このように、あなたはAPIを通してデータを要求し、それがデータで応答するのを待つ必要があります。あなたはURLセッションとAlamofireのようなサードパーティのライブラリでこれを達成することができます。以下はURLセッションメソッドのコードです。

let urlString = "https://api.forecast.io/forecast/apiKey/37.5673776,122.048951"

let url = URL.init(string: urlString)
URLSession.shared.dataTask(with:url!) { (data, response, error) in
  guard error == nil else {
  print(error)
  }
  do {

    let Data = try JSONSerialization.jsonObject(with: data!) as! [String:Any] // Note if your data is coming in Array you should be using [Any]()
    //Now your data is parsed in Data variable and you can use it normally

    let currentConditions = Data["currently"] as! [String:Any]

    print(currentConditions)

    let currentTemperatureF = currentConditions["temperature"] as! Double
    print(currentTemperatureF)
  } catch let error as NSError {
    print(error)

  }

}.resume()
0
Arun K

Swiftは強力な型推論をします。 「if let」または「guard let」の定型句を削除し、機能的なアプローチを使用してラップを解除することができます。

  1. これが私たちのJSONです。オプションのJSONまたは通常のものを使用できます。私たちの例ではoptionalを使っています:

    let json: Dictionary<String, Any>? = ["current": ["temperature": 10]]

  1. ヘルパー関数私たちはそれらを一度だけ書いて、それからどんな辞書でも再利用する必要があります。

    /// Curry
    public func curry<A, B, C>(_ f: @escaping (A, B) -> C) -> (A) -> (B) -> C {
        return { a in
            { f(a, $0) }
        }
    }

    /// Function that takes key and optional dictionary and returns optional value
    public func extract<Key, Value>(_ key: Key, _ json: Dictionary<Key, Any>?) -> Value? {
        return json.flatMap {
            cast($0[key])
        }
    }

    /// Function that takes key and return function that takes optional dictionary and returns optional value
    public func extract<Key, Value>(_ key: Key) -> (Dictionary<Key, Any>?) -> Value? {
        return curry(extract)(key)
    }

    /// Precedence group for our operator
    precedencegroup RightApplyPrecedence {
        associativity: right
        higherThan: AssignmentPrecedence
        lowerThan: TernaryPrecedence
    }

    /// Apply. g § f § a === g(f(a))
    infix operator § : RightApplyPrecedence
    public func §<A, B>(_ f: (A) -> B, _ a: A) -> B {
        return f(a)
    }

    /// Wrapper around operator "as".
    public func cast<A, B>(_ a: A) -> B? {
        return a as? B
    }

  1. そしてここに私たちの魔法があります - 値を抽出してください:

    let temperature = (extract("temperature") § extract("current") § json) ?? NSNotFound

1行のコードで、強制的に展開したり手動で型キャストしたりすることはありません。このコードは遊び場で機能するので、コピーして確認することができます。これがGitHubでの実装 です。

0
J. Doe