AppDelegate + SceneDelegate
SwiftUI ではAppDelegateとSceneDelegateがないので@UIApplicationDelegateAdaptorを使って対応する。
こうすると今までと同じようにAppDelegateとSceneDelegateが使えます。
@mainstruct mainApp: SwiftUI.App {@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene { WindowGroup { ContentView() } }}
class AppDelegate: NSObject, UIApplicationDelegate, UIWindowSceneDelegate { func application( _ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions ) -> UISceneConfiguration { let config = UISceneConfiguration( name: nil, sessionRole: connectingSceneSession.role ) config.delegateClass = AppDelegate.self return config }}Custom URL Scheme
URLScheme を動かそうとするとちょっと詰まったので備忘録。
URLScheme でアプリを呼び出した時、アプリが起動状態かそうでないかで処理が分岐する。当たり前ですが、Info.plistのURL Typesに URLScheme を設定しておくこと。
起動中
SceneDelegateのfunc scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>)が呼ばれるので、以下のように書けば良い。
func scene( _ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) { if let url: URL = URLContexts.first?.url { }}未起動
SceneDelegateのfunc scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions)が呼ばれるので、以下のように書けば良い。
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>)は呼ばれないので注意
func scene( _ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { if let url = connectionOptions.urlContexts.first?.url { }}UIViewController
たまに現在表示されているViewControllerのインスタンスが欲しくなるときがあります。present()で何かを表示しようとしたら最前面のViewControllerから呼ばないと何も起きないためです。
Xcode13 くらいからDeprecatedなメソッドが増えたので、Warning なしでrootViewControllerをとってくるには以下のようなコードが必要になります。
extension UIApplication { internal var rootViewController: UIViewController? { UIApplication.shared.connectedScenes .filter({ $0.activationState == .foregroundActive }) .compactMap({ $0 as? UIWindowScene }) .first? .windows .first? .rootViewController }}じゃあこれで動くのかというと常に動くわけではないのが玉に瑕。というのも、SwiftUI の場合はsheet()やfullScreenCover()でその View よりも更に上位の View がpresentedされている可能性があるため。上のコードは現在、表示されている View の親は返しますが、現在表示されている View とは限らないわけです。
というわけで上のコードを拡張して現在のUIViewControllerを取得するコードは以下の通り。
extension UIApplication { internal var current: UIViewController? { if let current = rootViewController.presentedViewController { return current } return rootViewController }}ただこれでも、上にどんどん重ねていると最前面のUIViewControllerは取れないのでそこは各自修正してください。
UIView + SwiftUI
UIHostingController
SwiftUI のViewをUIKitで使えるUIViewControllerっぽいものに変換してくれます。
let hosting: UIHostingController = UIHostingController(rootView: ContentView())みたいな感じでいつも書いてます。
UIViewControllerRepresentable
上とは逆に UIKit のUIViewControllerを SwiftUI のViewに変換してくれます。
UIViewRepresentable
UIKit のUIViewを SwiftUI のViewに変換してくれます。
どっちかというといつもUIViewControllerRepresentableを使うので出番はあまりなかったりする。
SwiftUI の拡張
UITabBarController
SwiftUI で TabView を実装しようとするとTabViewを普通に使うことになると思うのですが、これを使うといろいろと欠点が見えてきたのでそれを列挙します。
タブタップを検知できない
最も大きな問題がこれ。SwiftUI-Introspectを使っても全くわからなかったので結構根が深い問題なのかもしれない。英語で検索してもidを書き換えたりselectionをBindingするような正攻法とは思えない方法しかでてこない。
じゃあUIViewControllerRepresentableでUITabBarControllerのラッパーを作るしかないわけです。
ただ、従来の方法と違ってTabViewは内部にViewを複数持つわけで、それを個別にUIHostingControllerで扱う方法がわかりませんでした。ただ、今回は利用したい場面ではあらかじめタブの個数が決まっているので決め打ちする感じで対応しました。SwiftUIXのコードは読んだんですけどAnyForEach<Page>とか使っててよくわかりませんでした。
大雑把に書くと以下のようなコードで実現できました。
private struct _ContentView: UIViewControllerRepresentable { func makeUIViewController(context: Context) -> UITabBarController { let controller = UITabBarController()
let tab1 = UIHostingController(rootView: TabView1()) let tab2 = UIHostingController(rootView: TabView2()) let tab3 = UIHostingController(rootView: TabView3())
let views = [tab1, tab2, tab3] controller.setViewControllers(views, animated: false) return controller }}タブに突っ込みたい SwiftUI の View をそれぞれUIHostingControllerでUIViewController化してsetViewControllersでセットする感じです。往々にしてNavigationViewと併用したい場合があると思うのですが、その場合は、
private struct _ContentView: UIViewControllerRepresentable { func makeUIViewController(context: Context) -> UITabBarController { let controller = UITabBarController()
let tab1 = UINavigationController(rootViewController: UIHostingController(rootView: TabView1())) let tab2 = UINavigationController(rootViewController: UIHostingController(rootView: TabView2())) let tab3 = UINavigationController(rootViewController: UIHostingController(rootView: TabView3()))
let views = [tab1, tab2, tab3] controller.setViewControllers(views, animated: false) return controller }}という感じで更にUINavigationControllerを利用すればいけます。こう書けば以下の SwiftUI のコードとほぼ同じ機能が実現できます。
で、更に UIKit で細かいところが弄ることができるので、こちらのほうが圧倒的に優れていますね。ちゃんと同じタブをタップするとNavigationViewのpopToRootViewControllerが効いてくれます。
あとは動的にタブを追加できたら便利なんですけどね。何れにせよ、UIKit は細かいところまで手が届くので書いていて面白いです。
struct ContentView: View { var body: some View { TabView(content: { NavigationView(content: { TabView1() }) NavigationView(content: { TabView2() }) NavigationView(content: { TabView3() }) }) }}UISplitViewController
UINavigationController
Xcode
ビルド ID の自動インクリメント
色々情報が錯綜しているけれど Xcode14 でも現役で動いてかつ簡単なのがこれ。
Edit Scheme から Archive の Post-actions に以下のコマンドを書き込み。
# Type a script or drag a script file from your workspace to insert its path.cd "${PROJECT_DIR}" ; agvtool bumpこのとき、Provide build settings from に開発中のアプリを連携させるのを忘れないこと。