Умные часы

Часть 2. Создание приложения для Apple Watch

https://ru.freepik.com/author/freepik
2

If,While, For, Switch | Управление потоком DART

If,While, For, Switch | Изучение управления потоком

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

Включение грядущих возможностей языка Swift

Марсель Восс 22 марта 2023

Swift 6 несет множество изменений, которые переосмыслят сам способ использования Swift в будущем, причем некоторые из них - радикальные. Было бы кстати, если бы мы могли о…

1

Крупный гайд по Svelte

Svelte — веб-фреймворк, который отличается принципом работы от React и Vue.

React и Vue формируют приложение прямо в браузере, когда пользователь открывает необходимый ему ресурс, Svelte же заранее компилирует исходный код и предоставляет часть приложения статичной версткой, а затем гидрирует приложение, благодаря чему приложение получается б…

17

Безопасная отладка вашего приложения в продакшене

12 января 2023

Джеймс ШерлокСоздано 31 декабря

Сделай сам: умная камера для наблюдения за питомцами

Обучаем нейросеть на котиках.

Сделай сам: умная камера для наблюдения за питомцами
20

Обзор новой версии языка Swift 5.5

Состоялся официальный релиз iOS 15, а значит разработчикам стала доступна новая версия Xcode под номером 13, а вместе с ним и новая версия языка Swift - 5.5.

Обзор новой версии языка Swift 5.5
8

Реализация аутентификации пользователя в Django/Python

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

Django – это фреймворк для работы с данными с использованием доступа через Web. Один из видов MVC — и называется MVT.

5

Как ускорить набор кода и сделать его без ошибок?

Как использовать сниппеты в Visual Studio? Как создавать свои? Как посмотреть, какие уже созданы? Попробуем ответить на все эти вопросы.

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

4
\n\n\n\n","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Все что находится внутри <script> выполняется во время создания компонента.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Внутри <script> также есть дополнительные правила, которые будет рассмотрены ниже.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Важно помнить что Svelte — фреймворк, который компилирует исходный код, а значит у Svelte может быть дополнительные значения у привычных для нас вещей (напр. export, который описан ниже)

"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Состояния"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Состояния внутри Svelte объявляются с помощью let и const. Все верно, каждая переменная объявленная в глобальном скоупе является состоянием🫡

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"","lang":""}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Шаблоны"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Шаблон компонента можно писать прямо в файле .svelte:

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"\n\n
\n \n
","lang":""}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Интерполяция"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Для того чтобы использовать состояния внутри шаблона нужно укзаать на имя состояния в фигурных скобках:

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"\n\n

Hello, { name }

\n","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"\n\n
\n

Пользователь старше 18 лет?

\n { age > 18 } \n
","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Также мы можем вставлять HTML с помощью модификатора @html:

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"\n\n{@html htmlCode}","lang":""}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Атрибуты"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Интерполяция работает с атрибутами точно также, как и с текстом внутри элемента:

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"\n\n\"Default\n\n\"Default","lang":""}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Условный рендеринг"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

