20200714のSwiftに関する記事は7件です。

Swift UIでSceneKitを使う

個人製作でSceneKitでARを実装することがあったのでメモります

SwiftUIをVIPERとCombineで書いていきます

CALayerの表示

SceneKitのカメラからの入力を表示するためにCALayerを表示するための UIViewControllerRepresentable を用意します

import SwiftUI

struct CALayerView: UIViewControllerRepresentable {
    var caLayer: CALayer
    let frame: CGRect // frameの大きさ

    func makeUIViewController(context: UIViewControllerRepresentableContext<CALayerView>) -> UIViewController {
        let viewController = UIViewController()

        viewController.view.layer.addSublayer(caLayer)
        caLayer.frame = frame

        return viewController
    }

    func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<CALayerView>) {
        caLayer.frame = frame
    }
}

セッションの操作

InteractorにSceneKitのセッションのセットアップなどのメソッドを定義します

import ARKit
import Foundation
import SceneKit
import SwiftUI

final class ArInteractor: NSObject, ARSCNViewDelegate {
    private let sceneView = ARSCNView()

    func setupAR() -> CALayer {
        sceneView.scene = SCNScene()
        sceneView.autoenablesDefaultLighting = true
        sceneView.delegate = self

        return sceneView.layer
    }

    func startSettion() {
        let configuration = ARWorldTrackingConfiguration()

      // SceneKitで3Dオブジェクトのdetectionをしたかったのでリソースを読み込ませる
        guard let detectionObjects = ARReferenceObject.referenceObjects(
            inGroupNamed: "AR Resources",
            bundle: nil
        ) else {
            fatalError("Missing expected asset catalog resources.")
        }
        configuration.detectionObjects = detectionObjects
        sceneView.session.run(configuration)
    }

    func stopSettion() {
        sceneView.session.pause()
    }

    func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
      // この中で3Dオブジェクトを検知した際の処理を書く
    }
}

ViewとSceneKitを繋ぐ

PresenterでViewとInteractorの繋ぎ込みをして、ViewからSceneKitを操作できるようにします。

import Combine
import Foundation
import SwiftUI

final class ArPresenter: ObservableObject {
    private let router = ArRouter()
    private let interactor = ArInteractor()

    @Published var previewLayer: CALayer {
        willSet {
            objectWillChange.send()
        }
    }

    init() {
        previewLayer = interactor.setupAR()
    }

    func onAppearArView() {
        interactor.startSettion()
    }

    func onDisappearArView() {
        interactor.stopSettion()
    }
}

ARSCNViewを表示させる

最後にViewで先ほど作ったCALayerViewコンポーネントを使ってARSCNViewのCALayerを流し込んで表示させます

import SwiftUI

struct ArView: View {
    @ObservedObject var presenter: ArPresenter

    var body: some View {
        ZStack {
            VStack {
                ZStack {
                    CALayerView(
                        caLayer: presenter.previewLayer,
                        frame: CGRect(x: 0, y: 0, width: 300, height: 300)
                    )
                }
                .onAppear {
                    self.presenter.onAppearArView()
                }
                .onDisappear {
                    self.presenter.onDisappearArView()
                }
            }
        }
    }
}

これで表示されたと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【小ネタ】あなたの平均値の計算、本当にできていますか?

免責事項

本記事で扱っているのはあくまでかなり極端的なケースであって、普段の開発ではそこまで神経質になる必要はないと思います;そのため「こう言うこともあるね」とどこか頭の中に入れて、実際このようなバグが発生したとき思い出していただければと思いますので「小ネタ」として書かせていただきます。

うわっ…私の平均値、膨らみすぎ…?

安心してください、私は別にこの記事で平均値と中央値の違いとかみたいなセコいことを言うつもりはありません、あくまで我々が日常で使ってる「平均値」のことです。$(x + y) \div 2$ のことです。

ところで上記の数式の通り、我々は平均値の定義1を、二つの数字を足して 2 で割る、という風に教わったはずかと思います。そのため我々は基本このように平均値メソッドを書くことがほとんどかと思いますね。筆者は iOS エンジニアなので、ここは Swift のプログラム2を書きますが他の言語も似たような実装になるかと思います:

