この記事は?
最近iOSアプリ開発を始めた者です。現在は練習用として電話帳アプリを作成しています。このアプリには以下のような画面で電話帳に友人の情報を登録できます。「郵便番号を入力」と書いているフォームには、郵便番号を入力して検索ボタンを押下すると、postmanAPIが一致する住所を取得し、その結果を「住所を入力」と書かれたTextFileldに表示します。
このpostmanAPIと接続する部分は外部APIと接続するという事になるので、非同期処理が必要です。この記事では、非同期処理の方法について今回私が学んだ事をまとめます。内容としてはかなり基本的な内容になります。
Swiftでの非同期処理における登場人物
- DispatchQueue(ディスパッチキュー)
参考:【Swift 3】非同期処理・DispatchQueueについてのメモ
DispatchQueueは、一つ以上のタスクを管理するクラスで、独立したスレッドにて、登録されたタスクを実行していくものです。
タスクは、大きく分けて同期処理(.sync())または非同期処理(.async(), .asyncAfter())のどちらかで登録します。
- closure(クロージャ)
クロージャとはスコープ内の変数や定数を保持したひとまとまりの処理のことである
よく分かりません。。。
結局、実際の処理を見ないとこの辺は理解するのが難しい。。。
実際の処理を見てみよう
swiftUIで画面のレイアウトを作っています。また、アーキテクチャはMVVMです。今回の場合だと、以下のような構成になっています
View...swiftUIでレイアウトを定義したファイル
Model...構造体が書かれたファイル
ViewModel...非同期処理でAPIと接続を行うファイル
今回はViewとViewModelのコードを見る事で、swiftでの非同期処理の動きを簡単に追っていきます。
こちらがViewのコードです。(説明に関係のある部分のコードだけ表示しています)
@ObservedObject var profileRegisterVM: ProfileRegisterViewModel = ProfileRegisterViewModel()
@State var address = ""
@State var postNum = ""
...
// 郵便番号入力フォーム
TextField("郵便番号を入力", text: $postNum)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding(.leading)
// 検索ボタン
Button(action:{
// ボタンをタップした時の処理
profileRegisterVM.request(postNum: postNum, completion: {
address = profileRegisterVM.changedString
})
})
{
Text("検索")
.foregroundColor(Color(.systemGreen))
.frame(height: 20)
.padding(10)
...
}
...
// 住所入力フォーム
TextField("住所を入力", text: $address, onCommit: {
self.validateLength()
})
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding(.horizontal)
...
こちらがViewModelのコードです
import Foundation
import SwiftUI
class ProfileRegisterViewModel: ObservableObject {
@Published var changedString = ""
var responseValue: ViewHelper?
func request(postNum: String, completion: @escaping () -> Void) {
// APIに接続
let zipcode = postNum
guard let url = ResponseHelper.createUrl(zipcode: zipcode) else { return }
let request = URLRequest(url: url)
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
guard let data = data else { return }
do {
// デコードする
let requestResults: RequestResults = try JSONDecoder().decode(RequestResults.self, from: data)
// checkErrorTypeを初期化
self.responseValue = ResponseType.init(rawValue: requestResults.status)?
.checkErrorType(requestResults: requestResults)
// UIに変更を加える
DispatchQueue.main.async {
self.changedString = (self.responseValue?.string)!
completion()
}
} catch let error {
print(error)
}
}
task.resume()
}
}
コードの解説
順を追って見ていきます。
まず、ViewModelが更新されたら、再びViewが更新(再描画)される必要があるので、ViewModelがViewに監視されている状態にする必要があります。
Viewで監視したいViewModelを、@ObservedObject
を使ってViewで宣言します。
@ObservedObject var profileRegisterVM: ProfileRegisterViewModel = ProfileRegisterViewModel()
また、ViewModelではObservableObject
を継承させます。
class ProfileRegisterViewModel: ObservableObject {
...
今回は、郵便番号入力フォームに郵便番号を入力する→検索ボタン押下→postmanAPIを通して郵便番号と一致する住所を取得→取得した結果を住所入力フォームに表示
を実装したいです。これを分解すると以下のようになります。
郵便番号入力フォームに郵便番号を入力する→検索ボタン押下
...普通にswiftUiを使ってViewに実装
postmanAPIを通して郵便番号と一致する住所を取得
...ViewModelとModelに実装
取得した結果を住所入力フォームに表示
...クロージャに実装
現在の私の解釈ですが、外部と接続した後にしたい処理はクロージャに書いておくのが良いのだと思います。「外部との接続が終わったらその結果を用いてこれをするんだっ!」という処理をViewでクロージャの中に書いておいて、「よし、外部との接続が終わったぞ!」というタイミングの時にViewModelでそのクロージャを呼び出す。。という流れかなと思ってます。
検索ボタンを押下した後で、結果を住所入力のテキストフィールドに表示したいので、クロージャは検索ボタンのactionの中に書きます。Buttonのactionに実装する事で、ボタン押下時に実行されます。
// 検索ボタン
Button(action:{
// ボタンをタップした時の処理
profileRegisterVM.request(postNum: postNum, completion: {
address = profileRegisterVM.changedString
})
})
closureはcompletionの部分です。
completion: {
address = profileRegisterVM.changedString
}
このclosureはViewModelのrequest()
というメソッドの引数という形で呼び出されているので、このような書き方になります。
// ボタンをタップした時の処理
profileRegisterVM.request(postNum: postNum, completion: {
address = profileRegisterVM.changedString
})
実際にViewModelのrequest()
の引数としてどのように呼び出されているか見てみます。
func request(postNum: String, completion: @escaping () -> Void) {
引数には、郵便番号入力フォームに入力された郵便番号とクロージャが入っています。
TextFieldはこのように宣言すると、
TextField("郵便番号を入力", text: $postNum)
postNum
で、入力された値を受け取れます。異なるファイルだとしてもこの形で受け取れます。swiftUI便利すぎますねぇ。。
ViewModelでの処理はざっくり以下のような感じです
func request(postNum: String, completion: @escaping () -> Void) {
// 外部APIに接続
:
:
// postmanAPIの結果を受け取る
self.responseValue = ResponseType.init(rawValue: requestResults.status)?
.checkErrorType(requestResults: requestResults)
// UIに変更を加える
DispatchQueue.main.async {
self.changedString = (self.responseValue?.string)!
completion()
}
}
簡単に説明すると、DispatchQueue.main.async{}
の中でUIに関わる処理をして、その外ではAPI接続に関する処理を書きます。completion()
はクロージャの事です。クロージャでは表示する文字列を変更する処理を書いてますよね。つまりUIを変更する処理なので、DispatchQueue.main.async{}
の中で呼び出します。request()
の引数に入れてるので、ViewModelのファイルからでも呼び出せるのです。
ちなみに、クロージャではaddress
という変数の中身を書き換えてますよね。address
は住所入力を行うテキストフィールドのtext
に指定された変数です。
TextField("住所を入力", text: $address, onCommit: {
self.validateLength()
})
先ほど、テキストフィールドに入力された値は(この場合は)address
で受け取れると説明しましたが、同時にこのaddress
に適当な文字列を代入すると、代入した文字列がフォームに表示されるようになります。
DispatchQueue.main.async{}
の中には、クロージャを呼び出す以外にも行っている処理があります。この部分です。
self.changedString = (self.responseValue?.string)!
このchangedStringがViewModelの冒頭でこのように宣言されてます。
@Published var changedString = ""
@Published
をつけて宣言することによって、Viewとバインディングできるようになります。詳しく順を追って見てみるとこんな感じです〜。
// ===== ViewModel =====
// Viewとバインディングできるように@Publishedで宣言
@Published var changedString = ""
// UIを更新する処理を書く
DispatchQueue.main.async {
// apiと接続した結果(取得してきた住所)をchangedStringに格納
self.changedString = (self.responseValue?.string)!
// クロージャを実行
completion()
}
// ======= View =======
// クロージャ
completion: {
// ViewModelで更新したchangedStringをaddressに代入することで
// 住所を入力するTextFieldに取得した住所の結果を表示する
address = profileRegisterVM.changedString
}
これによって、郵便番号入力フォームに郵便番号を入力し、検索ボタンを押下すると、住所入力フォームに住所が表示されます!!こんな感じ!!
最後に
まだまだ初心者なもんで、いろいろ解釈とか使うべき用語の選択とか間違っていると思います。間違ってるところはご指摘頂きたいです!!!