Share
Embed
画面最前面に表示されるデバッグツールを作る
2024/05/23(木) pixiv App Nightで発表した「画面最前面に表示されるデバッグツールを作る」の資料です
Atsuya Sato
May 23, 2024
Transcript
32 slides
-
atsuyan*1 / 𝕏 : ͋ͭ (@n_atmark) • ৽نࣄۀ෦ iOSΤϯδχΞ •
20242݄த్ೖࣾ • PastelaͷiPad͚ΞϓϦέʔγϣϯ։ൃΛ୲ *1: ࣾͰatsuyanͱ໊͍ͬͯ·͢: χοΫωʔϜ੍ͬͯϚδͰ͢ - pixiv inside *2: લ৬࣌ͷొஃࢿྉͰਓؒͷࠨ্ʹ͔ͬͬͯ·͕ͨ͠ɺຊମΛͬऔΒΕͯ͠·͍·ͨ͠ *2 -
ը໘࠷લ໘ʹදࣔ͞ΕΔσόοάπʔϧΛ࡞Δ • PastelaͰ͜ΜͳσόοάπʔϧΛͬͯ·͢ • CPU༻ / ϝϞϦ༻ྔ / FPS ͷදࣔ
• ϝϞϦάϥϑͷදࣔ • TCAͷAction࣮ߦϩάදࣔ -
• ඳըॲཧपΓͰFPS͕མͪͯͳ͍͔ɺಛఆͷૢ࡞ͰCPU͕ߴෛՙঢ়ଶʹ ͳͬͯͳ͍͔ͳͲͷ֬ೝͰ͍ͬͯ·͢ • FPS CADisplayLink , ϝϞϦ༻ྔͱCPU༻ task_info() thread_info()
ͳͲΛར༻ͯ͠ද͍ࣔͯ͠·͢ *1 *1: GDPerformanceView-Swift - GitHub ͷ࣮ SwiftͰΞϓϦͷCPU༻ͱϝϞϦ༻ྔΛऔಘ͢Δ - Qiita Λࢀߟʹ͠·ͨ͠ ɹCPU / ϝϞϦ༻ྔ / FPS ͷදࣔ -
• υϩʔը໘ͳͲͷϝϞϦΛଟ֬͘อ͢Δը໘͔ΒΕͨࡍʹɺ֬อͨ͠ϝϞϦ͕ղ ์͞Ε͍ͯΔ͔Ͳ͏͔ͷ҆ͱͯͬͯ͠·͢ • ΦϨϯδ৭ ͷΓ্͕͍ͬͯΔ෦͕ υϩʔը໘ 🎨 දࣔɹ •
৭ ͷ෦͕ ΩϟϯόεҰཡ 🖼 දࣔ • ࣮ઌ΄ͲͷϝϞϦ༻ྔΛ࣌ܥྻͰอଘ͓͍ͯͯ͠ Swift Charts Ͱද͍ࣔͯ͠Δ ɹϝϞϦάϥϑͷදࣔ -
• TCAͷύϑΥʔϚϯεͷ1ͭͰ͋Δ High-frequency actions *1 ʹؾ͖ͮ͘͢ ͢ΔͨΊʹ͍ͬͯ·͢ • TCAͷσόοάπʔϧͰ͋Δ _printChanges(_:)
ʹΧελϚΠζͨ͠ _ReducerPrinter Λઃఆ͢Δ͜ͱͰΞΫγϣϯϩάΛදࣔͰ͖ΔΑ͏ʹ͍ͯ͠·͢ *2 *1: ΉΈʹActionΛૹ৴͍͚ͯ͠ͳ͍ - SpeakerDeck Ͱৄ͘͠հ͞Ε͍ͯͯࢀߟʹͳΓ·ͨ͠ *2: σϑΥϧτͩͱ .customDump ͕ࢦఆ͞Ε͍ͯ·͢ɻ .actionLabels σϑΥϧτͰ༻ҙ͞Ε͍ͯ·͢ (DebugReducer.swift - swift-composable-architecture) ɹTCAͷAction࣮ߦϩάදࣔ -
ը໘࠷લ໘ʹදࣔ͢Δʹ • ӈͷΑ͏ͳUIΛ࣮͢Δ࣌ʹ1ը໘͚ͩͳΒoverlay ͢Δ͚ͩͰྑ͍͔͠Ε·ͤΜ͕… struct ContentView: View { var body:
some View { List { ForEach(0..<100) { index in Text(“\(index)”).foregroundStyle(.black) } } .overlay(alignment: .bottom) { PerformanceView() } } } -
ը໘࠷લ໘ʹදࣔ͢Δʹ • .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 -
ৗʹը໘࠷લ໘ʹදࣔ͢Δʹ • UIWindow Λར༻͢ΔࣄͰը໘࠷લ໘ʹৗʹUIΛදࣔ ͢Δ͜ͱ͕Ͱ͖·͢ (UIKit ͔࣌ΒΞϓϦΛॻ͍͍ͯΔਓʹೃછΈਂ͍ Ͱ͢Ͷ) Windows and
screens - Apple Developer -
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:) ͰॳظԽ͢Δ -
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 Λར༻Ͱ͖ΔΑ͏ʹ͢Δ -
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 {} -
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 } } -
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 Ͱ࣮ͨ͠ͷΛࢼ͢ͱ ࠷લ໘Ͱද͕ࣔ͞Ε͍ͯΔ🎉 -
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) Λࢦఆ͢Δͱ -
ίϥϜ: શը໘දࣔ͞Ε͍ͯΔ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)Ͱઃఆ͞Εͯ͠·͏ -
ίϥϜ: ࠷લ໘ͷUIWindowͱ • Α͘ isKeyWindow ͳ UIWindow ͕࠷લͩͱࢥΘΕ͍ͯΔ͕ਖ਼֬ʹෆे • UIWindowΛ࣌͢ʹ
makeKeyAndVisible() Λݺͳͯ͘ɺ৽͘͠Ճ͞Εͨ UIWindow͕લ໘ʹདྷΔ͜ͱ͕֬ೝͰ͖Δ keyWindow ΩʔϘʔυೖྗ ͳͲඇλονܥΠϕϯτΛड͚औΕΔ །ҰͷUIWindowͰ͋Δ͜ͱΛ͍ࣔͯ͠Δ͚ͩ (≒ଟ͘ͷ߹͜ΕΛຬͨ͢UIWindow͕࠷લͰ͋Δࣄ͕ଟ͍) -
• UIWindowSceneͷ var windows: [UIWindow] { get } ͷதͰ •
isHidden: Bool ͕ false • windowLevel: UIWindow.Level ͷ͕Ұ൪ߴ͘ • ಉҰ windowLevel ͷதͰ࠷ޙʹ visible ʹͳͬͨ UIWindow ίϥϜ: ࠷લ໘ͷUIWindowͱ -
• ಉҰ windowLevel ͷதͰ࠷ޙʹ visible ʹͳͬͨ UIWindow ίϥϜ: ࠷લ໘ͷUIWindowͱ UIWindow
ͷ visibility ʹมߋ͕͋ͬͨࡍʹ UIWindow.didBecomeVisibleNoti fi cation Ͱߋ৽͞Εͨ UIWindow ͕௨͞ΕΔͷͰɺ͜Εͷ௨͕࠷ޙʹ͞Εͨͷ*1 *1: Undocumented ͕ͩɺprivate APIΛίʔϧͯ͠ௐͯΈͨײ͓ͦ͡Β࣮͘ࡍͷදࣔॱܾఆ UIScene ͕ ෦Ͱอ͍࣋ͯ͠Δ NSArray *_visibleWindows ॱʹͳ͍ͬͯͦ͏ -
ΦʔόʔϨΠUIWindow্ͰεςʔλεόʔΛ ϋϯυϦϯά͢Δtips • εςʔλεόʔͷελΠϧʹؔͯ͠ ࠷લ໘ͷUIWindow ʹඥͮ͘ rootViewController͕ج४ʹͳͬͯ͠·͏ • → ্ʹදࣔ͢ΔUIWindowͷ
rootViewController.childForStatusBarStyle ͕ϝΠϯͷUIWindowͷ rootViewController ͷͷΛࢦఆͰ͖Ε UIWindowΛॏͶͭͭɺεςʔλεόʔΛϋϯυϦϯάͰ͖Δ -
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 Λࢀߟʹ͠·ͨ͠ -
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 Λࢀߟʹ͠·ͨ͠ -
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 } }