Хабрахабр

[Перевод] Приложение в строке меню для macOS

Приложения, размещенные в строке меню, уже давно известны пользователям macOS. У некоторых из этих приложений есть «обычная» часть, другие размещены только в строке меню.
В этом руководстве вы напишете приложение, которое показывает во всплывающем окне несколько цитат известных людей. В процессе создания этого приложения вы научитесь:

  • назначать иконку приложения в строке меню
  • делать приложение размещенным только в строке меню
  • добавлять пользовательское меню
  • показывать всплывающее по запросу пользователя окно и прятать его, когда необходимо, используя Event Monitoring

Замечание: это руководство предполагает, что вы знакомы со Swift и macOS.

Начинаем

Запускаем Xcode. Далее в меню File/New/Project…, выберите шаблон macOS/Application/Cocoa App и нажмите Next.

Затем удостоверьтесь, что в качестве языка приложения выбран Swift и отмечен чекбокс Use Storyboards. На следующем экране введите Quotes в качестве Product Name, выберите ваши Organization Name и Organization Identifier. Снимите отметку с чекбоксов Create Document-Based Application, Use Core Data, Include Unit tests и Include UI Tests.

Наконец, кликните ещё раз Next, укажите место для сохранения проекта и кликните Create.
Как только новый проект будет создан, откройте AppDelegate.swift и добавьте к классу следующее свойство:

let statusItem = NSStatusBar.system.statusItem(withLength:NSStatusItem.squareLength)

Тут мы создаём в строке меню Status Item (иконку приложения) фиксированной длины, которая будет видна пользователям.

Затем нам нужно назначить свою картинку этому новому элементу в строке меню, чтобы мы могли различить наше новое приложение.

В project navigator перейдите в Assets.xcassets, загрузите картинку и перетащите её в каталог ресурсов (asset catalog).

Измените опцию Render As на Template Image. Выберите картинку и откройте инспектор атрибутов.

Если вы используете свою собственную картинку, убедитесь в том, что изображение черно-белое и сконфигурируйте его как Template image, чтобы иконка выглядела отлично как на темной, так и на светлой строке меню.

Вернитесь к AppDelegate.swift, и добавьте следующий код к applicationDidFinishLaunching(_:)

if let button = statusItem.button { button.image = NSImage(named:NSImage.Name("StatusBarButtonImage")) button.action = #selector(printQuote(_:))
}

Здесь мы назначаем иконке приложения картинку, которую только что добавили и назначаем action, когда кликаем на ней.

Добавьте в класс следующий метод:

@objc func printQuote(_ sender: Any?) { let quoteText = "Never put off until tomorrow what you can do the day after tomorrow." let quoteAuthor = "Mark Twain" print("\(quoteText) — \(quoteAuthor)")
}

Этот метод просто выводит цитату на консоль.

Это позволяет использовать этот метод в качестве отклика на нажатие кнопки. Обратите внимание на директиву метода objc.

Ура!
Каждый раз, как вы кликаете на иконке в строке меню, в консоли Xcode выводится известное изречение Марка Твена. Постройте и запустите приложение, и вы увидите новое приложение в строке меню.

Прячем главное окно и иконку в доке

Есть пара мелочей, которые нам нужно сделать, прежде чем заняться непосредственно функционалом:

  • удалить иконку в доке
  • удалить ненужное главное окно приложения

Чтобы удалить иконку в доке, откройте Info.plist. Добавьте новый ключ Application is agent (UIElement) и установите его значение в YES.

Теперь время разобраться с главным окном приложения.

  • откройте Main.storyboard
  • выберите Window Controller scene и удалите его
  • View Controller scene оставьте, мы скоро будем использовать его

Теперь у приложения нет как главного окна, так и ненужной иконки в доке. Постройте и запустите приложение. Отлично!

Добавляем к Status Item меню

Одной-единственной реакции на клик явно недостаточно для серьёзного приложения. Простейший способ добавить функциональности — добавить меню. Допишите эту функцию в конце AppDelegate.

