【SwiftUI】非公開フレームワークのOnBoardingKitというものがあるらしい

# はじめに
アップル製アプリで初回に表示される以下のような画面があります。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1745371/dd032492-00d5-693d-1f0a-0394ac54e827.png" width="50%">

これはOnBoardingKitのOBWelcomeControllerというものらしいです。
ただ、全体公開されているものではなく、非公開のフレームワークです。
このフレームワークを使う方法があるらしいので試してみました。

:::note warn
**注意**
非公開フレームワークなので、これを使用してAppStoreにリリースすることはできないと思われます。
遊んでみたい人向けの記事です。
:::

# サンプルアプリ
![Simulator Screen Recording - iPhone 15 Pro - 2024-02-25 at 22.18.51.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1745371/bca96e29-2d08-379c-028a-8b83edc687ec.gif)

# 実装
```swift
import UIKit

class OBWelcomeController {
    private(set) var viewController: UIViewController!
    private let frameworkPath = "/System/Library/PrivateFrameworks/OnBoardingKit.framework/OnBoardingKit"
    
    init(
        title: NSString,
        detailText: NSString,
        symbolName: NSString?
    ) {
        dlopen(frameworkPath, RTLD_NOW)
        
        let initWithTitleDetailTextSymbolName = (@convention(c) (NSObject, Selector, NSString, NSString, NSString?) -> UIViewController).self
        
        let OBWelcomeController = NSClassFromString("OBWelcomeController") as! NSObject.Type
        let welcomeController = OBWelcomeController
            .perform(NSSelectorFromString("alloc"))
            .takeUnretainedValue() as! NSObject
        
        let selector = NSSelectorFromString("initWithTitle:detailText:symbolName:")
        let implementation = welcomeController.method(for: selector)
        let method = unsafeBitCast(implementation, to: initWithTitleDetailTextSymbolName.self)

        viewController = method(welcomeController, selector, title, detailText, symbolName)
    }
    
    func addBulletedListItem(
        title: NSString,
        description: NSString,
        symbolName: NSString,
        tintColor: UIColor = .tintColor
    ) {
        let addBulletedListItemWithTitleDescriptionSymbolNameTintColor = (@convention(c) (NSObject, Selector, NSString, NSString, NSString, UIColor) -> Void).self
        let selector = NSSelectorFromString("addBulletedListItemWithTitle:description:symbolName:tintColor:")
        let implementation = viewController.method(for: selector)
        let method = unsafeBitCast(implementation, to: addBulletedListItemWithTitleDescriptionSymbolNameTintColor.self)
        _ = method(viewController, selector, title, description, symbolName, tintColor)
    }
    
    func addBoldButton(
        title: NSString,
        action: @escaping () -> Void
    ) {
        let OBBoldTrayButton = NSClassFromString("OBBoldTrayButton") as! NSObject.Type
        let selector = NSSelectorFromString("boldButton")
        let button = OBBoldTrayButton.perform(selector).takeUnretainedValue() as! UIButton
        button.configuration?.title = String(title)
        button.addAction(UIAction { _ in action() }, for: .touchUpInside)
        
        let buttonTray = viewController.value(forKey: "buttonTray") as! NSObject
        buttonTray.perform(NSSelectorFromString("addButton:"), with: button)
    }
    
    func addLinkButton(
        title: NSString,
        action: @escaping () -> Void
    ) {
        let OBLinkTrayButton = NSClassFromString("OBLinkTrayButton") as! NSObject.Type
        let selector = NSSelectorFromString("linkButton")
        let button = OBLinkTrayButton.perform(selector).takeUnretainedValue() as! UIButton
        button.configuration?.title = String(title)
        button.addAction(UIAction { _ in action() }, for: .touchUpInside)
        
        let buttonTray = viewController.value(forKey: "buttonTray") as! NSObject
        buttonTray.perform(NSSelectorFromString("addButton:"), with: button)
    }
}

extension OBWelcomeController {
    struct OBWelcomeBulletedListItem {
        let title: NSString
        let description: NSString
        let symbolName: NSString
        
        init(
            title: NSString,
            description: NSString,
            symbolName: NSString
        ) {
            self.title = title
            self.description = description
            self.symbolName = symbolName
        }
    }
}

extension OBWelcomeController {
    struct OBWelcomeButtonItem {
        let title: NSString
        let action: () -> Void
    }
}
```

```swift
struct OBWelcomeView: UIViewControllerRepresentable {
    private let title: NSString
    private let detailText: NSString
    private let symbolName: NSString?
    private let bulletedListItems: [OBWelcomeController.OBWelcomeBulletedListItem]
    private let boldButtonItem: OBWelcomeController.OBWelcomeButtonItem
    private let linkButtonItem: OBWelcomeController.OBWelcomeButtonItem?
    
    init(
        title: NSString,
        detailText: NSString,
        symbolName: NSString? = nil,
        bulletedListItems: [OBWelcomeController.OBWelcomeBulletedListItem],
        boldButtonItem: OBWelcomeController.OBWelcomeButtonItem,
        linkButtonItem: OBWelcomeController.OBWelcomeButtonItem? = nil
    ) {
        self.title = title
        self.detailText = detailText
        self.symbolName = symbolName
        self.bulletedListItems = bulletedListItems
        self.boldButtonItem = boldButtonItem
        self.linkButtonItem = linkButtonItem
    }
    
    func makeUIViewController(context: Context) -> UIViewController {
        let welcomeController = OBWelcomeController(
            title: title,
            detailText: detailText,
            symbolName: symbolName
        )
        
        bulletedListItems.forEach { bulletedListItem in
            welcomeController.addBulletedListItem(
                title: bulletedListItem.title,
                description: bulletedListItem.description,
                symbolName: bulletedListItem.symbolName
            )
        }
        
        welcomeController.addBoldButton(title: boldButtonItem.title, action: boldButtonItem.action)
        
        if let linkButtonItem {
            welcomeController.addLinkButton(title: linkButtonItem.title, action: linkButtonItem.action)
        }
        
        return welcomeController.viewController
    }
    
    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
}
```

# 使用側
```swift
import SwiftUI

struct ContentView: View {
    @State private var isPresentedOBWelcomeView = false
    
    var body: some View {
        Button {
            isPresentedOBWelcomeView = true
        } label: {
            Text("OBWelcomeViewを表示")
        }
        .sheet(isPresented: $isPresentedOBWelcomeView) {
            OBWelcomeView(
                title: "ようこそサンプルアプリへ",
                detailText: "これはOnBoardingKitのサンプルアプリです",
                bulletedListItems: [
                    .init(title: "アプリの特徴1", description: "いろいろなことができます。", symbolName: "1.circle"),
                    .init(title: "アプリの特徴2", description: "いろいろなことができます。", symbolName: "2.circle"),
                    .init(title: "アプリの特徴3", description: "いろいろなことができます。", symbolName: "3.circle"),
                ],
                boldButtonItem: .init(title: "続ける", action: {
                    print("続ける")
                }),
                linkButtonItem: .init(title: "利用規約", action: {
                    print("利用規約")
                })
            )
        }
    }
}
```

# おわり
今回のOnBoardingKitは公式の非公開フレームワークでしたが、
サードパーティ製のライブラリで同じようなことができるものもあります。

https://github.com/SvenTiigi/WhatsNewKit

AppStoreにリリースするものであれば、非公開のフレームワークを使用すると審査落ちすると思うので、上記のライブラリを使用するのがいいと思います。

# 参考
https://github.com/sebjvidal/OnBoardingKit-Demo/blob/main/OnBoardingKit-Demo.swift