В HTML нет условий, в Svelte они есть. Использовать условный рендеринг можно следующим образом:

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"\n\n
\n {#if hasButton}\n \n\t \n\n {:else}\n \n
Тут нет кнопки :(
\n {/if}\n
","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Также мы можем использовать {:else if <условие>}, для того чтобы задать дополнительный логический блок. Прямо как `else if` в JS👀

"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Цикличный рендеринг"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Также как и с условным рендерингом — в Svelte есть цикличный рендеринг. Он позволяет проходится по спискам и для каждого элемента рендерить одну и ту же верстку:

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"\n\n
\n Список имен:\n \n
","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Данный кусок кода выведет следующий список в HTML:

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"
\n Список имен:\n \n
","lang":""}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Изменяемый список"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

В случае если список изменяется — нужно дать каждом элементу id, для того чтобы Svelte лучше управлялся с такими элементами и рендерил все правильно:

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"\n\n
\n Список имен:\n \n
","lang":""}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Асинхронные блоки"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Svelte позволяет нам отрисовывать разный шаблон по мере жизнедеятельности промиса:

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"\n\n\n\n\n\n{#await promise}\n

Данные запрашиваются

\n{:then data}\n\n {data}\n\n{:catch error}\n

\n Данные не получены :(\n

\n{/await}","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Обратите внимание, что:

"}},{"type":"list","cover":false,"hidden":false,"anchor":"","data":{"items":["# в начале названия блока используется для объвления блока
",": в начале названия блока используется для того чтобы продлить блок","/ в начале названия блока используется для того чтобы закрыть блок"],"type":"UL"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Импортирование компонентов"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Можно импортировать компоненты в другие компоненты:

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"\n\n","lang":""}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Кастомные ивенты"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Иногда нам нужны кастомные ивенты, которые бы тригерили функции извне компонента. В Vue для этого есть emit, в Svelte — eventDispatcher 💁🏻‍♂

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Допустим, что у родительского компонента нет доступа к состояниям дочернего компонента. Но, мы все же хотим получить оттуда какие-либо данные. Для этого мы создаем коллбэк, который будет с этими данными взаимодействовать (допустим выводить их с помощью alert):

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"\n\n\n","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"\n\n\n","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Теперь при клике на кнопку мы создадим специальный ивент, который вызовет коллбэк handleMessage из родительского компонента и передаст ему все данные, которые нам нужны 🙌

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

> К слову мы могли назвать ивент как хотим. Важно, чтобы первый аргумент в dispatch() и слово после on: совпадали.

"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Всплытие"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Кастомные ивенты не всплывают, но если нам нужно, чтобы они всплывали, то у каждого дочернего компонента в иерархии нужно указывать `on:message`. Это довольно редкий кейс, однако подробнее можно узнать тут.

"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Биндинги"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Биндинги существуют для того чтобы облегчать двухстороннее связывание 🥂

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Двухстороннее связывание — техника, которую в основном применяют к инпутам. Она связывает value и событие oninput, таким образом чтобы пользовательский ввод все время обновлял value.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

В Svelte это выглядит следующим образом:

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"\n\n\n\n

{ inputValue }

","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"\n\n\n\n\n

{ inputValue }

","lang":""}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Хуки"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Хуки — это методы, которые цепляются за жизненный цикл компонента🪝

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

В Svelte существует следующие хуки:

"}},{"type":"list","cover":false,"hidden":false,"anchor":"","data":{"items":["onMount — хук, который выполняется как только компонент примонтировался к DOM.","beforeUpdate — хук, который работает перед тем как в компонент придут новые данные (например из пропсов)","afterUpdate — хук, который работает после того, как в компонент пришли новые данные
","onDestroy — хук, который работает когда компонент размонтируется (удаляется из DOM)
"],"type":"UL"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Обычно хук onMount используют для того, чтобы подтянуть какие-то данные со сторонних сервисов с помощью fetch и использовать их внутри компонента.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Если использовать SvelteKit (для рендеринга на стороне сервера) и расположить fetch внутри onMount, то данные будут запрашиваться на стороне клиента, а если просто внутри <script>, то запрос отправится еще на сервере.

"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Тик"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Все фреймворки для создания веб-приложений работают следующим образом:

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

1. Сбор операций которые нужно выполнить пачкой

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

2. Подбор времени для выполнения

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

3. Оптимизация собраного стека

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

4. Выполнение этих операций (тик)

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

В Svelte тоже есть тики. Например, если мы используем специальный метод tick внутри хука beforeUpdate, то после выполнения мы уже будем находиться на ивенте afterUpdate:

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"","lang":""}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Сторы"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Как уже понятно — мы можем объявлять состояния внутри компонентов. Однако бывают ситуации, когда нам нужно использовать глобальные состояния, при обновлении которых компоненты также будут обновляться (перерендериваться). Для этого сторы и придумали.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Допустим что у нас есть два разных компонента:

"}},{"type":"list","cover":false,"hidden":false,"anchor":"","data":{"items":["В одном из них мы управляем нашим счетчиком","В другом мы используем значения из этого счетчика
"],"type":"UL"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Мы можем объявлять счетчики вне компонентов в отдельных файлах JS/TS:

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"import {writable} from 'svelte';\nexport const counter = writable(0);","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"\n\n\n\n","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

В данном компоненте мы будем отрисовывать счетчик:

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"\n\n

Счетчик: { count }

","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Чтобы не делать этого мы можем просто использовать стор добавив к его названию $$counter.

"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Заключение"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Еще обязательно увидимся в следующих гайдах и туториалах ❤

"}}],"summaryContent":null,"isExistSummaryContent":false,"warningFromEditor":null,"warningFromEditorTitle":null,"counters":{"comments":31,"favorites":39,"reposts":0,"views":15616,"hits":17828,"reads":null,"online":0},"dateFavorite":0,"hitsCount":17828,"isCommentsEnabled":true,"isLikesEnabled":true,"isRemovedByUserRequest":false,"isFavorited":false,"isPinned":false,"repostId":null,"repostData":null,"subscribedToTreads":false,"isEditorial":false,"isAudioAvailable":false,"audioUrl":null,"isAudioAvailableToGenerate":false,"commentEditor":{"enabled":true,"who":null,"text":"","until":null,"reason":null,"type":"everybody"},"isBlur":false,"isPublished":true,"isDisabledAd":false,"withheld":[],"ogTitle":null,"ogDescription":null,"url":"https://vc.ru/dev/605614-krupnyi-gaid-po-svelte","author":{"id":1178100,"name":"Даниил Шило","nickname":null,"description":"Frontend Engineer в Firecode","uri":"","avatar":{"type":"image","data":{"uuid":"59e8fb72-4a49-5932-af48-bdb368b827e7","width":400,"height":400,"size":28098,"type":"png","color":"dfcac6","hash":"73610f6d350e00","external_service":[]}},"cover":{"cover":{"type":"image","data":{"uuid":"ab766887-ce9f-5181-b1e9-e877385348cb","width":5120,"height":2160,"size":561896,"type":"jpg","color":"85adba","hash":"","external_service":[]}},"cover_y":0},"achievements":[{"title":"Год на vc.ru","code":"registration_1_year","description":"Первый год с vc.ru. Получена 24 июля 2025.","previewUuid":"0d11c244-49de-50e7-894e-b9b27945d42b","formats":{"glb":"https://static.vc.ru/achievements/fish.glb","usdz":"https://static.vc.ru/achievements/fish.usdz"},"viewData":{"contentColor":"#C67AA3","textMaxWidth":0.634765625,"textX":0.5888671875,"textY":0.54296875,"logoX":0.5859375,"logoY":0.6669921875,"logoXNoText":0.6044921875,"logoYNoText":0.5439453125},"id":4265614,"userId":1178100,"count":0,"shareImage":"https://api.vc.ru/achievements/share/4265614"},{"title":"3 года на vc.ru","code":"registration_3_years","description":"Провёл 3 года вместе с vc.ru. Получена 23 июля 2025.","previewUuid":"d9d72ac5-bcb5-55e0-8c72-b99251e5cdd9","formats":{"glb":"https://static.vc.ru/achievements/shark.glb","usdz":"https://static.vc.ru/achievements/shark.usdz"},"viewData":{"contentColor":"#8E6F09","textMaxWidth":0.66796875,"textX":0.5205078125,"textY":0.341796875,"logoX":0.5205078125,"logoY":0.4609375,"logoXNoText":0.5,"logoYNoText":0.3662109375},"id":642080,"userId":1178100,"count":0,"shareImage":"https://api.vc.ru/achievements/share/642080"}],"lastModificationDate":1765017968,"isSubscribed":false,"isSubscribedToNewPosts":false,"isMuted":false,"isAvailableForMessenger":true,"badgeId":null,"isDonationsEnabled":false,"isPlusGiftEnabled":true,"isUnverifiedBlogForCompanyWithoutPro":false,"isRemovedByUserRequest":false,"isFrozen":false,"isDisabledAd":false,"isPlus":false,"isVerified":false,"isPro":false,"yandexMetricaId":null,"badge":null,"isOnline":false,"tgChannelShortname":null,"isUnsubscribable":true,"type":1,"subtype":"personal_blog"},"subsite":{"id":235819,"name":"Разработка","description":"Сообщество разработчиков: публикации о личном опыте, выдающиеся приёмы при решении рутинных задач, полезные материалы для профессионального роста.","uri":"/dev","avatar":{"type":"image","data":{"uuid":"fef5b5fb-e488-5b7f-8445-e3a26a910b44","width":1200,"height":1200,"size":7757,"type":"png","color":"343434","hash":"04042b2b1c1000","external_service":[]}},"cover":{"type":"image","data":{"uuid":"2a214cc5-35cc-58ca-bc07-fc1c892d2101","width":960,"height":280,"size":177,"type":"png","color":"343434","hash":"","external_service":[]}},"lastModificationDate":1642411346,"isSubscribed":false,"isSubscribedToNewPosts":false,"isMuted":false,"isAvailableForMessenger":false,"isDisabledAd":false,"nickname":"dev","isUnsubscribable":true,"badge":null,"badgeId":null,"isDonationsEnabled":false,"isOnline":false,"isPlus":false,"isUnverifiedBlogForCompanyWithoutPro":false,"isVerified":false,"isRemovedByUserRequest":false,"isFrozen":false,"isPro":false,"type":2,"subtype":"community"},"reactions":{"counters":[{"id":1,"count":17}],"reactionId":0},"isNews":false,"source":null,"clusters":[],"donations":{"amount":0,"isDonated":false},"commentsSeenCount":null}},{"type":"entry","data":{"id":584196,"customUri":null,"subsiteId":1165340,"title":"Безопасная отладка вашего приложения в продакшене","date":1674455435,"dateModified":1674455435,"blocks":[{"type":"text","cover":true,"hidden":false,"anchor":"","data":{"text":"

12 января 2023

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Джеймс ШерлокСоздано 31 декабря

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Production (продакшен, прод) - версия продукта, прошедшая все стадии тестирования и выложена онлайн / установлена клиенту.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Проведите пальцем вверх, проведите пальцем вниз, покружитесь и постучите ногой в ритме “Макарена”. Теперь у вас есть доступ к ✨ секретному меню разработчика ✨.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"media","cover":false,"hidden":false,"anchor":"","data":{"items":[{"title":"","image":{"type":"image","data":{"uuid":"9978f1a1-3317-5f9f-9eb7-169cee555f79","width":1920,"height":800,"size":102436,"type":"png","color":"2d4e6c","hash":"","external_service":[]}}}]}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Варианты на выбор"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Симулятор и Отладочные Сборки"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Например, #if DEBUG гарантирует, что обернутый код не будет включен в релизные сборки вашего приложения. Аналогично, #if targetEnvironment(simulator) будет компилироваться только когда таргетом сборки вы назначаете свой iOS-симулятор.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Когда SwiftUI был выпущен в начальной стадии, превью были заключены в директиву #if DEBUG, чтобы они не раздували двоичный файл App Store. Начиная с Xcode 11 Apple делает это автоматически. Аналогично, такие инструменты, как FLEX и Inject, отключают определенные функциональные возможности при компиляции для релизных сборок, демонстрируя хороший пример использования.

"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"TestFlight"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Проверив URL-расписку в App Store, можно определить, была ли установлена сборка вашего приложения через TestFlight. Это может быть полезно для включения возможностей отладки для определенных подписанных пользователей.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Однако нет различий между внутренними группами TestFlight и различными внешними группами TestFlight (что может означать предоставление доступа к большей группе, чем вы предполагали). Кроме того, это решение не работает для приложений Catalyst или приложений, распространяемых через TestFlight для macOS. Он официально не поддерживается Apple и может отказать в любой момент.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Вы можете рассмотреть возможность создания отдельной конфигурации сборки TestFlight. Это позволит вам использовать директивы компилятора (похожие на #if DEBUG) и создавать специальные сборки только для TestFlight. Вы можете отправлять разные сборки разным группам, что даст вам полный контроль.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Жесты"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Konami Code — это чит-код, состоящий из последовательности 10 нажатий кнопок, ставший популярным в 1980-х годах благодаря игре Contra, ввод которой давал игроку 30 дополнительных жизней. В настоящее время он был адаптирован для мобильных устройств с использованием жестов смахивания. Есть много библиотек с открытым исходным кодом, обеспечивающих такое поведение — я знаю это, потому что, если вы проверите страницу благодарностей с открытым исходным кодом ряда крупных приложений, вы можете просто найти ссылку на нее.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Схемы URL и Вводы Текста"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Определить пользовательскую схему URL в iOS так же просто, как добавить пару строк в ваш Info.plist и обработать URL в файле AppDelegate или блоке onOpenURL в SwiftUI. Если у вас есть поле ввода текста, например строка поиска, вместо URL вы можете использовать его значение.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Это позволит тестовому пользователю ввести URL, например sidetrack://super-secret-debug-menu, в приложение, такое как Safari, и ваше приложение предоставит ему доступ к меню отладки.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Тестовые Аккаунты"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Доверенные Профили (Наше Решение)"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Решение, к которому мы пришли в Sidetrack, заключалось в использовании профиля конфигурации. Это особый вид сертификата, который можно создать с помощью Keychain Access и установить на свои мобильные устройства в качестве пользовательского профиля. Затем ваше мобильное приложение может определить наличие этого пользовательского профиля, проверив файл сертификата, встроенный в двоичный файл приложения.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Но как именно это работает? Что ж, он опирается на основу безопасности Apple, а точнее на функцию SecTrustEvaluate, которая позволяет нам оценить, доверяем ли мы данному сертификату (профилю, который загружается из двоичного файла приложения). Поскольку сертификат использует пользовательский центр подписи, это значение будет истинным только в том случае, если на устройстве установлен специальный профиль конфигурации, который мы генерируем отдельно и который связан с центром подписи. Только соответствующий профиль приведет к успешному выполнению кода.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Другие варианты"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Мы понимаем, что это не исчерпывающий список. В ходе нашего исследования мы выявили множество других решений, в том числе:

"}},{"type":"list","cover":false,"hidden":false,"anchor":"","data":{"items":["Пароль или PIN-код запрашиваемый при открытии меню отладки","Разблокировка меню отладки с помощью удаленного push-уведомления","Проверка определенных метаданных устройства, таких как имя или адрес подключенного WiFi","Использование общей связки ключей или группы пользовательских настроек по умолчанию, а также другого приложения, установленного на том же устройстве."],"type":"UL"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Мы будем рады услышать ваши отзывы, возможно, есть решение, которое мы не рассмотрели, или вы узнали здесь что-то новое. В любом случае вы можете связаться с нами через наш Twitter.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Оригинал статьи

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Подписывайся на наши соцсети: Telegram / VKontakte

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Вступай в открытый чат для iOS-разработчиков: t.me/swiftbook_chat

"}}],"summaryContent":null,"isExistSummaryContent":false,"warningFromEditor":null,"warningFromEditorTitle":null,"counters":{"comments":0,"favorites":0,"reposts":0,"views":77,"hits":125,"reads":null,"online":0},"dateFavorite":0,"hitsCount":125,"isCommentsEnabled":true,"isLikesEnabled":true,"isRemovedByUserRequest":false,"isFavorited":false,"isPinned":false,"repostId":null,"repostData":null,"subscribedToTreads":false,"isEditorial":false,"isAudioAvailable":false,"audioUrl":null,"isAudioAvailableToGenerate":false,"commentEditor":{"enabled":true,"who":null,"text":"","until":null,"reason":null,"type":"everybody"},"isBlur":false,"isPublished":true,"isDisabledAd":false,"withheld":[],"ogTitle":null,"ogDescription":null,"url":"https://vc.ru/id1165340/584196-bezopasnaya-otladka-vashego-prilozheniya-v-prodakshene","author":{"id":1165340,"name":"SwiftBook","nickname":null,"description":"Онлайн-школа мобильной разработки. Учим говорить правильно на языках Swift и Kotlin.","uri":"","avatar":{"type":"image","data":{"uuid":"41f2e62d-b447-56ec-b475-787323655092","width":500,"height":500,"size":30964,"type":"jpg","color":"e40424","hash":"","external_service":[]}},"cover":{"cover":{"type":"image","data":{"uuid":"b66c42a4-2ade-5705-b9ef-bc5f7876e598","width":1590,"height":400,"size":35401,"type":"jpg","color":"1c1c1c","hash":"","external_service":[]}},"cover_y":0},"achievements":[{"title":"Год на vc.ru","code":"registration_1_year","description":"Первый год с vc.ru. Получена 24 июля 2025.","previewUuid":"0d11c244-49de-50e7-894e-b9b27945d42b","formats":{"glb":"https://static.vc.ru/achievements/fish.glb","usdz":"https://static.vc.ru/achievements/fish.usdz"},"viewData":{"contentColor":"#C67AA3","textMaxWidth":0.634765625,"textX":0.5888671875,"textY":0.54296875,"logoX":0.5859375,"logoY":0.6669921875,"logoXNoText":0.6044921875,"logoYNoText":0.5439453125},"id":4277891,"userId":1165340,"count":0,"shareImage":"https://api.vc.ru/achievements/share/4277891"},{"title":"3 года на vc.ru","code":"registration_3_years","description":"Провёл 3 года вместе с vc.ru. Получена 23 июля 2025.","previewUuid":"d9d72ac5-bcb5-55e0-8c72-b99251e5cdd9","formats":{"glb":"https://static.vc.ru/achievements/shark.glb","usdz":"https://static.vc.ru/achievements/shark.usdz"},"viewData":{"contentColor":"#8E6F09","textMaxWidth":0.66796875,"textX":0.5205078125,"textY":0.341796875,"logoX":0.5205078125,"logoY":0.4609375,"logoXNoText":0.5,"logoYNoText":0.3662109375},"id":654357,"userId":1165340,"count":0,"shareImage":"https://api.vc.ru/achievements/share/654357"}],"lastModificationDate":1765017968,"isSubscribed":false,"isSubscribedToNewPosts":false,"isMuted":false,"isAvailableForMessenger":true,"badgeId":null,"isDonationsEnabled":false,"isPlusGiftEnabled":true,"isUnverifiedBlogForCompanyWithoutPro":true,"isRemovedByUserRequest":false,"isFrozen":false,"isDisabledAd":false,"isPlus":false,"isVerified":false,"isPro":false,"yandexMetricaId":null,"badge":null,"isOnline":false,"tgChannelShortname":null,"isUnsubscribable":true,"type":1,"subtype":"personal_blog"},"subsite":{"id":1165340,"name":"SwiftBook","nickname":null,"description":"Онлайн-школа мобильной разработки. Учим говорить правильно на языках Swift и Kotlin.","uri":"","avatar":{"type":"image","data":{"uuid":"41f2e62d-b447-56ec-b475-787323655092","width":500,"height":500,"size":30964,"type":"jpg","color":"e40424","hash":"","external_service":[]}},"cover":{"cover":{"type":"image","data":{"uuid":"b66c42a4-2ade-5705-b9ef-bc5f7876e598","width":1590,"height":400,"size":35401,"type":"jpg","color":"1c1c1c","hash":"","external_service":[]}},"cover_y":0},"achievements":[{"title":"Год на vc.ru","code":"registration_1_year","description":"Первый год с vc.ru. Получена 24 июля 2025.","previewUuid":"0d11c244-49de-50e7-894e-b9b27945d42b","formats":{"glb":"https://static.vc.ru/achievements/fish.glb","usdz":"https://static.vc.ru/achievements/fish.usdz"},"viewData":{"contentColor":"#C67AA3","textMaxWidth":0.634765625,"textX":0.5888671875,"textY":0.54296875,"logoX":0.5859375,"logoY":0.6669921875,"logoXNoText":0.6044921875,"logoYNoText":0.5439453125},"id":4277891,"userId":1165340,"count":0,"shareImage":"https://api.vc.ru/achievements/share/4277891"},{"title":"3 года на vc.ru","code":"registration_3_years","description":"Провёл 3 года вместе с vc.ru. Получена 23 июля 2025.","previewUuid":"d9d72ac5-bcb5-55e0-8c72-b99251e5cdd9","formats":{"glb":"https://static.vc.ru/achievements/shark.glb","usdz":"https://static.vc.ru/achievements/shark.usdz"},"viewData":{"contentColor":"#8E6F09","textMaxWidth":0.66796875,"textX":0.5205078125,"textY":0.341796875,"logoX":0.5205078125,"logoY":0.4609375,"logoXNoText":0.5,"logoYNoText":0.3662109375},"id":654357,"userId":1165340,"count":0,"shareImage":"https://api.vc.ru/achievements/share/654357"}],"lastModificationDate":1765017968,"isSubscribed":false,"isSubscribedToNewPosts":false,"isMuted":false,"isAvailableForMessenger":true,"badgeId":null,"isDonationsEnabled":false,"isPlusGiftEnabled":true,"isUnverifiedBlogForCompanyWithoutPro":true,"isRemovedByUserRequest":false,"isFrozen":false,"isDisabledAd":false,"isPlus":false,"isVerified":false,"isPro":false,"yandexMetricaId":null,"badge":null,"isOnline":false,"tgChannelShortname":null,"isUnsubscribable":true,"type":1,"subtype":"personal_blog"},"reactions":{"counters":[],"reactionId":0},"isNews":false,"source":null,"clusters":[],"donations":{"amount":0,"isDonated":false},"commentsSeenCount":null}},{"type":"entry","data":{"id":306690,"customUri":"cat-tracking","subsiteId":172558,"title":"Сделай сам: умная камера для наблюдения за питомцами","date":1634720722,"dateModified":1634720722,"blocks":[{"type":"text","cover":true,"hidden":false,"anchor":"","data":{"text":"

Обучаем нейросеть на котиках.

"}},{"type":"media","cover":true,"hidden":false,"anchor":"","data":{"items":[{"title":"","image":{"type":"image","data":{"uuid":"e2281589-5b83-5732-a7c5-d2e97548e1e9","width":3840,"height":2160,"size":2546255,"type":"png","color":"076ea5","hash":"","external_service":[]}}}]}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

В серии DIY-статей «Пространство для изобретений» мы пробуем в домашних условиях разработать необычные гаджеты и оставляем все необходимые инструкции, чтобы любой желающий мог повторить наш опыт. Серию статей поддерживает Selectel — провайдер ИТ-инфраструктуры, которая помогает в решении рабочих задач и разработке личных проектов. Посмотреть, что интересного есть у провайдера, и выбрать для себя подходящие решения можно на сайте.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Зачем нужна камера"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Рассказываю, что из этого вышло.

"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Что потребуется для сборки"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Большую часть времени коты проводят на участке — там у меня уже установлена система видеонаблюдения из четырёх аналоговых камер с простым регистратором Falcon Eye, поэтому ничего нового я не покупал. Для моей задумки было важно, чтобы регистратор мог отдавать в сеть RTSP-поток для покадровой обработки видео в режиме реального времени.

"}},{"type":"media","cover":false,"hidden":false,"anchor":"","data":{"items":[{"title":"","image":{"type":"image","data":{"uuid":"93f08457-5d04-56b2-9037-6c5ff11b589a","width":1486,"height":1334,"size":208285,"type":"png","color":"d2d3d0","hash":"","external_service":[]}}}]}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Устанавливаем софт"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Установил утилиту Virtualenv — она позволяет создавать изолированные виртуальные окружения для Python: создаёт внутри вашего проекта каталог, в который устанавливает нужную версию Python и все необходимые пакеты для приложения, фреймворка или скрипта. В результате вы получаете рабочее, полностью изолированное и переносимое окружение — это значит, что пакеты мы сможем устанавливать именно в это окружение, а не глобально.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Устанавливаю в терминале

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"sudo pip3 install virtualenv","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Если pip не установлен

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"sudo apt install python3-pip\npip3 install –upgrade pip","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Создаю виртуальную среду в папку Venv1 с python3.6

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"virtualenv -p python3.6 project/Venv1","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

И запускаю

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"source project/Venv1/bin/activate","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Для выхода из среды Venv1

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"deactivate","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Теперь устанавливаю библиотеку PixelLib, проект буду создавать в Virtualenv.

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"source project/Venv1/bin/activate","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

PixelLib требует Python версии 3.5–3.7 и версию pip больше 19.0.

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"pip3 install pip","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Устанавливаю последнюю версию Tensorflow (Tensorflow 2.0+)

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"pip3 install tensorflow","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

А теперь imgaug:

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"pip3 install imgaug","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

И последний шаг — установка PixelLib:

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"pip3 install pixellib --upgrade","lang":""}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Захватываем RTSP-поток с камер"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Регистратор отдаёт rtsp-поток по адресу

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"rtsp://:@:554/mode=real&idc=&ids=1","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Сначала я настроил камеры в самом щадящем режиме — один кадр в секунду. А ещё отключил на них режим «тревоги», при котором поток увеличивается до 15 кадров в секунду.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Пробую захватить поток с одной камеры и получить картинку в виде фрейма для последующей обработки. Полученную картинку прогоняю через детектор объекта (модель mask_rcnn_coco.h5, ссылка для скачивания) и сохраняю в папке, если объект (кот) обнаружен.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Библиотека PixelLib хорошо документирована, в ней очень много примеров. Для этого скрипта, например, я использую Instance segmentation of images with PixelLib.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Подключение библиотек

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"import os\nos.environ[\"TF_CPP_MIN_LOG_LEVEL\"] = \"3\"\nimport time\n#import _thread\nfrom pixellib.instance import instance_segmentation\nimport cv2\nfrom datetime import datetime","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Загрузка модели (указываю путь к скачанной модели mask_rcnn_coco.h5):

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"segment_frame.load_model(\"mask_rcnn_coco.h5\")","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Выбор объектов для определения (для тестирования person и cat):

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"target_class = segment_frame.select_target_classes(person=True,cat=True)","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Создание объекта камеры:

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"vcap = cv2.VideoCapture(urlRtsp+\"&idc=\"+idc+\"&ids=\"+ids)","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Получение кадра:

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"ret, frame = vcap.read()","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"output=cv2.resize(frame,(int(frame.shape[1]/2),int(frame.shape[0]/2)))","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