func constructMenu() { let menu = NSMenu() menu.addItem(NSMenuItem(title: "Print Quote", action: #selector(AppDelegate.printQuote(_:)), keyEquivalent: "P")) menu.addItem(NSMenuItem.separator()) menu.addItem(NSMenuItem(title: "Quit Quotes", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q")) statusItem.menu = menu
}

А затем добавьте этот вызов в конце applicationDidFinishLaunching(_:)

constructMenu()

Мы создаём NSMenu, добавляем к нему 3 экземпляра NSMenuItem и устанавливаем это меню как меню иконки приложения.

Несколько важных замечаний:

  • title элемента меню — это текст, который появится в меню. Хорошее место для локализации приложения (если это необходимо).
  • action, как и action кнопки или другого контрола — это метод, который вызывается, когда пользователь кликает на элементе меню
  • keyEquivalent — это сочетание клавиш, которое можно использовать для выбора элемента меню. Символы в нижнем регистре используют Cmd как модификатор, а в верхнем регистре — Cmd+Shift. Это работает только в том случае, если приложение находится на самом верху и активно. В нашем случае необходимо, чтобы было видно меню или какое-то другое окно, так как у нашего приложения нет иконки в доке
  • separatorItem — это неактивный элемент меню в виде серой линии между другими элементами. Используйте его для группировки
  • printQuote — метод, который вы уже определили в AppDelegate, а terminate — метод, определённый NSApplication.

Запустите приложение, и вы увидите меню, кликнув на иконке приложения.

Попробуйте кликнуть меню — выбор Print Quote выведет цитату в консоль Xcode, а Quit Quotes завершит приложение.

Добавляем всплывающее окно

Вы видели, как легко добавить меню из кода, но показ цитаты в консоли Xcode — это явно не то, что ожидают от приложения пользователи. Сейчас мы добавим простой view controller, чтобы показывать цитаты правильным образом.

Идите в меню File/New/File…, выберите шаблон macOS/Source/Cocoa Class и кликните Next.

  • назовите класс QuotesViewController
  • сделайте наследником NSViewController
  • убедитесь, что чекбокс Also create XIB file for user interface не отмечен
  • установите язык в Swift

Наконец, кликните опять Next, выберите место для сохранения файла и кликните Create.
Теперь откройте Main.storyboard. Раскройте View Controller Scene и выберите View Controller instance.

Сначала выберите Identity Inspector и измените класс на QuotesViewController, затем, установите Storyboard ID на QuotesViewController

Теперь добавьте следующий код к концу файла QuotesViewController.swift:

extension QuotesViewController return viewcontroller }
}

Что здесь происходит:

  1. получаем ссылку на Main.storyboard.
  2. создаем Scene identifier, который соответствует тому, который мы только что установили чуть выше.
  3. создаём экземпляр QuotesViewController и возвращаем его.

Вы создаёте этот метод, так что теперь всем, кто использует QuotesViewController, не нужно знать, как именно он создаётся. Это просто работает.

Бывает неплохо использовать его или assertionFailure, чтобы, если что-то в разработке пошло не так, самому, да и другим членам команды разработки, быть в курсе. Обратите внимание на fatalError внутри оператора guard.

Добавим новое свойство. Теперь вернемся к AppDelegate.swift.

let popover = NSPopover()

Затем замените applicationDidFinishLaunching(_:) следующим кодом:

func applicationDidFinishLaunching(_ aNotification: Notification) { if let button = statusItem.button { button.image = NSImage(named:NSImage.Name("StatusBarButtonImage")) button.action = #selector(togglePopover(_:)) } popover.contentViewController = QuotesViewController.freshController()
}

Вы изменили действие по клику на вызов метода togglePopover(_:), который мы напишем чуть позже. Также, вместо настройки и добавления меню, мы настроили всплывающее окно, которое будет показывать что-то из QuotesViewController.

Добавьте следующие три метода в AppDelegate:

@objc func togglePopover(_ sender: Any?) { if popover.isShown { closePopover(sender: sender) } else { showPopover(sender: sender) }
} func showPopover(sender: Any?) { if let button = statusItem.button { popover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY) }
} func closePopover(sender: Any?) { popover.performClose(sender)
}

showPopover() показывает всплывающее окно. Вы только указываете, откуда оно появляется, macOS позиционирует его и дорисовывает стрелочку, как будто оно появляется из строки меню.

closePopover() просто закрывает всплывающее окно, а togglePopover() — метод, который либо показывает, либо прячет всплывающее окно, в зависимости от его состояния.

Запустите приложение и кликните на его иконке.

Все отлично, но где контент?

Реализуем Quote View Controller

