Upgrade to Pro — share decks privately, control downloads, hide ads and more …

画面最前面に表示されるデバッグツールを作る

 画面最前面に表示されるデバッグツールを作る

2024/05/23(木) pixiv App Nightで発表した「画面最前面に表示されるデバッグツールを作る」の資料です

Atsuya Sato

May 23, 2024
Tweet

More Decks by Atsuya Sato

Other Decks in Technology

Transcript

  1. atsuyan*1 / 𝕏 : ͋ͭ΍ (@n_atmark) • ৽نࣄۀ෦ iOSΤϯδχΞ •

    2024೥2݄த్ೖࣾ • PastelaͷiPad޲͚ΞϓϦέʔγϣϯ։ൃΛ୲౰ *1: ࣾ಺Ͱ͸atsuyanͱ໊৐͍ͬͯ·͢: χοΫωʔϜ੍౓ͬͯϚδͰ͢ - pixiv inside *2: લ৬࣌୅ͷొஃࢿྉͰ͸ਓؒͷࠨ্ʹ৐͔ͬͬͯ·͕ͨ͠ɺຊମΛ৐ͬऔΒΕͯ͠·͍·ͨ͠ *2
  2. • ඳըॲཧपΓͰFPS͕མͪͯͳ͍͔ɺಛఆͷૢ࡞ͰCPU͕ߴෛՙঢ়ଶʹ ͳͬͯͳ͍͔ͳͲͷ֬ೝͰ࢖͍ͬͯ·͢ • FPS͸ CADisplayLink , ϝϞϦ࢖༻ྔͱCPU࢖༻཰͸ task_info() thread_info()

    ͳͲΛར༻ͯ͠ද͍ࣔͯ͠·͢ *1 *1: GDPerformanceView-Swift - GitHub ͷ࣮૷΍ SwiftͰΞϓϦͷCPU࢖༻཰ͱϝϞϦ࢖༻ྔΛऔಘ͢Δ - Qiita Λࢀߟʹ͠·ͨ͠ ɹCPU / ϝϞϦ࢖༻ྔ / FPS ͷදࣔ
  3. • υϩʔը໘ͳͲͷϝϞϦΛଟ֬͘อ͢Δը໘͔Β཭Εͨࡍʹɺ֬อͨ͠ϝϞϦ͕ղ ์͞Ε͍ͯΔ͔Ͳ͏͔ͷ໨҆ͱͯ͠࢖ͬͯ·͢ • ΦϨϯδ৭ ͷ੝Γ্͕͍ͬͯΔ෦෼͕ υϩʔը໘ 🎨 දࣔɹ •

    ྘৭ ͷ෦෼͕ ΩϟϯόεҰཡ 🖼 දࣔ • ࣮૷͸ઌ΄ͲͷϝϞϦ࢖༻ྔΛ࣌ܥྻͰอଘ͓͍ͯͯ͠ Swift Charts Ͱද͍ࣔͯ͠Δ ɹϝϞϦάϥϑͷදࣔ
  4. • TCAͷύϑΥʔϚϯε໰୊ͷ1ͭͰ͋Δ High-frequency actions *1 ʹؾ͖ͮ΍͘͢ ͢ΔͨΊʹ࢖͍ͬͯ·͢ • TCAͷσόοάπʔϧͰ͋Δ _printChanges(_:)

    ʹΧελϚΠζͨ͠ _ReducerPrinter Λઃఆ͢Δ͜ͱͰΞΫγϣϯϩάΛදࣔͰ͖ΔΑ͏ʹ͍ͯ͠·͢ *2 *1: Ή΍ΈʹActionΛૹ৴ͯ͠͸͍͚ͳ͍ - SpeakerDeck Ͱৄ͘͠঺հ͞Ε͍ͯͯࢀߟʹͳΓ·ͨ͠ *2: σϑΥϧτͩͱ .customDump ͕ࢦఆ͞Ε͍ͯ·͢ɻ .actionLabels ΋σϑΥϧτͰ༻ҙ͞Ε͍ͯ·͢ (DebugReducer.swift - swift-composable-architecture) ɹTCAͷAction࣮ߦϩάදࣔ
  5. ը໘࠷લ໘ʹදࣔ͢Δʹ͸ • ӈͷΑ͏ͳUIΛ࣮૷͢Δ࣌ʹ1ը໘͚ͩͳΒoverlay ͢Δ͚ͩͰྑ͍͔΋͠Ε·ͤΜ͕… struct ContentView: View { var body:

    some View { List { ForEach(0..<100) { index in Text(“\(index)”).foregroundStyle(.black) } } .overlay(alignment: .bottom) { PerformanceView() } } }
  6. ը໘࠷લ໘ʹදࣔ͢Δʹ͸ • .sheet(item:content:) ΍ .fullScreenCover(item:content:) ͕ར༻͞ΕΔͱɺ্͔Βදࣔ͞ΕΔViewͷํ͕લʹ දࣔ͞Εͯ͠·͍·͢ struct ContentView: View

    { @State private var sheetItem: SheetItem? = nil var body: some View { List { ForEach(0..<100) { index in Button(action: { sheetItem = .init(index: index) }, label: { Text("\(index)").foregroundStyle(.black) }) .sheet(item: $sheetItem) { sheetItem in DetailView(index: sheetItem.index) } } } .overlay(alignment: .bottom) { ŋŋŋ } } } Sheetͷ಺༰্͕͔Βॏͳͬͯ͠·͏༷ࢠͷgif
  7. SwiftUIͰUIWindowΛར༻͢Δʹ͸ 1. UIApplicationDelegateAdaptor Λར༻ͯ͠ AppDelegate Λར༻Ͱ͖ΔΑ͏ ʹ͢Δ 2. AppDelegate ͷ

    application(_:con fi gurationForConnecting:options:) -> UISceneCon fi guration Ͱ delegateClass ʹ SceneDelegate Λࢦఆͨ͠ UISceneCon fi guration Λฦ٫͢Δ 3. SceneDelegate ͷ scene(_:willConnectTo:options) Ͱऔಘͨ͠ windowScene Λ༻͍ͯ UIWindow.init(windowScene:) ͰॳظԽ͢Δ
  8. SwiftUIͰUIWindowΛར༻͢Δʹ͸ @main struct SampleApp: App { @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate var

    body: some Scene { WindowGroup { ContentView() } } } final class AppDelegate: NSObject, UIApplicationDelegate {} 1. UIApplicationDelegateAdaptor Λར༻ͯ͠ AppDelegate Λར༻Ͱ͖ΔΑ͏ʹ͢Δ
  9. SwiftUIͰUIWindowΛར༻͢Δʹ͸ 2. AppDelegate ͷ application(_:con fi gurationForConnecting:options:) -> UISceneCon fi

    guration Ͱ delegateClass ʹ SceneDelegate Λࢦఆͨ͠ UISceneCon fi guration Λฦ٫͢Δ final class AppDelegate: NSObject, UIApplicationDelegate { func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { let configuration = UISceneConfiguration(name: nil, sessionRole: connectingSceneSession.role) configuration.delegateClass = SceneDelegate.self return configuration } } final class SceneDelegate: NSObject, UIWindowSceneDelegate, ObservableObject {}
  10. SwiftUIͰUIWindowΛར༻͢Δʹ͸ 3. SceneDelegate ͷ scene(_:willConnectTo:options) Ͱऔಘͨ͠ windowScene Λ༻͍ͯ UIWindow.init(windowScene:) ͰॳظԽ͢Δ

    final class SceneDelegate: NSObject, UIWindowSceneDelegate, ObservableObject { var window: UIWindow? var overlayWindow: UIWindow? func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { guard let windowScene = scene as? UIWindowScene else { return } window = windowScene.keyWindow // ࠷લ໘ͷUIWindow let overlayWindow = UIWindow(windowScene: windowScene) // ࠷લදࣔ༻ͷΧελϜWindow overlayWindow.isHidden = false overlayWindow.isUserInteractionEnabled = false let rootViewController = UIHostingController(rootView: PerformanceView().frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom)) rootViewController.view.backgroundColor = .clear overlayWindow.rootViewController = rootViewController self.overlayWindow = overlayWindow } }
  11. SwiftUIͰUIWindowΛར༻͢Δʹ͸ UIWindowͩͱsheetͷ্ʹ΋UIදࣔͰ͖Δ༷ࢠͷgif struct ContentView: View { @State private var sheetItem:

    SheetItem? = nil var body: some View { List { ŋŋŋ } // .overlay(alignment: .bottom) { // PerformanceView() // } } } .overlay(alignment:content:) Ͱ࣮૷͍ͯͨ͠ՕॴΛ ίϝϯτΞ΢τͯ͠UIWindow Ͱ࣮૷ͨ͠΋ͷΛࢼ͢ͱ ࠷લ໘Ͱද͕ࣔ͞Ε͍ͯΔ🎉
  12. UIWindowΛར༻͢Δ࣌ͷ஫ҙ఺ struct ContentView: View { @State private var sheetItem: SheetItem?

    = nil var body: some View { List { ForEach(0..<100) { index in Button(action: { sheetItem = .init(index: index) }, label: { Text("\(index)").foregroundStyle(.black) }) - .sheet(item: $sheetItem) { sheetItem in + .fullScreenCover(item: $sheetItem) { sheetItem in DetailView(index: sheetItem.index) + .statusBar(hidden: true) } } } } } .sheet(item:content:) Λ .fullScreenCover(item:content:) ʹม͑ ͯ .statusBar(hidden: true) Λࢦఆ͢Δͱ
  13. ίϥϜ: શը໘දࣔ͞Ε͍ͯΔUIWindowͱ͸ • window.frame == windowScene.coordinateSpace.bounds Λຬͨ͢UIWindow • ͪͳΈʹ… UIWindowͷframeαΠζઃఆΛ͢Δ࣌

    window.frame = .init(x: x, y: y, width: windowScene.coordinateSpace.bounds.width height: windowScene.coordinateSpace.bounds.height) ੺ͷ෦෼Λຬͨ͢ͱx, yʹԿΛೖΕͯ΋frame.origin͕CGPoint(0, 0)Ͱઃఆ͞Εͯ͠·͏
  14. ίϥϜ: ࠷લ໘ͷUIWindowͱ͸ • Α͘ isKeyWindow ͳ UIWindow ͕࠷લͩͱࢥΘΕ͍ͯΔ͕ਖ਼֬ʹ͸ෆे෼ • UIWindowΛ଍࣌͢ʹ

    makeKeyAndVisible() Λݺ͹ͳͯ͘΋ɺ৽͘͠௥Ճ͞Εͨ UIWindow͕લ໘ʹདྷΔ͜ͱ͕֬ೝͰ͖Δ keyWindow ͸ ΩʔϘʔυೖྗ ͳͲඇλονܥΠϕϯτΛड͚औΕΔ །ҰͷUIWindowͰ͋Δ͜ͱΛ͍ࣔͯ͠Δ͚ͩ (≒ଟ͘ͷ৔߹͜ΕΛຬͨ͢UIWindow͕࠷લͰ͋Δࣄ͕ଟ͍)
  15. • UIWindowSceneͷ var windows: [UIWindow] { get } ͷதͰ •

    isHidden: Bool ͕ false • windowLevel: UIWindow.Level ͷ஋͕Ұ൪ߴ͘ • ಉҰ windowLevel ͷதͰ࠷ޙʹ visible ʹͳͬͨ UIWindow ίϥϜ: ࠷લ໘ͷUIWindowͱ͸
  16. • ಉҰ windowLevel ͷதͰ࠷ޙʹ visible ʹͳͬͨ UIWindow ίϥϜ: ࠷લ໘ͷUIWindowͱ͸ UIWindow

    ͷ visibility ʹมߋ͕͋ͬͨࡍʹ UIWindow.didBecomeVisibleNoti fi cation Ͱߋ৽͞Εͨ UIWindow ͕௨஌͞ΕΔͷͰɺ͜Εͷ௨஌͕࠷ޙʹ͞Εͨ΋ͷ*1 *1: Undocumented ͕ͩɺprivate APIΛίʔϧͯ͠ௐ΂ͯΈͨײ͓ͦ͡Β࣮͘ࡍͷදࣔॱܾఆ͸ UIScene ͕ ಺෦Ͱอ͍࣋ͯ͠Δ NSArray *_visibleWindows ॱʹͳ͍ͬͯͦ͏
  17. ΦʔόʔϨΠUIWindow্ͰεςʔλεόʔΛ ϋϯυϦϯά͢Δtips • εςʔλεόʔͷελΠϧʹؔͯ͠͸ ࠷લ໘ͷUIWindow ʹඥͮ͘ rootViewController͕ج४ʹͳͬͯ͠·͏ • → ্ʹදࣔ͢ΔUIWindowͷ

    rootViewController.childForStatusBarStyle ͕ϝΠϯͷUIWindowͷ rootViewController ͷ΋ͷΛࢦఆͰ͖Ε͹ UIWindowΛॏͶͭͭɺεςʔλεόʔΛϋϯυϦϯάͰ͖Δ
  18. public final class HUDHostingViewController: UIViewController { private weak var mainWindow:

    UIWindow? fileprivate init(mainWindow: UIWindow?) { self.mainWindow = mainWindow super.init(nibName: nil, bundle: nil) } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } // εςʔλεόʔͷελΠϧ͕mainWindowʹඥͮ͘rootViewControllerͷঢ়ଶʹΑͬͯมΘΔΑ͏ʹ͢Δ override public var childForStatusBarStyle: UIViewController? { return mainWindow?.rootViewController?.childForStatusBarStyle } } UIWindowͷ rootViewController ʹࢦఆ͢Δ༻ͷΧελϚΠζͨ͠UIViewController*1 *1: How to handle status bar with custom overlay UIWindow - Swift Discovery Λࢀߟʹ͠·ͨ͠
  19. public final class HUDWindow: UIWindow, ObservableObject { private weak var

    mainWindow: UIWindow? fileprivate init(windowScene: UIWindowScene, mainWindow: UIWindow?) { self.mainWindow = mainWindow super.init(windowScene: windowScene) } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } // HUDWindowͷrootViewControllerΛΦʔόʔϥΠυ͢Δ͜ͱͰɺHUDHostingViewControllerͷchildForStatusBarStyle͕ίʔϧ͞ΕΔͨΊͷ setNeedsStatusBarAppearanceUpdate͕ݺ͹ΕΔλΠϛϯάΛϑοΫͰ͖Δ override public var rootViewController: UIViewController? { get { mainWindow?.rootViewController } set { super.rootViewController = newValue } } } rootViewController ΛΦʔόʔϥΠυͨ͠ΧελϚΠζͷUIWindow*1 *1: How to handle status bar with custom overlay UIWindow - Swift Discovery Λࢀߟʹ͠·ͨ͠
  20. final class SceneDelegate: NSObject, UIWindowSceneDelegate, ObservableObject { var window: UIWindow?

    var overlayWindow: UIWindow? func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { guard let windowScene = scene as? UIWindowScene else { return } window = windowScene.keyWindow let overlayWindow = HUDWindow(windowScene: windowScene, mainWindow: window) overlayWindow.isHidden = false overlayWindow.isUserInteractionEnabled = false let hudHostingViewController = HUDHostingViewController(mainWindow: window) hudHostingViewController.view.backgroundColor = .clear overlayWindow.rootViewController = hudHostingViewController let rootController = UIHostingController(rootView: PerformanceView().frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom)) rootController.view.backgroundColor = .clear rootController.view.translatesAutoresizingMaskIntoConstraints = false hudHostingViewController.addChild(rootController) hudHostingViewController.view.addSubview(rootController.view) NSLayoutConstraint.activate([ rootController.view.leadingAnchor.constraint(equalTo: hudHostingViewController.view.leadingAnchor), rootController.view.trailingAnchor.constraint(equalTo: hudHostingViewController.view.trailingAnchor), rootController.view.topAnchor.constraint(equalTo: hudHostingViewController.view.topAnchor), rootController.view.bottomAnchor.constraint(equalTo: hudHostingViewController.view.bottomAnchor) ]) rootController.didMove(toParent: hudHostingViewController) self.overlayWindow = overlayWindow } }