【SwiftUI】iPadでUIActivityViewControllerがクラッシュする解決法!

この記事からわかること
- SwiftのUIActivityViewControllerの注意点と使い方
- iPadでクラッシュする原因と解決方法
- UIPopoverPresentationControllerクラスとpopoverPresentationControllerプロパティ
- sourceView/barButtonItem/sourceItem/sourceRect
index
[open]
\ アプリをリリースしました /
SwiftでiOSアプリを開発中にUIActivityViewControllerを使ってシェアボタンを実装していたところ、iPadでシミュレーションするとクラッシュしてしまいました。この原因と解決方法をまとめていきたいと思います。
UIActivityViewControllerがクラッシュする問題
Swift UIを使ったアプリにUIActivityViewController
を使ってシェアボタンを実装していた際にiPhoneのシミュレーションでは正常に動作したのですがiPadのシミュレーションだとクラッシュしてエラーが発生してしまいました。
シェアボタンの実装コード
func shareApp(shareText: String, shareImage: Image, shareLink: String) {
let items = [shareText, shareImage, URL(string: shareLink)!] as [Any]
let activityVC = UIActivityViewController(activityItems: items, applicationActivities: nil)
let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene
let rootVC = windowScene?.windows.first?.rootViewController
rootVC?.present(activityVC, animated: true,completion: {})
}
クラッシュ後に発生したエラー
Thread 1: "UIPopoverPresentationController (<UIPopoverPresentationController: 0x125114020>) should have a non-nil sourceView or barButtonItem set before the presentation occurs.""
原因と解決方法
原因はiPadの場合、シェアUIのようなポップオーバー画面を表示させる際はビューと表示位置を明示的に指定する必要があるようです。指定されていない場合はランタイムエラーとなり、エラーを吐いてしまうようです。
解決するにはエラーの内容に従いsourceView
を追加していきます。私はSwiftUIでビューを構築していたので同じ境遇の人の参考になると幸いです。
表示させるビューの指定と表示位置の指定はUIPopoverPresentationController
クラスのプロパティにセットする必要があります。先に全体のコードを載せておきます。
解決コード
func shareApp(shareText: String, shareImage: Image, shareLink: String) {
let items = [shareText, shareImage, URL(string: shareLink)!] as [Any]
let activityVC = UIActivityViewController(activityItems: items, applicationActivities: nil)
// 追加ここから
if UIDevice.current.userInterfaceIdiom == .pad {
let deviceSize = UIScreen.main.bounds
if let popPC = activityVC.popoverPresentationController {
popPC.sourceView = activityVC.view
popPC.barButtonItem = .none
popPC.sourceRect = CGRect(x:deviceSize.size.width/2, y: deviceSize.size.height, width: 0, height: 0)
}
}
// 追加ここまで
let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene
let rootVC = windowScene?.windows.first?.rootViewController
rootVC?.present(activityVC, animated: true,completion: {})
}
UIPopoverPresentationControllerクラス
@MainActor class UIPopoverPresentationController : UIPresentationController
UIPopoverPresentationController
クラスはポップオーバーの表示に関する操作を管理するクラスです。このクラスをインスタンス化するためにはUIActivityViewController
クラスのpopoverPresentationController
プロパティを使用します。
var popoverPresentationController: UIPopoverPresentationController? { get }
このプロパティは正確にはUIActivityViewController
クラスが準拠しているUIViewController
クラスのプロパティで、ポップオーバービューがあればそのオブジェクトが格納され、なければnil
が格納されます。
nil
の可能性があるのでオプショナルバインディングでインスタンスを変数に格納します。
if let popPC = activityVC.popoverPresentationController {
}
これでオブジェクトに参照できるようになったので各プロパティに値をセットしていきます。
sourceViewプロパティ
sourceView
はポップオーバーを表示させるアンカー(支え)となるビューを指定するプロパティです。
var sourceView: UIView? { get set }
格納するのはUIActivityViewController
のview
プロパティです。これも正確には準拠しているUIViewController
クラスのプロパティでコントローラが管理しているビューがここに格納されています。つまりここではアクティブなっているビューを示しています。
popPC.sourceView = activityVC.view
barButtonItemプロパティ
barButtonItem
は指定したバーボタンにポップオーバーを固定するプロパティです。指定するとポップオーバーの矢印は指定された項目を指すような見た目になります。ですがiOS 8.0–16.0 Deprecated:非推奨になっており、ゆくゆくはsourceItem
に変わっていくようですがこちらはまだベータ版なので注意が必要です。
一応コードを示しておきます。今回は特に指し示す項目を指定しないので.none
を渡します。
popPC.barButtonItem = .none
sourceRectプロパティ
sourceRect
はポップオーバーを固定するソースビュー内の領域を指定するプロパティです。格納する値は四角形領域の位置と寸法を定義するCGRect
型で指定します。
let deviceSize = UIScreen.main.bounds
popPC.sourceRect = CGRect(x:deviceSize.size.width/2, y: deviceSize.size.height, width: 0, height: 0)
今回sourceRect
に指定したいのは位置(座標)部分を示すx
とy
です。位置情報には使用しているデバイス(画面)のサイズをUIScreen.main.bounds
で取得して指定します。
デバイスを識別してiPadの時のみ実行させる
最後に実行されているデバイスを識別し、iPadの時のみ処理を実行させるようにしていきます。先ほどの処理を囲うようにUIDevice.current.userInterfaceIdiom == .pad
を使ってiPadかどうかで分岐できるようにしておきます。
if UIDevice.current.userInterfaceIdiom == .pad {
let deviceSize = UIScreen.main.bounds
if let popPC = activityVC.popoverPresentationController {
popPC.sourceView = activityVC.view
popPC.barButtonItem = .none
popPC.sourceRect = CGRect(x:deviceSize.size.width/2, y: deviceSize.size.height, width: 0, height: 0)
}
}
これで無事SwiftUIでシェアボタンを作成した際にiPadでクラッシュしないようにすることができました。
iPadでシェアボタンが動作しない
追記:2022/9/14
上記の方法で使用できていたシェアボタンが突然使用できなくなってしまいました。発生していたエラー内容は以下の通りです。
Unable to simultaneously satisfy constraints.
Probably at least one of the constraints in the following list is one you don't want.
Try this:
(1) look at each constraint and try to figure out which you don't expect;
(2) find the code that added the unwanted constraint or constraints and fix it.
// 翻訳
制約を同時に満たすことができません。
おそらく、次のリストの制約の少なくとも 1 つが望ましくないものです。
これを試して:
(1) 各制約を見て、どれが予期しないものかを把握してみてください。
(2) 不要な制約を追加したコードを見つけて修正します。
上記のエラー内容をみてもよくわかりませんでした。なかなか不親切ですね。。もう1つそれっぽいエラーが出ていました。
Changing the translatesAutoresizingMaskIntoConstraints property of a UICollectionReusableView that is managed by a UICollectionView is not supported, and will result in incorrect self-sizing.
// 翻訳
UICollectionView によって管理される UICollectionReusableView の translatesAutoresizingMaskIntoConstraints プロパティの変更はサポートされておらず、不適切な自己サイジングが発生します。
解決の糸口が少し見えた気がします。つまりUICollectionView
の何かしらのプロパティに対しての変更が許容されていないようです。
模索してみたところsourceRect
プロパティへのCGRectを使っての指定が原因 のようです。とりあえずaccessibilityFrame
プロパティをそのまま指定することで無事表示させることはできましたが、表示位置が左上部になってしまいしました。また良い方法があれば追記しておきます。
// シェアボタン
func shareApp(shareText: String, shareLink: String) {
let items = [shareText, URL(string: shareLink)!] as [Any]
let activityVC = UIActivityViewController(activityItems: items, applicationActivities: nil)
if UIDevice.current.userInterfaceIdiom == .pad {
if let popPC = activityVC.popoverPresentationController {
popPC.sourceView = activityVC.view
popPC.barButtonItem = .none
popPC.sourceRect = activityVC.accessibilityFrame
}
}
let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene
let rootVC = windowScene?.windows.first?.rootViewController
rootVC?.present(activityVC, animated: true,completion: {})
}
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
ご覧いただきありがとうございました。
私がSwift UI学習に使用した参考書