LoginSignup
1
1

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-02-05

「The Ultimate Guide to WKWebView」をSwiftUIで実装してみるの、
3つ目になります。

目次

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

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

環境

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

実現したいこと

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

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

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

app.gif

(またしても画像の下の方が黒い・・・)

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


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

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

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

他のHTMLは、サーバーから取得しているのに1つだけアプリバンドル内にありました。
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ファイルが読み込まれなくなった!」の記事によると、DocumentsやLibraryで同じことをやろうとすると、読み込めないようなので、
そういう場合にallowingReadAccessToにディレクトリパスを指定する必要があるのではと思っています。
(いつか自分でも検証してみる!)

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を読み込んで表示するためのボタン
  • 画像を読み込んで表示するためのボタン

です。

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

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()
    }
}

以上です!

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

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

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

参考

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1