Flutterアプリケーション開発概論

【iOS共有エラー対策】sharePositionOriginを指定してPlatformExceptionを防ぐ

23NETFLIX風動画アプリを作りながらUI・検索・動画再生・共有機能を学ぶFlutter(iOS・Android)アプリ開発
FlutteriOSAndroidMacOSWindows基礎から学ぶ開発アプリ開発

この節で学ぶこと

前の節では、share_plus を使って、作品タイトルやYouTubeリンクを共有する方法を学びました。

今回の節では、その共有機能で起きやすい iOSのエラー対策 を学びます。

特に、iPadや一部のiOS環境では、共有画面を表示するときに次のようなエラーが出ることがあります。

PlatformException
sharePositionOrigin: argument must be set

このエラーは、共有画面を表示する位置が正しく指定されていないときに起こります。

今回のアプリでは、このエラーを防ぐために sharePositionOrigin を指定しています。

sharePositionOrigin: shareOriginFromContext(context),

この節では、なぜこの指定が必要なのか、そしてどのように安全な表示位置を作っているのかを、順番に見ていきます。


まずエラーの意味を確認しよう

共有機能を作っていると、iOSで次のようなエラーが出ることがあります。

sharePositionOrigin: argument must be set

日本語にすると、だいたい次のような意味です。

共有画面を表示する位置を指定してください

スマホでは共有画面が下から出るだけなので、位置指定を強く意識しないこともあります。

しかし、iPadでは共有画面がポップオーバーのように表示されることがあります。

その場合、iOS側は次のように考えます。

どのボタンから共有画面を出すの?
どの位置を基準に表示すればいいの?

この基準位置がないと、共有画面を表示できずにエラーになることがあります。


sharePositionOriginとは?

sharePositionOrigin は、共有画面を表示するときの基準位置です。

コードでは、ShareParams の中に指定しています。

ShareParams(
  text:
      'NETAFLIXで「${movie.title}」をチェック!\nhttps://www.youtube.com/watch?v=${movie.youtubeVideoId}',
  subject: movie.title,
  sharePositionOrigin: shareOriginFromContext(context),
)

この中で大切なのが、次の行です。

sharePositionOrigin: shareOriginFromContext(context),

これは、共有画面を表示するための四角い範囲を渡しています。

Flutterでは、この範囲を Rect という型で表します。


Rectとは?

Rect は、画面上の四角形を表すデータです。

たとえば、次のような情報を持ちます。

左から何pxの位置か
上から何pxの位置か
横幅はいくつか
高さはいくつか

コードでは、次のように作ります。

Rect.fromLTWH(
  left,
  top,
  width,
  height,
)

LTWH は、次の意味です。

文字意味
LLeft。左からの位置
TTop。上からの位置
WWidth。横幅
HHeight。高さ

つまり、Rect.fromLTWH(1, 1, 300, 500) と書くと、画面の左上から少し内側にある、横300・縦500の四角形を表します。


今回使っている共有位置の関数

今回のアプリでは、共有画面の表示位置を作るために、次の関数を用意しています。

Rect shareOriginFromContext(BuildContext context) {
  final size = MediaQuery.sizeOf(context);

  return Rect.fromLTWH(
    1,
    1,
    size.width <= 2 ? 1 : size.width - 2,
    size.height <= 2 ? 1 : size.height - 2,
  );
}

この関数の役割は、画面サイズをもとに、共有画面を表示するための安全な Rect を作ることです。

ポイントは、幅や高さが 0 にならないようにしていることです。


BuildContextを受け取る理由

この関数は、BuildContext を受け取っています。

