Adwaita-swift: теперь можно писать приложения для GNOME на языке Swift

от автора

Язык программирования 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.


ссылка на оригинал статьи https://habr.com/ru/articles/837724/


Комментарии

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

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