minulo は、アプリの中で m.youtube.com を WKWebView で開き、ショートの棚と下のショートタブだけを見えなくする iPhone アプリです。この記事では、その作り方と、運用してみて分かった壊れやすいところをまとめます。(この記事は minulo の開発者が書いています)
先に結論を書きます。
- 隠すのは CSS が基本で、WKUserScript を
.atDocumentStartで入れる - 後から足される要素と、ページを読み直さない画面の切り替えには MutationObserver で追いつく
- 一番の手間は、YouTube 側が画面の作りを変えたときに黙って効かなくなること
- 審査では「ただの Web の包み」に見えないこと、YouTube の名前やロゴの扱いに気をつける
セレクタはすべて説明用の例です。HTML は予告なく変わります。
なぜ公式アプリではなく WKWebView なのか
iPhone では、ほかのアプリの表示を変えるアプリは作れません。なので「YouTube のショートだけ消したい」なら、Safari の機能拡張を作るか、自分のアプリの中でウェブ版を開いて表示を変えるか、のどちらかになります。
1. WKUserScript で CSS を最初に入れる
CSS はページの読み込みが終わってから入れると、棚が一瞬見えてから消えます。.atDocumentStart で入れておけば、最初に描かれる時点で効いています。
import WebKit
let css = """
ytm-reel-shelf-renderer,
ytm-pivot-bar-item-renderer:has(a[href^="/shorts"]) {
display: none !important;
}
"""
let source = """
(function () {
var style = document.createElement('style');
style.id = 'my-hide-style';
style.textContent = \(String(reflecting: css));
(document.head || document.documentElement).appendChild(style);
})();
"""
let script = WKUserScript(
source: source,
injectionTime: .atDocumentStart,
forMainFrameOnly: true
)
let config = WKWebViewConfiguration()
config.userContentController.addUserScript(script)
let webView = WKWebView(frame: .zero, configuration: config)
webView.load(URLRequest(url: URL(string: "https://m.youtube.com/")!))
気をつける点は3つです。
-
.atDocumentStartの時点ではheadがまだ無いことがあるので、documentElementにも逃げられるようにしておきます。 -
:has()は iOS 15.4 以降の WebKit で使えます。それより古い iOS も対象なら、親を隠す処理は次の JS 側でやります。 - CSS の文字列を Swift から埋め込むときは、引用符や改行で壊れないように
String(reflecting:)などで JS の文字列にしてから入れます。
iOS 14 以降なら、WKContentWorld を指定してページ側の JS と分けておくと、ページのスクリプトに自分の関数を書き換えられる心配が減ります。
let isolated = WKUserScript(
source: source,
injectionTime: .atDocumentStart,
forMainFrameOnly: true,
in: .defaultClient
)
ただし style 要素は DOM に入るのでページから見えます。
2. MutationObserver で後から出てくる要素に追いつく
m.youtube.com は、スクロールすると棚を足し、画面を移るときもページを読み直さずに中身を差し替えます。CSS で済むものは CSS が勝手に効きますが、「この見出しの棚だけ」「このリンクを含む親だけ」のように JS で判断したいものは、1回動かしただけでは足りません。
(function () {
function apply() {
document.querySelectorAll('a[href^="/shorts/"]').forEach(function (a) {
var shelf = a.closest('ytm-reel-shelf-renderer, ytm-item-section-renderer');
if (shelf && !shelf.dataset.myHidden) {
shelf.dataset.myHidden = '1';
shelf.style.setProperty('display', 'none', 'important');
}
});
}
var scheduled = false;
var observer = new MutationObserver(function () {
if (scheduled) return;
scheduled = true;
requestAnimationFrame(function () {
scheduled = false;
apply();
});
});
function start() {
observer.observe(document.documentElement, { childList: true, subtree: true });
apply();
}
if (document.documentElement) start();
else document.addEventListener('DOMContentLoaded', start);
})();
書いてみて分かったことです。
-
呼ばれる回数を絞る。 DOM の変化がとても多いので、上の例では
requestAnimationFrameで1コマに1回にまとめています。 -
画面の切り替えは URL で見る。 ページを読み直さない切り替えでは WKUserScript はもう一度動きません。
location.pathnameを覚えておき、変わったら画面ごとのルールを入れ替えます。 -
/shorts/動画IDに直接来た場合。 ほかのページからショートの動画へ移ると、プレーヤーがそのまま出ます。読み直しを伴う移動ならwebView(_:decidePolicyFor:decisionHandler:)で止められますが、pushStateによる移動はそこを通りません。JS 側で URL の変化を見て、元の画面へ戻すなどの対応が要ります。
3. セレクタが変わったときに壊れる
この方法で一番手間がかかるのはここです。YouTube がタグ名や属性、並び方を変えると、セレクタは何も言わずに当たらなくなります。エラーも例外も出ず、隠していた棚がまた出てくるだけです。気づくのは、たいてい利用者から言われたときです。
やっていることを挙げます。
- 当たった数を数える。 ルールごとに、そのページで何件当たったかを記録します。ホームなのにショートの棚のルールが0件なら、壊れた疑いがあります。
- 定期的に自動で確かめる。 主なページを開いて、ルールが当たるかを週に1回くらい確かめます。
-
変わりにくい手がかりを選ぶ。 自動で付く class 名より、リンク先の
hrefやaria-label、タグ名のほうが長持ちしやすいです。ただaria-labelは表示言語で変わるので、多言語で使うなら頼りすぎないほうが安全です。 -
隠すより押せなくするほうがよい場所もある。 下のタブのように横に並んでいるものは、1つ消すと残りの幅が変わって崩れることがあります。
pointer-events: noneとopacityで押せなくするだけのほうが、見た目が壊れにくい場合があります。
それでも、直すまでの間は隠していたものが出ます。防げないので、アプリの説明に先に書いておくほうが、低い評価を付けられるより楽です。
4. App Store の審査で気をつけること
ガイドラインは改定されるので、提出前に原文を確かめてください。そのうえで意識しておくとよい点です。
- ただの Web の包みに見せない。 最低限の機能(4.2)に当たると、サイトを開くだけのアプリは断られやすいです。何をするアプリなのか、ウェブ版をそのまま開くのと何が違うのかを、画面と説明文ではっきりさせます。
- YouTube の名前とロゴ。 アプリ名やアイコン、スクリーンショットで YouTube のロゴを大きく使うと、知的財産(5.2)の点で引っかかりやすくなります。「YouTube のショートを隠す」のように説明の中で名前を出すのと、公式に見える使い方は分けて考えます。
- 動画の保存やバックグラウンド再生をしない。 隠すこととは別に、相手のサービスの利用規約で禁じられている機能を足すと、そこを理由に断られることがあります。
- ログインとプライバシー。 ログインは YouTube(Google)のページで直接してもらい、アプリでは ID やパスワードを受け取らないようにします。差し込んだスクリプトはページの中身を読めるので、何を送って何を送らないかをプライバシーの申告と説明文に正直に書きます。
- 開く先を決めておく。 ページとして開く行き先を、対象のサイトとログインの画面に絞っておくと、どこでも開けるブラウザとして見られにくくなります。reCAPTCHA などの確認は iframe で来るので、そこまで止めるとログインできなくなります。
minulo での使い方
ここからは自分のアプリの話です。私は北林 歩といって、minulo という iPhone アプリをひとりで作っています。minulo は Instagram・YouTube・TikTok などの公式のサイトをアプリの中で開き、リールやショート、おすすめを隠します。DM やふつうの動画はそのまま使えます。隠す機能は無料です。
公式アプリの中は変えられないので、公式の Instagram・YouTube・TikTok アプリはスクリーンタイムで止め、アイコンを押すと「minulo で開く」から minulo に移れるようにしています。
上で書いた壊れやすさは minulo でもそのままです。隠すルールは毎週自動で点検していますが、SNS 側が画面の作りを変えると、隠していたものがしばらく出ることがあります。ほかにも、公式アプリを止めている間はそのアプリの通知が届かない、ストーリーズの投稿や通話はウェブ版に無いので使えない、といった限界があります。ログインは各SNSのページで直接行い、ページに入れるスクリプトは画面の文字を見ますが、保存も送信もしません。