Rect shareOriginFromContext(BuildContext context) {

BuildContext は、Flutterの画面やWidgetの場所を知るための情報です。

今回の場合は、画面サイズを取得するために使っています。

final size = MediaQuery.sizeOf(context);

このように、context を使うことで、現在表示されている画面のサイズを調べられます。


MediaQuery.sizeOf(context)とは?

画面サイズを取得しているのが、次のコードです。

final size = MediaQuery.sizeOf(context);

MediaQuery.sizeOf(context) を使うと、現在の画面サイズが分かります。

たとえば、スマホ画面なら次のような情報が取れます。

横幅:390
高さ:844

この size には、widthheight があります。

size.width
size.height

これを使って、共有画面の表示位置を作ります。


Rect.fromLTWHで表示位置を作る

次に、Rect.fromLTWH を使って、共有画面の表示位置を作っています。

return Rect.fromLTWH(
  1,
  1,
  size.width <= 2 ? 1 : size.width - 2,
  size.height <= 2 ? 1 : size.height - 2,
);

ここでは、次のような四角形を作っています。

項目
左の位置1
上の位置1
横幅size.width - 2 または 1
高さsize.height - 2 または 1

画面全体にかなり近い範囲を指定していますが、完全に 0, 0 から始めていないのがポイントです。


なぜ左と上を1にしているのか

次の部分を見てください。

1,
1,

これは、左から 1、上から 1 の位置を指定しています。

本当は 0, 0 でも画面左上を表せそうですが、共有位置として {0, 0}, {0, 0} のような値になると、iOSでエラーになることがあります。

そのため、完全な0ではなく、少し内側の 1, 1 から始めています。

左上ぴったりではなく
少しだけ内側から始める

小さな工夫ですが、共有画面を安定して表示するために大切です。


幅と高さを0にしない工夫

次の部分も重要です。

size.width <= 2 ? 1 : size.width - 2,
size.height <= 2 ? 1 : size.height - 2,

これは、三項演算子を使っています。

意味は次の通りです。

画面幅が2以下なら、幅は1にする
それ以外なら、画面幅から2を引く

画面高さが2以下なら、高さは1にする
それ以外なら、画面高さから2を引く

なぜこのような書き方をしているのでしょうか。

理由は、Rect の幅や高さが 0 になるのを防ぐためです。


幅や高さが0だと何が問題なのか

共有位置として、次のような Rect が渡されると問題になります。

Rect.fromLTWH(0, 0, 0, 0)

これは、左上の位置はあるけれど、幅も高さもない四角形です。

つまり、表示位置として使うには不十分です。

横幅がない
高さもない
↓
どこから共有画面を出せばいいか分からない

そのため、最低でも幅と高さが 1 以上になるようにしています。

size.width <= 2 ? 1 : size.width - 2

このように書くことで、かなり小さな画面サイズになったとしても、0 にはなりません。


三項演算子を読み解く

次のコードをもう少し分解してみましょう。

size.width <= 2 ? 1 : size.width - 2

これは、次のような意味です。

もし size.width が 2 以下なら
    1 を使う
そうでなければ
    size.width - 2 を使う

普通の if 文で書くと、次のようなイメージです。

double rectWidth;

if (size.width <= 2) {
  rectWidth = 1;
} else {
  rectWidth = size.width - 2;
}

三項演算子を使うと、これを1行で書けます。

共有位置のように、短く安全な値を作りたいときに便利です。


shareNetaflixMovieで使う

この shareOriginFromContext は、共有関数の中で使っています。

Future<void> shareNetaflixMovie({
  required BuildContext context,
  required MovieItem movie,
}) async {
  await SharePlus.instance.share(
    ShareParams(
      text:
          'NETAFLIXで「${movie.title}」をチェック!\nhttps://www.youtube.com/watch?v=${movie.youtubeVideoId}',
      subject: movie.title,
      sharePositionOrigin: shareOriginFromContext(context),
    ),
  );
}

ここで、共有文や件名と一緒に、共有位置も指定しています。

sharePositionOrigin: shareOriginFromContext(context),

これにより、共有画面を開くときにiOS側へ「この範囲を基準に表示してください」と伝えています。


共有関数全体の流れ

shareNetaflixMovie の流れを整理すると、次のようになります。

Shareボタンを押す
↓
shareNetaflixMovieが呼ばれる
↓
movie.titleで作品名を取得する
↓
movie.youtubeVideoIdでYouTubeリンクを作る
↓
shareOriginFromContextで安全なRectを作る
↓
ShareParamsにtext・subject・sharePositionOriginを入れる
↓
SharePlus.instance.shareで共有画面を開く

このように、共有文を作る処理と、iOSのエラー対策が1つの関数にまとまっています。


Shareボタン側のコード

詳細画面では、Shareボタンから次のように呼び出しています。

DetailAction(
  icon: Icons.share_outlined,
  label: 'Share',
  active: false,
  onTap: () {
    shareNetaflixMovie(
      context: context,
      movie: movie,
    );
  },
),

Shareボタンが押されると、shareNetaflixMovie が呼ばれます。

このとき、現在の context と、今表示している movie を渡しています。

context: context,
movie: movie,

context は共有位置を作るために使います。

movie は共有文を作るために使います。


Clips画面でも同じ対策を使う

Clips画面でも、共有処理は同じです。

void shareMovie(MovieItem movie) {
  shareNetaflixMovie(
    context: context,
    movie: movie,
  );
}

そして、ClipMoviePage に渡しています。

onTapShare: () => shareMovie(item),

これにより、Clips画面のShareボタンでも、同じ sharePositionOrigin 対策が使われます。

詳細画面のShare
↓
shareNetaflixMovie

Clips画面のShare
↓
shareNetaflixMovie

共有処理を共通関数にしているので、iOS対策も一か所にまとめられます。


ここまでのまとめ

ここまでで、sharePositionOrigin を使ってiOSの共有エラーを防ぐ基本を確認しました。

大切なポイントは次の通りです。

  • iOSやiPadでは、共有画面の表示位置が必要になることがある。
  • sharePositionOrigin は、共有画面を表示する基準位置。
  • sharePositionOrigin には Rect を渡す。
  • Rect は、画面上の四角い範囲を表す。
  • MediaQuery.sizeOf(context) で画面サイズを取得できる。
  • Rect.fromLTWH で、左・上・幅・高さを指定した四角形を作れる。
  • 1, 1 から始めることで、完全な0位置を避けている。
  • 幅や高さが0にならないように、三項演算子で最低値を作っている。
  • shareOriginFromContext(context) を共通関数にしておくと、どの画面から共有しても同じ対策を使える。

よくあるつまずきポイント

Q. iOSで共有画面を開くとエラーになります。

まず、ShareParams の中に sharePositionOrigin が入っているか確認してください。

ShareParams(
  text:
      'NETAFLIXで「${movie.title}」をチェック!\nhttps://www.youtube.com/watch?v=${movie.youtubeVideoId}',
  subject: movie.title,
  sharePositionOrigin: shareOriginFromContext(context),
)

特に大切なのは、この部分です。

sharePositionOrigin: shareOriginFromContext(context),

iOSやiPadでは、共有画面をどこから表示するかを指定しておくと、エラーを防ぎやすくなります。


Q. Rect.fromLTWH(0, 0, 0, 0) ではだめですか?

避けた方がよいです。

Rect.fromLTWH(0, 0, 0, 0)

これは、左上の位置は指定していますが、幅と高さがありません。

iOS側から見ると、「共有画面を表示する基準になる範囲がない」と判断されることがあります。

そのため、今回のように最低でも幅と高さが 1 以上になるようにします。

Rect.fromLTWH(
  1,
  1,
  size.width <= 2 ? 1 : size.width - 2,
  size.height <= 2 ? 1 : size.height - 2,
)

Q. context はどこのものを渡せばよいですか?

基本的には、Shareボタンを表示している画面の context を渡せば大丈夫です。

shareNetaflixMovie(
  context: context,
  movie: movie,
);

より厳密に「押したボタンの位置」を使いたい場合は、Builder を使ってボタン付近の context を渡す方法もあります。

ただし、この教材では、画面全体を基準にした安全な Rect を作っているため、まずはこの書き方で十分です。


Q. Androidでも sharePositionOrigin は必要ですか?

Androidでは、iOSほど強く意識しなくても動くことが多いです。

ただし、同じ共有関数をiOSとAndroidの両方で使うなら、sharePositionOrigin を入れておいて問題ありません。

sharePositionOrigin: shareOriginFromContext(context),

iOS対策を入れた共通関数として作っておけば、どの画面から共有しても安定しやすくなります。


Q. 共有文は出るのに、件名が出ません。

subject は、共有先によって使われ方が変わります。

subject: movie.title,

メールでは件名として使われることがありますが、LINEやメッセージアプリでは表示されない場合もあります。

これはアプリ側の仕様なので、subject が必ず表示されるとは限りません。

共有の本文として必ず見せたい内容は、text に入れておきましょう。


まずカスタマイズしてみよう

共有画面のエラー対策はそのままにして、共有文だけを少し変えてみましょう。

次のコードを探してください。

text:
    'NETAFLIXで「${movie.title}」をチェック!\nhttps://www.youtube.com/watch?v=${movie.youtubeVideoId}',

これを次のように変更します。

text:
    'この作品、ちょっと気になりました。\nNETAFLIXで「${movie.title}」をチェック!\nhttps://www.youtube.com/watch?v=${movie.youtubeVideoId}',

保存して、Shareボタンを押してみましょう。

共有画面に表示される文章が変わります。


件名も変えてみよう

次に、subject を変えてみます。

変更前はこちらです。

subject: movie.title,

これを次のように変更します。

subject: '${movie.title} の予告編',

メールで共有した場合、件名が「作品名 の予告編」のようになります。

ただし、共有先アプリによっては件名が表示されない場合もあります。


shareOriginFromContextを少し読みやすくする

今回の関数は、このように書いています。

Rect shareOriginFromContext(BuildContext context) {
  final size = MediaQuery.sizeOf(context);

  return Rect.fromLTWH(
    1,
    1,
    size.width <= 2 ? 1 : size.width - 2,
    size.height <= 2 ? 1 : size.height - 2,
  );
}

このままでも問題ありませんが、変数に分けると少し読みやすくなります。

Rect shareOriginFromContext(BuildContext context) {
  final size = MediaQuery.sizeOf(context);

  final width = size.width <= 2 ? 1.0 : size.width - 2;
  final height = size.height <= 2 ? 1.0 : size.height - 2;

  return Rect.fromLTWH(
    1,
    1,
    width,
    height,
  );
}

やっていることは同じです。

ただ、widthheight に分けることで、「幅と高さを0にしないようにしている」と読み取りやすくなります。


ボタン位置を基準にしたい場合

今回の教材では、画面全体に近い範囲を共有位置として指定しています。

sharePositionOrigin: shareOriginFromContext(context),

一方で、より細かく「Shareボタンの位置から共有画面を出したい」場合は、ボタンの RenderBox を使う方法もあります。

考え方としては、次のような流れです。

Shareボタンのcontextを取得する
↓
RenderBoxを取得する
↓
ボタンの位置とサイズをRectにする
↓
sharePositionOriginに渡す

ただし、初学者向けには少し難しくなります。

今回の教材では、まずエラーを防ぐために、画面全体を基準にした安全な Rect を使う方法を採用しています。


チャレンジ

チャレンジ1:共有文にカテゴリを追加しよう

次のコードを探してください。

text:
    'NETAFLIXで「${movie.title}」をチェック!\nhttps://www.youtube.com/watch?v=${movie.youtubeVideoId}',

これを次のように変更します。

text:
    'NETAFLIXで「${movie.title}」をチェック!\nカテゴリ:${movie.category}\nhttps://www.youtube.com/watch?v=${movie.youtubeVideoId}',

共有文にカテゴリが表示されるか確認してください。


チャレンジ2:件名に「おすすめ作品」を追加しよう

次のコードを探してください。

subject: movie.title,

これを次のように変更します。

subject: 'おすすめ作品:${movie.title}',

メールなどで共有したときに、件名が変わるか確認してみましょう。


チャレンジ3:shareOriginFromContextを変数つきで書き直そう

次の関数を探してください。

Rect shareOriginFromContext(BuildContext context) {
  final size = MediaQuery.sizeOf(context);

  return Rect.fromLTWH(
    1,
    1,
    size.width <= 2 ? 1 : size.width - 2,
    size.height <= 2 ? 1 : size.height - 2,
  );
}

これを次のように変更します。

Rect shareOriginFromContext(BuildContext context) {
  final size = MediaQuery.sizeOf(context);

  final width = size.width <= 2 ? 1.0 : size.width - 2;
  final height = size.height <= 2 ? 1.0 : size.height - 2;

  return Rect.fromLTWH(
    1,
    1,
    width,
    height,
  );
}

動きは同じですが、読みやすくなります。


チャレンジ4:sharePositionOrigin を一度消して挙動を確認する

実験として、次の行を一度コメントアウトしてみましょう。

sharePositionOrigin: shareOriginFromContext(context),

ただし、iOSやiPadではエラーになる可能性があります。

確認が終わったら、必ず元に戻してください。

sharePositionOrigin: shareOriginFromContext(context),

「なぜこの指定が必要なのか」を体験として理解できます。


チャレンジの答え

チャレンジ1の答え

変更前:

text:
    'NETAFLIXで「${movie.title}」をチェック!\nhttps://www.youtube.com/watch?v=${movie.youtubeVideoId}',

変更後:

text:
    'NETAFLIXで「${movie.title}」をチェック!\nカテゴリ:${movie.category}\nhttps://www.youtube.com/watch?v=${movie.youtubeVideoId}',

共有文に作品カテゴリが追加されます。


チャレンジ2の答え

変更前:

subject: movie.title,

変更後:

subject: 'おすすめ作品:${movie.title}',

メール共有などで、件名に「おすすめ作品:作品名」と表示されます。


チャレンジ3の答え

変更前:

Rect shareOriginFromContext(BuildContext context) {
  final size = MediaQuery.sizeOf(context);

  return Rect.fromLTWH(
    1,
    1,
    size.width <= 2 ? 1 : size.width - 2,
    size.height <= 2 ? 1 : size.height - 2,
  );
}

変更後:

Rect shareOriginFromContext(BuildContext context) {
  final size = MediaQuery.sizeOf(context);

  final width = size.width <= 2 ? 1.0 : size.width - 2;
  final height = size.height <= 2 ? 1.0 : size.height - 2;

  return Rect.fromLTWH(
    1,
    1,
    width,
    height,
  );
}

widthheight に分けることで、コードの意味が読み取りやすくなります。


チャレンジ4の答え

確認用に一度コメントアウトします。

// sharePositionOrigin: shareOriginFromContext(context),

iOSやiPadでは、共有画面を出す位置が分からず、エラーになることがあります。

確認が終わったら、必ず元に戻します。

sharePositionOrigin: shareOriginFromContext(context),

この指定が、iOS共有エラー対策として大切です。


この節のまとめ

この節では、sharePositionOrigin を指定して、iOSの共有エラーを防ぐ方法を学びました。

大切なポイントは次の通りです。

  • iOSやiPadでは、共有画面を表示する基準位置が必要になることがある。
  • sharePositionOrigin は、共有画面を表示するための位置情報。
  • sharePositionOrigin には Rect を渡す。
  • Rect.fromLTWH は、左・上・幅・高さから四角形を作る。
  • Rect.fromLTWH(0, 0, 0, 0) のような幅と高さがない値は避ける。
  • MediaQuery.sizeOf(context) で現在の画面サイズを取得できる。
  • size.width <= 2 ? 1 : size.width - 2 のように書くことで、幅が0になるのを防げる。
  • shareOriginFromContext を関数化すると、どの画面から共有しても同じ対策を使える。
  • 共有処理を shareNetaflixMovie にまとめると、詳細画面やClips画面から安全に呼び出せる。
  • 共有文や件名は、movie.titlemovie.categorymovie.youtubeVideoId を使ってカスタマイズできる。

次のステップ

次の節では、Search画面を作ります。

TextField に入力された文字を使って、作品タイトル、説明文、カテゴリなどを検索し、条件に合う作品だけを表示する方法を学びます。

検索機能が入ると、アプリらしさが一気に高まります。


教材トップへ戻る