И прогон через детектор:

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"result,img = segment_frame.segmentFrame (\n output,\n #show_bboxes=True,\n segment_target_classes=target_class,\n #extract_segmented_objects=True,\n #save_extracted_objects=True,\n #output_image_name=\"output.jpg\"\n )","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

При ненулевом результате сохранение картинки в папку:

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"f=cv2.imwrite(pathSaveImg+\"/cam\"+idc+\"/\"+ftime.strftime(\"%d-%m-%Y\")+\"/_\"+ftime.strftime(\"%H:%M:%S.%f\")+\".jpg\", output)","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Скрипт целиком:

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"import os\nos.environ[\"TF_CPP_MIN_LOG_LEVEL\"] = \"3\"\nimport time\n#import _thread\nfrom pixellib.instance import instance_segmentation\nimport cv2\nfrom datetime import datetime\n\n# \"rtsp://admin:191066@192.168.0.109:554/mode=real&idc=3&ids=1\"\nurlRtsp=\"rtsp://admin:191066@192.168.0.109:554/mode=real\"\npathSaveImg=\"/home/petin/python3_prgs_1\"\nidc=\"3\"\nind=1;\nids=\"1\"\ntimePrevFrame=0\ncountAll=0\ncountDetect=0\n\nsegment_frame = instance_segmentation(infer_speed = \"fast\")\nsegment_frame.load_model(\"mask_rcnn_coco.h5\")\ntarget_class = segment_frame.select_target_classes(person=True,cat=True)\n\nvcap = cv2.VideoCapture(urlRtsp+\"&idc=\"+idc+\"&ids=\"+ids)\nprint(\"start**********************************************\")\n\nwhile(1):\n\n ret, frame = vcap.read()\n if ret:\n dt = datetime.now() \n countAll=countAll+1\n print(\"countAll=\",countAll)\n print(\" pixellib \")\n output=cv2.resize(frame,(int(frame.shape[1]/2),int(frame.shape[0]/2)))\n timePrevFrame=dt.timestamp()\n result,img = segment_frame.segmentFrame (\n output,\n #show_bboxes=True,\n segment_target_classes=target_class,\n #extract_segmented_objects=True,\n #save_extracted_objects=True,\n #output_image_name=\"output.jpg\"\n )\n dt = datetime.now()\n timeTekFrame=dt.timestamp()\n print(timePrevFrame,\" \",timeTekFrame,\" \",timeTekFrame-timePrevFrame)\n objects_count = len(result[\"scores\"])\n print(f\"Найдено объектов: {objects_count}\")\n if objects_count>0:\n #_thread.start_new_thread(detect,(frame,))\n ftime=datetime.now()\n if(os.path.exists(pathSaveImg+\"/cam\"+idc+\"/\"+ftime.strftime(\"%d-%m-%Y\"))==False):\n os.mkdir(pathSaveImg+\"/cam\"+idc+\"/\"+ftime.strftime(\"%d-%m-%Y\"))\n f=cv2.imwrite(pathSaveImg+\"/cam\"+idc+\"/\"+ ftime.strftime(\"%d-%m-%Y\")+\"/_\"+ftime.strftime(\"%H:%M:%S.%f\")+\".jpg\", output)\n print(\"write file = \",f)\n cv2.imshow('VIDEO1', output)\n elif ret:\n vcap.release()\n vcap = cv2.VideoCapture(urlRtsp+\"&idc=\"+idc+\"&ids=\"+ids)\n print(\"restart vcap\")\n \n else:\n print(\"no ret\",countAll)\n\n cv2.waitKey(1)","lang":""}},{"type":"media","cover":false,"hidden":false,"anchor":"","data":{"items":[{"title":"","image":{"type":"image","data":{"uuid":"fd981354-cc76-5e24-81fb-786feedfe4c8","width":1770,"height":868,"size":152355,"type":"png","color":"412244","hash":"","external_service":[]}}}]}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Запускаю программу — опознание приемлемое, но скорость не радует: на анализ картинки уходит секунда. Это неудивительно: детектор работает с CPU. Для работы с графикой библиотеке необходима установка CUDA, а для этого нужны графическая карта NVIDIA, которой на NUC нет.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Есть пара альтернатив: можно установить OpenVINO и подключить Neural Compute Stick 2 или попробовать Nvidia Jetson Nano. У меня был Neural Compute Stick 2, поэтому пробую этот вариант.

"}},{"type":"media","cover":false,"hidden":false,"anchor":"","data":{"items":[{"title":"","image":{"type":"image","data":{"uuid":"7d59cad1-b06d-5820-a5f8-4003a414f455","width":1462,"height":706,"size":124966,"type":"png","color":"457ece","hash":"","external_service":[]}}}]}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Устанавливаю OpenVINO toolkit — открытый бесплатный набор инструментов, который помогает детектировать лицо, распознавать объекты, текст и речи. По описаниям Intel производительность OpenVINO при вычислении сетей на платформах Intel в разы выше по сравнению с популярными фреймворками, а требования памяти — значительно ниже.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Во время установки выбираю последний дистрибутив 2021.3.394 и устанавливаю Intel® Neural Compute Stick 2. OpenVINO включает в себя несколько модулей и скриптов. Сразу попробовал, как работает определение объектов. Я использовал модель mobilenet-ssd — её нужно преобразовать во внутренний формат OpenVino с помощью Model Optimizer.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Загрузка модели

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

путь к downloader.py

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"/opt/intel/openvino_2021/deployment_tools/open_model_zoo/tools/downloader","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

команда загрузки модели

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"python3 /opt/intel/openvino_2021/deployment_tools/open_model_zoo/tools/downloader/downloader.py --name mobilenet-ssd","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Перед запуском примера с обученной моделью убедитесь, что она преобразована в формат механизма вывода (* .xml + * .bin) с помощью инструмента Model Optimizer.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Конвертирование модели

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

путь к converter.py

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"/opt/intel/openvino_2021/deployment_tools/open_model_zoo/tools/downloader","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

После конвертирования получаю модель в формате OpenVino.

"}},{"type":"media","cover":false,"hidden":false,"anchor":"","data":{"items":[{"title":"","image":{"type":"image","data":{"uuid":"410340e0-d392-504d-8cd4-699f4a13b62d","width":1211,"height":458,"size":17075,"type":"png","color":"f2f2f2","hash":"","external_service":[]}}}]}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Проверяю работу на старых фотографиях.

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"python3 /opt/intel/openvino_2021/inference_engine/samples/python/object_detection_sample_ssd.py -i cat01.jpg -m home/petin/public/mobilenet-ssd/FP32/mobilenet-ssd.xml -nt 5 -d CPU","lang":""}},{"type":"media","cover":false,"hidden":false,"anchor":"","data":{"items":[{"title":"","image":{"type":"image","data":{"uuid":"5d21e25f-6bc2-59ca-a244-502818ee8074","width":995,"height":502,"size":109987,"type":"png","color":"251821","hash":"","external_service":[]}}},{"title":"","image":{"type":"image","data":{"uuid":"d5ce6954-7d4a-5d18-aa1c-9f47fff82a6d","width":992,"height":490,"size":93856,"type":"png","color":"c8b8b9","hash":"","external_service":[]}}},{"title":"","image":{"type":"image","data":{"uuid":"c7c54374-f166-5435-bcd3-84ed2d4e1f88","width":983,"height":477,"size":92462,"type":"png","color":"c8bbbb","hash":"","external_service":[]}}},{"title":"","image":{"type":"image","data":{"uuid":"10af6a5e-d125-58d6-a849-7cd9886ec277","width":978,"height":521,"size":109083,"type":"png","color":"c3afb3","hash":"","external_service":[]}}},{"title":"","image":{"type":"image","data":{"uuid":"17e0a457-eec2-5dc5-a9a7-d2dba7a95e56","width":1020,"height":516,"size":120621,"type":"png","color":"302029","hash":"","external_service":[]}}},{"title":"","image":{"type":"image","data":{"uuid":"cd6f892d-af00-55eb-8a83-f9f57ad7cfab","width":1000,"height":461,"size":101469,"type":"png","color":"c6baba","hash":"","external_service":[]}}},{"title":"","image":{"type":"image","data":{"uuid":"c7aceb39-79b2-5fb8-83ae-84f9e998f687","width":1025,"height":468,"size":119755,"type":"png","color":"c3b9b6","hash":"","external_service":[]}}}]}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Опознание в модели mobilenet-ssd приемлемое, буду использовать ее. Дополнительно устанавливаю пакет imutils.

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"pip3 install imutils","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Скачиваю файлы модели mobilenet-ssd.caffemodel и mobilenet-ssd.prototxt с помощью «Загрузчика моделей» (Model Downloader) OpenVino

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"python3 /opt/intel/openvino_2021/deployment_tools/open_model_zoo/tools/downloader/downloader.py ----name mobilenet-ssd","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Пишу скрипт захвата видео с четырёх камер, прогон картинки через детектор и сохранение картинки в каталог cam<n>/<d-n-Y> при детектировании кота.

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"# подключение библиотек\nfrom imutils.video import VideoStream\nfrom imutils.video import FPS\nimport numpy as np\nimport sys\nimport argparse\nimport imutils\nimport time\nimport cv2\nfrom urllib.request import urlopen\nfrom datetime import datetime\nimport os\n\ntekcamera=1\npathSaveImg=\"/home/petin/python3_prgs_1/OpenVino01\"\ncameras=[[0,0,0,0,0],\n\t[0,0,0,0,0],\n\t[False,True,False,True,False],\n\t[\"\",\"Camera1\",\"Camera2\",\"Camera3\",\"Camera4\"]]\nfor i in range(1,5):\n\tcameras[1][i] = 'rtsp://admin:191066@192.168.0.109:554/mode=real&idc='+str(i)+'&ids=1'\n\nfps = FPS().start()\n\n# получение аргументов командной строки\n# --prototxt путь к файлу mobilenet-ssd.prototxt\n# --model путь к файлу модели mobilenet-ssd.caffemodel\n# --show вывод изображений с камер в окна\n# -c минимальная точность определения объекта\n\nap = argparse.ArgumentParser()\nap.add_argument(\"--prototxt\", required=True,\n\thelp=\"path to Caffe 'deploy' prototxt file\")\nap.add_argument(\"--model\", required=True,\n\thelp=\"path to Caffe pre-trained model\")\nap.add_argument(\"--show\", required=True, \n\thelp=\"Show cv2.imshow)\")\nap.add_argument(\"-c\", \"--confidence\", type=float, default=0.2,\n\thelp=\"minimum probability to filter weak detections\")\nargs = vars(ap.parse_args())\n\n# загрузка модели\nCLASSES = [\"background\", \"aeroplane\", \"bicycle\", \"bird\", \"boat\",\n\t\"bottle\", \"bus\", \"car\", \"cat\", \"chair\", \"cow\", \"diningtable\",\n\t\"dog\", \"horse\", \"motorbike\", \"person\", \"pottedplant\", \"sheep\",\n\t\"sofa\", \"train\", \"tvmonitor\"]\nCOLORS = np.random.uniform(0, 255, size=(len(CLASSES), 3))\nprint(\"[INFO] loading model...\")\nnet = cv2.dnn.readNetFromCaffe(args[\"prototxt\"], args[\"model\"])\n# обработка в Neural Compute Stick\nnet.setPreferableTarget(cv2.dnn.DNN_TARGET_MYRIAD)\n\n# инициализация получения потока с камер\nprint(\"[INFO] starting video stream...\")\n\nfor i in range(1,5):\n\tcameras[0][i] = cv2.VideoCapture(cameras[1][i])\nprint(\"OK\")\ntime.sleep(5.0)\n\ndetected_objects = []\n\n# цикл\nwhile(1):\n\ttekcamera = tekcamera+1\n if tekcamera+==5:\n tekcamera=1\n logfile=open(\"last.txt\",\"w+\")\n ftime=datetime.now()\n str1=.strftime(\"%d-%m-%Y % %H:%M:%S\\n \")\n logfile.write(str1)\n logfile.close()\n\tif cameras[2][tekcamera] == False:\n\t\tcontinue\t\n\t# получение кадров из потока\n\tret, frame = cameras[0][tekcamera].read()\n\t\n\tframe = imutils.resize(frame, width=800)\n\t\t\n\t# grab the frame dimensions and convert it to a blob\n\t(h, w) = frame.shape[:2]\n\tblob = cv2.dnn.blobFromImage(cv2.resize(frame, (300, 300)),\n\t\t0.007843, (300, 300), 127.5)\n\n\t# pass the blob through the network and obtain the detections and\n\t# predictions\n\tnet.setInput(blob)\n\tdetections = net.forward()\n \n\t# обработка результатов детектирования\n\tprint(\"******************\")\n\tfor i in np.arange(0, detections.shape[2]):\n\t\tconfidence = detections[0, 0, i, 2]\n\t\tidx = int(detections[0, 0, i, 1])\n\t\t#if confidence > args[\"confidence\"] and idx==15 :\n\t\tif confidence > args[\"confidence\"] and (idx==8 or idx==12) :\n\t\t\t# save files\n\t\t\tftime=datetime.now()\n\t\t\tif(os.path.exists(pathSaveImg+\"/cam\"+str(tekcamera)+\"/\"+ftime.strftime(\"%d-%m-%Y\"))==False):\n\t\t\t\tos.mkdir(pathSaveImg+\"/cam\"+str(tekcamera)+\"/\"+ftime.strftime(\"%d-%m-%Y\"))\n\t\t\tf=cv2.imwrite(pathSaveImg+\"/cam\"+str(tekcamera)+\"/\"+ftime.strftime(\"%d-%m-%Y\")+\"/_\"+ftime.strftime(\"%H:%M:%S.%f\")+\".jpg\", frame)\n\t\t\tprint(\"write file = \",f)\n\n\t\t\t# extract the index of the class label from the\n\t\t\t# `detections`, then compute the (x, y)-coordinates of\n\t\t\t# the bounding box for the object\n\t\t\t#idx = int(detections[0, 0, i, 1])\n\t\t\tbox = detections[0, 0, i, 3:7] * np.array([w, h, w, h])\n\t\t\t(startX, startY, endX, endY) = box.astype(\"int\")\n\n\t\t\t# draw the prediction on the frame\n\t\t\tlabel = \"{}: {:.2f}%\".format(CLASSES[idx],\n\t\t\t\tconfidence * 100)\n\t\t\tprint(confidence,\" \",idx,\" - \",CLASSES[idx])\n\t\t\tdetected_objects.append(label)\n\t\t\tcv2.rectangle(frame, (startX, startY), (endX, endY),\n\t\t\t\tCOLORS[idx], 2)\n\t\t\ty = startY - 15 if startY - 15 > 15 else startY + 15\n\t\t\tcv2.putText(frame, label, (startX, y),\n\t\t\t\tcv2.FONT_HERSHEY_SIMPLEX, 0.5, COLORS[idx], 2)\t\n\t# show the output frame\n\tif args[\"show\"] == \"True\":\n\t\tcv2.imshow(cameras[3][tekcamera], frame)\n\n\tkey = cv2.waitKey(1) & 0xFF\n\t# выход по клавише 'q'\n\tif key == ord(\"q\"):\n\t\tbreak\n\tfps.update()\n\nfps.stop()\nprint(\"[INFO] elasped time: {:.2f}\".format(fps.elapsed()))\nprint(\"[INFO] approx. FPS: {:.2f}\".format(fps.fps()))\n# do a bit of cleanup\ncv2.destroyAllWindows()","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Запуск скрипта на выполнение

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"source Venv1/bin/activate\ncd /home/petin/python3_prgs_1/OpenVino01\npython3 pets04.py --prototxt mobilenet-ssd.prototxt --model mobilenet-ssd.caffemodel --show True -c 0.3","lang":""}},{"type":"media","cover":false,"hidden":false,"anchor":"","data":{"items":[{"title":"","image":{"type":"image","data":{"uuid":"7aa7dc80-8d78-5283-87dd-f9fe80c01d90","width":942,"height":713,"size":53785,"type":"png","color":"ecebe7","hash":"","external_service":[]}}}]}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Изображения сохраняются в папках cam/. Скорость хорошая — для двух камер при частоте 5 кадров в секунду задержек нет. Но получается слишком много фото, поэтому поток я уменьшил до 1 кадра в секунду.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Обязательно делаю автозагрузку скрипта. Создаю файл autoupload.sh следующего содержания:

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"#!/bin/bash\nsource Venv1/bin/activate\ncd /home/petin/python3_prgs_1/OpenVino01\npython3 pets04.py --prototxt mobilenet-ssd.prototxt --model mobilenet-ssd.caffemodel --show True -c 0.3","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

И помещаю в автозагрузку autoupload.sh.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Обзор → Автоматически запускаемые приложения → Добавить

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

И указываю путь к python-скрипту.

"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Собираем видео и отправляем в Telegram"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Фотографии котов я хочу получать в виде ролика. Для этого устанавливаю пакет ffmpeg.

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"sudo apt install ffmpeg","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

И запускаю следующие команды:

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Для камеры 1:

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"ffmpeg -f image2 -pattern_type glob -i /home/petin/python3_prgs_1/OpenVino01/cam1/$(date +%d-%m-%Y)/'*.jpg' -y /home/petin/python3_prgs_1/OpenVino01/cam1-$(date +%d-%m-%Y).mp4 –r 10","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Для камеры 2:

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"ffmpeg -f image2 -pattern_type glob -i /home/petin/python3_prgs_1/OpenVino01/cam2/$(date +%d-%m-%Y)/'*.jpg' -y /home/petin/python3_prgs_1/OpenVino01/cam2-$(date +%d-%m-%Y).mp4 –r 10","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Для остальных камер — по аналогии. Показалось, что фото будет много, поэтому выбрал скорость 10 кадров в секунду. Команды помещаю в cron (запуск в конце дня — в 18:00) для формирования файлов cam-.mp4.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Создаю файл cron.sh:

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"#!/bin/bash\nffmpeg -f image2 -pattern_type glob -r 10 -i /home/petin/python3_prgs_1/OpenVino01/cam1/$(date +%d-%m-%Y)/'*.jpg' -y /home/petin/python3_prgs_1/OpenVino01/cam2-$(date +%d-%m-%Y).mp4\nffmpeg -f image2 -pattern_type glob -r 10 -i /home/petin/python3_prgs_1/OpenVino01/cam2/$(date +%d-%m-%Y)/'*.jpg' -y /home/petin/python3_prgs_1/OpenVino01/cam2-$(date +%d-%m-%Y).mp4\nffmpeg -f image2 -pattern_type glob -r 10 -i /home/petin/python3_prgs_1/OpenVino01/cam3/$(date +%d-%m-%Y)/'*.jpg' -y /home/petin/python3_prgs_1/OpenVino01/cam3-$(date +%d-%m-%Y).mp4\nffmpeg -f image2 -pattern_type glob -r 10 -i /home/petin/python3_prgs_1/OpenVino01/cam4/$(date +%d-%m-%Y)/'*.jpg' -y /home/petin/python3_prgs_1/OpenVino01/cam4-$(date +%d-%m-%Y).mp4","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Добавляю задание cron. Записываю в файл /var/spool/cron/crontabs/petin строку

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"* 18 * * * /home/petin/python3_prgs_1/OpenVino01/cron.sh","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Теперь настраиваю отправку файлов в Telegram. Создаю телеграм-бота: пишу пользователю @BotFather и придумываю имя. В ответ приходит сообщение с токеном для http api — его нужно сохранить.

"}},{"type":"media","cover":false,"hidden":false,"anchor":"","data":{"items":[{"title":"","image":{"type":"image","data":{"uuid":"5e70f2b2-17ca-5232-b92d-b47f020f4806","width":552,"height":531,"size":38251,"type":"png","color":"bbcfcd","hash":"","external_service":[]}}}]}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Создаю группу, добавляю в неё бота, устанавливаю библиотеку:

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"pip3 install pytelegrambotapi","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

И пишу программу:

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"import telebot\nfrom telebot import types\nfrom datetime import datetime, timedelta\nfrom pathlib import Path\n\nkeyboard1 = [[\"\",\"Камера 1\",\"Камера 2\",\"Камера 3\",\"Камера 4\",\"Не надо\"],[\"\",\"cam1\",\"cam2\",\"cam3\",\"cam4\",\"no\"]]\nbot = telebot.TeleBot('your-token');\n\n@bot.message_handler(content_types=['text', 'document', 'audio'])\ndef get_text_messages(message):\n\tchatId=message.chat.id\n\tprint(chatId)\n\tkey_1=[0,0,0,0,0,0]\n\tftime=datetime.now()\n\tprint(ftime.day,\" \",ftime.hour)\n\tday=ftime.strftime(\"%d-%m-%Y\")\n\tif message.text == \"/help\":\n\t\tbot.send_message(message.from_user.id, \"Привет, здесь ты можешь посмотреть, что делали мои котики за день 9:00–18:00. Набери /video\")\n\telif message.text == \"/video\":\n\t\tmsg=\"здесь ты можешь посмотреть, что делали мои котики за день 9:00–18:00\"+day\n\t\tbot.send_message(message.from_user.id, msg)\n\t\tkeyboard = types.InlineKeyboardMarkup(); # клавиатура\n\t\tfor i in range(1,6):\n\t\t\t#кнопка\n\t\t\tkey_1[i] = types.InlineKeyboardButton(text=keyboard1[0][i], callback_data=keyboard1[1][i]); \n\t\t\t#добавляем кнопку в клавиатуру\n\t\t\tkeyboard.add(key_1[i]); \n\t\tbot.send_message(message.from_user.id, \"Выбери камеру\", reply_markup=keyboard) \n\telse:\n\t\tbot.send_message(message.from_user.id, \"Я тебя не понимаю. Напиши /help.\")\n\n@bot.callback_query_handler(func=lambda call: True)\ndef callback_worker(call):\n\tftime=datetime.now()\n\tday=ftime.strftime(\"%d-%m-%Y\")\n\n\tif call.data.find(\"cam\") < 0:\n\t\tbot.send_message(call.message.chat.id, 'No');\t\t\n\telse:\n\t\tif ftime.hour >= 18:\n\t\t\tbot.send_message(call.message.chat.id, 'Видео с камеры '+call.data.replace(\"cam\",\"\")+'. Wait ....');\n\t\t\tvideofile = Path(call.data+'-'+day+'.mp4')\n\t\t\tif videofile.is_file():\n\t\t\t\tvideo = open(call.data+'-'+day+'.mp4', 'rb')\n\t\t\t\tbot.send_video(call.message.chat.id, video)\n\t\t\t\tbot.send_message(call.message.chat.id, \"Загружено\")\n\t\t\telse:\n\t\t\t\tbot.send_message(call.message.chat.id, 'Видео отсутствует !!!');\t\n\t\telse:\n\t\t\tbot.send_message(call.message.chat.id, 'Просмотр видео текущего дня только после 18:00, Просмотр за предыдущий день');\n\t\t\tftime=datetime.now()- timedelta(days=1)\n\t\t\tdayold=ftime.strftime(\"%d-%m-%Y\")\n\t\t\tvideofile = Path(call.data+'-'+dayold+'.mp4')\n\t\t\tif videofile.is_file():\n\t\t\t\tvideo = open(call.data+'-'+dayold+'.mp4', 'rb')\n\t\t\t\tbot.send_video(call.message.chat.id, video)\n\t\t\t\tbot.send_message(call.message.chat.id, \"Загружено\")\n\t\t\telse:\n\t\t\t\tbot.send_message(call.message.chat.id, 'Видео отсутствует !!!');\t\n\t\t\t\nbot.polling(none_stop=True, interval=0)","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Запуск бота (чтобы он мог отвечать на сообщения, скрипт должен быть постоянно запущен):

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"cd /home/petin/python3_prgs_1/OpenVino01\npython3 pets_send_telegram.py","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Скрипт бота также добавляю в автозагрузку.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Создаю файл autoupload_telegram.sh

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"#!/bin/bash\ncd /home/petin/python3_prgs_1/OpenVino01\npython3 pets_send_telegram.py","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

И помещаю в автозагрузку autoupload.sh.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Обзор → Автоматически запускаемые приложения → Добавить

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

И указываю путь к python-скрипту.

"}},{"type":"media","cover":false,"hidden":false,"anchor":"","data":{"items":[{"title":"","image":{"type":"image","data":{"uuid":"1b2d64eb-ab4c-50b9-8aec-d8a4be229dbc","width":1410,"height":486,"size":55308,"type":"png","color":"52514f","hash":"","external_service":[]}}}]}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Бот знает две команды: /help и /video. Когда просишь видео — открывает меню с выбором камеры. Бот присылает видео от сегодняшнего дня (если просим после шести вечера) или от вчерашнего.

"}},{"type":"media","cover":false,"hidden":false,"anchor":"","data":{"items":[{"title":"","image":{"type":"image","data":{"uuid":"212b0675-ce86-5452-8746-1a3a6328eff8","width":517,"height":500,"size":64534,"type":"png","color":"719971","hash":"","external_service":[]}}}]}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Выбираем место для установки камер"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"media","cover":false,"hidden":false,"anchor":"","data":{"items":[{"title":"","image":{"type":"image","data":{"uuid":"47ce9004-9612-5aa3-b4a7-84024c15a752","width":1920,"height":1080,"size":412204,"type":"jpg","color":"404641","hash":"","external_service":[]}}},{"title":"","image":{"type":"image","data":{"uuid":"2c516f5d-0917-5844-961a-2fe64eb75daa","width":1920,"height":1080,"size":130875,"type":"jpg","color":"2d3633","hash":"","external_service":[]}}},{"title":"","image":{"type":"image","data":{"uuid":"674e4971-13ba-5016-a5c4-6c83035191b4","width":1920,"height":1080,"size":229616,"type":"jpg","color":"4e5954","hash":"","external_service":[]}}}]}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Подсоединяю камеры к регистратору.

"}},{"type":"media","cover":false,"hidden":false,"anchor":"","data":{"items":[{"title":"","image":{"type":"image","data":{"uuid":"4379c3a9-b786-5064-aca9-d632e6349958","width":1920,"height":1080,"size":299727,"type":"jpg","color":"2e241d","hash":"","external_service":[]}}}]}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

В сети — роутер, регистратор, NUC по Wi-Fi. При загрузке запускаются python-скрипт получения данных с камер и детектирования, плюс скрипт бота.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Захожу в Telegram, выбираю камеру и… начинаю смотреть за котиками.

"}},{"type":"media","cover":false,"hidden":false,"anchor":"","data":{"items":[{"title":"","image":{"type":"image","data":{"uuid":"df40b1b9-49c3-5b57-bb09-46075305404b","width":1412,"height":704,"size":377223,"type":"png","color":"687269","hash":"","external_service":[]}}}]}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Подводим итоги и приступаем к новой версии"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Все файлы проекта доступны на github.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Я доволен результатом — теперь могу медитировать, наблюдая за своими питомцами.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

До совершенства пока далеко: хвостатые постоянно меняют места отдыха и разбредаются, ловить их на большом участке непросто. К тому же появились новые «хотелки»: здорово было бы получать видео по каждому коту — для этого нужно обучить модель их различать. Занялся этим сразу.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Первым делом решил собрать фотографии питомцев: для тренировки сети нужно минимум по 300 фото каждого кота и по 100 — для тестирования. При этом коты не хотели фотографироваться в разных позах, так что после нескольких дней съёмок я собрал всего 314 снимков.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Выделил объекты на фото и пометил их (использую программу labelme):

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"sudo pip3 install labelme","lang":""}},{"type":"media","cover":false,"hidden":false,"anchor":"","data":{"items":[{"title":"","image":{"type":"image","data":{"uuid":"75ba7435-7a45-5a32-a388-834ce927175c","width":969,"height":769,"size":187828,"type":"png","color":"e3e2db","hash":"","external_service":[]}}},{"title":"","image":{"type":"image","data":{"uuid":"b90727a0-d900-5dac-9cc5-887c0ac06244","width":968,"height":526,"size":156163,"type":"png","color":"e5e4dc","hash":"","external_service":[]}}},{"title":"","image":{"type":"image","data":{"uuid":"082610e3-0f3f-5e2d-a834-b76e6d86c811","width":965,"height":529,"size":144475,"type":"png","color":"d8d9d3","hash":"","external_service":[]}}},{"title":"","image":{"type":"image","data":{"uuid":"e9360b1d-5601-5107-9c16-3ebe02c8284b","width":976,"height":534,"size":165402,"type":"png","color":"d1cfc6","hash":"","external_service":[]}}},{"title":"","image":{"type":"image","data":{"uuid":"e1351152-24bd-5033-adfc-31687b1bbae4","width":972,"height":531,"size":143078,"type":"png","color":"d4d1c4","hash":"","external_service":[]}}},{"title":"","image":{"type":"image","data":{"uuid":"170b5f8b-9b64-5bb2-adcb-d24e5a12d180","width":978,"height":532,"size":98485,"type":"png","color":"363536","hash":"","external_service":[]}}}]}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Для каждой фотографии создаётся файл json.

"}},{"type":"media","cover":false,"hidden":false,"anchor":"","data":{"items":[{"title":"","image":{"type":"image","data":{"uuid":"bad5e999-badb-5912-91d6-4f8508ef3bd9","width":1001,"height":941,"size":125133,"type":"png","color":"efeeed","hash":"","external_service":[]}}}]}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Разделяю фото в папки train и test и запускаю скрипт для обучения модели.

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"import pixellib\nfrom pixellib.custom_train import instance_custom_training\n\ntrain_maskrcnn = instance_custom_training()\ntrain_maskrcnn.modelConfig(network_backbone = \"resnet101\", num_classes= 2, batch_size = 4)\ntrain_maskrcnn.load_pretrained_model(\"mask_rcnn_coco.h5\")\ntrain_maskrcnn.load_dataset(\"Nature\")\ntrain_maskrcnn.train_model(num_epochs = 300, augmentation=True, path_trained_models = \"mask_rcnn_models\")","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

За 7 дней прошло 25 шагов тренировки из 300 — не очень-то быстро. Ускориться помогут либо мощное железо, либо облачные сервисы с графическими процессорами. Например, серверы с GPU от Selectel: провайдер предлагает мощную ресурсную базу, с которой время тренировки удастся сократить с нескольких недель — до дней или даже часов.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Вот такую конфигурацию служба поддержки Selectel подобрала для меня:

"}},{"type":"media","cover":false,"hidden":false,"anchor":"","data":{"items":[{"title":"","image":{"type":"image","data":{"uuid":"0e69a5c2-85bc-5da1-b6ef-8a380d9a0be5","width":1630,"height":816,"size":71298,"type":"png","color":"dadbd8","hash":"","external_service":[]}}}]}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

На сервере уже была установлена заказанная мной ОС Ubuntu 20.04. Устанавливаю CUDA по этой инструкции и библиотеку pixellib.

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"Install the latest version of tensorflow(Tensorflow 2.0+) with:\npip3 install tensorflow-gpu\npip3 install imgaug\npip3 install pixellib --upgrade","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Сделал много дополнительных фотографий, разделил их на две папки и разметил, как предыдущие в программе labelme. Загрузил набор данных на сервер.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Для обучения нашей модели будем применять методику трансферного обучения, используя базовую модель mask_rcnn_coco.h5, обученную на 80 категориях объектов.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Загрузка mask_rcnn_coco.h5

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"wget \"https://github.com/ayoolaolafenwa/PixelLib/releases/download/1.2/mask_rcnn_coco.h5\"","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Скрипт для обучения customtrain01.py

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"import pixellib\nfrom pixellib.custom_train import instance_custom_training\ntrain_maskrcnn = instance_custom_training()\ntrain_maskrcnn.modelConfig(network_backbone = \"resnet50\", num_classes= 5, batch_size = 4)\ntrain_maskrcnn.load_pretrained_model(\"mask_rcnn_coco.h5\")\ntrain_maskrcnn.load_dataset(\"pets03\")\ntrain_maskrcnn.train_model(num_epochs = 200, augmentation=True, path_trained_models = \"mask_rcnn_models\")","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Запуск скрипта

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"python3 customtrain01.py","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Процесс обучения отображается в терминале

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"Using resnet50 as network backbone For Mask R-CNN model\nApplying Default Augmentation on Dataset\nTrain 608 images\nValidate 89 images\nCheckpoint Path: /home/petin/petscats/mask_rcnn_models\nSelecting layers to train\nEpoch 1/100\n100/100 [==============================] - 394s 3s/step - batch: 49.5000 - size: 4.0000 - loss: 1.9734 - rpn_class_loss: 0.1194 - rpn_bbox_loss: 0.7562 - mrcnn_class_loss: 0.1773 - mrcnn_bbox_loss: 0.7554 - mrcnn_mask_loss: 0.1652 - val_loss: 1.8159 - val_rpn_class_loss: 0.0354 - val_rpn_bbox_loss: 0.7352 - val_mrcnn_class_loss: 0.2133 - val_mrcnn_bbox_loss: 0.7738 - val_mrcnn_mask_loss: 0.0583 - lr: 0.0010\nEpoch 2/100\n100/100 [==============================] - 287s 3s/step - batch: 49.5000 - size: 4.0000 - loss: 1.5770 - rpn_class_loss: 0.0347 - rpn_bbox_loss: 0.6571 - mrcnn_class_loss: 0.1848 - mrcnn_bbox_loss: 0.6495 - mrcnn_mask_loss: 0.0510 - val_loss: 1.5399 - val_rpn_class_loss: 0.0318 - val_rpn_bbox_loss: 0.5624 - val_mrcnn_class_loss: 0.1974 - val_mrcnn_bbox_loss: 0.6905 - val_mrcnn_mask_loss: 0.0578 - lr: 0.0010\nEpoch 3/100\n 13/100 [==>...........................] - ETA: 3:29 - batch: 6.0000 - size: 4.0000 - loss: 1.6279 - rpn_class_loss: 0.0347 - rpn_bbox_loss: 0.6848 - mrcnn_class_loss: 0.1869 - mrcnn_bbox_loss: 0.6644 - mrcnn_mask_loss: 0.0571","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Модели сохраняются в папку mask_rcnn_models на основе уменьшения потерь при проверке

"}},{"type":"media","cover":false,"hidden":false,"anchor":"","data":{"items":[{"title":"","image":{"type":"image","data":{"uuid":"b7af40db-0a55-5ab2-be60-9ea1a74d1997","width":2170,"height":688,"size":217269,"type":"png","color":"060606","hash":"","external_service":[]}}}]}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Запуск

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"python3 testtrain01.py","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Вывод

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"mask_rcnn_models\\ mask_rcnn_model.001-1.851553.h5 evaluation using iou_threshold 0.5 is 0.723500","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Проверяем модель на фотографиях

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"import pixellib\nfrom pixellib.instance import custom_segmentation\nsegment_image = custom_segmentation()\nsegment_image.inferConfig(num_classes= 5, class_names= [\"BG\", \"mimicat\", \"tigercat\",\"smokeycat\",\"redcat\",\"wildcat\"])\nsegment_image.load_model(\"mask_rcnn_models/mask_rcnn_model.xxx-1.yyyyyy.h5\")\nsegment_image.segmentImage(\"myimg.jpg\", show_bboxes=True, output_image_name=\"sample_out.jpg\")","lang":""}},{"type":"media","cover":false,"hidden":false,"anchor":"","data":{"items":[{"title":"","image":{"type":"image","data":{"uuid":"6185650f-3d8c-585e-ae56-ea3a272897b7","width":1566,"height":1126,"size":826916,"type":"png","color":"2f3425","hash":"","external_service":[]}}},{"title":"","image":{"type":"image","data":{"uuid":"cec72f4a-47bb-5812-b8a5-0349ca4c026e","width":1568,"height":1174,"size":983549,"type":"png","color":"6e774f","hash":"","external_service":[]}}},{"title":"","image":{"type":"image","data":{"uuid":"2d618750-add7-53e4-851a-98ba7fa4dfbe","width":1570,"height":1176,"size":795190,"type":"png","color":"babdb5","hash":"","external_service":[]}}}]}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Модель готова, теперь её можно использовать в проекте.

"}},{"type":"delimiter","cover":false,"hidden":false,"anchor":"","data":{"type":"default"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

За месяц аренды сервера для своей задачи я бы потратил 25 600 рублей. Целого сервера для одного пет-проекта «слишком много»: арендовать его можно совместно с друзьями, чтобы по очереди учить модели для разных задач.

"}},{"type":"special_button","cover":false,"hidden":false,"anchor":"","data":{"text":"Выбрать подходящий сервер","textColor":"#000000","backgroundColor":"#FFEBEF","url":"https://api.vc.ru/v2.8/redirect?to=https%3A%2F%2Fgo.the.tj%2FTA39qq&postId=306690"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

#SelectelDIY

"}}],"summaryContent":null,"isExistSummaryContent":false,"warningFromEditor":null,"warningFromEditorTitle":null,"counters":{"comments":6,"favorites":44,"reposts":0,"views":5,"hits":17868,"reads":null,"online":0},"dateFavorite":0,"hitsCount":17868,"isCommentsEnabled":true,"isLikesEnabled":true,"isRemovedByUserRequest":false,"isFavorited":false,"isPinned":false,"repostId":null,"repostData":null,"subscribedToTreads":false,"isEditorial":false,"isAudioAvailable":false,"audioUrl":null,"isAudioAvailableToGenerate":false,"commentEditor":{"enabled":true,"who":null,"text":"","until":null,"reason":null,"type":"everybody"},"isBlur":false,"isPublished":true,"isDisabledAd":false,"withheld":[],"ogTitle":null,"ogDescription":null,"url":"https://vc.ru/dev/306690-cat-tracking","author":{"id":172558,"name":"Selectel","nickname":null,"description":"Крупнейший независимый провайдер сервисов IT-инфраструктуры в России. Облачные серверы от 29 ₽/час: slc.tl/bfnnu","uri":"","avatar":{"type":"image","data":{"uuid":"bdaede51-3c77-55c3-a3ab-e9940df92258","width":640,"height":640,"size":103719,"type":"jpg","color":"d2454a","hash":"","external_service":[],"base64preview":"/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAAICAgICAQICAgIDAgIDAwYEAwMDAwcFBQQGCAcJCAgHCAgJCg0LCQoMCggICw8LDA0ODg8OCQsQERAOEQ0ODg7/2wBDAQIDAwMDAwcEBAcOCQgJDg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg7/wAARCAAKAAoDASIAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAACAIG/8QAIRAAAgEDBQADAAAAAAAAAAAAAQIDBAURAAYHITEIEiP/xAAVAQEBAAAAAAAAAAAAAAAAAAAEBf/EAB4RAAEDBAMAAAAAAAAAAAAAAAECAwQABREhQYHB/9oADAMBAAIRAxEAPwA8cV8WcVX34ebqv98rlj3FSozQO9yjhWMBcj8zlnLHzrB8yND6e2Wxa2ZRUhQHIA+3nertk8y7UqQszgDOAHPWsoSSSSck+nVKMw4h1wlwnJpl4uMR2FFSiMlJCdkc6A8z2a//2Q=="}},"cover":{"cover":{"type":"image","data":{"uuid":"861886cd-1ef2-5754-af38-6219cc645017","width":1280,"height":511,"size":28049,"type":"jpg","color":"142434","hash":"","external_service":[],"base64preview":"/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAcFBQYFBAcGBgYIBwcICxILCwoKCxYPEA0SGhYbGhkWGRgcICgiHB4mHhgZIzAkJiorLS4tGyIyNTEsNSgsLSz/2wBDAQcICAsJCxULCxUsHRkdLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCz/wAARCAAKAAoDASIAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAAAAMH/8QAGhAAAQUBAAAAAAAAAAAAAAAAAAEDE1SSAv/EABUBAQEAAAAAAAAAAAAAAAAAAAQF/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8AxOFmymFELNnnKkQNT3//2Q=="}},"cover_y":57},"achievements":[{"title":"Год на vc.ru","code":"registration_1_year","description":"Первый год с vc.ru. Получена 24 июля 2025.","previewUuid":"0d11c244-49de-50e7-894e-b9b27945d42b","formats":{"glb":"https://static.vc.ru/achievements/fish.glb","usdz":"https://static.vc.ru/achievements/fish.usdz"},"viewData":{"contentColor":"#C67AA3","textMaxWidth":0.634765625,"textX":0.5888671875,"textY":0.54296875,"logoX":0.5859375,"logoY":0.6669921875,"logoXNoText":0.6044921875,"logoYNoText":0.5439453125},"id":5249500,"userId":172558,"count":0,"shareImage":"https://api.vc.ru/achievements/share/5249500"},{"title":"3 года на vc.ru","code":"registration_3_years","description":"Провёл 3 года вместе с vc.ru. Получена 23 июля 2025.","previewUuid":"d9d72ac5-bcb5-55e0-8c72-b99251e5cdd9","formats":{"glb":"https://static.vc.ru/achievements/shark.glb","usdz":"https://static.vc.ru/achievements/shark.usdz"},"viewData":{"contentColor":"#8E6F09","textMaxWidth":0.66796875,"textX":0.5205078125,"textY":0.341796875,"logoX":0.5205078125,"logoY":0.4609375,"logoXNoText":0.5,"logoYNoText":0.3662109375},"id":1625413,"userId":172558,"count":0,"shareImage":"https://api.vc.ru/achievements/share/1625413"},{"title":"5 лет на vc.ru","code":"registration_5_years","description":"Провёл 5 лет вместе с vc.ru. Получена 23 июля 2025.","previewUuid":"a9140d54-73b8-5f40-afa8-449fbaafd42b","formats":{"glb":"https://static.vc.ru/achievements/whale.glb","usdz":"https://static.vc.ru/achievements/whale.usdz"},"viewData":{"contentColor":"#8E6F09","textMaxWidth":0.66796875,"textX":0.533203125,"textY":0.658203125,"logoX":0.533203125,"logoY":0.77734375,"logoXNoText":0.4375,"logoYNoText":0.66015625},"id":398705,"userId":172558,"count":0,"shareImage":"https://api.vc.ru/achievements/share/398705"}],"lastModificationDate":1765017968,"isSubscribed":false,"isSubscribedToNewPosts":false,"isMuted":false,"isAvailableForMessenger":true,"badgeId":null,"isDonationsEnabled":false,"isPlusGiftEnabled":true,"isUnverifiedBlogForCompanyWithoutPro":true,"isRemovedByUserRequest":false,"isFrozen":false,"isDisabledAd":false,"isPlus":false,"isVerified":false,"isPro":false,"yandexMetricaId":null,"badge":null,"isOnline":false,"tgChannelShortname":null,"isUnsubscribable":true,"type":1,"subtype":"personal_blog"},"subsite":{"id":235819,"name":"Разработка","description":"Сообщество разработчиков: публикации о личном опыте, выдающиеся приёмы при решении рутинных задач, полезные материалы для профессионального роста.","uri":"/dev","avatar":{"type":"image","data":{"uuid":"fef5b5fb-e488-5b7f-8445-e3a26a910b44","width":1200,"height":1200,"size":7757,"type":"png","color":"343434","hash":"04042b2b1c1000","external_service":[]}},"cover":{"type":"image","data":{"uuid":"2a214cc5-35cc-58ca-bc07-fc1c892d2101","width":960,"height":280,"size":177,"type":"png","color":"343434","hash":"","external_service":[]}},"lastModificationDate":1642411346,"isSubscribed":false,"isSubscribedToNewPosts":false,"isMuted":false,"isAvailableForMessenger":false,"isDisabledAd":false,"nickname":"dev","isUnsubscribable":true,"badge":null,"badgeId":null,"isDonationsEnabled":false,"isOnline":false,"isPlus":false,"isUnverifiedBlogForCompanyWithoutPro":false,"isVerified":false,"isRemovedByUserRequest":false,"isFrozen":false,"isPro":false,"type":2,"subtype":"community"},"reactions":{"counters":[{"id":1,"count":20}],"reactionId":0},"isNews":false,"source":null,"clusters":[],"donations":{"amount":0,"isDonated":false},"commentsSeenCount":null}},{"type":"entry","data":{"id":293380,"customUri":null,"subsiteId":584869,"title":"Обзор новой версии языка Swift 5.5","date":1633014073,"dateModified":1633014073,"blocks":[{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Состоялся официальный релиз iOS 15, а значит разработчикам стала доступна новая версия Xcode под номером 13, а вместе с ним и новая версия языка Swift - 5.5.

"}},{"type":"media","cover":true,"hidden":false,"anchor":"","data":{"items":[{"title":"","image":{"type":"image","data":{"uuid":"e6286469-39ea-5a15-8d51-3d893024aabc","width":1198,"height":800,"size":17203,"type":"png","color":"f96a31","hash":"","external_service":[]}}}]}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

В этой версии языка, разработчики из Apple добавили очень много долгожданных изменений. Самые большие из них связаны с механизмом параллельного выполнения задач(concurrency).

"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Async/Await"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"func fetchIds(completion: @escaping ([Int]) -> Void) {\n DispatchQueue.global().async {\n let results = (1...10_000).map { $0 }\n completion(results)\n }\n}\n\nfunc performSomeWork(for ids: [Int], completion: @escaping (String) -> Void) {\n DispatchQueue.global().async {\n // Perform some heavy weight work here\n let workResult = \"Very important result\"\n completion(workResult)\n }\n}\n\nfunc upload(result: String, completion: @escaping (Bool) -> Void) {\n DispatchQueue.global().async {\n // Upload result to server\n completion(true)\n }\n}\n\n// Pyramid of Doom. The begining\nfetchIds { ids in\n performSomeWork(for: ids) { workResult in\n upload(result: workResult) { uploadResult in\n print(\"Done\")\n }\n }\n}","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Как видите вызов 3-х асинхронных методов уже превращается в не очень красивый и потенциально опасный код. С выходом Swift 5.5 и появлением async/await разработчикам станет намного проще писать код такого типа. Давайте переделаем наш код под новую парадигму.

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"func fetchIds() async -> [Int] {\n\t\treturn (1...10_000).map { $0 }\n}\n \nfunc performSomeWork(for ids: [Int]) async -> String {\n\t\treturn \"Very important result\"\n}\n \nfunc upload(result: String) async -> String {\n \"Done\"\n}\n\nlet ids = await fetchIds()\nlet workResult = await performSimeWork(for: ids)\nlet someSyncWork = foo()\nlet response = await upload(result)\nprint(response) // Done","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Как можно заметить код стал намного чище и понятнее, вызовы похожи на вызов синхронного кода, за исключением await.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Теперь немного разберемся как это все работает.

"}},{"type":"list","cover":false,"hidden":false,"anchor":"","data":{"items":["Когда мы доходим до первого await fetchIds() Swift приостановит текущий поток и будет ждать пока завершится вызов асинхронного кода, в это время он может нагрузить наш поток какой-либо другой работой.","Как только мы получаем результат из fetchIds() , мы переходим к следующему асинхронному вызову и повторяется шаг 1.","После получения результата из performSomeWork(for:) у нас идет обычный синхронный код foo(), который выполнится в текущем потоке и ничего приостанавливаться не будет.","Затем на вызове await upload() мы опять приостанавливаем наш поток, до тех пор пока не будет получен результат. После получения результата мы продолжим выполнение нашей прграммы."],"type":"OL"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"list","cover":false,"hidden":false,"anchor":"","data":{"items":["В других асинхронных функциях","В функции main() помеченной @main других структур, классов или enum-ов.","В новых структурах Task доступных в iOS 15."],"type":"UL"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Поэтому, можно сделать вывод что вроде как async/await фича языка, но скорее всего пользоваться ими получится в ограниченных местах, если таргет вашего приложения не iOS 15.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Так же async функции прекрасно работают с механизмом try/catch, достаточно пометить функцию как throws.

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"enum CustomError: Error { \n\tcase badFunction\n} \n\nfunc foo() async throws -> Int {\n\tif Bool.random() {\n\t\treturn Int.random()\n\t} else {\n\t\tthrow CustomError.badFunction\n\t}\n}\n\nfunc bar() async {\n\tdo {\n\t\tlet result = try await foo()\n\t\tprint(result)\n\t} catch {\n\t\tprint(\"Error!\")\n\t}\n}","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Как видите, все достаточно просто. Но стоит упомянуть о некоторых правилах/особенностях:

"}},{"type":"list","cover":false,"hidden":false,"anchor":"","data":{"items":["Синхронные функции можно вызвать в асинхронном контексте.","Если у вас есть две функции различающиеся только параметром async , то Swift будет вызывать ту, которая больше подходит по контексту. Если контекст асинхронный, то вызовется асинхронная, и наоборот. Удобно."],"type":"UL"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Вызов нескольких асинхронных функций одновременно"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"func upload(_ file: String) async -> String {\n await Task.sleep(UInt64.random(in: 0...5) * 1_000_000_000) // nanoseconds\n return \"\\(file) Uploaded\"\n}\n\nasync let firstTask = await upload(\"first\")\nasync let secondTask = await upload(\"second\")\nasync let thirdTask = await upload(\"third\")\n\nlet result = await [firstTask, secondTask, thirdTask]\nprint(result) // [\"second Uploaded\", \"first Uploaded\", \"third Uploaded\"]","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Asynchronous Sequences"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"struct FileReader: AsyncSequence {\n typealias Element = String\n \n struct AsyncIterator: AsyncIteratorProtocol {\n var current = \"\"\n\n mutating func next() async -> String? {\n defer {\n // read next batch\n }\n\n return current\n }\n }\n \n func makeAsyncIterator() -> AsyncIterator {\n AsyncIterator()\n }\n}\n\nfunc readFile() async {\n for await fileBatch in FileReader() {\n print(fileBatch)\n }\n}","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Единственное отличие от привычного нам for-in loop это ключевое слово await для асинхронных последовательностей нужно использовать for-await-in. Бонус пойнтом к этому всему идет доступность high order functions из коробки.

"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Асинхронный контекст"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Как я и сказал, для того чтобы использовать все прелести async/await нам нужен асинхронный контекст, где же нам его взять?

"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"@main"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Это специальный атрибут, которым можно пометить функцию main() ваших классов, структур и enum-ов, внутри этого метода вы сможете вызывать асинхронный код.

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"@main\nstruct UberHardWorkNeedToBeDone {\n static func main() async throws {\n let hardWorkResult = try await performSomeHardWork()\n print(\"Result is \\(hardWorkResult)\")\n }\n}","lang":""}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Task и TaskGroup"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Это новые типы которые, sad but true, доступны только с @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *), но тем не менее они очень важны, так как они очень напоминают старые добрые Operation, которые используются повсеместно.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Task представляет собой элементарный кусок работы, которая ваша программа может выполнить асинхронно. Мы уже встречались с ним когда использовали async let который под капотом создает Task и выполняет его. Стоит упомянуть, что код переданный в Task начнет выполнятся как только процесс создания объекта будет завершен. Для получения результата можно обратиться к свойству value , если же вы не ожидаете никакого результата, то можете просто создать объект и забыть про него, работа выполнится сама.

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"Task { () -> Void in\n\tprint(\"Нам не важен результат этой работы, мы просто хотим чтобы она была выполнена\")\n}\n\nlet task = Task { _ -> String in \n\treturn someImportantWork()\n}\nlet value = await task.value","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Closure который захватывает Task является non-escaping , потому что он начинает выполняться моментально, поэтому нет необходимости использовать self , если задача была создана внутри класса или структуры.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

