iOS WKWebView|ページ&ファイル資源のPreloadとCache活用で高速表示を実現
WKWebViewのページとファイル資源を事前にプリロード&キャッシュし、読み込み遅延を解消。高速表示でユーザー体験を大幅改善する手法を詳解します。
本記事は AI による翻訳をもとに作成されています。表現が不自然な箇所がありましたら、ぜひコメントでお知らせください。
記事一覧
iOS WKWebView ページおよびファイルリソースのPreload(プリロード)/ Cache(キャッシュ)研究
iOS WKWebView のリソース事前ダウンロードとキャッシュによるページ読み込み速度向上の研究。
写真提供:Antoine Gravier
背景
なぜか「Cache(キャッシュ)」とは縁が深く、以前はAVPlayerの「iOS HLS Cache 実践方法探究の旅」や「AVPlayer 実践ローカル Cache 機能大全」の研究と実装を担当していました。ストリーミングキャッシュは再生の通信量を減らすことが目的ですが、今回はIn-app WKWebViewの読み込み速度向上が主なミッションです。その中でWKWebViewの事前読み込みとキャッシュの研究も含まれています。ただ正直に言うと、WKWebViewのシナリオはより複雑です。AVPlayerのストリーミング動画は一つまたは複数の連続したチャンクファイルに対してキャッシュすればよいのに対し、WKWebViewはページ本体のファイルだけでなく、読み込むリソースファイル(.js、.css、フォント、画像など)もあり、ブラウザエンジンを介してページをレンダリングしユーザーに表示します。この間、アプリが制御できない部分が多く、ネットワークからフロントエンドのJavaScriptのパフォーマンスやレンダリング方法まで、多くの時間がかかります。
本記事はiOS技術の実現可能性を検証したものであり、必ずしも最終的な解決策ではありません。総合的に見ると、この課題はフロントエンド側で対応する方が効果的です。 フロントエンドの皆様には、ファーストコンテンツフルペイント(First Contentful Paint)の最適化とHTTPキャッシュ機構の充実をお願いします。これにより、Web/mWeb自体の高速化だけでなく、Android/iOSのIn-app WebViewの速度向上にもつながり、さらにGoogle SEOの評価も高まります。
技術的詳細内容
iOS の制限
Apple Review Guidelines 2.5.6 によると:
ウェブを閲覧するアプリは、適切なWebKitフレームワークとWebKit JavaScriptを使用する必要があります。アプリ内で別のウェブブラウザエンジンを使用するための権利を申請することができます。これらの権利について詳しくはこちら 。
アプリ内では Apple 提供の WebKit フレームワーク (WKWebView) のみ使用可能で、サードパーティや改変された WebKit エンジンの使用は許可されていません。これに違反すると審査通過できません。また、iOS 17.4 以降、規制遵守のため、EU地域では Apple の特別許可を取得すれば 他のブラウザエンジンを使用可能 となります。
Appleが許可しないものは、私たちにもできません。
[未検証] 調べたところ、iOS版のChromeやFirefoxもAppleのWebKit(WKWebView)しか使えないようです。
もう一つとても重要なことがあります:
WKWebViewはアプリのメインスレッドとは別の独立したスレッドで動作するため、すべてのリクエストや操作はアプリを経由しません。
HTTP Cache フロー
HTTPプロトコルにはCacheプロトコルが含まれており、ネットワークに関わるすべてのコンポーネント(URLSession、WKWebViewなど)でシステムがすでにCache機構を実装しています。そのため、クライアントアプリ側で独自のCache機構を実装する必要はなく、推奨もされません。HTTPプロトコルに従うことが最も速く、安定し、効果的な方法です。
HTTP Cache の大まかな動作フローは上図の通りです:
Clientがリクエストを送信する
サーバーのレスポンスキャッシュ戦略はResponse Headerにあり、システムのURLSessionやWKWebViewはCache Headerに従って自動的にレスポンスをキャッシュし、後続のリクエストにも自動的にこの戦略を適用します。
同じリソースを再度リクエストする場合、キャッシュが有効期限内であれば、メモリやディスクからローカルキャッシュを直接読み込み、Appに応答します。
もし期限切れの場合(期限切れ=無効ではない)、実際のネットワークリクエストをサーバーに送信し、内容が変更されていなければ(期限切れでも有効)、サーバーは直接304 Not Modified(空のボディ)で応答します。実際にリクエストは送信されますが、基本的にミリ秒単位の応答でレスポンスボディもなく、ほとんど通信量の消費はありません。
内容が変更された場合は、再度データとキャッシュヘッダーを提供してください。
キャッシュはローカルキャッシュだけでなく、ネットワークプロキシサーバーや経路上にもネットワークキャッシュが存在する場合があります。
よく使われる HTTP レスポンスキャッシュヘッダーのパラメータ:
1
2
3
4
5
expires: RFC 2822 日付
pragma: no-cache
# 新しいパラメータ:
cache-control: private/public/no-store/no-cache/max-age/s-max-age/must-revalidate/proxy-revalidate...
etag: XXX
よく使われる HTTP リクエストキャッシュヘッダーのパラメータ:
1
2
If-Modified-Since: 2024-07-18 13:00:00
IF-None-Match: 1234
iOSではネットワーク関連のコンポーネント(URLSession、WKWebViewなど)がHTTPリクエスト/レスポンスのキャッシュヘッダーを自動的に処理し、キャッシュを管理するため、キャッシュヘッダーのパラメータを自分で操作する必要はありません。
より詳細な HTTP Cache の動作詳細は「 Huli 大大が書いた段階的に理解する HTTP Cache の仕組み 」をご参照ください。
iOS WKWebView 総覧
iOSに戻ると、AppleのWebKitしか使えないため、Appleが提供するWebKitの方法からプリロードキャッシュを実現できる可能性を探るしかありません。
上の図はChatGPT 4oを使って紹介した、Apple iOSのWebKit(WKWebView)に関するすべてのメソッドと簡単な説明です。緑色の部分はデータ保存に関係するメソッドです。
みなさんにいくつか面白い方法を共有します:
WKProcessPool:複数のWKWebView間でリソース、データ、Cookieなどを共有できます。
WKHTTPCookieStore:WKWebViewのCookieを管理でき、WKWebView間やアプリ内のURLSessionのCookieとWKWebViewのCookieを共有できます。
WKWebsiteDataStore:ウェブサイトのキャッシュファイルを管理します。(情報の読み取りと削除のみ可能)
WKURLSchemeHandler:WKWebViewが認識できないURLスキームを処理するために、カスタムハンドラーを登録できます。
WKContentWorld:注入するJavaScript(WKUserScript)スクリプトをグループ管理できます。
WKFindXXX:ページ内検索機能を制御できます。
WKContentRuleListStore:WKWebView内でコンテンツブロッカー機能(例:広告のブロックなど)を実現できます。
iOS WKWebView プリロードとキャッシュの実現可能性に関する研究
HTTPキャッシュの最適化 ✅
前述で紹介したHTTPキャッシュ機構と同様に、WebチームにイベントページのHTTPキャッシュ設定を完璧にしてもらい、クライアントiOS側はCachePolicyの設定を簡単に確認するだけで、あとはシステムがすべて処理してくれます!
CachePolicy 設定
URLSession:
1
2
3
let configuration = URLSessionConfiguration.default
configuration.requestCachePolicy = .useProtocolCachePolicy // プロトコルのキャッシュポリシーを使用する
let session = URLSession(configuration: configuration)
URLRequest/WKWebView:
1
2
3
4
var request = URLRequest(url: url)
request.cachePolicy = .reloadRevalidatingCacheData
//
wkWebView.load(request)
useProtocolCachePolicy : デフォルトで、HTTPキャッシュ制御に従います。
reloadIgnoringLocalCacheData : ローカルキャッシュを使用せず、毎回ネットワークからデータを読み込みます(ただしネットワークやプロキシのキャッシュは許可)。
reloadIgnoringLocalAndRemoteCacheData : ローカルおよびリモートのキャッシュを無視して、常にネットワークからデータを読み込みます。
returnCacheDataElseLoad : キャッシュデータがあればそれを使用し、なければネットワークからデータを読み込みます。
returnCacheDataDontLoad : キャッシュデータのみを使用し、キャッシュがない場合はネットワークリクエストを行いません。
reloadRevalidatingCacheData : リクエストを送信してローカルキャッシュの有効期限を確認し、期限切れでなければ(304 Not Modified)キャッシュデータを使用し、そうでなければネットワークから再読み込みします。
キャッシュサイズの設定
App 全体:
1
2
3
4
5
let memoryCapacity = 512 * 1024 * 1024 // 512 MB
let diskCapacity = 10 * 1024 * 1024 * 1024 // 10 GB
let urlCache = URLCache(memoryCapacity: memoryCapacity, diskCapacity: diskCapacity, diskPath: "myCache")
URLCache.shared = urlCache
個別 URLSession:
1
2
3
4
5
6
let memoryCapacity = 512 * 1024 * 1024 // 512 MB
let diskCapacity = 10 * 1024 * 1024 * 1024 // 10 GB
let cache = URLCache(memoryCapacity: memoryCapacity, diskCapacity: diskCapacity, diskPath: "myCache")
let configuration = URLSessionConfiguration.default
configuration.urlCache = cache
前述の通り、WKWebView はアプリのメインスレッドとは別の独立したスレッドで動作しているため、URLRequest や URLSession のキャッシュは WKWebView と共有されません。
WKWebViewでSafari開発者ツールを使う方法
ローカルキャッシュを使用しているか確認する。
Safariで開発者機能を有効にする:
WKWebView の isInspectable を有効にする:
1
2
3
4
5
func makeWKWebView() -> WKWebView {
let webView = WKWebView(frame: .zero)
webView.isInspectable = true // iOS 16.4以降でのみ利用可能
return webView
}
WKWebView に webView.isInspectable = true を設定すると、Debug Build 版で Safari 開発者ツールを使用できます。
p.s. これは私が別に作成したWKWebView用のテストプロジェクトです
webView.load の箇所にブレークポイントを設定してください。
テスト開始:
ビルド&実行:
webView.load のブレークポイントに到達したら、「ステップ実行」をクリックしてください。
Safariに戻り、ツールバーの「開発」->「シミュレーター」->「あなたのプロジェクト」->「about:blank」を選択します。
ページがまだ読み込みを開始していないため、URLは about:blank になります。
about:blank が表示されない場合は、XCodeに戻り、逐次デバッグボタンを何度かクリックして、表示されるまで続けてください。
該ページに対応する開発者ツールが表示されます:
XCodeに戻り、「続行」をクリックしてください:
再びSafariの開発者ツールに戻ると、リソースの読み込み状況や完全な開発者ツール機能(コンポーネント、ストレージのデバッグなど)が確認できます。
ネットワークリソースにHTTPキャッシュがある場合、転送サイズは「ディスク」と表示されます:
クリックしてもキャッシュ情報が表示されます。
WKWebView キャッシュのクリア
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// クッキーを削除
HTTPCookieStorage.shared.removeCookies(since: Date.distantPast)
// 保存データとキャッシュデータを削除
let dataTypes = WKWebsiteDataStore.allWebsiteDataTypes()
let store = WKWebsiteDataStore.default()
store.fetchDataRecords(ofTypes: dataTypes) { records in
records.forEach { record in
store.removeData(
ofTypes: record.dataTypes,
for: records,
completionHandler: {
print("clearWebViewCache() - \(record)")
}
)
}
}
上記の方法を使用して、WKWebViewにキャッシュされたリソース、ローカルデータ、Cookieデータを削除できます。
しかし、HTTPキャッシュの最適化はキャッシュ部分(2回目以降のアクセスが速くなる)にしか効果がなく、プリロード(初回アクセス)には影響しません。 ✅
完璧な HTTP キャッシュ + WKWebView 全ページプリロード 😕
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class WebViewPreloader {
static let shared = WebViewPreloader()
private var _webview: WKWebView = WKWebView()
private init() { }
func preload(url: URL) {
let request = URLRequest(url: url)
Task { @MainActor in
webview.load(request) // リクエストをロードする
}
}
}
WebViewPreloader.shared.preload("https://zhgchg.li/campaign/summer")
HTTP Cacheを最適化した後、2回目のWKWebViewの読み込みはキャッシュが利用されます。リストやホーム画面であらかじめ内部のURLをすべて読み込んでキャッシュを作成しておくことで、ユーザーがアクセスした際により高速に表示されます。
テストの結果、原理的には可能ですが、性能やネットワークの負荷が大きすぎます ;ユーザーが詳細ページに入らない可能性があるのに、プリロードのためにすべてのページを一度にロードしてしまい、無駄撃ちのような感じです。
個人的には現実的に不可能であり、メリットよりデメリットが大きく、過剰反応だと思います。😕
完善なHTTPキャッシュ + WKWebViewの純リソースプリロード🎉
上記の方法による最適化を基に、HTMLのLink Preload手法を組み合わせて、ページ内で使用されるリソースファイル(例:.js、.css、フォント、画像など)のみをPreloadします。これにより、ユーザーがアクセスした際にキャッシュされたリソースを直接利用でき、再度ネットワークリクエストを送ってリソースを取得する必要がなくなります。
つまり、ページ全体をプリロードするのではなく、ページで使うリソースファイルだけをプリロードします。これらのファイルはページ間で共有されることもあります。ページの.htmlファイルはネットから取得し、プリロードしたファイルと組み合わせてページをレンダリングします。
ご注意ください:ここでは引き続きHTTPキャッシュを使用しているため、これらのリソースもHTTPキャッシュに対応していなければ、後のリクエストは依然としてネットワーク経由になります。
ご注意ください:ここでは引き続きHTTP Cacheを使用しているため、これらのリソースもHTTP Cacheに対応していなければ、後のリクエストは依然としてネットワーク経由になります。
ご注意ください:ここでは引き続きHTTPキャッシュを使用しているため、これらのリソースもHTTPキャッシュに対応していなければ、後のリクエストは依然としてネットワーク経由になります。
1
2
3
4
5
6
7
8
9
<!DOCTYPE html>
<html lang="ja">
<head>
<link rel="preload" href="https://cdn.zhgchg.li/dist/main.js" as="script">
<link rel="preload" href="https://image.zhgchg.li/v2/image/get/campaign.jpg" as="image">
<link rel="preload" href="https://cdn.zhgchg.li/assets/fonts/glyphicons-halflings-regular.woff2" as="font">
<link rel="preload" href="https://cdn.zhgchg.li/assets/fonts/Simple-Line-Icons.woff2?v=2.4.0" as="font">
</head>
</html>
よくサポートされるファイルタイプ:
.js スクリプト
.css スタイル
フォント
画像
Webチームは上記のHTML内容をAppと約束したパスに配置し、私たちの WebViewPreloader はそのパスをロードします。WKWebViewがロードされると同時に、<link>のpreloadリソースが解析されキャッシュが生成されます。
1
2
3
WebViewPreloader.shared.preload("https://zhgchg.li/campaign/summer/preload")
// または一括で
WebViewPreloader.shared.preload("https://zhgchg.li/assets/preload")
テストの結果、通信量の損失とプリロードのバランスが良好に取れることが確認できました 。 🎉
欠点は、このCacheリソースリストのメンテナンスが必要であり、さらにWebページのレンダリングと読み込みの最適化も必要なことです。そうしないと、最初のページ表示までの体感時間は依然として長くなります。
URLProtocol ❌
また、私たちの古くからの友人である URLProtocol を思い出しました。URL Loading System に基づくすべてのリクエスト(URLSession、openURLなど)は、すべてインターセプトして操作することが可能です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class CustomURLProtocol: URLProtocol {
override class func canInit(with request: URLRequest) -> Bool {
// このリクエストを処理するかどうか判断する
if let url = request.url {
return url.scheme == "custom"
}
return false
}
override class func canonicalRequest(for request: URLRequest) -> URLRequest {
// リクエストを返す
return request
}
override func startLoading() {
// リクエストを処理してデータを読み込む
// キャッシュ戦略に変更し、まずローカルからファイルを読む
if let url = request.url {
let response = URLResponse(url: url, mimeType: "text/plain", expectedContentLength: -1, textEncodingName: nil)
self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
let data = "This is a custom response!".data(using: .utf8)!
self.client?.urlProtocol(self, didLoad: data)
self.client?.urlProtocolDidFinishLoading(self)
}
}
override func stopLoading() {
// データの読み込みを停止する
}
}
// AppDelegate.swift didFinishLaunchingWithOptions:
URLProtocol.registerClass(CustomURLProtocol.self)
抽象的なアイデアは、バックグラウンドでURLRequest -> URLProtocol -> 自分で全てのリソースをダウンロードし、ユーザー -> WKWebView -> リクエスト -> URLProtocol -> プリロードしたリソースを返すことです。
前述の通り、WKWebViewはアプリのメインスレッドとは別の独立したスレッドで動作しているため、URLProtocolではWKWebViewのリクエストをインターセプトできません。
しかし、ブラックマジックを使えば可能なようですが、おすすめしません。別の問題が発生するため(審査で拒否される)
この道は通れません ❌。
WKURLSchemeHandler 😕
AppleがiOS 11で導入した新しい方法は、WKWebViewがURLProtocolを使用できない制約を補うためのように感じられます。しかし、この方法はAVPlayerのResourceLoaderに似ており、システムが認識できないスキームのみが自分たちで定義したWKURLSchemeHandlerに渡されて処理されます。
抽象的なアイデアは同じで、バックグラウンドで WKWebView -> Request -> WKURLSchemeHandler -> 自分で全てのリソースをダウンロードし、ユーザー -> WKWebView -> Request -> WKURLSchemeHandler -> プリロードしたリソースを返す。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import WebKit
class CustomSchemeHandler: NSObject, WKURLSchemeHandler {
func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) {
// カスタム処理
let url = urlSchemeTask.request.url!
if url.scheme == "custom-scheme" {
// キャッシュ戦略に変更し、まずローカルからファイルを読み込む
let response = URLResponse(url: url, mimeType: "text/html", expectedContentLength: -1, textEncodingName: nil)
urlSchemeTask.didReceive(response)
let html = "<html><body><h1>Hello from custom scheme!</h1></body></html>"
let data = html.data(using: .utf8)!
urlSchemeTask.didReceive(data)
urlSchemeTask.didFinish()
}
}
func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) {
// 停止
}
}
let webViewConfiguration = WKWebViewConfiguration()
webViewConfiguration.setURLSchemeHandler(CustomSchemeHandler(), forURLScheme: "mycacher")
let customURL = URL(string: "mycacher://zhgchg.li/campaign/summer")!
webView.load(URLRequest(url: customURL))
http/https はシステムが処理するSchemeであるため、http/https の処理をカスタマイズすることはできません。Scheme をシステムが認識しないもの(例:
mycacher://)に変更する必要があります。ページ内ではすべて相対パスを使用することで、自動的に
mycacher://が付与され、私たちのハンドラーがキャッチします。http/https を変更せずに http/https リクエストを取得したい場合は、ブラックマジックしかありませんが、おすすめしません。 他の問題(審査拒否など)を引き起こす可能性があります。
ページファイルを自前でキャッシュして応答する場合、ページ内で使用される Ajax、XMLHttpRequest、Fetch リクエストは CORS 同一生成ポリシー によって ブロックされる 可能性があります。これを回避するには、サイトのセキュリティを低く設定する必要があります(mycacher:// から http://zhgchg.li/xxx へリクエストを送るため、オリジンが異なるため)。
キャッシュポリシーを自分で実装する必要があるかもしれません。例えば、いつ更新すべきか?有効期限はどれくらいか? (これはHTTPキャッシュが行っていることと同じです)
以上を総合すると、原理的には可能ですが、実装には多大な労力が必要です。全体的に見てコストパフォーマンスが悪く、拡張性や安定性の維持も難しいです 😕
WKURLSchemeHandler の方法は、ウェブページ内に大きなリソースファイルが多くある場合に適していると感じます。カスタムスキームを宣言してアプリに処理を任せ、協力してページをレンダリングします。
WKWebViewのネットワークリクエストをアプリから送信に橋渡し 🫥
WKWebView を App が定めたインターフェース(WkUserScript)に変更し、Ajax、XMLHttpRequest、Fetch の代わりに App がリソースをリクエストする。
このケースはあまり役に立ちません。なぜなら最初の画面表示が遅いのであって、その後の読み込みが遅いわけではないからです。また、この方法はWebとAppの間に過度で奇妙な依存関係を生んでしまいます🫥
Service Workerから始める ❌
セキュリティ上の問題から、Apple純正のSafariアプリのみ対応しており、WKWebViewは対応していません❌。
WKWebView パフォーマンス最適化 🫥
WKWebViewのロードビューのパフォーマンスを最適化・向上する。
WKWebView自体は骨格のようなもので、Webページは肉体にあたります。調査したところ、骨格の最適化(例:WKProcessPoolの再利用)の効果は非常に限定的で、0.0003秒から0.000015秒の違い程度です。
ローカルHTML、ローカルリソースファイル 🫥
Preloadに似ていますが、アクティビティページをApp Bundleに入れるか、起動時にリモートから取得する方法です。
HTML全体のページを配置するとCORSの同一生成問題が発生する可能性があります;純粋にウェブリソースファイルを配置する場合は「完璧なHTTPキャッシュ + WKWebViewで純資源をプリロード」方式で代替できそうです;Appバンドルに入れるとアプリサイズが増えるだけで、遠隔から取得するのがWKWebViewプリロードです🫥
フロントエンドの最適化に着手 🎉🎉🎉
参考 wedevs 優化建議 によると、フロントエンドのHTMLページは4つの読み込み段階があります。最初にページファイル(.html)を読み込み、First Paint(白紙のページ)、次にFirst Contentful Paint(ページの骨組みのレンダリング)、さらにFirst Meaningful Paint(ページ内容の補完)、最後にTime To Interactive(ユーザーが操作可能になる時間)です。
私たちのページでテストすると、ブラウザやWKWebViewはまずページ本体の.htmlをリクエストし、その後必要なリソースを読み込みます。同時にプログラムの指示に従って画面を構築します。記事と比較すると、ページ段階では実際にFirst Paint(空白)からTime To Interactive(First Contentful Paintはナビゲーションバーのみであまりカウントされないと思われます)までしかなく、中間の段階的なレンダリングがユーザーに提供されていません。そのため、ユーザーの全体的な待機時間が長くなります。
そして現在はリソースファイルのみHTTPキャッシュが設定されており、ページ本体には設定されていません。
また、Google PageSpeed Insights の提案を参考に、圧縮やスクリプトサイズの削減などの最適化を行うこともできます。
in-app WKWebViewのコアはあくまでWebページ自体であるため、フロントエンドのウェブページを調整することが効果的な解決策です。🎉🎉🎉
ユーザー体験の改善 🎉🎉🎉
シンプルな実装として、ユーザー体験を向上させるためにLoading Progress Barを追加しましょう。白い空白ページだけを表示してユーザーを戸惑わせるのではなく、ページが読み込み中であることと進捗状況を知らせます。🎉🎉🎉
結論
以上が今回のWKWebViewのプリロードとキャッシュに関する検討のまとめです。技術自体は最大の問題ではなく、重要なのはユーザーに最も効果的で開発コストが最も低い方法を選ぶことです。正しい方法を選べば、少しの調整で目標を達成できますが、誤った方法を選ぶと多大なリソースを無駄にし、後のメンテナンスや運用が非常に困難になる可能性があります。
方法は困難より多いことが多く、時には想像力の欠如が原因です。
もしかすると私が考えていない神レベルの組み合わせもあるかもしれません。ぜひ皆さんのご意見をお寄せください。
参考資料
WKWebViewの純資源プリロード🎉 については以下の動画を参照してください
<iframe class=”embed-video” loading=”lazy” src=”https://www.youtube.com/embed/ZQvyfFieBfs” title=”“Preload strategies using WKWebView” by Jonatán Urquiza” frameborder=”0” allow=”accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture” allowfullscreen></iframe>
また、著者はWKURLSchemeHandlerの方法についても言及しています。
動画内の完全なデモリポジトリは以下の通りです:
iOS ベテランエンジニア週報
老舗ドライバー週報のWKWebViewに関する共有も一見の価値があります。
雑談
久しぶりにiOS開発に関する長文記事を執筆します。
ご質問やご意見がございましたら、こちらからご連絡ください 。
Post Mediumから変換されたもの by ZMediumToMarkdown.
本記事は Medium にて初公開されました(こちらからオリジナル版を確認)。ZMediumToMarkdown による自動変換・同期技術を使用しています。












{:target="_blank"}](/assets/5033090c18ba/1*Y3nDpbc4aEd0wg7Enk4k8A.webp)


