Язык программирования Swift наиболее широко применяется в разработке программного обеспечения для операционных систем от компании Apple. Но не так давно появилась заметка, в которой говорится, что теперь на этом языке можно писать программы, основанные на GTK4+Libadwaita.
В статье рассмотрим несколько небольших примеров, иллюстрирующих применение Swift в разработке приложений для GNOME, а в конце создадим простенький генератор паролей.
Примеры кода
Примеры можно найти в этом репозитории. Запускать их лучше всего в GNOME Builder. В репозитории уже есть все необходимые файлы, включая манифест для изолированной сборки и запуска приложения.
Игра в кости
Начнем мы с имитатора бросания кости. Интерфейс этого примера очень простой: результат бросания сразу же отображается на кнопке в виде числа от 1 до 6. Вот так это выглядит:
Код:
import Adwaita
struct DiceDemo: View {
@State private var number: Int?
private var label: String {
if let number {
return "\(number)"
} else {
return "Roll the Dice!"
}
}
var view: Body {
VStack {
Button(label) {
number = .random(in: 1...6)
}
.pill()
.suggested()
.style("dice-button")
.css {
"""
.dice-button {
background-color: @green_5;
}
"""
}
.frame(maxWidth: 100)
}
.valign(.center)
.padding()
}
}
Как видим, в разработке этого примера применяется декларативный подход, то есть такой подход, при котором описывается ожидаемый результат, а не способы его достижения. Он используется во всех последующих примерах. Кнопка упаковывается в контейнер VStack. Она отображает метку label, которая определяется выше. К кнопке применены некоторые свойства, включая style с классом dice-button, который уже на следующей строке и прописывается в свойстве css. Очень удобно и лаконично.
Список
Этот пример демонстрирует добавление и удаление пунктов в списке List:
Код:
import Adwaita
import Foundation
struct ListDemo: View {
@State private var items: [Element] = []
@State private var selectedItem = ""
var view: Body {
HStack {
Button("Add Row") {
let element = Element(id: UUID().uuidString)
items.append(element)
selectedItem = element.id
}
Button("Delete Selected Row") {
let index = items.firstIndex { $0.id == selectedItem }
items = items.filter { $0.id != selectedItem }
selectedItem = items[safe: index]?.id ?? items[safe: index ?? 0 - 1]?.id ?? items.first?.id ?? ""
}
}
.linked()
.padding()
.halign(.center)
if !items.isEmpty {
List(items, selection: $selectedItem) { item in
HStack {
Text("\(item.id)")
.hexpand()
}
.padding()
}
.boxedList()
.valign(.center)
.padding()
}
}
struct Element: Identifiable, CustomStringConvertible, Equatable {
var id: String
var description: String { id }
}
}
Список включает HStack, в котором содержится компонент Text. Этот компонент отображает идентификатор пункта. К списку применен встроенный стиль, определяемый свойством boxedList, который визуально разделяет список на отдельные пункты. Делается это при помощи сепаратора в виде горизонтальной линии, которая помещается между пунктами списка.
Карусель
Этот пример показывает, как создавать и удалять элементы в виде карточек при помощи компонента Carousel:
Код примера:
import Adwaita
import Foundation
struct CarouselDemo: View {
@State private var items: [ListDemo.Element] = [.init(id: "Hello"), .init(id: "World")]
var view: Body {
Button("Add Card") {
let element = ListDemo.Element(id: UUID().uuidString)
items.append(element)
}
.padding()
.halign(.center)
Carousel(items) { element in
VStack {
Text(element.id)
.vexpand()
Button("Delete") {
items = items.filter { $0.id != element.id }
}
.padding()
}
.vexpand()
.hexpand()
.card()
.onClick { print(element.id) }
.padding(20)
.frame(minWidth: 300, minHeight: 200)
.frame(maxWidth: 500)
}
.longSwipes()
}
}
Сами карточки представляют собой контейнер VStack, в котором расположены компоненты Text и Button. К контейнеру применено несколько свойств, одно из которых — card. Оно и определяет внешний вид этого компонента.
Формы
Данный пример иллюстрирует создание форм (Form) и размещение в них различных компонентов:
Код примера:
import Adwaita
struct FormDemo: View {
var app: GTUIApp
var view: Body {
VStack {
Button("View Demo") {
app.showWindow("form-demo")
}
.suggested()
.pill()
.frame(maxWidth: 100)
}
}
struct WindowContent: View {
@State private var text = "They also have a subtitle"
@State private var password = "Password"
@State private var value = 0
@State private var isOn = true
@State private var selection = "World"
let values: [ListDemo.Element] = [.init(id: "Hello"), .init(id: "World")]
var view: Body {
ScrollView {
VStack {
actionRows
FormSection("Entry Rows") {
Form {
EntryRow("Entry Row", text: $text)
.suffix {
Button(icon: .default(icon: .editCopy)) { State<Any>.copy(text) }
.flat()
.verticalCenter()
}
EntryRow(password, text: $password)
.secure(text: $password)
}
}
.padding()
rowDemo("Spin Rows", row: SpinRow("Spin Row", value: $value, min: 0, max: 100).subtitle("\(value)"))
rowDemo("Switch Rows", row: SwitchRow("Switch Row", isOn: $isOn).subtitle(isOn ? "On" : "Off"))
rowDemo(
"Combo Rows",
row: ComboRow("Combo Row", selection: $selection, values: values).subtitle(selection)
)
rowDemo("Expander Rows", row: ExpanderRow().title("Expander Row").rows {
ActionRow("Hello")
ActionRow("World")
})
}
.padding()
.frame(maxWidth: 400)
}
.topToolbar {
HeaderBar.empty()
}
}
var actionRows: View {
Form {
ActionRow("Rows have a title")
.subtitle(text)
ActionRow("Rows can have suffix widgets")
.suffix {
Button("Action") { }
.verticalCenter()
}
}
.padding()
}
func rowDemo(_ title: String, row: View) -> View {
FormSection(title) {
Form {
row
}
}
.padding()
}
}
}
Здесь мы видим примеры создания форм с самыми разными компонентами. Присутствуют следующие компоненты: ActionRow (пункт списка), EntryRow (поле для ввода), ComboRow (выпадающий список), ExpanderRow (раскрывающийся пункт списка) и так далее. Формы показаны как с секциями, так и без них.
Нижняя панель
Этот пример демонстрирует, как можно показывать и скрывать нижнюю панель:
Код:
import Adwaita
struct ToolbarDemo: View {
var app: GTUIApp
var view: Body {
VStack {
Button("View Demo") {
app.showWindow("toolbar-demo")
}
.suggested()
.pill()
.frame(maxWidth: 100)
}
}
struct WindowContent: View {
@State private var visible = false
@State private var moreContent = false
var view: Body {
VStack {
Button("Toggle Toolbar") {
visible.toggle()
}
.suggested()
.pill()
.frame(maxWidth: 100)
.padding(15)
}
.valign(.center)
.bottomToolbar(visible: visible) {
HeaderBar(titleButtons: false) {
Button(icon: .default(icon: .audioInputMicrophone)) { }
} end: {
Button(icon: .default(icon: .userTrash)) { }
}
.headerBarTitle { }
}
.topToolbar {
HeaderBar.empty()
}
}
}
}
В роли нижней панели используется HeaderBar. В начало добавляется кнопка с иконкой микрофона, а в конец — кнопка с иконкой корзины. При нажатии на кнопку с меткой «Toggle Toolbar» панель скрывается или показывается.
Переключение вида
Данный пример показывает реализацию переключения расположения группы вкладок. Такой пример можно использовать, например, при создании медиаплеера с адаптивным дизайном. Ниже показан внешний вид окна до переключения:
А это после:
Код:
import Adwaita
struct ViewSwitcherDemo: View {
var app: GTUIApp
var view: Body {
VStack {
Button("View Demo") {
app.showWindow("switcher-demo")
}
.suggested()
.pill()
.frame(maxWidth: 100)
}
}
struct WindowContent: View {
@State private var selection: ViewSwitcherView = .albums
@State private var bottom = false
var view: Body {
VStack {
Text(selection.title)
.padding()
HStack {
Button(bottom ? "Show Top Bar" : "Show Bottom Bar") {
bottom.toggle()
}
}
.halign(.center)
}
.valign(.center)
.topToolbar {
if bottom {
HeaderBar
.empty()
} else {
toolbar
}
}
.bottomToolbar(visible: bottom) {
toolbar
}
}
var toolbar: View {
HeaderBar(titleButtons: !bottom) { } end: { }
.headerBarTitle {
ViewSwitcher(selection: $selection)
.wideDesign(!bottom)
}
}
}
enum ViewSwitcherView: String, ViewSwitcherOption {
case albums
case artists
case songs
case playlists
var title: String {
rawValue.capitalized
}
var icon: Icon {
.default(icon: {
switch self {
case .albums:
return .mediaOpticalCdAudio
case .artists:
return .avatarDefault
case .songs:
return .emblemMusic
case .playlists:
return .viewList
}
}())
}
init?(title: String) {
self.init(rawValue: title.lowercased())
}
}
}
Для переключения расположения вкладок используется компонент ViewSwitcher, находящийся в HeaderBar. ViewSwitcher прописан в свойстве headerBarTitle, которое определяет заголовок для HeaderBar. Для активации переключения применяется кнопка Button, расположенная в HStack. HStack вместе с компонентом Text находится внутри VStack. Компонент Text служит для отображения заголовков выбранных вкладок.
Генератор паролей
Для создания данного приложения был использован этот шаблон. В шаблоне уже содержится все, что нужно для запуска и сборки приложения. Репозиторий программы можно найти здесь.
Интерфейс
Интерфейс программы состоит из двух составляющих. Это поле для показа пароля и обычная кнопка, при нажатии на которую этот самый пароль генерируется. В качестве поля используется компонент PasswordEntryRow, а в качестве кнопки — Button. Внешний вид приложения:
Код интерфейса:
var view: Body {
VStack {
Form {
PasswordEntryRow(Loc.password, text: $password)
.suffix {
Button(icon: .default(icon: .editCopy)) {
State<Any>.copy(password)
copied.signal()
}
.flat()
.verticalCenter()
.tooltip(Loc.copy)
Button(icon: .default(icon: .editClear)) {
password = ""
}
.flat()
.verticalCenter()
.tooltip(Loc.clear)
}
}
.padding()
Button(Loc.generate) {
password = createPassword(size: 12)
}
.pill()
.suggested()
.padding()
}
.valign(.center)
.toast(Loc.clipboard, signal: copied)
.topToolbar {
ToolbarView(app: app, window: window)
}
}
Приведенный код располагается в файле AdwaitaTemplate.swift. В свойстве suffix для поля показа пароля прописаны кнопки для очистки поля и для копирования пароля в буфер обмена. Для операции копирования нужен сигнал. Он определяется в самом начале структуры вместе с переменной password:
@State private var password = ""
@State private var copied: Signal = .init()
Этот сигнал активирует показ всплывающего сообщения toast. Toast добавлен в виде еще одного свойства к контейнеру VStack, в котором содержатся все элементы интерфейса приложения.
Логика
Генерация пароля происходит при помощи следующей функции:
func createPassword(size: Int) -> String {
let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
let numbers = "0123456789"
let specialChars = "!@$%#^(&)*_-+~=`|[{]}/:;<>,.?/"
let chars = letters + numbers + specialChars
var password = ""
for _ in 0..<size {
let randomIndex = Int.random(in: 0..<chars.count)
let character = Array(chars)[randomIndex]
password.append(character)
}
return password
}
Пароль создается из набора букв, чисел и специальных символов. Функция вызывается при нажатии на соответствующую кнопку.
Перевод
Переводы на другие языки хранятся в файле Localized.yml. Он имеет следующий вид:
default: en
password:
en: Password
ru: Пароль
copy:
en: Copy
ru: Копировать
clear:
en: Clear
ru: Очистить
generate:
en: GENERATE
ru: СОЗДАТЬ
clipboard:
en: Copied to clipboard
ru: Скопировано в буфер обмена
newWindow:
en: New Window
ru: Новое Окно
closeWindow:
en: Close Window
ru: Закрыть Окно
quit:
en: Quit
ru: Выйти
mainMenu:
en: Main Menu
ru: Главное Меню
Для каждой строки, которую нужно перевести, создается ключ. Под ключом располагаются переводы. В исходнике ключ прописывается, например, как Loc.generate. Примеры можно посмотреть выше. В самом начале файла указывается язык по умолчанию.
Сборка
Обычно для приложений создаваемых для GNOME используется система сборки Meson, но в случае с adwaita-swift разработчик решил обойтись без него. В шаблоне имеется манифест для сборки пакета flatpak, в котором модуль приложения имеет следующий вид:
{
"name": "AdwaitaTemplate",
"builddir": true,
"buildsystem": "simple",
"sources": [
{
"type": "dir",
"path": "."
}
],
"build-commands": [
"swift build -c debug --static-swift-stdlib",
"strip .build/debug/AdwaitaTemplate",
"install -Dm755 .build/debug/AdwaitaTemplate /app/bin/AdwaitaTemplate",
"install -Dm644 data/io.github.AparokshaUI.AdwaitaTemplate.metainfo.xml $DESTDIR/app/share/metainfo/io.github.AparokshaUI.AdwaitaTemplate.metainfo.xml",
"install -Dm644 data/io.github.AparokshaUI.AdwaitaTemplate.desktop $DESTDIR/app/share/applications/io.github.AparokshaUI.AdwaitaTemplate.desktop",
"install -Dm644 data/icons/io.github.AparokshaUI.AdwaitaTemplate.svg $DESTDIR/app/share/icons/hicolor/scalable/apps/io.github.AparokshaUI.AdwaitaTemplate.svg",
"install -Dm644 data/icons/io.github.AparokshaUI.AdwaitaTemplate-symbolic.svg $DESTDIR/app/share/icons/hicolor/symbolic/apps/io.github.AparokshaUI.AdwaitaTemplate-symbolic.svg"
]
}
Сборочная система здесь указана как simple, а в командах сборки подробно расписано, что надо делать, куда и какие файлы следует положить. В начале манифеста указаны самые свежие на данный момент версии платформы GNOME и SDK, а также дополнительно прописано расширение SDK для языка Swift:
"runtime": "org.gnome.Platform",
"runtime-version": "46",
"sdk": "org.gnome.Sdk",
"sdk-extensions": [
"org.freedesktop.Sdk.Extension.swift5"
]
Для сборки приложения в среде разработки GNOME Builder следует перейти во вкладку конвейера сборки в левой панели и в ней выбрать последний пункт из списка. После успешной сборки пакета папка с ним будет открыта в файловом менеджере.
Лично мне по душе, что появилась возможность писать приложения для GNOME еще на одном языке. Для разработки под GNOME можно использовать несколько языков, среди которых есть Vala, Python, JavaScript, Go, Rust и даже Java. В списке шаблонов GNOME Builder присутствуют далеко не все доступные для разработки языки. Но, может быть, в будущем свое скромное место среди них займет и Swift.
Автор статьи @KAlexAl
НЛО прилетело и оставило здесь промокод для читателей нашего блога:
-15% на заказ любого VDS (кроме тарифа Прогрев) — HABRFIRSTVDS.