Slide 1

Slide 1 text

2024/05/23 pixiv App Night / atsuyan (@n_atmark) ը໘࠷લ໘ʹදࣔ͞ΕΔσόοάπʔϧΛ࡞Δ

Slide 2

Slide 2 text

atsuyan*1 / 𝕏 : ͋ͭ΍ (@n_atmark) • ৽نࣄۀ෦ iOSΤϯδχΞ • 2024೥2݄த్ೖࣾ • PastelaͷiPad޲͚ΞϓϦέʔγϣϯ։ൃΛ୲౰ *1: ࣾ಺Ͱ͸atsuyanͱ໊৐͍ͬͯ·͢: χοΫωʔϜ੍౓ͬͯϚδͰ͢ - pixiv inside *2: લ৬࣌୅ͷొஃࢿྉͰ͸ਓؒͷࠨ্ʹ৐͔ͬͬͯ·͕ͨ͠ɺຊମΛ৐ͬऔΒΕͯ͠·͍·ͨ͠ *2

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

ը໘࠷લ໘ʹදࣔ͞ΕΔσόοάπʔϧΛ࡞Δ • PastelaͰ͸͜ΜͳσόοάπʔϧΛ࢖ͬͯ·͢ • CPU࢖༻཰ / ϝϞϦ࢖༻ྔ / FPS ͷදࣔ • ϝϞϦάϥϑͷදࣔ • TCAͷAction࣮ߦϩάදࣔ

Slide 5

Slide 5 text

ɹCPU / ϝϞϦ࢖༻ྔ / FPS ͷදࣔ

Slide 6

Slide 6 text

• ඳըॲཧपΓͰFPS͕མͪͯͳ͍͔ɺಛఆͷૢ࡞ͰCPU͕ߴෛՙঢ়ଶʹ ͳͬͯͳ͍͔ͳͲͷ֬ೝͰ࢖͍ͬͯ·͢ • FPS͸ CADisplayLink , ϝϞϦ࢖༻ྔͱCPU࢖༻཰͸ task_info() thread_info() ͳͲΛར༻ͯ͠ද͍ࣔͯ͠·͢ *1 *1: GDPerformanceView-Swift - GitHub ͷ࣮૷΍ SwiftͰΞϓϦͷCPU࢖༻཰ͱϝϞϦ࢖༻ྔΛऔಘ͢Δ - Qiita Λࢀߟʹ͠·ͨ͠ ɹCPU / ϝϞϦ࢖༻ྔ / FPS ͷදࣔ

Slide 7

Slide 7 text

ɹϝϞϦάϥϑͷදࣔ

Slide 8

Slide 8 text

• υϩʔը໘ͳͲͷϝϞϦΛଟ֬͘อ͢Δը໘͔Β཭Εͨࡍʹɺ֬อͨ͠ϝϞϦ͕ղ ์͞Ε͍ͯΔ͔Ͳ͏͔ͷ໨҆ͱͯ͠࢖ͬͯ·͢ • ΦϨϯδ৭ ͷ੝Γ্͕͍ͬͯΔ෦෼͕ υϩʔը໘ 🎨 දࣔɹ • ྘৭ ͷ෦෼͕ ΩϟϯόεҰཡ 🖼 දࣔ • ࣮૷͸ઌ΄ͲͷϝϞϦ࢖༻ྔΛ࣌ܥྻͰอଘ͓͍ͯͯ͠ Swift Charts Ͱද͍ࣔͯ͠Δ ɹϝϞϦάϥϑͷදࣔ

Slide 9

Slide 9 text

ɹTCAͷAction࣮ߦϩάදࣔ

Slide 10

Slide 10 text