func average(x: Int, y: Int) -> Int {
    return (x + y) / 2
}

実際このプログラムを回してみて、例えば 13 の平均値をこれで回してみれば、確かに正しい平均値の 2 が得られます:

average(x: 1, y: 3) // 2

何も問題ないじゃん?と思いますよね。

では、46116860184273879034611686018427387905 の平均値はどうでしょう?実際回してみましょうか

average(x: 4611686018427387903, y: 4611686018427387905) // ???

さて、ここは言語や処理系によって違いが出るところですかね?例えば Swift ならここではプログラムが EXC_BAD_INSTRUCTION エラーで落ちます;C ならここで 0 が出るかもしれませんし、他に -4611686018427387904 が出る言語もあるかもしれません。どのみち、64 bit の処理系なら、おそらくここで正解の 4611686018427387904 が出る言語はそうそうないでしょう。

お気づきでしょうか?そうです。プログラミングの計算と、実際の数学の計算と大きな違いがあります:数字自体の制限です。

64 bit の処理系なら、Int の最大値は 9223372036854775807 です。ところで 46116860184273879034611686018427387905 を愚直に足してしまうと 9223372036854775808 になり、Int の極限を超えちゃってオーバーフローになります。そのため結果が Int で正しく表現できず、Swift ならここで落ちますし、C 言語ならここで 0 になってしまいます。

ちなみに記憶が曖昧ですが、確か Java が長い間このバグが潜んでいたことがあったはずです3

では平均値をどう求めればいいか

平均値をグラフの考え方で考えてみれば、低い方の数字に、両方の数字の差の半分を足す、という考え方もできます。つまりコードにするとこんな感じです:

func average(x: Int, y: Int) -> Int {
    return x + ((y - x) / 2)
}

上記のコードは暗黙的に xy より小さい、という前提条件が入ってるように見えますが、実際は符号が着くおかげで、y の方が小さかったら y - x もマイナスになって結果正しい平均値が得られます4

average(x: 3, y: 1) // 2

そしてこの方法の一番のメリットは、途中の計算結果が xy を超えることがないので、xy さえ制限を超えなければ正しい平均値が求められます。実際先ほどの Java のバグの件も、このように修正を入れてたはずです。ですので今後は平均値を求めるときは

(x + y) \div 2

ではなく、

x + ((y - x) \div 2)

で求めましょう!

ちょっと待って…本当にそれでいいの??

先ほど私

途中の計算結果が xy を超えることがない

と言いましたよね?あれは嘘です。超えることがあるんです。

これまで私が挙げてきた例は全て符号付き整数型を使っていましいたが、具体的な数字は全て正数です。ところが負数が入ってきたらどうなるのかな?

答えは、両方とも正数もしくは両方とも負数の場合、確かに $x - y$ の差(の絶対値)が xy を超えることはないが、逆に片方が正数で片方が負数の場合、その差は逆に xy を超えます。例えば:

average(a: 4611686018427387905, b: -4611686018427387905) // ???

上記の場合、逆に $x + ((y - x) \div 2)$ で計算するとオーバーフローが発生します。この場合、むしろ普通の和を 2 で割る方法で求めれば正しい結果が得られます。

まとめると:

平均値
$x$ と $y$ が同じ符号 $x + ((y - x) \div 2)$
$x$ と $y$ が違う符号 $(x + y) \div 2$

です。

後書き

