2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

SwiftUIでSFSafariViewControllerをプッシュ遷移させたかった

Last updated at Posted at 2022-05-08

やりたいこと

UIKitで実装されていた「SFSafariViewControllerを使って、Webページを閲覧する」実装をSwiftUIに移行していく.
表示するWebの情報は一覧で表示されており、その一覧はモーダルで表示されている.
SFSafariViewControllerを表示するときは、プッシュ遷移させる.

結論

  • UIViewControllerRepresentableを使うと、NavigationBarが2つ表示されてしまう
  • UIKitのUINavigationController(EmbedされてるViewController)から、presentしてあげる(要はSwiftUIを諦める)

環境

2022年5月8日時点
Xcode 13.3.1
Swift 5.6
Simulatorでの確認
UIKitにSwiftUIを埋め込む形での使用
これ以外の環境については特に検証してないのでわからないです.

要件の整理

Page A: UIWindowのRootViewController(もしくは、そのchildren).
Page List: Page Webに渡すURLの一覧を表示. Page Aからモーダル遷移.
Page Web: Page Listで一覧されてるURLを貰いWebページを表示. Page Listからプッシュ遷移.
スクリーンショット 2022-05-08 17.39.17.png
また、SafariViewと表現するときは、SFSafariViewControllerをUIViewControllerRepresentableをつかってSwiftUIから呼び出せるようにしてものと認識してください. UIKitから呼び出す際には、SFSafariViewControllerと表現します.

SafariView

UIViewControllerRepresentableによってSFSafariViewControllerをSwiftUIから呼び出しできるようにする実装.
「SwiftUI SFSafariViewController」とかで検索するといっぱいヒットするので、調べてみてください.

import SwiftUI
import SafariServices

struct SafariView: UIViewControllerRepresentable {
  typealias UIViewControllerType = SFSafariViewController
  var url: URL

  func makeUIViewController(context: UIViewControllerRepresentableContext<SafariView>) -> SFSafariViewController {
    return SFSafariViewController(url: url)
  }

  func updateUIViewController(_ uiViewController: SFSafariViewController, context: UIViewControllerRepresentableContext<SafariView>) {
  }
}

また、 BetterSafariViewというOSSがあって、要件次第では検討してもいいかもしれません.
※ 初稿時点では実際に触ったりしてないので、所管とかわからないのと、今回の要件が満たせるのかとか把握してないです.もし触ったら、感想等追記する予定.

実験パターン

  1. Page ListをNavigationView(SwiftUI)、直接Page Aから呼びだす.Page WebはSafariViewで実装.
  2. Page ListをList(SwiftUI)とNavigationLink(SwiftUI)、それをUINavigationControllerのrootViewControllerにしてPage Aから呼び出す.Page WebはSafariViewで実装.
  3. 2のPage WebをSFSafariViewControllerで実装し、遷移をDelegate等を使ってUINavigationControllerで処理.
    本記事の結論は3になります.

1. Page ListをNavigationView(SwiftUI)、直接Page Aから呼びだす.Page WebはSafariViewで実装.

イメージ図(逆にわかりづらい?コードもあるのでみてください)
スクリーンショット 2022-05-08 17.56.15.png

実装

// PageAViewController.swift
class PageAViewController: UIViewController {
  // 真っ白の画面の中央にボタンを置くとかでやってます
  @IBAction func showList(_ sender: Any) {
    let pageList = PageList.init()
    let host = UIHostingController.init(rootView: pageList)
    show(host, sender: nil)
  }
}
// PageList.swift
struct PageList: View {
  let urls = ["https://apple.com", "https://www.google.com/", "https://www.microsoft.com/"]
  var body: some View {
    NavigationView {
      List.init(urls, id: \.self) { url in
        NavigationLink(destination: SafariView(url: URL.init(string: url)!)) {
          Text(url)
        }
      }
    }
  }
}

結果

NavigationBarが表示されており、さらにその下にSafariViewのNavigationBarも表示される.
「完了」ボタンを押したらモーダルが閉じられてしまいます(Page Aまで戻ってしまう).
なんなら、どうしてこんなに余白ができるんだっていうくらい余白があります.
スクリーンショット 2022-05-08 18.17.27.png

2. Page ListをList(SwiftUI)とNavigationLink(SwiftUI)、それをUINavigationControllerのrootViewControllerにしてPage Aから呼び出す.Page WebはSafariViewで実装.

イメージ図
スクリーンショット 2022-05-08 18.22.44.png

実装