• TCAͷύϑΥʔϚϯε໰୊ͷ1ͭͰ͋Δ High-frequency actions *1 ʹؾ͖ͮ΍͘͢ ͢ΔͨΊʹ࢖͍ͬͯ·͢ • TCAͷσόοάπʔϧͰ͋Δ _printChanges(_:) ʹΧελϚΠζͨ͠ _ReducerPrinter Λઃఆ͢Δ͜ͱͰΞΫγϣϯϩάΛදࣔͰ͖ΔΑ͏ʹ͍ͯ͠·͢ *2 *1: Ή΍ΈʹActionΛૹ৴ͯ͠͸͍͚ͳ͍ - SpeakerDeck Ͱৄ͘͠঺հ͞Ε͍ͯͯࢀߟʹͳΓ·ͨ͠ *2: σϑΥϧτͩͱ .customDump ͕ࢦఆ͞Ε͍ͯ·͢ɻ .actionLabels ΋σϑΥϧτͰ༻ҙ͞Ε͍ͯ·͢ (DebugReducer.swift - swift-composable-architecture) ɹTCAͷAction࣮ߦϩάදࣔ

Slide 11

Slide 11 text

ը໘࠷લ໘ʹදࣔ͢Δʹ͸ • ӈͷΑ͏ͳUIΛ࣮૷͢Δ࣌ʹ1ը໘͚ͩͳΒoverlay ͢Δ͚ͩͰྑ͍͔΋͠Ε·ͤΜ͕… struct ContentView: View { var body: some View { List { ForEach(0..<100) { index in Text(“\(index)”).foregroundStyle(.black) } } .overlay(alignment: .bottom) { PerformanceView() } } }

Slide 12

Slide 12 text

ը໘࠷લ໘ʹදࣔ͢Δʹ͸ • .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

Slide 13

Slide 13 text

ৗʹը໘࠷લ໘ʹදࣔ͢Δʹ͸ • UIWindow Λར༻͢ΔࣄͰը໘࠷લ໘ʹৗʹUIΛදࣔ ͢Δ͜ͱ͕Ͱ͖·͢ (UIKit ࣌୅͔ΒΞϓϦΛॻ͍͍ͯΔਓʹ͸ೃછΈਂ͍ Ͱ͢Ͷ) Windows and screens - Apple Developer

Slide 14

Slide 14 text

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:) ͰॳظԽ͢Δ

Slide 15

Slide 15 text

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 Λར༻Ͱ͖ΔΑ͏ʹ͢Δ

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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 Ͱ࣮૷ͨ͠΋ͷΛࢼ͢ͱ ࠷લ໘Ͱද͕ࣔ͞Ε͍ͯΔ🎉

Slide 19

Slide 19 text

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) Λࢦఆ͢Δͱ

Slide 20

Slide 20 text

UIWindowΛར༻͢Δ࣌ͷ஫ҙ఺ overlayWindowදࣔͳ͠ overlayWindowදࣔ͋Γ .statusBar(hidden: true) Λࢦఆ͍ͯ͠Δ ͕εςʔλεόʔ͕දࣔ͞Εͯ͠·͏ (ɹ Pastelaͷυϩʔը໘΋εςʔλε όʔӅ͍ͯ͠ΔͷͰࠔͬͨɻɻɻ)

Slide 21

Slide 21 text

ΦʔόʔϨΠUIWindow্ͰεςʔλεόʔΛ ϋϯυϦϯά͢Δtips • εςʔλεόʔͷදࣔʹؔͯ͠͸ શը໘දࣔ͞Ε͍ͯΔ UIWindowͷ͏ͪ ࠷લ໘දࣔ͞Ε͍ͯΔ UIWindowʹඥͮ͘rootViewController͕ج४ʹͳͬͯ͠·͏

Slide 22

Slide 22 text

ίϥϜ: શը໘දࣔ͞Ε͍ͯΔ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)Ͱઃఆ͞Εͯ͠·͏

Slide 23

Slide 23 text

ίϥϜ: ࠷લ໘ͷUIWindowͱ͸ • Α͘ isKeyWindow ͳ UIWindow ͕࠷લͩͱࢥΘΕ͍ͯΔ͕ਖ਼֬ʹ͸ෆे෼ • UIWindowΛ଍࣌͢ʹ makeKeyAndVisible() Λݺ͹ͳͯ͘΋ɺ৽͘͠௥Ճ͞Εͨ UIWindow͕લ໘ʹདྷΔ͜ͱ͕֬ೝͰ͖Δ keyWindow ͸ ΩʔϘʔυೖྗ ͳͲඇλονܥΠϕϯτΛड͚औΕΔ །ҰͷUIWindowͰ͋Δ͜ͱΛ͍ࣔͯ͠Δ͚ͩ (≒ଟ͘ͷ৔߹͜ΕΛຬͨ͢UIWindow͕࠷લͰ͋Δࣄ͕ଟ͍)