免責事項で書きましたとおり、本記事はあくまでかなり極端的なケースであって、普段の開発ではそこまで神経質になる必要はないと思います。


  1. 数字が三つ以上出ると話がややこしくなるので、ここはひとまず二つの数字の平均値を扱います。 

  2. 本当は割り算があるので整数はあまりよくないですが、本記事もひとまず理解がしやすいためにあえて整数を選びました 

  3. 以前どこかで Java かなんかのシステムがずっとこの実装で平均値を求めていて、どこかでバグって多くのデバイスに影響を及ぼした的な記事を読んだ記憶がありますが、はっきりと出自が思い出せないので、知ってる方がいらっしゃいましたら補足できると嬉しいです。 @naskya さんと @fujitanozomu さんのおかげで、そのバグは Java の二分探索の実装にあったことが判明しました。詳しくはコメント欄をご参照ください。 

  4. 正確には、x + ((y - x) / 2) の足し算基準値に使われる x が大きい方か小さい方かによって、演算精度の影響による誤差が出て違う結果になることがあり得ます。詳しくはコメント欄をご参照ください。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SwiftUI でアプリ全体の TintColor を変更する

SceneDelegate で色を指定する

SwiftUI プロジェクトを新規作成すると、 SceneDelegate 内に UIWindow を生成するコードがデフォルトで用意されるはずです。
生成された window の tintColor プロパティに任意の色を指定すると、アプリ全体の TintColor を変更することができます。

SceneDelegate.swift
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
    let contentView = ContentView()

    if let windowScene = scene as? UIWindowScene {
        let window = UIWindow(windowScene: windowScene)
        window.rootViewController = UIHostingController(rootView: contentView)

        // この 1 行を追加する
        window.tintColor = .systemGreen

        self.window = window
        window.makeKeyAndVisible()
    }
}

今回の例では systemGreen を指定していますが、 型は UIColor なのでアプリのイメージカラーに応じて自由に設定できます。
例のように systemColor にしておくと、ダークモード時にシステム側が上手く色を調整してくれるので大変便利です。
カスタムカラーを用いるかどうかはデザイナーさんと相談して決めてください。

window.tintColor = UIColor(displayP3Red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0)

ちなみに残念ながら window.tintColor で指定した値は Preview には反映されません。

余談

SwiftUI で色を変更する手段として、 foregroundColor() , accentColor() , background() などを設定する方法があります。
これらを ContentView のようなルートとなる View に設定して色を変えるという方法も考えられます。

struct ContentView: View {
    var body: some View {
        TabView {
            ...
        }.accentColor(.green)
    }
}

この場合は Preview にも反映されるため、その恩恵を受けることができます。しかし、

色の指定が反映されない View もあるので注意が必要です。

例えば Button 内の Text がデフォルトカラーのまま表示されることを確認しています。
この挙動はおそらく不具合ではなく SwiftUI の仕様です。

全体の色を一括で変更したい場合は tintColor を指定し、accentColor() などは各 View に対して細かな指定をしたい場合のみ設定する方が良いと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Swift】NSLayoutAnchorを使用し、AutoLayoutをコードで実装してみる(画面の右下に固定したボタンを表示)

はじめに

いつもはStoryboardからAutoLayoutを使用し制約を付けていたのですが、
コードから制約をつける機会があり、NSLayoutAnchorを使用したので、その方法を記載したいと思います。
今回はTwitterのように、画面の右下に固定されたボタンを表示させる方法を書いていきます(UITableViewの上に設置しています)。
概要

環境

  • Swift:5.1.3
  • Xcode:11.3.1

NSLayoutAnchorの使い方

こちらの記事が詳しいです。
どの位置を起点にして制約をつけるかは、先ほどの記事を引用させていただくと下記のようになります。

プロパティ名 位置
leadingAnchor 左端
trailingAnchor 右端
topAnchor 上端
bottomAnchor 下端
centerYAnchor オブジェクトのY座標軸の中心
centerXAnchor オブジェクトのX座標軸の中心
widthAnchor オブジェクトの横幅
heightAnchor オブジェクトの縦幅

実装してみる

// UIButtonを生成する
let plusButton = UIButton()
// 必ずfalseにする(理由は後述)
plusButton.translatesAutoresizingMaskIntoConstraints = false
// tintColorを黒にする
plusButton.tintColor = .black
// グレーっぽくする
plusButton.backgroundColor = UIColor(red: 0.92, green: 0.92, blue: 0.92, alpha: 1)
// 正円にする
plusButton.layer.cornerRadius = 25
// plustButtonのImageをplus(+)に設定する
plusButton.setImage(UIImage(systemName: "plus"), for: .normal)
// ViewにplusButtonを設置する(必ず制約を設定する前に記述する)
self.view.addSubview(plusButton)

