minulo は、アプリの中で Instagram を開いて[DM以外のタブ]をオンにすると、リール・発見・ホームのフィード・おすすめ投稿が消え、DM とお知らせのタブだけが残る iPhone アプリです。消すのは無料です。Instagram のアプリ自体には「DMだけ」のモードはなく、スクリーンタイムで時間を制限すると DM も一緒に止まります。(この記事は minulo の開発者が書いています)
私は minulo の作者です。この記事では、同じ形を自分で作るときの一般的なやり方を書きます。WKWebView で instagram.com を開いて CSS/JS でタブを隠し、Screen Time(FamilyControls)で公式アプリを止める組み合わせです。最後に、minulo ではどう使っているかと限界を書きます。
コードとセレクタはすべて説明用の例で、minulo のものではありません。Instagram の HTML は予告なく変わります。
先に:公式の設定でどこまでできるか
作る前に、OS とアプリの設定でできることを確かめておきます(Apple の iPhone ユーザガイドと Instagram ヘルプ、2026年10月)。
- 通知を DM だけにする:Instagram の[お知らせ]で、メッセージ以外の通知を1つずつオフにします。通知をタップすると、そのチャットが開きます。ただし届いたときしか使えず、チャットから戻るとフィードとリールがあります。
- アイコンの長押し:クイックアクションにメッセージの項目があれば DM に直行できます。何が並ぶかは Instagram が決めていて、今の中身は確かめられませんでした。
- ロック画面で通知を長押し:アプリが対応していれば返信できます。Instagram の DM が対応しているかは確かめられませんでした。
どれも DM には早く着けますが、アプリの中にフィードとリールが残ります。iPhone では、ほかのアプリの表示を変えるアプリは作れません。なので「DM だけの画面」を出すには、ウェブ版を自分のアプリの中で開いて表示を変えることになります。
1. WKWebView で instagram.com を開く
import WebKit
let config = WKWebViewConfiguration()
// ログイン状態を残すなら永続のストアを使う(.nonPersistent() だと毎回ログインになる)
config.websiteDataStore = .default()
config.userContentController.addUserScript(
WKUserScript(source: hideScript, // 下の JS
injectionTime: .atDocumentStart,
forMainFrameOnly: true)
)
let webView = WKWebView(frame: .zero, configuration: config)
webView.navigationDelegate = self
webView.load(URLRequest(url: URL(string: "https://www.instagram.com/direct/inbox/")!))
最初から受信箱の URL を開いておくと、ホームのフィードを通らずに始められます。CSS は .atDocumentStart で入れます。読み込み後に入れると、タブが一瞬見えてから消えます。
2. CSS でタブを隠し、JS で画面の切り替えに追いつく
(function () {
// 説明用の例。実際のセレクタは HTML を見て決め、定期的に確かめる
var css = [
'a[href="/reels/"]',
'a[href="/explore/"]'
].join(',') + '{ display: none !important; }';
var style = document.createElement('style');
style.textContent = css;
(document.head || document.documentElement).appendChild(style);
// ページを読み直さない切り替え(pushState)は WKUserScript が再実行されない
var last = location.pathname;
function onRoute() {
if (location.pathname === last) return;
last = location.pathname;
if (last === '/') location.replace('/direct/inbox/'); // ホームのフィードは受信箱へ
}
new MutationObserver(onRoute)
.observe(document.documentElement, { childList: true, subtree: true });
window.addEventListener('popstate', onRoute);
})();
読み直しを伴う移動は、Swift 側の decidePolicyFor でも見られます。
func webView(_ webView: WKWebView,
decidePolicyFor action: WKNavigationAction,
decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
guard let url = action.request.url, let host = url.host else {
return decisionHandler(.cancel)
}
// reCAPTCHA などの確認は iframe で来るので、サブフレームは止めない
if action.targetFrame?.isMainFrame == false { return decisionHandler(.allow) }
let allowed: Set<String> = ["instagram.com", "www.instagram.com"] // 例
guard allowed.contains(host) else { return decisionHandler(.cancel) }
if url.path.hasPrefix("/explore/") {
decisionHandler(.cancel)
webView.load(URLRequest(url: URL(string: "https://www.instagram.com/direct/inbox/")!))
return
}
decisionHandler(.allow)
}
注意点です。リールの URL まで一律に止めると、DM で友だちから送られてきたリールも開けなくなります。どこまで止めるかは先に決めておきます。また、メインフレームとして開く先を対象のサイトとログインの画面に絞っておくと、どこでも開けるブラウザとして見られにくくなります。
3. Screen Time で公式アプリを止める
WebView 側で隠しても、ホーム画面の公式アプリを押せば元どおりです。そこで FamilyControls と ManagedSettings で公式アプリを止めます。
import FamilyControls
import ManagedSettings
import SwiftUI
// 本人の端末で使う許可
try await AuthorizationCenter.shared.requestAuthorization(for: .individual)
// 止めるアプリを選んでもらう(アプリ名ではなく中身の分からない印が返る)
struct PickView: View {
@State private var selection = FamilyActivitySelection()
@State private var isPresented = false
var body: some View {
Button("止めるアプリを選ぶ") { isPresented = true }
.familyActivityPicker(isPresented: $isPresented, selection: $selection)
.onChange(of: selection) { _, new in
ManagedSettingsStore().shield.applications = new.applicationTokens
}
}
}
止めたアプリのアイコンを押すとブロック画面が出ます。ブロック画面の見た目は ShieldConfiguration の拡張で変えられますが、ボタンの処理(ShieldAction)から自分のアプリを直接開くことはできませんでした。私は通知を1つ出して、それを押してもらう形にしています。くわしくは前に書いたスクリーンタイムのブロック画面から自分のアプリを開くにあります。
4. 限界と気をつけること
-
セレクタが変わると黙って効かなくなる。 エラーは出ず、隠していたタブが戻るだけです。主なページでルールが当たるかを定期的に自動で確かめます。class 名より
hrefのほうが長持ちしやすいですが、それでも変わります。 - ログインはサイトのページで直接してもらう。 ID とパスワードはアプリで受け取らず、Cookie は WebKit のストアに任せます。二段階認証や reCAPTCHA の iframe を止めないようにします。
- ウェブ版に無い機能は使えない。 ストーリーズの投稿・音声メッセージ・通話はウェブ版に無いので、アプリの中ではできません。
- 止めている間は公式アプリの通知が来ない。 Screen Time で止めたアプリの通知は届きません。
- 審査。 ガイドラインは改定されるので原文を確かめてください。サイトを開くだけのアプリは最低限の機能(4.2)で断られやすいので、何が違うのかを画面と説明で示します。Instagram の名前やロゴを公式に見える形で使うと知的財産(5.2)に触れます。差し込んだスクリプトはページの中身を読めるので、何を送って何を送らないかをプライバシーの申告に正直に書きます。FamilyControls を配布版で使うには、Apple への申請も要ります。
minulo ではこう使っている
ここからは自分のアプリの話です。私は北林 歩といって、minulo という iPhone アプリをひとりで作っています。minulo は Instagram などの公式サイトをアプリの中で開き、Instagram では[DM以外のタブ]で DM とお知らせのタブだけを残します。ログインは Instagram のログイン画面で直接行い、パスワードは minulo には届かず、保存もされません。ページに入れるスクリプトは画面の文字を見ますが、保存も送信もしません。隠すルールは毎週自動で点検していますが、Instagram 側が画面の作りを変えると、隠していたものがしばらく出ることがあります。
使う手順は、minulo の中で Instagram を開いてログインし、Instagram の横の歯車から[DM以外のタブ]をオンにするだけです。公式アプリは「ブロック」タブでスクリーンタイムで止められ、アイコンを押すとブロック画面の「minulo で開く」から DM に移れます。グループDMも DM の一部なので、そのまま使えます。
限界は5つあります。
- 公式アプリは変わりません。リールが消えるのは minulo の中だけです。
- 公式アプリを止める無料のロックは1回2時間までです。
- 毎晩決まった時間に止める予定は Pro です。
- 止めている間は、そのアプリの通知が届きません。minulo が代わりに送ることもありません。
- 使えるのは iPhone と Mac だけです(Chrome と Safari の機能拡張もあります)。Android 版はまだありません。