Сначала вам нужна модель для хранения цитат и атрибутов. Идите в меню File/New/File… и выберите macOS/Source/Swift File template, затем Next. Назовите файл Quote и кликните Create.

Откройте файл Quote.swift и добавьте в него следующий код:

struct Quote { let text: String let author: String static let all: [Quote] = [ Quote(text: "Never put off until tomorrow what you can do the day after tomorrow.", author: "Mark Twain"), Quote(text: "Efficiency is doing better what is already being done.", author: "Peter Drucker"), Quote(text: "To infinity and beyond!", author: "Buzz Lightyear"), Quote(text: "May the Force be with you.", author: "Han Solo"), Quote(text: "Simplicity is the ultimate sophistication", author: "Leonardo da Vinci"), Quote(text: "It’s not just what it looks like and feels like. Design is how it works.", author: "Steve Jobs") ]
} extension Quote: CustomStringConvertible { var description: String { return "\"\(text)\" — \(author)" }
}

Здесь мы определяем простую структуру цитаты и статическое свойство, которое возвращает все цитаты. Так как мы сделали Quote соответствующим протоколу CustomStringConvertible, мы легко можем получить удобно отформатированный текст.

Есть прогресс, но нам еще нужны элементы управления, чтобы все это отобразить.

Добавляем элементы интерфейса

Откройте Main.storyboard и вытащите 3 кноки (Push Button) и метку (Multiline Label) на view controller.

Расположите кнопки и метку так, чтобы они выглядели примерно так:

Прикрепите левую кнопку к левому краю с промежутком 20 и отцентрируйте вертикально.
Прикрепите правую кнопку к правому краю с промежутком 20 и отцентрируйте вертикально.
Прикрепите нижнюю кнопку к нижнему краю с промежутком 20 и отцентрируйте горизонтально.
Прикрепите левый и правый край метки к кнопкам с промежутком 20 отцентрируйте вертикально.

Вы увидите несколько ошибок расположения, так как для auto layout недостаточно информации, чтобы разобраться.

Установите у метки Horizontal Content Hugging Priority в 249, чтобы позволить метке изменять размер.

Теперь сделайте следующее:

  • установите image левой кнопки в NSGoLeftTemplate и очистите title
  • установите image правой кнопки в NSGoRightTemplate и очистите title
  • установите title кнопки внизу в Quit Quotes.
  • установите text alignment у метки в center.
  • проверьте, что Line Break у метки установлен в Word Wrap.

Теперь откройте QuotesViewController.swift и добавьте следующий код в реализацию класса QuotesViewController:

@IBOutlet var textLabel: NSTextField!

Теперь в QuotesViewController.swift два расширения класса. Добавьте этот экстеншн в реализацию класса.

// MARK: Actions extension QuotesViewController { @IBAction func previous(_ sender: NSButton) { } @IBAction func next(_ sender: NSButton) { } @IBAction func quit(_ sender: NSButton) { }
}

Мы только что добавили outlet для метки, которую мы будем использовать для показа цитат, и 3 метода-заглушки, которые соединим с кнопками.

Соединяем код с Interface Builder

Обратите внимание: Xcode разместил кружки слева от вашего кода — возле ключевых слов IBAction и IBOutlet.

Мы будем их использовать, чтобы соединить код с UI.

Таким образом storyboard откроется в Assistant Editor справа, а код — слева. Держа нажатой клавишу alt кликните на Main.storyboard в the project navigator.

Таким же образом соедините методы previous, next и quit с левой, правой и нижней кнопками соответственно. Перетащите кружок слева от textLabel на метку в interface builder.

Запустите ваше приложение.

Если вы хотите всплывающее окно побольше или поменьше, просто измените его размер в storyboard. Мы использовали размер всплывающего окна по умолчанию.

Пишем код для кнопок

Если вы еще не скрыли Assistant Editor, нажмите Cmd-Return или View > Standard Editor > Show Standard Editor

Откройте QuotesViewController.swift и добавьте следующие свойства в реализацию класса:

let quotes = Quote.all var currentQuoteIndex: Int = 0 { didSet { updateQuote() }
}

Свойство quotes содержит все цитаты, а currentQuoteIndex — это индекс цитаты, которая выводится в данный момент. У currentQuoteIndex есть также property observer, чтобы обновить содержимое метки новой цитатой, когда меняется индекс.