// 以下のコードから制約を設定している
// plustButtonの下端をViewの下端から-50pt(=上に50pt)
plusButton.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: -50).isActive = true
// plustButtonの右端をViewの右端から-30pt(=左に30pt)
plusButton.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -30).isActive = true
// plustButtonの幅を50にする
plusButton.heightAnchor.constraint(equalToConstant: 50).isActive = true
// plusButtonの高さを50にする
plusButton.widthAnchor.constraint(equalToConstant: 50).isActive = true

注意点

制約は必ず部品を配置してから

部品を配置していないのに制約をつけることはできません。
つまり、今回で言うと制約を設定する前にself.view.addSubview(plusButton)を記述しておく必要があります。
そうしないとビルドは通りますが、下記実行時エラーでクラッシュします。

Terminating app due to uncaught exception 'NSGenericException', reason: 'Unable to activate constraint with anchors <NSLayoutYAxisAnchor:0x600003dec2c0 "UIButton:0x7fa4a7704d90.bottom"> and <NSLayoutYAxisAnchor:0x600003d6b140 "UIView:0x7fa4a77024c0.bottom"> because they have no common ancestor.  Does the constraint or its anchors reference items in different view hierarchies?  That's illegal.'

translatesAutoresizingMaskIntoConstraints は false に

translatesAutoresizingMaskIntoConstraintsはAutoLayout以前に使われていたAutosizingというレイアウトの仕組みを、AutoLayoutに変換するかどうかを設定するフラグらしいです。
デフォルトでは、trueになっていますが、Autosizingの制約とAutoLayoutの制約がコンフリクトを起こす可能性があり、意図した動作にならないことがあるのでfalseにしましょう。
今回の場合、falseに設定しないとplusButtonが表示されませんでした。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SwiftUI で @State の初期状態を外から受け取る

@State private var の場合

例えば、テキストを編集するための View について考えてみましょう。

  1. 前画面から既存の文字列を受け取り、 TextField の初期値として表示する
  2. 表示された文字列を修正することができる

このような仕様の場合、 1 は以下のようなコードで実現できます。

struct TextEditView: View {
    @State private var text: String

    init(text currentText: String) {
        _text = State(initialValue: currentText)
    }

    var body: some View {
        TextField("", text: $text)
    }
    ...
}

let currentText = "Today was a good day."
TextEditView(text: currentText)

あれ?こっちは??

struct TextEditView: View {
    @State private var text: String

    init(text currentText: String) {
        text = currentText    // ❗️ Return from initializer without initializing all stored properties
    }
    ...
}

初期化が完了していないというエラーが出ます。text に代入してるのに。。

実は初期化する必要があるのは、

文字列 ではなく 文字列の状態
String ではなく State<String>
text ではなく _text 1

です。 text の 初期状態 をセットしてあげる必要があります。
(初期値ではなく、初期状態と表現する方が適切かと思います。)

今回の例では currentText: StringState でラップし 状態 として代入しています。

_text = State(initialValue: currentText)

@State var の場合

こちらは素直にコンパイルが通ります。

struct TextEditView: View {
    @State var text: String
    ...
}

let currentText = "Today was a good day."
TextEditView(text: currentText)

イニシャライザが内部でうまく処理してくれてるのかな。


  1. _ (アンダースコア) を付けることで Property Wrapper 自身にアクセスできる。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Alamofire.AFErrorのコード一覧

Alamofire のエラー列挙型 AFError のコード一覧です。

Firebase CrashlyticsCrashlytics.crashlytics().record(error:) でエラーを記録した際にどのエラーが起きているのかわかりにくいので。