Slide 24

Slide 24 text

• UIWindowSceneͷ var windows: [UIWindow] { get } ͷதͰ • isHidden: Bool ͕ false • windowLevel: UIWindow.Level ͷ஋͕Ұ൪ߴ͘ • ಉҰ windowLevel ͷதͰ࠷ޙʹ visible ʹͳͬͨ UIWindow ίϥϜ: ࠷લ໘ͷUIWindowͱ͸

Slide 25

Slide 25 text

• ಉҰ windowLevel ͷதͰ࠷ޙʹ visible ʹͳͬͨ UIWindow ίϥϜ: ࠷લ໘ͷUIWindowͱ͸ UIWindow ͷ visibility ʹมߋ͕͋ͬͨࡍʹ UIWindow.didBecomeVisibleNoti fi cation Ͱߋ৽͞Εͨ UIWindow ͕௨஌͞ΕΔͷͰɺ͜Εͷ௨஌͕࠷ޙʹ͞Εͨ΋ͷ*1 *1: Undocumented ͕ͩɺprivate APIΛίʔϧͯ͠ௐ΂ͯΈͨײ͓ͦ͡Β࣮͘ࡍͷදࣔॱܾఆ͸ UIScene ͕ ಺෦Ͱอ͍࣋ͯ͠Δ NSArray *_visibleWindows ॱʹͳ͍ͬͯͦ͏

Slide 26

Slide 26 text

ΦʔόʔϨΠUIWindow্ͰεςʔλεόʔΛ ϋϯυϦϯά͢Δtips • εςʔλεόʔͷελΠϧʹؔͯ͠͸ ࠷લ໘ͷUIWindow ʹඥͮ͘ rootViewController͕ج४ʹͳͬͯ͠·͏ • → ্ʹදࣔ͢ΔUIWindowͷ rootViewController.childForStatusBarStyle ͕ϝΠϯͷUIWindowͷ rootViewController ͷ΋ͷΛࢦఆͰ͖Ε͹ UIWindowΛॏͶͭͭɺεςʔλεόʔΛϋϯυϦϯάͰ͖Δ

Slide 27

Slide 27 text

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 Λࢀߟʹ͠·ͨ͠

Slide 28

Slide 28 text

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 Λࢀߟʹ͠·ͨ͠

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

UIWindowͷ rootViewController.childForStatusBarStyle ʹϝΠϯͷUIWindowͷ rootViewController ͷ΋ͷΛࢦఆ͢Δ͜ͱͰεςʔλεόʔΛӅͭͭ͠લ໘දࣔͰ͖ͨ🎉

Slide 31

Slide 31 text

σόοάπʔϧҎ֎Ͱ΋׆༻Ͱ͖Δ࠷લ໘දࣔ • PastelaͰ͸ଞʹ΋࠷લ໘දࣔΛ৭ʑͳՕॴͰ׆༻͍ͯ͠·͢ શը໘ϩʔσΟϯάදࣔ τʔετදࣔ

Slide 32

Slide 32 text

·ͱΊ • ࠷લ໘දࣔͰ͖Δσόοάπʔϧ͸υοάϑʔσΟϯάͳͲͷࡍʹαΫοͱ ύϑΥʔϚϯεΛ͔֬ΊΒΕͯศར • UIWindowΛ࢖͏ࣄͰSwiftUI੡ͷը໘Ͱ΋࠷લ໘ද͕ࣔͰ͖Δ • ੍ͨͩ͠໿΋͋ΔͷͰར༻͢Δࡍ͸ࡉ͔ͳڍಈʹཁ஫ҙ • pixivͷ৽͍͠ϖΠϯτπʔϧPastelaɺ৮ͬͯΈͯͶʂ