// PageAViewController.swift
class PageAViewController: UIViewController {
  // 真っ白の画面の中央にボタンを置くとかでやってます
  @IBAction func showList(_ sender: Any) {
    let pageList = PageList.init()
    let host = UIHostingController.init(rootView: pageList)
    let navi = UINavigationController.init(rootViewController: host)
    show(navi, sender: nil)
  }
}
// PageList.swift
struct PageList: View {
  let urls = ["https://apple.com", "https://www.google.com/", "https://www.microsoft.com/"]
  var body: some View {
    List.init(urls, id: \.self) { url in
      NavigationLink(destination: SafariView(url: URL.init(string: url)!)) {
        Text(url)
      }
    }
  }
}

結果

先ほどより、良くなりましたね. ただ、相変わらず2つ表示されてしまいますし、「完了」ボタンを押したらモーダルが閉じられてしまいます.
あと、「完了」ボタンのタップ判定の位置が、少し変になってました.
余談ですが、UINavigationControllerに入ってればNavigationViewに入ってないNavigationLinkで遷移できるのは、ちょっとビックリしました.
スクリーンショット 2022-05-08 18.27.19.png

実験1と2までで困ること

  • SafariViewのNavigationBarと、PageListのNavigationBarの2つ表示されてしまう.
  • 「完了」ボタンが押されたときに、モーダルが閉じられてしまう.
    前者の問題、実はNavigationBarを非表示にする処理を入れることで解決します.
// PageList.swift
   //・・・
   NavigationLink(
     destination: SafariView(url: URL.init(string: url)!)
                    .navigationBarHidden(true)) // 遷移先のSafariViewのNavigationBarを非表示にする
 
   //・・・

スクリーンショット 2022-05-08 18.39.27.png

ここまでで私は「完了」ボタンが押されたときに、モーダルを閉じるじゃなくてプッシュバックするようにハンドリングしなおせばいける?っと考えました.
しかし、残念ながら現時点では「完了」ボタンの処理を変更することはできないようです.
ref: Apple Developer Document - SFSafariViewController

2のPage WebをSFSafariViewControllerで実装し、遷移をDelegate等を使ってUINavigationControllerで処理.

イメージ図
Delegateの結果を受け取るために、EmbedViewControllerが追加されてる.
スクリーンショット 2022-05-08 18.53.33.png

実装

// PageAViewController.swift
class PageAViewController: UIViewController {
  // 真っ白の画面の中央にボタンを置くとかでやってます
  @IBAction func showList(_ sender: Any) {
    let embed = EmbedViewController.init(nibName: nil, bundle: nil)
    let navi = UINavigationController.init(rootViewController: embed)
    show(navi, sender: nil)
  }
}
class EmbedViewController: UIViewController {
  override func viewDidLoad() {
    super.viewDidLoad()
    let pageList = PageList.init(delegate: self)
    let host = UIHostingController.init(rootView: pageList)

    addChild(host)
    // ・・・ ViewControllerをEmbedする処理, 長くなるので割愛
  }
}
// PageList.swift
struct PageList: View {
  let urls = ["https://apple.com", "https://www.google.com/", "https://www.microsoft.com/"]
  var delegate: PageListDelegate? = nil

  var body: some View {
    List.init(urls, id: \.self) { url in
      Text(url)
        .onTapGesture {
          delegate?.pageList(didSelectRow: URL.init(string: url))
        }
    }
  }
}
// Delegate関連
import SafariServices

protocol PageListDelegate {
  func pageList(didSelectRow url: URL?)
}
extension EmbedViewController: PageListDelegate {
  func pageList(didSelectRow url: URL?) {
    let vc = SFSafariViewController.init(url: url!)
    present(vc, animated: true)
  }
}

結果

NavigationBarが2つ表示されない.
「完了」ボタンを押してプッシュバックの遷移になる(モーダルが閉じられない).
スクリーンショット 2022-05-08 19.09.02.png
やってることとしては、UIKitで実装してSwiftUIで呼び出してるだけなので、できて当たり前といえば当たり前です.

余談

  • Delegateの処理でpresentで処理してるが、 navigationController?.pushViewController(vc, animated: true) で遷移すると、実験2と同じような表示になります.
  • SwiftUIにUIKitの present 相当のものがあるとうまくいくのかなって思ったのですが、現状は見つけられませんでした.  

まとめ

最終的にはUIKitに遷移処理を任せる形で実装することになりました.
タイトルが SwiftUIでSFSafariViewControllerをプッシュ遷移させたかった と過去形になってるのは、そういう理由です.
UIKitにできて、SwiftUIにできないことが多い間は、こういう実装が増えるのかなーなんて思ってたりします.
個人的には fullScreenCover とかでモーダルさせれば、正直気にならないので無理に既存の仕様に合わせるのかは、チームと相談していけると良さそうです.
実際にこのコードが通用し続けるのか、自信はありません.SwiftUIの変更や、仕様が複雑化していくことで、対応できなくなる怖さもあります.
もっと良い実装があれば、教えていただけると嬉しいです.

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?