case code
createUploadableFailed 0
createURLRequestFailed 1
downloadedFileMoveFailed 2
invalidURL 3
multipartEncodingFailed 4
parameterEncodingFailed 5
parameterEncoderFailed 6
requestAdaptationFailed 7
requestRetryFailed 8
responseValidationFailed 9
responseSerializationFailed 10
serverTrustEvaluationFailed 11
sessionInvalidated 12
sessionTaskFailed 13
urlRequestValidationFailed 14
explicitlyCancelled 15
sessionDeinitialized 16
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SwiftでAPIを使う! モヤモヤを無くしたい

はじめに

swiftでAPIを利用しようとすると、sessionやcompletionHandlerなど操作の意図がわからないコードが多く、モヤモヤしたままとりあえずお手本のコードを写してAPIを利用することが多いと思います。そこでこの記事を読むことで1つ1つの操作を理解し、よりAPIに関する理解を深められたらいいなと思っています。

ちなみにこの記事は、APIを使ってデータを取ってきて、print()で出力するところまでの手順が書かれています。これらの操作以降のコードは省略させていただきます。

今回使うAPI

今回使うAPIは、OpenWeatherMapという無料で世界中の天気予報などの情報を提供してくれる物を使います。

API Keyの取得

API Keyの取得にはアカウントの作成が必要です。
詳しくは、こちらの記事を参考にしてアカウントを作成し、自身のAPIキーを取得してください。
無料天気予報APIのOpenWeatherMapを使ってみる

実際に使ってみる

api.openweathermap.org/data/2.5/weather?q={city name}&appid={your api key}
このURLのappid=というところに、自分のAPI Keyを入力し、q=のところに天気の情報が欲しい都市の名前を入力します。ちなみにこれらの部分を、クエリと呼びます。

そしてクエリの入力をしたURLをブラウザなどに入れてみると、以下のように現在の天気の情報が表示されると思います。
ちなみにこれはGoogle ChromeのJSON View Awesomeという拡張機能を使って、みやすくしているのでChromeを使っている人にはおすすめです。スクリーンショット 2020-07-11 18.23.58.png

コードを書いていく

大きな手順としては4つです

1. URLをつくる
2. URLSessionをつくる
3. URLSessionにタスクを与える
4. タスクを実行する

最終的には私たちが先ほどのURLを入力をし、データを取得したようなことをSwiftにやってもらい、そしてそのデータを出力したりして扱えるようになることが目標です。

まず最終的なコードは以下のようになります。記事を読む際に参考にしてみてください。

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        let weatherURL = "https://api.openweathermap.org/data/2.5/weather?appid=e46af54662e696acdb562c881cef4aa6&q=London"

        if let url = URL(string: weatherURL) {
            let session = URLSession(configuration: .default)
            let task = session.dataTask(with: url) { (data, response, error) in
                if error != nil {
                    print(error!)
                 }

                 if let safeData = data {
                     self.parseJSON(weatherData: safeData)
                 }
            }
            task.resume() 
        }
    }

    func parseJSON(weatherData: Data) {
        let decoder = JSONDecoder()
        do {
            let decodedData = try decoder.decode(WeatherData.self, from: weatherData)
            print(decodedData.name)
        } catch {
            print(error)
        }
    }
}

1.URLをつくる

let weatherURL = "https://api.openweathermap.org/data/2.5/weather?appid=e46af54662e696acdb562c881cef4aa6&q=London"

まずここで注意したいのが、URLの最初httpsとすることです。こうすることで通信が暗号化されます。Swiftでは暗号化された通信しかできないようにデフォルトでは設定されているので、このようにしないと通信してくれなくてエラーが起こります。

これでURLが作れたとおもうかもしれませんが、weatherURLの型をみてみると、String型であることがわかると思います。
スクリーンショット 2020-07-11 19.03.11.png
まずこれをURL型というデータ型に変更します。操作は以下のようです。

import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        let weatherURL = "https://api.openweathermap.org/data/2.5/weather?appid=e46af54662e696acdb562c881cef4aa6&q=London"
        let url = URL(string: weatherURL)
    }
}

URLメソッドを使って、String型からURL型のurlという変数ができたと思います。
実際に確認してみると
スクリーンショット 2020-07-11 19.27.18.png

