
Outline
iOS 開発で必須とも言える API クライアントの設計手法を、初心者にもわかりやすく紹介します。
はじめに
あなたは、どのように API クライアントを設計していますか。
まずはライブラリを選ぶでしょうか。
それとも、クラス図を書くのでしょうか。
なるほど、なるほど、ふーむ。
この記事では、もっと別のより良い設計方法を紹介します。
紹介する設計方法は、ほとんど設計知識のない状況から始めることができます。しかも、最終的にはあなたのプロジェクトにぴったりの設計を手に入れられる方法です。
対象読者
さて、この記事では、対象読者を次のように設定しています:
- どのような API 設計にしたらいいかわからない人
- どのような API のライブラリを使うべきかわからない人
また、最終的には以下のレベルの目標を達成できることでしょう:
- あなたのプロジェクトの API 層設計者になれるレベル
目次
- はじめに
- 目次
- プロジェクトを準備する
- インターフェースを想像する
- API についてわかっていることを整理する
- リクエストとレスポンス
- リクエストについてわかっていること
- リクエストとレスポンスの対応関係
- レスポンスについてわかっていること
- リクエストからレスポンスへの変換過程
- わかっているところまでコードにする
- なぜコードにするのか
-
XCTestCase
クラスをつくる - リクエストの入力部分をコードにする
- レスポンスの出力部分をコードにする
- レスポンスをわかりやすいオブジェクトへと変換する
- 非同期な部分をコードにする
- 標準ライブラリから出発する
-
URLSession
クラスを使う - リクエストを
URLRequest
へ変換する -
URLResponse
などからレスポンスを作成する - 通信部分を実装する
-
- 使いやすさを再点検する
- API クライアントを使ってみる
- API 呼び出し部分を簡略化する
- 対応 API を増やしてみる
- サードパーティ製ライブラリを使う
- 現時点の標準ライブラリでは対応していないもの
- サードパーティ製ライブラリを使うメリットとリスク
- Easy と Simple のどちらを選ぶべきか
- 終わりに
プロジェクトを準備する
この記事では、実際に手を動かしながら解説をします。
STEP1: Single View App を作成する
まずは、Xcode9 を開き「Single View App」を作成しましょう。
プロジェクト名は「StartSmallForAPI
」、チーム・組織名・組織IDは適当なもので構いません。
言語は「Swift」を選び、「Include Unit test」にチェックをつけておいてください。
STEP2: ⌘ + U
で正常に作成できたことを確認する
新しいファイルをつくる前に、Destination に iOS Simulator のいずれかを選び(iPhone X とかで OK)、必ず ⌘+U を実行しましょう。
成功した場合は、次のようなモーダルが表示されます。
もし、これが失敗するようならプロジェクトをうまく作成できていません。最初からやり直してみてください。
STEP3: 最初のファイルを作成する
うまくプロジェクトを作成できたら、Project navigator から「StartSmallForAPI
」グループに「WebAPI.swift
」を作成してください。ビルドターゲットには「StartSmallForAPI
」と「’StartSmallForAPITests’」を選んでおいてください。
準備完了!
お疲れ様です。
以上で準備が整いました。
インターフェースを想像する
では、設計の主要部分であるインターフェースの設計に移ります。
API についてわかっていることを整理する
インターフェースを設計する上では、わかっていることの整理がとても重要です。そのため、まずは一般的な API についてわかっていることを整理しましょう。
リクエストとレスポンス
API にはリクエストとレスポンスがあります。一般的には、リクエストをサーバーへ送信し、サーバーの応答をレスポンスとして受け取ります。
+---------+ +----------+
Client --> | Request | --> Server --> | Response | --> Client
+---------+ +----------+
では、このリクエストについてわかっていることを整理しましょう。
リクエストについてわかっていること
さて、リクエストは一般的に次の要素から構成されます:
- URL
-
- 説明
- リソースの所在地。
- 例
-
http://example.com/foo/bar
のような URL。
- URL クエリ文字列
-
- 説明
- URL に付与される、
?
始まりで&
で連結された文字列。 - 例
- GitHub API でページ番号や1ページに含まれる要素数を指定するクエリ文字列は
?page=1&per_page=100
。
- HTTP メソッド
-
- 説明
- リクエストの種類。
- 例
- 何かを取得したければ
GET
など、何かをサーバーへ送信したければPOST
やPUT
など。
- HTTP ヘッダー
-
- 説明
- リクエストに付与できる追加情報。ユーザーの認証や認可などによく使われる。
- 例
- 認可情報のトークンを示す HTTP ヘッダーは
Authorization: token XXXXXXXXX
。他にも送信主のアプリケーションを示す HTTP ヘッダーはUser-Agent: XXXXXX
。
- ペイロード
-
- 説明
- リクエストの本文。
POST
やPUT
の送信内容はペイロードに置く決まりになっている。なお、HTTP メソッドがGET
のときは、ペイロードは取れないという制約がある。 - 例
- 様々な種類があるが、
key=value
や{"key":"value"}
のような文字列や、画像などのデータを配置できる。
「何でこんなことを知る必要があるの?」って思われるかもしれません。しかし、構成要素を知ることはとても重要です。構成要素を知ることが、共通化の鍵となってくるからです。
上で挙げたように、リクエストの構成要素はたくさんあります。これらすべてを細かに指定できるようにすれば自由度は上がり、GitHubのAPIでもあなたの作ったAPIでも同じコードを使いまわせるようになるでしょう。反対に、いくつかの構成要素を隠して指定できないようにすると、コードを使いまわせる場面が減っていきます。
また、別の例も考えてみましょう。今まで HTTP ヘッダーを指定していない状況から、サーバーのフレームワークが変わって HTTP ヘッダーを指定しないといけない状況に変わったとします。この状況でも依然としてクライアントが対応できるようにするためには、構成要素をなるべく広く受け取れるようにしておいた方がいいのです。
つまり、使いまわせる範囲を広くしつつ、サーバーの変更にも耐えらえるようにするためには、なるべくこれら現時点で判明しているすべての構成要素をリクエストとして指定できるようにするべきなのです。
さて、リクエストを送信した後、サーバーから返ってくるのがレスポンスです。このレスポンスについてもわかっていることを整理してみましょう。
レスポンスについてわかっていること
一般的に、レスポンスは次の要素から構成されます:
- HTTP ステータスコード
-
- 説明
- レスポンスの意味。
- 例
- もし成功であれば 200 番台の整数で、よく見かける 404 は指定した項目が見つからないという意味を持つ。
- HTTP ヘッダー
-
- 説明
- レスポンスに付与できる追加情報。
- 例
-
Content-Type
ヘッダーは、後述するペイロードの形式を表す。また、Link
ヘッダーは次のページや最後のページの URL を表す。
- ペイロード
-
- 説明
- レスポンスの内容。
- 例
-
Content-Type
の形式で表現されたデータ。このデータは画像や動画なこともあるので、文字列がくるとは限らない。
このレスポンスとリクエストの関係についても整理してみましょう。
リクエストからレスポンスへの変換過程
一般的に、何かリクエストを選べば、対応するレスポンスはざっくりと決まります。つまり、リクエストとレスポンスの間には対応関係があるということです。
ただ、一点例外があり、リクエストを送信したとしてもレスポンスが返ってこない場合があります。たとえば、何らかの理由で通信が遮断されたり、サーバーが故障している場合にはレスポンスは返ってきません。以降では、これらのことを通信エラーと呼ぶことにします。
まとめると、リクエストとレスポンスの間には、リクエストがレスポンスまたは通信エラーになるという対応関係があるということです。
なお、一点付け加えるなら、この関係は非同期の対応関係になっています。なぜ非同期かというと、この変換の途中で UI の描画処理などを止めないためです。もし、UI の描画処理が止まってしまうと、API の呼び出しがあるたび、ユーザーは何も操作ができなくなってしまいます。これはなるべく避けたいですから、リクエストからレスポンスを受け取るまでは非同期であるべきなのです。
これまでで、リクエストとレスポンスについてわかっていることを整理できました。次から、実際に今わかっているところまでをコードにしてみましょう。
わかっているところまでをコードにする
さて、APIのお話をするためにここまで進めてきたはずですが、肝心のAPIについての詳細はまだ出てきていません。もしかすると、「わたしのAPIの詳細を知らずに、私にぴったりなAPI設計ができるのか?」と疑問に感じるかもしれません。
答えは、「Yes」です。
しかし不安でしょうから、これからの流れを軽く説明しておきましょう。はじめに、これまでわかっている一般的な部分をコード化します。次に、この一般的なコードをそれぞれの API に合った形へと修正していきます。そして最後に、ライブラリによるコードの省略について説明をします。
では、まず一般的な部分のコード化について説明してきます。
まずコードにしてみよう
しかし、なぜ、クラス図などの設計文書も書かずにコードを書き始めるのでしょうか。実はこれには2つの目的があります:
- より具体的にしたいから
- 実際に動作を検証できた方が、自分の理解を確認できるから
これらの目的を満たすには、コードを書くことが一番です。そのため、設計文書については傍に置いておいて、わかっているところまでコードにしてみましょう。
XCTestCase
クラスをつくる
さて、先ほどコード化する目的の1つとして「実際に動作を検証できること」をあげました。この動作の検証とはどのようにすればいいのでしょうか。
ささっと Playground などを使ったりもできますが、きちんと設計したいときには Playground は不向きです。こういうとき、できるエンジニアは XCTestCase
で動作を確認します。この XCTestCase
は動作を確認するためのクラスで、Swift に標準で組み込まれています。
XCTestCase
の使い方は簡単です。次のようなボイラープレートを用意し、ビルドターゲットを StartSmallForAPITests
にしてから ⌘ + U で実行するだけです:
import XCTest
@testable import StartSmallForAPI
class StartSmallForAPITests: XCTestCase {
func testExample() {
// ここに動作を確認したいコードを書く。
}
}
実は、StartSmallForAPITests
というグループの中には、既に StartSmallForAPITests.swift
という XCTestCase が入っているはずです。そこで、これを改造していくこととしましょう。
コードを書くときの約束
なお、今回コードにするとき、下の3つの約束を守っています:
- 約束1
-
- 内容
-
WebAPI.swift
では force unwrap してはダメ。 - 理由
- 本番でクラッシュするのを防ぐため。
- 約束2
-
- 内容
-
StartSmallForAPITests.swift
は動作確認用なので force unwrap してもいい。 - 理由
- バグに気づきやすいため。むしろ、クラッシュしてくれればすぐにおかしいことがわかって便利。
- 約束3
-
- 内容
- エラーの情報量は落とさない。
- 理由
- バグの原因を素早く特定できるようにすることで、デバッグ時間を短縮したいため。なお、記事末にエラーの情報量を落とさない実装方法を解説しています。
いずれのルールもプロジェクトに関わらない普遍的なものなので、多くのプロジェクトで安全・親切なAPIを設計するために適用できるはずです。
リクエストの入力部分をコードにする
では、リクエストの入力部分をコード化してみましょう。先ほど、リクエストの構成要素は、URLとクエリ文字列、HTTPヘッダー、ペイロードと説明しました。これらをまとめたタプルを Request
とし、WebAPI.swift
に書きます(タプルでなく struct でも問題はありません):
import Foundation
/// API への入力は Request そのもの。
typealias Input = Request
/// Request は以下の要素から構成される:
typealias Request = (
/// リクエストの向き先の URL。
url: URL,
/// クエリ文字列。クエリは URLQueryItem という標準のクラスを使っている。
queries: [URLQueryItem],
/// HTTP ヘッダー。ヘッダー名と値の辞書になっている。
headers: [String: String],
/// HTTP メソッドとペイロードの組み合わせ。
/// GET にはペイロードがなく、PUT や POST にはペイロードがあることを
/// 表現するために、後述する enum を使っている。
methodAndPayload: HTTPMethodAndPayload
)
/// HTTP メソッドとペイロードの組み合わせ。
enum HTTPMethodAndPayload {
/// GET メソッドの定義。
case get
/// POST メソッドの定義(必要になるまでは省略)。
// case post(payload: Data?)
/// メソッドの文字列表現。
var method: String {
switch self {
case .get:
return "GET"
}
}
/// ペイロード。ペイロードがないメソッドの場合は nil。
var body: Data? {
switch self {
case .get:
// GET はペイロードを取れないので nil。
return nil
}
}
}
このコードの動作確認をするために、StartSmallForAPITests.swift
に次のようなコードを書きます。対象の API はなんでもいいのですが、とりあえず誰でも使える GitHub Zen API を使うようにしてみましょう:
import XCTest
@testable import StartSmallForAPI
class StartSmallForAPITests: XCTestCase {
func testRequest() {
// リクエストを作成する。
let input: Request = (
// GitHub の Zen API を指定。
url: URL(string: "https://api.github.com/zen")!,
// Zen API はパラメータを取らない。
queries: [],
// 特にヘッダーもいらない。
headers: [:],
// HTTP メソッドは GET のみ対応している。
methodAndPayload: .get
)
// この内容で API を呼び出す(注: WebAPI.call は後で定義する)。
WebAPI.call(with: input)
}
}
ここまで書き終わったら、⌘ + U でビルドできることを確認します。おっと、まだ WebAPI.call
が定義されていないので、ビルドは失敗するはずです。とりあえず、ビルドを通すために次のような仮の実装をしておきましょう:
// ...(前に書いた Input は省略)...
enum WebAPI {
// ビルドを通すために call 関数を用意しておく。
static func call(with input: Input) {
// TODO: もう少しインターフェースが固まったら実装する。
}
}
なお、このコードで WebAPI
を enum としたのは名前空間として扱いたいためです(記事末に解説があります)。
もう一度、⌘ + U でビルドできることを確認します。もし、これでビルドができなければどこかでコードを間違えてるので、読み返して確認してください。
レスポンスの出力部分をコードにする
次に、レスポンスの出力部分をコードにしてみましょう。レスポンスについても構成要素はわかっているので、それを元にコードを書きます:
// ...(前に書いた Input は省略)...
enum WebAPI {
// ...(省略)...
}
/// API の出力にをあらわす enum。
/// API の出力でありえるのは、
enum Output {
/// レスポンスがある場合か、
case hasResponse(Response)
/// 通信エラーでレスポンスがない場合。
case noResponse(ConnectionError)
}
/// 通信エラー。
enum ConnectionError {
/// データまたはレスポンスが存在しない場合のエラー。
case noDataOrNoResponse(debugInfo: String)
}
/// API のレスポンス。構成要素は、以下の3つ。
typealias Response = (
/// レスポンスの意味をあらわすステータスコード。
statusCode: HTTPStatus,
/// HTTP ヘッダー。
headers: [String: String],
/// レスポンスの本文。
payload: Data
)
/// HTTPステータスコードを読みやすくする型。
enum HTTPStatus {
/// OK の場合。HTTP ステータスコードでは 200 にあたる。
case ok
/// OK ではなかった場合の例。
/// notFound の HTTP ステータスコードは 404 で、
/// リクエストで要求された項目が存在しなかったことを意味する。
case notFound
/// 他にもステータスコードはあるが、全部定義するのは面倒なので、
/// 必要ペースで定義できるようにする。
case unsupported(code: Int)
/// HTTP ステータスコードから HTTPステータス型を作る関数。
static func from(code: Int) -> HTTPStatus {
switch code {
case 200:
// 200 は OK の意味。
return .ok
case 404:
// 404 は notFound の意味。
return .notFound
default:
// それ以外はまだ対応しない。
return .unsupported(code: code)
}
}
}
レスポンスが定義できたので、動作確認のコードを書きます:
import XCTest
@testable import StartSmallForAPI
class StartSmallForAPITests: XCTestCase {
func testRequest() {
// ... (省略) ...
}
func testResopnse() {
// 仮のレスポンスを定義する。
let response: Response = (
// ステータスコードは 200 OK なはず。
statusCode: .ok,
// 読み取るべきヘッダーは特にない。
headers: [:],
// Zen API のレスポンスは、禅なフレーズの文字列。
payload: "this is a response text".data(using: .utf8)!
)
// TODO: このままだとペイロードが Data になってしまっていて使いづらいので、
// よりわかりやすいレスポンスのオブジェクトへと変換する。
}
}
ここまで書き終わったら、⌘ + U でビルドできることを確認します。もしビルドできなかったら、写経をミスってるのでコードを見直してみてください。
さて、このままではレスポンスのペイロードが Data
になっていて使いづらくなっています。そこで、レスポンスに対応するわかりやすいオブジェクトへと変換しましょう。
レスポンスをわかりやすいオブジェクトへと変換する
ここからは GitHub API 固有の処理を書いていくので、WebAPI.swift
とは別のファイルに書いていきましょう。そのために、StartSmallForAPI
グループの下に GitHubAPI.swift
というファイルを作成してください。このファイルのビルドターゲットは StartSmallForAPI
にしてください。
さて、GitHub Zen API を例として、わかりやすいオブジェクトへの変換を実装します。このわかりやすいオブジェクトとは、下のようなものです:
/// GitHub Zen API の結果。
struct GitHubZen {
/// Zen(禅)なフレーズの文字列。
let text: String
}
この定義をみただけで、GitHub Zen API が文字列だけを返す API だとわかります。そのため、レスポンスからこのようなわかりやすいオブジェクトへ変換してあげると、とても見通しがよくなります。つまり、下のような関数を用意してあげると良いということです:
/// GitHub Zen API の結果。
struct GitHubZen {
let text: String
/// レスポンスからわかりやすいオブジェクトへと変換する関数。
static func from(response: Response) -> GitHubZen {
// TODO
}
}
ただし、気をつけないといけないのは、常にわかりやすいオブジェクトへと変換できるというわけではないということです。たとえば、サーバーがエラーのレスポンスを返してきた場合、ペイロードは禅なフレーズではなくエラーを表す JSON 文字列になります。そのため、この from の戻り値の型は、禅なフレーズまたはエラーのどちらかの型をもつはずです。これを今まで通り enum で表現すると次のようになります:
// レスポンスごとに success と failure を定義していく…。
enum GitHubZenResponse {
case success(GitHubZen)
case failure(GitHubZen.TransformError)
}
しかし、もし GitHubZen
以外の API を足していくことを考えると、API を足すごとに ***Response
が増えていくことになってしまいます。これでは面倒なので Either
という汎用の enum を作ります:
// ...(前に書いた Input と WebAPI と Output は省略)...
/// 型 A か型 B のどちらかのオブジェクトを表す型。
/// たとえば、Either<String, Int> は文字列か整数のどちらかを意味する。
/// なお、慣例的にどちらの型かを左右で表現することが多い。
enum Either<Left, Right> {
/// Eigher<A, B> の A の方の型。
case left(Left)
/// Eigher<A, B> の B の方の型。
case right(Right)
/// もし、左側の型ならその値を、右側の型なら nil を返す。
var left: Left? {
switch self {
case let .left(x):
return x
case .right:
return nil
}
}
/// もし、右側の型ならその値を、左側の型なら nil を返す。
var right: Right? {
switch self {
case .left:
return nil
case let .right(x):
return x
}
}
}
この Either
を使うと、GitHubZenResponse
と同じ意味を次のように表現できます:
GitHubZenResponse.success(zen) -> Either.left(zen)
GitHubZenResponse.failure(error) -> Either.right(error)
では、Either
を使って GitHubZen
の from
関数を次のように書いてみましょう:
enum Either<Left, Right> {
// ...(省略)...
}
/// GitHub Zen API の結果。
struct GitHubZen {
let text: String
/// レスポンスからわかりやすいオブジェクトへと変換する関数。
///
/// ただし、サーバーがエラーを返してきた場合などは変換できないので、
/// その場合はエラーを返す。つまり、戻り値はエラーがわかりやすいオブジェクトになる。
/// このような、「どちらか」を意味する Either という型で表現する。
/// GitHubZen が左でなく右なのは、正しいと Right をかけた慣例。
static func from(response: Response) -> Either<TransformError, GitHubZen> {
// TODO
}
/// GitHub Zen API の変換で起きうるエラーの一覧。
enum TransformError {
/// HTTP ステータスコードが OK 以外だった場合のエラー。
case unexpectedStatusCode(debugInfo: String)
/// ペイロードが壊れた文字列だった場合のエラー。
case malformedData(debugInfo: String)
}
}
この関数の実装へ移る前に、使い勝手をみてみましょう。この GitHubZen.from
の使い勝手を検証するために、これまでと同じような動作確認のコードをかいてみます。この使い勝手を確かめるコードは次のようになるはずです:
import XCTest
@testable import StartSmallForAPI
class StartSmallForAPITests: XCTestCase {
func testRequest() {
// ... (省略) ...
}
func testResopnse() {
// 仮のレスポンスを定義する。
let response: Response = (
statusCode: .ok,
headers: [:],
payload: "this is a response text".data(using: .utf8)!
)
// GitHubZen.from 関数を呼び出してみる。
let errorOrZen = GitHubZen.from(response: response)
// 結果は、エラーか禅なフレーズのどちらか。
switch errorOrZen {
case let .left(error):
// 上の仮のレスポンスであれば、エラーにはならないはず。
// そういう場合は、XCTFail という関数でこちらにきてしまったことをわかるようにする。
XCTFail("\(error)")
case let .right(zen):
// 上の仮のレスポンスの禅なフレーズをちゃんと読み取れたかどうか検証したい。
// そういう場合は、XCTAssertEqual という関数で内容があっているかどうかを検証する。
XCTAssertEqual(zen.text, "this is a response text")
}
}
}
このコードをみた通り、コード量はそこまで多くなく、意味も明快です。つまり、GitHubZen.from
関数の使い勝手はよいといえるでしょう。
このように、こまめに使い勝手を確認していくことは、使いやすい設計をしていく上でとても重要です。
さて、使い勝手がよいとわかったので、中身の実装にとりかかりましょう:
// ...(前に書いた と Either は省略)...
struct GitHubZen {
let text: String
static func from(response: Response) -> Either<TransformError, GitHubZen> {
switch response.statusCode {
case .ok:
// HTTP ステータスが OK だったら、ペイロードの中身を確認する。
// Zen API は UTF-8 で符号化された文字列を返すはずので Data を UTF-8 として
// 解釈してみる。
guard let string = String(data: response.payload, encoding: .utf8) else {
// もし、Data が UTF-8 の文字列でなければ、誤って画像などを受信してしまったのかもしれない。。
// この場合は、malformedData エラーを返す(エラーの型は左なので .left を使う)。
return .left(.malformedData(debugInfo: "not UTF-8 string"))
}
// もし、内容を UTF-8 で符号化された文字列として読み取れたなら、
// その文字列から GitHubZen を作って返す(エラーではない型は右なので .right を使う)
return .right(GitHubZen(text: string))
default:
// もし、HTTP ステータスコードが OK 以外であれば、エラーとして扱う。
// たとえば、GitHub API を呼び出しすぎたときは 200 OK ではなく 403 Forbidden が
// 返るのでこちらにくる。
return .left(.unexpectedStatusCode(
// エラーの内容がわかりやすいようにステータスコードを入れて返す。
debugInfo: "\(response.statusCode)")
)
}
}
/// GitHub Zen API で起きうるエラーの一覧。
enum TransformError {
/// ペイロードが壊れた文字列だった場合のエラー。
case malformedData(debugInfo: String)
/// HTTP ステータスコードが OK 以外だった場合のエラー。
case unexpectedStatusCode(debugInfo: String)
}
}
中身はかなり単純なコードで、UTF-8 で符号化された文字列が渡されたらそれを取り出しているだけです。また、もし UTF-8 で符号化されていない文字がきた場合や、HTTP ステータスコードが 200 OK
でなければエラーを返します。
ここまで書き終わったら、⌘ + U でビルドできることを確認します。もしビルドできなかったら、写経をミスってるのでコードを見直してみてください。
これまでで、リクエストの入力部分と、レスポンスの出力部分を実装できました。ここからは、リクエストからレスポンスへ変換する非同期な部分をコードにしてみましょう。
非同期な部分をコードにする
非同期なコードの動作確認は少々複雑です。この場合、XCTestExpectation
という動作確認完了までの待ち合わせをするオブジェクトを作成しなければなりません。この XCTestExpectation
を使ったコードは次のようになります:
import XCTest
class ExampleAsyncTests: XCTestCase {
func testAsync() {
// XCTestExpectation オブジェクトを作成する。
// これを作成した時点で、動作確認のモードが非同期モードになる。
let expectation = self.expectation(description: "非同期に待つ")
// 1秒経過したら、expectation.fulfill を実行する。
Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { _ in
expectation.fulfill()
}
// 動作確認が完了するまで、10 秒待つ。
// 10 秒たっても expectation.fulfill が呼ばれなければ、
// 何かがおかしいので、わかりやすいエラーがでるようにしておく。
self.waitForExpectations(timeout: 10)
// ここは expectation.fulfill が呼ばれるかタイムアウトするまで
// 実行されない。
}
}
この XCTestExpectation
が作成されると、XCTestCase
は非同期モードになります。非同期モードになった XCTestCase
は XCTestExpectation.fulfill
が呼ばれるまで待機するようになります。この待機を実際にする関数が、XCTestCase.waitForExpectations
です。この XCTestCase.waitForExpectations
以降のコードは、XCTestExpectation.fulfill
が呼ばれるかタイムアウトするまで実行されません。
さて、XCTestExpectation
を使った動作確認のコードは次のようになります:
import XCTest
@testable import StartSmallForAPI
class StartSmallForAPITests: XCTestCase {
func testRequest() {
// ... (省略) ...
}
func testResopnse() {
// ... (省略) ...
}
func testRequestAndResopnse() {
let expectation = self.expectation(description: "API を待つ")
// これまでと同じようにリクエストを作成する。
let input: Input = (
url: URL(string: "https://api.github.com/zen")!,
queries: [],
headers: [:],
methodAndPayload: .get
)
// このリクエストで API を呼び出す。
// WebAPI.call の結果は、非同期なのでコールバックになるはず。
// また、コールバックの引数は Output 型(レスポンスありか通信エラー)になるはず。
// (注: WebAPI.call がコールバックを受け取れるようにするようにあとで修正する)
WebAPI.call(with: input) { output in
// サーバーからのレスポンスが帰ってきた。
// Zen API のレスポンスの内容を確認する。
switch output {
case let .noResponse(connectionError):
// もし、通信エラーが起きていたらわかるようにしておく。
XCTFail("\(connectionError)")
case let .hasResponse(response):
// レスポンスがちゃんときていた場合は、わかりやすいオブジェクトへと
// 変換してみる。
let errorOrZen = GitHubZen.from(response: response)
// 正しく呼び出せていれば GitHubZen が帰ってくるはずなので、
// 右側が nil ではなく値が入っていることを確認する。
XCTAssertNotNil(errorOrZen.right)
}
expectation.fulfill()
}
self.waitForExpectations(timeout: 10)
}
}
ただ、まだリクエストを実際に送信する部分は実装していません。そのため、WebAPI.call
のインターフェースは「非同期ならコールバックになるだろう」という予想に基づいて実装しています。なお、このままではビルドが通らないので、WebAPI.call
がコールバックを受け取れるようにします。
// ...(前に書いた Input は省略)...
enum WebAPI {
// コールバックつきの call 関数を用意する。
// コールバック関数に与えられる引数は、Output 型(レスポンスか通信エラーのどちらか)。
static func call(with input: Input, _ block: @escaping (Output) -> Void) {
// 実際にサーバーと通信するコードはまだはっきりしていないので、
// Timer を使って非同期なコード実行だけを再現する。
Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { _ in
// 仮のレスポンスをでっちあげる。
let response: Response = (
statusCode: .ok,
headers: [:],
payload: "this is a response text".data(using: .utf8)! // 👈 最終的にこのコードは消えるので force unwrap しています
)
// 仮のレスポンスでコールバックを呼び出す。
block(.hasResponse(response))
}
}
static func call(with input: Input) {
self.call(with: input) { _ in
// NOTE: コールバックでは何もしない
}
}
}
// ...(前に書いた Output と Either は省略)...
ただ、まだ実際にサーバーと通信するコードははっきりしていません。そのため、代わりに Timer.scheduledTimer
関数と仮のレスポンスでサーバーからのレスポンスがきた状態を再現しています。この状態で、ビルドが通ることを ⌘ + U で確認しましょう。
もしビルドが成功したら、残すは実際にサーバーと通信するコードのみです。まずは、サードパーティ製のライブラリに頼らず、標準ライブラリだけを使ってこの通信コードを実装してみましょう。
標準ライブラリから出発する
これまでは、リクエストとレスポンスの構成要素をもとに、API クライアントの入力部分と出力部分を実装してきました。ここからは、実際の通信部分を標準ライブラリを使って実装してきます。
URLSession
クラスを使う
Swift で通信を担当する標準ライブラリのクラスは URLSession
です。この URLSession
を使って通信するには、次のようなコードを書く必要があります:
// URLSession が受け付けられるリクエストの型。
// URL とクエリ文字列、HTTP ヘッダや HTTP メソッド、
// リクエストの本文などから構成される。
let urlRequest: URLRequest
// 与えられた URLRequest を使って、サーバーとの通信を準備しておく。
let task = URLSession.shared.dataTask(with: urlRequest) { (data, urlResponse, error) in
// data には、レスポンスのペイロードが入っている。
dump(data)
// urlResponse には HTTP ヘッダーと HTTP ステータスコードが入っている。
dump(urlResponse)
// 通信エラーが起きた時はエラーが入っている。それ以外の時は nil。
dump(error)
}
// サーバーとの通信を始める。
task.resume()
URLSession
は URLRequest
というオブジェクトを受け取り、resume()
関数で通信を開始します。このとき、レスポンスを受け取るか通信エラーが発生すると、コールバックが呼ばれます。このコールバックには、レスポンスのデータとHTTPヘッダー、ステータスコード、通信エラーが与えられます。少し複雑に見えますが、取り扱っているのはどれも Web API の構成要素のみです。そのため、先ほどまで書いた WebAPI.swift
から URLSession
を呼び出すのは難しくありません。
それでは、リクエストの作成部分を書いてみましょう。
リクエストを URLRequest へ変換する
URLSession
への入力は URLRequest
クラスが担当しています。私たちが前に書いた Input
型から URLRequest
型を作成する関数を書いてみましょう:
// ...(Input は省略)...
enum WebAPI {
static func call(with input: Input) {
// ...(省略)...
}
static func call(with input: Input, _ block: @escaping (Output) -> Void) {
// ...(省略)...
}
// Input から URLRequest を作成する関数。
static private func createURLRequest(by input: Input) -> URLRequest {
// URL から URLRequeast を作成する。
var request = URLRequest(url: input.url)
// HTTP メソッドを設定する。
request.httpMethod = input.methodAndPayload.method
// リクエストの本文を設定する。
request.httpBody = input.methodAndPayload.body
// HTTP ヘッダを設定する。
request.allHTTPHeaderFields = input.headers
return request
}
}
// ...(Output と Either は省略)...
特に説明が必要ないほど簡単なコードになっています。次に、URLSession.dataTask
のコールバックに与えられた引数から Output 型を作る関数を書いてみましょう。
URLResponse
などからレスポンスを作成する
では URLSession.dataTask
のコールバックの 3 つの引数をもう一度整理しましょう:
- レスポンス本文のデータ。通信エラーなどでなければ nil。
- HTTP ヘッダなどをもつ
URLResponse
オブジェクト。通信エラーなどでなければ nil。 - 通信エラーがあればそのエラーオブジェクト。なければ nil。
これらを Output 型に変換するコードは次のようになります:
// ...(Input は省略)...
enum WebAPI {
static func call(with input: Input) {
// ...(省略)...
}
static func call(with input: Input, _ block: @escaping (Output) -> Void) {
// ...(省略)...
}
static private func createURLRequest(by input: Input) -> URLRequest {
// ...(省略)...
}
// URLSession.dataTask のコールバック引数から Output オブジェクトを作成する関数。
static private func createOutput(
data: Data?,
urlResponse: HTTPURLResponse?,
error: Error?
) -> Output {
// データと URLResponse がなければ通信エラー。
guard let data = data, let response = urlResponse else {
// エラーの内容を debugInfo に格納して通信エラーを返す。
return .noResponse(.noDataOrNoResponse(debugInfo: error.debugDescription))
}
// HTTP ヘッダーを URLResponse から取り出して Output 型の
// HTTP ヘッダーの型 [String: String] と一致するように変換する。
var headers: [String: String] = [:]
for (key, value) in response.allHeaderFields.enumerated() {
headers[key.description] = String(describing: value)
}
// Output オブジェクトを作成して返す。
return .hasResponse((
// HTTP ステータスコードから HTTPStatus を作成する。
statusCode: .from(code: response.statusCode),
// 変換後の HTTP ヘッダーを返す。
headers: headers,
// レスポンスの本文をそのまま返す。
payload: data
))
}
}
// ...(Output と Either は省略)...
コードを見ての通り、HTTP ヘッダーの変換が少し複雑ですが、それ以外は単純にプロパティへ格納するだけになっています。
さて、これで URLSession
への入力部分と出力部分を繋げられるようになりました。最後に URLSession.dataTask
を WebAPI
へ組み込んでみましょう。
通信部分を実装する
先ほど実装した createURLRequest
と createOutput
を使えば、WebAPI.call
の実装は簡単です:
// ...(Input は省略)...
enum WebAPI {
static func call(with input: Input) {
// ...(省略)...
}
static func call(with input: Input, _ block: @escaping (Output) -> Void) {
// URLSession へ渡す URLRequest を作成する。
let urlRequest = self.createURLRequest(by: input)
// レスポンス受信後のコールバックを登録する。
let task = URLSession.shared.dataTask(with: urlRequest) { (data, urlResponse, error) in
// 受信したレスポンスまたは通信エラーを Output オブジェクトへ変換する。
let output = self.createOutput(
data: data,
urlResponse: urlResponse as? HTTPURLResponse,
error: error
)
// コールバックに Output オブジェクトを渡す。
block(output)
}
task.resume()
}
static private func createURLRequest(by input: Input) -> URLRequest {
// ...(省略)...
}
static private func createOutput(data: Data?, urlResponse: HTTPURLResponse?, error: Error?) -> Output {
// ...(省略)...
}
}
// ...(Output は省略)...
この状態でビルドが通ることを ⌘ + U で確認しましょう。特に問題なければ、実際のサーバーとの通信がうまくいくとわかりました!
さて、これまでの実装で、GitHubZen
オブジェクトを取得できるようになりました。この処理の流れを図にすると、次のようになります:
.....................|.......................
: GitHub Zen API : | :
:````````````````` | :
: ...................V..................... :
: : WebAPI : +-------+ : :
: :````````` | Input | : :
: : +-------* : :
: : .................|................... : :
: : : URLSession : | : : :
: : :````````````` V : : :
: : : +------------+ : : :
: : : | URLRequest | : : :
: : : +------------+ : : :
: : : | : : :
: : : V : : :
: : : +---------------------+ : : :
: : : | URLSession.dataTask | : : :
: : : +---------------------+ : : :
: : : | : : :
: : : V : : :
: : : +-------------------------------+ : : :
: : : | (Data?, URLResponse?, Error?) | : : :
: : : +-------------------------------+ : : :
: : :................|..................: : :
: : V : :
: : +--------+ : :
: : | Output | : :
: : +--------+ : :
: :..................|....................: :
: V :
: +------------------------------------+ :
: | Either<TransformError, GitHubZen> | :
: +------------------------------------+ :
:....................|......................:
V
この図をよく見ると、綺麗に抽象層が分かれていることがわかります。つまり過不足なく抽象化して設計できたということです。このようにうまく抽象化できた設計は、それぞれの層を交換できるようになるというメリットがあります。例えば、WebAPI
より下の層は、他の Web API でも使いまわすことができます。したがって、別の API に対応したい場合でも、今回の GitHubZen
のように Output を引数にとって Either<Foo.TransformError, Foo>
を返す関数を実装するだけで対応できます。もちろん、レスポンスが JSON 形式の文字列の場合でも同様に対処できます。要するに、好きなようにカスタマイズできる柔軟な設計を手に入れられたということなのです。
しかし、使いやすさについてはどうでしょうか。WebAPI については使いやすいということはわかっていましたが、GitHubZen
が使いやすいかどうかはまだわかっていません。そこで、動作確認のコードを書くことで、使いやすさを再点検してみましょう。
使いやすさを再点検する
API クライアントを使ってみる
今回使いやすさを点検するのは GitHubZen
なので、これまで動作確認をしてきた StartSmallForAPITests.swift
とは別のファイルに書いていきましょう。そこで、StartSmallForAPITests
グループの下に GitHubAPITests.swift
というファイルを作成してください。また、このファイルのビルドターゲットは StartSmallForAPITests
にしてください。なお、ファイルの内容は次のボイラープレートのものにしておきましょう:
import XCTest
@testable import StartSmallForAPI
class GitHubAPITests: XCTestCase {
func testZenFetch() {
// TODO: 動作確認のコードをかく
}
}
次に、GitHubZen を呼び出すコードを書いてみましょう。これまでみてきた通りのコードです:
import XCTest
@testable import StartSmallForAPI
class GitHubAPITests: XCTestCase {
func testZenFetch() {
// コードは StartSmallForAPITests.testRequestAndResopnse から拝借してきた。
let expectation = self.expectation(description: "API")
let input: Input = (
url: URL(string: "https://api.github.com/zen")!,
queries: [],
headers: [:],
methodAndPayload: .get
)
WebAPI.call(with: input) { output in
switch output {
case .noResponse:
XCTFail("No response")
case let .hasResponse(response):
let errorOrZen = GitHubZen.from(response: response)
XCTAssertNotNil(errorOrZen.right)
}
expectation.fulfill()
}
self.waitForExpectations(timeout: 10)
}
}
しかし、この GitHubZen
からみると、この Input の入力は余計に感じます。なぜなら、GitHub Zen API には何も入力がないはずなのに、毎度入力を用意しなければならないからです。この煩雑さは、次のように GitHub Zen API を複数回呼ぶコードを書いてみると顕在化します:
import XCTest
@testable import StartSmallForAPI
class GitHubAPITests: XCTestCase {
func testZenFetch() {
// ...(省略)...
}
// GitHubZen API を呼び出し、結果が返ってきたらさらにもう一度呼び出す関数
// (初見で何をやってるかが掴みづらい…!)。
func testZenFetchTwice() {
let expectation = self.expectation(description: "API")
let input: Input = (
url: URL(string: "https://api.github.com/zen")!,
queries: [],
headers: [:],
methodAndPayload: .get
)
WebAPI.call(with: input) { output in
switch output {
case .noResponse:
XCTFail("No response")
case let .hasResponse(response):
let nextInput: Input = (
url: URL(string: "https://api.github.com/zen")!,
queries: [],
headers: [:],
methodAndPayload: .get
)
WebAPI.call(with: nextInput) { nextOutput in
switch nextOutput {
case .noResponse:
XCTFail("No response")
case let .hasResponse(response):
let errorOrZen = GitHubZen.from(response: response)
XCTAssertNotNil(errorOrZen.right)
}
expectation.fulfill()
}
}
expectation.fulfill()
}
self.waitForExpectations(timeout: 10)
}
}
よく内容を読めば難しいことはしていないことがわかりますが、読みづらいコードになっています。つまり、今のままでは、GitHubZen
が使いやすいとはいえなさそうです。こういうときは、再度インターフェースの想像に戻りましょう。動作確認のコードに本来あるべき姿を想像して書いてみます:
import XCTest
@testable import StartSmallForAPI
class GitHubAPITests: XCTestCase {
func testZenFetch() {
let expectation = self.expectation(description: "API")
// GitHub Zen API には入力パラメータがないので、関数呼び出し時には
// 引数は指定しなくて済むようにしたい。また、API 呼び出しは非同期なので、
// コールバックをとるはず(注: GitHubZen.fetch はあとで定義する)。
GitHubZen.fetch { errorOrZen in
// エラーかレスポンスがきたらコールバックが実行されて欲しい。
// できれば、結果はすでに変換済みの GitHubZen オブジェクトを受け取りたい。
switch errorOrZen {
case let .left(error):
// エラーがきたらわかりやすいようにする。
XCTFail("\(error)")
case let .right(zen):
// 結果をきちんと受け取れたことを確認する。
XCTAssertNotNil(zen)
}
expectation.fulfill()
}
self.waitForExpectations(timeout: 10)
}
// API を二度呼ぶ方もかなり可読性が上がっている。
func testZenFetchTwice() {
let expectation = self.expectation(description: "API")
GitHubZen.fetch { errorOrZen in
switch errorOrZen {
case let .left(error):
XCTFail("\(error)")
case .right(_):
GitHubZen.fetch { errorOrZen in
switch errorOrZen {
case let .left(error):
XCTFail("\(error)")
case let .right(zen):
XCTAssertNotNil(zen)
expectation.fulfill()
}
}
}
}
self.waitForExpectations(timeout: 10)
}
}
このような要件を満たす GitHubZen.fetch
関数を用意できれば、GitHubZen
の使い勝手もよくなりそうです。
では、実装にとりかかりましょう。
API 呼び出し部分を簡略化する
GitHubZen
に API 経由で禅なメッセージを取得する fetch
関数を実装します:
import Foundation
enum Either<Left, Right> {
// ...(省略)...
}
struct GitHubZen {
let text: String
static func from(response: Response) -> Either<TransformError, GitHubZen> {
// ...(省略)...
}
/// GitHub Zen API を使って、禅なフレーズを取得する関数。
static func fetch(
// コールバック経由で、接続エラーか変換エラーか GitHubZen のいずれかを受け取れるようにする。
_ block: @escaping (Either<Either<ConnectionError, TransformError>, GitHubZen>) -> Void
// コールバックの引数の型が少しわかりづらいが、次の3パターンになる。
//
// - 接続エラーの場合 → .left(.left(ConnectionEither))
// - 変換エラーの場合 → .left(.right(TransformError))
// - 正常に取得できた場合 → .right(GitHubZen)
) {
// URL が生成できない場合は不正な URL エラーを返す
let urlString = "https://api.github.com/zen"
guard let url = URL(string: urlString) else {
block(.left(.left(.malformedURL(debugInfo: urlString))))
return
}
// GitHub Zen API は何も入力パラメータがないので入力は固定値になる。
let input: Input = (
url: url,
queries: [],
headers: [:],
methodAndPayload: .get
)
// GitHub Zen API を呼び出す。
WebAPI.call(with: input) { output in
switch output {
case let .noResponse(connectionError):
// 接続エラーの場合は、接続エラーを渡す。
block(.left(.left(connectionError)))
case let .hasResponse(response):
// レスポンスがわかりやすくなるように GitHubZen へと変換する。
let errorOrZen = GitHubZen.from(response: response)
switch errorOrZen {
case let .left(error):
// 変換エラーの場合は、変換エラーを渡す。
block(.left(.right(error)))
case let .right(zen):
// 正常に変換できた場合は、GitHubZen オブジェクトを渡す。
block(.right(zen))
}
}
}
}
enum TransformError {
// ...(省略)...
}
}
また、接続エラーの種類に不正な URL であることを意味する malformedURL
を追加しましょう。
// ...(Input は省略)...
enum ConnectionError {
case noDataOrNoResponse(debugInfo: String)
/// 不正な URL の場合のエラー。
case malformedURL(debugInfo: String)
}
// ...(Output は省略)...
実装できたら、⌘ + U で動作を確認しましょう。
さて、これで GitHubZen
を使いやすくする対応が完了しました。これまでの作業を振り返ると、設計の見直しによって私たちは使いやすい API クライアントを手に入れられたことがわかります。さらに、これまでに WebAPI
を使いやすい設計にしておいたおかげで、実装したコードもシンプルになっています。
しかし、実際に私たちが対応しなければならない API の数は 1 つでないはずです。そこで、対応する API を増やした場合でも、これまでの設計が耐えられるかどうかについても試してみましょう。
対応する API を増やす
今度は GitHub User API に対応してみます。この GitHub User API は、ユーザーのログイン名を指定すると、そのユーザーの詳細を返す API です。このユーザーの詳細は、次のようなオブジェクトになります:
struct GitHubUser {
/// GitHub の ID 番号。
let id: Int
/// GitHub のログイン名。
let login: String
// (プロパティは他にもあるが今回は省略して実装する)
}
さて、これまでと同じように、Output
から GitHubUser
への変換が必要と予想されます。そこで、インターフェースを想像するために変換部分の動作確認コードを書きます:
import XCTest
@testable import StartSmallForAPI
class GitHubAPITests: XCTestCase {
func testZenFetch() {
// ...(省略)...
}
func testZenFetchTwice() {
// ...(省略)...
}
// レスポンスを GitHubUser へ変換できることを確かめる動作確認コード。
func testUser() throws {
// レスポンスを定義。
let response: Response = (
// 200 OK が必要。
statusCode: .ok,
// 必要なヘッダーは特にない。
headers: [:],
// API レスポンスを GitHubUser へ変換できるか試すだけなので、
// 適当な ID とログイン名を指定。
payload: try JSONSerialization.data(withJSONObject: [
"id": 1,
"login": "octocat"
])
)
switch GitHubUser.from(response: response) {
case let .left(error):
// ここにきてしまったらわかりやすいようにする。
XCTFail("\(error)")
case let .right(user):
// ID とログイン名が正しく変換できたことを確認する。
XCTAssertEqual(user.id, 1)
XCTAssertEqual(user.login, "octocat")
}
}
}
変換部分の動作確認コードは、ほぼ GitHubZen
と同じインターフェースになりました。そのため、GitHubZen
と同じように使いやすいコードになっていると期待できます。次に、変換部分のコードを実装してみましょう:
// ...(GitHubZen は省略)...
// JSON からこのオブジェクトを作成したいため、Codable を実装させる
// (Codable は Swift4 から追加されたシリアライズ/デシリアライズ用のプロトコル)。
struct GitHubUser: Codable {
let id: Int
let login: String
/// レスポンスから GitHubUser オブジェクトへ変換する関数。
static func from(response: Response) -> Either<TransformError, GitHubUser> {
switch response.statusCode {
// HTTP ステータスが OK だったら、ペイロードの中身を確認する。
case .ok:
do {
// User API は JSON 形式の文字列を返すはずので Data を JSON として
// 解釈してみる。
let jsonDecoder = JSONDecoder()
let user = try jsonDecoder.decode(GitHubUser.self, from: response.payload)
// もし、内容を JSON として解釈できたなら、
// その文字列から GitHubUser を作って返す(エラーではない型は右なので .right を使う)
return .right(user)
}
catch {
// もし、Data が JSON 文字列でなければ、何か間違ったデータを受信してしまったのかもしれない。
// この場合は、malformedData エラーを返す(エラーの型は左なので .left を使う)。
return .left(.malformedData(debugInfo: "\(error)"))
}
// もし、HTTP ステータスコードが OK 以外であれば、エラーとして扱う。
// たとえば、GitHub API を呼び出しすぎたときは 200 OK ではなく 403 Forbidden が
// 返るのでこちらにくる。
default:
// エラーの内容がわかりやすいようにステータスコードを入れて返す。
return .left(.unexpectedStatusCode(debugInfo: "\(response.statusCode)"))
}
}
/// GitHub User API の変換で起きうるエラーの一覧。
enum TransformError {
/// ペイロードが壊れた JSON だった場合のエラー。
case malformedData(debugInfo: String)
/// HTTP ステータスコードが OK 以外だった場合のエラー。
case unexpectedStatusCode(debugInfo: String)
}
}
ここまで実装できたら ⌘ + U で動作を確認してみましょう。
うまく実装できたら、最後に GitHubUser
についてもサーバー経由で GitHubUser
を取得する処理を fetch
関数へとまとめてしまいます:
import XCTest
@testable import StartSmallForAPI
class GitHubAPITests: XCTestCase {
func testZenFetch() {
// ...(省略)...
}
func testZenFetchTwice() {
// ...(省略)...
}
func testUser() throws {
// ...(省略)...
}
// サーバー経由で GitHubUser を取得する処理の動作確認コード。
func testUserFetch() {
let expectation = self.expectation(description: "API")
// ログイン名から GitHubUser を取得する関数を呼び出す。
// 非同期で結果を取得するのでコールバックになると推測。
GitHubUser.fetch(byLogin: "Kuniwak") { errorOrUser in
// 結果は、通信エラーや変換エラーか取得できたユーザーのいずれかになると推測。
switch errorOrUser {
case let .left(error):
// エラーになったらわかりやすいようにしておく。
XCTFail("\(error)")
case let .right(user):
// 取得できた実際の ID をログイン名を確認する。
XCTAssertEqual(user.id, 1124024)
XCTAssertEqual(user.login, "Kuniwak")
}
expectation.fulfill()
}
self.waitForExpectations(timeout: 10)
}
}
こちらもほぼ GitHubZen.fetch
と同じ使い勝手にできそうです。
では、使い勝手を確認できたので、中身の実装へと移りましょう:
// ...(GitHubZen は省略)...
struct GitHubUser: Codable {
let id: Int
let login: String
static func from(response: Response) -> Either<TransformError, GitHubUser> {
// ...(省略)...
}
/// ログイン名から GitHubUser を取得する関数。
static func fetch(
// 取得したいユーザーのログイン名。
by login: String,
// コールバック経由で、接続エラーか変換エラーか GitHubUser のいずれかを受け取れるようにする。
_ block: @escaping (Either<Either<ConnectionError, TransformError>, GitHubUser>) -> Void
// コールバックの引数の型が少しわかりづらいが、次の3パターンのいずれかになる。
//
// - 接続エラーの場合 → .left(.left(ConnectionEither))
// - 変換エラーの場合 → .left(.right(TransformError))
// - 正常に取得できた場合 → .right(GitHubUser)
) {
// GitHub User API の URL の形式は https://api.github.com/users/<ログイン名> なので、
// URL の末尾にログイン名を付加する。
let urlString = "https://api.github.com/users"
guard let url = URL(string: urlString)?.appendingPathComponent(login) else {
// もし、不正な URL になったらコールバックにエラーを渡す。
block(.left(.left(.malformedURL(debugInfo: "\(urlString)/\(login)"))))
return
}
let input: Input = (
url: url,
queries: [],
headers: [:],
methodAndPayload: .get
)
// 指定したパラメーターで GitHub User API を呼び出す。
WebAPI.call(with: input) { output in
switch output {
case let .noResponse(connectionError):
// もし、接続エラーになったらコールバックにエラーを渡す。
block(.left(.left(connectionError)))
case let .hasResponse(response):
// レスポンスを GitHubUser へと変換する。
let errorOrUser = GitHubUser.from(response: response)
switch errorOrUser {
case let .left(transformError):
// もし、変換エラーになったらコールバックにエラーを渡す。
block(.left(.right(transformError)))
case let .right(user):
// 正常に GitHubUser へ変換できたのでコールバックへ渡す。
block(.right(user))
}
}
}
}
enum TransformError {
// ...(省略)...
}
}
ここまで実装できたら ⌘ + U で動作を確認してみましょう。もし確認に成功すれば、簡単に API を追加できたことがわかりました!
これまでの設計・再設計の流れを振り返ってみましょう。今回のように動作確認のコードを書きながら漸進的に設計を進めることで、使いやすい設計を得られることが体感できたのではないでしょうか。
さて、ここであなたのこれまでの経験を振り返ってみてください。もし、これまでの実装で不足がなければ、実は標準ライブラリと動作確認のためのコードを書くことだけで十分綺麗な設計ができるのです。今、あなたがサードパーティのライブラリを使っているのであれば、あなたの設計に本当に必要なのかどうかを自問してみてください。
次の章ではサードパーティ製のライブラリが必要になった場合の方法をみていきましょう。
サードパーティ製ライブラリを使う
これまでは標準ライブラリだけを使って、API クライアントを設計してきました。しかし、世の中には数多くのサードパーティ製通信ライブラリが存在します。たとえば、有名なものでは、Alamofire や APIKit などがあります。これらのライブラリは、皆さんもよく耳にするのではないでしょうか。
さて、これらのサードパーティ製のライブラリを使うという判断はどのようにするべきでしょうか。また、サードパーティ製のライブラリを使うと判断したとして、どのようなライブラリを使うべきでしょうか。
では、まずサードパーティ製のライブラリをなぜ使うのか整理してみましょう。
なぜサードパーティ製のライブラリを使うのか
まず、Web API ライブラリにまつわる重要な事実がひとつあります。それは Web API ライブラリのほとんどが URLSession
を内部的に使っており、実際のところ URLSession
のラッパーに過ぎないということです。そのため、現時点のサードパーティ製ライブラリの役割は多くありません。私の知っている限りでは、サードパーティ製ライブラリの役割は次のように限定されています:
- 特定の仕様への特化
- RESTful API や JSON RPC への特化など。
- 入出力形式の拡張
- 標準ライブラリではまだ対応されていない
multipart/form-data
やapplication/x-www-form-urlencoded
への対応など。 -
URLSession
とは異なるインターフェースの提供 - メソッドチェーンによるインターフェースの導入や、コールバック以外の非同期処理インターフェース(Promise や Reactive Extensions)のサポートなど。
このうち、「特定の仕様への特化」と「入出力形式の拡張」が目的であれば、ほぼ間違いなくライブラリを使う価値があります。しかし、「URLSession
とは異なるインターフェースの提供」については注意が必要です。これを説明するには、インターフェースの Easy さと Simple さを説明しなければなりません。次の節では、それらの区別とメリット/デメリットについて説明します。
Easy なインターフェースと Simple なインターフェース
まず、標準ライブラリのラッパーが提供するインターフェースは、Easy なものと Simple なものの2つに分類できます。この Easy と Simple の間には、利用できる構成要素を隠す/隠さないという違いがあります。
インターフェースを Easy にするライブラリは、構成要素理解を必要とせず使えるようにするため、多くの構成要素を隠しています。例えば、あまり使われない機能である HTTP ヘッダーの入力/出力インターフェースは隠してもいいかもしれません。また、リクエストのパラメーターの形式がクエリ文字列でレスポンスのパラメータの形式が JSON 文字列のみならば、入力形式の指定部分も隠せます。そうすることで見た目のコード量は減り、指示された通りにパーツを当てはめていけば動作してくれるため、レールに乗ったかのような使い心地を味わえます。
しかし、Easy にされたインターフェースには深刻な欠点もあります。構成要素が隠されるのですから、実現できない入力や出力がでてくるのです。つまり、レールから外れてしまうと途端に難しくなるのが Easy にされたインターフェースの欠点です。
そして、もう一方のインターフェースを Simple にするライブラリは、構成要素が多くても多いまま提供します。ただし、よくある共通の処理があるならば、それらを単純化したインターフェースとして提供します。たとえば、 URLSession
は構成要素が多いため、Easy なインターフェースとはいえません。しかし、インターフェースはいたって Simple であり、構成要素をうまく利用できれば実現できない入出力はありません。このような Simple なライブラリの強みは、構成要素をフルに使える表現力です。なお、Simple なライブラリの欠点は、構成要素を理解していないと使い方も理解できないことです。しかし、構成要素を理解してしまえば、Simple なものほど心強いものはありません。そして、これこそが、今回の記事で Web API クライアントの構成要素の把握を最初に持ってきた理由なのです。
そこで、もしあなたが URLSession
とは異なるインターフェースを提供するライブラリを使う際には、インターフェースが Easy と Simple のどちらなのか注意深く観察してみてください。もし、インターフェースが Easy だとすれば、将来的に対応できない入出力がでてくる可能性があります。したがって、そのようなライブラリの利用は避けたほうがよいでしょう。
さて、この節では Easy なインターフェースのリスクについて説明しました。しかし、インターフェースが Simple だからといって、すぐに使うという判断を下すのは早計です。実際にはインターフェースの Easy さや Simple さに関わらないリスクも存在するからです。次の節ではそれらについてみていきましょう。
サードパーティ製ライブラリを使うメリットとリスク
サードパーティ製ライブラリのリスクは、次の 3 つに分けられます:
- サードパーティ製ライブラリは自分で直せないかもしれない
- 明日にはよりよい標準ライブラリが使えるかもしれない
- 明日にはこのライブラリは使えなくなるかもしれない
最初のリスクは、ライブラリのコードは他人のコードであるという根源的な悩みです。もし、ライブラリにバグがあったとしても、直すことを拒否されるかもしれません。ライブラリによっては、修正自体を許可されていない可能性もあります。
2つめと3つめのリスクは、どちらも Swift の速い変化に関係するものです。たとえば、Swift4 で導入された Codable
によって、JSON から特定の struct や class へとマッピングするライブラリはその輝きを失いました。このように、標準ライブラリ自体の進化によって、特定のライブラリへの依存が負債となることがあります。さらに、Swift では API の廃止も頻繁におこなわれています。もし、あなたの使っているライブラリが廃止された API に依存していたなら、早急にこの問題を解消しなければなりません。
このように、ライブラリを使うことにはリスクもあります。したがって、サードパーティ製ライブラリのメリットとリスクを天秤にかけ、どちらかを選ぶ判断をしなければなりません。ここに参考例として、いくつかの私の判断を紹介しましょう:
- メソッドチェーンによるインターフェースの導入
-
- メリット
- メソッドチェーンによる Swifty で Easy な実装が可能になる。しかし、メソッドチェーンや Easy なインターフェースには欠点も多く、メリットは少なめ。
- リスク
- Swift やライブラリのバージョンアップによって、コードが壊れるリスクは高い。
- 最終的な判断
- 導入しないことにした。リスクに比べてメリットが少なすぎるため。
- コールバックとは別の非同期インターフェースの導入
-
- メリット
- 非同期なインターフェースへの変換を自分で書かなくてすむ。しかし、コード量はそこまで多くないため、メリットは少なめ。
- リスク
- Swift や非同期インターフェースライブラリのバージョンアップによって、コードが壊れるリスクは高い。
- 最終的な判断
- ライブラリを使わず、自分で実装することにした。ライブラリの制約に囚われずに、好きなタイミングで Swift や非同期インターフェースのバージョンを選べることを重視。
終わりに
さて、これでこの長い記事も終わりになります。いかがだったでしょうか。最後に、これまでの内容を簡単にまとめましょう:
- ある設計が使いやすいかどうかは、動作確認のコードを書けばわかる
- 動作確認のコードを都度書いていれば、自然と過不足なく抽象化される
- 構成要素を把握して、Easy なライブラリではなく Simple なライブラリに依存しよう
解説: エラーの使い分け
Swift では、エラーが起きたことを知らせる方法が4つあります:
-
throw
などの例外 -
- メリット
- Swift の標準の方法なのでわかりやすい。
- デメリット
- 例外の型は強制的に
Error
になってしまい、情報量が落ちる。
-
T?
などの optional -
- メリット
- Foundation の一部のライブラリはこの形式なので、一貫性を出せる。
- デメリット
- 例外の内容がわからないため、情報量が少ない。
-
(MyError?, T?)
などの tuple -
- メリット
- エラーの情報量が落ちない。
- デメリット
-
(nil, nil)
などの無意味な組み合わせを許容してしまう。
-
Either
やResult
などの enum -
- メリット
- エラーの情報量が落ちない。
- デメリット
- 特にない。
約束3「エラーの情報量を落とさない」を重視すると、約束にあった手法は 3つめの tuple か4つめの enum に絞り込まれます。
そのうち、デメリットの少ない enum を採用しています。
解説: 名前空間としての enum
この記事では名前空間として enum を使っています。struct や class ではなく enum を使う理由は、名前空間のインスタンス化という無意味な操作ができないことです。前者は、init
を隠さない限り名前空間をインスタンス化できてしまいます:
struct Namespace {
static func doSomething() {}
}
// 名前空間をインスタンス化するという意味のないことができてしまう。
let whatIsThis = Namespace()
また、init
を隠すことで名前空間のインスタンス化は防げるようになりますが、都度このコードを書くのは煩雑です:
struct Namespace {
// 煩雑な記述が増えてしまう
private init() {}
static func doSomething() {}
}
そこで、enum を使えば煩雑な記述を必要とせずにインスタンス化できない名前空間が作成できます:
enum Namespace {
static func doSomething() {}
}
// 名前空間はインスタンス化できないので、純粋に名前空間として使える。
そのため、この記事では名前空間の作成に enum を使っています。
付録: 最終的なコード
import Foundation
typealias Input = Request
typealias Request = (
url: URL,
queries: [URLQueryItem],
headers: [String: String],
methodAndPayload: HTTPMethodAndPayload
)
enum HTTPMethodAndPayload {
case get
// case post(payload: Data?)
var method: String {
switch self {
case .get:
return "GET"
}
}
var body: Data? {
switch self {
case .get:
return nil
}
}
}
enum Output {
case hasResponse(Response)
case noResponse(ConnectionError)
}
enum ConnectionError {
case malformedURL(debugInfo: String)
case noDataOrNoResponse(debugInfo: String)
}
typealias Response = (
statusCode: HTTPStatus,
headers: [String: String],
payload: Data
)
enum HTTPStatus {
case ok
case notFound
case unsupported(code: Int)
static func from(code: Int) -> HTTPStatus {
switch code {
case 200:
return .ok
case 404:
return .notFound
default:
return .unsupported(code: code)
}
}
}
enum WebAPI {
static func call(with input: Input) {
self.call(with: input) { _ in
// 何もしない
}
}
static func call(with input: Input, _ block: @escaping (Output) -> Void) {
let urlRequest = self.createURLRequest(by: input)
let task = URLSession.shared.dataTask(with: urlRequest) { (data, urlResponse, error) in
let output = self.createOutput(
data: data,
urlResponse: urlResponse as? HTTPURLResponse,
error: error
)
block(output)
}
task.resume()
}
static private func createURLRequest(by input: Input) -> URLRequest {
var request = URLRequest(url: input.url)
request.httpMethod = input.methodAndPayload.method
request.httpBody = input.methodAndPayload.body
request.allHTTPHeaderFields = input.headers
return request
}
static private func createOutput(
data: Data?,
urlResponse: HTTPURLResponse?,
error: Error?
) -> Output {
guard let data = data, let response = urlResponse else {
return .noResponse(.noDataOrNoResponse(debugInfo: error.debugDescription))
}
var headers: [String: String] = [:]
for (key, value) in response.allHeaderFields.enumerated() {
headers[key.description] = String(describing: value)
}
return .hasResponse((
statusCode: .from(code: response.statusCode),
headers: headers,
payload: data
))
}
}
import Foundation
enum Either<Left, Right> {
case left(Left)
case right(Right)
var left: Left? {
switch self {
case let .left(x):
return x
case .right:
return nil
}
}
var right: Right? {
switch self {
case .left:
return nil
case let .right(x):
return x
}
}
}
struct GitHubZen {
let text: String
static func from(response: Response) -> Either<TransformError, GitHubZen> {
switch response.statusCode {
case .ok:
guard let string = String(data: response.payload, encoding: .utf8) else {
return .left(.malformedData(debugInfo: "not UTF-8 string"))
}
return .right(GitHubZen(text: string))
default:
return .left(.unexpectedStatusCode(
debugInfo: "\(response.statusCode)")
)
}
}
static func fetch(
_ block: @escaping (Either<Either<ConnectionError, TransformError>, GitHubZen>) -> Void
) {
let urlString = "https://api.github.com/zen"
guard let url = URL(string: urlString) else {
block(.left(.left(.malformedURL(debugInfo: urlString))))
return
}
let input: Input = (
url: url,
queries: [],
headers: [:],
methodAndPayload: .get
)
WebAPI.call(with: input) { output in
switch output {
case let .noResponse(connectionError):
block(.left(.left(connectionError)))
case let .hasResponse(response):
let errorOrZen = GitHubZen.from(response: response)
switch errorOrZen {
case let .left(error):
block(.left(.right(error)))
case let .right(zen):
block(.right(zen))
}
}
}
}
enum TransformError {
case malformedData(debugInfo: String)
case unexpectedStatusCode(debugInfo: String)
}
}
struct GitHubUser: Codable {
let id: Int
let login: String
static func from(response: Response) -> Either<TransformError, GitHubUser> {
switch response.statusCode {
case .ok:
do {
let jsonDecoder = JSONDecoder()
let user = try jsonDecoder.decode(GitHubUser.self, from: response.payload)
return .right(user)
}
catch {
return .left(.malformedData(debugInfo: "\(error)"))
}
default:
return .left(.unexpectedStatusCode(debugInfo: "\(response.statusCode)"))
}
}
static func fetch(
byLogin login: String,
_ block: @escaping (Either<Either<ConnectionError, TransformError>, GitHubUser>) -> Void
) {
let urlString = "https://api.github.com/users"
guard let url = URL(string: urlString)?.appendingPathComponent(login) else {
block(.left(.left(.malformedURL(debugInfo: "\(urlString)/\(login)"))))
return
}
let input: Input = (
url: url,
queries: [],
headers: [:],
methodAndPayload: .get
)
WebAPI.call(with: input) { output in
switch output {
case let .noResponse(connectionError):
block(.left(.left(connectionError)))
case let .hasResponse(response):
let errorOrUser = GitHubUser.from(response: response)
switch errorOrUser {
case let .left(transformError):
block(.left(.right(transformError)))
case let .right(user):
block(.right(user))
}
}
}
}
enum TransformError {
case malformedUsername(debugInfo: String)
case malformedData(debugInfo: String)
case unexpectedStatusCode(debugInfo: String)
}
}
import XCTest
@testable import StartSmallForAPI
class StartSmallForAPITests: XCTestCase {
func testRequest() {
let input: Input = (
url: URL(string: "https://api.github.com/zen")!,
queries: [],
headers: [:],
methodAndPayload: .get
)
WebAPI.call(with: input)
}
func testResponse() {
let text = "this is a response text"
let response: Response = (
statusCode: .ok,
headers: [:],
payload: text.data(using: .utf8)!
)
let errorOrZen = GitHubZen.from(response: response)
switch errorOrZen {
case let .left(error):
XCTFail("\(error)")
case let .right(zen):
XCTAssertEqual(zen.text, text)
}
}
func testRequestAndResponse() {
let expectation = self.expectation(description: "API")
let input: Input = (
url: URL(string: "https://api.github.com/zen")!,
queries: [],
headers: [:],
methodAndPayload: .get
)
WebAPI.call(with: input) { output in
switch output {
case let .noResponse(connectionError):
XCTFail("\(connectionError)")
case let .hasResponse(response):
let errorOrZen = GitHubZen.from(response: response)
XCTAssertNotNil(errorOrZen.right)
}
expectation.fulfill()
}
self.waitForExpectations(timeout: 10)
}
}
import XCTest
@testable import StartSmallForAPI
class GitHubAPITests: XCTestCase {
func testZenFetch() {
let expectation = self.expectation(description: "API")
GitHubZen.fetch { errorOrZen in
switch errorOrZen {
case let .left(error):
XCTFail("\(error)")
case let .right(zen):
XCTAssertNotNil(zen)
}
expectation.fulfill()
}
self.waitForExpectations(timeout: 10)
}
func testZenFetchTwice() {
let expectation = self.expectation(description: "API")
GitHubZen.fetch { errorOrZen in
switch errorOrZen {
case let .left(error):
XCTFail("\(error)")
case .right(_):
GitHubZen.fetch { errorOrZen in
switch errorOrZen {
case let .left(error):
XCTFail("\(error)")
case let .right(zen):
XCTAssertNotNil(zen)
expectation.fulfill()
}
}
}
}
self.waitForExpectations(timeout: 10)
}
func testUser() throws {
let response: Response = (
statusCode: .ok,
headers: [:],
payload: try JSONSerialization.data(withJSONObject: [
"id": 1,
"login": "octocat"
])
)
switch GitHubUser.from(response: response) {
case let .left(error):
XCTFail("\(error)")
case let .right(user):
XCTAssertEqual(user.id, 1)
XCTAssertEqual(user.login, "octocat")
}
}
func testUserFetch() {
let expectation = self.expectation(description: "API")
GitHubUser.fetch(byLogin: "Kuniwak") { errorOrUser in
switch errorOrUser {
case let .left(error):
XCTFail("\(error)")
case let .right(user):
XCTAssertEqual(user.id, 1124024)
XCTAssertEqual(user.login, "Kuniwak")
}
expectation.fulfill()
}
self.waitForExpectations(timeout: 10)
}
}
- FirebaseのデータをObjectMapperを用いてデータ構造を管理する方法
- Metal逆引きレシピ
- 1年遅れのWWDC 2015 Tour
- Swiftの静的コード解析ツールTailorの使い方
- Swift3でFacebook SDKを使う
- UITableViewのデリゲートメソッドまとめ
- iOS コーディングガイドライン
- Notification的にRxSwiftをクラス間の通知に使う
- React NativeでObjective-C/SwiftのAPIを扱う(Native Component編)
- Firebase Notificationを使用してiOS端末にPush通知を送信する
- Metalの恩恵は受けつつCore Imageで「手軽に」画像処理
- Swiftでのローカル通知・リモート通知の実装メモ
- UINavigationControllerと座標ズレの小ネタ
- [swift] Firebase Realtime Databaseのリスナーについて
- モジュール結合度について
- FacebookやTwitterのアプリで気になった表現を自分なりにトレースした際の実装ポイントまとめ(タイルレイアウトがサムネイル画像の枚数に応じて変わる表現)
- Widgets(Today Extension)のまとめ
- iOSアプリに審査なしでパッチを当てるライブラリを作ってみた
- Swift Evolution の Commonly Proposed がすごい勉強になる
- AndroidとiOSの実装を徹底比較する
- CALayer概要
- [Swift]0から作るXmasカウントダウンアプリ
- 猿でもできるKituraでJSONを生成する方法
- Visual Format Languageを使う【Swift3.0】
- User登録画面で活用できそうなProtocolを活用したDesignPattern3選
- RxSwift スレッドクイズ (解答・解説編)
- Kickstarter-iOSのViewModelの作り方がウマかった
- Swift で 2つの線分の交点を求める
- Metalでカメラからの動画入力をリアルタイム処理する
- iOSアプリ開発でアニメーションするなら押さえておきたい基礎
- iOS標準の写真アプリのように画像をズームしながら遷移させる
- プロトコル指向言語としてのSwift – OOPからPOPへのパラダイムシフトと注意点
- Actionを使って快適なViewModel生活を🏄
- 【Swift】最前面のUIViewControllerを取得する方法
- UIFeedbackGeneratorをiOS10未満対応アプリで楽に書けるUtility
- ViewControllerをObservableとして考える
- 【swift】イラストで分かる!具体的なDelegateの使い方。
- iPod20台を通信/制御/連携してみた【Max × Swift × OSC】
- iOSのMPSCNNによる手書き数字認識のサンプルを読む – 前編
- swift3でCGFloat.min, CGFloat.maxを利用したい
- Objective-CからSwiftへの移行でバグりやすいポイント
- コードレビューをチームにリクエストするSlack BotをSwift実装したお話🚢
- 誰でもわかるプログラミング入門を目指し隊〜Swift編〜
- Metal で 三角形を組み合わせて 2D の線を描く
- Swift中間言語の、ひとまず入り口手前まで
- MetalでiOSアプリに宿る生命
- Xcode 8.2, Swift 3.0でTwitterの認証を通してタイムラインを取得するまで
- Swift 3 以降の NotificationCenter の正しい使い方
- Windows 10でSwift開発環境構築 with Atom
- Observable.just()からはじめるRxSwift
- `as AnyObject` で何が起こるのか
- まだiOS Clean Architecture で消耗してるの? 爆速開発ツールを作ったのでご紹介
- Swiftにおけるアニメーションの世界観を掴む
- [Swift] [iOS] Watson Visual Recognitionを使って顔解析アプリを作ってみた
- 【Swift】いまさらですがiOS10でプッシュ通知を実装したサンプルアプリを作ってみた
- 【Swift3】日本語を含むURLをNSURLに
- スワイプすると出てくるメニューをSwiftで作る
- 脱・文字列ハードコーディング
- Objective-C の循環的複雑度を計測する
- 続:いろんなところでsushiを流す話
- iOSのMetalで畳み込みニューラルネットワーク – MPSCNNを用いた手書き数字認識の実装
- [iOS10] カメラを触ってみる
- ビルドで待たないための、Simulator 上で実行中の view をいじる方法
- Swift での UI テストの雑なまとめ
- iOSの新規プロジェクトでClean Architecture + Application Coordinatorの構成にした理由と感触
- [Swift] バリデーションチェックは、正規表現で!
- MkMapViewの使い方(swift版)
- UIKitにある機能でWebで見かけるようなUI達を作る
- Swift 3でSwiftBondを使ってMVVMしちゃおう
- [Swift] サンプルを見ながら、リファクタリングを勉強する
- Swift製のコマンドラインツールをbrewでインストールできるようにする
- 【Swift】アプリからメーラーを起動する方法
- Swiftで日記アプリを作ろう 〜その1 カレンダー表示編〜
- Carthageを使っていて”$(SRCROOT)/Carthage/Build/iOS/*.framework”を打ち飽きた方へ
- AWS + Nginx + Node.js + iOS(Swift) でリアルタイムチャットアプリを作ろう
- 【Xcode 8対応版】Xcodeプラグイン Alcatrazを導入しよう
- SwiftのAddress/Thread Sanitizer
- iOSで指先またはペン先の動きを記録
- Swiftで日記アプリを作ろう 〜その2 Realm導入編〜
- IBM OpenWhiskというサーバーレス実行環境を用いて、Swiftでおみくじ🎍
- 初心者のためのSprite Kit入門
- 【Swift】Realm BrowserでRealm Mobile Databaseの中身を確認する
- Swiftで日記アプリを作ろう 〜その3 Realm活用編〜
- iOSプロジェクトのテストバンドルに存在するファイルを取得する
- Swift3でDocumentディレクトリのファイルにアクセスする
- RxSwiftでUITextFiledのtextのオブザーバーがSwift3から変わっていた
- 【Swift】UITextFieldのカーソル非表示&コピー・ペーストを不可にする方法
- Alamofire, URLSessionの通信処理をMethod Swizzlingでスタブに置き換える
- 冬休みの8日間を使って、ダンス動画アプリを作った
- Swift で計算して Bezier 曲線を描く
- Swiftで小学生の頃ハマった定規でぐるぐるするやつを描く
- iOSアプリが何回もリジェクトされた後にAppleからフォローアップされる件
- ジェスチャーやカスタムトランジションを利用して入力時やコンテンツ表示時に一工夫を加えたUIの実装ポイントまとめ
- Xcodeの旧バージョンをインストールする方法
- Share Extensionでデータを共有する
- 【iOS10】Firebaseでサイレント通知を行う
- iOSで音声/ビデオ通話をWebRTCを使って実現する(調査編)
- iOSアプリ向け通信スタブの使い方
- Swift3.0対応 CoreGraphicsでPieを描く
- 【iOS】Tinder的アニメーション
- Instagramライクな画像フィルタライブラリをCocoaPodsで公開してみた
- SceneKitで扱える3Dモデルのフォーマット/アニメーションつき3DモデルをSceneKitで使う
- UITabBarにcustomなバッジをつける
- SwiftとJavascriptの変数宣言の比較
- RxSwiftをサクッと勉強してみた
- そういえばMacでAndroidアプリつくるために必要な準備ってなんだっけ?
- 吹き出しのようなViewを作ってみる
- Swiftのエラー4分類が素晴らしすぎるのでみんなに知ってほしい
- Swiftの型の限界を超える
- SwiftのString(文字列) APIとの付き合い方
- 恐怖!忍び寄るライブラリへのロックイン
- 関数型プログラミングの実際のところ
- Never GiveUp というタイトルで Swift Tweets で発表しました。 #swtws
- Clean Architecture 開発ツールの話
- iOSにおけるクリーンアーキテクチャよもやま話 #swtws
- 型推論のビルドが遅いらしいから調べてみる
- iOSエンジニアの正規表現入門
- UIViewPropertyAnimatorの基本まとめ①
- Swift3での日時に関する処理
- プッシュ通知を究める!その①〜普通のプッシュ通知の実装の仕方〜
- プッシュ通知を究める!その②〜リッチ通知(メディア付き)の実装の仕方〜
- KickStarter iOSアプリのStoryboardのenumでの管理のしかたに感動した
- iOSアプリからFirebase Cloud Messaging経由でプッシュ通知を送る
- Swift で Elixir のパイプ演算子を実装してみる
- Swiftの値型と参照型、値渡しと参照渡し
- Swiftのクロージャにおける循環参照
- [Swift] Validatorというライブラリを使ってValidationする
- KituraでWebSocketを動かす
- iOSアプリ開発基礎ハンズオン
- 素晴らしいSwiftのポインタ型の解説
- Swiftのメモリレイアウトを調べる
- Storyboard を使わずコードだけで画面を生成、遷移をしてみる
- DispatchQueueでthrottle/debounceを実現する
- Swiftのenumのメモリレイアウトの最適化が凄い
- RxSwiftでの実装練習の記録ノート(前編:Observerパターンの例とUITableViewの例)
- [Swift3]SegmentedControlでViewを切り替える
- SwiftLintのバージョンを固定する方法
- iOS 10.3で追加されたアプリレビューを投稿できるSKStoreReviewControllerを試してみた
- UIViewControllerAnimatedTransitioning によるカスタムトランジション
- CFSocket を使って iOS で socket 通信した話
- 画像をダブルタップとピンチイン・ピンチアウトで拡大・縮小する Swift3編
-
iOS 10.3からアプリ内レーティングが可能に!- SKStoreReviewController – - Swift3による自動更新購読のアプリ内課金(In-App Purchase)の実装 for 月額課金
- 【Swift】AppStoreのレビューフォームを開くパラメーターが新登場 – action=write-review –
- Swift EvolutionのAcceptedステータスまとめ (2017.1.27) – Protocol-oriented integers, Permit where clauses to constrain associated types, …etc
- 【Swift3.0】xibを使ったカスタムビューの作り方
- iOSの審査に関するまとめ
- Apple Watch に Core Motion を使って色々なデータを取得して表示させてみる
- [macOS]macOS Cocoaプログラミングをはじめる
- 値渡しと参照渡しと参照の値渡しと
- 【iOS】UILabelにPaddingをつける(Swift3対応)
- 【AutoLayout】UILabelの幅を文字列の長さに合わせて可変にする方法
- 【Swift3】ToDoアプリを作る【CoreData】
- Cloud9で始めるServer Side Swift (Vapor)
- UIViewPropertyAnimatorの基本まとめ②
- #Swift fastlaneの次に来る? Sourcery🔮でメタプログラミングする
- iOSのMetal Performance Shadersでニューラルネットを実行する際のモデルの渡し方
- iOSアプリ開発時に使う小ネタ
- 2点間の距離計算 (C, Clojure, Go, Haskell, Java, LOGO, OCaml, Ruby, Rust, Scratch, Swift)
- SwiftでBase64 Encode / Decodeする
- Today Extensionについて
- [Swift 3.0] ViewController単位で画面の向きを制御する
- RxSwiftでの実装練習の記録ノート(後編:DriverパターンとAPIへの通信を伴うMVVM構成のサンプル例)
- swiftのguardを使ってみる
- 乾電池型IoT”MaBeee”を使って簡単なおもちゃ制御アプリを作ってみる
- ReactiveSwift 基本要素の使い方 Signal / SignalProducer編
- [Swift|Kotlin]+Rxで作ったネイティブアプリのコードの違い
- 1行で全てのUIButton(UIView)の同時押しを無効にする
- ありそうでなかったSwiftでのPostリクエストの投げ方
- Swiftによるシリーズアプリの共通部分を切り出して一括管理する
- [iOS] Google VR SDK の代替案としての MetalScope
- iOS アプリの Unit Test – Swift 編
- Swift で Quadratic Bezier 曲線の長さを計算する
- 脳波でスカートをめくるP-WAVEを開発した話
- 今更聞けない?Struct と Class の使い分け方(補足)
- わーい、すごーい
- Swiftで「サクッと使える」通信ライブラリ【RapidFire】
- Swift GPUImage2で画像の平均色を抽出する
- 【Swift x PHP】iOS端末とPHPサーバでHTTP通信
- Auto Layout を使わずにわかりやすいコードでレイアウトが組めるフレームワーク NotAutoLayout
- Carthageで管理しているライブラリのライセンスをまとめてくれるスクリプト
- 【Swift 3】処理の完了を待ってから後続処理を行う
- 【Swift x Ruby】iOS端末とRuby on RailsサーバでHTTP通信
- Swift:UIColorを16進数カラーコードで初期化する
- SwiftLintのRules全まとめ
- Xcode 不使用リソースを抽出する
- ios10 シャッタースピードやホワイトバランスなどiPhoneカメラに特殊機能を実装する方法
- RxSwiftとAlamofireとObject Mapperで
- UITextViewにタップ可能なリンクを挿入する
- web制作者にもわかる、Swift 3が++と–を削除した理由
- iOSのMPSCNNに渡すモデルパラメータのフォーマット / TensorFlowからの書き出し
- [Swift] 三角形のボタンを作る
- [Swift]segueを使った画面遷移、segueを使わない画面遷移
- SwiftにおけるMethod Dispatchについて
- ZIG SIMが送信するセンサの種類とデータ構造について
- TwitterAPIとSwiftを使ってiOSアプリを作ろう! – 前編 – #dotsgirls
- TwitterAPIとSwiftを使ってiOSアプリを作ろう! – 後編 – #dotsgirls
- TwitterAPIとSwiftを使ってiOSアプリを作ろう! – カスタマイズ編 – #dotsgirls
- Google画像検索APIをiOSで利用する
- Apple公式のiPhoneアプリチュートリアルやってみた①(Build a Basic UI)
- Swift3対応をこれからする人が役立ちそうなこと!
- tableView(_:didSelectRowAt:)にて、modalなSegueをperformSegueすると描画が遅い件は、DispatchQueue.main.asyncで解決できる
- Custom URL Schemeでアプリ内の任意のページを表示する
- Swiftの強力な機能であるstaticメソッド制約の紹介と、Kotlin, TypeScript, Java, Scala, C++との比較
- [iOS] Swift + Kannaでtableをスクレイピング
- 激安NSAttributedString
- lazy var の遅延参照?という面白い動き
- UIButtonの画像をUIButtonに対してAspectFitする方法
- iOS の時間関数の精度
- [Swift] count == 0 より isEmpty を使うべき理由
- [swift] InterfaceBuilderで多角形のボタンを作る
- Swift3で消費型アプリ内課金(In-App-Purchase)を実装してみた
- UIStackViewを使って詳細ページの実装をシンプルにする
- Swift 3のUIScrollViewでカルーセルUI(ページング/画像などをスワイプで行き来できるView)をつくる
- iOS(Swift)でアラート画面のようにViewの背景透過をする
- Concourse CIでiOSアプリのCIを行う
- VO, DTO, POSO, DAO, Entity の違い
- Swift の文字列の長さ
- モックオブジェクトをより便利にする (Try! Swift2017)
- よくわかるUIGestureRecognizerDelegate
- Swift,iOS,Xcodeの設計についてメモ
- サーバサイドSwiftの現状考察 #tryswiftconf
- Try! Swift 2017のまとめ
- TableViewで複数セルを一気に複数削除する
- 新しいアプリを作るときによく使うSwift Extension集
- ユニットテストにも可読性を持たせる (Four Phase Test)
- try! Swift 2017 Tokyo 参加レポート
- UIImageのリサイズ方法と注意点
- 始めて見よう! SwiftでWebアプリ開発(基礎編)
- try! Swift Tokyo 2017 に参加した&リンク集
- 2017年におけるObjective-Cコミュニティの動向
- SpriteKitの衝突処理について(categoryBitMask collisionBitMask contactTestBitMask 使い方)
- 猿がようやく理解したSwiftのdeferの使いどころ
- [Swift3.0][iOS][SpriteKit]SpriteKitでテキストを落としたり、ぶっ飛ばしたりした!
- SwiftはどのようにJavaの検査例外を改善したか
- iOS 10以降のNotificationの基本
- 猿がついに理解できたSwiftのthrow・do・try・catchの意味
- print()やNSLogを書かずにconsoleにメッセージを出力する方法
- 猿がもがきまくって理解したSwiftのデリゲート(Delegate)という仕組み
- iOS Test Night_#3に参加できず悲しいのでまとめた
- iOS Test Night #3
- Firebase Cloud Messagingで画像つきプッシュ通知を送信する
- TypeというiOS用のMarkdownエディタを作った
- iOSのCIにモバイル向けCI、Bitriseを導入する(CocoaPods, GitHub Privateレポジトリ対応版)
- RxSwift 3.3.0で追加された3つのUnit(Single, Maybe, Completable)
- CAMPFIRE iOS #1 参加レポート
- 【iOS】ViewControllerを汚さずにUITableViewの下部からCellを追加する実装
- [Swift3]文字列が人名かどうかをバリデーションする方法
- [Swift] シンプルなカウントアップでSequenceに強くなる
- LINQライブラリまとめ
- iOSのBackground Transferがよくわからない人用に整理した
- [iOS][Swift]UITableViewのヘッダーとフッターを設定する方法
- 【Git】 project.pbxprojのコンフリクトの直し方
- Swiftのmutatingキーワードについて
- アプリにApple Payを導入する – 商品購入のハードルを下げる –
- Swiftでasync/awaitな書き方もできるPromiseライブラリHydra
- Playground で Carthage ライブラリを import する
- キリスト教の牧師(見習い)からWebエンジニアに転職した話
- [Swift 3.0]NSLayoutAnchorを用いたコードによるAuto Layout
- 俺の俺による俺のための iOS プログラム設計
- 和暦に関するメモ
- Interface Builderとソースコードで共通のカラーパターンを利用する方法
- SwiftyBeaver でアプリケーションログをメール送信しデバッグしやすい環境を作る
- [MVVM] kickstarter/ios-oss での画面遷移のやり方
-
図解 MemoryLayout
で解き明かす型のメモリー構成 - iOSのビルド高速化 7つの方法
- LINE NEWS風のUI実装
- カメラロールのようなシンプルなイメージピッカーの作成
- swiftLint 導入
- Instagramライクなパン/ピンチ操作できるイメージビューの作成
- 【iOS】アプリ内からレビューを依頼する 10.3未満も対応
- How to write basic UnitTestsを発表した際の参考資料
- Swiftのstatic、classキーワードの違いについて
- 心がHomebrewで旧バージョンのパッケージを入れたがってるんだ
- ただ画像を右側に表示したいだけなんだ・・・ – UIButtonの画像を右寄せにする
- 制約の修正なしで縦方向に要素を追加可能なビューの作成
- Storyboard上で置いてるUIVisualEffectViewのブラーのかかり具合を調節、アニメーションさせる [iOS10]
- DI(Dependency Injection)の概要
- SwiftRater を使って好きなタイミングで SKStoreReviewController を表示する
- Swift 3.1で死んだコードまとめ
- クラスや変数の名前とかを安易に英語にしたい
- Swiftのprint()をファイル名、行数、関数名を出力して分かりやすくする
- iPhoneのSFSpeechRecognizerとAVSpeechSynthesizerと発泡スチロールでボスっぽいなにかを作る
- [Swift] 順序付き辞書、DictionaryLiteral
- iOS/Androidアプリを突貫工事で開発するときにやって良かった!悪かった!こと まとめ
- APIKitでRequestのイニシャライザに渡すのはValueObjectやEntityじゃないほうが良い
- iOS 実装サンプルアプリ集
- iOSアプリを新規開発するときに最低限守っていること
- 日付関連クラスのまとめ(Swift3)
- Foundation.Operationの並列オペレーションがよくわからない人向けの説明
- Swiftで複数のフラグを管理するためにOptionSetを使うと便利だった
- Swiftで連番画像のコマ送りアニメーション
- Swift3でJSONパースを行う
- Swift3.0 NavigationBarをhidden(隠す)方法
- iOS 10 での通知処理について
- 【Swift3】RxSwift + APIKit + Himotokiで作るAPIクライアント
- Swiftでいい感じのViewModelを作るためのメモ
- IGListKitとRxSwiftを掛け合わせてみた🤔
- UIFeedbackGeneratorの使い方と便利に使えるライブラリ
- カスタムViewをNibから初期化の最新版
- 【iOS】ステータスバーのカスタマイズ【Swift3.0】
- carthageのバージョンを今すぐ上げよう
- SwiftでContainerViewを使ってみる
- Swift の struct の stored property は var にしよう
- アイコンを申請なしで変える(iOS10.3)
- SwiftのOptionalのベストプラクティス
- Vapor as a web framework
- モックを「差し込む方法」を考える
- Swift4で何がかわりそうなのか
- Swift Standard Library相関図を作ってみた・眺めてみた #swtws
- iOS(Swift)から3ステップでMastodonに投稿を行う
- なぜDelegateをプロパティに持つとweakを指定しなければいけないの?
- SlideMenuControllerSwiftの使い方とカスタマイズ
- あとで読むQiitaリーダーアプリをリリースしました
- null安全な言語でも、バグ検知を怠れば安心ではない
- 【ネタ】Swift で Sleep Sort
- [Swift] enumerated()はindexを返さない
- [SiriKit]Siriから料金の支払いをする
- 2017年版Realmのエレガントな使い方
- Swiftのプロパティとinout参照を組み合わせたときの挙動が面白い
- swiftのenumでStringがカスタムなときでもenumを諦めない
- 【iOS】Fluxを利用して画面遷移を制御する
- Swift3.0 で CoreGraphicsを使って見る
- Debug Memory Graph で Memory Leak を調査する
- Apple公式のiOS開発チュートリアルをやってみた(Andoridエンジニア視点)
- Swift 4の新しいreduceが素晴らしいので紹介する
- Alert,ActionSheetの表示処理がスッキリかけるUIAlertControllerのラッパーライブラリを作りました
- 体系的なSwift言語学習
- AppのIconからVersionやCommitを判別して混乱を防ぐ
- IB上で範囲の確認ができるUIButtonのタップエリアを拡大する実装
- RxSwift 用語解説
- 【Swift3.0】Alamofireで画像&パラメータを送信
- Swift Package Manager(SwiftPM)で作ったコマンドラインツールをHomebrewに登録する方法
- SwiftではバージョンはStructにして演算子オーバーロードで比較したらどうでしょう
- Swift3ではKVOにkeyPath()式を使っていくのが便利
- やはりお前らのboundingRectWithSizeは間違っている
- SwiftでTestコード(on Xcode)
- `typealias My` をススメようと思ったけどやめた件 あるいはstatic methodをどうやって呼ぶか
- Firebase iOS SDKが刷新されましたよっていう話し
- [2017年版]RxSwift + Alamofire + ObjectMapper + RealmのSwift実装について
- BluetoothをもちいたiOS同士の通信
- Rails 5 Action CableチャットアプリのiOSクライアント側を作る
- SpeakerDeckのスライドをPDF形式で表示できるiOSアプリを作った
- SwiftでArrayがnilか0件の場合に共通の処理を動作させたい時はnil結合演算子を使う
- fastlaneを導入してビルドを楽にする
- 10年間の iOS 機能のまとめ with WWDC
- 書評:Swiftの各機能が「なぜ」存在し「いつ」使うべきかを解説した技術書 – Swift実践入門
- iOS SwiftでBLEのサンプルを動かしてみる
- Swift3で日付(Date)の比較が超簡単になっていた件
- AppleWatchKitとSpriteKitでスペースシューティングゲームを作ってみた
- 様々な言語でMap, Filter, Reduceを実現してみた(1)
- アニメーション付きのボタンを実装するためのテクニック
- Setを使いこなしたい(願望)
- AVFoundationで動画のリアルタイム合成
- Swiftガイドライン的な
- 位置情報アプリ開発者必見!Energy Efficiency Guide for iOS AppsのReduce Location Accuracy and Durationを読んでみた
- Instagramのような画面UIを簡単に作れるPastelViewを試してみた
- Swift API デザインガイドライン
- インタフェースと型クラス、どちらでもできること・どちらかでしかできないこと
- サーバーレスサーバーサイドSwiftとHexaville
- RxSwiftのExamplesにしれっと入ってる双方向データバインディングの演算子がイケてた
- Swift3対応をしてハマった不具合
- iOSでアプリ間でデータをやり取りするためのNの試行
- Embedded framework使用時の肥大化問題
- Swiftならメモ化も最高にスッキリ書けます
- 詳解! ios-Charts
- Instagramのログイン画面みたいなグラデーションのアニメーションを自分で作る
- Swiftのmap, filter, reduce, etc…は(あたりまえだけど)for, if, switch, whileからできている
- iOS11で新しく導入されたFramework
- pageViewController(_: viewControllerBefore:) および pageViewController(_: viewControllerAfter:) が呼び出されるタイミングについて(UIPageViewController)
- What’s New in iOS11まとめ (Metal2以外)
- Xcode による iOS 開発で秘匿したい情報をどう管理するか
- iOS11のCoreNFCを使う
- 公式ドキュメントを追いながらARKitを試してみよう
- Swiftでクラス名や関数名等をログ出力する
- 【iOS 11】開発者ドキュメントから見る iOS 11 の新機能 #WWDC17
- WWDC2017で更新されたサンプルコードまとめ
- Xcode8のDebug Memory GraphでCFArrayのメモリリークの原因を探る
- ARKitを触ってみよう 〜第1話〜
- Swift4.0 で追加される Codable
- 引っ張って閉じることができるモーダルを実装する (UINavigationControllerの場合)
- iOS11 Swipe Actions
- [iOS][Swift3] ニュース系アプリのユーザインタフェース PageMenuKit の実装
- Swift4 CodableでJSONが扱いやすくなる?
- Swift4のCodableでフラットなJSONからネストしたオブジェクトにデコードする
- Swift4のCodableでISO8601の日付をデコードする
- 【iOS 11】【Core ML】pip install coremltools でエラーになった場合の対処法
- Swift4のJSONDecorderは、Date等のパース方法をカスタマイズできるみたい
- ReactorKit(Flux + Reactive Programming)を学ぶ1 入門編
- ReactorKit(Flux + Reactive Programming)を学ぶ2 基礎編
- Swift4のCodableに対応した、独自のDecoder(CSVDecoder)を実装してみよう
- 個人開発アプリでSwift 4に一足先に対応しました
- [Xcode 8] Swiftのドキュメントコメントについての簡潔なまとめ
- init の名は。
- iOS11で追加されたDeviceCheckについて
- SwiftでWebRTC実装ハンズオン 事前準備編
- iOS11で発表されたMusicKitについて
- iOS 11 WKWebView 3大新機能 (WWDC 2017)
- NSURLSessionがメモリリークしてしまうのをなんとかした
- Array や Dictionary にもモダンでオシャレな extension を実装する
- 99%くらいのSwiftプログラマーが使わないであろう演算子の話
- UIKitのView表示ライフサイクルを理解する
- 【まとめ】What’s New in Testing【WWDC 2017】
- Swift PlaygroundsでXcode projectのコードを動かしてみよう
- WWDC17で新しく発表された画像フォーマットHEIFを使ってみた
- iOSでの各回転検知方法とその結果
- SwiftでWebRTC実装ハンズオン 本編
- [Swift4] privateにextensionからアクセスできる
- iOS – CrashlyticsのrecordErrorでカスタムログを送る
- コードを一行も書かずにHello Worldする方法
- 画像を表示する際にモアレ(干渉縞)を抑制する方法
- APIKitとCodableでAPIクライアントを作る
- CodingKeyで、case名のcamelCase ⇄ stringValueのsnake_case を自動で変換する
- 【まとめ】Engineering for Testability (前半)【WWDC 2017】
- 【まとめ】Engineering for Testability (後半)【WWDC 2017】
- 【Swift】iOSで放置型育成ゲームを作りたい(0)
- [Swift]初心者のためのSwiftチートシート
- Swift Package Manager (SwiftPM) Version 4 概要
- ARKitで豆腐作り
- Swiftで複数の非同期処理の完了時に処理を行う
- 【Swift】hogehoge.delegate = self は何をしているのか。
- cmdshelfによるスクリプト運用のご提案
- mbed × BLE × iOSでとりあえず通信したい人のための記事
- iOS LINEでログイン(Line SDK)
- Swift:UserDefaultsで初期値を設定する方法
- ios, androidのローカライズファイルを共通管理して幸せになった話
- 型システムの理論からみるSwiftの存在型(Existential Type)
- Swift4.0でDictionaryが大幅にパワーアップした
- Swift4で変更されたStringAPIをいくつか試してみた
- NO MORE ビルド時間泥棒 ☕️❌【RxSwift編】
- 新規アプリサービスのためのログ実装とサービス選定
- SwiftのSelfキーワードの使い方まとめ
- [Swift]WinでやるC#erの為のSwift基本文法覚書
- Decoder, DecodingContainerの、デコード先の型を推論させたい!明示的に指定したくない!
- XCode9で追加されたAsset catalogsのNamed colors supportについて
- Swiftのattributeまとめ[Swift4対応]
- SwiftでもKotlinのif式を使いたい
- iOSでBeaconの振る舞いを確認する
- はじめてのSwiftアプリ制作4: StoryboardとAuto Layoutその1
- Swift4でのSingletonを用いた共通データの値渡し
- ルートが配列のJSONをCoadableでカスタムモデルにマッピングする
- iOSで楽にデバッグメニューをつける
- 【Swift】TextFieldのキーボードを閉じる方法3選
- SwiftとKotlinの文法を比較してみた(基礎パート)
- [コピペで使える]swift3/swift4でリアルタイム顔認識をする方法
- Swift4 [SE-160 Limiting @objc inference] 概要
- SwiftのExtensionによるクラス分割
- Xcodeのビルド待ちで消耗してたので見直したら50%以上削減できた話
- Swiftコンパイラ開発環境構築
- (初心者向け)Flickr Apiを使って画像を引っ張ってくる(1)
- Equatableとは?(swift) ~ object同士を比較できるようにしよう〜
- Swift4のCodableが内部で何をやっているか確認する
- SwiftでアプリのCPU使用率とメモリ使用量を取得する
- できるだけプログラムっぽくないプログラミングへの挑戦(Swift編)
- MVVMをベースにしつつCleanArchitectureを取り入れてみた
- GCD(Grand Central Dispatch)でキューの順番とスピードを制御する方法(1/2)
- GCD(Grand Central Dispatch)でキューの順番とスピードを制御する方法(2/2)
- Swiftコンパイラのテスト環境
- できるだけプログラムっぽくないプログラミングへの挑戦(Objective-C編)
- 特定のアプリがインストール済みかチェックする
- 「PythonとSwiftは結構似ている」説の検証
- ARKitでタップした座標を検出する方法
- Aspect Fill, Aspect Fit, Scale to Fillの違い
- APIKitとCodableとの連携
- ストーリーボードでUIを綺麗にレイアウトするネタ集
- Swift コンパイラのアーキテクチャ
- 最近Swift書いていて可読性を上げるために意識していること
- Swiftで静的DI(Mixin-Injection)
- UIImageView で cornerRadius と Shadow を同時に使いたい
- Enigmaの実装
- iOS開発でClean Architectureを採用した際のイイ感じのディレクトリ構成とは
- iOS開発で導入しているライブラリの一言説明
- 【メモ】Xcode9ビルドでCarthage経由で導入したライブラリに関してswift version errorが発生
- インクリメンタルサーチ【RxSwift/RxCocoa編】
- CotEditor を Swift に移行する
- Swift3.0でアニメーション1 ~ Animate()メソッド編~
- Swiftに息づくstructural types(構造的型)
- メソッドのhookは正しいタイミングで行おう【RxSwift/RxCocoa】
- 実践Swiftコンパイラ #swtws
- 純粋値型Swift
- Mac: 開発向け厳選ツール群(18/6/23更新)
- Swift でアニメーションの連続実行をしてみる話
- SwiftでiOS脱獄チェック
- iOSの機械学習フレームワークの比較 – Core ML / Metal Performance Shaders (MPSCNN) / BNNS
- 【初心者向け】Core Dataの使い方と説明swift3.0
- RxSwiftのshare*の早見表
- Swiftの有名画像キャッシュライブラリを比較してみた
- indexがArrayの範囲内かチェックする色々な書き方
- 【iOS】fastlaneでipaファイルを作成して、fabric crashlyticsでベータ版を配布し、Slackで完了通知を行う
- iOSアプリでMockを使ってUnitTestを書く
- iOSアプリでCIを始めようとサービスを調べた
- StubとSpyを使ってiOSのUnitテストを書いてみた(Clean architecture)
- RSKImageCropperの使い方とカスタマイズ
- アプリ内での Touch ID を利用したユーザ認証
- Automatically manage signingとxcconigで超効率化
- Instagram APIでOAuth認証する (Swift3版)
- [Swift] MainThreadで処理を実行する
- Carthage updateとCarthage bootstrapの違い
- iOSのクラッシュログをSymbolicate(復元)して解析する
- やさしいSwift単体テスト~テスト可能なクラス設計・前編~
- やさしいSwift単体テスト~テスト可能なクラス設計・後編~
- [iOS][Swift 4] CodableでJSONのパース
- [Swift] Dictionaryをこねくり回すネタ集
- Screen Recordingの録画開始・停止を取得する
- TDD ✕ Property-based Testing (SwiftCheck) で数学パズルを検証してみる
- 超効率化外伝: xcconfigの便利なところ&設定例
- Swiftで花火を作った話
- SwiftでHigher Kinded Polymorphismを実現する
- 絶対にやってはいけない「Apple IDをテストで13歳未満にすること・・」
- Spajam2017優秀賞「嫌われAIの命名」の発想プロセスからiOSアプリ実装まで
- RxSwift `a.withLatestFrom(a)` 同じ上流元の同期的合流問題
- Range が Codable に適合してなかったので後付けで適合させてみる話と、Codable のエラーハンドリングについて
- Swiftのfinalについて
- 【Swift】Dateの王道 【日付】
- サーバーレスとiOSアプリの連携 〜IBM Cloud Functionsを使ってサーバーサイドSwiftで試してみる
- ARKitで立方体の6面それぞれに異なるテクスチャを貼る方法
- iOSでlottie-iosを使ってリッチなアニメーションを簡単に実現してみる
- Swift の class の mutating func とは何か
- 余計なOptionalはやめてくれ
- 【プッシュ通知】Y Combinatorも投資するOneSignalがFirebaseより便利で素晴らしかった
- 【swiftエラー】clang: error: linker command failed with exit code 1 (use -vto see invocation)
- RxSwiftでwithLatestFromが最新じゃなくなることがあるorz
- RxSwiftについてようやく理解できてきたのでまとめることにした(1)
- RxSwiftについてようやく理解できてきたのでまとめることにした(2)
- RxSwiftについてようやく理解できてきたのでまとめることにした(3)
- RxSwiftについてようやく理解できてきたのでまとめることにした(4)
- iOSの消耗型課金のサーバーサイドTipsまとめ
- iOS 11 WKWebViewで広告などのコンテンツブロックをする
- タブスワイプで画面を切り替えるメニューUI
- 【MacOS】スクリーンレコーディング 【Swift】
- Codableについて色々まとめた[Swift4.x]
- FirebaseStorageの画像をSDWebImageで表示しようとして詰まった話
- [Swift] classにEquatableを実装するのは一筋縄ではいかない(ことがある)点に注意。
- ニュースアプリでAPIの記事をRealmにキャッシュして有効期限内だったらそれを表示する
- Swiftで参照型の値から生ポインタを作る方法
- Swift の protocol における Interface と Method の違いを理解しよう
- ARKitを扱う際の心構えとTips
- 脱Storyboardのすすめ
- Firebaseでアプリを開発するならClient Side Joinを前提にすること
- UILabelの文字色をグラデーションさせる
- [Swift3]ローカル通知の実装方法
- [Swift]iOSのデフォルトの関数を活用した Validation String Extension集
- Closureって美味しいの?
- [Swift3]アプリ内でレビューを依頼する
- プッシュ通知設定画面へ遷移させる為の実装
- iOS9, 10 WKWebView – Cookie操作
- iPad対応アプリを開発するときに、UI周りで気をつけることをまとめてみた
- Swift 4 で「プラマイ」範囲を作る
- ぼくのやっているVIPER(のようなもの)
- MVVMを勉強するときに参考になったリンク集 & 概要まとめ
- Xcode8でiOS11beta端末の動作確認がしたい(iOS11実機ビルドしたい)Could not locate device support files.エラー
- iOS11のバグ修正を行うに当たって気になったレイアウト関連の変更点(contentInsetAdjustmentBehavior, SafeAreaLayoutGuide)
- iOS 11 UITableViewでcontentOffsetを使ったスクロールが上手くいかない
- iOS 11 の Safe Area は Auto Layout だけでなくコードベースでも取れる
- iOS向けfastlaneアクションまとめ
- iPhone Xをネイティブ解像度から判定する
- Swift4 Stringのsubstring周りが変わっていた
- iPhone X Human Interface Guidelinesの要点
- iOSDC 2017 まとめ
- RxSwift の Observable とは何か
- Swaggerで始めるAPI定義管理とコードジェネレート
- iOS11で追加されるScreen Recordingについて
- iOS11 カメラとCoreML(Vision)で画像検出
- Swift初心者が3ヶ月でiOSアプリを公開するまでにやったこと、ハマったこと。
- iOSアプリ開発の全体像
- ARKitのコードによく出てくる4次元行列transformについて
- Swiftの @escaping と weak/unowned の理解
- iOSDC Japan 2017で「Auto Layoutのアルゴリズム」について発表しました
- Setは遅いのか
- iOSDC 2017 でさらっと出てきた Phantom Type さらっとやった話
- 超朗報。Xcode 9でやっとSwiftのリファクタリングが可能に
- iOS11から搭載されるスクリーンレコーディングでの録画を検知する方法
- Swift 4 マイグレーション、またはXcode9対応 メモ
- iOSと人工知能(AI) -GPU並列演算の仕組みと機械学習- というタイトルで、iOSDC2017に登壇しました
- 今度のiPhone Xは我々開発者をどれほど苦しめるのか #okamoba
- ARKitのサンプルコード集「ARKit-Sampler」
- iOS11のTwitter投稿対応(Social.framework → TwitterKit)
- iOS11で Grouped UITableView のセクションヘッダーに余分な高さが出る問題について
- [Swift] UserDefaults に画像を保存するとフリーズした
- iOS11 + Xcode9.0でedgesForExtendedLayoutの値を空にしていると、UITableViewのドリルダウンでアニメーションが崩れる
- 【iOSDC2017】MVC→MVP→MVVM→Fluxの実装の違いを比較してみる
- [Swift4]ARKitで球体をランダムに描画する
- 【Xcode9】ファイルヘッダーコメントをカスタマイズする
- [Swift] CharacterSetはCharacterのsetではありませんよ?
- iOS11のVision.frameworkを使ってみる
- iOS11からViewの一部だけを角丸にすることが簡単になった
- Swift 4の魅力の一面を3行で表す
- [Swift] 読み上げ機能、簡単に使えるライブラリつくったよ
- iOS11.0でUINavigationControllerのTitleViewのタッチイベントが呼ばれない現象について
- PDFKit を使ってみた
- Xcode の Debug Memory Graph が便利
- iOSアプリで紙吹雪を降らして画面を賑やかにする
- iOS 11ファイルAppにDocumentsフォルダを表示して他のアプリと共有する方法
- 【Swift】 画像を3種類も書き出したくないでござる
- Swift4 全予約語 (98語) の解説
- ライブラリを使わずにMV*の話(iOS)~ViewとModelの役割〜
- ライブラリを使わずにMV*の話(iOS)〜MVC, MVP, MVVM〜
- .ipa file を実機にインストールする方法(iTunes 12.7)
- UILabelをNSAttributedStringで文字装飾(Swift 4対応)
- AutoLayoutでiPhoneXのedge-to-edge対応

Androidに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Pythonに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Swiftに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Unityに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。