При создании Task можно задать приоритет high, medium, low, background, userInitiated, utility.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

По аналогии с операциями, задачи можно прерывать вызовом метода cancel в таком случае, задача прекратиться и вернет ошибку CancellationError, или nil, или часть выполненной работы, в зависимости от того, как вы обработаете вызов.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

TaskGrup же нужна для объединения нескольких задач в группу и получения результата, когда все задачи будут выполнены. При добавлении задачи в группу, возвращаемый тип у всех задач должен совпадать. Чтобы обойти это ограничение можно использовать свой enum или воспользоваться конструкцией async let.

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"func upload(_ file: String) async -> String {\n await Task.sleep(UInt64.random(in: 0...5) * 1_000_000_000) // nanoseconds\n return \"\\(file) Uploaded\"\n}\n\nlet result = await withTaskGroup(of: String.self) { group -> String in\n\t\tgroup.addTask { await upload(\"first\" ) }\n group.addTask { await upload(\"second\" ) }\n group.addTask { await upload(\"third\" ) }\n\n var result: [String] = []\n for await value in group\n\t result.append(value)\n }\n\n return result.joined(separator: \" \")\n}\nprint(result) // \"second Uploaded first Uploaded third Uploaded\"","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Actors"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Думаю, многие из вас знают про такой термин, как гонка состояний(Race Conditions). Предположим, у нас есть вот такой вот NotificationCenter, в который можно добавлять/удалять каких-либо Observer. Проблемы могут возникнуть когда мы попытаемся добавить/удалить Observer'a из разных потоков.

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"class UnsafeNotificationsCenter {\n\n var observers: Array = []\n\n func add(observer: Observer) {\n guard !observers.contains(observer) else { return }\n observers.append(observer)\n }\n\n func remove(observer: Observer) {\n guard let observerIndex = observers.firstIndex(of: observer) else { return }\n observers.remove(at: observerIndex)\n }\n}","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Раньше мы решали эту проблему с помощью различных локов или очередей, которые синхронизируют доступ к массиву observers. Теперь у нас появились акторы. Они создаются с помощью специального слова actor.

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"actor SafeNotificationsCenter {\n\n var observers: Array = []\n\n func add(observer: Observer) {\n guard !observers.contains(observer) else { return }\n observers.append(observer)\n }\n\n func remove(observer: Observer) {\n guard let observerIndex = observers.firstIndex(of: observer) else { return }\n observers.remove(at: observerIndex)\n }\n}","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Теперь мы точно знаем, что доступ к переменной observers будет защищен, а каким образом это будет сделано, с помощью локов или очередей, нам не интересно, это детали реализации. Работать с таким типом можно только из асинхронного контекста, с помощью ключевых слов await или async let

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"let task = Task {\n let center = SafeNotificationsCenter()\n await center.add(observer: ...)\n await center.remove(observer: ...)\n}","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Так же появились Global Actors, как можно догадаться это тоже акторы, которые должны решить проблему с RaceConditions. Мы можем создавать своих и использовать их так же как и Property Wrappers. Сейчас в SDK есть несколько различных акторов, но самый полезный из них это MainActor, как несложно догадаться из названия, он нужен для того, чтобы быть уверенным, что код выполнится в главном потоке, очень удобно для работы с UI. Но не стоит забывать, что обращение к функциям/переменным осуществляется через await или async let.

