3
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?

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

Posted at

はじめに

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

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

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

サンプルアプリ

Simulator Screen Recording - iPhone 15 Pro - 2024-02-25 at 22.18.51.gif

実装

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
    }
}
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) {}
}

使用側

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は公式の非公開フレームワークでしたが、
サードパーティ製のライブラリで同じようなことができるものもあります。

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

参考

3
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
3
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?