「The Ultimate Guide to WKWebView」をSwiftUIで実装する #03 - Loading local content -

[「The Ultimate Guide to WKWebView」](https://www.hackingwithswift.com/articles/112/the-ultimate-guide-to-wkwebvie)をSwiftUIで実装してみるの、
３つ目になります。

## 目次

シリーズ化していこうと思うので、全体の目次を置いておきます。
リンクが貼られていないタイトルは、記事作成中または未作成のものになります。

|#|タイトル|
|------|-----------------|
|01|[Making a web view fill the screen](https://qiita.com/kamimi01/items/b4d6cbafa587831c1be0) <br> （WebViewを画面に表示する）|
|02|[Loading remote content](https://qiita.com/kamimi01/items/c889979964820d5f9fa2) <br> （リモートのコンテンツを読み込む）|
|03|[Loading local content](https://qiita.com/kamimi01/items/9cefe1e73ab5b88051c6) <br> （ローカルのコンテンツを読み込む）|
|04|[Loading HTML fragments](https://qiita.com/kamimi01/items/2f267b328f3f6158bc26) <br> （HTMLフラグメントの読み込み）|
|05|[Controlling which sites can be visited](https://qiita.com/kamimi01/items/7c01aed497108da35b5a) <br> （訪問可能なサイトの制御）|
|06|[Opening a link in the external browser](https://qiita.com/kamimi01/items/716dccb6f109ee50c900) <br> （外部ブラウザでリンクを開く）|
|07|[Monitoring page loads](https://qiita.com/kamimi01/items/33446e032822518cb7f7) <br> （ページの読み込みを監視する）|
|08|[Reading a web page’s title as it changes](https://qiita.com/kamimi01/items/7e0153ee68474a155b9a) <br> （Webページのタイトルの変化を読み取る）|
|09|[Reading pages the user has visited](https://qiita.com/kamimi01/items/f5f6c35a6a68726fa485) <br> （ユーザーが閲覧したページを読み取る）|
|10|[Injecting JavaScript into a page](https://qiita.com/kamimi01/items/6076081dc54e2b3d0abf) <br> （JavaScriptをページに注入する）|
|11|[Reading and deleting cookies](https://qiita.com/kamimi01/items/90139fe692ec988d28bb) <br> （cookieの読み取りと削除）|
|12|[Providing a custom user agent](https://qiita.com/kamimi01/items/5884330e315338df8ec0) <br> （カスタムUser Agentを提供する）|
|13|[Showing custom UI ](https://qiita.com/kamimi01/items/3315e4df0813eda75eeb)<br> （カスタムUIを表示する）|
|14|Snapshot part of the page <br> （ページの一部のスナップショットを撮る）|
|15|Detecting data <br> （データの探索）|

## 環境

【Xcode】13.1
【Swift】5.5
【iOS】15.0
【macOS】Big Sur バージョン 11.4

## 実現したいこと

今回やることは
アプリバンドル内にあるHTMLと画像（jpg）を読み込んで表示します。

またHTMLは、同じくアプリバンドル内にあるCSSを読み込んでいます。

今回の成果物は、こんな感じになりました。

![app.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401966/dc16e0c0-e5b7-1497-c259-df71a75a9168.gif)

（またしても画像の下の方が黒い・・・）

「HTML開く」押してから実際に表示されるまでめっちゃ遅いけど、一旦そこは置いとく・・・

---

ちなみにこれをやるのは初めてです。
と言いますか、Swiftでファイル操作関連するのがそもそも初めてかも・・・？

これのメリットってなんでしょう・・・
サーバーと通信しないので表示が早いのでしょうか。（ちゃんと測ってないけど早いのかな？）
HTML変更したら、アプリ再申請してリリースしないといけないのではと思ったけど、更新頻度が低いなら良いのかな？ :thinking: 

と思っていたのですが、前関わったアプリのリプレースの仕事で、
リプレース前のアプリがアプリバンドル内にHTMLを持っていました。

他のHTMLは、サーバーから取得しているのに１つだけアプリバンドル内にありました。
iOSアプリ側で入力した値を元にHTMLにあるJavaScriptの関数を実行して何かをしてたようですが、
それiOS側でやる必要あったのでしょうか。わからん。

まあ何か理由があったということにしましょう。

他にもどういうユースケースでこれが必要になるのか探したのですが、
あまり見つからず・・・今後の経験の中で探していくことにします。

何かこういうメリットあるよ、こういうユースケースで使うよという人がいれば、コメントくださると嬉しいです! :blush:

## 実現方法

まずWebViewです。

今回はHTMLと画像どちらもこのWebViewを使い回すため、
コンテンツのファイル名を外から受け取る形にしました。

`updateUIView()`の処理の前半は、
受け取ったファイル名（index.html）、ファイル名(index)と拡張子(html)に分けて取得しています。

そしてそれぞれを`Bundle.main.url(forResource:withExtension)`の引数に入れます。
（これわざわざわけないといけないのかな・・・もっと良い方法あるかも？？）

次に、`loadFileURL()`メソッドで、引数で渡されたローカルのコンテンツを読み込みます。

Hacking with Swiftでは、
`allowingReadAccessTo`の値は`url.deletingLastPathComponent()`になっていたのですが、
 **アプリバンドル内であれば、ディレクトリパスではなくても、同ディレクトリの他のファイルの読み込みは問題ないのでは**と思っています。

実際同じディレクトリにあるCSSが読み込まれているからです。
HTMLの文字色がオレンジになっているのがその証拠。

「[【WKWebView】loadFileURLの取得元をBundleから変更したら、.jsファイルが読み込まれなくなった！](https://qiita.com/TakadaTentaro/items/44763b89a5bfa2a023f2)」の記事によると、**DocumentsやLibraryで同じことをやろうとすると、読み込めないよう**なので、
そういう場合に`allowingReadAccessTo`にディレクトリパスを指定する必要があるのではと思っています。
（いつか自分でも検証してみる！）

```swift
import SwiftUI
import WebKit

struct WebView: UIViewRepresentable {
    @Binding var assetName: String

    func makeUIView(context: Context) -> WKWebView {
        return WKWebView()
    }

    func updateUIView(_ uiView: WKWebView, context: Context) {
        let fileName = NSString(string: assetName)
        let pathPrefix = fileName.deletingPathExtension
        let pathExtension = fileName.pathExtension
        if pathPrefix.isEmpty || pathExtension.isEmpty {
            return
        }

        if let url = Bundle.main.url(forResource: pathPrefix, withExtension: pathExtension) {
            uiView.loadFileURL(url, allowingReadAccessTo: url)
        }
    }
}
```

次にこのWebViewを表示するViewです。

ちょっと長いですが、最初に表示される画面に配置しているのは、

* HTMLを読み込んで表示するためのボタン
* 画像を読み込んで表示するためのボタン

です。

表示した後、閉じれるように「閉じる」ボタンをつけました。

```swift
import SwiftUI

struct ContentView: View {
    @State private var isShownLocalAsset = false
    @State private var shownAssetName = ""

    var body: some View {
        VStack {
            Spacer()
            showAssetButton(
                assetName: "index.html",
                title: "HTML開く"
            )
            Spacer()
            showAssetButton(
                assetName: "profile.jpg",
                title: "画像開く"
            )
            Spacer()
        }
        .fullScreenCover(isPresented: $isShownLocalAsset) {
            webBaseView
        }
    }
}

private extension ContentView {
    func showAssetButton(assetName: String, title: String) -> some View {
        Button(action: {
            shownAssetName = assetName
            isShownLocalAsset.toggle()
        }) {
            Text(title)
        }
    }

    var webBaseView: some View {
        NavigationView {
            WebView(assetName: $shownAssetName)
                .navigationBarItems(leading: closeButton)
        }
    }

    var closeButton: some View {
        Button(action: {
            isShownLocalAsset.toggle()
        }) {
            Text("閉じる")
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
```

以上です！

コード全体はこちらに上がっています。

https://github.com/kamimi01/TheUltimateGuidetoWKWebViewWithSwiftUI/tree/main/LoadingLocalContent

ファイル操作慣れなすぎて、色々時間かかったのと、いまだに解決していない疑問もあります・・・ :sweat: 

使ったことがないとどういう場合にこれを実装しないといけないのが想像がつかないです...
もっと経験積もうわたし。

## 参考

- [【Swift】URLでパスを扱うときに便利なプロパティとメソッド](https://qiita.com/toshi586014/items/52b87c034e411681ca7b)
- [【Swift】プロジェクト内のファイル読み込み](https://capibara1969.com/2253/)
- [【WKWebView】loadFileURLの取得元をBundleから変更したら、.jsファイルが読み込まれなくなった！](https://qiita.com/TakadaTentaro/items/44763b89a5bfa2a023f2)
- [How to split filename from file extension in Swift?](https://stackoverflow.com/questions/26707352/how-to-split-filename-from-file-extension-in-swift)
- [The Ultimate Guide to WKWebView](https://www.hackingwithswift.com/articles/112/the-ultimate-guide-to-wkwebvie)
- [iOS storage best practices](https://swiftlyanand.medium.com/ios-storage-best-practices-294fca83ad9)