"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Другие изменения"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"#if для postfix операторов"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Это изменение призвано решить распространенный паттерн использующийся в SwiftUI, когда в зависимости от различных условий окружения(операционной системы, флагов компиляции и тд), нам нужны различные модификаторы для наших View.

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"Text(\"Hello world!\")\n#if os(iOS)\n .font(.body)\n #if DEBUG\n .background(Color.red)\n #endif\n#else\n .font(.largeTitle)\n#endif","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Думаю применений этой фиче можно найти немало, не только в SwiftUI.

"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Codable для enum с associated type"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"enum Component: Codable {\n case label(text: String)\n case loader(progress: Double)\n case stepper(count: Int)\n}\n\nlet screen: [Component] = [\n .label(text: \"Header\"),\n .loader(progress: 0.5),\n .stepper(count: 20)\n]\n\ndo {\n let encodedScreen = try JSONEncoder().encode(screen)\n let jsonString = String(decoding: encodedScreen, as: UTF8.self)\n print(jsonString)\n // [{\"label\":{\"text\":\"Header\"}},{\"loader\":{\"progress\":0.5}},{\"stepper\":{\"count\":20}}]\n} catch {\n print(error.localizedDescription)\n}","lang":""}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"PropertyWrappers в функциях"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Заключение"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Как по мне, релиз получился отличным. Наконец-то обновили работу с параллелизмом, о которой разработчики просили уже давно. К сожалению, только с iOS 15, но все же, первый шаг сделан. Добавили очень много синтаксического сахара. Разработчики языка проделали отличную работу.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}}],"summaryContent":null,"isExistSummaryContent":false,"warningFromEditor":null,"warningFromEditorTitle":null,"counters":{"comments":0,"favorites":7,"reposts":0,"views":4,"hits":1000,"reads":null,"online":0},"dateFavorite":0,"hitsCount":1000,"isCommentsEnabled":true,"isLikesEnabled":true,"isRemovedByUserRequest":false,"isFavorited":false,"isPinned":false,"repostId":null,"repostData":null,"subscribedToTreads":false,"isEditorial":false,"isAudioAvailable":false,"audioUrl":null,"isAudioAvailableToGenerate":false,"commentEditor":{"enabled":true,"who":null,"text":"","until":null,"reason":null,"type":"everybody"},"isBlur":false,"isPublished":true,"isDisabledAd":false,"withheld":[],"ogTitle":null,"ogDescription":null,"url":"https://vc.ru/dev/293380-obzor-novoi-versii-yazyka-swift-55","author":{"id":584869,"name":"Булыга Игорь","nickname":null,"description":null,"uri":"","avatar":{"type":"image","data":{"uuid":"6db496a5-43dd-52c3-9de6-6b0ba02a5ba2","width":600,"height":600,"size":47731,"type":"jpg","color":"c9a490","hash":"307068787078e8ad","external_service":[]}},"cover":null,"achievements":[{"title":"5 лет на vc.ru","code":"registration_5_years","description":"Провёл 5 лет вместе с vc.ru. Получена 31 августа 2025.","previewUuid":"a9140d54-73b8-5f40-afa8-449fbaafd42b","formats":{"glb":"https://static.vc.ru/achievements/whale.glb","usdz":"https://static.vc.ru/achievements/whale.usdz"},"viewData":{"contentColor":"#8E6F09","textMaxWidth":0.66796875,"textX":0.533203125,"textY":0.658203125,"logoX":0.533203125,"logoY":0.77734375,"logoXNoText":0.4375,"logoYNoText":0.66015625},"id":5643052,"userId":584869,"count":0,"shareImage":"https://api.vc.ru/achievements/share/5643052"},{"title":"Год на vc.ru","code":"registration_1_year","description":"Первый год с vc.ru. Получена 24 июля 2025.","previewUuid":"0d11c244-49de-50e7-894e-b9b27945d42b","formats":{"glb":"https://static.vc.ru/achievements/fish.glb","usdz":"https://static.vc.ru/achievements/fish.usdz"},"viewData":{"contentColor":"#C67AA3","textMaxWidth":0.634765625,"textX":0.5888671875,"textY":0.54296875,"logoX":0.5859375,"logoY":0.6669921875,"logoXNoText":0.6044921875,"logoYNoText":0.5439453125},"id":4845024,"userId":584869,"count":0,"shareImage":"https://api.vc.ru/achievements/share/4845024"},{"title":"3 года на vc.ru","code":"registration_3_years","description":"Провёл 3 года вместе с vc.ru. Получена 23 июля 2025.","previewUuid":"d9d72ac5-bcb5-55e0-8c72-b99251e5cdd9","formats":{"glb":"https://static.vc.ru/achievements/shark.glb","usdz":"https://static.vc.ru/achievements/shark.usdz"},"viewData":{"contentColor":"#8E6F09","textMaxWidth":0.66796875,"textX":0.5205078125,"textY":0.341796875,"logoX":0.5205078125,"logoY":0.4609375,"logoXNoText":0.5,"logoYNoText":0.3662109375},"id":1220937,"userId":584869,"count":0,"shareImage":"https://api.vc.ru/achievements/share/1220937"}],"lastModificationDate":1765017968,"isSubscribed":false,"isSubscribedToNewPosts":false,"isMuted":false,"isAvailableForMessenger":true,"badgeId":null,"isDonationsEnabled":false,"isPlusGiftEnabled":true,"isUnverifiedBlogForCompanyWithoutPro":false,"isRemovedByUserRequest":false,"isFrozen":false,"isDisabledAd":false,"isPlus":false,"isVerified":false,"isPro":false,"yandexMetricaId":null,"badge":null,"isOnline":false,"tgChannelShortname":null,"isUnsubscribable":true,"type":1,"subtype":"personal_blog"},"subsite":{"id":235819,"name":"Разработка","description":"Сообщество разработчиков: публикации о личном опыте, выдающиеся приёмы при решении рутинных задач, полезные материалы для профессионального роста.","uri":"/dev","avatar":{"type":"image","data":{"uuid":"fef5b5fb-e488-5b7f-8445-e3a26a910b44","width":1200,"height":1200,"size":7757,"type":"png","color":"343434","hash":"04042b2b1c1000","external_service":[]}},"cover":{"type":"image","data":{"uuid":"2a214cc5-35cc-58ca-bc07-fc1c892d2101","width":960,"height":280,"size":177,"type":"png","color":"343434","hash":"","external_service":[]}},"lastModificationDate":1642411346,"isSubscribed":false,"isSubscribedToNewPosts":false,"isMuted":false,"isAvailableForMessenger":false,"isDisabledAd":false,"nickname":"dev","isUnsubscribable":true,"badge":null,"badgeId":null,"isDonationsEnabled":false,"isOnline":false,"isPlus":false,"isUnverifiedBlogForCompanyWithoutPro":false,"isVerified":false,"isRemovedByUserRequest":false,"isFrozen":false,"isPro":false,"type":2,"subtype":"community"},"reactions":{"counters":[{"id":1,"count":8}],"reactionId":0},"isNews":false,"source":null,"clusters":[],"donations":{"amount":0,"isDonated":false},"commentsSeenCount":null}},{"type":"entry","data":{"id":185945,"customUri":null,"subsiteId":447345,"title":"Реализация аутентификации пользователя в Django/Python","date":1607586273,"dateModified":1607586273,"blocks":[{"type":"text","cover":true,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Django – это фреймворк для работы с данными с использованием доступа через Web. Один из видов MVC — и называется MVT.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Для таких случаев возможна следующая реализация (использованы Centos 7, Python 2.7, Django 1.11):