Теперь добавьте следующие методы:

override func viewDidLoad() { super.viewDidLoad() currentQuoteIndex = 0
} func updateQuote() { textLabel.stringValue = String(describing: quotes[currentQuoteIndex])
}

Когда view загружается, мы устанавливаем индекс цитаты в 0, что, в свою очередь, приводит к обновлению интерфейса. updateQuote() просто обновляет текстовую метку, чтобы отобразить цитату. соответствующую currentQuoteIndex.

Наконец, обновите эти методы следующим кодом:

@IBAction func previous(_ sender: NSButton) { currentQuoteIndex = (currentQuoteIndex - 1 + quotes.count) % quotes.count
} @IBAction func next(_ sender: NSButton) { currentQuoteIndex = (currentQuoteIndex + 1) % quotes.count
} @IBAction func quit(_ sender: NSButton) { NSApplication.shared.terminate(sender)
}

Методы next() и previous() циклически перелистывают все цитаты. quit закрывает приложение.

Запустите приложение:

Мониторинг событий

Есть ещё одна вещь, которую пользователи ждут от нашего приложения — прятать всплывающее окно, когда пользователь кликает где-то вне него. Для этого нам нужен механизм, называемый macOS global event monitor.

Создадим новый Swift файл, назовём его EventMonitor, и заменим его содержимое следующим кодом:

import Cocoa public class EventMonitor { private var monitor: Any? private let mask: NSEvent.EventTypeMask private let handler: (NSEvent?) -> Void public init(mask: NSEvent.EventTypeMask, handler: @escaping (NSEvent?) -> Void) { self.mask = mask self.handler = handler } deinit { stop() } public func start() { monitor = NSEvent.addGlobalMonitorForEvents(matching: mask, handler: handler) } public func stop() { if monitor != nil { NSEvent.removeMonitor(monitor!) monitor = nil } }
}

При инициализации экземпляра этого класса мы передаем ему маску событий, который будем прослушивать (вроде нажатия клавиш, прокрутки колеса мыши и т.д.) и обработчик события.
Когда мы готовы начать прослушивание, start() вызывает addGlobalMonitorForEventsMatchingMask(_:handler:), который возвращает объект, который мы сохраняем. Как только случается событие, содержавшееся в маске, система вызывает ваш обработчик.

Чтобы прекратить мониторинг событий, в stop() вызывается removeMonitor() и мы удаляем объект путем присваивания ему значения nil.

Класс также вызывает stop() в деинициалайзере, чтобы прибрать за собой. Все, что нам остается — это вызывать в нужное время start() and stop().

Подключаем Event Monitor

Откройте AppDelegate.swift в последний раз и добавьте новое свойство:

var eventMonitor: EventMonitor?

Затем добавьте этот код, чтобы сконфигурировать event monitor в конце applicationDidFinishLaunching(_:)

eventMonitor = EventMonitor(mask: [.leftMouseDown, .rightMouseDown]) { [weak self] event in if let strongSelf = self, strongSelf.popover.isShown { strongSelf.closePopover(sender: event) }
}

Это проинформирует ваше приложение при клике левой или правой кнопки. Обратите внимание: обработчик не будет вызван в ответ на клики мышью внутри вашего приложения. Вот почему всплывающее окно не закроется, пока вы кликаете внутри него.

Мы используем weak ссылку на self, чтобы избежать опасности цикла сильных ссылок между AppDelegate и EventMonitor.

Добавьте следующий код в конец метода showPopover(_:):

eventMonitor?.start()

Здесь мы стартуем мониторинг событий, когда появляется всплывающее окно.

Теперь добавьте код в конце метода closePopover(_:):

eventMonitor?.stop()

Здесь мы завершаем мониторинг, когда всплывающее окно закрывается.

Приложение готово!

Заключение

Здесь вы найдете полный код этого проекта.

Почему бы не поэкспериментировать с несколькими метками или форматированным текстом, чтобы получше оформить вид цитат? Вы узнали, как установить меню и всплывающее окно в приложении, размещенном в строке меню. Или вы захотите использовать клавиатуру для навигации между цитатами? Или подключить бэкенд, чтобы получать цитаты из Интернета?

Хорошее место для исследований — официальная документация: NSMenu, NSPopover и NSStatusItem.

Показать больше

Похожие публикации

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»