こうなっていれば大丈夫です。
しかし、今変数urlはオプショナル型であることがわかります。これをアンラップするために以下のように書き換えます。

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        let weatherURL = "https://api.openweathermap.org/data/2.5/weather?appid=e46af54662e696acdb562c881cef4aa6&q=London"
        if let url = URL(string: weatherURL) { //ここを書き換える!
            //nilでない時の処理を書く
        } //else {
            //nilだった時の処理を書く
        }
    }
}

これはオプショナルバインディングというやり方で、ifによってurlの値がnilかどうかで処理を分けてくれます。今回urlはnilではないので、nilでない時の処理を書くところにこれからのコードを書いていきます。
スクリーンショット 2020-07-12 9.05.26.png
しっかりアンラップできていることを確認してみてください。
詳しいアンラップについての情報は、こちらの記事を参考にしてください。
どこよりも分かりやすいSwiftの"?"と"!"

2.URLSessionを作る

URLSessionとは簡単にいうと、safariやChromeなどのブラウザのような物だと思ってください。私たちがURLを入力してデータを取得するといった操作をURLSession上で行います。
まずはその操作を行う場所を作ります。

if let url = URL(string: weatherURL) {
    let session = URLSession(configuration: .default)
}

configurationで細かな設定ができますが、defaultで大丈夫です。
ifの中括弧の中に記述していることに注意してください。

3.URLSessionにタスクを与える

具体的に言うと、先ほど作ったURLSessionに手順1で作ったURLを渡して天気のデータを取ってきてもらいます。

この部分が一番重たいので、手順後ことに区切って説明します。

スクリーンショット 2020-07-12 9.46.03.png
まずこの状態でEnterを押して、以下のように入力してください。

if let url = URL(string: weatherURL) {
    let session = URLSession(configuration: .default)
//------------------------------------------------ここから下の部分を入力してください
    let task = session.dataTask(with: url) { (data, response, error) in
        if error != nil {
            print(error!)
         }

         if let safeData = data {
             self.parseJSON(weatherData: safeData)
         }
    }
}

func parseJSON(weatherData: data) {
}

ここは少し複雑なので、詳しくみていきます。

let task = session.dataTask(with: url)

3行目の前半のこの部分は、先ほど作ったURLSessionに手順1のurl(タスク)を渡して、データを取ってきてもらうための操作です。

問題はその後の操作だと思います。見慣れないコードで混乱するかもしれませんが、ここで行うのはデータを取ってきた後に、そのままではデータを扱うことができないので、受け取ったデータをSwiftで扱えるように変換する操作をします。

これらの操作はクロージャーという関数に似た物を使って書いていますが、ちょっと難しいので詳しい説明は省かせてもらいます。簡単にいうといつもは関数を使って書くところを、クロージャーで簡潔にコードを少なく書いている感じです。

またこの操作(completionHandler)の中では、URLSessionがデータを取ってきた後に行う操作なので、変数data, response, errorにはそれぞれ取得した値が入っています。

例えばdataには天気の情報、errorにはどんなエラーが発生したかなどが入っています。
これから、これらの情報を利用していきます。

if error != nil {
    print(error!)
}

if let safeData = data {
    self.parseJSON(weatherData: safeData)
}

もしデータを受け取った際にerrorに値が入っていたら、つまり何らかのエラーが発生していたらアンラップしてそのエラーを出力する。エラーが発生していなければ、errorはnilとなってなんの操作も行われません。

そして、きちんとdataに天気の情報が入っていたら、関数parseJSON()を使って、データを解析をするという手順です。

またクロージャーないでは関数を呼び出す時、直前にselfをつけることが必要になります。

これからデータの解析の手順を説明します。

構造体の作成

まずデータを入れる構造体を作成します。データを受け取るためには入れ物が必要です。
入れ物ができたら、そこに受け取ったデータを入れて解析をすると言った感じです。
スクリーンショット 2020-07-11 18.23.58.png
先ほどの受け取ったデータは、たくさんのプロパティが存在しますが、まず最初は下から二番目のnameという名前のプロパティの入れ物だけを作っていきましょう。nameの入れ物しか作らないので、今回はその他のデータは使うことができません。