"}},{"type":"list","cover":false,"hidden":false,"anchor":"","data":{"items":["Администратор добавляет в систему пользователя, внутри системы генерируется пароль (который не доступен администратору);","Имя и пароль пользователя отправляется на указанный для пользователя почтовый ящик;","Пользователь вводит логин и пароль, ему на ящик или на телефон приходит код доступа, который нужно ввести для дальнейшей аутентификации."],"type":"OL"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Для реализации выполним следующие несложные действия:

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Необходимо использовать свою модель пользователей, где будут добавлены поля (файл models.py):

"}},{"type":"list","cover":false,"hidden":false,"anchor":"","data":{"items":["phone(Номер телефона),","is_adm(пользователи поделены на админов и на обычных пользователей),","number (временное хранение кода доступа)"],"type":"UL"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"class Profile(models.Model):\n username = models.OneToOneField(User)\n phone = models.CharField(verbose_name='Номер телефона',max_length=20, unique=True, db_index=True)\n is_adm = models.BooleanField(verbose_name= 'админ' , default= False)\n number = models.CharField(verbose_name='код', max_length=10, blank=True)","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Изменим вид формы заполнения данных о пользователе, где заранее установим значение пароля по умолчанию равным, например, ‘Aa12345678’(файл forms.py):

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"class UserForm(UserCreationForm):\n password1 = forms.CharField(initial='Aa12345678',widget = forms.TextInput(\n attrs={'class':'form-control','type':'password', 'name':'password', 'readonly': True}), #\n label=\"Пароль\")\n password2 = None","lang":""}},{"type":"media","cover":false,"hidden":false,"anchor":"","data":{"items":[{"title":"","image":{"type":"image","data":{"uuid":"dfd61dd2-7cc3-54e5-927a-b55ec858a6e2","width":624,"height":255,"size":11486,"type":"png","color":"e5e9ee","hash":"","external_service":[]}}}]}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

При открытии формы видно, что поле «Пароль» уже заполнено и недоступно для редактирования (серый цвет). Заносим остальные данные и сохраняем.

"}},{"type":"media","cover":false,"hidden":false,"anchor":"","data":{"items":[{"title":"","image":{"type":"image","data":{"uuid":"d5c3b864-67df-5ada-8203-c8cc445d0aa9","width":624,"height":247,"size":13950,"type":"png","color":"e6edf5","hash":"","external_service":[]}}}]}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

После нажатия кнопки «Сохранить», пользователь будет создан и ему на почту придет сообщение:

"}},{"type":"media","cover":false,"hidden":false,"anchor":"","data":{"items":[{"title":"","image":{"type":"image","data":{"uuid":"cc0fd905-c9f0-55f3-84b9-c352a4e9b61c","width":624,"height":216,"size":15347,"type":"png","color":"d0d1d3","hash":"","external_service":[]}}}]}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

И мы видим, что пароль установлен отличный от пароля по умолчанию.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Это достигается при сохранении данных о пользователе (файл views.py — для нового пароля использован генератор случайных чисел):

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"def form_valid(self, form):\n *********\n passw = 'Zz' + str(randint(100000,999999))\n user.set_password(passw)\n user.save()\n send_mail(\"Сообщение\",\"Имя пользователя:{}, пароль: {}\".format(user.username, passw), settings.EMAIL_HOST_USER, [user.email],fail_silently=False)\n *********","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Пользователь, получив сообщение, может уже входить в систему:

"}},{"type":"media","cover":false,"hidden":false,"anchor":"","data":{"items":[{"title":"","image":{"type":"image","data":{"uuid":"311b1a7d-933a-5afc-91ba-f324c2253b83","width":624,"height":139,"size":5648,"type":"png","color":"f3f3f4","hash":"","external_service":[]}}}]}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"media","cover":false,"hidden":false,"anchor":"","data":{"items":[{"title":"","image":{"type":"image","data":{"uuid":"b19aa409-96f1-5c0c-9f4a-1940c8061f42","width":624,"height":155,"size":9806,"type":"png","color":"f3f3f3","hash":"","external_service":[]}}}]}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Который направлен на почтовый ящик:

"}},{"type":"media","cover":false,"hidden":false,"anchor":"","data":{"items":[{"title":"","image":{"type":"image","data":{"uuid":"4ee29a11-f726-5acf-a0d6-e4346e7712f4","width":624,"height":147,"size":4305,"type":"png","color":"f2f2f2","hash":"","external_service":[]}}}]}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Или на телефон (здесь имитация реализована через сервис портала smsc.ru, с подключением файла smsc_api.py):

"}},{"type":"media","cover":false,"hidden":false,"anchor":"","data":{"items":[{"title":"","image":{"type":"image","data":{"uuid":"5e022261-daf7-5ae3-a008-2e4b2dba8a4c","width":624,"height":245,"size":70249,"type":"png","color":"dce5ec","hash":"","external_service":[]}}}]}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Пользователь вводит код:

"}},{"type":"media","cover":false,"hidden":false,"anchor":"","data":{"items":[{"title":"","image":{"type":"image","data":{"uuid":"e6feeaaa-a68a-5597-a6db-1adbd6beec48","width":624,"height":120,"size":5983,"type":"png","color":"f3f3f3","hash":"","external_service":[]}}}]}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Код реализации для отправки кода авторизации в файле views.py:,

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"@login_required(login_url='/core/next')\ndef next_sms(request):\n if request.user.is_superuser:\n title = 'Главная'\n content = {'title': title}\n return render(request, 'core/index.html', content)\n else:\n title = 'Код СМС'\n content = {'title': title}\n user = User.objects.get(username=request.user)\n profile = Profile.objects.get(username = user.id)\n #if profile.number != '': \n gen_number = randint(10000,99999)\n profile.number = str(gen_number)\n profile.save()\n # Отправка кода на почтовый ящик\n send_mail(\"subject\",\"Your code: {}\".format(gen_number), settings.EMAIL_HOST_USER, [user.email],fail_silently=False)\n # Отправка кода на телефон\n smsc = SMSC()\n smsc.send_sms(profile.phone, \"Your code: {}\".format(gen_number), sender = \"sms\")\n return render(request, 'core/next.html', content)","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Для отправки почтовых сообщений вносим необходимые изменения в файл settings.py:

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"EMAIL_HOST = 'smtp'\nEMAIL_PORT = 465\nEMAIL_HOST_USER = \"пользователь@\"\nEMAIL_HOST_PASSWORD = \"дополнительный_пароль\"\nEMAIL_USE_SSL = True\nSERVER_EMAIL = EMAIL_HOST_USER\nDEFAULT_FROM_EMAIL = EMAIL_HOST_USER","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}}],"summaryContent":null,"isExistSummaryContent":false,"warningFromEditor":null,"warningFromEditorTitle":null,"counters":{"comments":0,"favorites":17,"reposts":1,"views":7,"hits":5305,"reads":null,"online":0},"dateFavorite":0,"hitsCount":5305,"isCommentsEnabled":true,"isLikesEnabled":true,"isRemovedByUserRequest":false,"isFavorited":false,"isPinned":false,"repostId":null,"repostData":null,"subscribedToTreads":false,"isEditorial":false,"isAudioAvailable":false,"audioUrl":null,"isAudioAvailableToGenerate":false,"commentEditor":{"enabled":true,"who":null,"text":"","until":null,"reason":null,"type":"everybody"},"isBlur":false,"isPublished":true,"isDisabledAd":false,"withheld":[],"ogTitle":null,"ogDescription":null,"url":"https://vc.ru/dev/185945-realizaciya-autentifikacii-polzovatelya-v-django-python","author":{"id":447345,"name":"NTA","nickname":null,"description":null,"uri":"","avatar":{"type":"image","data":{"uuid":"e7175678-aaab-09f8-0271-7b3af7ac4670","width":1207,"height":1207,"size":83090,"type":"jpg","color":"e3ebf8","hash":"","external_service":[]}},"cover":{"cover":{"type":"image","data":{"uuid":"8575b55d-a1f0-53ee-9ad6-78268f9ea45c","width":1280,"height":373,"size":56005,"type":"jpg","color":"b0b9ca","hash":"","external_service":[]}},"cover_y":0},"achievements":[{"title":"Год на vc.ru","code":"registration_1_year","description":"Первый год с vc.ru. Получена 24 июля 2025.","previewUuid":"0d11c244-49de-50e7-894e-b9b27945d42b","formats":{"glb":"https://static.vc.ru/achievements/fish.glb","usdz":"https://static.vc.ru/achievements/fish.usdz"},"viewData":{"contentColor":"#C67AA3","textMaxWidth":0.634765625,"textX":0.5888671875,"textY":0.54296875,"logoX":0.5859375,"logoY":0.6669921875,"logoXNoText":0.6044921875,"logoYNoText":0.5439453125},"id":4980908,"userId":447345,"count":0,"shareImage":"https://api.vc.ru/achievements/share/4980908"},{"title":"3 года на vc.ru","code":"registration_3_years","description":"Провёл 3 года вместе с vc.ru. Получена 23 июля 2025.","previewUuid":"d9d72ac5-bcb5-55e0-8c72-b99251e5cdd9","formats":{"glb":"https://static.vc.ru/achievements/shark.glb","usdz":"https://static.vc.ru/achievements/shark.usdz"},"viewData":{"contentColor":"#8E6F09","textMaxWidth":0.66796875,"textX":0.5205078125,"textY":0.341796875,"logoX":0.5205078125,"logoY":0.4609375,"logoXNoText":0.5,"logoYNoText":0.3662109375},"id":1356821,"userId":447345,"count":0,"shareImage":"https://api.vc.ru/achievements/share/1356821"},{"title":"5 лет на vc.ru","code":"registration_5_years","description":"Провёл 5 лет вместе с vc.ru. Получена 23 июля 2025.","previewUuid":"a9140d54-73b8-5f40-afa8-449fbaafd42b","formats":{"glb":"https://static.vc.ru/achievements/whale.glb","usdz":"https://static.vc.ru/achievements/whale.usdz"},"viewData":{"contentColor":"#8E6F09","textMaxWidth":0.66796875,"textX":0.533203125,"textY":0.658203125,"logoX":0.533203125,"logoY":0.77734375,"logoXNoText":0.4375,"logoYNoText":0.66015625},"id":130113,"userId":447345,"count":0,"shareImage":"https://api.vc.ru/achievements/share/130113"}],"lastModificationDate":1765017968,"isSubscribed":false,"isSubscribedToNewPosts":false,"isMuted":false,"isAvailableForMessenger":true,"badgeId":null,"isDonationsEnabled":false,"isPlusGiftEnabled":true,"isUnverifiedBlogForCompanyWithoutPro":true,"isRemovedByUserRequest":false,"isFrozen":false,"isDisabledAd":false,"isPlus":false,"isVerified":false,"isPro":false,"yandexMetricaId":null,"badge":null,"isOnline":false,"tgChannelShortname":null,"isUnsubscribable":true,"type":1,"subtype":"personal_blog"},"subsite":{"id":235819,"name":"Разработка","description":"Сообщество разработчиков: публикации о личном опыте, выдающиеся приёмы при решении рутинных задач, полезные материалы для профессионального роста.","uri":"/dev","avatar":{"type":"image","data":{"uuid":"fef5b5fb-e488-5b7f-8445-e3a26a910b44","width":1200,"height":1200,"size":7757,"type":"png","color":"343434","hash":"04042b2b1c1000","external_service":[]}},"cover":{"type":"image","data":{"uuid":"2a214cc5-35cc-58ca-bc07-fc1c892d2101","width":960,"height":280,"size":177,"type":"png","color":"343434","hash":"","external_service":[]}},"lastModificationDate":1642411346,"isSubscribed":false,"isSubscribedToNewPosts":false,"isMuted":false,"isAvailableForMessenger":false,"isDisabledAd":false,"nickname":"dev","isUnsubscribable":true,"badge":null,"badgeId":null,"isDonationsEnabled":false,"isOnline":false,"isPlus":false,"isUnverifiedBlogForCompanyWithoutPro":false,"isVerified":false,"isRemovedByUserRequest":false,"isFrozen":false,"isPro":false,"type":2,"subtype":"community"},"reactions":{"counters":[{"id":1,"count":5}],"reactionId":0},"isNews":false,"source":null,"clusters":[],"donations":{"amount":0,"isDonated":false},"commentsSeenCount":null}},{"type":"entry","data":{"id":174068,"customUri":null,"subsiteId":447345,"title":"Как ускорить набор кода и сделать его без ошибок?","date":1604605240,"dateModified":1604605240,"blocks":[{"type":"text","cover":true,"hidden":false,"anchor":"","data":{"text":"

Как использовать сниппеты в Visual Studio? Как создавать свои? Как посмотреть, какие уже созданы? Попробуем ответить на все эти вопросы.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"media","cover":false,"hidden":false,"anchor":"","data":{"items":[{"title":"","image":{"type":"image","data":{"uuid":"92014668-ba58-5e32-a61a-009df96d36b5","width":618,"height":97,"size":30534,"type":"png","color":"0e74c7","hash":"","external_service":[]}}}]}},{"type":"media","cover":true,"hidden":false,"anchor":"","data":{"items":[{"title":"","image":{"type":"image","data":{"uuid":"bf647548-4f65-581a-a581-52002c116fe5","width":575,"height":179,"size":57401,"type":"png","color":"f1f1f2","hash":"","external_service":[]}}}]}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

При нажатии два раза на «Tab» за нас машина заполнила полностью всю команду.

"}},{"type":"media","cover":false,"hidden":false,"anchor":"","data":{"items":[{"title":"","image":{"type":"image","data":{"uuid":"78d7d2bd-e872-5851-a75a-ae2da67d57ee","width":193,"height":94,"size":8740,"type":"png","color":"c5cfcf","hash":"","external_service":[]}}}]}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Сниппеты могут:

"}},{"type":"list","cover":false,"hidden":false,"anchor":"","data":{"items":["вставлять «кусок» команды;","окружить код, вокруг которого он будет использован."],"type":"UL"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"media","cover":false,"hidden":false,"anchor":"","data":{"items":[{"title":"","image":{"type":"image","data":{"uuid":"949776cc-9cab-5da8-9a90-f73aaecdd1fa","width":616,"height":357,"size":39923,"type":"png","color":"eef0f2","hash":"","external_service":[]}}}]}},{"type":"media","cover":false,"hidden":false,"anchor":"","data":{"items":[{"title":"","image":{"type":"image","data":{"uuid":"7baadadd-edc6-54e1-88f2-04e1a0e79d20","width":606,"height":110,"size":33851,"type":"png","color":"edeeef","hash":"","external_service":[]}}}]}},{"type":"media","cover":false,"hidden":false,"anchor":"","data":{"items":[{"title":"","image":{"type":"image","data":{"uuid":"3ed5d1b6-fd8d-5712-8c0c-7686c7a93432","width":606,"height":266,"size":52056,"type":"png","color":"edeeef","hash":"","external_service":[]}}}]}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"if (true)\n{\nConsole.WriteLine(\" \");\n}","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Мы «завернули» команду Console.WriteLine в If.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Так же сниппеты позволяют использовать параметры, как в данном случае с False:

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"if (false)\n{\nConsole.WriteLine(\" \");\n}","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Еще, при помощи сниппетов мы можем вставлять комментарии в начале метода.

"}},{"type":"media","cover":false,"hidden":false,"anchor":"","data":{"items":[{"title":"","image":{"type":"image","data":{"uuid":"e604edbc-7160-5a27-b65c-88d95796293b","width":509,"height":420,"size":38688,"type":"png","color":"edeef1","hash":"","external_service":[]}}}]}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"using System; \nnamespace SnippetExample \n{ \n class Program \n { \n /// \n /// Пример комментария \n /// \n /// входные аргументы \n static void Main(string[] args) \n { \n Console.WriteLine(\"Hello Word!\"); \n if (false) \n { \n Console.WriteLine(\"\"); \n } \n for (int a = 0; a < length; a++) \n { \n \n } \n } \n } \n}","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Теперь метод помечен надписью, которую мы внесли в комментарии, как и в аргумент.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Где они находятся?

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

В Tools->Code Snippets Manage… ->

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"media","cover":false,"hidden":false,"anchor":"","data":{"items":[{"title":"","image":{"type":"image","data":{"uuid":"76d72878-8bd2-51c8-b286-94e2d6af14e9","width":326,"height":288,"size":32544,"type":"png","color":"eeeeef","hash":"","external_service":[]}}}]}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

В папке Visual C# находятся сниппеты, которые есть по умолчанию.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Можно выбрать сниппет, посмотреть его описание, клавиатурное сочетание. Чтобы посмотреть код шаблона, нужно найти папку где они находятся. Копируем адрес из окна «Location» и вставляем в проводник, перетаскиваем сниппет (например, «If») в окно программы. Перед нами код шаблона.

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"\n\n \n
\n #if\n #if\n Фрагмент кода для #if\n Microsoft Corporation\n \n Expansion\n SurroundsWith\n \n
\n \n \n \n expression\n Выражение препроцессора для вычисления\n true\n \n \n \n \n\n
\n
","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Можно ли создавать свои сниппеты?

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Да, можно, это позволяет достаточно сильно экономить время. Но это не просто команда CNTRL+C и CNTRL+V, сниппеты позволяют использовать параметры, и кое-что еще.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Способы создания:

"}},{"type":"list","cover":false,"hidden":false,"anchor":"","data":{"items":["копировать код уже готового шаблона сниппета и доработать его;","создать с нуля."],"type":"UL"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Мы рассмотрим второй способ.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Создаем новый файл (Add->New Ite… ->XML File) и даем ему имя (MySnippet.xml)

"}},{"type":"media","cover":false,"hidden":false,"anchor":"","data":{"items":[{"title":"","image":{"type":"image","data":{"uuid":"2a7ef6ef-467a-5015-b0e6-dc8a18be3137","width":340,"height":238,"size":65085,"type":"png","color":"eaebef","hash":"","external_service":[]}}}]}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"media","cover":false,"hidden":false,"anchor":"","data":{"items":[{"title":"","image":{"type":"image","data":{"uuid":"57a32908-3172-55ff-8f04-8738c7690022","width":408,"height":225,"size":65926,"type":"png","color":"e8eaef","hash":"","external_service":[]}}}]}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Выбираем snippet

"}},{"type":"media","cover":false,"hidden":false,"anchor":"","data":{"items":[{"title":"","image":{"type":"image","data":{"uuid":"ac57d207-8500-51bb-87c0-4ca63f5a8243","width":365,"height":140,"size":45773,"type":"png","color":"eaeaee","hash":"","external_service":[]}}}]}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

В итоге мы получили пустой шаблон (желтым отмечены строки, которые мы можем менять)

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"\n\n
\n заголовок\n автор\n ярлык\n описание\n \n SurroundsWith\n Expansion\n \n
\n \n \n \n имя\n значение\n \n \n \n \n $имя$\n $selected$ $end$]]>\n \n \n
","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

После того, как мы все настроим, нужно переименовать файл с расширением *.snippet

"}},{"type":"media","cover":false,"hidden":false,"anchor":"","data":{"items":[{"title":"","image":{"type":"image","data":{"uuid":"81919e07-3d75-5f5a-9bb0-2ff78112d451","width":285,"height":146,"size":48042,"type":"png","color":"ececee","hash":"","external_service":[]}}}]}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Теперь добавляем сниппет: Tools->Code Snippets Manage… ->Import…

"}},{"type":"media","cover":false,"hidden":false,"anchor":"","data":{"items":[{"title":"","image":{"type":"image","data":{"uuid":"b441c4c6-39ca-5c7f-a6b0-51a7b52c2011","width":380,"height":285,"size":40789,"type":"png","color":"e9ebec","hash":"","external_service":[]}}}]}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Открываем папку, где мы работаем и выбираем сниппет и папку, в которую его поместим.

"}},{"type":"media","cover":false,"hidden":false,"anchor":"","data":{"items":[{"title":"","image":{"type":"image","data":{"uuid":"a1f14af7-b657-5439-b2f4-fe4ab68cb2ca","width":327,"height":318,"size":28122,"type":"png","color":"ebeced","hash":"","external_service":[]}}}]}}],"summaryContent":null,"isExistSummaryContent":false,"warningFromEditor":null,"warningFromEditorTitle":null,"counters":{"comments":2,"favorites":8,"reposts":0,"views":7,"hits":1457,"reads":null,"online":0},"dateFavorite":0,"hitsCount":1457,"isCommentsEnabled":true,"isLikesEnabled":true,"isRemovedByUserRequest":false,"isFavorited":false,"isPinned":false,"repostId":null,"repostData":null,"subscribedToTreads":false,"isEditorial":false,"isAudioAvailable":false,"audioUrl":null,"isAudioAvailableToGenerate":false,"commentEditor":{"enabled":true,"who":null,"text":"","until":null,"reason":null,"type":"everybody"},"isBlur":false,"isPublished":true,"isDisabledAd":false,"withheld":[],"ogTitle":null,"ogDescription":null,"url":"https://vc.ru/dev/174068-kak-uskorit-nabor-koda-i-sdelat-ego-bez-oshibok","author":{"id":447345,"name":"NTA","nickname":null,"description":null,"uri":"","avatar":{"type":"image","data":{"uuid":"e7175678-aaab-09f8-0271-7b3af7ac4670","width":1207,"height":1207,"size":83090,"type":"jpg","color":"e3ebf8","hash":"","external_service":[]}},"cover":{"cover":{"type":"image","data":{"uuid":"8575b55d-a1f0-53ee-9ad6-78268f9ea45c","width":1280,"height":373,"size":56005,"type":"jpg","color":"b0b9ca","hash":"","external_service":[]}},"cover_y":0},"achievements":[{"title":"Год на vc.ru","code":"registration_1_year","description":"Первый год с vc.ru. Получена 24 июля 2025.","previewUuid":"0d11c244-49de-50e7-894e-b9b27945d42b","formats":{"glb":"https://static.vc.ru/achievements/fish.glb","usdz":"https://static.vc.ru/achievements/fish.usdz"},"viewData":{"contentColor":"#C67AA3","textMaxWidth":0.634765625,"textX":0.5888671875,"textY":0.54296875,"logoX":0.5859375,"logoY":0.6669921875,"logoXNoText":0.6044921875,"logoYNoText":0.5439453125},"id":4980908,"userId":447345,"count":0,"shareImage":"https://api.vc.ru/achievements/share/4980908"},{"title":"3 года на vc.ru","code":"registration_3_years","description":"Провёл 3 года вместе с vc.ru. Получена 23 июля 2025.","previewUuid":"d9d72ac5-bcb5-55e0-8c72-b99251e5cdd9","formats":{"glb":"https://static.vc.ru/achievements/shark.glb","usdz":"https://static.vc.ru/achievements/shark.usdz"},"viewData":{"contentColor":"#8E6F09","textMaxWidth":0.66796875,"textX":0.5205078125,"textY":0.341796875,"logoX":0.5205078125,"logoY":0.4609375,"logoXNoText":0.5,"logoYNoText":0.3662109375},"id":1356821,"userId":447345,"count":0,"shareImage":"https://api.vc.ru/achievements/share/1356821"},{"title":"5 лет на vc.ru","code":"registration_5_years","description":"Провёл 5 лет вместе с vc.ru. Получена 23 июля 2025.","previewUuid":"a9140d54-73b8-5f40-afa8-449fbaafd42b","formats":{"glb":"https://static.vc.ru/achievements/whale.glb","usdz":"https://static.vc.ru/achievements/whale.usdz"},"viewData":{"contentColor":"#8E6F09","textMaxWidth":0.66796875,"textX":0.533203125,"textY":0.658203125,"logoX":0.533203125,"logoY":0.77734375,"logoXNoText":0.4375,"logoYNoText":0.66015625},"id":130113,"userId":447345,"count":0,"shareImage":"https://api.vc.ru/achievements/share/130113"}],"lastModificationDate":1765017968,"isSubscribed":false,"isSubscribedToNewPosts":false,"isMuted":false,"isAvailableForMessenger":true,"badgeId":null,"isDonationsEnabled":false,"isPlusGiftEnabled":true,"isUnverifiedBlogForCompanyWithoutPro":true,"isRemovedByUserRequest":false,"isFrozen":false,"isDisabledAd":false,"isPlus":false,"isVerified":false,"isPro":false,"yandexMetricaId":null,"badge":null,"isOnline":false,"tgChannelShortname":null,"isUnsubscribable":true,"type":1,"subtype":"personal_blog"},"subsite":{"id":235819,"name":"Разработка","description":"Сообщество разработчиков: публикации о личном опыте, выдающиеся приёмы при решении рутинных задач, полезные материалы для профессионального роста.","uri":"/dev","avatar":{"type":"image","data":{"uuid":"fef5b5fb-e488-5b7f-8445-e3a26a910b44","width":1200,"height":1200,"size":7757,"type":"png","color":"343434","hash":"04042b2b1c1000","external_service":[]}},"cover":{"type":"image","data":{"uuid":"2a214cc5-35cc-58ca-bc07-fc1c892d2101","width":960,"height":280,"size":177,"type":"png","color":"343434","hash":"","external_service":[]}},"lastModificationDate":1642411346,"isSubscribed":false,"isSubscribedToNewPosts":false,"isMuted":false,"isAvailableForMessenger":false,"isDisabledAd":false,"nickname":"dev","isUnsubscribable":true,"badge":null,"badgeId":null,"isDonationsEnabled":false,"isOnline":false,"isPlus":false,"isUnverifiedBlogForCompanyWithoutPro":false,"isVerified":false,"isRemovedByUserRequest":false,"isFrozen":false,"isPro":false,"type":2,"subtype":"community"},"reactions":{"counters":[{"id":1,"count":4}],"reactionId":0},"isNews":false,"source":null,"clusters":[],"donations":{"amount":0,"isDonated":false},"commentsSeenCount":null}}],"ogTitle":null,"ogDescription":null,"isAnonymized":true}};