$30 off During Our Annual Pro Sale. View Details »

Swift UIで始めるmacOSメニューバーアプリ

Swift UIで始めるmacOSメニューバーアプリ

株式会社ヌーラボ
PRO

April 22, 2022
Tweet

More Decks by 株式会社ヌーラボ

Other Decks in Technology

Transcript

  1. 2022೥4݄22೔ NuCon 2022 Spring


    দຊ༟ೋ
    Swift UIͰ࢝ΊΔ
    macOSϝχϡʔόʔΞϓϦ

    View Slide

  2. ͭͬͨ͘ΞϓϦ

    View Slide

  3. FreeeͷۈଵΞϓϦ
    ग़ۈͱୀۈΛߦ͏͚ͩͷγϯϓϧͳΞϓϦ

    (ψʔϥϘࣾ಺͚ͩͰ഑෍த)

    View Slide

  4. SwiftUI

    View Slide

  5. SwiftUI
    • એݴܕγϯλοΫεͳUIϑϨʔϜϫʔΫ

    • macOS, iOS, iPadOS, tvOS, watchOSͰ࢖͑Δ

    • XcodeͰͷϓϨϏϡʔ΍σβΠϯαϙʔτ
    AppKit
    • MVCͳUIϑϨʔϜϫʔΫ

    • macOSͰ࢖͑Δ

    • NSʙͱ͍͏pre
    fi
    x͕෇͍͍ͯΔ

    NBD04ʙ NBD04ʙ
    /FYU4UFQʜ

    View Slide

  6. Ψϫͷ෦෼͸4XJGU6*Ͱ௚઀࡞Εͳ͍ /41PQPWFS

    4XJGU6*Ͱ௚઀࡞Εͳ͍ /44UBUVT*UFN/44UBUVT#BS#VUUPO

    7JFX 1PQPWFS

    View Slide

  7. ϝχϡʔόʔͷΫϦοΫͰ


    SwiftUIͷϏϡʔΛϙοϓΞοϓͤ͞Δ·Ͱ

    View Slide

  8. View Slide

  9. DemoApp.swift
    import SwiftUI


    @main


    struct DemoApp: App {


    var body: some Scene {


    WindowGroup {


    ContentView()


    }


    }


    }


    import SwiftUI


    struct ContentView: View {


    var body: some View {


    Text("Hello, world!")


    .padding()


    }


    }
    ContentView.swift

    View Slide

  10. @NSApplicationDelegateAdaptor
    import SwiftUI


    @main


    struct DemoApp: App {


    @NSApplicationDelegateAdaptor(AppDelegate.self) var delegate


    var body: some Scene {


    WindowGroup {


    ContentView()


    }


    }


    }


    class AppDelegate: NSObject, NSApplicationDelegate {


    }


    ΞϓϦͷىಈ௚ޙʹݺ͹ΕΔϝιουͳͲ͕࢖͑ΔΑ͏ʹͳΔ

    View Slide



  11. class AppDelegate: NSObject, NSApplicationDelegate {


    }
    private var statusItem: NSStatusItem?


    func applicationDidFinishLaunching(_ notification: Notification) {


    statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)


    let button = statusItem!.button!


    button.image = NSImage(systemSymbolName: "leaf", accessibilityDescription: nil)


    }


    4'4ZNCPMT͕࢖͑Δ
    ΞϓϦͷىಈ௚ޙʹݺ͹ΕΔϝιου

    View Slide



  12. class AppDelegate: NSObject, NSApplicationDelegate {


    }
    private var statusItem: NSStatusItem?


    private var popover: NSPopover?


    func applicationDidFinishLaunching(_ notification: Notification) {


    statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)


    let button = statusItem!.button!


    button.image = NSImage(systemSymbolName: "leaf", accessibilityDescription: nil)


    button.action = #selector(showPopover)


    }


    @objc func showPopover(_ sender: NSStatusBarButton) {


    if popover == nil {


    let popover = NSPopover()


    popover.contentViewController = NSHostingController(rootView: ContentView())


    self.popover = popover


    }


    popover?.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxY)


    }
    4XJGU6*Λϗετͯ͘͠ΕΔ"QQ,JUͷίϯτϩʔϥ

    View Slide



  13. class AppDelegate: NSObject, NSApplicationDelegate {


    }
    private var statusItem: NSStatusItem?


    private var popover: NSPopover?


    func applicationDidFinishLaunching(_ notification: Notification) {


    NSApp.windows.forEach{ $0.close() }


    NSApp.setActivationPolicy(.accessory)


    statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)


    let button = statusItem!.button!


    button.image = NSImage(systemSymbolName: "leaf", accessibilityDescription: nil)


    button.action = #selector(showPopover)


    }


    @objc func showPopover(_ sender: NSStatusBarButton) {


    if popover == nil {


    let popover = NSPopover()


    popover.contentViewController = NSHostingController(rootView: ContentView())


    popover.behavior = .transient


    popover.animates = false


    self.popover = popover


    }


    popover?.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxY)


    NSApp.activate(ignoringOtherApps: true)


    }
    ΞϓϦىಈதʹ%PDL͔ΒΞΠίϯΛফ͢
    ΞϓϦىಈ௚ޙͷ΢Οϯυ΢Λશ෦ফ͢
    ΞϓϦҎ֎ͷྖҬͷΫϦοΫͰϙοϓΞοϓ͕ด͡ΔΑ͏ʹ͢Δ
    ΞϓϦΛΞΫςΟϒʹ͠ͳ͍ͱ͏·͘ด͡ͳ͍

    View Slide

  14. private var statusItem: NSStatusItem?


    @main


    struct DemoApp: App {


    #if os(macOS)


    @NSApplicationDelegateAdaptor(AppDelegate.self) var delegate


    #endif


    var body: some Scene {


    WindowGroup {


    ContentView()


    }


    }


    }


    #if os(macOS)


    class AppDelegate: NSObject, NSApplicationDelegate {





    }


    #endif

    View Slide

  15. SwiftUIΛ؆୯ʹ঺հ

    View Slide

  16. ContentView.swift
    import SwiftUI


    struct ContentView: View {


    var body: some View {


    Text("Hello, world!")


    .padding()


    }


    }


    struct ContentView_Previews: PreviewProvider {


    static var previews: some View {


    ContentView()


    }


    }


    View Slide



  17. struct ContentView: View {


    var body: some View {


    Button {


    print("pushed")


    } label: {


    HStack {


    Image(systemName: "face.smiling")


    Text("Hello, world!")


    }


    }


    .padding()


    }


    }


    View Slide

  18. struct ContentView: View {


    @State var point: CGFloat = -1




    var body: some View {


    let gradient = LinearGradient(gradient: Gradient(colors: [.blue, .green, .blue]),


    startPoint: UnitPoint(x: point, y: 0),


    endPoint: UnitPoint(x: 1.0 + point, y: 0))


    VStack {


    Capsule()


    .fill(gradient)


    .animation(Animation.linear(duration: 2)


    .repeatForever(autoreverses: false), value: point)


    .onAppear {


    point = 1


    }


    .frame(width: 200, height: 10, alignment: .leading)


    .padding()


    Button {


    print("pushed")


    } label: {


    HStack {


    Image(systemName: "face.smiling")


    Text("Hello, world!")


    }


    }


    }


    .padding()


    }


    }


    View Slide

  19. Tips

    View Slide

  20. ӈΫϦοΫϝχϡʔΛͭ͘Δ
    @objc func showPopover(_ sender: NSStatusBarButton) {


    guard let event = NSApp.currentEvent else { return }


    if event.type == NSEvent.EventType.rightMouseUp {


    let menu = NSMenu()


    menu.addItem(


    withTitle: NSLocalizedString("Preference", comment: "Show preferences window"),


    action: #selector(openPreferencesWindow),


    keyEquivalent: ""


    )


    menu.addItem(.separator())


    menu.addItem(


    withTitle: NSLocalizedString("Quit", comment: "Quit app"),


    action: #selector(terminate),


    keyEquivalent: ""


    )


    statusItem?.popUpMenu(menu)


    return


    }


    ɿ
    ࣄલʹ button.sendAction(on: [.leftMouseUp, .rightMouseUp]) ͕ඞཁ

    View Slide

  21. TextFieldͳͲͰΩʔϘʔυγϣʔτΧοτΛ༗ޮʹ͢Δ
    @main


    struct FreeeApp: App {


    #if os(macOS)


    @NSApplicationDelegateAdaptor(AppDelegate.self) var delegate


    #endif


    var body: some Scene {


    #if os(macOS)


    WindowGroup {


    // empty


    }.commands {


    TextEditingCommands()


    }


    Settings {


    SettingsView()


    }


    #else


    WindowGroup {


    ContentView(store: AppStore())


    }


    #endif


    }


    }
    ςΩετฤूܥͷϏϧτΠϯίϚϯυ܈

    View Slide

  22. ഑෍

    View Slide

  23. ΞϓϦͷ഑෍ (ඇAppStore)
    • macOS 10.15 (Catalina) Ҏ߱͸Appleͷެূ (Notarization) Λ෇͚
    ͍ͯͳ͍ΞϓϦ͸ىಈͤͮ͞Β͘ͳ͍ͬͯΔ

    • XcodeͰProduct→Archive

    • OrganizerΛ։͘

    • Distribute App → Developer ID → Upload

    • ͠͹Β͘଴ͭ

    • Distribute App → Developer ID

    • Successfully notarizedͱݴΘΕΔͷͰExportΛΫϦοΫ

    View Slide

  24. ඇදࣔAppͷ഑৴
    • Unlisted app distribution

    • AppStoreʹ͋Δ͚Ͳ௚ϦϯΫΛ஌Βͳ͍ͱ࢖͑ͳ͍

    • 2022೥1݄຤͝Ζ͔Β࢖͑ΔΑ͏ʹͳͬͨ

    • ࢼͯ͠Έ͍ͨ
    https://developer.apple.com/jp/support/unlisted-app-distribution/

    View Slide

  25. 4XJGU6*ͰϝχϡʔόʔΞϓϦΛ࡞ΕΔ

    View Slide