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

この記事からわかること

  • SwiftUIActivityViewController注意点使い方
  • iPadクラッシュする原因解決方法
  • UIPopoverPresentationControllerクラスpopoverPresentationControllerプロパティ
  • sourceView/barButtonItem/sourceItem/sourceRect

index

[open]

\ アプリをリリースしました /

みんなの誕生日

友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-

posted withアプリーチ

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 }

格納するのはUIActivityViewControllerviewプロパティです。これも正確には準拠している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に指定したいのは位置(座標)部分を示すxyです。位置情報には使用しているデバイス(画面)のサイズ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学習に使用した参考書

searchbox

スポンサー

ProFile

ame

趣味:読書,プログラミング学習,サイト制作,ブログ

IT嫌いを克服するためにITパスを取得しようと勉強してからサイト制作が趣味に変わりました笑
今はCMSを使わずこのサイトを完全自作でサイト運営中〜

New Article

index