別のファイルにWeatherDataという構造体を作り、その中にnameプロパティを定義します。

import Foundation

struct WeatherData: Decodable{
    let name: String
}

ここで注意したいのは、Decodableプロトコルを採用している点です。
これからJSON型で帰ってきたデータをSwiftで扱えるように変換する操作(デコード)を行います。ということで、この入れ物に入ったデータがデコードできますよと定義をする必要があるのです。

parseJSON()の定義

データの解析(変換)を行うparseJSON()の定義をしていきます。
最初に書いていたファイルの続きに書きます。

    func parseJSON(weatherData: Data) {
        let decoder = JSONDecoder()
        decoder.decode(WeatherData.self, from: weatherData)
    }
}

まず、decoder(解読者)を作ります。それは、JSONDecoderオブジェクトから作ることができ、decodeメソッドで解読をします。decodeメソッドの引数には、データの型と解読するデータを入力します。1つ目はデータの型を入力したいので、末尾にselfをつけます。2つ目は一行目にあるparseJSON()メソッドの変数weatherDataを入力します。

しかし、いま以下のようなエラーが出ていると思います。
スクリーンショット 2020-07-13 17.59.56.png

そこでdecodeメソッドの中身をみてみると、以下のようにthrowsという記述があることがわかります。これは何かうまくいかないことがあったら、エラーを返しますよというメソッドなのです。

そのため、今までのメソッドと同じように呼び出すのではなく、変わった呼び出し方をします。
スクリーンショット 2020-07-13 18.01.32.png
実際にはこのように呼び出します。

    func parseJSON(weatherData: Data) {
        let decoder = JSONDecoder()
        do {
            try decoder.decode(WeatherData.self, from: weatherData) //do, try, catchを使う!!
        } catch {
            print(error)
        }
    }
}

だいぶ複雑見えますが、do, try, catchを使っていて、エラーを返すときはcatchの中身を実行するということがわかれば大丈夫です。

詳しくはこちらの記事を参考にしてください。
猿がついに理解できたSwiftのthrow・do・try・catchの意味

次は解析した後のデータを使って、出力するだけです。

func parseJSON(weatherData: Data) {
        let decoder = JSONDecoder()
        do {
            let decodedData = try decoder.decode(WeatherData.self, from: weatherData)
            print(decodedData.name)
        } catch {
            print(error)
        }
    }

4.タスクを実行する

最後は一文加えるだけです。

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        let weatherURL = "https://api.openweathermap.org/data/2.5/weather?appid=e46af54662e696acdb562c881cef4aa6&q=London"

        if let url = URL(string: weatherURL) {
            let session = URLSession(configuration: .default)
            let task = session.dataTask(with: url) { (data, response, error) in
                if error != nil {
                    print(error!)
                 }

                 if let safeData = data {
                     self.parseJSON(weatherData: safeData)
                 }
            }
            task.resume() //これを加えます!!
        }
    }

    func parseJSON(weatherData: Data) {
        let decoder = JSONDecoder()
        do {
            let decodedData = try decoder.decode(WeatherData.self, from: weatherData)
            print(decodedData.name)
        } catch {
            print(error)
        }
    }
}

resume()メソッドの意味としては、アップルのドキュメントを読むと、作成されたtaskは中断状態で開始するので、このメソッドでタスクを開始する必要があるかららしいです。細かいことはよくわかりませんがこれで、受け取った情報を出力することができました。

またname以外のプロパティを受け取りたいときは、構造体のプロパティを増やすことで可能となります。しかし中には配列の値などもあるので、そのような物に対するコードの書き方は以下の記事を参考にしてください。
[Swift] JSON文字列から任意のオブジェクトへ変換する(JSONDecoderとCodableの利用)

まとめ

以上で終わりとなります。
初心者がまとめようとすると、わからないところが多すぎてとても長い記事になってしまいましたが、間違いなどありましたらご指摘ください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む