<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="ru">
  
  <title>Блог Артёма Болотова</title>
  <subtitle>Блог инженера-программиста о разработке приложений, web, технологиях и жизни</subtitle>
  <link href="https://bolotov.dev/feed.xml" rel="self" type="application/atom+xml"/>
  <link href="https://bolotov.dev/blog/" rel="alternate" type="text/html"/>
  <id>https://bolotov.dev/blog/</id>
  <updated>2026-10-20T00:00:00+00:00</updated>
  <author>
    <name>Артём Болотов</name>
    <uri>https://bolotov.dev/</uri>
  </author>
  <icon>https://bolotov.dev/icons/favicon-32x32.png</icon>
  <logo>https://bolotov.dev/blog/img/avatar.jpg</logo>
  
  <entry>
    <title type="html">Россификация RedCalendar</title>
    <link href="https://bolotov.dev/blog/2026/10/20/redcalendar-russification" rel="alternate" type="text/html"/>
    <id>https://bolotov.dev/blog/2026/10/20/redcalendar-russification</id>
    <published>2026-10-20T00:00:00+00:00</published>
    <updated>2026-10-20T00:00:00+00:00</updated>
    
    


    <category term="iosdev" label="iosdev"/>
    
    <summary type="html">Как я перевёз RedCalendar с Firebase на свой сервер в России, сделал вход по почте и переписал приложение с нуля на SwiftUI</summary>
    
    <content type="html" xml:base="https://bolotov.dev/blog/2026/10/20/redcalendar-russification">&lt;p&gt;Думал, что просто меняю сервер. Оказалось — пишу новое приложение, спецификацию на 2 500 строк и согласие на обработку персональных данных.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://apps.apple.com/app/redcalendar-cycle-tracker/id1535523842&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;RedCalendar&lt;/a&gt; — облачный календарь женского цикла для iPhone. На днях вышло большое обновление: всё тот же календарь, но внутри от старого приложения не осталось ничего. Данные теперь хранятся в России, входить нужно по почте, а само приложение написано заново.&lt;/p&gt;

&lt;div class=&quot;image-gallery&quot; data-gallery-id=&quot;gallery-auto&quot;&gt;
  
  
  
  
  &lt;!-- Loading State --&gt;
  &lt;div class=&quot;gallery-loading&quot; data-images-count=&quot;6&quot;&gt;
    &lt;div class=&quot;gallery-loading-content&quot;&gt;
      &lt;div class=&quot;gallery-loading-spinner&quot;&gt;&lt;/div&gt;
      &lt;span class=&quot;gallery-loading-text&quot;&gt;Загрузка изображений...&lt;/span&gt;
      &lt;div class=&quot;gallery-loading-progress&quot;&gt;
        &lt;div class=&quot;gallery-loading-bar&quot;&gt;&lt;/div&gt;
      &lt;/div&gt;
      &lt;div class=&quot;gallery-loading-counter&quot;&gt;
        &lt;span class=&quot;gallery-loaded-count&quot;&gt;0&lt;/span&gt;/&lt;span class=&quot;gallery-total-count&quot;&gt;6&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Gallery Content (hidden by default) --&gt;
  &lt;div class=&quot;gallery-content&quot; style=&quot;opacity: 0; visibility: hidden;&quot;&gt;
    &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;6&quot;&gt;
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/appstore_1.webp&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/appstore_1@480w.webp 480w, https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/appstore_1@720w.webp 720w, https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/appstore_1.webp 920w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;Карточка App Store «Облачный календарь цикла»: календарь и карточка сегодняшнего дня&quot; width=&quot;920&quot; height=&quot;2000&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 920 / 2000;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/appstore_2.webp&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/appstore_2@480w.webp 480w, https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/appstore_2@720w.webp 720w, https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/appstore_2.webp 920w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;Карточка App Store «Каждый день как на ладони»: календарь с отмеченными месячными&quot; width=&quot;920&quot; height=&quot;2000&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 920 / 2000;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/appstore_4.webp&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/appstore_4@480w.webp 480w, https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/appstore_4@720w.webp 720w, https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/appstore_4.webp 920w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;Карточка App Store «Создавайте свои теги»: цветные теги по группам&quot; width=&quot;920&quot; height=&quot;2000&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 920 / 2000;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/appstore_3.webp&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/appstore_3@480w.webp 480w, https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/appstore_3@720w.webp 720w, https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/appstore_3.webp 920w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;Карточка App Store «Заметки к каждому дню»: экран комментария с клавиатурой&quot; width=&quot;920&quot; height=&quot;2000&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 920 / 2000;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/appstore_5.webp&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/appstore_5@480w.webp 480w, https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/appstore_5@720w.webp 720w, https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/appstore_5.webp 920w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;Карточка App Store «Цикл в цифрах и графиках»: средняя длина цикла, график и история&quot; width=&quot;920&quot; height=&quot;2000&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 920 / 2000;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/appstore_6.webp&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/appstore_6@480w.webp 480w, https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/appstore_6@720w.webp 720w, https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/appstore_6.webp 920w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;Карточка App Store «Настройте под себя»: экран настроек&quot; width=&quot;920&quot; height=&quot;2000&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 920 / 2000;&quot; /&gt;
        
      
    &lt;/div&gt;
    
    &lt;!-- Navigation controls - always present for galleries --&gt;
    &lt;div class=&quot;gallery-controls&quot;&gt;
      &lt;button class=&quot;gallery-nav gallery-prev&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;prev&quot; aria-label=&quot;Предыдущее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;15 18 9 12 15 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
      &lt;button class=&quot;gallery-nav gallery-next&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;next&quot; aria-label=&quot;Следующее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;9 18 15 12 9 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Fallback for no-JS --&gt;
  &lt;noscript&gt;
    &lt;div class=&quot;gallery-fallback&quot;&gt;
      &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;6&quot;&gt;
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/appstore_1.webp&quot; alt=&quot;Карточка App Store «Облачный календарь цикла»: календарь и карточка сегодняшнего дня&quot; width=&quot;920&quot; height=&quot;2000&quot; style=&quot;aspect-ratio: 920 / 2000;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/appstore_2.webp&quot; alt=&quot;Карточка App Store «Каждый день как на ладони»: календарь с отмеченными месячными&quot; width=&quot;920&quot; height=&quot;2000&quot; style=&quot;aspect-ratio: 920 / 2000;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/appstore_4.webp&quot; alt=&quot;Карточка App Store «Создавайте свои теги»: цветные теги по группам&quot; width=&quot;920&quot; height=&quot;2000&quot; style=&quot;aspect-ratio: 920 / 2000;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/appstore_3.webp&quot; alt=&quot;Карточка App Store «Заметки к каждому дню»: экран комментария с клавиатурой&quot; width=&quot;920&quot; height=&quot;2000&quot; style=&quot;aspect-ratio: 920 / 2000;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/appstore_5.webp&quot; alt=&quot;Карточка App Store «Цикл в цифрах и графиках»: средняя длина цикла, график и история&quot; width=&quot;920&quot; height=&quot;2000&quot; style=&quot;aspect-ratio: 920 / 2000;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/appstore_6.webp&quot; alt=&quot;Карточка App Store «Настройте под себя»: экран настроек&quot; width=&quot;920&quot; height=&quot;2000&quot; style=&quot;aspect-ratio: 920 / 2000;&quot; /&gt;
          
        
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/noscript&gt;
&lt;/div&gt;

&lt;h2 id=&quot;почему-пришлось-уезжать&quot;&gt;Почему пришлось уезжать&lt;/h2&gt;

&lt;p&gt;Раньше всё держалось на &lt;a href=&quot;https://firebase.google.com&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Firebase&lt;/a&gt; — это сервис Google для разработчиков приложений, где база данных, вход и уведомления идут готовыми кирпичиками. Удобно, быстро и почти бесплатно. Только серверы Firebase стоят в США.&lt;/p&gt;

&lt;p&gt;Главная причина переезда — закон о персональных данных (152-ФЗ). Данные россиян нужно хранить на серверах в России, а сведения о здоровье, к которым относится и менструальный цикл, — это отдельная, особо охраняемая категория. Хранить их у Google в американском дата-центре для российского приложения становилось всё менее разумно.&lt;/p&gt;

&lt;h2 id=&quot;свой-сервер-в-россии&quot;&gt;Свой сервер в России&lt;/h2&gt;

&lt;p&gt;Сервер RedCalendar живёт у российского облачного хостинга &lt;a href=&quot;https://www.netangels.ru&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;NetAngels&lt;/a&gt; — я давно пользуюсь им в других проектах. Он хранит записи, рассылает уведомления и отправляет письма с кодом входа. Сервер я намеренно сделал скучным: только проверенные временем технологии и минимум сторонних библиотек.&lt;/p&gt;

&lt;p&gt;Что чем заменилось:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Что&lt;/th&gt;
      &lt;th&gt;Было&lt;/th&gt;
      &lt;th&gt;Стало&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Сервер и база данных&lt;/td&gt;
      &lt;td&gt;Firebase, США&lt;/td&gt;
      &lt;td&gt;Свой сервер в NetAngels, Россия&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Вход по номеру&lt;/td&gt;
      &lt;td&gt;Через Firebase&lt;/td&gt;
      &lt;td&gt;Звонок: код — последние 4 цифры номера, который звонит&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Вход по почте&lt;/td&gt;
      &lt;td&gt;Не было&lt;/td&gt;
      &lt;td&gt;Код в письме через Yandex Cloud Postbox&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Уведомления&lt;/td&gt;
      &lt;td&gt;Сервис Google&lt;/td&gt;
      &lt;td&gt;Свой сервер напрямую через Apple&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Аналитика&lt;/td&gt;
      &lt;td&gt;Google&lt;/td&gt;
      &lt;td&gt;AppMetrica&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Письма с кодом отправляет &lt;a href=&quot;https://yandex.cloud/ru/services/postbox&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Yandex Cloud Postbox&lt;/a&gt;. Летом я писал про &lt;a href=&quot;https://bolotov.dev/blog/2026/07/11/foreign-email-law&quot;&gt;закон об иностранной почте&lt;/a&gt; и почему считаю, что своя система входа по почте лучше кнопок «Войти через…». Вот это она и есть.&lt;/p&gt;

&lt;p&gt;Postbox я выбрал как самый надёжный сервис с подходящим тарифом. Чтобы письма не попадали в спам, ещё настроил для домена строгие правила DMARC — по ним почтовые сервисы проверяют, что письмо действительно отправил я, — и зарегистрировал домен в &lt;a href=&quot;https://postmaster.mail.ru&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Postmaster Mail.ru&lt;/a&gt;. В общем, сделал всё, что мог.&lt;/p&gt;

&lt;p&gt;Но и самый надёжный сервис может подвести. Когда дата-центры Яндекса вывели из строя, Postbox продолжал считать, что отправляет письма, а на деле они не приходили. Я поймал это на тестировании и задержал выход обновления.&lt;/p&gt;

&lt;div class=&quot;image-gallery&quot; data-gallery-id=&quot;gallery-auto&quot;&gt;
  
  
  
  
  &lt;!-- Loading State --&gt;
  &lt;div class=&quot;gallery-loading&quot; data-images-count=&quot;3&quot;&gt;
    &lt;div class=&quot;gallery-loading-content&quot;&gt;
      &lt;div class=&quot;gallery-loading-spinner&quot;&gt;&lt;/div&gt;
      &lt;span class=&quot;gallery-loading-text&quot;&gt;Загрузка изображений...&lt;/span&gt;
      &lt;div class=&quot;gallery-loading-progress&quot;&gt;
        &lt;div class=&quot;gallery-loading-bar&quot;&gt;&lt;/div&gt;
      &lt;/div&gt;
      &lt;div class=&quot;gallery-loading-counter&quot;&gt;
        &lt;span class=&quot;gallery-loaded-count&quot;&gt;0&lt;/span&gt;/&lt;span class=&quot;gallery-total-count&quot;&gt;3&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Gallery Content (hidden by default) --&gt;
  &lt;div class=&quot;gallery-content&quot; style=&quot;opacity: 0; visibility: hidden;&quot;&gt;
    &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;3&quot;&gt;
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/login_email.webp&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/login_email@480w.webp 480w, https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/login_email@720w.webp 720w, https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/login_email.webp 920w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;Экран входа «Добро пожаловать!» с полем для email и ссылкой на вход по номеру телефона для пользователей RedCalendar 2.0&quot; width=&quot;920&quot; height=&quot;2000&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 920 / 2000;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/login_code.webp&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/login_code@480w.webp 480w, https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/login_code@720w.webp 720w, https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/login_code.webp 920w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;Экран «Привет, Арина!»: поле для кода из письма и кнопка «Отправить новый код»&quot; width=&quot;920&quot; height=&quot;2000&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 920 / 2000;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/login_letter.webp&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/login_letter@480w.webp 480w, https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/login_letter@720w.webp 720w, https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/login_letter.webp 920w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;Письмо «Код подтверждения RedCalendar» от noreply@calendar.red с шестизначным кодом&quot; width=&quot;920&quot; height=&quot;2000&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 920 / 2000;&quot; /&gt;
        
      
    &lt;/div&gt;
    
    &lt;!-- Navigation controls - always present for galleries --&gt;
    &lt;div class=&quot;gallery-controls&quot;&gt;
      &lt;button class=&quot;gallery-nav gallery-prev&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;prev&quot; aria-label=&quot;Предыдущее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;15 18 9 12 15 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
      &lt;button class=&quot;gallery-nav gallery-next&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;next&quot; aria-label=&quot;Следующее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;9 18 15 12 9 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Fallback for no-JS --&gt;
  &lt;noscript&gt;
    &lt;div class=&quot;gallery-fallback&quot;&gt;
      &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;3&quot;&gt;
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/login_email.webp&quot; alt=&quot;Экран входа «Добро пожаловать!» с полем для email и ссылкой на вход по номеру телефона для пользователей RedCalendar 2.0&quot; width=&quot;920&quot; height=&quot;2000&quot; style=&quot;aspect-ratio: 920 / 2000;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/login_code.webp&quot; alt=&quot;Экран «Привет, Арина!»: поле для кода из письма и кнопка «Отправить новый код»&quot; width=&quot;920&quot; height=&quot;2000&quot; style=&quot;aspect-ratio: 920 / 2000;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/login_letter.webp&quot; alt=&quot;Письмо «Код подтверждения RedCalendar» от noreply@calendar.red с шестизначным кодом&quot; width=&quot;920&quot; height=&quot;2000&quot; style=&quot;aspect-ratio: 920 / 2000;&quot; /&gt;
          
        
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/noscript&gt;
&lt;/div&gt;

&lt;h2 id=&quot;как-перевезти-людей-не-заставив-их-ничего-делать&quot;&gt;Как перевезти людей, не заставив их ничего делать&lt;/h2&gt;

&lt;p&gt;Главное правило переезда: никто не должен ничего переносить вручную, заново входить в аккаунт или делать что-то ещё. Обновил приложение — и все записи на месте.&lt;/p&gt;

&lt;p&gt;Готовиться к этому я начал больше чем за год. Летом 2025 года, в версии 2.2.2, старый RedCalendar стал при каждом запуске тихо оставлять себе записку: идентификатор аккаунта в Keychain — защищённом хранилище iPhone, которое переживает обновления, в секторе, доступном только для RedCalendar. Новое приложение вышло в App Store под тем же именем (то есть идентификатором), то есть пришло обычным обновлением. При первом запуске оно находит записку и узнаёт человека без всякого входа. От человека нужно только одно — дать согласие на обработку данных.&lt;/p&gt;

&lt;p&gt;Сами записи телефон не перевозит: главная копия хранилась в Firebase, поэтому за ними идёт сервер. Он выгружает историю, приводит её к новому формату, проверяет по тем же правилам, что и любые правки с телефона, и складывает в свою базу. Пока это происходит, вместо пустого календаря крутится значок синхронизации. Когда всё готово, история приезжает целиком.&lt;/p&gt;

&lt;p&gt;Сломать переезд на полпути нельзя. Записка стирается только после того, как сервер выдал телефону новый ключ входа: пропала сеть или приложение закрыли — при следующем запуске всё начнётся заново. А записи для каждого аккаунта переносятся один раз, поэтому второй телефон или повторный вход ничего не скопируют дважды и не вернут то, что уже удалено.&lt;/p&gt;

&lt;p&gt;Если записки нет — например, старую версию не открывали с лета 2025 года, — можно войти по номеру телефона, как раньше. Новый сервер сохранил идентификаторы аккаунтов из Firebase, так что по номеру находится тот же самый аккаунт, и дальше всё идёт точно так же.&lt;/p&gt;

&lt;h2 id=&quot;закон-согласие-и-удаление&quot;&gt;Закон: согласие и удаление&lt;/h2&gt;

&lt;p&gt;Переезд — это не только про серверы, но и про документы.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Согласие перед входом.&lt;/strong&gt; С 1 сентября 2025 года согласие на обработку персональных данных должно быть отдельным документом, а не строчкой в правилах. Поэтому перед входом приложение просит его явно, а сервер записывает, кто, когда и какую версию документа принял. Без этой записи войти нельзя.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Удаление аккаунта.&lt;/strong&gt; По закону на уничтожение данных есть 30 дней. Резервные копии у хостинга хранятся около трёх недель. Поэтому аккаунт удаляется через 7 дней после запроса: 7 дней плюс примерно 23 дня жизни копий как раз дают 30. Передумали в течение недели — просто войдите снова, и удаление отменится.&lt;/p&gt;

&lt;div class=&quot;image-gallery&quot; data-gallery-id=&quot;gallery-auto&quot;&gt;
  
  
  
  
  &lt;!-- Loading State --&gt;
  &lt;div class=&quot;gallery-loading&quot; data-images-count=&quot;2&quot;&gt;
    &lt;div class=&quot;gallery-loading-content&quot;&gt;
      &lt;div class=&quot;gallery-loading-spinner&quot;&gt;&lt;/div&gt;
      &lt;span class=&quot;gallery-loading-text&quot;&gt;Загрузка изображений...&lt;/span&gt;
      &lt;div class=&quot;gallery-loading-progress&quot;&gt;
        &lt;div class=&quot;gallery-loading-bar&quot;&gt;&lt;/div&gt;
      &lt;/div&gt;
      &lt;div class=&quot;gallery-loading-counter&quot;&gt;
        &lt;span class=&quot;gallery-loaded-count&quot;&gt;0&lt;/span&gt;/&lt;span class=&quot;gallery-total-count&quot;&gt;2&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Gallery Content (hidden by default) --&gt;
  &lt;div class=&quot;gallery-content&quot; style=&quot;opacity: 0; visibility: hidden;&quot;&gt;
    &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;2&quot;&gt;
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/consent_on.webp&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/consent_on@480w.webp 480w, https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/consent_on@720w.webp 720w, https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/consent_on.webp 920w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;Экран «Прежде чем продолжить»: ссылка на согласие на обработку персональных данных и включённый переключатель согласия&quot; width=&quot;920&quot; height=&quot;2000&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 920 / 2000;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/delete_account.webp&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/delete_account@480w.webp 480w, https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/delete_account@720w.webp 720w, https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/delete_account.webp 920w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;Экран «Удаление аккаунта»: данные стираются окончательно через 7 дней, а если войти раньше, всё вернётся&quot; width=&quot;920&quot; height=&quot;2000&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 920 / 2000;&quot; /&gt;
        
      
    &lt;/div&gt;
    
    &lt;!-- Navigation controls - always present for galleries --&gt;
    &lt;div class=&quot;gallery-controls&quot;&gt;
      &lt;button class=&quot;gallery-nav gallery-prev&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;prev&quot; aria-label=&quot;Предыдущее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;15 18 9 12 15 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
      &lt;button class=&quot;gallery-nav gallery-next&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;next&quot; aria-label=&quot;Следующее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;9 18 15 12 9 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Fallback for no-JS --&gt;
  &lt;noscript&gt;
    &lt;div class=&quot;gallery-fallback&quot;&gt;
      &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;2&quot;&gt;
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/consent_on.webp&quot; alt=&quot;Экран «Прежде чем продолжить»: ссылка на согласие на обработку персональных данных и включённый переключатель согласия&quot; width=&quot;920&quot; height=&quot;2000&quot; style=&quot;aspect-ratio: 920 / 2000;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/delete_account.webp&quot; alt=&quot;Экран «Удаление аккаунта»: данные стираются окончательно через 7 дней, а если войти раньше, всё вернётся&quot; width=&quot;920&quot; height=&quot;2000&quot; style=&quot;aspect-ratio: 920 / 2000;&quot; /&gt;
          
        
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/noscript&gt;
&lt;/div&gt;

&lt;h2 id=&quot;новое-приложение-с-нуля&quot;&gt;Новое приложение с нуля&lt;/h2&gt;

&lt;p&gt;Старый RedCalendar был написан на UIKit — прежнем способе делать интерфейсы для iPhone. Новый написан на SwiftUI, современном инструменте Apple. Переносить старый код не стал: проще и надёжнее было начать заново.&lt;/p&gt;

&lt;p&gt;Главное, что хотелось получить, — предсказуемость. Всё, что знает приложение — какой день открыт, что в нём отмечено, выполнен ли вход, — лежит в одном месте. Экраны ничего не меняют сами: они только показывают это состояние и отправляют заявки вроде «отмечено начало месячных», а заявки выполняются строго по очереди. Поэтому, если на экране что-то не так, причину не нужно искать по всему приложению. Программисты узнают в этом схему Redux и, на мой взгляд, она идеальна для SwiftUI.&lt;/p&gt;

&lt;p&gt;Самая сложная часть нового приложения глазами не видна — это синхронизация. Все записи хранятся прямо на телефоне, поэтому календарь работает и без интернета, а изменения уходят на сервер, как только появится связь. Звучит просто, пока не начинаешь задавать вопросы. Что делать, если интернет пропал посреди отправки? Как не дать удалённой записи «воскреснуть» на другом телефоне? Чья правка победит, если одну и ту же запись поменяли на двух устройствах? Ответы я сначала записал в документ на 2 500 строк и только потом начал писать код.&lt;/p&gt;

&lt;p&gt;Интерфейс тоже сделан заново. Прогноз месячных теперь нарисован контуром, а отмеченные дни — сплошной полосой, так что прошлое с прогнозом не спутать. Цветные точки под днями остались: каждая показывает цвет категории тегов, а какой смысл вложить в каждую категорию, решает сам пользователь. Появился и модный эффект стекла.&lt;/p&gt;

&lt;p&gt;Теги и комментарий показываются прямо в карточке дня. Открытый день, как и раньше, можно листать свайпом, но теперь можно и просто нажать на другой день в календаре — сразу откроется его карточка. В старой версии на UIKit сделать так у меня не получилось: нажатие на календарь закрывало карточку, и чтобы выбрать другой день, приходилось нажимать ещё раз. А акцентный цвет приложения можно выбрать в настройках.&lt;/p&gt;

&lt;p&gt;Так выглядел старый RedCalendar:&lt;/p&gt;

&lt;div class=&quot;image-gallery&quot; data-gallery-id=&quot;gallery-auto&quot;&gt;
  
  
  
  
  &lt;!-- Loading State --&gt;
  &lt;div class=&quot;gallery-loading&quot; data-images-count=&quot;4&quot;&gt;
    &lt;div class=&quot;gallery-loading-content&quot;&gt;
      &lt;div class=&quot;gallery-loading-spinner&quot;&gt;&lt;/div&gt;
      &lt;span class=&quot;gallery-loading-text&quot;&gt;Загрузка изображений...&lt;/span&gt;
      &lt;div class=&quot;gallery-loading-progress&quot;&gt;
        &lt;div class=&quot;gallery-loading-bar&quot;&gt;&lt;/div&gt;
      &lt;/div&gt;
      &lt;div class=&quot;gallery-loading-counter&quot;&gt;
        &lt;span class=&quot;gallery-loaded-count&quot;&gt;0&lt;/span&gt;/&lt;span class=&quot;gallery-total-count&quot;&gt;4&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Gallery Content (hidden by default) --&gt;
  &lt;div class=&quot;gallery-content&quot; style=&quot;opacity: 0; visibility: hidden;&quot;&gt;
    &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;4&quot;&gt;
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/old_today.webp&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/old_today@480w.webp 480w, https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/old_today@720w.webp 720w, https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/old_today.webp 920w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;Старый RedCalendar: календарь мая с месячными и карточка сегодняшнего дня&quot; width=&quot;920&quot; height=&quot;2000&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 920 / 2000;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/old_day.webp&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/old_day@480w.webp 480w, https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/old_day@720w.webp 720w, https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/old_day.webp 920w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;Старая карточка дня 20 мая с отметкой «Начало месячных» и обильностью&quot; width=&quot;920&quot; height=&quot;2000&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 920 / 2000;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/old_tags.webp&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/old_tags@480w.webp 480w, https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/old_tags@720w.webp 720w, https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/old_tags.webp 920w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;Старый экран выбора тегов с поиском&quot; width=&quot;920&quot; height=&quot;2000&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 920 / 2000;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/old_comment.webp&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/old_comment@480w.webp 480w, https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/old_comment@720w.webp 720w, https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/old_comment.webp 920w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;Старый экран комментария к дню&quot; width=&quot;920&quot; height=&quot;2000&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 920 / 2000;&quot; /&gt;
        
      
    &lt;/div&gt;
    
    &lt;!-- Navigation controls - always present for galleries --&gt;
    &lt;div class=&quot;gallery-controls&quot;&gt;
      &lt;button class=&quot;gallery-nav gallery-prev&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;prev&quot; aria-label=&quot;Предыдущее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;15 18 9 12 15 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
      &lt;button class=&quot;gallery-nav gallery-next&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;next&quot; aria-label=&quot;Следующее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;9 18 15 12 9 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Fallback for no-JS --&gt;
  &lt;noscript&gt;
    &lt;div class=&quot;gallery-fallback&quot;&gt;
      &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;4&quot;&gt;
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/old_today.webp&quot; alt=&quot;Старый RedCalendar: календарь мая с месячными и карточка сегодняшнего дня&quot; width=&quot;920&quot; height=&quot;2000&quot; style=&quot;aspect-ratio: 920 / 2000;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/old_day.webp&quot; alt=&quot;Старая карточка дня 20 мая с отметкой «Начало месячных» и обильностью&quot; width=&quot;920&quot; height=&quot;2000&quot; style=&quot;aspect-ratio: 920 / 2000;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/old_tags.webp&quot; alt=&quot;Старый экран выбора тегов с поиском&quot; width=&quot;920&quot; height=&quot;2000&quot; style=&quot;aspect-ratio: 920 / 2000;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/old_comment.webp&quot; alt=&quot;Старый экран комментария к дню&quot; width=&quot;920&quot; height=&quot;2000&quot; style=&quot;aspect-ratio: 920 / 2000;&quot; /&gt;
          
        
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/noscript&gt;
&lt;/div&gt;

&lt;p&gt;А так — новый:&lt;/p&gt;

&lt;div class=&quot;image-gallery&quot; data-gallery-id=&quot;gallery-auto&quot;&gt;
  
  
  
  
  &lt;!-- Loading State --&gt;
  &lt;div class=&quot;gallery-loading&quot; data-images-count=&quot;4&quot;&gt;
    &lt;div class=&quot;gallery-loading-content&quot;&gt;
      &lt;div class=&quot;gallery-loading-spinner&quot;&gt;&lt;/div&gt;
      &lt;span class=&quot;gallery-loading-text&quot;&gt;Загрузка изображений...&lt;/span&gt;
      &lt;div class=&quot;gallery-loading-progress&quot;&gt;
        &lt;div class=&quot;gallery-loading-bar&quot;&gt;&lt;/div&gt;
      &lt;/div&gt;
      &lt;div class=&quot;gallery-loading-counter&quot;&gt;
        &lt;span class=&quot;gallery-loaded-count&quot;&gt;0&lt;/span&gt;/&lt;span class=&quot;gallery-total-count&quot;&gt;4&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Gallery Content (hidden by default) --&gt;
  &lt;div class=&quot;gallery-content&quot; style=&quot;opacity: 0; visibility: hidden;&quot;&gt;
    &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;4&quot;&gt;
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/calendar.webp&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/calendar@480w.webp 480w, https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/calendar@720w.webp 720w, https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/calendar.webp 920w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;Новый календарь: апрель с прогнозом овуляции и месячных, снизу карточка сегодняшнего дня&quot; width=&quot;920&quot; height=&quot;2000&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 920 / 2000;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/day.webp&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/day@480w.webp 480w, https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/day@720w.webp 720w, https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/day.webp 920w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;Карточка дня 26 марта: обильность, теги и комментарий&quot; width=&quot;920&quot; height=&quot;2000&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 920 / 2000;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/tags.webp&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/tags@480w.webp 480w, https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/tags@720w.webp 720w, https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/tags.webp 920w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;Новый экран тегов: цветные теги по группам, поиск и кнопка «Новый тег»&quot; width=&quot;920&quot; height=&quot;2000&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 920 / 2000;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/comment.webp&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/comment@480w.webp 480w, https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/comment@720w.webp 720w, https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/comment.webp 920w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;Новый экран комментария к дню&quot; width=&quot;920&quot; height=&quot;2000&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 920 / 2000;&quot; /&gt;
        
      
    &lt;/div&gt;
    
    &lt;!-- Navigation controls - always present for galleries --&gt;
    &lt;div class=&quot;gallery-controls&quot;&gt;
      &lt;button class=&quot;gallery-nav gallery-prev&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;prev&quot; aria-label=&quot;Предыдущее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;15 18 9 12 15 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
      &lt;button class=&quot;gallery-nav gallery-next&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;next&quot; aria-label=&quot;Следующее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;9 18 15 12 9 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Fallback for no-JS --&gt;
  &lt;noscript&gt;
    &lt;div class=&quot;gallery-fallback&quot;&gt;
      &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;4&quot;&gt;
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/calendar.webp&quot; alt=&quot;Новый календарь: апрель с прогнозом овуляции и месячных, снизу карточка сегодняшнего дня&quot; width=&quot;920&quot; height=&quot;2000&quot; style=&quot;aspect-ratio: 920 / 2000;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/day.webp&quot; alt=&quot;Карточка дня 26 марта: обильность, теги и комментарий&quot; width=&quot;920&quot; height=&quot;2000&quot; style=&quot;aspect-ratio: 920 / 2000;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/tags.webp&quot; alt=&quot;Новый экран тегов: цветные теги по группам, поиск и кнопка «Новый тег»&quot; width=&quot;920&quot; height=&quot;2000&quot; style=&quot;aspect-ratio: 920 / 2000;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/comment.webp&quot; alt=&quot;Новый экран комментария к дню&quot; width=&quot;920&quot; height=&quot;2000&quot; style=&quot;aspect-ratio: 920 / 2000;&quot; /&gt;
          
        
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/noscript&gt;
&lt;/div&gt;

&lt;p&gt;Была задача освежить приложение, но оставить узнаваемым.&lt;/p&gt;

&lt;h2 id=&quot;что-дальше&quot;&gt;Что дальше&lt;/h2&gt;

&lt;p&gt;Регистрации по номеру телефона в новом приложении нет: этот вход оставлен только для тех, кто пользовался старой версией, а новые пользователи входят по почте.&lt;/p&gt;

&lt;p&gt;Сама идея входа по номеру мне нравилась больше, но стала плохо работать. Дело не только в деньгах: SMS с кодом теперь стоит очень дорого, а приходит не обязательно SMS (звонок робота, сообщение в Telegram, WhatsApp или, упаси Господи, в Max). А может и не прийти вооьще. Вход по звонку — хорошая штука, но заботливые операторы теперь блокируют такие звонки, а человек думает, что это приложение кривое, и может поставить одну звезду:&lt;/p&gt;

&lt;div class=&quot;image-gallery&quot; data-gallery-id=&quot;gallery-auto&quot;&gt;
  
  
  
  
  &lt;!-- Loading State --&gt;
  &lt;div class=&quot;gallery-loading&quot; data-images-count=&quot;1&quot;&gt;
    &lt;div class=&quot;gallery-loading-content&quot;&gt;
      &lt;div class=&quot;gallery-loading-spinner&quot;&gt;&lt;/div&gt;
      &lt;span class=&quot;gallery-loading-text&quot;&gt;Загрузка изображений...&lt;/span&gt;
      &lt;div class=&quot;gallery-loading-progress&quot;&gt;
        &lt;div class=&quot;gallery-loading-bar&quot;&gt;&lt;/div&gt;
      &lt;/div&gt;
      &lt;div class=&quot;gallery-loading-counter&quot;&gt;
        &lt;span class=&quot;gallery-loaded-count&quot;&gt;0&lt;/span&gt;/&lt;span class=&quot;gallery-total-count&quot;&gt;1&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Gallery Content (hidden by default) --&gt;
  &lt;div class=&quot;gallery-content&quot; style=&quot;opacity: 0; visibility: hidden;&quot;&gt;
    &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;1&quot;&gt;
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/review.webp&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/review@480w.webp 480w, https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/review@720w.webp 720w, https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/review.webp 920w&quot; sizes=&quot;(max-width: 475px) 85vw, 404px&quot; alt=&quot;Отзыв в App Store на одну звезду «Не получилось»: код не приходит, звонка тоже нет. Ниже ответ разработчика про блокировку SMS и звонков&quot; width=&quot;920&quot; height=&quot;1140&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 920 / 1140;&quot; /&gt;
        
      
    &lt;/div&gt;
    
    &lt;!-- Navigation controls - always present for galleries --&gt;
    &lt;div class=&quot;gallery-controls&quot;&gt;
      &lt;button class=&quot;gallery-nav gallery-prev&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;prev&quot; aria-label=&quot;Предыдущее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;15 18 9 12 15 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
      &lt;button class=&quot;gallery-nav gallery-next&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;next&quot; aria-label=&quot;Следующее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;9 18 15 12 9 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Fallback for no-JS --&gt;
  &lt;noscript&gt;
    &lt;div class=&quot;gallery-fallback&quot;&gt;
      &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;1&quot;&gt;
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-10-20-redcalendar-russification/review.webp&quot; alt=&quot;Отзыв в App Store на одну звезду «Не получилось»: код не приходит, звонка тоже нет. Ниже ответ разработчика про блокировку SMS и звонков&quot; width=&quot;920&quot; height=&quot;1140&quot; style=&quot;aspect-ratio: 920 / 1140;&quot; /&gt;
          
        
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/noscript&gt;
&lt;/div&gt;

&lt;p&gt;Письмо тоже может попасть в спам, но к этому давно привыкли. Поэтому со временем я попрошу пользователей добавить почту и выключу вход по номеру. Экран для привязки уже готов.&lt;/p&gt;

&lt;p&gt;Firebase тоже пока работает: в нём остаются аккаунты и записи тех, кто ещё не открыл новую версию. Когда переезжать станет некому, старый проект можно будет удалить. После этого RedCalendar окончательно станет российским — от сервера до последней резервной копии.&lt;/p&gt;
</content>
  </entry>
  
  <entry>
    <title type="html">От git push до сайта</title>
    <link href="https://bolotov.dev/blog/2026/08/24/git-push-deploy" rel="alternate" type="text/html"/>
    <id>https://bolotov.dev/blog/2026/08/24/git-push-deploy</id>
    <published>2026-08-24T00:00:00+00:00</published>
    <updated>2026-08-24T00:00:00+00:00</updated>
    
    


    <category term="webdev" label="webdev"/>
    
    <summary type="html">Настраиваем автоматическую публикацию сайта на хостинг через GitHub Actions: SSH-ключи, секреты и сам workflow</summary>
    
    <content type="html" xml:base="https://bolotov.dev/blog/2026/08/24/git-push-deploy">&lt;p&gt;Любой проект — это много мелких правок, часть которых можно делать «между делом». Но мы часто их откладываем, ведь в классическом варианте эти правки нужно не только внести, но и как-то применить. Поэтому они копятся, забываются, а потом просто становится не до них. Как, к примеру, выглядит работа с сайтом? Есть у вас на компьютере локальная версия, в ней вы и «играетесь», а потом публикуете. Физически нужен доступ к этой версии, компьютер. Короче, это неудобно, поэтому давайте попробуем другой способ. Пусть код сайта хранится где-то в облаке, а изменения заливаются на хостинг автоматически. Быстро, без ноутбука, без каких-то программ для загрузки файлов, без «доеду до дома — поправлю».&lt;/p&gt;

&lt;p&gt;Называется это автоматической публикацией — вы отправляете изменения в специальное хранилище кода, а дальше сервис сам берёт свежую версию сайта и выкладывает её на хостинг в соответствии с правилами, которые вы определили. Я хочу показать, как это устроено, на примере реального проекта на PHP. А в следующих статьях разберём, как быть с сервисом на Node.js и как автоматически собирать нативные мобильные приложения без компьютера и посторонних сервисов. Начнём с сайта, потому что это просто.&lt;/p&gt;

&lt;h2 id=&quot;как-обычно-выкладывают-сайт&quot;&gt;Как обычно выкладывают сайт&lt;/h2&gt;

&lt;p&gt;Классический способ — зайти на хостинг по FTP или SFTP каким-нибудь файловым клиентом (например, FileZilla), найти на компьютере изменённые файлы и перетащить их на сервер поверх старых.&lt;/p&gt;

&lt;p&gt;Работает это до поры до времени, а потом начинаются мелкие неприятности. Забыли залить один из изменённых файлов — и часть сайта работает по-старому. Не помните, какая версия файла сейчас на сервере — та, что вы правили вчера, или ещё более старая. Два человека работают над одним проектом — и рискуют затереть правки друг друга, просто перезалив файлы поверх. А если что-то сломалось после заливки — вернуть как было можно только если у вас случайно осталась резервная копия старых файлов.&lt;/p&gt;

&lt;p&gt;Ни истории изменений, ни возможности откатиться на шаг назад, ни ясности, что именно сейчас лежит на сервере.&lt;/p&gt;

&lt;h2 id=&quot;репозиторий--это-надёжнее&quot;&gt;Репозиторий — это надёжнее&lt;/h2&gt;

&lt;p&gt;Есть другой подход: хранить код сайта не просто как папку с файлами на компьютере, а в системе контроля версий — например, в git. Такое хранилище называется репозиторием, а сервисов, которые их у себя размещают, много — самый известный GitHub.&lt;/p&gt;

&lt;p&gt;Если совсем просто: репозиторий запоминает каждое изменение файлов как отдельный шаг с датой, автором и описанием. Захотели узнать, что изменилось за последний месяц, — посмотрели историю. Что-то сломали — вернулись на шаг назад одной командой. Работаете вдвоём — правки не затираются, а аккуратно складываются друг на друга.&lt;/p&gt;

&lt;p&gt;Но главное для нашей темы — другое. Раз весь сайт целиком лежит в репозитории, то у вас появляется однозначный ответ на вопрос «а что сейчас на сайте?»: то, что лежит в репозитории в основной ветке (обычно она называется &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;main&lt;/code&gt;), — то и должно быть на сайте. И никакой путаницы.&lt;/p&gt;

&lt;h2 id=&quot;можно-работать-откуда-угодно&quot;&gt;Можно работать откуда угодно&lt;/h2&gt;

&lt;p&gt;И вот тут открывается неочевидная выгода. Раз ваш сайт — это просто репозиторий на GitHub, работать с ним можно не только с рабочего компьютера, где стоит привычная программа для редактирования кода.&lt;/p&gt;

&lt;p&gt;А ещё с репозиториями хорошо работают помощники вроде Claude Code — им можно описать задачу («поправь вёрстку карточки на мобильных» или «добавь новую страницу с прайсом»), и они сами напишут код и отправят изменение в репозиторий. То есть можно между делом, буквально с телефона, попросить что-то поправить или попробовать новую идею на сайте — и не нужно вообще садиться за компьютер.&lt;/p&gt;

&lt;h2 id=&quot;что-такое-github-actions&quot;&gt;Что такое GitHub Actions&lt;/h2&gt;

&lt;p&gt;GitHub Actions — это встроенный в GitHub робот, который умеет что-то делать автоматически при разных событиях в репозитории. Самое частое событие — отправка изменений (это и называется словом push, «отправить»): вы отправили правки в основную ветку — робот заметил это и запустил заранее описанный набор шагов.&lt;/p&gt;

&lt;p&gt;Набор шагов описывается в обычном текстовом файле в формате YAML, который лежит прямо в репозитории по пути &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.github/workflows/имя-файла.yml&lt;/code&gt;. В этом файле написано человеческим языком (ну, почти): «при отправке изменений в ветку main — скачай свежий код и отправь его на хостинг по такому-то адресу».&lt;/p&gt;

&lt;h2 id=&quot;готовим-ssh-ключ&quot;&gt;Готовим SSH-ключ&lt;/h2&gt;

&lt;p&gt;Чтобы робот GitHub Actions мог зайти на хостинг и оставить там файлы, ему нужен способ авторизоваться — представиться и доказать, что он действительно имеет право туда заходить. Самый надёжный вариант — SSH-ключ.&lt;/p&gt;

&lt;p&gt;SSH-ключ — это пара файлов: закрытый (приватный) и открытый (публичный). Работает это так: открытый ключ кладётся на сервер и говорит «пускай того, у кого есть парный закрытый ключ». Закрытый ключ никому не показывается и остаётся только у вас — точнее, в нашем случае, только у GitHub, в специальном защищённом хранилище, о котором дальше.&lt;/p&gt;

&lt;p&gt;Создать пару ключей можно одной командой в терминале (на Mac и Linux — обычный терминал, на Windows — например, PowerShell или Git Bash):&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;ssh-keygen &lt;span class=&quot;nt&quot;&gt;-t&lt;/span&gt; ed25519 &lt;span class=&quot;nt&quot;&gt;-C&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;deploy-key&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;На вопросы, где сохранить файл и нужен ли пароль на сам ключ, можно просто нажимать Enter, соглашаясь со значениями по умолчанию (для ключа, который будет использовать робот, а не вы лично, пароль на сам ключ не нужен — иначе его некому будет вводить). В результате появятся два файла: например, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;id_ed25519&lt;/code&gt; (закрытый ключ) и &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;id_ed25519.pub&lt;/code&gt; (открытый).&lt;/p&gt;

&lt;h2 id=&quot;кладём-ключ-на-хостинг-и-в-настройки-репозитория&quot;&gt;Кладём ключ на хостинг и в настройки репозитория&lt;/h2&gt;

&lt;p&gt;Дальше открытый ключ (файл с окончанием &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.pub&lt;/code&gt;) нужно положить на хостинг, а закрытый — спрятать в настройках репозитория на GitHub, откуда его сможет достать только сам GitHub Actions.&lt;/p&gt;

&lt;p&gt;Я показываю на примере хостинга &lt;a href=&quot;https://netangels.ru/?p_ref=u61786&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;NetAngels&lt;/a&gt; — пользуюсь им давно, но подойдёт в целом любой хостинг с поддержкой SSH, шаги будут примерно такими же.&lt;/p&gt;

&lt;p&gt;В личном кабинете NetAngels заходим в нужный контейнер сайта и открываем вкладку «SSH». Там же будут видны логин (что-то вроде &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;c123456&lt;/code&gt;), адрес сервера (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;h60.netangels.ru&lt;/code&gt;) и порт (обычно 22) — эти данные понадобятся чуть позже.&lt;/p&gt;

&lt;div class=&quot;single_image&quot;&gt;
  &lt;img src=&quot;https://bolotov.dev/blog/img/2026-08-24-git-push-deploy/netangels-ssh-access.png&quot; alt=&quot;Блок SSH-доступ к контейнеру в личном кабинете NetAngels: логин, IP-адрес, адрес сервера и список SSH-ключей&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-single-image=&quot;&quot; /&gt;
  
&lt;/div&gt;

&lt;p&gt;Нажимаем «Добавить ключ», в выпадающем списке выбираем «Создать новый ключ», указываем любое понятное название (например, «GitHub Actions») и вставляем в поле «Ключ» содержимое файла &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;id_ed25519.pub&lt;/code&gt; — того самого, открытого. Нажимаем «Добавить ключ».&lt;/p&gt;

&lt;div class=&quot;single_image&quot;&gt;
  &lt;img src=&quot;https://bolotov.dev/blog/img/2026-08-24-git-push-deploy/netangels-add-ssh-key.png&quot; alt=&quot;Форма добавления SSH-ключа в NetAngels с полями «Имя ключа» и «Ключ»&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-single-image=&quot;&quot; /&gt;
  
&lt;/div&gt;

&lt;p&gt;Теперь идём в репозиторий на GitHub, в раздел &lt;strong&gt;Settings → Secrets and variables → Actions&lt;/strong&gt;. Там два похожих, но разных списка:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Secrets&lt;/strong&gt; — для по-настоящему секретных данных, вроде паролей и ключей. Значение можно один раз задать, но потом нигде, даже вам самим, посмотреть его снова не получится — только заменить.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Variables&lt;/strong&gt; — для данных, которые не секрет, а просто настройка, например путь к папке на сервере.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Добавляем секреты (кнопка «New repository secret»):&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SSH_HOST&lt;/code&gt; — адрес сервера (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;h60.netangels.ru&lt;/code&gt;)&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SSH_USERNAME&lt;/code&gt; — логин с той же вкладки «SSH»&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SSH_PORT&lt;/code&gt; — порт (обычно &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;22&lt;/code&gt;)&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SSH_KEY&lt;/code&gt; — содержимое закрытого ключа целиком, файла &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;id_ed25519&lt;/code&gt; (не &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.pub&lt;/code&gt;!)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;В итоге список секретов должен выглядеть примерно так:&lt;/p&gt;

&lt;div class=&quot;single_image&quot;&gt;
  &lt;img src=&quot;https://bolotov.dev/blog/img/2026-08-24-git-push-deploy/github-repository-secrets.png&quot; alt=&quot;Список Repository secrets в настройках GitHub: SSH_HOST, SSH_KEY, SSH_PORT, SSH_USERNAME&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-single-image=&quot;&quot; /&gt;
  
&lt;/div&gt;

&lt;p&gt;И одну переменную (кнопка «New repository variable», на отдельной вкладке «Variables» рядом с «Secrets»):&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DESTINATION_PATH&lt;/code&gt; — путь до папки на сервере, куда нужно класть файлы сайта (включая конечную папку www, если она есть), например &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/home/c123456/my-cool-website.ru/www&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class=&quot;single_image&quot;&gt;
  &lt;img src=&quot;https://bolotov.dev/blog/img/2026-08-24-git-push-deploy/github-repository-variables.png&quot; alt=&quot;Вкладка Variables в настройках GitHub с переменной DESTINATION_PATH&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-single-image=&quot;&quot; /&gt;
  
&lt;/div&gt;

&lt;h2 id=&quot;сам-файл-разбираем-построчно&quot;&gt;Сам файл: разбираем построчно&lt;/h2&gt;

&lt;p&gt;Ниже файл &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.github/workflows/deploy.yml&lt;/code&gt;, которым я пользуюсь у себя в одном из PHP-проектов:&lt;/p&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Deploy to NetAngels&lt;/span&gt;

&lt;span class=&quot;na&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;branches&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;main&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;master&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;workflow_dispatch&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;

&lt;span class=&quot;na&quot;&gt;jobs&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;deploy&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Deploy to NetAngels hosting&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;runs-on&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;ubuntu-latest&lt;/span&gt;

    &lt;span class=&quot;na&quot;&gt;steps&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Checkout code&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;actions/checkout@v7&lt;/span&gt;

    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Setup PHP&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;shivammathur/setup-php@v2&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;php-version&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;8.5&apos;&lt;/span&gt;

    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Validate PHP syntax&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;|&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;echo &quot;🔍 Checking PHP syntax...&quot;&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;find www -name &quot;*.php&quot; -exec php -l {} \; | tee php_check.log&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;if grep -q &quot;Parse error\|Fatal error\|Syntax error&quot; php_check.log; then&lt;/span&gt;
          &lt;span class=&quot;s&quot;&gt;echo &quot;❌ PHP syntax errors found!&quot;&lt;/span&gt;
          &lt;span class=&quot;s&quot;&gt;exit 1&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;fi&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;echo &quot;✅ PHP syntax validation passed&quot;&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;rm php_check.log&lt;/span&gt;

    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;🚀 Deploy via rsync&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;burnett01/rsync-deployments@v9&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;switches&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;-avzr --delete --exclude-from=.rsyncignore&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;./www/&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;remote_path&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;${{ vars.DESTINATION_PATH }}&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;remote_host&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;${{ secrets.SSH_HOST }}&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;remote_user&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;${{ secrets.SSH_USERNAME }}&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;remote_key&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;${{ secrets.SSH_KEY }}&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;remote_port&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;${{ secrets.SSH_PORT }}&lt;/span&gt;

    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Verify deployment&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;|&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;echo &quot;🔍 Verifying deployment...&quot;&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;echo &quot;✅ Deployment completed successfully!&quot;&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;echo &quot;📊 Deployment summary:&quot;&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;echo &quot;   - Server: ${{ secrets.SSH_HOST }}&quot;&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;echo &quot;   - Directory: ${{ vars.DESTINATION_PATH }}&quot;&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;echo &quot;   - Branch: ${{ github.ref_name }}&quot;&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;echo &quot;   - Commit: ${{ github.sha }}&quot;&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;echo &quot;   - Time: $(date)&quot;&lt;/span&gt;

    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Notify on failure&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;failure()&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;|&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;echo &quot;❌ Deployment failed!&quot;&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;echo &quot;Please check the logs above for details.&quot;&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;echo &quot;Common issues:&quot;&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;echo &quot;  - SSH key not added to server&quot;&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;echo &quot;  - SSH username incorrect&quot;&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;echo &quot;  - Server directory doesn&apos;t exist&quot;&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;echo &quot;  - Permission issues&quot;&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;echo &quot;  - PHP syntax errors&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Разберём по частям:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;on: push: branches: [ main, master ]&lt;/code&gt; плюс &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;workflow_dispatch:&lt;/code&gt; — запускать сценарий при отправке изменений сразу в обе ветки (у старых проектов основная ветка иногда называется &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;master&lt;/code&gt;, а не &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;main&lt;/code&gt;), а &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;workflow_dispatch&lt;/code&gt; добавляет в интерфейсе GitHub кнопку «Run workflow» — можно запустить публикацию вручную, без нового изменения, если, например, просто хочется выложить то же самое ещё раз.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;runs-on: ubuntu-latest&lt;/code&gt; — GitHub на минуту-другую выделяет под задачу временный компьютер с Linux. Он появляется, выполняет шаги и после этого исчезает — никаких следов не остаётся, кроме журнала выполнения.&lt;/li&gt;
  &lt;li&gt;Шаг &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Checkout code&lt;/code&gt; — скачивает на этот временный компьютер код репозитория.&lt;/li&gt;
  &lt;li&gt;Шаг &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Setup PHP&lt;/code&gt; — на временном компьютере по умолчанию нет PHP, поэтому его ставит отдельным шагом, той же версии, что и на хостинге (8.5), чтобы дальнейшая проверка была честной.&lt;/li&gt;
  &lt;li&gt;Шаг &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Validate PHP syntax&lt;/code&gt; — прогоняет каждый &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.php&lt;/code&gt;-файл в папке &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;www&lt;/code&gt; через встроенную проверку синтаксиса PHP (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;php -l&lt;/code&gt;, l — от «lint»), собирает результат в файл-журнал и ищет там слова вроде «Parse error». Если находит — сценарий останавливается с ошибкой (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;exit 1&lt;/code&gt;) и до следующего шага, самой отправки на сервер, дело просто не доходит. Опечатка в коде так и остаётся опечаткой в репозитории, а не превращается в сломанный сайт. Эмодзи в &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;echo&lt;/code&gt; — просто украшение для журнала выполнения, чтобы результат было проще пробежать глазами.&lt;/li&gt;
  &lt;li&gt;Шаг &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;🚀 Deploy via rsync&lt;/code&gt; — тот самый &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;burnett01/rsync-deployments&lt;/code&gt;, который копирует файлы на сервер по SSH программой &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rsync&lt;/code&gt;. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;path: ./www/&lt;/code&gt; — на сервер уходит содержимое только папки &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;www&lt;/code&gt;, где лежит собственно сайт, а не весь репозиторий целиком (там ведь есть ещё и сам файл &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;deploy.yml&lt;/code&gt;, и служебные файлы, которым на сервере делать нечего). &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--exclude-from=.rsyncignore&lt;/code&gt; подключает список исключений — отдельный файл, о котором дальше.&lt;/li&gt;
  &lt;li&gt;Шаги &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Verify deployment&lt;/code&gt; и &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Notify on failure&lt;/code&gt; не делают ничего технического — это просто аккуратный вывод в журнал: что именно и когда было выложено, а если что-то из предыдущих шагов сломалось (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;if: failure()&lt;/code&gt;) — подсказка, в какую сторону смотреть. Настоящего отката к прошлой версии тут нет, но и без него, если ошибка — на этапе проверки синтаксиса сайт просто не тронут, а не сломан наполовину.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ключ &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;${{ secrets.SSH_KEY }}&lt;/code&gt; — это то место, где GitHub подставляет ваш закрытый ключ в момент выполнения, никому его не показывая: ни в журнале выполнения, ни в самом файле настроек, который лежит в открытом репозитории.&lt;/p&gt;

&lt;p&gt;Флаги &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-avzr --delete&lt;/code&gt; у &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rsync&lt;/code&gt; означают: скопировать файлы рекурсивно, со сжатием, сохраняя даты изменения, и удалить на сервере то, чего больше нет в репозитории — чтобы сервер всегда точно повторял содержимое ветки, а не постепенно обрастал забытым старьём.&lt;/p&gt;

&lt;p&gt;Раз уж мы здесь: обратите внимание на цифры версий у сторонних actions в файле — &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;actions/checkout@v7&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;burnett01/rsync-deployments@v9&lt;/code&gt;. Их стоит время от времени проверять на актуальность: выходят более новые версии с исправлениями, а совсем старые рано или поздно перестают нормально работать. Сделать это просто: берём часть до &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@&lt;/code&gt; (например, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;burnett01/rsync-deployments&lt;/code&gt;) и открываем в браузере &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;github.com/burnett01/rsync-deployments&lt;/code&gt; — это и есть страница репозитория того action’а на GitHub, а на ней видно, какая версия сейчас самая свежая.&lt;/p&gt;

&lt;h2 id=&quot;что-кладём-в-rsyncignore&quot;&gt;Что кладём в .rsyncignore&lt;/h2&gt;

&lt;p&gt;В корне репозитория лежит ещё один файл — &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.rsyncignore&lt;/code&gt;. Это простой список того, что не нужно отправлять на сервер, даже если оно случайно оказалось в папке &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;www&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;.git
.github
.gitignore
.gitattributes
.vscode
.idea
.editorconfig
.envrc
.env*
vendor
*.log
logs
tmp
*.tmp
*.bak
*cookies*.txt
config.php
debug.php
phpinfo.php
test.php
bin
README.md
.DS_Store
Thumbs.db
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Сочинять надо под себя. Важный нюанс: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--exclude-from&lt;/code&gt; работает вместе с &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--delete&lt;/code&gt; по правилу «не трогать». Если такой файл случайно уже лежит на сервере — &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--delete&lt;/code&gt; его не удалит просто потому, что он подходит под исключение. Список работает только в одну сторону: не давать таким файлам попасть на сервер через эту отправку, а не выковыривать их оттуда, если они там уже были.&lt;/p&gt;

&lt;h2 id=&quot;пробуем&quot;&gt;Пробуем&lt;/h2&gt;

&lt;p&gt;Сохраняем файл &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;deploy.yml&lt;/code&gt;, отправляем это изменение в репозиторий — и дальше можно зайти на GitHub, во вкладку &lt;strong&gt;Actions&lt;/strong&gt;, и увидеть, как запустился и выполняется наш сценарий: шаг за шагом, с журналом всего происходящего. Через несколько секунд напротив всех шагов появятся зелёные галочки, а обновлённый сайт уже будет на хостинге.&lt;/p&gt;

&lt;div class=&quot;single_image&quot;&gt;
  &lt;img src=&quot;https://bolotov.dev/blog/img/2026-08-24-git-push-deploy/github-actions-run.png&quot; alt=&quot;Завершённый запуск workflow Deploy to NetAngels hosting в GitHub Actions: все шаги отмечены зелёными галочками&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-single-image=&quot;&quot; /&gt;
  
    &lt;em class=&quot;image-caption&quot;&gt;Шаг «Notify on failure» остался серым — он выполняется только при ошибке&lt;/em&gt;
  
&lt;/div&gt;

&lt;p&gt;Дальше это работает само: любое изменение, отправленное в &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;main&lt;/code&gt; — хоть с ноутбука, хоть из приложения GitHub на телефоне, хоть руками того же Claude Code, — само доедет до сайта за считаные секунды, без каких-либо действий с вашей стороны.&lt;/p&gt;

&lt;h2 id=&quot;что-дальше&quot;&gt;Что дальше&lt;/h2&gt;

&lt;p&gt;Мы разобрались, как сайт может публиковаться сам, при каждой отправке изменений в репозиторий. В следующих статьях посмотрим на вещи чуть сложнее: как таким же способом работать с проектом на Node.js, автоматически собрать мобильное приложение под Android или iOS и отправить его на тестирование — тоже без единого лишнего действия с компьютера.&lt;/p&gt;
</content>
  </entry>
  
  <entry>
    <title type="html">Закон об&amp;nbsp;иностранной почте</title>
    <link href="https://bolotov.dev/blog/2026/07/11/foreign-email-law" rel="alternate" type="text/html"/>
    <id>https://bolotov.dev/blog/2026/07/11/foreign-email-law</id>
    <published>2026-07-11T00:00:00+00:00</published>
    <updated>2026-07-11T00:00:00+00:00</updated>
    
    


    <category term="notes" label="заметки"/>
    
    <summary type="html">Что запрещает новый закон об&amp;nbsp;авторизации и&amp;nbsp;почему «национальность» почты не&amp;nbsp;имеет смысла</summary>
    
    <content type="html" xml:base="https://bolotov.dev/blog/2026/07/11/foreign-email-law">&lt;p&gt;С 7 июля 2026 года действует &lt;a href=&quot;https://www.consultant.ru/document/cons_doc_LAW_34661/eecaf3bcdcd8f21011f025127298179a3bfd8877/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;статья 13.55 КоАП&lt;/a&gt; — штрафы за нарушение правил авторизации на сайтах и в приложениях. В ней предусмотрены и штрафы для физических лиц. Если не читать подробнее, можно подумать, что теперь вход с Google или Apple стал «платным» (для физлиц от 10 000 рублей). На самом деле, это касается только владельцев сайтов и сервисов.&lt;/p&gt;

&lt;div class=&quot;single_image&quot;&gt;
  &lt;img src=&quot;https://bolotov.dev/blog/img/2026-07-11-foreign-email-law/og-image.png&quot; alt=&quot;Закон об иностранной почте&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-single-image=&quot;&quot; /&gt;
  
&lt;/div&gt;

&lt;h2 id=&quot;что-теперь-нельзя&quot;&gt;Что теперь нельзя?&lt;/h2&gt;

&lt;p&gt;Точнее, что можно:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Российский номер мобильного телефона (звонок/смс-код через оператора связи)&lt;/li&gt;
  &lt;li&gt;ЕСИА — &lt;a href=&quot;https://www.gosuslugi.ru/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;«Госуслуги»&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://ebs.ru/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;ЕБС&lt;/a&gt; — единая биометрическая система&lt;/li&gt;
  &lt;li&gt;«Иная информационная система», принадлежащая гражданину РФ или российскому юрлицу и соответствующая требованиям к защите информации по ст. 16 149-ФЗ. Формально подойдёт любая система с такими признаками — хоть &lt;a href=&quot;https://id.vk.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;VK ID&lt;/a&gt;, &lt;a href=&quot;https://id.yandex.ru/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Яндекс ID&lt;/a&gt; или &lt;a href=&quot;https://id.sber.ru/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Сбер ID&lt;/a&gt;, хоть самописная&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Остальное нельзя. По мнению людей, придумавших эту инициативу, авторизация через Google опаснее, чем «иная информационная система», которую мамкин вайбкодер сделал с помощью гигачата. Спасибо, что думаете о нас, но, может, мы сами как-нибудь разберёмся?&lt;/p&gt;

&lt;h2 id=&quot;кого-это-касается&quot;&gt;Кого это касается&lt;/h2&gt;

&lt;p&gt;Почему-то эту новость иногда выдают за запрет иностранной почты для входа. Некоторые сервисы заранее запретили почту gmail, предложив привязать учётку яндекса или мейл.ру «пока не стало поздно». Хотя я ничего подобного в тексте не нашёл, желание выслужиться или перестраховаться иногда сильнее здравого смысла.&lt;/p&gt;

&lt;h2 id=&quot;почему-национальность-почты--бессмысленное-понятие&quot;&gt;Почему «национальность» почты — бессмысленное понятие&lt;/h2&gt;

&lt;p&gt;Возможно, депутаты не знают, что такое электронная почта и как она работает. Я готовлю серию статей &lt;a href=&quot;https://bolotov.dev/blog/internet&quot;&gt;«Внутри интернета»&lt;/a&gt;, где в десятой мы будем разбираться, как работает email и чем &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vasya@yandex.ru&lt;/code&gt; отличается от &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;petya@gmail.com&lt;/code&gt;, а заодно узнаем, что &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;misha@mishin.site&lt;/code&gt; может обслуживаться хоть в яндексе, хоть в mail.ru, хоть в gmail, хоть вообще на своём личном сервере. И смена «местожительства» почты доступна любому владельцу домена. Поэтому сегодня он русский, завтра эстонский или финский.&lt;/p&gt;

&lt;h2 id=&quot;что-дальше&quot;&gt;Что дальше&lt;/h2&gt;

&lt;p&gt;Я сам старался не пользоваться этими кнопками «Вход с Apple» и «Вход с Google». Мне удобнее просто ввести почту, установить пароль, а потом, когда через год вернусь на этот сайт, менеджер паролей сразу предложит войти, без гаданий, какую из кнопок авторизации я нажимал. Поэтому сейчас, когда нам навязывают вход через Госуслуги или Сбер ID, я всё равно выбрал бы почту. Своя система авторизации всё равно лучше: даже рассылку писем можно настроить самому, например через &lt;a href=&quot;https://yandex.cloud/ru/services/postbox&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Yandex Postbox&lt;/a&gt;. Да, с этим придётся повозиться — разобраться с отправкой писем, настроить домен, — но по итогу можно убрать лишние кнопки и сделать ресурс одинаково удобным для пользователей из любой страны.&lt;/p&gt;

&lt;p&gt;Судя по всему, примерно так авторы закона видят идеальную авторизацию:&lt;/p&gt;

&lt;div class=&quot;image-gallery&quot; data-gallery-id=&quot;gallery-auto&quot;&gt;
  
  
  
  
  &lt;!-- Loading State --&gt;
  &lt;div class=&quot;gallery-loading&quot; data-images-count=&quot;1&quot;&gt;
    &lt;div class=&quot;gallery-loading-content&quot;&gt;
      &lt;div class=&quot;gallery-loading-spinner&quot;&gt;&lt;/div&gt;
      &lt;span class=&quot;gallery-loading-text&quot;&gt;Загрузка изображений...&lt;/span&gt;
      &lt;div class=&quot;gallery-loading-progress&quot;&gt;
        &lt;div class=&quot;gallery-loading-bar&quot;&gt;&lt;/div&gt;
      &lt;/div&gt;
      &lt;div class=&quot;gallery-loading-counter&quot;&gt;
        &lt;span class=&quot;gallery-loaded-count&quot;&gt;0&lt;/span&gt;/&lt;span class=&quot;gallery-total-count&quot;&gt;1&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Gallery Content (hidden by default) --&gt;
  &lt;div class=&quot;gallery-content&quot; style=&quot;opacity: 0; visibility: hidden;&quot;&gt;
    &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;1&quot;&gt;
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-07-11-foreign-email-law/nsis.png&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-07-11-foreign-email-law/nsis@480w.webp 480w, https://bolotov.dev/blog/img/2026-07-11-foreign-email-law/nsis@720w.webp 720w, https://bolotov.dev/blog/img/2026-07-11-foreign-email-law/nsis.png 1206w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;Экран авторизации НСИС с кнопками входа через Госуслуги, Сбер ID, Т-ID, Альфа ID и ВТБ ID&quot; width=&quot;1206&quot; height=&quot;2622&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 1206 / 2622;&quot; /&gt;
        
      
    &lt;/div&gt;
    
    &lt;!-- Navigation controls - always present for galleries --&gt;
    &lt;div class=&quot;gallery-controls&quot;&gt;
      &lt;button class=&quot;gallery-nav gallery-prev&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;prev&quot; aria-label=&quot;Предыдущее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;15 18 9 12 15 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
      &lt;button class=&quot;gallery-nav gallery-next&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;next&quot; aria-label=&quot;Следующее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;9 18 15 12 9 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Fallback for no-JS --&gt;
  &lt;noscript&gt;
    &lt;div class=&quot;gallery-fallback&quot;&gt;
      &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;1&quot;&gt;
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-07-11-foreign-email-law/nsis.png&quot; alt=&quot;Экран авторизации НСИС с кнопками входа через Госуслуги, Сбер ID, Т-ID, Альфа ID и ВТБ ID&quot; width=&quot;1206&quot; height=&quot;2622&quot; style=&quot;aspect-ratio: 1206 / 2622;&quot; /&gt;
          
        
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/noscript&gt;
&lt;/div&gt;
</content>
  </entry>
  
  <entry>
    <title type="html">Как вернуть push-уведомления в Макс на iPhone?</title>
    <link href="https://bolotov.dev/blog/2026/06/17/max-push-iphone" rel="alternate" type="text/html"/>
    <id>https://bolotov.dev/blog/2026/06/17/max-push-iphone</id>
    <published>2026-06-17T00:00:00+00:00</published>
    <updated>2026-06-17T00:00:00+00:00</updated>
    
    


    <category term="notes" label="заметки"/>
    
    <summary type="html">Обходной путь через PWA-версию мессенджера, которую раньше не разрешали запускать с телефона.</summary>
    
    <content type="html" xml:base="https://bolotov.dev/blog/2026/06/17/max-push-iphone">&lt;p&gt;Заходите на &lt;a href=&quot;https://web.max.ru/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;web.max.ru&lt;/a&gt;, нажимайте &lt;strong&gt;Поделиться → Добавить на экран «Домой»&lt;/strong&gt;.&lt;/p&gt;

&lt;div class=&quot;image-gallery&quot; data-gallery-id=&quot;gallery-auto&quot;&gt;
  
  
  
  
  &lt;!-- Loading State --&gt;
  &lt;div class=&quot;gallery-loading&quot; data-images-count=&quot;1&quot;&gt;
    &lt;div class=&quot;gallery-loading-content&quot;&gt;
      &lt;div class=&quot;gallery-loading-spinner&quot;&gt;&lt;/div&gt;
      &lt;span class=&quot;gallery-loading-text&quot;&gt;Загрузка изображений...&lt;/span&gt;
      &lt;div class=&quot;gallery-loading-progress&quot;&gt;
        &lt;div class=&quot;gallery-loading-bar&quot;&gt;&lt;/div&gt;
      &lt;/div&gt;
      &lt;div class=&quot;gallery-loading-counter&quot;&gt;
        &lt;span class=&quot;gallery-loaded-count&quot;&gt;0&lt;/span&gt;/&lt;span class=&quot;gallery-total-count&quot;&gt;1&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Gallery Content (hidden by default) --&gt;
  &lt;div class=&quot;gallery-content&quot; style=&quot;opacity: 0; visibility: hidden;&quot;&gt;
    &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;1&quot;&gt;
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-06-17-max-push-iphone/01.jpeg&quot; alt=&quot;Меню «Поделиться» в Safari с выделенным пунктом «Добавить на экран «Домой»&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease;&quot; /&gt;
        
      
    &lt;/div&gt;
    
    &lt;!-- Navigation controls - always present for galleries --&gt;
    &lt;div class=&quot;gallery-controls&quot;&gt;
      &lt;button class=&quot;gallery-nav gallery-prev&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;prev&quot; aria-label=&quot;Предыдущее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;15 18 9 12 15 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
      &lt;button class=&quot;gallery-nav gallery-next&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;next&quot; aria-label=&quot;Следующее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;9 18 15 12 9 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Fallback for no-JS --&gt;
  &lt;noscript&gt;
    &lt;div class=&quot;gallery-fallback&quot;&gt;
      &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;1&quot;&gt;
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-06-17-max-push-iphone/01.jpeg&quot; alt=&quot;Меню «Поделиться» в Safari с выделенным пунктом «Добавить на экран «Домой»&quot; /&gt;
          
        
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/noscript&gt;
&lt;/div&gt;

&lt;p&gt;Потом запускайте с этого ярлыка PWA-версию Макса (которая раньше была категорически запрещена, ведь в ней вас не получится отслеживать), входите по коду из смс (и, надеюсь, паролю) и разрешайте уведомления.&lt;/p&gt;

&lt;p&gt;Конечно, в идеальном мире &lt;del&gt;это приложение должно быть установлено на отдельном телефоне&lt;/del&gt; этого приложения существовать не должно вообще.&lt;/p&gt;
</content>
  </entry>
  
  <entry>
    <title type="html">appreach.ru: какой регион App Store выбрать после отключения мобильного платежа</title>
    <link href="https://bolotov.dev/blog/2026/06/04/appreach" rel="alternate" type="text/html"/>
    <id>https://bolotov.dev/blog/2026/06/04/appreach</id>
    <published>2026-06-04T00:00:00+00:00</published>
    <updated>2026-06-04T00:00:00+00:00</updated>
    
    


    <category term="webdev" label="webdev"/>
    
    <summary type="html">Сервис appreach.ru покажет, в каких странах доступны нужные вам приложения</summary>
    
    <content type="html" xml:base="https://bolotov.dev/blog/2026/06/04/appreach">&lt;h2 id=&quot;проверка-на-патриотизм&quot;&gt;Проверка на «патриотизм»?&lt;/h2&gt;

&lt;p&gt;После отключения возможности пополнять баланс аккаунта Apple каждый россиянин вынужден принять решение: оставаться с «красным» и нерабочим айклаудом и терпеть или поменять регион и получить новые возможности. Выбор, на мой взгляд, очевиден. Но какой регион выбрать? Вопрос относится к категории религиозных и подход может быть разным:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;«В Турции дешевле!»&lt;/li&gt;
  &lt;li&gt;«У меня есть карта Казахстана!»&lt;/li&gt;
  &lt;li&gt;«Хочу все приложения!»&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Казалось бы, выбираете США и не знаете никаких проблем. Оказалось, это не совсем так.&lt;/p&gt;

&lt;h2 id=&quot;регионы-в-app-store&quot;&gt;Регионы в App Store&lt;/h2&gt;

&lt;p&gt;Конечно, оптимальным вариантом будет выбрать регион, соответствующий вашему проживанию: приложения, которые вам можно использовать, цены в вашей валюте, &lt;del&gt;можно привязать свою карту и без проблем оплачивать iCloud и подписки&lt;/del&gt;. Кроме того, раньше, если у Apple (Google, любого сервиса) возникали сомнения по поводу его корректности, компания могла запросить подтверждающие документы (выписку из банковского счёта или квитанции за квартиру).&lt;/p&gt;

&lt;p&gt;После отмены мобильного платежа Apple разрешила россиянам менять регион, закрывая глаза на фактическое проживание. Возможно, это было самым простым решением, хотя, на мой взгляд, могли разрешить добавлять зарубежные карты или организовать покупку подарочных карт с кодами другим способом. Если вы iOS-разработчик и публикуете приложение, проще выбрать сразу все регионы, вместо того, чтобы выбирать из списка. Если не использовать какое-то особое шифрование, которое может не понравиться Франции, трудностей не будет. Однако некоторые приложения всё же недоступны в определённых регионах. Поэтому — вот мой &lt;a href=&quot;https://appreach.ru/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;новый сервис&lt;/a&gt;, который поможет быстро это проверить.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://bolotov.dev/blog/img/2026-06-04-appreach/appreach-demo.png&quot; alt=&quot;AppReach — доступность приложений по регионам&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Помимо приложений из таблицы можно проверить любое другое — просто вставьте ссылку из App Store и нажмите на кнопку. И это та опция, которая позволила отследить момент, в который из App Store &lt;a href=&quot;https://t.me/bolotovblog/202&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;удалили Макс&lt;/a&gt;. Таблица обновляется раз в час, а ручная проверка показывает результат «здесь и сейчас». Правда, я до последнего был уверен, что это какая-то ошибка в коде.&lt;/p&gt;

&lt;p&gt;Если вы ещё не поменяли регион — в статье &lt;a href=&quot;https://bolotov.dev/blog/2026/05/24/apple-change-region&quot;&gt;меняем регион аккаунта Apple&lt;/a&gt; по шагам разобрал этот процесс.&lt;/p&gt;
</content>
  </entry>
  
  <entry>
    <title type="html">Меняем регион аккаунта Apple</title>
    <link href="https://bolotov.dev/blog/2026/05/24/apple-change-region" rel="alternate" type="text/html"/>
    <id>https://bolotov.dev/blog/2026/05/24/apple-change-region</id>
    <published>2026-05-24T00:00:00+00:00</published>
    <updated>2026-05-24T00:00:00+00:00</updated>
    
    


    <category term="notes" label="заметки"/>
    
    <summary type="html">Семейный доступ не мешает. Разбираемся, как полноценно пользоваться iCloud без мобильного платежа</summary>
    
    <content type="html" xml:base="https://bolotov.dev/blog/2026/05/24/apple-change-region">&lt;p&gt;После того, как в России отключили мобильный платёж для оплаты подписок Apple, поменять регион — разумное решение. Но люди откладывают это, лишаясь полноценной работы iCloud и других сервисов.&lt;/p&gt;

&lt;p&gt;Главный миф: «с семейным аккаунтом поменять регион не получится».&lt;/p&gt;

&lt;h2 id=&quot;что-мешает&quot;&gt;Что мешает&lt;/h2&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Подписка Apple Music.&lt;/strong&gt;&lt;br /&gt;
Если она активна — сначала отменить, потом менять регион.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Деньги на балансе.&lt;/strong&gt;&lt;br /&gt;
Ноль рублей на счёте — обязательное условие. &lt;a href=&quot;https://bolotov.dev/blog/2026/05/04/spend-apple-balance#spend-balance&quot;&gt;Вот пример&lt;/a&gt;, как потратить. Остаток (до 15 рублей) нужно списать через поддержку Apple по телефону &lt;a href=&quot;tel:+74999512579&quot;&gt;+7 499 951-25-79&lt;/a&gt;.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Общие семейные покупки.&lt;/strong&gt;&lt;br /&gt;
Самый неочевидный пункт. Заходим в &lt;strong&gt;Настройки → [Ваше имя] → Семья → Общие покупки&lt;/strong&gt; и отключаем.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class=&quot;image-gallery&quot; data-gallery-id=&quot;gallery-auto&quot;&gt;
  
  
  
  
  &lt;!-- Loading State --&gt;
  &lt;div class=&quot;gallery-loading&quot; data-images-count=&quot;2&quot;&gt;
    &lt;div class=&quot;gallery-loading-content&quot;&gt;
      &lt;div class=&quot;gallery-loading-spinner&quot;&gt;&lt;/div&gt;
      &lt;span class=&quot;gallery-loading-text&quot;&gt;Загрузка изображений...&lt;/span&gt;
      &lt;div class=&quot;gallery-loading-progress&quot;&gt;
        &lt;div class=&quot;gallery-loading-bar&quot;&gt;&lt;/div&gt;
      &lt;/div&gt;
      &lt;div class=&quot;gallery-loading-counter&quot;&gt;
        &lt;span class=&quot;gallery-loaded-count&quot;&gt;0&lt;/span&gt;/&lt;span class=&quot;gallery-total-count&quot;&gt;2&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Gallery Content (hidden by default) --&gt;
  &lt;div class=&quot;gallery-content&quot; style=&quot;opacity: 0; visibility: hidden;&quot;&gt;
    &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;2&quot;&gt;
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-05-24-apple-change-region/01.png&quot; alt=&quot;&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-05-24-apple-change-region/02.png&quot; alt=&quot;&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease;&quot; /&gt;
        
      
    &lt;/div&gt;
    
    &lt;!-- Navigation controls - always present for galleries --&gt;
    &lt;div class=&quot;gallery-controls&quot;&gt;
      &lt;button class=&quot;gallery-nav gallery-prev&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;prev&quot; aria-label=&quot;Предыдущее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;15 18 9 12 15 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
      &lt;button class=&quot;gallery-nav gallery-next&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;next&quot; aria-label=&quot;Следующее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;9 18 15 12 9 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Fallback for no-JS --&gt;
  &lt;noscript&gt;
    &lt;div class=&quot;gallery-fallback&quot;&gt;
      &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;2&quot;&gt;
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-05-24-apple-change-region/01.png&quot; alt=&quot;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-05-24-apple-change-region/02.png&quot; alt=&quot;&quot; /&gt;
          
        
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/noscript&gt;
&lt;/div&gt;

&lt;h2 id=&quot;какой-регион-выбрать&quot;&gt;Какой регион выбрать&lt;/h2&gt;

&lt;p&gt;Универсального ответа нет: не все приложения доступны во всех регионах. Поэтому перед выбором лучше проверить нужные именно вам приложения на &lt;a href=&quot;https://appreach.ru/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;appreach.ru&lt;/a&gt; — &lt;a href=&quot;https://bolotov.dev/blog/2026/06/04/appreach&quot;&gt;моём сервисе&lt;/a&gt;, который покажет, в каких странах они доступны.&lt;/p&gt;

&lt;p&gt;США привлекательны доступностью подарочных карт оплаты и широким выбором приложений и сервисов. Но музыка дорогая!&lt;/p&gt;

&lt;p&gt;В приложении Сбера можно купить подарочные карты для США, ОАЭ, Польши и Турции: &lt;strong&gt;Платежи → Отдых и развлечения&lt;/strong&gt; → ищем &lt;strong&gt;Apple&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Казахстан может показаться удобным, если у вас есть казахстанская карта.&lt;/p&gt;

&lt;p&gt;Турция была привлекательна дешёвыми ценами, но за последние годы стоимость подписок там заметно выросла.&lt;/p&gt;

&lt;h2 id=&quot;меняем-регион&quot;&gt;Меняем регион&lt;/h2&gt;

&lt;p&gt;Открываем &lt;strong&gt;App Store&lt;/strong&gt; и нажимаем на аватарку в правом верхнем углу — попадаем в аккаунт.&lt;/p&gt;

&lt;div class=&quot;image-gallery&quot; data-gallery-id=&quot;gallery-auto&quot;&gt;
  
  
  
  
  &lt;!-- Loading State --&gt;
  &lt;div class=&quot;gallery-loading&quot; data-images-count=&quot;3&quot;&gt;
    &lt;div class=&quot;gallery-loading-content&quot;&gt;
      &lt;div class=&quot;gallery-loading-spinner&quot;&gt;&lt;/div&gt;
      &lt;span class=&quot;gallery-loading-text&quot;&gt;Загрузка изображений...&lt;/span&gt;
      &lt;div class=&quot;gallery-loading-progress&quot;&gt;
        &lt;div class=&quot;gallery-loading-bar&quot;&gt;&lt;/div&gt;
      &lt;/div&gt;
      &lt;div class=&quot;gallery-loading-counter&quot;&gt;
        &lt;span class=&quot;gallery-loaded-count&quot;&gt;0&lt;/span&gt;/&lt;span class=&quot;gallery-total-count&quot;&gt;3&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Gallery Content (hidden by default) --&gt;
  &lt;div class=&quot;gallery-content&quot; style=&quot;opacity: 0; visibility: hidden;&quot;&gt;
    &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;3&quot;&gt;
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-05-24-apple-change-region/03.png&quot; alt=&quot;&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-05-24-apple-change-region/04.png&quot; alt=&quot;&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-05-24-apple-change-region/05.png&quot; alt=&quot;&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease;&quot; /&gt;
        
      
    &lt;/div&gt;
    
    &lt;!-- Navigation controls - always present for galleries --&gt;
    &lt;div class=&quot;gallery-controls&quot;&gt;
      &lt;button class=&quot;gallery-nav gallery-prev&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;prev&quot; aria-label=&quot;Предыдущее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;15 18 9 12 15 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
      &lt;button class=&quot;gallery-nav gallery-next&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;next&quot; aria-label=&quot;Следующее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;9 18 15 12 9 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Fallback for no-JS --&gt;
  &lt;noscript&gt;
    &lt;div class=&quot;gallery-fallback&quot;&gt;
      &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;3&quot;&gt;
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-05-24-apple-change-region/03.png&quot; alt=&quot;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-05-24-apple-change-region/04.png&quot; alt=&quot;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-05-24-apple-change-region/05.png&quot; alt=&quot;&quot; /&gt;
          
        
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/noscript&gt;
&lt;/div&gt;

&lt;p&gt;Нажимаем на своё имя → &lt;strong&gt;Страна/регион&lt;/strong&gt; — там сейчас стоит Россия. Ищем нужную страну, нажимаем &lt;strong&gt;Продолжить&lt;/strong&gt; и принимаем условия.&lt;/p&gt;

&lt;div class=&quot;image-gallery&quot; data-gallery-id=&quot;gallery-auto&quot;&gt;
  
  
  
  
  &lt;!-- Loading State --&gt;
  &lt;div class=&quot;gallery-loading&quot; data-images-count=&quot;3&quot;&gt;
    &lt;div class=&quot;gallery-loading-content&quot;&gt;
      &lt;div class=&quot;gallery-loading-spinner&quot;&gt;&lt;/div&gt;
      &lt;span class=&quot;gallery-loading-text&quot;&gt;Загрузка изображений...&lt;/span&gt;
      &lt;div class=&quot;gallery-loading-progress&quot;&gt;
        &lt;div class=&quot;gallery-loading-bar&quot;&gt;&lt;/div&gt;
      &lt;/div&gt;
      &lt;div class=&quot;gallery-loading-counter&quot;&gt;
        &lt;span class=&quot;gallery-loaded-count&quot;&gt;0&lt;/span&gt;/&lt;span class=&quot;gallery-total-count&quot;&gt;3&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Gallery Content (hidden by default) --&gt;
  &lt;div class=&quot;gallery-content&quot; style=&quot;opacity: 0; visibility: hidden;&quot;&gt;
    &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;3&quot;&gt;
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-05-24-apple-change-region/06.png&quot; alt=&quot;&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-05-24-apple-change-region/07.png&quot; alt=&quot;&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-05-24-apple-change-region/08.png&quot; alt=&quot;&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease;&quot; /&gt;
        
      
    &lt;/div&gt;
    
    &lt;!-- Navigation controls - always present for galleries --&gt;
    &lt;div class=&quot;gallery-controls&quot;&gt;
      &lt;button class=&quot;gallery-nav gallery-prev&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;prev&quot; aria-label=&quot;Предыдущее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;15 18 9 12 15 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
      &lt;button class=&quot;gallery-nav gallery-next&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;next&quot; aria-label=&quot;Следующее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;9 18 15 12 9 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Fallback for no-JS --&gt;
  &lt;noscript&gt;
    &lt;div class=&quot;gallery-fallback&quot;&gt;
      &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;3&quot;&gt;
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-05-24-apple-change-region/06.png&quot; alt=&quot;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-05-24-apple-change-region/07.png&quot; alt=&quot;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-05-24-apple-change-region/08.png&quot; alt=&quot;&quot; /&gt;
          
        
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/noscript&gt;
&lt;/div&gt;

&lt;p&gt;Следующий экран — способ оплаты. Выбираем &lt;strong&gt;Нет&lt;/strong&gt; и заполняем адрес. Адрес должен соответствовать выбранному региону. Если выбрали США, указывайте штат Монтана: там нет налога с продаж, поэтому итоговая цена в App Store будет ровно такой, как написано, без сюрпризов. Улицу можно писать любую — можно взять мой пример, индекс соответствует городу.&lt;/p&gt;

&lt;div class=&quot;image-gallery&quot; data-gallery-id=&quot;gallery-auto&quot;&gt;
  
  
  
  
  &lt;!-- Loading State --&gt;
  &lt;div class=&quot;gallery-loading&quot; data-images-count=&quot;2&quot;&gt;
    &lt;div class=&quot;gallery-loading-content&quot;&gt;
      &lt;div class=&quot;gallery-loading-spinner&quot;&gt;&lt;/div&gt;
      &lt;span class=&quot;gallery-loading-text&quot;&gt;Загрузка изображений...&lt;/span&gt;
      &lt;div class=&quot;gallery-loading-progress&quot;&gt;
        &lt;div class=&quot;gallery-loading-bar&quot;&gt;&lt;/div&gt;
      &lt;/div&gt;
      &lt;div class=&quot;gallery-loading-counter&quot;&gt;
        &lt;span class=&quot;gallery-loaded-count&quot;&gt;0&lt;/span&gt;/&lt;span class=&quot;gallery-total-count&quot;&gt;2&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Gallery Content (hidden by default) --&gt;
  &lt;div class=&quot;gallery-content&quot; style=&quot;opacity: 0; visibility: hidden;&quot;&gt;
    &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;2&quot;&gt;
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-05-24-apple-change-region/09.png&quot; alt=&quot;&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-05-24-apple-change-region/10.png&quot; alt=&quot;&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease;&quot; /&gt;
        
      
    &lt;/div&gt;
    
    &lt;!-- Navigation controls - always present for galleries --&gt;
    &lt;div class=&quot;gallery-controls&quot;&gt;
      &lt;button class=&quot;gallery-nav gallery-prev&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;prev&quot; aria-label=&quot;Предыдущее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;15 18 9 12 15 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
      &lt;button class=&quot;gallery-nav gallery-next&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;next&quot; aria-label=&quot;Следующее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;9 18 15 12 9 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Fallback for no-JS --&gt;
  &lt;noscript&gt;
    &lt;div class=&quot;gallery-fallback&quot;&gt;
      &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;2&quot;&gt;
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-05-24-apple-change-region/09.png&quot; alt=&quot;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-05-24-apple-change-region/10.png&quot; alt=&quot;&quot; /&gt;
          
        
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/noscript&gt;
&lt;/div&gt;

&lt;p&gt;Готово — регион изменён.&lt;/p&gt;

&lt;p&gt;Если пункта «нет» у вас нет, значит какое-то из обязательных условий не соблюдено. Обычно это общие покупки.&lt;/p&gt;

&lt;h2 id=&quot;пополняем-баланс&quot;&gt;Пополняем баланс&lt;/h2&gt;

&lt;p&gt;Самый удобный вариант — подарочная карта через Сбер. Как это сделать читайте в статье про &lt;a href=&quot;https://bolotov.dev/blog/2026/04/05/apple-id-other-region#top-up-balance&quot;&gt;аккаунт Apple другого региона&lt;/a&gt;.&lt;/p&gt;
</content>
  </entry>
  
  <entry>
    <title type="html">Не знаете, куда деть деньги? Отдайте мне</title>
    <link href="https://bolotov.dev/blog/2026/05/04/spend-apple-balance" rel="alternate" type="text/html"/>
    <id>https://bolotov.dev/blog/2026/05/04/spend-apple-balance</id>
    <published>2026-05-04T00:00:00+00:00</published>
    <updated>2026-05-04T00:00:00+00:00</updated>
    
    


    <category term="notes" label="заметки"/>
    
    <summary type="html">Баланс на счёте аккаунта Apple мешает сменить регион. Разбираемся, как его потратить</summary>
    
    <content type="html" xml:base="https://bolotov.dev/blog/2026/05/04/spend-apple-balance">&lt;p&gt;Не так давно Минцифры ввели очередные санкции против россиян — баланс аккаунта Apple больше нельзя пополнять с Билайна и МТС, стоимость подарочных карт для региона Россия взлетела в космос, поэтому люди стали дружно менять платёжные регионы на США, Казахстан, Турцию и Эмираты, бонусом получая возможность скачивать приложения, которые РКН заставил Apple убрать из российского App Store.&lt;/p&gt;

&lt;p&gt;Раньше поменять регион было прямо сложной задачей, особенно, если у вас семейный аккаунт. Сейчас всё стало проще: заходим и меняем, активируем подарочную карту нового региона, купленную в приложении Сбера, заново оформляем подписки. Но это работает только при одном условии: на балансе ноль. Если оплата списывалась напрямую со счёта мобильного, никаких трудностей не возникнет. А если вы заблаговременно пополняли счёт и для следующего платежа денег не хватает, эта статья для вас.&lt;/p&gt;

&lt;h2 id=&quot;деньги-на-балансе&quot;&gt;Деньги на балансе&lt;/h2&gt;

&lt;p&gt;Чтобы узнать остаток средств, зайдите в App Store и нажмите на свою аватарку. Баланс Apple можно тратить только внутри экосистемы: купить приложение, песню, оплатить iCloud. Если деньги «мешают», их можно списать через службу поддержки.&lt;/p&gt;

&lt;p&gt;Бесплатной горячей линии Apple в России больше нет — номер 8-800 отключили. Поэтому заполняем форму &lt;a href=&quot;https://getsupport.apple.com/solutions/call-later&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;getsupport.apple.com&lt;/a&gt; или сразу звоним &lt;a href=&quot;tel:+74999512579&quot;&gt;+7 499 951-25-79&lt;/a&gt;, с 10:00 до 18:45 по московскому времени.&lt;/p&gt;

&lt;p&gt;Если сумма небольшая (например, 10 рублей), служба поддержки обнулит ваш баланс и вы сможете поменять регион. Богатым же придётся сначала потратить баланс до минимума.&lt;/p&gt;

&lt;h2 id=&quot;twinkey-ступени-благодарности&quot;&gt;TwinKey: ступени благодарности&lt;/h2&gt;

&lt;p&gt;В моём приложении &lt;strong&gt;&lt;a href=&quot;https://twinkey.app&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;TwinKey&lt;/a&gt;&lt;/strong&gt; была опция «поддержать разработчика», которая добровольно списывала с баланса 499 рублей. Никаких премиум опций она не добавляла, а повторить платёж можно сколько угодно раз. Решил добавить несколько дополнительных вариантов сумм, чтобы этими платежами можно было приблизить баланс к нулю.&lt;/p&gt;

&lt;p&gt;По правилам Apple разработчики не могут назначать произвольную сумму за покупку. Минимальная стоимость в App Store России — &lt;strong&gt;29 рублей&lt;/strong&gt; (по состоянию на 3 мая 2026). Это ограничение самого магазина, не приложений.&lt;/p&gt;

&lt;p&gt;29 рублей — это много, поэтому я выбрал базовой страной для варианта Light Нигерию — там минимальный порог ниже российского, а конвертация бережно превратила 100 NGN в 15 рублей.&lt;/p&gt;

&lt;p&gt;Так получилось несколько ступеней единоразового платежа: 15, 49, 99 рублей и выше.&lt;/p&gt;

&lt;h2 id=&quot;spend-balance&quot;&gt;Как потратить остаток&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://apps.apple.com/ru/app/id6450484235&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;TwinKey&lt;/a&gt;&lt;/strong&gt; — это моё приложение для двухфакторной аутентификации. Тот же гугл аутентификатор, только отечественный, с дополнительными опциями. Поддерживает любые TOTP-коды, можно даже вход в госуслуги вместо Макса подтверждать им, без интернета вообще. Но если вы только скачали его, нужно добавить какой-нибудь аккаунт. Можете отсканировать этот код (или сделайте скрин и скормите TwinKey — он умеет распознавать коды из сохранённых изображений):&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://bolotov.dev/blog/img/2026-05-04-spend-apple-balance/qr.svg&quot; alt=&quot;QR-код для TwinKey&quot; /&gt;&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Заходим в настройки, выбираем «поддержать разработчика».&lt;/li&gt;
  &lt;li&gt;Выбираем подходящую сумму — такую, чтобы остаток на балансе стал минимальным.&lt;/li&gt;
  &lt;li&gt;Оплачиваем.&lt;/li&gt;
  &lt;li&gt;Возвращаемся к шагу 2, если ещё есть, что списать.&lt;/li&gt;
&lt;/ol&gt;

&lt;div class=&quot;image-gallery&quot; data-gallery-id=&quot;gallery-auto&quot;&gt;
  
  
  
  
  &lt;!-- Loading State --&gt;
  &lt;div class=&quot;gallery-loading&quot; data-images-count=&quot;2&quot;&gt;
    &lt;div class=&quot;gallery-loading-content&quot;&gt;
      &lt;div class=&quot;gallery-loading-spinner&quot;&gt;&lt;/div&gt;
      &lt;span class=&quot;gallery-loading-text&quot;&gt;Загрузка изображений...&lt;/span&gt;
      &lt;div class=&quot;gallery-loading-progress&quot;&gt;
        &lt;div class=&quot;gallery-loading-bar&quot;&gt;&lt;/div&gt;
      &lt;/div&gt;
      &lt;div class=&quot;gallery-loading-counter&quot;&gt;
        &lt;span class=&quot;gallery-loaded-count&quot;&gt;0&lt;/span&gt;/&lt;span class=&quot;gallery-total-count&quot;&gt;2&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Gallery Content (hidden by default) --&gt;
  &lt;div class=&quot;gallery-content&quot; style=&quot;opacity: 0; visibility: hidden;&quot;&gt;
    &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;2&quot;&gt;
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-05-04-spend-apple-balance/support.png&quot; alt=&quot;&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-05-04-spend-apple-balance/large.png&quot; alt=&quot;&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease;&quot; /&gt;
        
      
    &lt;/div&gt;
    
    &lt;!-- Navigation controls - always present for galleries --&gt;
    &lt;div class=&quot;gallery-controls&quot;&gt;
      &lt;button class=&quot;gallery-nav gallery-prev&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;prev&quot; aria-label=&quot;Предыдущее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;15 18 9 12 15 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
      &lt;button class=&quot;gallery-nav gallery-next&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;next&quot; aria-label=&quot;Следующее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;9 18 15 12 9 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Fallback for no-JS --&gt;
  &lt;noscript&gt;
    &lt;div class=&quot;gallery-fallback&quot;&gt;
      &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;2&quot;&gt;
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-05-04-spend-apple-balance/support.png&quot; alt=&quot;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-05-04-spend-apple-balance/large.png&quot; alt=&quot;&quot; /&gt;
          
        
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/noscript&gt;
&lt;/div&gt;

&lt;p&gt;После этого на балансе остаётся сумма, с которой в App Store уже ничего не купишь. Звоним в поддержку Apple — просим списать остаток.&lt;/p&gt;

&lt;p&gt;Теперь баланс пуст, можно &lt;a href=&quot;https://bolotov.dev/blog/2026/05/24/apple-change-region&quot;&gt;менять регион&lt;/a&gt; и пополнять счёт подарочными картами.&lt;/p&gt;
</content>
  </entry>
  
  <entry>
    <title type="html">Аккаунт Apple другого региона: проще, чем кажется</title>
    <link href="https://bolotov.dev/blog/2026/04/05/apple-id-other-region" rel="alternate" type="text/html"/>
    <id>https://bolotov.dev/blog/2026/04/05/apple-id-other-region</id>
    <published>2026-04-05T00:00:00+00:00</published>
    <updated>2026-04-05T00:00:00+00:00</updated>
    
    


    <category term="notes" label="заметки"/>
    
    <summary type="html">Без иностранной eSIM и Авито. Регистрируем учётку Apple для американского App Store.</summary>
    
    <content type="html" xml:base="https://bolotov.dev/blog/2026/04/05/apple-id-other-region">&lt;p&gt;По инициативе Минцифры мобильный платёж в App Store для российских номеров отключили. Карты пополнения для российского региона подорожали в разы. Некоторых приложений в российском App Store никогда не было, а каталог продолжает сокращаться. Разумнее завести отдельный аккаунт Apple того региона, где приложения не исчезают по велению какого-нибудь пенсионера, и пополнять его через подарочные карты.&lt;/p&gt;

&lt;p&gt;Кто-то пишет, что Apple заблокировала регистрацию новых аккаунтов из России. Кто-то советует «чистый» номер телефона, который нигде раньше не использовался. Кто-то учит регистрировать eSIM через зарубежные сервисы. Wylsacom и вовсе предложил покупать готовые аккаунты Apple на Авито. В общем, выглядит всё сложно и делать не хочется.&lt;/p&gt;

&lt;p&gt;Поэтому сейчас я вам покажу, &lt;del&gt;откуда готовилось нападение&lt;/del&gt; как сделать всё самому, буквально за 5 минут.&lt;/p&gt;

&lt;h2 id=&quot;создаём-новый-аккаунт-apple&quot;&gt;Создаём новый аккаунт Apple&lt;/h2&gt;

&lt;p&gt;Открываем &lt;strong&gt;Настройки&lt;/strong&gt;, листаем вниз до раздела &lt;strong&gt;Приложения&lt;/strong&gt;, заходим в &lt;strong&gt;Заметки&lt;/strong&gt; → &lt;strong&gt;Учётные записи Заметок&lt;/strong&gt; → &lt;strong&gt;Добавить учётную запись&lt;/strong&gt; → &lt;strong&gt;iCloud&lt;/strong&gt;.&lt;/p&gt;

&lt;div class=&quot;image-gallery&quot; data-gallery-id=&quot;gallery-auto&quot;&gt;
  
  
  
  
  &lt;!-- Loading State --&gt;
  &lt;div class=&quot;gallery-loading&quot; data-images-count=&quot;4&quot;&gt;
    &lt;div class=&quot;gallery-loading-content&quot;&gt;
      &lt;div class=&quot;gallery-loading-spinner&quot;&gt;&lt;/div&gt;
      &lt;span class=&quot;gallery-loading-text&quot;&gt;Загрузка изображений...&lt;/span&gt;
      &lt;div class=&quot;gallery-loading-progress&quot;&gt;
        &lt;div class=&quot;gallery-loading-bar&quot;&gt;&lt;/div&gt;
      &lt;/div&gt;
      &lt;div class=&quot;gallery-loading-counter&quot;&gt;
        &lt;span class=&quot;gallery-loaded-count&quot;&gt;0&lt;/span&gt;/&lt;span class=&quot;gallery-total-count&quot;&gt;4&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Gallery Content (hidden by default) --&gt;
  &lt;div class=&quot;gallery-content&quot; style=&quot;opacity: 0; visibility: hidden;&quot;&gt;
    &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;4&quot;&gt;
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/01.png&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/01@480w.webp 480w, https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/01.png 603w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 603 / 1311;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/02.png&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/02@480w.webp 480w, https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/02.png 603w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 603 / 1311;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/03.png&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/03@480w.webp 480w, https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/03.png 603w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 603 / 1311;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/04.png&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/04@480w.webp 480w, https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/04.png 603w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 603 / 1311;&quot; /&gt;
        
      
    &lt;/div&gt;
    
    &lt;!-- Navigation controls - always present for galleries --&gt;
    &lt;div class=&quot;gallery-controls&quot;&gt;
      &lt;button class=&quot;gallery-nav gallery-prev&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;prev&quot; aria-label=&quot;Предыдущее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;15 18 9 12 15 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
      &lt;button class=&quot;gallery-nav gallery-next&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;next&quot; aria-label=&quot;Следующее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;9 18 15 12 9 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Fallback for no-JS --&gt;
  &lt;noscript&gt;
    &lt;div class=&quot;gallery-fallback&quot;&gt;
      &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;4&quot;&gt;
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/01.png&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; style=&quot;aspect-ratio: 603 / 1311;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/02.png&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; style=&quot;aspect-ratio: 603 / 1311;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/03.png&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; style=&quot;aspect-ratio: 603 / 1311;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/04.png&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; style=&quot;aspect-ratio: 603 / 1311;&quot; /&gt;
          
        
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/noscript&gt;
&lt;/div&gt;

&lt;p&gt;На экране входа нажимаем &lt;strong&gt;Создать новый аккаунт Apple&lt;/strong&gt; → &lt;strong&gt;Нет адреса электронной почты?&lt;/strong&gt; → &lt;strong&gt;Получить e-mail в iCloud&lt;/strong&gt; — Apple сама создаст адрес вида &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;что-то@icloud.com&lt;/code&gt;.&lt;/p&gt;

&lt;div class=&quot;image-gallery&quot; data-gallery-id=&quot;gallery-auto&quot;&gt;
  
  
  
  
  &lt;!-- Loading State --&gt;
  &lt;div class=&quot;gallery-loading&quot; data-images-count=&quot;3&quot;&gt;
    &lt;div class=&quot;gallery-loading-content&quot;&gt;
      &lt;div class=&quot;gallery-loading-spinner&quot;&gt;&lt;/div&gt;
      &lt;span class=&quot;gallery-loading-text&quot;&gt;Загрузка изображений...&lt;/span&gt;
      &lt;div class=&quot;gallery-loading-progress&quot;&gt;
        &lt;div class=&quot;gallery-loading-bar&quot;&gt;&lt;/div&gt;
      &lt;/div&gt;
      &lt;div class=&quot;gallery-loading-counter&quot;&gt;
        &lt;span class=&quot;gallery-loaded-count&quot;&gt;0&lt;/span&gt;/&lt;span class=&quot;gallery-total-count&quot;&gt;3&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Gallery Content (hidden by default) --&gt;
  &lt;div class=&quot;gallery-content&quot; style=&quot;opacity: 0; visibility: hidden;&quot;&gt;
    &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;3&quot;&gt;
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/05.png&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/05@480w.webp 480w, https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/05.png 603w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 603 / 1311;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/06.png&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/06@480w.webp 480w, https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/06.png 603w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 603 / 1311;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/07.png&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/07@480w.webp 480w, https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/07.png 603w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 603 / 1311;&quot; /&gt;
        
      
    &lt;/div&gt;
    
    &lt;!-- Navigation controls - always present for galleries --&gt;
    &lt;div class=&quot;gallery-controls&quot;&gt;
      &lt;button class=&quot;gallery-nav gallery-prev&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;prev&quot; aria-label=&quot;Предыдущее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;15 18 9 12 15 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
      &lt;button class=&quot;gallery-nav gallery-next&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;next&quot; aria-label=&quot;Следующее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;9 18 15 12 9 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Fallback for no-JS --&gt;
  &lt;noscript&gt;
    &lt;div class=&quot;gallery-fallback&quot;&gt;
      &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;3&quot;&gt;
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/05.png&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; style=&quot;aspect-ratio: 603 / 1311;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/06.png&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; style=&quot;aspect-ratio: 603 / 1311;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/07.png&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; style=&quot;aspect-ratio: 603 / 1311;&quot; /&gt;
          
        
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/noscript&gt;
&lt;/div&gt;

&lt;p&gt;Придумываем пароль. Apple предложит использовать номер телефона, который уже есть — соглашаемся. Принимаем условия.&lt;/p&gt;

&lt;div class=&quot;image-gallery&quot; data-gallery-id=&quot;gallery-auto&quot;&gt;
  
  
  
  
  &lt;!-- Loading State --&gt;
  &lt;div class=&quot;gallery-loading&quot; data-images-count=&quot;3&quot;&gt;
    &lt;div class=&quot;gallery-loading-content&quot;&gt;
      &lt;div class=&quot;gallery-loading-spinner&quot;&gt;&lt;/div&gt;
      &lt;span class=&quot;gallery-loading-text&quot;&gt;Загрузка изображений...&lt;/span&gt;
      &lt;div class=&quot;gallery-loading-progress&quot;&gt;
        &lt;div class=&quot;gallery-loading-bar&quot;&gt;&lt;/div&gt;
      &lt;/div&gt;
      &lt;div class=&quot;gallery-loading-counter&quot;&gt;
        &lt;span class=&quot;gallery-loaded-count&quot;&gt;0&lt;/span&gt;/&lt;span class=&quot;gallery-total-count&quot;&gt;3&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Gallery Content (hidden by default) --&gt;
  &lt;div class=&quot;gallery-content&quot; style=&quot;opacity: 0; visibility: hidden;&quot;&gt;
    &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;3&quot;&gt;
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/08.png&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/08@480w.webp 480w, https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/08.png 603w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 603 / 1311;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/09.png&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/09@480w.webp 480w, https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/09.png 603w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 603 / 1311;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/10.png&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/10@480w.webp 480w, https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/10.png 603w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 603 / 1311;&quot; /&gt;
        
      
    &lt;/div&gt;
    
    &lt;!-- Navigation controls - always present for galleries --&gt;
    &lt;div class=&quot;gallery-controls&quot;&gt;
      &lt;button class=&quot;gallery-nav gallery-prev&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;prev&quot; aria-label=&quot;Предыдущее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;15 18 9 12 15 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
      &lt;button class=&quot;gallery-nav gallery-next&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;next&quot; aria-label=&quot;Следующее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;9 18 15 12 9 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Fallback for no-JS --&gt;
  &lt;noscript&gt;
    &lt;div class=&quot;gallery-fallback&quot;&gt;
      &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;3&quot;&gt;
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/08.png&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; style=&quot;aspect-ratio: 603 / 1311;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/09.png&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; style=&quot;aspect-ratio: 603 / 1311;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/10.png&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; style=&quot;aspect-ratio: 603 / 1311;&quot; /&gt;
          
        
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/noscript&gt;
&lt;/div&gt;

&lt;h2 id=&quot;настраиваем-новый-аккаунт&quot;&gt;Настраиваем новый аккаунт&lt;/h2&gt;

&lt;p&gt;У нас теперь два аккаунта Apple на одном устройстве: основной — для iCloud, Сообщений и всего остального, новый — только для App Store. Чтобы они не мешали друг другу, нужно отключить в новом аккаунте всё лишнее.&lt;/p&gt;

&lt;p&gt;Учётных записей для заметок стало больше на одну. Заходим в неё, открываем &lt;strong&gt;iCloud&lt;/strong&gt; и выключаем все переключатели — Календарь, Контакты, Напоминания. Учётка будет показана как «Неактивна» — так и должно быть.&lt;/p&gt;

&lt;div class=&quot;image-gallery&quot; data-gallery-id=&quot;gallery-auto&quot;&gt;
  
  
  
  
  &lt;!-- Loading State --&gt;
  &lt;div class=&quot;gallery-loading&quot; data-images-count=&quot;4&quot;&gt;
    &lt;div class=&quot;gallery-loading-content&quot;&gt;
      &lt;div class=&quot;gallery-loading-spinner&quot;&gt;&lt;/div&gt;
      &lt;span class=&quot;gallery-loading-text&quot;&gt;Загрузка изображений...&lt;/span&gt;
      &lt;div class=&quot;gallery-loading-progress&quot;&gt;
        &lt;div class=&quot;gallery-loading-bar&quot;&gt;&lt;/div&gt;
      &lt;/div&gt;
      &lt;div class=&quot;gallery-loading-counter&quot;&gt;
        &lt;span class=&quot;gallery-loaded-count&quot;&gt;0&lt;/span&gt;/&lt;span class=&quot;gallery-total-count&quot;&gt;4&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Gallery Content (hidden by default) --&gt;
  &lt;div class=&quot;gallery-content&quot; style=&quot;opacity: 0; visibility: hidden;&quot;&gt;
    &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;4&quot;&gt;
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/11.png&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/11@480w.webp 480w, https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/11.png 603w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 603 / 1311;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/12.png&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/12@480w.webp 480w, https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/12.png 603w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 603 / 1311;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/13.png&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/13@480w.webp 480w, https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/13.png 603w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 603 / 1311;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/14.png&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/14@480w.webp 480w, https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/14.png 603w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 603 / 1311;&quot; /&gt;
        
      
    &lt;/div&gt;
    
    &lt;!-- Navigation controls - always present for galleries --&gt;
    &lt;div class=&quot;gallery-controls&quot;&gt;
      &lt;button class=&quot;gallery-nav gallery-prev&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;prev&quot; aria-label=&quot;Предыдущее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;15 18 9 12 15 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
      &lt;button class=&quot;gallery-nav gallery-next&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;next&quot; aria-label=&quot;Следующее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;9 18 15 12 9 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Fallback for no-JS --&gt;
  &lt;noscript&gt;
    &lt;div class=&quot;gallery-fallback&quot;&gt;
      &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;4&quot;&gt;
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/11.png&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; style=&quot;aspect-ratio: 603 / 1311;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/12.png&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; style=&quot;aspect-ratio: 603 / 1311;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/13.png&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; style=&quot;aspect-ratio: 603 / 1311;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/14.png&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; style=&quot;aspect-ratio: 603 / 1311;&quot; /&gt;
          
        
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/noscript&gt;
&lt;/div&gt;

&lt;h2 id=&quot;переключаем-app-store-на-новый-аккаунт&quot;&gt;Переключаем App Store на новый аккаунт&lt;/h2&gt;

&lt;p&gt;Теперь нужно привязать новый аккаунт Apple к App Store. Это делается через раздел &lt;strong&gt;Контент и покупки&lt;/strong&gt; в настройках основного аккаунта.&lt;/p&gt;

&lt;p&gt;Заходим в &lt;strong&gt;Настройки → [основной аккаунт Apple] → Контент и покупки&lt;/strong&gt; и нажимаем &lt;strong&gt;Выйти&lt;/strong&gt;.&lt;/p&gt;

&lt;div class=&quot;image-gallery&quot; data-gallery-id=&quot;gallery-auto&quot;&gt;
  
  
  
  
  &lt;!-- Loading State --&gt;
  &lt;div class=&quot;gallery-loading&quot; data-images-count=&quot;3&quot;&gt;
    &lt;div class=&quot;gallery-loading-content&quot;&gt;
      &lt;div class=&quot;gallery-loading-spinner&quot;&gt;&lt;/div&gt;
      &lt;span class=&quot;gallery-loading-text&quot;&gt;Загрузка изображений...&lt;/span&gt;
      &lt;div class=&quot;gallery-loading-progress&quot;&gt;
        &lt;div class=&quot;gallery-loading-bar&quot;&gt;&lt;/div&gt;
      &lt;/div&gt;
      &lt;div class=&quot;gallery-loading-counter&quot;&gt;
        &lt;span class=&quot;gallery-loaded-count&quot;&gt;0&lt;/span&gt;/&lt;span class=&quot;gallery-total-count&quot;&gt;3&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Gallery Content (hidden by default) --&gt;
  &lt;div class=&quot;gallery-content&quot; style=&quot;opacity: 0; visibility: hidden;&quot;&gt;
    &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;3&quot;&gt;
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/15.png&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/15@480w.webp 480w, https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/15.png 603w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 603 / 1311;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/16.png&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/16@480w.webp 480w, https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/16.png 603w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 603 / 1311;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/17.png&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/17@480w.webp 480w, https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/17.png 603w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 603 / 1311;&quot; /&gt;
        
      
    &lt;/div&gt;
    
    &lt;!-- Navigation controls - always present for galleries --&gt;
    &lt;div class=&quot;gallery-controls&quot;&gt;
      &lt;button class=&quot;gallery-nav gallery-prev&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;prev&quot; aria-label=&quot;Предыдущее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;15 18 9 12 15 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
      &lt;button class=&quot;gallery-nav gallery-next&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;next&quot; aria-label=&quot;Следующее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;9 18 15 12 9 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Fallback for no-JS --&gt;
  &lt;noscript&gt;
    &lt;div class=&quot;gallery-fallback&quot;&gt;
      &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;3&quot;&gt;
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/15.png&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; style=&quot;aspect-ratio: 603 / 1311;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/16.png&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; style=&quot;aspect-ratio: 603 / 1311;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/17.png&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; style=&quot;aspect-ratio: 603 / 1311;&quot; /&gt;
          
        
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/noscript&gt;
&lt;/div&gt;

&lt;p&gt;Рядом с «Контент и покупки» появится надпись &lt;strong&gt;Настроить&lt;/strong&gt;. Нажимаем, появится вопрос — от чьего имени настраивать. Выбираем &lt;strong&gt;Не [ваше имя]?&lt;/strong&gt; и входим в новый аккаунт.&lt;/p&gt;

&lt;div class=&quot;image-gallery&quot; data-gallery-id=&quot;gallery-auto&quot;&gt;
  
  
  
  
  &lt;!-- Loading State --&gt;
  &lt;div class=&quot;gallery-loading&quot; data-images-count=&quot;4&quot;&gt;
    &lt;div class=&quot;gallery-loading-content&quot;&gt;
      &lt;div class=&quot;gallery-loading-spinner&quot;&gt;&lt;/div&gt;
      &lt;span class=&quot;gallery-loading-text&quot;&gt;Загрузка изображений...&lt;/span&gt;
      &lt;div class=&quot;gallery-loading-progress&quot;&gt;
        &lt;div class=&quot;gallery-loading-bar&quot;&gt;&lt;/div&gt;
      &lt;/div&gt;
      &lt;div class=&quot;gallery-loading-counter&quot;&gt;
        &lt;span class=&quot;gallery-loaded-count&quot;&gt;0&lt;/span&gt;/&lt;span class=&quot;gallery-total-count&quot;&gt;4&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Gallery Content (hidden by default) --&gt;
  &lt;div class=&quot;gallery-content&quot; style=&quot;opacity: 0; visibility: hidden;&quot;&gt;
    &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;4&quot;&gt;
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/18.png&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/18@480w.webp 480w, https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/18.png 603w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 603 / 1311;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/19.png&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/19@480w.webp 480w, https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/19.png 603w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 603 / 1311;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/20.png&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/20@480w.webp 480w, https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/20.png 603w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 603 / 1311;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/21.png&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/21@480w.webp 480w, https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/21.png 603w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 603 / 1311;&quot; /&gt;
        
      
    &lt;/div&gt;
    
    &lt;!-- Navigation controls - always present for galleries --&gt;
    &lt;div class=&quot;gallery-controls&quot;&gt;
      &lt;button class=&quot;gallery-nav gallery-prev&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;prev&quot; aria-label=&quot;Предыдущее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;15 18 9 12 15 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
      &lt;button class=&quot;gallery-nav gallery-next&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;next&quot; aria-label=&quot;Следующее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;9 18 15 12 9 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Fallback for no-JS --&gt;
  &lt;noscript&gt;
    &lt;div class=&quot;gallery-fallback&quot;&gt;
      &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;4&quot;&gt;
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/18.png&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; style=&quot;aspect-ratio: 603 / 1311;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/19.png&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; style=&quot;aspect-ratio: 603 / 1311;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/20.png&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; style=&quot;aspect-ratio: 603 / 1311;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/21.png&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; style=&quot;aspect-ratio: 603 / 1311;&quot; /&gt;
          
        
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/noscript&gt;
&lt;/div&gt;

&lt;h2 id=&quot;меняем-регион-на-сша&quot;&gt;Меняем регион на США&lt;/h2&gt;

&lt;p&gt;Открываем &lt;strong&gt;App Store&lt;/strong&gt; — нас встретит экран приветствия. Нажимаем на аватар в правом верхнем углу и заходим в настройки аккаунта.&lt;/p&gt;

&lt;div class=&quot;image-gallery&quot; data-gallery-id=&quot;gallery-auto&quot;&gt;
  
  
  
  
  &lt;!-- Loading State --&gt;
  &lt;div class=&quot;gallery-loading&quot; data-images-count=&quot;3&quot;&gt;
    &lt;div class=&quot;gallery-loading-content&quot;&gt;
      &lt;div class=&quot;gallery-loading-spinner&quot;&gt;&lt;/div&gt;
      &lt;span class=&quot;gallery-loading-text&quot;&gt;Загрузка изображений...&lt;/span&gt;
      &lt;div class=&quot;gallery-loading-progress&quot;&gt;
        &lt;div class=&quot;gallery-loading-bar&quot;&gt;&lt;/div&gt;
      &lt;/div&gt;
      &lt;div class=&quot;gallery-loading-counter&quot;&gt;
        &lt;span class=&quot;gallery-loaded-count&quot;&gt;0&lt;/span&gt;/&lt;span class=&quot;gallery-total-count&quot;&gt;3&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Gallery Content (hidden by default) --&gt;
  &lt;div class=&quot;gallery-content&quot; style=&quot;opacity: 0; visibility: hidden;&quot;&gt;
    &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;3&quot;&gt;
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/22.png&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/22@480w.webp 480w, https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/22.png 603w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 603 / 1311;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/23.png&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/23@480w.webp 480w, https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/23.png 603w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 603 / 1311;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/24.png&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/24@480w.webp 480w, https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/24.png 603w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 603 / 1311;&quot; /&gt;
        
      
    &lt;/div&gt;
    
    &lt;!-- Navigation controls - always present for galleries --&gt;
    &lt;div class=&quot;gallery-controls&quot;&gt;
      &lt;button class=&quot;gallery-nav gallery-prev&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;prev&quot; aria-label=&quot;Предыдущее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;15 18 9 12 15 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
      &lt;button class=&quot;gallery-nav gallery-next&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;next&quot; aria-label=&quot;Следующее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;9 18 15 12 9 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Fallback for no-JS --&gt;
  &lt;noscript&gt;
    &lt;div class=&quot;gallery-fallback&quot;&gt;
      &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;3&quot;&gt;
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/22.png&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; style=&quot;aspect-ratio: 603 / 1311;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/23.png&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; style=&quot;aspect-ratio: 603 / 1311;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/24.png&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; style=&quot;aspect-ratio: 603 / 1311;&quot; /&gt;
          
        
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/noscript&gt;
&lt;/div&gt;

&lt;p&gt;Нажимаем &lt;strong&gt;Страна/регион&lt;/strong&gt; — там сейчас стоит Россия. Выбираем &lt;strong&gt;Соединённые Штаты&lt;/strong&gt;, читаем предупреждение и принимаем условия использования.&lt;/p&gt;

&lt;div class=&quot;image-gallery&quot; data-gallery-id=&quot;gallery-auto&quot;&gt;
  
  
  
  
  &lt;!-- Loading State --&gt;
  &lt;div class=&quot;gallery-loading&quot; data-images-count=&quot;4&quot;&gt;
    &lt;div class=&quot;gallery-loading-content&quot;&gt;
      &lt;div class=&quot;gallery-loading-spinner&quot;&gt;&lt;/div&gt;
      &lt;span class=&quot;gallery-loading-text&quot;&gt;Загрузка изображений...&lt;/span&gt;
      &lt;div class=&quot;gallery-loading-progress&quot;&gt;
        &lt;div class=&quot;gallery-loading-bar&quot;&gt;&lt;/div&gt;
      &lt;/div&gt;
      &lt;div class=&quot;gallery-loading-counter&quot;&gt;
        &lt;span class=&quot;gallery-loaded-count&quot;&gt;0&lt;/span&gt;/&lt;span class=&quot;gallery-total-count&quot;&gt;4&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Gallery Content (hidden by default) --&gt;
  &lt;div class=&quot;gallery-content&quot; style=&quot;opacity: 0; visibility: hidden;&quot;&gt;
    &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;4&quot;&gt;
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/25.png&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/25@480w.webp 480w, https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/25.png 603w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 603 / 1311;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/26.png&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/26@480w.webp 480w, https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/26.png 603w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 603 / 1311;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/27.png&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/27@480w.webp 480w, https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/27.png 603w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 603 / 1311;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/28.png&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/28@480w.webp 480w, https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/28.png 603w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 603 / 1311;&quot; /&gt;
        
      
    &lt;/div&gt;
    
    &lt;!-- Navigation controls - always present for galleries --&gt;
    &lt;div class=&quot;gallery-controls&quot;&gt;
      &lt;button class=&quot;gallery-nav gallery-prev&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;prev&quot; aria-label=&quot;Предыдущее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;15 18 9 12 15 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
      &lt;button class=&quot;gallery-nav gallery-next&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;next&quot; aria-label=&quot;Следующее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;9 18 15 12 9 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Fallback for no-JS --&gt;
  &lt;noscript&gt;
    &lt;div class=&quot;gallery-fallback&quot;&gt;
      &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;4&quot;&gt;
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/25.png&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; style=&quot;aspect-ratio: 603 / 1311;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/26.png&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; style=&quot;aspect-ratio: 603 / 1311;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/27.png&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; style=&quot;aspect-ratio: 603 / 1311;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/28.png&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; style=&quot;aspect-ratio: 603 / 1311;&quot; /&gt;
          
        
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/noscript&gt;
&lt;/div&gt;

&lt;p&gt;Следующий экран — способ оплаты. Выбираем &lt;strong&gt;Нет&lt;/strong&gt; и заполняем адрес. Адрес должен быть американским — я использую штат Монтана: там нет налога с продаж, поэтому цены в App Store будут ровно такими, как указано. Индекс должен соответствовать городу — его можно найти в интернете или берите мой из примера. Улицу пишите любую, хоть Sovetskaya. При следующем открытии App Store попросит принять обновлённые условия — соглашаемся.&lt;/p&gt;

&lt;div class=&quot;image-gallery&quot; data-gallery-id=&quot;gallery-auto&quot;&gt;
  
  
  
  
  &lt;!-- Loading State --&gt;
  &lt;div class=&quot;gallery-loading&quot; data-images-count=&quot;3&quot;&gt;
    &lt;div class=&quot;gallery-loading-content&quot;&gt;
      &lt;div class=&quot;gallery-loading-spinner&quot;&gt;&lt;/div&gt;
      &lt;span class=&quot;gallery-loading-text&quot;&gt;Загрузка изображений...&lt;/span&gt;
      &lt;div class=&quot;gallery-loading-progress&quot;&gt;
        &lt;div class=&quot;gallery-loading-bar&quot;&gt;&lt;/div&gt;
      &lt;/div&gt;
      &lt;div class=&quot;gallery-loading-counter&quot;&gt;
        &lt;span class=&quot;gallery-loaded-count&quot;&gt;0&lt;/span&gt;/&lt;span class=&quot;gallery-total-count&quot;&gt;3&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Gallery Content (hidden by default) --&gt;
  &lt;div class=&quot;gallery-content&quot; style=&quot;opacity: 0; visibility: hidden;&quot;&gt;
    &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;3&quot;&gt;
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/29.png&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/29@480w.webp 480w, https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/29.png 603w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 603 / 1311;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/30.png&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/30@480w.webp 480w, https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/30.png 603w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 603 / 1311;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/31.png&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/31@480w.webp 480w, https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/31.png 603w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 603 / 1311;&quot; /&gt;
        
      
    &lt;/div&gt;
    
    &lt;!-- Navigation controls - always present for galleries --&gt;
    &lt;div class=&quot;gallery-controls&quot;&gt;
      &lt;button class=&quot;gallery-nav gallery-prev&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;prev&quot; aria-label=&quot;Предыдущее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;15 18 9 12 15 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
      &lt;button class=&quot;gallery-nav gallery-next&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;next&quot; aria-label=&quot;Следующее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;9 18 15 12 9 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Fallback for no-JS --&gt;
  &lt;noscript&gt;
    &lt;div class=&quot;gallery-fallback&quot;&gt;
      &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;3&quot;&gt;
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/29.png&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; style=&quot;aspect-ratio: 603 / 1311;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/30.png&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; style=&quot;aspect-ratio: 603 / 1311;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/31.png&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; style=&quot;aspect-ratio: 603 / 1311;&quot; /&gt;
          
        
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/noscript&gt;
&lt;/div&gt;

&lt;p&gt;Регион изменён. Теперь доступен американский App Store со всеми его приложениями.&lt;/p&gt;

&lt;h2 id=&quot;top-up-balance&quot;&gt;Пополняем баланс&lt;/h2&gt;

&lt;p&gt;Чтобы оплачивать подписки, нужно пополнить счёт подарочной картой. Я показываю на примере США, потому что меня устраивают ассортимент и цены. Можно изучить и другие регионы — например, Турцию или ОАЭ, где цены на приложения и подписки могут отличаться. Но у каждого региона свои ограничения: в Казахстане, например, нет подписки Apple One.&lt;/p&gt;

&lt;p&gt;Код можно купить на разных ресурсах, главное — убедитесь, что он для вашего региона: код для Турции не активируется на американском аккаунте.&lt;/p&gt;

&lt;p&gt;Достаточно удобно это реализовано в приложении Сбера: &lt;strong&gt;Платежи → Отдых и развлечения&lt;/strong&gt;, в поиске пишем &lt;strong&gt;Apple&lt;/strong&gt;. Сбер предлагает карты для ОАЭ, Польши, США, Турции и Франции. Выбираем &lt;strong&gt;Apple Card, США&lt;/strong&gt;, вводим любой живой email, которым пользуетесь — туда придёт письмо с кодом (не тот, что мы только что создали). Выбираем сумму, оплачиваем рублями. Например, подарочная карта на двадцать долларов (что равно подписке ChatGPT) 5 апреля 2026 года обходилась в 1672,55 рублей. Та же подписка, оформленная через сайт днём раньше, обошлась на 200 рублей дороже.&lt;/p&gt;

&lt;div class=&quot;image-gallery&quot; data-gallery-id=&quot;gallery-auto&quot;&gt;
  
  
  
  
  &lt;!-- Loading State --&gt;
  &lt;div class=&quot;gallery-loading&quot; data-images-count=&quot;4&quot;&gt;
    &lt;div class=&quot;gallery-loading-content&quot;&gt;
      &lt;div class=&quot;gallery-loading-spinner&quot;&gt;&lt;/div&gt;
      &lt;span class=&quot;gallery-loading-text&quot;&gt;Загрузка изображений...&lt;/span&gt;
      &lt;div class=&quot;gallery-loading-progress&quot;&gt;
        &lt;div class=&quot;gallery-loading-bar&quot;&gt;&lt;/div&gt;
      &lt;/div&gt;
      &lt;div class=&quot;gallery-loading-counter&quot;&gt;
        &lt;span class=&quot;gallery-loaded-count&quot;&gt;0&lt;/span&gt;/&lt;span class=&quot;gallery-total-count&quot;&gt;4&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Gallery Content (hidden by default) --&gt;
  &lt;div class=&quot;gallery-content&quot; style=&quot;opacity: 0; visibility: hidden;&quot;&gt;
    &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;4&quot;&gt;
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/32.png&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/32@480w.webp 480w, https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/32.png 603w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 603 / 1311;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/33.png&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/33@480w.webp 480w, https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/33.png 603w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 603 / 1311;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/34.png&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/34@480w.webp 480w, https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/34.png 603w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 603 / 1311;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/35.png&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/35@480w.webp 480w, https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/35.png 603w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 603 / 1311;&quot; /&gt;
        
      
    &lt;/div&gt;
    
    &lt;!-- Navigation controls - always present for galleries --&gt;
    &lt;div class=&quot;gallery-controls&quot;&gt;
      &lt;button class=&quot;gallery-nav gallery-prev&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;prev&quot; aria-label=&quot;Предыдущее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;15 18 9 12 15 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
      &lt;button class=&quot;gallery-nav gallery-next&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;next&quot; aria-label=&quot;Следующее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;9 18 15 12 9 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Fallback for no-JS --&gt;
  &lt;noscript&gt;
    &lt;div class=&quot;gallery-fallback&quot;&gt;
      &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;4&quot;&gt;
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/32.png&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; style=&quot;aspect-ratio: 603 / 1311;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/33.png&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; style=&quot;aspect-ratio: 603 / 1311;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/34.png&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; style=&quot;aspect-ratio: 603 / 1311;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/35.png&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; style=&quot;aspect-ratio: 603 / 1311;&quot; /&gt;
          
        
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/noscript&gt;
&lt;/div&gt;

&lt;p&gt;Заходим в App Store, открываем аккаунт, нажимаем &lt;strong&gt;Активировать карту → Ввести код вручную&lt;/strong&gt; и вводим полученный код.&lt;/p&gt;

&lt;div class=&quot;image-gallery&quot; data-gallery-id=&quot;gallery-auto&quot;&gt;
  
  
  
  
  &lt;!-- Loading State --&gt;
  &lt;div class=&quot;gallery-loading&quot; data-images-count=&quot;2&quot;&gt;
    &lt;div class=&quot;gallery-loading-content&quot;&gt;
      &lt;div class=&quot;gallery-loading-spinner&quot;&gt;&lt;/div&gt;
      &lt;span class=&quot;gallery-loading-text&quot;&gt;Загрузка изображений...&lt;/span&gt;
      &lt;div class=&quot;gallery-loading-progress&quot;&gt;
        &lt;div class=&quot;gallery-loading-bar&quot;&gt;&lt;/div&gt;
      &lt;/div&gt;
      &lt;div class=&quot;gallery-loading-counter&quot;&gt;
        &lt;span class=&quot;gallery-loaded-count&quot;&gt;0&lt;/span&gt;/&lt;span class=&quot;gallery-total-count&quot;&gt;2&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Gallery Content (hidden by default) --&gt;
  &lt;div class=&quot;gallery-content&quot; style=&quot;opacity: 0; visibility: hidden;&quot;&gt;
    &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;2&quot;&gt;
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/36.png&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/36@480w.webp 480w, https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/36.png 603w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 603 / 1311;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/37.png&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/37@480w.webp 480w, https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/37.png 603w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 603 / 1311;&quot; /&gt;
        
      
    &lt;/div&gt;
    
    &lt;!-- Navigation controls - always present for galleries --&gt;
    &lt;div class=&quot;gallery-controls&quot;&gt;
      &lt;button class=&quot;gallery-nav gallery-prev&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;prev&quot; aria-label=&quot;Предыдущее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;15 18 9 12 15 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
      &lt;button class=&quot;gallery-nav gallery-next&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;next&quot; aria-label=&quot;Следующее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;9 18 15 12 9 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Fallback for no-JS --&gt;
  &lt;noscript&gt;
    &lt;div class=&quot;gallery-fallback&quot;&gt;
      &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;2&quot;&gt;
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/36.png&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; style=&quot;aspect-ratio: 603 / 1311;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-04-05-apple-id-other-region/37.png&quot; alt=&quot;&quot; width=&quot;603&quot; height=&quot;1311&quot; style=&quot;aspect-ratio: 603 / 1311;&quot; /&gt;
          
        
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/noscript&gt;
&lt;/div&gt;

&lt;p&gt;Готово — баланс пополнен, можно оплачивать подписки.&lt;/p&gt;

&lt;h2 id=&quot;бонус-аккаунт-живёт-в-телефоне&quot;&gt;Бонус: аккаунт живёт в телефоне&lt;/h2&gt;

&lt;p&gt;Поскольку новый аккаунт Apple добавлен на устройство в учётных записях заметок, а не просто введён в App Store, он никуда не исчезнет, даже если вы им не пользуетесь.&lt;/p&gt;

&lt;p&gt;Если вы будете помогать друзьям устанавливать недоступные приложения — коды двухфакторной аутентификации будут приходить push-уведомлением на ваш телефон. Если бы учётка «жила отдельно», коды приходили по СМС или звонку и, если запрашивать их часто, Apple временно приостановит отправку кодов на номер. «Временно» — понятие растяжимое: я однажды ждал больше двух недель.&lt;/p&gt;

&lt;p&gt;Кстати, раньше с одного устройства можно было зарегистрировать не более трёх аккаунтов Apple. Никаких предупреждений не было, система просто сообщала об ошибке, если количество превышено. Причём ни полный сброс настроек, ни перепрошивка ситуацию не меняли. Так что делитесь инструкцией, а не учётками.&lt;/p&gt;
</content>
  </entry>
  
  <entry>
    <title type="html">ГдеЯ? geo-сниппеты для России и мира</title>
    <link href="https://bolotov.dev/blog/2026/03/22/gdeya" rel="alternate" type="text/html"/>
    <id>https://bolotov.dev/blog/2026/03/22/gdeya</id>
    <published>2026-03-22T00:00:00+00:00</published>
    <updated>2026-03-22T00:00:00+00:00</updated>
    
    


    <category term="webdev" label="webdev"/>
    
    <summary type="html">Мой вариант &quot;цензуры&quot; в интернете</summary>
    
    <content type="html" xml:base="https://bolotov.dev/blog/2026/03/22/gdeya">&lt;p&gt;Давайте зайдём на сайт &lt;a href=&quot;https://mid.ru/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Министерства иностранных дел&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://bolotov.dev/blog/img/2026-03-22-gdeya/mid.png&quot; alt=&quot;Скриншот сайта сделан 22.03.2025&quot; /&gt;&lt;/p&gt;

&lt;p&gt;На странице такой серьёзной организации размещены значки некоторых организаций, признанных в России экстремистскими. Чтобы не травмировать вашу психику, я применил к ним эффект стекла (а заодно и к некоторым другим, потому что не знаю, в каком году вы открыли эту статью).&lt;/p&gt;

&lt;p&gt;Размещение некоторых значков считается публичной демонстрацией запрещенной экстремистской символики, а значит может примениться административная ответственность по &lt;a href=&quot;https://www.consultant.ru/document/cons_doc_LAW_34661/e3620d183bd6d1fe2ab8b0c912809857217325a2/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;ст. 20.3 КоАП РФ&lt;/a&gt;. А если может, значит &lt;a href=&quot;https://lenta.ru/news/2026/02/24/rossiyanina-arestovali-za-logotip-instagram/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;уже применяется&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;И вот как быть серьёзной организации, которая работает не только для России? Помимо нашей страны есть и другие, в которых тоже есть интернет. И там свои варианты дозволенного.&lt;/p&gt;

&lt;p&gt;Самый известный пример — GDPR (General Data Protection Regulation), регламент Евросоюза о защите персональных данных. Если ваш сайт посещает пользователь из ЕС, вы обязаны спросить у него разрешение на сбор cookies и объяснить, зачем они нужны. Именно поэтому при первом заходе на европейские сайты вас встречает баннер «Мы используем cookies» — это не прихоть дизайнера, а юридическое требование. Чтобы показывать этот баннер только нужным людям (и не раздражать им всех остальных), сайты и используют геотаргетинг: определяют страну посетителя по IP-адресу и уже тогда решают, что ему показать.&lt;/p&gt;

&lt;p&gt;Та же задача встала передо мной, когда я делал промо-страницу для своего приложения &lt;a href=&quot;https://screenlines.app&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;ScreenLines&lt;/a&gt;. Приложение доступно в &lt;a href=&quot;https://apps.apple.com/app/id6739783494&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;App Store&lt;/a&gt;, а там пользователи со всего мира. На странице я предлагал промокод на премиум-подписку в обмен на видео: нужно было снять, как клеишь защитное стекло на телефон, и выложить в соцсети. Но в какие именно? ВКонтакте понятен российской аудитории и бесполезен для иностранца. Instagram (принадлежит компании Meta, признанной экстремистской организацией и запрещённой в РФ) и X (Twitter) — наоборот, привычны на Западе, но в России заблокированы. Вот тут и пригодился геотаргетинг. Для посетителей из России — одно предложение, для всех остальных — другое.&lt;/p&gt;

&lt;h2 id=&quot;первый-подход-цепочка-сторонних-сервисов&quot;&gt;Первый подход: цепочка сторонних сервисов&lt;/h2&gt;

&lt;p&gt;Есть несколько зарубежных сервисов, которые определяют страну пользователя по IP-адресу провайдера. Проблема в том, что они все с нюансами: одни блокируются расширениями для блокировки рекламы, другие работают с ограничениями на количество запросов.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://ip-api.com&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;ip-api.com&lt;/a&gt; — попадает в чёрные списки блокировщиков рекламы, лимит 45 запросов в минуту с одного IP&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://ipinfo.io&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;ipinfo.io&lt;/a&gt; — лимит 1000 запросов в сутки на бесплатном тарифе (общий на всех с одного IP)&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://ipapi.co&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;ipapi.co&lt;/a&gt; — аналогично в чёрных списках, бесплатный тариф явно запрещён для коммерческого использования&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://ipgeolocation.io&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;ipgeolocation.io&lt;/a&gt; — требует регистрации и API-ключа&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Чтобы не зависеть от одного сервиса, я выстроил цепочку: если первый не ответил — пробуем второй, если и второй не сработал — следующий. Если никто не ответил — на всякий случай показываем российский вариант. Логика разумная, но у неё есть цена: каждый запрос ждёт ответа и только после этого передаёт эстафету следующему. В итоге страница могла подвисать на несколько секунд, пока вся цепочка не отработает.&lt;/p&gt;

&lt;p&gt;Но главная проблема обнаружилась позже. В России на мобильном интернете действуют белые списки: без ограничений открываются только одобренные ресурсы, особенно хорошо работает виртуальный хостинг — провайдеры этот вопрос решают. А вот обращение к зарубежному геосервису, например &lt;a href=&quot;https://ipinfo.io&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;ipinfo.io&lt;/a&gt;, попадает под шейпинг — это когда доступ не заблокирован, но скорость намеренно урезана до минимума. Нам нужно получить буквально несколько байт с геоданными, но из-за шейпинга даже этот крошечный запрос может тянуться секунды.&lt;/p&gt;

&lt;h2 id=&quot;второй-подход-свой-сервис&quot;&gt;Второй подход: свой сервис&lt;/h2&gt;

&lt;p&gt;Я решил сделать иначе. Если вы когда-то настраивали хостинг, то могли видеть галочку «доступ к сайту только из РФ». На мой взгляд, абсурднейший флажок — особенно сейчас, когда люди запросто «оказываются в другой стране», как только просыпаются. Но именно эта механика мне и нужна.&lt;/p&gt;

&lt;p&gt;Идея: разместить на своём хостинге специальный адрес &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/geo/ru&lt;/code&gt; и закрыть к нему доступ для российских IP. Дальше всё просто: скрипт на странице делает один HEAD-запрос на этот адрес — он не скачивает никакого содержимого, только получает код ответа сервера. Если пришёл &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;403&lt;/code&gt; (или любая другая ошибка) — значит, доступ закрыт, пользователь в России. Если &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;200&lt;/code&gt; — пользователь за рубежом.&lt;/p&gt;

&lt;p&gt;CDN — российский, запрос к нему не шейпится, ответ приходит мгновенно. Никаких цепочек, никаких зависимостей от сторонних сервисов.&lt;/p&gt;

&lt;p&gt;Так появился &lt;a href=&quot;https://gdeya.bolotov.dev/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;ГдеЯ&lt;/a&gt; — инструмент с открытым исходным кодом, который доступен на &lt;a href=&quot;https://github.com/artembolotov/gdeya-web&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;GitHub&lt;/a&gt;. Можно использовать мой сервис или развернуть собственный.&lt;/p&gt;

&lt;h2 id=&quot;как-этим-пользоваться&quot;&gt;Как этим пользоваться&lt;/h2&gt;

&lt;p&gt;Вы задаёте два варианта содержимого — один увидят пользователи из России, другой — все остальные. Текст, ссылка, кнопка, целый блок — что угодно.&lt;/p&gt;

&lt;p&gt;Сервис генерирует фрагмент кода, который вы вставляете в нужное место страницы. Оба варианта встроены в страницу в закодированном виде — в исходном коде их не прочитать.&lt;/p&gt;

&lt;p&gt;При загрузке страницы скрипт определяет местоположение пользователя по одному HEAD-запросу. За пределами России — показывается международный вариант. В остальных случаях (ошибка, таймаут, Россия) — российский.&lt;/p&gt;

&lt;p&gt;Вот как выглядит сниппет для строчного элемента — например, ссылки или кнопки:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;span&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;data-gdeya-ru=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;t9G08LCjEOKFt9C1zEzjsuKIR7Taqd8S4I/m37TfqOISUUBCAgkHFg1dRF1A&quot;&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;data-gdeya-intl=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;t9ZDFwNBQAkKBkQNCwRUDRBeExAVClsdH1tYFBAEHhNTXRxVCAlKGBNGVV9UCAgKDQ5EEgzm37TYqODjsuKGt9e0+bGC4I7m0lhKGF8=&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;А вот пункт списка со ссылками на соцсети. Обратите внимание: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-gdeya-ru&lt;/code&gt; пустой — для российских пользователей этот элемент просто не появится:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;li&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;data-gdeya-ru=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&quot;&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;data-gdeya-intl=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;R0RFWUESEBIKBkQGFQBBQw8UDgcKF0MS...&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Содержимое закодировано — в исходном коде страницы его не прочитать. Это важно, если вы, например, прячете иконки заблокированных в России соцсетей: они не присутствуют в HTML явно, а раскодируются и подставляются скриптом уже в браузере.&lt;/p&gt;

&lt;p&gt;Кстати, МИД тоже мог бы воспользоваться &lt;a href=&quot;https://gdeya.bolotov.dev/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;ГдеЯ&lt;/a&gt;. Показывать российским пользователям одно, иностранным — другое. Но, видимо, пока не в курсе.&lt;/p&gt;

</content>
  </entry>
  
  <entry>
    <title type="html">Роутинг для белых списков</title>
    <link href="https://bolotov.dev/blog/2026/02/04/vpn-route" rel="alternate" type="text/html"/>
    <id>https://bolotov.dev/blog/2026/02/04/vpn-route</id>
    <published>2026-02-04T00:00:00+00:00</published>
    <updated>2026-02-04T00:00:00+00:00</updated>
    
    


    <category term="notes" label="заметки"/>
    
    <summary type="html">Делаем сложный интернет проще при использовании VPN</summary>
    
    <content type="html" xml:base="https://bolotov.dev/blog/2026/02/04/vpn-route">&lt;p&gt;Все по-разному пользуются интернетом. Для кого-то, это что-то базовое: посидеть в ВК, скинуть квитанцию в чат садика в Максе, посмотреть погоду в яндексе и вызвать там же такси. Бывают и другие сценарии: например, мне нужен YouTube, нейронки и доступ к зарубежным исследованиям и статьям. Всё это мало возможно без VPN. Заходя на какой-то сайт, нужно либо включать его, либо выключать (поэтому я вообще назначил команду на физическую кнопку в телефоне). Обычно он всё же включён и, заходя на тот же Кинопоиск или в приложение Сбера, для сервиса мы выглядим как гражданин Нидерландов. И в этом есть минусы, поскольку для нас могут убрать какие-то фильмы, а перевод вообще заблокировать. С введением так называемых белых списков, когда не работает ничего кроме сайтов из этого списка (у нас в Костроме этот режим стал постоянным — скорость к ресурсам не из БС режется до 0,2 Мб/с), всё стало ещё неудобнее.&lt;/p&gt;

&lt;h2 id=&quot;решение-1--роутинг-в-приложении&quot;&gt;Решение 1 — роутинг в приложении&lt;/h2&gt;
&lt;p&gt;На телефоне я пользуюсь &lt;a href=&quot;https://apps.apple.com/app/id6450534064&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Streisand&lt;/a&gt;, куда добавляю свою конфигурацию c VLESS + Reality. Есть и другие вариации (к примеру, &lt;a href=&quot;https://apps.apple.com/app/id6476628951&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;v2RayTune&lt;/a&gt; или &lt;a href=&quot;https://apps.apple.com/app/id6446814690&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;v2Box&lt;/a&gt;. Суть и них примерно одинаковая, думаю, движок тоже один. Но именно Streisand мне нравился больше (не добавляет периодически дубликаты профилей, стабильнее находит интернет при смене WiFi на сотовые данные).&lt;/p&gt;

&lt;p&gt;Приложение позволяет создать свой роутинг из правил, которые будут выполняться по очереди, а если не выполнится ничего, трафик пойдёт по стандартному маршруту (через VPN).&lt;/p&gt;

&lt;div class=&quot;image-gallery&quot; data-gallery-id=&quot;gallery-auto&quot;&gt;
  
  
  
  
  &lt;!-- Loading State --&gt;
  &lt;div class=&quot;gallery-loading&quot; data-images-count=&quot;4&quot;&gt;
    &lt;div class=&quot;gallery-loading-content&quot;&gt;
      &lt;div class=&quot;gallery-loading-spinner&quot;&gt;&lt;/div&gt;
      &lt;span class=&quot;gallery-loading-text&quot;&gt;Загрузка изображений...&lt;/span&gt;
      &lt;div class=&quot;gallery-loading-progress&quot;&gt;
        &lt;div class=&quot;gallery-loading-bar&quot;&gt;&lt;/div&gt;
      &lt;/div&gt;
      &lt;div class=&quot;gallery-loading-counter&quot;&gt;
        &lt;span class=&quot;gallery-loaded-count&quot;&gt;0&lt;/span&gt;/&lt;span class=&quot;gallery-total-count&quot;&gt;4&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Gallery Content (hidden by default) --&gt;
  &lt;div class=&quot;gallery-content&quot; style=&quot;opacity: 0; visibility: hidden;&quot;&gt;
    &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;4&quot;&gt;
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-02-04-vpn-route/01.jpg&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-02-04-vpn-route/01@480w.webp 480w, https://bolotov.dev/blog/img/2026-02-04-vpn-route/01@720w.webp 720w, https://bolotov.dev/blog/img/2026-02-04-vpn-route/01.jpg 1177w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;&quot; width=&quot;1177&quot; height=&quot;2560&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 1177 / 2560;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-02-04-vpn-route/02.jpg&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-02-04-vpn-route/02@480w.webp 480w, https://bolotov.dev/blog/img/2026-02-04-vpn-route/02@720w.webp 720w, https://bolotov.dev/blog/img/2026-02-04-vpn-route/02.jpg 1177w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;&quot; width=&quot;1177&quot; height=&quot;2560&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 1177 / 2560;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-02-04-vpn-route/03.jpg&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-02-04-vpn-route/03@480w.webp 480w, https://bolotov.dev/blog/img/2026-02-04-vpn-route/03@720w.webp 720w, https://bolotov.dev/blog/img/2026-02-04-vpn-route/03.jpg 1177w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;&quot; width=&quot;1177&quot; height=&quot;2560&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 1177 / 2560;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-02-04-vpn-route/04.jpg&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-02-04-vpn-route/04@480w.webp 480w, https://bolotov.dev/blog/img/2026-02-04-vpn-route/04@720w.webp 720w, https://bolotov.dev/blog/img/2026-02-04-vpn-route/04.jpg 1177w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;&quot; width=&quot;1177&quot; height=&quot;2560&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 1177 / 2560;&quot; /&gt;
        
      
    &lt;/div&gt;
    
    &lt;!-- Navigation controls - always present for galleries --&gt;
    &lt;div class=&quot;gallery-controls&quot;&gt;
      &lt;button class=&quot;gallery-nav gallery-prev&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;prev&quot; aria-label=&quot;Предыдущее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;15 18 9 12 15 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
      &lt;button class=&quot;gallery-nav gallery-next&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;next&quot; aria-label=&quot;Следующее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;9 18 15 12 9 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Fallback for no-JS --&gt;
  &lt;noscript&gt;
    &lt;div class=&quot;gallery-fallback&quot;&gt;
      &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;4&quot;&gt;
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-02-04-vpn-route/01.jpg&quot; alt=&quot;&quot; width=&quot;1177&quot; height=&quot;2560&quot; style=&quot;aspect-ratio: 1177 / 2560;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-02-04-vpn-route/02.jpg&quot; alt=&quot;&quot; width=&quot;1177&quot; height=&quot;2560&quot; style=&quot;aspect-ratio: 1177 / 2560;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-02-04-vpn-route/03.jpg&quot; alt=&quot;&quot; width=&quot;1177&quot; height=&quot;2560&quot; style=&quot;aspect-ratio: 1177 / 2560;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-02-04-vpn-route/04.jpg&quot; alt=&quot;&quot; width=&quot;1177&quot; height=&quot;2560&quot; style=&quot;aspect-ratio: 1177 / 2560;&quot; /&gt;
          
        
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/noscript&gt;
&lt;/div&gt;

&lt;p&gt;Меня устраивала такая реализация, но хотелось продублировать такую настройку на телефонах друзей и родственников. К счастью, здесь предусмотрена такая опция и маршрутом можно поделиться&lt;/p&gt;

&lt;div class=&quot;language-xml highlighter-rouge&quot;&gt;
  &lt;div class=&quot;highlight wrap-text&quot;&gt;
    &lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;streisand://aW1wb3J0L3JvdXRlOi8vWW5Cc2FYTjBNRERWQVFJREJBVUdCd2dKQ2xSdVlXMWxWSFYxYVdSZVpHOTRZV2x1VTNSeVlYUmxaM2xkWkc5dFlXbHVUV0YwWTJobGNsVnlkV3hsYzFKU1ZWOFFKRGRCUXpneE1qSXdMVEZGTmtNdE5EUXlPQzFDT0VFekxVWXhNVFJGTVVZd01qZERNbGxKVUU5dVJHVnRZVzVrVm1oNVluSnBaS01MUUZEVEJBd05DUTQvVm1SdmJXRnBibHR2ZFhSaWIzVnVaRlJoWjY4UU1BOFFFUklURkJVV0Z4Z1pHaHNjSFI0ZklDRWlJeVFsSmljb0tTb3JMQzB1THpBeE1qTTBOVFkzT0RrNk96dzlQbGd5WjJsekxtTnZiVmN5WjJsekxuSjFXVFEzYm1WM2N5NXlkVnRoYkdaaFltRnVheTV5ZFZ4aGRYUm9MVzV6WkdrdWNuVlhZWFYwYnk1eWRWaGhkbWwwYnk1eWRWaGhkbWwwYnk1emRGNWpaRzR0ZEdsdWEyOW1aaTV5ZFZsalpHNDRkbXN1Y25WWVkybHJjbVl1Y25WWFpIcGxiaTV5ZFZ4bmIzTjFjMngxWjJrdWNuVldaMjkyTG5KMVdHZDFMWE4wTG5KMVcybDZZbWx5YTI5dExuSjFYR3RwYm05d2IybHpheTV5ZFZ4c1pXMWhibUZ3Y204dWNuVlpiRzE5ZFM1MFpXTm9WMjFoYVd3dWNuVldiV0Y0TG5KMVdXMXlZV1I0TG01bGRGVnZheTV5ZFZkdmEyTmtiaTV5VjI5dVpXMWxMbkpYYjNwdmJpNXlkVmh2ZW05dVpTNXlkVnR5WlhNdGJuTmthUzV5ZFZseWRYUjFZbVV1Y25WZGNuVjBkV0psYkdsemRDNXlkVlp5ZW1RdWNuVlhjMkpsY2k1eWRWdHpZbVZ5WW1GdWF5NXlkVlYwTWk1eWRWaDBZbUZ1YXk1eWRWdDFjMlZ5WVhCcExtTnZiVjEyYXkxd2IzSjBZV3d1Ym1WMFZuWnJMbU52YlZWMmF5NXlkVloyZEdJdWNuVlZkMkl1Y25WZWQybHNaR0psY25KcFpYTXVjblZWZWdFdWNuVmFlV0Z1WkdWNExtTnZiVmg1WVc1a1pYZ3VibVYwV1hsaGJtUmxlQzV5ZFZ4NVlYTjBZWFJwWXk1dVpYUmFZbVZsYkdsdVpTNXlkVlprYVhKbFkzVFVCQXhCRFFsQ1RrOVNhWkZyUjBSRlRVZElTVXBMVEUxYmVXOTFkSFZpWlM1amIyMWJhUzU1ZEdsdFp5NWpiMjFiWjNOMFlYUnBZeTVqYjIxZGVYUXpMbWRuY0doMExtTnZiVjhRRDJkdmIyZHNaWFpwWkdWdkxtTnZiVjhRRldSbGRtVnNiM0JsY25NdVoyOXZaMnhsTG1OdmJWOFFGV2R2YjJkc1pYVnlaVzl1YVhSbGNubFZkR1Z1YzI4dVkyOWs=&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;Это прекрасно работало, пока список из доменов не вырос. Теперь при попытке импорта приложение просто вылетает, записывая &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;EXC_BAD_ACCESS (Thread stack size exceeded due to excessive recursion)&lt;/code&gt; в отчёт Panic. Я писал разработчику, подробно описывал ситуацию, скидывал пример, но он так и не понял, что я от него хочу. Поэтому идём дальше.&lt;/p&gt;

&lt;h2 id=&quot;manual&quot;&gt;Решение 2 — роутинг в приложении и файл формата geosite&lt;/h2&gt;

&lt;p&gt;Уважающие себя VPN клиенты (во всех значениях) умеют строить правила опираясь на файлы &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;geosite.dat&lt;/code&gt; и &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;geoip.dat&lt;/code&gt;. Суть такая: пишем вместо домена что-то типа &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;geoip:ru&lt;/code&gt; и все сайты с русским IP-адресом попадают в наше правило. Либо, добавив &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;geosite:google&lt;/code&gt;, получаем набор всех доменов, принадлежащих Google. Конечно, кто-то заботливо собирает эти данные, запаковывает в бинарный формат и выкладывает в открытый доступ. Но поиски такой волшебной &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;geosite:ru-whitelisted&lt;/code&gt; ничего не дали. Более того, нет вообще какого-то вменяемого списка этих ресурсов (Минцифры публиковало новости в формате “добавлено …”). Самая полная информация есть у операторов сотовой связи. Вот это и было взято за основу для формирования своего списка с доменами.&lt;/p&gt;

&lt;p&gt;Создал репозиторий на &lt;a href=&quot;https://github.com/artembolotov/custom-geosite&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;GitHub&lt;/a&gt;, в папке &lt;a href=&quot;https://github.com/artembolotov/custom-geosite/tree/main/lists&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;lists&lt;/a&gt; разместил два текстовых файла &lt;a href=&quot;https://github.com/artembolotov/custom-geosite/blob/main/lists/direct.txt&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;direct.txt&lt;/a&gt; и &lt;a href=&quot;https://github.com/artembolotov/custom-geosite/blob/main/lists/proxy.txt&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;proxy.txt&lt;/a&gt;. При изменении репозитория, они скриптом на Go компилируются в файл custom.dat, который, благодаря GithubPages, доступен по отдельному адресу.&lt;/p&gt;

&lt;p&gt;Теперь правила можно упросить, убрав список доменов. Но сначала добавим геофайл в приложение.&lt;/p&gt;

&lt;p&gt;Заходим в Streisand → Настройки → Роутинг → Настройки → Геофайлы, удаляем преднастроенные источники (свайпом влево), нажимаем сверху Плюс, вставляем &lt;a href=&quot;https://geofiles.bolotov.dev/custom.dat&quot; target=&quot;blank_&quot;&gt;https://geofiles.bolotov.dev/custom.dat&lt;/a&gt;, включаем автообновление.&lt;/p&gt;

&lt;div class=&quot;image-gallery&quot; data-gallery-id=&quot;gallery-auto&quot;&gt;
  
  
  
  
  &lt;!-- Loading State --&gt;
  &lt;div class=&quot;gallery-loading&quot; data-images-count=&quot;2&quot;&gt;
    &lt;div class=&quot;gallery-loading-content&quot;&gt;
      &lt;div class=&quot;gallery-loading-spinner&quot;&gt;&lt;/div&gt;
      &lt;span class=&quot;gallery-loading-text&quot;&gt;Загрузка изображений...&lt;/span&gt;
      &lt;div class=&quot;gallery-loading-progress&quot;&gt;
        &lt;div class=&quot;gallery-loading-bar&quot;&gt;&lt;/div&gt;
      &lt;/div&gt;
      &lt;div class=&quot;gallery-loading-counter&quot;&gt;
        &lt;span class=&quot;gallery-loaded-count&quot;&gt;0&lt;/span&gt;/&lt;span class=&quot;gallery-total-count&quot;&gt;2&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Gallery Content (hidden by default) --&gt;
  &lt;div class=&quot;gallery-content&quot; style=&quot;opacity: 0; visibility: hidden;&quot;&gt;
    &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;2&quot;&gt;
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-02-04-vpn-route/08.jpg&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-02-04-vpn-route/08@480w.webp 480w, https://bolotov.dev/blog/img/2026-02-04-vpn-route/08@720w.webp 720w, https://bolotov.dev/blog/img/2026-02-04-vpn-route/08.jpg 1177w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;&quot; width=&quot;1177&quot; height=&quot;2560&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 1177 / 2560;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-02-04-vpn-route/09.jpg&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-02-04-vpn-route/09@480w.webp 480w, https://bolotov.dev/blog/img/2026-02-04-vpn-route/09@720w.webp 720w, https://bolotov.dev/blog/img/2026-02-04-vpn-route/09.jpg 1177w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;&quot; width=&quot;1177&quot; height=&quot;2560&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 1177 / 2560;&quot; /&gt;
        
      
    &lt;/div&gt;
    
    &lt;!-- Navigation controls - always present for galleries --&gt;
    &lt;div class=&quot;gallery-controls&quot;&gt;
      &lt;button class=&quot;gallery-nav gallery-prev&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;prev&quot; aria-label=&quot;Предыдущее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;15 18 9 12 15 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
      &lt;button class=&quot;gallery-nav gallery-next&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;next&quot; aria-label=&quot;Следующее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;9 18 15 12 9 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Fallback for no-JS --&gt;
  &lt;noscript&gt;
    &lt;div class=&quot;gallery-fallback&quot;&gt;
      &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;2&quot;&gt;
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-02-04-vpn-route/08.jpg&quot; alt=&quot;&quot; width=&quot;1177&quot; height=&quot;2560&quot; style=&quot;aspect-ratio: 1177 / 2560;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-02-04-vpn-route/09.jpg&quot; alt=&quot;&quot; width=&quot;1177&quot; height=&quot;2560&quot; style=&quot;aspect-ratio: 1177 / 2560;&quot; /&gt;
          
        
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/noscript&gt;
&lt;/div&gt;

&lt;p&gt;Теперь правила можно сделать максимально простыми:&lt;/p&gt;

&lt;div class=&quot;image-gallery&quot; data-gallery-id=&quot;gallery-auto&quot;&gt;
  
  
  
  
  &lt;!-- Loading State --&gt;
  &lt;div class=&quot;gallery-loading&quot; data-images-count=&quot;3&quot;&gt;
    &lt;div class=&quot;gallery-loading-content&quot;&gt;
      &lt;div class=&quot;gallery-loading-spinner&quot;&gt;&lt;/div&gt;
      &lt;span class=&quot;gallery-loading-text&quot;&gt;Загрузка изображений...&lt;/span&gt;
      &lt;div class=&quot;gallery-loading-progress&quot;&gt;
        &lt;div class=&quot;gallery-loading-bar&quot;&gt;&lt;/div&gt;
      &lt;/div&gt;
      &lt;div class=&quot;gallery-loading-counter&quot;&gt;
        &lt;span class=&quot;gallery-loaded-count&quot;&gt;0&lt;/span&gt;/&lt;span class=&quot;gallery-total-count&quot;&gt;3&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Gallery Content (hidden by default) --&gt;
  &lt;div class=&quot;gallery-content&quot; style=&quot;opacity: 0; visibility: hidden;&quot;&gt;
    &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;3&quot;&gt;
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-02-04-vpn-route/05.jpg&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-02-04-vpn-route/05@480w.webp 480w, https://bolotov.dev/blog/img/2026-02-04-vpn-route/05@720w.webp 720w, https://bolotov.dev/blog/img/2026-02-04-vpn-route/05.jpg 1177w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;&quot; width=&quot;1177&quot; height=&quot;2560&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 1177 / 2560;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-02-04-vpn-route/06.jpg&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-02-04-vpn-route/06@480w.webp 480w, https://bolotov.dev/blog/img/2026-02-04-vpn-route/06@720w.webp 720w, https://bolotov.dev/blog/img/2026-02-04-vpn-route/06.jpg 1177w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;&quot; width=&quot;1177&quot; height=&quot;2560&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 1177 / 2560;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-02-04-vpn-route/07.jpg&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-02-04-vpn-route/07@480w.webp 480w, https://bolotov.dev/blog/img/2026-02-04-vpn-route/07@720w.webp 720w, https://bolotov.dev/blog/img/2026-02-04-vpn-route/07.jpg 1177w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;&quot; width=&quot;1177&quot; height=&quot;2560&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 1177 / 2560;&quot; /&gt;
        
      
    &lt;/div&gt;
    
    &lt;!-- Navigation controls - always present for galleries --&gt;
    &lt;div class=&quot;gallery-controls&quot;&gt;
      &lt;button class=&quot;gallery-nav gallery-prev&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;prev&quot; aria-label=&quot;Предыдущее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;15 18 9 12 15 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
      &lt;button class=&quot;gallery-nav gallery-next&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;next&quot; aria-label=&quot;Следующее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;9 18 15 12 9 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Fallback for no-JS --&gt;
  &lt;noscript&gt;
    &lt;div class=&quot;gallery-fallback&quot;&gt;
      &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;3&quot;&gt;
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-02-04-vpn-route/05.jpg&quot; alt=&quot;&quot; width=&quot;1177&quot; height=&quot;2560&quot; style=&quot;aspect-ratio: 1177 / 2560;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-02-04-vpn-route/06.jpg&quot; alt=&quot;&quot; width=&quot;1177&quot; height=&quot;2560&quot; style=&quot;aspect-ratio: 1177 / 2560;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-02-04-vpn-route/07.jpg&quot; alt=&quot;&quot; width=&quot;1177&quot; height=&quot;2560&quot; style=&quot;aspect-ratio: 1177 / 2560;&quot; /&gt;
          
        
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/noscript&gt;
&lt;/div&gt;

&lt;p&gt;Заметьте, вместо стандартных &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;IPifNonMatch&lt;/code&gt; (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;IPOnDemand&lt;/code&gt;) мы включили &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AsIS&lt;/code&gt;. Поскольку наш спискок содержит только текстовые домены без IP-адресов, проверять, нужен ли VPN, можно не делая запрос к DNS серверу. Казалось бы милисекунды экономии времени, но если есть проблемы с сетью, это становится критичным.&lt;/p&gt;

&lt;p&gt;Разумеется, новый роутинг можно добавить через кнопку Плюс на &lt;strong&gt;главном&lt;/strong&gt; экране приложения, скормив приложению QR-код со скрина ниже или это:&lt;/p&gt;

&lt;div class=&quot;language-xml highlighter-rouge&quot;&gt;
  &lt;div class=&quot;highlight wrap-text&quot;&gt;
    &lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;streisand://aW1wb3J0L3JvdXRlOi8vWW5Cc2FYTjBNRERWQVFJREJBVUdCd2dKQ2xSdVlXMWxWSFYxYVdSZVpHOXRZV2x1VTNSeVlYUmxaM2xkWkc5dFlXbHVUV0YwWTJobGNsVnlkV3hsYzF0WGFHbDBaV3hwYzNSbFpGOFFKRGt5T1VJeE0wUkZMVUZDUWtNdE5EaEJRaTA0TTBKR0xUQTBOVFpFTXpGQ1JqUkVSVlJCYzBselZtaDVZbkpwWktJTEVkTUVEQTBKRGhCV1pHOXRZV2x1VzI5MWRHSnZkVzVrVkdGbm9ROWZFQlJsZUhRNlkzVnpkRzl0TG1SaGREcHdjbTk0ZVZWd2NtOTRlZE1FREEwSkVoU2hFMThRRldWNGREcGpkWE4wYjIwdVpHRjBPbVJwY21WamRGWmthWEpsWTNRSUV4Z2RMRHBBVEhONGY0S0prSnlldGJ2Q3hOd0FBQUFBQUFBQkFRQUFBQUFBQUFBVkFBQUFBQUFBQUFBQUFBQUFBQUFBNHc9PQ==&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;а затем включив в настройках&lt;/p&gt;

&lt;div class=&quot;image-gallery&quot; data-gallery-id=&quot;gallery-auto&quot;&gt;
  
  
  
  
  &lt;!-- Loading State --&gt;
  &lt;div class=&quot;gallery-loading&quot; data-images-count=&quot;2&quot;&gt;
    &lt;div class=&quot;gallery-loading-content&quot;&gt;
      &lt;div class=&quot;gallery-loading-spinner&quot;&gt;&lt;/div&gt;
      &lt;span class=&quot;gallery-loading-text&quot;&gt;Загрузка изображений...&lt;/span&gt;
      &lt;div class=&quot;gallery-loading-progress&quot;&gt;
        &lt;div class=&quot;gallery-loading-bar&quot;&gt;&lt;/div&gt;
      &lt;/div&gt;
      &lt;div class=&quot;gallery-loading-counter&quot;&gt;
        &lt;span class=&quot;gallery-loaded-count&quot;&gt;0&lt;/span&gt;/&lt;span class=&quot;gallery-total-count&quot;&gt;2&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Gallery Content (hidden by default) --&gt;
  &lt;div class=&quot;gallery-content&quot; style=&quot;opacity: 0; visibility: hidden;&quot;&gt;
    &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;2&quot;&gt;
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-02-04-vpn-route/14.jpg&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-02-04-vpn-route/14@480w.webp 480w, https://bolotov.dev/blog/img/2026-02-04-vpn-route/14@720w.webp 720w, https://bolotov.dev/blog/img/2026-02-04-vpn-route/14.jpg 1177w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;&quot; width=&quot;1177&quot; height=&quot;2560&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 1177 / 2560;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-02-04-vpn-route/10.jpg&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-02-04-vpn-route/10@480w.webp 480w, https://bolotov.dev/blog/img/2026-02-04-vpn-route/10@720w.webp 720w, https://bolotov.dev/blog/img/2026-02-04-vpn-route/10.jpg 1177w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;&quot; width=&quot;1177&quot; height=&quot;2560&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 1177 / 2560;&quot; /&gt;
        
      
    &lt;/div&gt;
    
    &lt;!-- Navigation controls - always present for galleries --&gt;
    &lt;div class=&quot;gallery-controls&quot;&gt;
      &lt;button class=&quot;gallery-nav gallery-prev&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;prev&quot; aria-label=&quot;Предыдущее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;15 18 9 12 15 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
      &lt;button class=&quot;gallery-nav gallery-next&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;next&quot; aria-label=&quot;Следующее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;9 18 15 12 9 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Fallback for no-JS --&gt;
  &lt;noscript&gt;
    &lt;div class=&quot;gallery-fallback&quot;&gt;
      &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;2&quot;&gt;
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-02-04-vpn-route/14.jpg&quot; alt=&quot;&quot; width=&quot;1177&quot; height=&quot;2560&quot; style=&quot;aspect-ratio: 1177 / 2560;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-02-04-vpn-route/10.jpg&quot; alt=&quot;&quot; width=&quot;1177&quot; height=&quot;2560&quot; style=&quot;aspect-ratio: 1177 / 2560;&quot; /&gt;
          
        
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/noscript&gt;
&lt;/div&gt;

&lt;p&gt;Перезапускаем туннель и видим, что для Гугла и Яндекса мы в России, YouTube работает, нейронки тоже.&lt;/p&gt;

&lt;div class=&quot;image-gallery&quot; data-gallery-id=&quot;gallery-auto&quot;&gt;
  
  
  
  
  &lt;!-- Loading State --&gt;
  &lt;div class=&quot;gallery-loading&quot; data-images-count=&quot;3&quot;&gt;
    &lt;div class=&quot;gallery-loading-content&quot;&gt;
      &lt;div class=&quot;gallery-loading-spinner&quot;&gt;&lt;/div&gt;
      &lt;span class=&quot;gallery-loading-text&quot;&gt;Загрузка изображений...&lt;/span&gt;
      &lt;div class=&quot;gallery-loading-progress&quot;&gt;
        &lt;div class=&quot;gallery-loading-bar&quot;&gt;&lt;/div&gt;
      &lt;/div&gt;
      &lt;div class=&quot;gallery-loading-counter&quot;&gt;
        &lt;span class=&quot;gallery-loaded-count&quot;&gt;0&lt;/span&gt;/&lt;span class=&quot;gallery-total-count&quot;&gt;3&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Gallery Content (hidden by default) --&gt;
  &lt;div class=&quot;gallery-content&quot; style=&quot;opacity: 0; visibility: hidden;&quot;&gt;
    &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;3&quot;&gt;
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-02-04-vpn-route/11.jpg&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-02-04-vpn-route/11@480w.webp 480w, https://bolotov.dev/blog/img/2026-02-04-vpn-route/11@720w.webp 720w, https://bolotov.dev/blog/img/2026-02-04-vpn-route/11.jpg 1177w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;&quot; width=&quot;1177&quot; height=&quot;2560&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 1177 / 2560;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-02-04-vpn-route/12.jpg&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-02-04-vpn-route/12@480w.webp 480w, https://bolotov.dev/blog/img/2026-02-04-vpn-route/12@720w.webp 720w, https://bolotov.dev/blog/img/2026-02-04-vpn-route/12.jpg 1177w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;&quot; width=&quot;1177&quot; height=&quot;2560&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 1177 / 2560;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2026-02-04-vpn-route/13.jpg&quot; srcset=&quot;https://bolotov.dev/blog/img/2026-02-04-vpn-route/13@480w.webp 480w, https://bolotov.dev/blog/img/2026-02-04-vpn-route/13@720w.webp 720w, https://bolotov.dev/blog/img/2026-02-04-vpn-route/13.jpg 1177w&quot; sizes=&quot;(max-width: 270px) 85vw, 230px&quot; alt=&quot;&quot; width=&quot;1177&quot; height=&quot;2560&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease; aspect-ratio: 1177 / 2560;&quot; /&gt;
        
      
    &lt;/div&gt;
    
    &lt;!-- Navigation controls - always present for galleries --&gt;
    &lt;div class=&quot;gallery-controls&quot;&gt;
      &lt;button class=&quot;gallery-nav gallery-prev&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;prev&quot; aria-label=&quot;Предыдущее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;15 18 9 12 15 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
      &lt;button class=&quot;gallery-nav gallery-next&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;next&quot; aria-label=&quot;Следующее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;9 18 15 12 9 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Fallback for no-JS --&gt;
  &lt;noscript&gt;
    &lt;div class=&quot;gallery-fallback&quot;&gt;
      &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;3&quot;&gt;
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-02-04-vpn-route/11.jpg&quot; alt=&quot;&quot; width=&quot;1177&quot; height=&quot;2560&quot; style=&quot;aspect-ratio: 1177 / 2560;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-02-04-vpn-route/12.jpg&quot; alt=&quot;&quot; width=&quot;1177&quot; height=&quot;2560&quot; style=&quot;aspect-ratio: 1177 / 2560;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2026-02-04-vpn-route/13.jpg&quot; alt=&quot;&quot; width=&quot;1177&quot; height=&quot;2560&quot; style=&quot;aspect-ratio: 1177 / 2560;&quot; /&gt;
          
        
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/noscript&gt;
&lt;/div&gt;

&lt;p&gt;Что может быть прекраснее этого?&lt;/p&gt;
</content>
  </entry>
  
  <entry>
    <title type="html">О чём не знает iPhone Air</title>
    <link href="https://bolotov.dev/blog/2025/09/11/iphone-air" rel="alternate" type="text/html"/>
    <id>https://bolotov.dev/blog/2025/09/11/iphone-air</id>
    <published>2025-09-11T00:00:00+00:00</published>
    <updated>2025-09-11T00:00:00+00:00</updated>
    
    


    <category term="iosdev" label="iosdev"/>
    
    <summary type="html">И проблемы нового ConcentricRectangle. Почему лично мне хочется поскорее увидеть самый тонкий iPhone.</summary>
    
    <content type="html" xml:base="https://bolotov.dev/blog/2025/09/11/iphone-air">&lt;p&gt;После презентации Apple решил узнать и добавить углы скругления экрана в &lt;a href=&quot;https://screenlines.app&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;ScreenLines&lt;/a&gt; для новых устройств. Установил XCode RC и начал заполнять таблицу.&lt;/p&gt;

&lt;p&gt;Для определения радиуса используем значение скрытой переменной. При публикации в AppStore это нужно убрать, поэтому завернём в &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;#if DEBUG&lt;/code&gt;, чтобы всё само сделалось.&lt;/p&gt;

&lt;div class=&quot;language-swift highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;extension&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UIScreen&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;current&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UIScreen&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;kt&quot;&gt;UIWindow&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;current&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;screen&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    
    &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;deviceCornerRadius&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;CGFloat&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;model&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UIDevice&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;current&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;type&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;radius&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;screenCornerRadius&lt;/span&gt;
        
        &lt;span class=&quot;cp&quot;&gt;#if DEBUG&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;actualRadius&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;forKey&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;_displayCornerRadius&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as?&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;CGFloat&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;
            &lt;span class=&quot;nf&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Model: &lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;, stored: &lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;radius&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;, actual: &lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;actualRadius&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;cp&quot;&gt;#endif&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;radius&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;И получаем такие же углы, как и в iPhone 16 Pro&lt;/p&gt;

&lt;div class=&quot;language-swift highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;extension&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Model&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;screenCornerRadius&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;CGFloat&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;switch&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;c1&quot;&gt;//...&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;iPhone16Pro&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;iPhone16ProMax&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;62.0&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;iPhone17&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;62.0&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;iPhone17Pro&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;62.0&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;iPhone17ProMax&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;62.0&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;iPhoneAir&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;62.0&lt;/span&gt;
            &lt;span class=&quot;c1&quot;&gt;//...&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;//...&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Но запускаем приложение на симуляторе и видим, что углы обрезаны:&lt;/p&gt;

&lt;div class=&quot;single_image&quot;&gt;
  &lt;img src=&quot;https://bolotov.dev/blog/img/2025-09-11-iphone-air/air-corners-problem.png&quot; alt=&quot;iPhone Air corners problem&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-single-image=&quot;&quot; /&gt;
  
&lt;/div&gt;

&lt;p&gt;Проверяем на iPhone 17, 17 Pro и 17 Pro Max — такой проблемы нет. Проблема симулятора? XCode Preview тоже срезает углы. Попробуем новый ConcentricRectangle, представленный в iOS 26, который сам подстраивается под родительский элемент?&lt;/p&gt;

&lt;div class=&quot;single_image&quot;&gt;
  &lt;img src=&quot;https://bolotov.dev/blog/img/2025-09-11-iphone-air/concentric-rectangle.png&quot; alt=&quot;Concentric rectangle проблему не решил&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-single-image=&quot;&quot; /&gt;
  
&lt;/div&gt;

&lt;p&gt;Проблема осталась (фон покрасил в зелёный чтобы лучше видеть линию и отличать от рамок устройства). Кстати, для iPhone 17 Pro и других устройств этот ConcentricRectangle работает на четыре с минусом (линия угла режется):&lt;/p&gt;

&lt;div class=&quot;single_image&quot;&gt;
  &lt;img src=&quot;https://bolotov.dev/blog/img/2025-09-11-iphone-air/сoncentric-rectangle-17-pro.png&quot; alt=&quot;Concentric rectangle на 17 Pro&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-single-image=&quot;&quot; /&gt;
  
&lt;/div&gt;

&lt;p&gt;В итоге просто исправил таблицу вручную, проверив на каждом устройстве, а ConcentricRectangle убрал. Исправят ли проблему к официальному релизу iOS 26? Или на реальном iPhone Air всё будет по-другому?&lt;/p&gt;
</content>
  </entry>
  
  <entry>
    <title type="html">RuStore на iPhone. Был ли прав Билан?</title>
    <link href="https://bolotov.dev/blog/2025/08/31/rustore-ios" rel="alternate" type="text/html"/>
    <id>https://bolotov.dev/blog/2025/08/31/rustore-ios</id>
    <published>2025-08-31T00:00:00+00:00</published>
    <updated>2025-08-31T00:00:00+00:00</updated>
    
    


    <category term="iosdev" label="iosdev"/>
    
    


    <category term="notes" label="заметки"/>
    
    <summary type="html">Как может работать обязательный российский магазин приложений на iOS и что будет с push-уведомлениями</summary>
    
    <content type="html" xml:base="https://bolotov.dev/blog/2025/08/31/rustore-ios">&lt;p&gt;Возможно ли невозможное? С 1 сентября 2025 года в России вступает в силу закон, обязывающий производителей предустанавливать RuStore на все продаваемые устройства, включая iPhone. Как это можно реализовать на закрытой платформе iOS? И что будет с критически важными функциями вроде push-уведомлений?&lt;/p&gt;

&lt;div class=&quot;single_image&quot;&gt;
  &lt;img src=&quot;https://bolotov.dev/blog/img/2025-08-31-rustore-ios/rustore.png&quot; alt=&quot;RuStore на iPhone?&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-single-image=&quot;&quot; /&gt;
  
&lt;/div&gt;

&lt;h2 id=&quot;вода-не-теки&quot;&gt;Вода, не теки&lt;/h2&gt;

&lt;p&gt;Закон, подписанный президентом 7 июля 2025 года, запрещает производителям ограничивать установку приложений российских разработчиков и обязует установить наш отечественный магазин приложений. На бумаге всё выглядит просто: купил iPhone — получил RuStore, скачал спокойно сбербанк и всё остальное.
Но в реальности Apple не поставляет технику в Россию с марта 2022 года. Все iPhone в российских магазинах — это параллельный импорт через третьи страны. Возможен ли в таких условиях диалог?&lt;/p&gt;

&lt;h2 id=&quot;европейский-прецедент&quot;&gt;Европейский прецедент&lt;/h2&gt;

&lt;p&gt;В ЕС похожий закон — Digital Markets Act (DMA) — уже заставил Apple открыть iOS для альтернативных магазинов. С iOS 17.4 европейские пользователи могут устанавливать приложения из сторонних источников, но Apple создала настолько сложную систему ограничений, что CEO Epic Games назвал это «злонамеренным соответствием» (malicious compliance).&lt;/p&gt;

&lt;h2 id=&quot;финансовые-ловушки-apple&quot;&gt;Финансовые ловушки Apple&lt;/h2&gt;

&lt;h3 id=&quot;core-technology-fee--налог-на-успех&quot;&gt;Core Technology Fee — «налог на успех»&lt;/h3&gt;

&lt;p&gt;Apple ввела систему выбора для разработчиков в ЕС:&lt;/p&gt;

&lt;h3 id=&quot;старая-модель-app-store-only&quot;&gt;Старая модель (App Store only):&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;Стандартная комиссия 30%&lt;/li&gt;
  &lt;li&gt;Никаких дополнительных сборов&lt;/li&gt;
  &lt;li&gt;Только распространение через App Store&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;новая-модель-альтернативные-условия&quot;&gt;Новая модель (альтернативные условия):&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;Пониженные комиссии, но плата €0.50 за каждую установку свыше 1 млн в год&lt;/li&gt;
  &lt;li&gt;Доступ к альтернативным магазинам и внешним платежам&lt;/li&gt;
  &lt;li&gt;Пример: приложение с 2 млн пользователей заплатит €500,000 в год дополнительно&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;дополнительные-комиссии-в-новой-модели&quot;&gt;Дополнительные комиссии в новой модели:&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;2% — за привлечение новых пользователей (первые 6 месяцев)&lt;/li&gt;
  &lt;li&gt;5-13% — сервисная комиссия магазина&lt;/li&gt;
  &lt;li&gt;5% — Core Technology Commission за внешние платежи&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Итого в новой модели: до 20% комиссий + €500,000 фиксированно.
Для популярных приложений дешевле остаться на старых условиях с 30% комиссией App Store!&lt;/p&gt;

&lt;h3 id=&quot;барьер-входа-для-магазинов&quot;&gt;Барьер входа для магазинов&lt;/h3&gt;

&lt;p&gt;Чтобы создать альтернативный магазин, нужно ЛИБО:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Депозит €1,000,000 в банке с рейтингом A&lt;/li&gt;
  &lt;li&gt;ИЛИ 2+ года в Apple Developer Program + 1+ млн установок в прошлом году&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;психологические-и-технические-препятствия&quot;&gt;Психологические и технические препятствия&lt;/h2&gt;

&lt;h3 id=&quot;пугающие-предупреждения&quot;&gt;Пугающие предупреждения&lt;/h3&gt;

&lt;p&gt;Apple показывает множественные экраны:&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;⚠️ «Вы можете потерять защиту данных»
⚠️ «Apple не сможет помочь с возвратами»
⚠️ «Возможны проблемы с безопасностью»
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;географические-ограничения&quot;&gt;Географические ограничения&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;Работает только в ЕС — система проверяет геолокацию&lt;/li&gt;
  &lt;li&gt;Apple ID должен быть зарегистрирован в стране ЕС&lt;/li&gt;
  &lt;li&gt;При выезде из ЕС функции могут перестать работать&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;сложная-процедура-установки&quot;&gt;Сложная процедура установки&lt;/h3&gt;

&lt;p&gt;Пользователь должен:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Скачать файл с сайта разработчика&lt;/li&gt;
  &lt;li&gt;Подтвердить установку в Настройках&lt;/li&gt;
  &lt;li&gt;Пройти 3-4 экрана предупреждений&lt;/li&gt;
  &lt;li&gt;Разрешить установку в системных настройках&lt;/li&gt;
  &lt;li&gt;Подтвердить каждое приложение из магазина отдельно&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;скрытый-контроль-остаётся&quot;&gt;Скрытый контроль остаётся&lt;/h3&gt;

&lt;p&gt;Все альтернативные приложения обязательно проходят проверку Apple (Notarization). Apple может заблокировать любое приложение по своему усмотрению, сохраняя фактический контроль над экосистемой.&lt;/p&gt;

&lt;h3 id=&quot;результат-формальное-соответствие-практический-провал&quot;&gt;Результат: формальное соответствие, практический провал&lt;/h3&gt;

&lt;p&gt;В апреле 2025 года Еврокомиссия оштрафовала Apple на €500 млн за несоответствие духу DMA, но технически Apple выполняет букву закона. Альтернативные магазины существуют, но их доля рынка остаётся мизерной из-за искусственных препятствий.&lt;/p&gt;

&lt;h2 id=&quot;rustore-на-iphone-возможные-сценарии&quot;&gt;RuStore на iPhone: возможные сценарии&lt;/h2&gt;

&lt;p&gt;Депутат Антон Горелкин &lt;a href=&quot;https://tass.ru/ekonomika/24849423&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;утверждает&lt;/a&gt;, что российский магазин приложений RuStore может до конца 2025 года стать доступен в App Store и войти в список рекомендуемых к установке приложений при первом запуске устройств компании Apple. Рассмотрим технически возможные варианты:&lt;/p&gt;

&lt;h3 id=&quot;сценарий-1-apple-идёт-на-уступки&quot;&gt;Сценарий 1: Apple идёт на уступки&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;RuStore появляется как веб-приложение (PWA) с иконкой на рабочем столе&lt;/li&gt;
  &lt;li&gt;При первой настройке iPhone предлагает «установить» RuStore&lt;/li&gt;
  &lt;li&gt;Технически это будет закладка в Safari с расширенными возможностями&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;сценарий-2-принудительная-авторизация&quot;&gt;Сценарий 2: Принудительная авторизация&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;Apple создаёт специальный API для российского рынка&lt;/li&gt;
  &lt;li&gt;RuStore получает те же права, что и App Store&lt;/li&gt;
  &lt;li&gt;Приложения из RuStore проходят полную нотаризацию Apple&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Правда пока Apple никогда не создавала исключения для отдельных стран в архитектуре iOS.&lt;/p&gt;

&lt;h3 id=&quot;сценарий-3-россиянам-разрешат-скачивать-и-устанавливать-app-файлы&quot;&gt;Сценарий 3: Россиянам разрешат скачивать и устанавливать .app-файлы.&lt;/h3&gt;

&lt;p&gt;Мне кажется, для реализации такого подхода достаточно где-то «снять галочку», поскольку это прекрасно работает на Mac OS.&lt;/p&gt;

&lt;h2 id=&quot;push-уведомления&quot;&gt;Push-уведомления&lt;/h2&gt;

&lt;h3 id=&quot;нативные-push-уведомления-монополия-apple&quot;&gt;Нативные push-уведомления: монополия Apple&lt;/h3&gt;

&lt;p&gt;Push-уведомления в нативных iOS-приложениях работают исключительно через Apple Push Notification Service (APNS). Для их отправки нужны:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Сертификат разработчика — привязан к Apple Developer Program аккаунту&lt;/li&gt;
  &lt;li&gt;Bundle ID приложения — должен совпадать с зарегистрированным в Apple&lt;/li&gt;
  &lt;li&gt;Действующая подписка Developer Program ($99/год)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;web-push-новые-возможности-pwa&quot;&gt;Web Push: новые возможности PWA&lt;/h3&gt;

&lt;p&gt;В марте 2023 года Apple добавила поддержку Web Push Notifications в iOS 16.4, что открыло новые возможности для веб-приложений:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Работает без аккаунта Apple Developer Program&lt;/li&gt;
  &lt;li&gt;Не привязан к App Store&lt;/li&gt;
  &lt;li&gt;Используется стандарт W3C, совместимый с другими браузерами&lt;/li&gt;
  &lt;li&gt;Может работать даже при заблокированном домене в App Store&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;ограничения-web-push-на-ios&quot;&gt;Ограничения Web Push на iOS:&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;Требует добавления PWA на домашний экран&lt;/li&gt;
  &lt;li&gt;Работает только при явном согласии пользователя&lt;/li&gt;
  &lt;li&gt;Менее надёжная доставка по сравнению с APNS&lt;/li&gt;
  &lt;li&gt;Нет поддержки rich notifications (изображения, кнопки действий)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;В целом приложения в формате PWA могут получать push-уведомления, но с ограничениями по сравнению с нативными приложениями.&lt;/p&gt;

&lt;h3 id=&quot;что-происходит-при-блокировке-аккаунта&quot;&gt;Что происходит при блокировке аккаунта?&lt;/h3&gt;

&lt;p&gt;Вместе с удалением приложения из AppStore, Apple обычно блокирует аккаунт разработчика и отзывает push-сертификаты. Даже если приложение работает, push-уведомления зависят от действующего сертификата в Apple Developer Program. При блокировке аккаунта разработчика:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Останавливается доставка push-уведомлений&lt;/li&gt;
  &lt;li&gt;Приложение не может обновляться&lt;/li&gt;
  &lt;li&gt;Новые пользователи не могут его установить&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Вот почему авито, которое у вас ещё установлено, больше не получает уведомления.&lt;/p&gt;

&lt;h2 id=&quot;технические-препятствия-для-rustore&quot;&gt;Технические препятствия для RuStore&lt;/h2&gt;

&lt;h3 id=&quot;1-архитектура-ios&quot;&gt;1. Архитектура iOS&lt;/h3&gt;

&lt;p&gt;iOS принципиально построена как закрытая система. Каждое приложение должно пройти подпись Apple, иначе система откажется его запускать. Даже в ЕС альтернативные магазины работают через API, предоставленные самой Apple.&lt;/p&gt;

&lt;h3 id=&quot;2-sandboxing-и-права-доступа&quot;&gt;2. Sandboxing и права доступа&lt;/h3&gt;

&lt;p&gt;Приложения в iOS работают в изолированной среде. RuStore не сможет:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Устанавливать другие приложения без участия системы&lt;/li&gt;
  &lt;li&gt;Получать доступ к файлам других приложений&lt;/li&gt;
  &lt;li&gt;Обходить системы безопасности&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;последствия-для-пользователей&quot;&gt;Последствия для пользователей&lt;/h2&gt;

&lt;p&gt;Согласно закону, устройства без поддержки RuStore будут считаться технически некачественными, но прямого запрета на продажу iPhone нет. Это означает:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Покупатели смогут вернуть iPhone как бракованный товар&lt;/li&gt;
  &lt;li&gt;Продавцы рискуют получить претензии от Роспотребнадзора&lt;/li&gt;
  &lt;li&gt;Но сами устройства продолжат поступать через параллельный импорт&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;прогноз-что-получится-на-практике&quot;&gt;Прогноз: что получится на практике&lt;/h2&gt;

&lt;p&gt;Анализируя технические возможности и политические реалии, наиболее вероятным видится сценарий PWA-магазина с ограниченной функциональностью:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;RuStore как PWA — полноценное веб-приложение с возможностью установки на домашний экран&lt;/li&gt;
  &lt;li&gt;Поддержка Web Push — базовые уведомления доступны, но с ограничениями&lt;/li&gt;
  &lt;li&gt;Каталог PWA-приложений — российские разработчики смогут публиковать веб-приложения с push-поддержкой&lt;/li&gt;
  &lt;li&gt;Обход Apple Developer Program — не требуются сертификаты Apple для базовой функциональности&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;преимущества-такого-подхода&quot;&gt;Преимущества такого подхода:&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;Реальная техническая возможность реализации&lt;/li&gt;
  &lt;li&gt;Независимость от решений Apple&lt;/li&gt;
  &lt;li&gt;Кроссплатформенность (работает на Android и iOS)&lt;/li&gt;
  &lt;li&gt;Возможность push-уведомлений через Web Push API&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;недостатки&quot;&gt;Недостатки:&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;Ограниченная интеграция с системой&lt;/li&gt;
  &lt;li&gt;Менее надёжные push-уведомления&lt;/li&gt;
  &lt;li&gt;Необходимость переписывания существующих нативных приложений&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;техническая-реальность-vs-политические-амбиции&quot;&gt;Техническая реальность vs политические амбиции&lt;/h2&gt;

&lt;p&gt;История с RuStore на iPhone — яркий пример того, как политические решения сталкиваются с техническими реалиями. Даже в ЕС, где Apple была вынуждена открыть платформу, альтернативные магазины не смогли серьёзно потеснить App Store.&lt;/p&gt;

&lt;p&gt;В российском случае ситуация ещё сложнее: нет прямого диалога с Apple, нет официальных поставок техники, а требования закона противоречат базовой архитектуре iOS.&lt;/p&gt;

&lt;p&gt;Итог: Опыт ЕС показывает, что RuStore на iPhone скорее всего станет PWA-платформой с искусственными ограничениями. Apple создаст формальное соответствие российскому закону, но с системой препятствий (предупреждения, сложная установка, ограниченная функциональность), которая сделает альтернативный магазин малопривлекательным для массового пользователя.&lt;/p&gt;

&lt;p&gt;Для российских разработчиков это означает необходимость инвестировать в веб-технологии как основную стратегию обхода монополии Apple, принимая при этом ограничения в пользовательском опыте по сравнению с нативными приложениями.&lt;/p&gt;

&lt;h2 id=&quot;зачем-так-сложно&quot;&gt;Зачем так сложно?&lt;/h2&gt;

&lt;p&gt;Ну и главный вопрос: почему на той же MacOS можно спокойно скачать и установить *.app файл и пользоваться им, а на iPhone так же сделать не разрешают? Может для России всё же сделают исключение? Я уже зарегистрировал в RuStore аккаунт (за это не просят $99/год). Всё, что нужно для этого — учётная запись VK. В личном кабинете у вас даже не будет возможности редактировать информацию о себе — всё делается через почту поддержки. Но (на удивление) отвечают они моментально: мой вопрос решили минут за 10, в выходной день. И всё же надеюсь, мы вернёмся в мир, где не нужны ни RuStore, ни «Вжух»&lt;/p&gt;
</content>
  </entry>
  
  <entry>
    <title type="html">ScreenLines: обгоняем Столото и Парковки России. Моё новое приложение</title>
    <link href="https://bolotov.dev/blog/2025/08/10/screenlines" rel="alternate" type="text/html"/>
    <id>https://bolotov.dev/blog/2025/08/10/screenlines</id>
    <published>2025-08-10T00:00:00+00:00</published>
    <updated>2025-08-10T00:00:00+00:00</updated>
    
    


    <category term="iosdev" label="iosdev"/>
    
    


    <category term="notes" label="заметки"/>
    
    <summary type="html">Решаем маленькую проблему и попадаем в топ-40 App Store за 3 дня. В конце статьи секрет.</summary>
    
    <content type="html" xml:base="https://bolotov.dev/blog/2025/08/10/screenlines">&lt;p&gt;4700 скачиваний за три дня. 34 место в топе утилит App Store России. &lt;a href=&quot;https://x.com/ios_dev_alb/status/1956656538841477335&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Приложение недели&lt;/a&gt; в сообществе &lt;a href=&quot;https://x.com/ios_dev_alb/status/1956656538841477335?s=46&amp;amp;t=XX_GDAhHPLfRalDTeluI4w&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Indie iOS Apps&lt;/a&gt;. И всё это — приложение, которое просто показывает белую линию на экране.&lt;/p&gt;

&lt;div class=&quot;image-gallery&quot; data-gallery-id=&quot;gallery-auto&quot;&gt;
  
  
  
  
  &lt;!-- Loading State --&gt;
  &lt;div class=&quot;gallery-loading&quot; data-images-count=&quot;6&quot;&gt;
    &lt;div class=&quot;gallery-loading-content&quot;&gt;
      &lt;div class=&quot;gallery-loading-spinner&quot;&gt;&lt;/div&gt;
      &lt;span class=&quot;gallery-loading-text&quot;&gt;Загрузка изображений...&lt;/span&gt;
      &lt;div class=&quot;gallery-loading-progress&quot;&gt;
        &lt;div class=&quot;gallery-loading-bar&quot;&gt;&lt;/div&gt;
      &lt;/div&gt;
      &lt;div class=&quot;gallery-loading-counter&quot;&gt;
        &lt;span class=&quot;gallery-loaded-count&quot;&gt;0&lt;/span&gt;/&lt;span class=&quot;gallery-total-count&quot;&gt;6&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Gallery Content (hidden by default) --&gt;
  &lt;div class=&quot;gallery-content&quot; style=&quot;opacity: 0; visibility: hidden;&quot;&gt;
    &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;6&quot;&gt;
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2025-08-10-screenlines/ru_0.png&quot; alt=&quot;&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2025-08-10-screenlines/ru_1.png&quot; alt=&quot;&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2025-08-10-screenlines/ru_2.png&quot; alt=&quot;&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2025-08-10-screenlines/ru_3.png&quot; alt=&quot;&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2025-08-10-screenlines/ru_4.png&quot; alt=&quot;&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2025-08-10-screenlines/ru_5.png&quot; alt=&quot;&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease;&quot; /&gt;
        
      
    &lt;/div&gt;
    
    &lt;!-- Navigation controls - always present for galleries --&gt;
    &lt;div class=&quot;gallery-controls&quot;&gt;
      &lt;button class=&quot;gallery-nav gallery-prev&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;prev&quot; aria-label=&quot;Предыдущее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;15 18 9 12 15 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
      &lt;button class=&quot;gallery-nav gallery-next&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;next&quot; aria-label=&quot;Следующее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;9 18 15 12 9 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Fallback for no-JS --&gt;
  &lt;noscript&gt;
    &lt;div class=&quot;gallery-fallback&quot;&gt;
      &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;6&quot;&gt;
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2025-08-10-screenlines/ru_0.png&quot; alt=&quot;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2025-08-10-screenlines/ru_1.png&quot; alt=&quot;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2025-08-10-screenlines/ru_2.png&quot; alt=&quot;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2025-08-10-screenlines/ru_3.png&quot; alt=&quot;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2025-08-10-screenlines/ru_4.png&quot; alt=&quot;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2025-08-10-screenlines/ru_5.png&quot; alt=&quot;&quot; /&gt;
          
        
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/noscript&gt;
&lt;/div&gt;

&lt;h2 id=&quot;простая-и-регулярная-проблема&quot;&gt;Простая и регулярная проблема&lt;/h2&gt;

&lt;p&gt;Приклеить защитное стекло несложно: очищаем экран и ровно размещаем стекло, избегая попадания пыли. Но чёрные рамки стекла и телефона сливаются. Нужен какой-то понятный ориентир. Вам не нужен? Прямо по телефону выравниваете и всё получается отлично? И всё же измерительные приборы придуманы, чтобы убрать элемент случайности и упростить жизнь.&lt;/p&gt;

&lt;p&gt;В сервисных центрах за установку стекла берут от 500 до 2000 рублей, но и там их ставят «на глазок». Некоторые стёкла продаются с аппликаторами. Только зачем платить за одноразовую пластиковую рамку, которая в итоге займёт место в мусорном ведре?&lt;/p&gt;

&lt;h2 id=&quot;от-лайфхака-к-продукту&quot;&gt;От лайфхака к продукту&lt;/h2&gt;

&lt;p&gt;Отдельно раздражало, когда часть изображения просто «съедалась» криво наклеенным стеклом. Поэтому я нарисовал чёрный прямоугольник с белой границей под размер экрана и сохранил в галерею. Приложил стекло так, чтобы линия была видна полностью, прижал. Идеально!&lt;/p&gt;

&lt;p&gt;Проверил идею на друзьях и детях. Даже те, у кого «руки не из того места», смогли наклеить стекло идеально с первого раза. Возможно, главную роль играет скорость — вы сразу видите результат.&lt;/p&gt;

&lt;div class=&quot;single_image&quot;&gt;
  &lt;img src=&quot;https://bolotov.dev/blog/img/2025-08-10-screenlines/veronika.png&quot; alt=&quot;Ребёнок клеит стекло с помощью ScreenLines&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-single-image=&quot;&quot; /&gt;
  
    &lt;em class=&quot;image-caption&quot;&gt;Вероника клеит стёкла сама&lt;/em&gt;
  
&lt;/div&gt;

&lt;p&gt;Учитывая, сколько в семье айфонов, понял, что простой прямоугольник может стать приложением. Но слишком просто, поэтому докрутим до продукта и добавим:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Пошаговую инструкцию с видео-туториалом&lt;/li&gt;
  &lt;li&gt;Систему оценки результата и статистику&lt;/li&gt;
  &lt;li&gt;Весёлое конфетти, если человек доволен результатом&lt;/li&gt;
  &lt;li&gt;Полноценный сайт с калькулятором экономии, политикой конфиденциальности, условиями использования&lt;/li&gt;
  &lt;li&gt;Поддержку всех iPhone и iPad&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Разработка началась в январе 2024 года. Казалось бы, что сложного — нарисовать линию?&lt;/p&gt;

&lt;h2 id=&quot;техническая-кухня&quot;&gt;Техническая кухня&lt;/h2&gt;

&lt;h3 id=&quot;80-моделей--80-радиусов&quot;&gt;80 моделей — 80 радиусов&lt;/h3&gt;

&lt;p&gt;Белая линия должна идеально повторять радиус скругления iPhone. iOS может определить радиус программно, но Apple запрещает использовать приватные методы. Спасибо, что можно получить идентификатор модели: например, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;iPhone17,1&lt;/code&gt; — это iPhone16 Pro, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;iPhone17,5&lt;/code&gt; — iPhone 16e. Для 11 дюймового iPad Air на M3 сразу четыре идентификатора: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;iPad15,3&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;iPad15,4&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;iPad15,5&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;iPad15,6&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Поэтому делаем таблицу радиусов, сопоставляем модели и рисуем. Выглядит она примерно так:&lt;/p&gt;

&lt;div class=&quot;language-swift highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;extension&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Model&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;screenCornerRadius&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;CGFloat&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;switch&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;    
        &lt;span class=&quot;c1&quot;&gt;// ... &lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;iPhone12ProMax&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;iPhone13ProMax&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;iPhone14Plus&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;53.33&lt;/span&gt;
            
        &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;iPhone12Mini&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;iPhone13Mini&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;44.0&lt;/span&gt;
                
        &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;iPhone14Pro&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;iPhone14ProMax&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;iPhone15&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;iPhone15Plus&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;iPhone15Pro&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;iPhone15ProMax&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;iPhone16&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;iPhone16Plus&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;55.0&lt;/span&gt;
            
        &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;iPhone16Pro&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;iPhone16ProMax&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;62.0&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// ... &lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;hls-вместо-встроенного-видео&quot;&gt;HLS вместо встроенного видео&lt;/h3&gt;

&lt;p&gt;Одно из ключевых технических решений в ScreenLines — использование HLS (HTTP Live Streaming) для видеоинструкции вместо обычного встроенного файла.&lt;/p&gt;

&lt;p&gt;HLS — это протокол потокового видео, разработанный Apple. Вместо одного большого файла видео разбивается на множество коротких сегментов (обычно по 6-10 секунд), которые загружаются по мере необходимости. Каждый сегмент — это обычный .ts файл, а .m3u8 содержит список всех сегментов и метаданные.&lt;/p&gt;

&lt;p&gt;Почему не просто файл в приложении?&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Размер&lt;/strong&gt;: 6 МБ вместо 50+&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Гибкость&lt;/strong&gt;: можно обновить видео без релиза&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Локализация&lt;/strong&gt;: разные языки без раздувания&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;HLS разбивает видео на короткие сегменты по 6-10 секунд. Вместо загрузки всего файла AVPlayer подгружает только нужные части:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Сегментация&lt;/strong&gt;: Видео нарезается на мелкие кусочки&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Адаптация&lt;/strong&gt;: Плеер автоматически выбирает качество под скорость сети&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Кэширование&lt;/strong&gt;: Просмотренные сегменты остаются в памяти&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;некоторые-мелочи-недоступные-картинке-с-прямоугольником&quot;&gt;Некоторые мелочи, недоступные картинке с прямоугольником:&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Экран не должен гаснуть&lt;/strong&gt; пока клеишь стекло&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Скрываем статусбар&lt;/strong&gt;, чтобы не отвлекал&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Конфетти за успех&lt;/strong&gt; — радуемся успеху пользователей&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Сохраняем результаты&lt;/strong&gt; и ведём статистику&lt;/li&gt;
&lt;/ul&gt;

&lt;div class=&quot;single_image&quot;&gt;
  &lt;img src=&quot;https://bolotov.dev/blog/img/2025-08-10-screenlines/confetti.png&quot; alt=&quot;&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-single-image=&quot;&quot; /&gt;
  
&lt;/div&gt;

&lt;h2 id=&quot;эволюция-помощников&quot;&gt;Эволюция помощников&lt;/h2&gt;

&lt;p&gt;Проекту нужен сайт. Использовал ChatGPT, DeepSeek, Qwen, Gemini — все изображали бурную деятельность, но результат был так себе.
&lt;a href=&quot;https://claude.ai&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Claude&lt;/a&gt; оказался единственным, кто подходит для работы. Давно купил подписку и сейчас использую везде. Даже на платной версии есть лимиты на количество сообщений, но результат стоит того.&lt;/p&gt;

&lt;p&gt;С енотом, кстати, вышло случайно — просил разные нейросети рисовать что-то абсурдное. ChatGPT предложил такую идею.&lt;/p&gt;

&lt;div class=&quot;image-gallery&quot; data-gallery-id=&quot;gallery-auto&quot;&gt;
  
  
  
  
  &lt;!-- Loading State --&gt;
  &lt;div class=&quot;gallery-loading&quot; data-images-count=&quot;3&quot;&gt;
    &lt;div class=&quot;gallery-loading-content&quot;&gt;
      &lt;div class=&quot;gallery-loading-spinner&quot;&gt;&lt;/div&gt;
      &lt;span class=&quot;gallery-loading-text&quot;&gt;Загрузка изображений...&lt;/span&gt;
      &lt;div class=&quot;gallery-loading-progress&quot;&gt;
        &lt;div class=&quot;gallery-loading-bar&quot;&gt;&lt;/div&gt;
      &lt;/div&gt;
      &lt;div class=&quot;gallery-loading-counter&quot;&gt;
        &lt;span class=&quot;gallery-loaded-count&quot;&gt;0&lt;/span&gt;/&lt;span class=&quot;gallery-total-count&quot;&gt;3&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Gallery Content (hidden by default) --&gt;
  &lt;div class=&quot;gallery-content&quot; style=&quot;opacity: 0; visibility: hidden;&quot;&gt;
    &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;3&quot;&gt;
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2025-08-10-screenlines/racoon_1.png&quot; alt=&quot;&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2025-08-10-screenlines/racoon_2.png&quot; alt=&quot;&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2025-08-10-screenlines/racoon_3.png&quot; alt=&quot;&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease;&quot; /&gt;
        
      
    &lt;/div&gt;
    
    &lt;!-- Navigation controls - always present for galleries --&gt;
    &lt;div class=&quot;gallery-controls&quot;&gt;
      &lt;button class=&quot;gallery-nav gallery-prev&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;prev&quot; aria-label=&quot;Предыдущее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;15 18 9 12 15 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
      &lt;button class=&quot;gallery-nav gallery-next&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;next&quot; aria-label=&quot;Следующее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;9 18 15 12 9 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Fallback for no-JS --&gt;
  &lt;noscript&gt;
    &lt;div class=&quot;gallery-fallback&quot;&gt;
      &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;3&quot;&gt;
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2025-08-10-screenlines/racoon_1.png&quot; alt=&quot;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2025-08-10-screenlines/racoon_2.png&quot; alt=&quot;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2025-08-10-screenlines/racoon_3.png&quot; alt=&quot;&quot; /&gt;
          
        
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/noscript&gt;
&lt;/div&gt;

&lt;h3 id=&quot;калькулятор-экономии&quot;&gt;Калькулятор экономии&lt;/h3&gt;

&lt;p&gt;Claude предложил калькулятор — простую, но интересную штуку. Составили таблицу устройств с ценами и простейшей математикой.&lt;/p&gt;

&lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;devices&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;iPhone 16 Pro&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;serviceCost&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;selfCost&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;700&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;iPhone 16 Pro Max&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;serviceCost&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;selfCost&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;550&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// ... &lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;totalSavings&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;totalServiceCost&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;totalSelfCost&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;результаты-взлёта&quot;&gt;Результаты взлёта&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;4700 скачиваний&lt;/strong&gt; за первые 3 дня&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;34 место&lt;/strong&gt; в топе утилит русского App Store&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Обогнали&lt;/strong&gt; «Парковки России и «Столото»&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;География&lt;/strong&gt;: Россия, Нидерланды, Германия, США&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Оценка&lt;/strong&gt;: 4.9&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Приложение попало в топ за три дня. Без затрат на продвижение.&lt;/p&gt;

&lt;div class=&quot;image-gallery&quot; data-gallery-id=&quot;gallery-auto&quot;&gt;
  
  
  
  
  &lt;!-- Loading State --&gt;
  &lt;div class=&quot;gallery-loading&quot; data-images-count=&quot;4&quot;&gt;
    &lt;div class=&quot;gallery-loading-content&quot;&gt;
      &lt;div class=&quot;gallery-loading-spinner&quot;&gt;&lt;/div&gt;
      &lt;span class=&quot;gallery-loading-text&quot;&gt;Загрузка изображений...&lt;/span&gt;
      &lt;div class=&quot;gallery-loading-progress&quot;&gt;
        &lt;div class=&quot;gallery-loading-bar&quot;&gt;&lt;/div&gt;
      &lt;/div&gt;
      &lt;div class=&quot;gallery-loading-counter&quot;&gt;
        &lt;span class=&quot;gallery-loaded-count&quot;&gt;0&lt;/span&gt;/&lt;span class=&quot;gallery-total-count&quot;&gt;4&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Gallery Content (hidden by default) --&gt;
  &lt;div class=&quot;gallery-content&quot; style=&quot;opacity: 0; visibility: hidden;&quot;&gt;
    &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;4&quot;&gt;
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2025-08-10-screenlines/results_1.png&quot; alt=&quot;&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2025-08-10-screenlines/results_2.png&quot; alt=&quot;&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2025-08-10-screenlines/results_3.png&quot; alt=&quot;&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2025-08-10-screenlines/results_4.png&quot; alt=&quot;&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease;&quot; /&gt;
        
      
    &lt;/div&gt;
    
    &lt;!-- Navigation controls - always present for galleries --&gt;
    &lt;div class=&quot;gallery-controls&quot;&gt;
      &lt;button class=&quot;gallery-nav gallery-prev&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;prev&quot; aria-label=&quot;Предыдущее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;15 18 9 12 15 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
      &lt;button class=&quot;gallery-nav gallery-next&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;next&quot; aria-label=&quot;Следующее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;9 18 15 12 9 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Fallback for no-JS --&gt;
  &lt;noscript&gt;
    &lt;div class=&quot;gallery-fallback&quot;&gt;
      &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;4&quot;&gt;
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2025-08-10-screenlines/results_1.png&quot; alt=&quot;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2025-08-10-screenlines/results_2.png&quot; alt=&quot;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2025-08-10-screenlines/results_3.png&quot; alt=&quot;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2025-08-10-screenlines/results_4.png&quot; alt=&quot;&quot; /&gt;
          
        
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/noscript&gt;
&lt;/div&gt;

&lt;p&gt;А ещё получило звание &lt;a href=&quot;https://x.com/ios_dev_alb/status/1956656538841477335&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Indie App of the Week&lt;/a&gt;&lt;/p&gt;

&lt;div class=&quot;image-gallery&quot; data-gallery-id=&quot;gallery-auto&quot;&gt;
  
  
  
  
  &lt;!-- Loading State --&gt;
  &lt;div class=&quot;gallery-loading&quot; data-images-count=&quot;2&quot;&gt;
    &lt;div class=&quot;gallery-loading-content&quot;&gt;
      &lt;div class=&quot;gallery-loading-spinner&quot;&gt;&lt;/div&gt;
      &lt;span class=&quot;gallery-loading-text&quot;&gt;Загрузка изображений...&lt;/span&gt;
      &lt;div class=&quot;gallery-loading-progress&quot;&gt;
        &lt;div class=&quot;gallery-loading-bar&quot;&gt;&lt;/div&gt;
      &lt;/div&gt;
      &lt;div class=&quot;gallery-loading-counter&quot;&gt;
        &lt;span class=&quot;gallery-loaded-count&quot;&gt;0&lt;/span&gt;/&lt;span class=&quot;gallery-total-count&quot;&gt;2&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Gallery Content (hidden by default) --&gt;
  &lt;div class=&quot;gallery-content&quot; style=&quot;opacity: 0; visibility: hidden;&quot;&gt;
    &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;2&quot;&gt;
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2025-08-10-screenlines/indie_0.png&quot; alt=&quot;&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2025-08-10-screenlines/indie_1.png&quot; alt=&quot;&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease;&quot; /&gt;
        
      
    &lt;/div&gt;
    
    &lt;!-- Navigation controls - always present for galleries --&gt;
    &lt;div class=&quot;gallery-controls&quot;&gt;
      &lt;button class=&quot;gallery-nav gallery-prev&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;prev&quot; aria-label=&quot;Предыдущее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;15 18 9 12 15 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
      &lt;button class=&quot;gallery-nav gallery-next&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;next&quot; aria-label=&quot;Следующее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;9 18 15 12 9 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Fallback for no-JS --&gt;
  &lt;noscript&gt;
    &lt;div class=&quot;gallery-fallback&quot;&gt;
      &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;2&quot;&gt;
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2025-08-10-screenlines/indie_0.png&quot; alt=&quot;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2025-08-10-screenlines/indie_1.png&quot; alt=&quot;&quot; /&gt;
          
        
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/noscript&gt;
&lt;/div&gt;

&lt;h3 id=&quot;отзывы&quot;&gt;Отзывы&lt;/h3&gt;

&lt;p&gt;А теперь оценки пользователей:&lt;/p&gt;

&lt;div class=&quot;image-gallery&quot; data-gallery-id=&quot;gallery-auto&quot;&gt;
  
  
  
  
  &lt;!-- Loading State --&gt;
  &lt;div class=&quot;gallery-loading&quot; data-images-count=&quot;4&quot;&gt;
    &lt;div class=&quot;gallery-loading-content&quot;&gt;
      &lt;div class=&quot;gallery-loading-spinner&quot;&gt;&lt;/div&gt;
      &lt;span class=&quot;gallery-loading-text&quot;&gt;Загрузка изображений...&lt;/span&gt;
      &lt;div class=&quot;gallery-loading-progress&quot;&gt;
        &lt;div class=&quot;gallery-loading-bar&quot;&gt;&lt;/div&gt;
      &lt;/div&gt;
      &lt;div class=&quot;gallery-loading-counter&quot;&gt;
        &lt;span class=&quot;gallery-loaded-count&quot;&gt;0&lt;/span&gt;/&lt;span class=&quot;gallery-total-count&quot;&gt;4&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Gallery Content (hidden by default) --&gt;
  &lt;div class=&quot;gallery-content&quot; style=&quot;opacity: 0; visibility: hidden;&quot;&gt;
    &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;4&quot;&gt;
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2025-08-10-screenlines/reviews_0.png&quot; alt=&quot;&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2025-08-10-screenlines/reviews_1.png&quot; alt=&quot;&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2025-08-10-screenlines/reviews_2.png&quot; alt=&quot;&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2025-08-10-screenlines/reviews_3.png&quot; alt=&quot;&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease;&quot; /&gt;
        
      
    &lt;/div&gt;
    
    &lt;!-- Navigation controls - always present for galleries --&gt;
    &lt;div class=&quot;gallery-controls&quot;&gt;
      &lt;button class=&quot;gallery-nav gallery-prev&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;prev&quot; aria-label=&quot;Предыдущее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;15 18 9 12 15 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
      &lt;button class=&quot;gallery-nav gallery-next&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;next&quot; aria-label=&quot;Следующее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;9 18 15 12 9 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Fallback for no-JS --&gt;
  &lt;noscript&gt;
    &lt;div class=&quot;gallery-fallback&quot;&gt;
      &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;4&quot;&gt;
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2025-08-10-screenlines/reviews_0.png&quot; alt=&quot;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2025-08-10-screenlines/reviews_1.png&quot; alt=&quot;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2025-08-10-screenlines/reviews_2.png&quot; alt=&quot;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2025-08-10-screenlines/reviews_3.png&quot; alt=&quot;&quot; /&gt;
          
        
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/noscript&gt;
&lt;/div&gt;

&lt;h2 id=&quot;что-дальше&quot;&gt;Что дальше?&lt;/h2&gt;

&lt;p&gt;С MacBook и терпением можно создать продукт, который займёт своё место в App Store и будет доступен во всём мире. Да, вам не обязательны дизайнеры, аудиторы и куча бюрократии. Не нужно согласовывать ни с кем ничего. Просто делайте.
ScreenLines показал: в App Store всегда есть место для простых, но полезных решений. Не обязательно создавать сложные платформы — иногда достаточно элегантно решить одну проблему.
Но дело не только в этом. Приложение вряд ли так быстро вылезло в топ, если бы о нём не &lt;a href=&quot;https://t.me/Romancev768/13550&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;рассказал&lt;/a&gt; блогер с аудиторией 788k подписчиков.&lt;/p&gt;

&lt;div class=&quot;image-gallery&quot; data-gallery-id=&quot;gallery-auto&quot;&gt;
  
  
  
  
  &lt;!-- Loading State --&gt;
  &lt;div class=&quot;gallery-loading&quot; data-images-count=&quot;4&quot;&gt;
    &lt;div class=&quot;gallery-loading-content&quot;&gt;
      &lt;div class=&quot;gallery-loading-spinner&quot;&gt;&lt;/div&gt;
      &lt;span class=&quot;gallery-loading-text&quot;&gt;Загрузка изображений...&lt;/span&gt;
      &lt;div class=&quot;gallery-loading-progress&quot;&gt;
        &lt;div class=&quot;gallery-loading-bar&quot;&gt;&lt;/div&gt;
      &lt;/div&gt;
      &lt;div class=&quot;gallery-loading-counter&quot;&gt;
        &lt;span class=&quot;gallery-loaded-count&quot;&gt;0&lt;/span&gt;/&lt;span class=&quot;gallery-total-count&quot;&gt;4&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Gallery Content (hidden by default) --&gt;
  &lt;div class=&quot;gallery-content&quot; style=&quot;opacity: 0; visibility: hidden;&quot;&gt;
    &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;4&quot;&gt;
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2025-08-10-screenlines/marketing_1.jpg&quot; alt=&quot;&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2025-08-10-screenlines/marketing_2.png&quot; alt=&quot;&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2025-08-10-screenlines/marketing_3.png&quot; alt=&quot;&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2025-08-10-screenlines/marketing_4.png&quot; alt=&quot;&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease;&quot; /&gt;
        
      
    &lt;/div&gt;
    
    &lt;!-- Navigation controls - always present for galleries --&gt;
    &lt;div class=&quot;gallery-controls&quot;&gt;
      &lt;button class=&quot;gallery-nav gallery-prev&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;prev&quot; aria-label=&quot;Предыдущее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;15 18 9 12 15 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
      &lt;button class=&quot;gallery-nav gallery-next&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;next&quot; aria-label=&quot;Следующее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;9 18 15 12 9 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Fallback for no-JS --&gt;
  &lt;noscript&gt;
    &lt;div class=&quot;gallery-fallback&quot;&gt;
      &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;4&quot;&gt;
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2025-08-10-screenlines/marketing_1.jpg&quot; alt=&quot;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2025-08-10-screenlines/marketing_2.png&quot; alt=&quot;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2025-08-10-screenlines/marketing_3.png&quot; alt=&quot;&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2025-08-10-screenlines/marketing_4.png&quot; alt=&quot;&quot; /&gt;
          
        
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/noscript&gt;
&lt;/div&gt;

&lt;p&gt;Если приложение висит в топе, значит его кто-то скачивает. А это результат продвижения и маркетинга. И это то, что мне предстоит освоить.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;P.S.&lt;/strong&gt; Если у вас iPhone — попробуйте &lt;a href=&quot;https://apps.apple.com/app/id6739783494&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;ScreenLines&lt;/a&gt;. Бесплатно.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;P.P.S.&lt;/strong&gt; Енот передаёт привет 👋&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;P.P.P.S.&lt;/strong&gt; Оказывается, Георгий Лямин, главред iGuides, тоже написал про ScreenLines в &lt;a href=&quot;https://t.me/mediatrolling/7172&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;своём канале&lt;/a&gt;. Спасибо!&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;ссылки&quot;&gt;Ссылки&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://apps.apple.com/app/id6739783494&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;ScreenLines в App Store&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://screenlines.app&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Сайт проекта&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content>
  </entry>
  
  <entry>
    <title type="html">ServiceLocator и&amp;nbsp;@Injected: как упростить работу с&amp;nbsp;зависимостями в&amp;nbsp;iOS</title>
    <link href="https://bolotov.dev/blog/2025/06/07/servicelocator-injected" rel="alternate" type="text/html"/>
    <id>https://bolotov.dev/blog/2025/06/07/servicelocator-injected</id>
    <published>2025-06-07T00:00:00+00:00</published>
    <updated>2025-06-07T00:00:00+00:00</updated>
    
    


    <category term="iosdev" label="iosdev"/>
    
    <summary type="html">Простой и&amp;nbsp;понятный способ организации кода в&amp;nbsp;Swift-приложениях без сложных фреймворков</summary>
    
    <content type="html" xml:base="https://bolotov.dev/blog/2025/06/07/servicelocator-injected">&lt;p&gt;Представьте, что вы строите дом. У вас есть электрик, сантехник, маляр — каждый специалист отвечает за свою часть работы. В программировании похожая ситуация: у нас есть разные сервисы (службы), каждый из которых выполняет свою задачу. Сегодня расскажу, как удобно организовать работу с такими сервисами в iOS-приложении на SwiftUI.&lt;/p&gt;

&lt;h2 id=&quot;проблема-запутанные-связи&quot;&gt;Проблема: запутанные связи&lt;/h2&gt;

&lt;p&gt;Когда приложение растёт, в нём появляется всё больше компонентов, которые должны друг с другом взаимодействовать. Например:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Сервис для работы с сетью&lt;/li&gt;
  &lt;li&gt;Сервис для сохранения данных&lt;/li&gt;
  &lt;li&gt;Сервис аналитики&lt;/li&gt;
  &lt;li&gt;И так далее…&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Без правильной организации код быстро превращается в спагетти, где всё зависит от всего.&lt;/p&gt;

&lt;h2 id=&quot;решение-servicelocator&quot;&gt;Решение: ServiceLocator&lt;/h2&gt;

&lt;p&gt;ServiceLocator — это как справочная служба в большом офисном центре. Вместо того чтобы бегать по этажам в поисках нужного специалиста, вы просто обращаетесь в справочную: «Мне нужен бухгалтер» — и вам сразу говорят, где его найти.&lt;/p&gt;

&lt;p&gt;Вот как это выглядит в коде:&lt;/p&gt;

&lt;div class=&quot;language-swift highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;ServiceLocator&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// Singleton instance for the entire app&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;shared&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;ServiceLocator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    
    &lt;span class=&quot;c1&quot;&gt;// Dictionary to store all services&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;lazy&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;services&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Any&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]()&lt;/span&gt;
    
    &lt;span class=&quot;c1&quot;&gt;// Register a new service&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;addService&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;service&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;key&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;describing&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;services&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;service&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    
    &lt;span class=&quot;c1&quot;&gt;// Get a service&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;getService&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;T&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;key&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;describing&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;guard&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;service&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;services&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as?&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;T&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;nf&quot;&gt;fatalError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Service of type &lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; is not registered!&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;service&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;магия-propertywrapper&quot;&gt;Магия @propertyWrapper&lt;/h2&gt;

&lt;p&gt;Но постоянно писать &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ServiceLocator.shared.getService()&lt;/code&gt; — утомительно. Здесь на помощь приходит возможность Swift создавать собственные «обёртки» для свойств. Это как автоматический дозатор мыла — вы просто подносите руки, а он сам выдаёт нужное количество.&lt;/p&gt;

&lt;div class=&quot;language-swift highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;@propertyWrapper&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Injected&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;Service&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;lazy&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;service&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Service&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;ServiceLocator&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;shared&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;getService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    
    &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;wrappedValue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Service&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;mutating&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;get&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;service&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;projectedValue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Injected&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;Service&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;get&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;set&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;newValue&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;важность-протоколов-контракт-вместо-реализации&quot;&gt;Важность протоколов: контракт вместо реализации&lt;/h2&gt;

&lt;p&gt;Представьте, что вы нанимаете водителя. Вам важно, что он умеет водить машину, а не то, какой марки у него права. В программировании протоколы работают так же — они описывают, ЧТО должен уметь делать сервис, а не КАК он это делает.&lt;/p&gt;

&lt;h3 id=&quot;почему-это-важно&quot;&gt;Почему это важно?&lt;/h3&gt;

&lt;p&gt;Допустим, вы создали приложение, которое сохраняет данные в iCloud. Но потом решили, что хотите сохранять их локально. Если вы использовали протоколы, то замена будет простой как щелчок выключателя.&lt;/p&gt;

&lt;p&gt;Вот пример:&lt;/p&gt;

&lt;div class=&quot;language-swift highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// Protocol describes WHAT the storage service should do&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;protocol&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;StorageServiceProtocol&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;save&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;load&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;delete&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// Implementation for iCloud&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;CloudStorageService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;StorageServiceProtocol&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;save&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nf&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Saving to iCloud: &lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// Code to work with iCloud&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    
    &lt;span class=&quot;kd&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;load&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nf&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Loading from iCloud&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;data from cloud&quot;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    
    &lt;span class=&quot;kd&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;delete&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nf&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Deleting from iCloud&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// Implementation for local storage&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;LocalStorageService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;StorageServiceProtocol&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;save&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nf&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Saving locally: &lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// Code to work with UserDefaults&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    
    &lt;span class=&quot;kd&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;load&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nf&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Loading locally&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;local data&quot;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    
    &lt;span class=&quot;kd&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;delete&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nf&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Deleting locally&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;магия-замены&quot;&gt;Магия замены&lt;/h3&gt;

&lt;p&gt;Теперь смотрите, как просто поменять реализацию:&lt;/p&gt;

&lt;div class=&quot;language-swift highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// Choose implementation when app starts&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;userHasInternet&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kt&quot;&gt;ServiceLocator&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;shared&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;addService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;service&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;CloudStorageService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;StorageServiceProtocol&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kt&quot;&gt;ServiceLocator&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;shared&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;addService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;service&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;LocalStorageService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;StorageServiceProtocol&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// No need to change anything in the app code!&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;SettingsView&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;View&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;@Injected&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;storage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;StorageServiceProtocol&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;@State&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;userName&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;&quot;&lt;/span&gt;
    
    &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;some&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;View&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;kt&quot;&gt;VStack&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;kt&quot;&gt;TextField&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Your name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;userName&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            
            &lt;span class=&quot;kt&quot;&gt;Button&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Save&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;storage&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;save&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;userName&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;user_name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;onAppear&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;userName&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;storage&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;load&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;user_name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;&quot;&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;как-это-работает-напрактике-вswiftui&quot;&gt;Как это работает на практике в SwiftUI&lt;/h2&gt;

&lt;h3 id=&quot;шаг-1-создаём-протокол-иреализацию&quot;&gt;Шаг 1: Создаём протокол и реализацию&lt;/h3&gt;

&lt;div class=&quot;language-swift highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// Protocol for network operations&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;protocol&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;NetworkServiceProtocol&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;loadUserData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;throws&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UserData&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;sendAnalytics&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// Real implementation&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;NetworkService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;NetworkServiceProtocol&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;loadUserData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;throws&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UserData&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// API call code here&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UserData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;Artem&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;ar@bolotov.dev&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    
    &lt;span class=&quot;kd&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;sendAnalytics&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nf&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Sending analytics: &lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// Mock implementation for development&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;MockNetworkService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;NetworkServiceProtocol&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;loadUserData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;throws&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UserData&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// Return test data without server call&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UserData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;Test User&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;test@example.com&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    
    &lt;span class=&quot;kd&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;sendAnalytics&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nf&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;TEST: &lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// Data model&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UserData&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;String&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;String&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;шаг-2-регистрируем-сервисы-вточке-входа&quot;&gt;Шаг 2: Регистрируем сервисы в точке входа&lt;/h3&gt;

&lt;div class=&quot;language-swift highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;@main&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;MyApp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;App&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nf&quot;&gt;init&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nf&quot;&gt;setupServices&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    
    &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;some&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Scene&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;kt&quot;&gt;WindowGroup&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;kt&quot;&gt;ContentView&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    
    &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;setupServices&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;cp&quot;&gt;#if DEBUG&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// Use mock services in development&lt;/span&gt;
        &lt;span class=&quot;kt&quot;&gt;ServiceLocator&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;shared&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;addService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;service&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;MockNetworkService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;NetworkServiceProtocol&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;cp&quot;&gt;#else&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// Use real services in production&lt;/span&gt;
        &lt;span class=&quot;kt&quot;&gt;ServiceLocator&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;shared&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;addService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;service&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;NetworkService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;NetworkServiceProtocol&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;cp&quot;&gt;#endif&lt;/span&gt;
        
        &lt;span class=&quot;c1&quot;&gt;// Register other services&lt;/span&gt;
        &lt;span class=&quot;kt&quot;&gt;ServiceLocator&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;shared&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;addService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;service&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;AnalyticsService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;AnalyticsServiceProtocol&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;kt&quot;&gt;ServiceLocator&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;shared&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;addService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;service&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;KeychainService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;KeychainServiceProtocol&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;шаг-3-используем-вswiftui-views&quot;&gt;Шаг 3: Используем в SwiftUI Views&lt;/h3&gt;

&lt;div class=&quot;language-swift highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;ProfileView&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;View&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;@Injected&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;network&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;NetworkServiceProtocol&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;@Injected&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;analytics&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;AnalyticsServiceProtocol&lt;/span&gt;
    
    &lt;span class=&quot;kd&quot;&gt;@State&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;userData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UserData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;@State&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;isLoading&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;@State&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;errorMessage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?&lt;/span&gt;
    
    &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;some&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;View&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;kt&quot;&gt;NavigationView&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;kt&quot;&gt;VStack&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;isLoading&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                    &lt;span class=&quot;kt&quot;&gt;ProgressView&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
                        &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;padding&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;userData&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;userData&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                    &lt;span class=&quot;kt&quot;&gt;VStack&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;spacing&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;20&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                        &lt;span class=&quot;kt&quot;&gt;Text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Hello, &lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;userData&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;!&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;font&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                        
                        &lt;span class=&quot;kt&quot;&gt;Text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;userData&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;foregroundColor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;secondary&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
                    &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;padding&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;error&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;errorMessage&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                    &lt;span class=&quot;kt&quot;&gt;Text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                        &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;foregroundColor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;red&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                        &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;padding&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
                
                &lt;span class=&quot;kt&quot;&gt;Button&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Reload&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                    &lt;span class=&quot;kt&quot;&gt;Task&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                        &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;loadUserData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
                    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
                &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;buttonStyle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;borderedProminent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;navigationTitle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Profile&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;onAppear&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;analytics&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;trackEvent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;ProfileView opened&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;kt&quot;&gt;Task&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                    &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;loadUserData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    
    &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;loadUserData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;isLoading&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;errorMessage&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;nil&lt;/span&gt;
        
        &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;userData&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;network&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;loadUserData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;analytics&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;trackEvent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;User data loaded&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;errorMessage&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;Failed to load data: &lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;localizedDescription&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;analytics&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;trackEvent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;User data load failed&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        
        &lt;span class=&quot;n&quot;&gt;isLoading&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;продвинутый-пример-observableobject-ссервисами&quot;&gt;Продвинутый пример: ObservableObject с сервисами&lt;/h2&gt;

&lt;p&gt;Часто в SwiftUI нужно использовать сервисы внутри ObservableObject:&lt;/p&gt;

&lt;div class=&quot;language-swift highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// Protocol for authentication&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;protocol&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;AuthServiceProtocol&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;login&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;throws&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;User&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;logout&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;isAuthenticated&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Bool&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;get&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// ViewModel using services&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;AuthViewModel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;ObservableObject&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;@Injected&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;authService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;AuthServiceProtocol&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;@Injected&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;analytics&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;AnalyticsServiceProtocol&lt;/span&gt;
    
    &lt;span class=&quot;kd&quot;&gt;@Published&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;isLoading&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;@Published&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;errorMessage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;@Published&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;isAuthenticated&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;
    
    &lt;span class=&quot;kd&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;login&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;MainActor&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;run&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;isLoading&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;errorMessage&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;nil&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        
        &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;user&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;authService&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;login&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;analytics&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;trackEvent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Login successful&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;parameters&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;user_id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;
            
            &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;MainActor&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;run&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;isAuthenticated&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;isLoading&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;analytics&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;trackEvent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Login failed&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            
            &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;MainActor&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;run&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;errorMessage&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;localizedDescription&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;isLoading&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    
    &lt;span class=&quot;kd&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;logout&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;authService&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;logout&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;analytics&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;trackEvent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;User logged out&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;isAuthenticated&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// SwiftUI View using ViewModel&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;LoginView&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;View&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;@StateObject&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;viewModel&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;AuthViewModel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;@State&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;email&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;&quot;&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;@State&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;password&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;&quot;&lt;/span&gt;
    
    &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;some&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;View&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;kt&quot;&gt;Form&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;kt&quot;&gt;TextField&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Email&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;textContentType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;emailAddress&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;autocapitalization&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;none&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            
            &lt;span class=&quot;kt&quot;&gt;SecureField&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Password&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;textContentType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            
            &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;error&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;viewModel&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;errorMessage&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;kt&quot;&gt;Text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                    &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;foregroundColor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;red&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                    &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;font&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;caption&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
            
            &lt;span class=&quot;kt&quot;&gt;Button&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;kt&quot;&gt;Task&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                    &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;viewModel&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;login&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;viewModel&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;isLoading&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                    &lt;span class=&quot;kt&quot;&gt;ProgressView&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                    &lt;span class=&quot;kt&quot;&gt;Text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Login&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;disabled&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;viewModel&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;isLoading&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;isEmpty&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;isEmpty&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;navigationTitle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Login&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;практический-пример-переключение-окружений&quot;&gt;Практический пример: переключение окружений&lt;/h2&gt;

&lt;div class=&quot;language-swift highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// Protocol for configuration&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;protocol&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;ConfigServiceProtocol&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;apiBaseURL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;get&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;environment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Environment&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;get&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;enum&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Environment&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;development&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;staging&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;production&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// Different implementations&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;DevConfigService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;ConfigServiceProtocol&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;apiBaseURL&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;https://dev-api.example.com&quot;&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;environment&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Environment&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;development&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;ProdConfigService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;ConfigServiceProtocol&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;apiBaseURL&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;https://api.example.com&quot;&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;environment&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Environment&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;production&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// Settings view to switch environments&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;SettingsView&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;View&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;@Injected&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;config&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;ConfigServiceProtocol&lt;/span&gt;
    
    &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;some&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;View&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;kt&quot;&gt;List&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;kt&quot;&gt;Section&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Environment&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;kt&quot;&gt;Label&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;config&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;environment&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rawValue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;systemImage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;server.rack&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;kt&quot;&gt;Text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;config&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;apiBaseURL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                    &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;font&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;caption&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                    &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;foregroundColor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;secondary&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;navigationTitle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Settings&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;советы-для-swiftui-разработчиков&quot;&gt;Советы для SwiftUI разработчиков&lt;/h2&gt;

&lt;h3 id=&quot;1-используйте-injected-вview-напрямую&quot;&gt;1. Используйте @Injected в View напрямую&lt;/h3&gt;

&lt;div class=&quot;language-swift highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;ContentView&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;View&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;@Injected&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;network&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;NetworkServiceProtocol&lt;/span&gt;
    
    &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;some&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;View&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// Use service directly in view&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;2-для-stateobject-создайте-фабричный-метод&quot;&gt;2. Для StateObject создайте фабричный метод&lt;/h3&gt;

&lt;div class=&quot;language-swift highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;extension&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;ServiceLocator&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;makeAuthViewModel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;AuthViewModel&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;AuthViewModel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;RootView&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;View&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;@StateObject&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;authViewModel&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;ServiceLocator&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;shared&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;makeAuthViewModel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    
    &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;some&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;View&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// Your view code&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;3-группируйте-регистрацию-посмыслу&quot;&gt;3. Группируйте регистрацию по смыслу&lt;/h3&gt;

&lt;div class=&quot;language-swift highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;extension&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;ServiceLocator&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;registerNetworkServices&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nf&quot;&gt;addService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;service&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;APIService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;APIServiceProtocol&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;nf&quot;&gt;addService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;service&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;ImageLoader&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;ImageLoaderProtocol&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    
    &lt;span class=&quot;kd&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;registerStorageServices&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nf&quot;&gt;addService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;service&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;KeychainService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;KeychainServiceProtocol&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;nf&quot;&gt;addService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;service&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UserDefaultsService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;UserDefaultsServiceProtocol&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;заключение&quot;&gt;Заключение&lt;/h2&gt;

&lt;p&gt;ServiceLocator с @Injected и протоколами отлично работает в SwiftUI приложениях. Вы получаете простоту использования, чистый код и возможность легко менять реализацию сервисов.&lt;/p&gt;

&lt;p&gt;Помните: протокол — это контракт между частями вашего приложения. Он говорит «что делать», а реализация решает «как делать». Это даёт свободу менять «как» без изменения «что».&lt;/p&gt;

&lt;p&gt;Начните использовать этот подход в своих SwiftUI проектах — и вы удивитесь, насколько проще станет организовывать код и добавлять новые функции!&lt;/p&gt;
</content>
  </entry>
  
  <entry>
    <title type="html">Как я&amp;nbsp;перешёл с&amp;nbsp;Google Analytics на&amp;nbsp;Яндекс.Метрику</title>
    <link href="https://bolotov.dev/blog/2025/05/30/goodbye-ga" rel="alternate" type="text/html"/>
    <id>https://bolotov.dev/blog/2025/05/30/goodbye-ga</id>
    <published>2025-05-30T00:00:00+00:00</published>
    <updated>2025-05-30T00:00:00+00:00</updated>
    
    


    <category term="webdev" label="webdev"/>
    
    <summary type="html">И&amp;nbsp;почему пришлось это сделать</summary>
    
    <content type="html" xml:base="https://bolotov.dev/blog/2025/05/30/goodbye-ga">&lt;p&gt;&lt;img src=&quot;https://bolotov.dev/blog/img/2025-05-30-goodbye-ga/migrate.png&quot; alt=&quot;Уходим с GA&quot; /&gt;&lt;/p&gt;

&lt;p&gt;С 1 июля 2024 года вступили в силу изменения в законодательстве, которые ограничивают использование Google Analytics для российских сайтов. Расскажу, как перенёс аналитику на Яндекс.Метрику и настроил всё правильно с точки зрения защиты персональных данных.&lt;/p&gt;

&lt;h2 id=&quot;почему-пришлось-уходить&quot;&gt;Почему пришлось уходить&lt;/h2&gt;

&lt;p&gt;Новые требования ограничивают передачу данных российских пользователей в страны, которые не обеспечивают должный уровень защиты персональных данных. Google Analytics хранит всё на серверах в США, что теперь создаёт правовые риски.&lt;/p&gt;

&lt;p&gt;Выбор простой: или переходим на отечественные решения, или готовимся к возможным штрафам.&lt;/p&gt;

&lt;h2 id=&quot;что-понадобится&quot;&gt;Что понадобится&lt;/h2&gt;

&lt;p&gt;Для миграции нужно:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Аккаунт в Яндексе&lt;/li&gt;
  &lt;li&gt;Доступ к коду сайта&lt;/li&gt;
  &lt;li&gt;Понимание, где сейчас установлен Google Analytics&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;создание-счётчика&quot;&gt;Создание счётчика&lt;/h2&gt;

&lt;p&gt;Процесс довольно простой:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Заходим на &lt;a href=&quot;https://metrika.yandex.ru/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;metrika.yandex.ru&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;Нажимаем «Добавить счётчик»&lt;/li&gt;
  &lt;li&gt;Указываем адрес сайта&lt;/li&gt;
  &lt;li&gt;Получаем код&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Но не спешите его сразу устанавливать.&lt;/p&gt;

&lt;h2 id=&quot;правильная-установка-сучётом-gdpr&quot;&gt;Правильная установка с учётом GDPR&lt;/h2&gt;

&lt;p&gt;Главное правило — счётчик должен запускаться только после явного согласия пользователя на использование cookies.&lt;/p&gt;

&lt;h3 id=&quot;базовый-код&quot;&gt;Базовый код&lt;/h3&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;&amp;lt;!-- Yandex.Metrika counter --&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;script &lt;/span&gt;&lt;span class=&quot;na&quot;&gt;type=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text/javascript&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;m&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;r&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;k&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;){&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;m&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;m&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;||&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(){(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;m&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;m&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;||&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[]).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;arguments&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)};&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;m&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;l&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;j&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;j&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;scripts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;length&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;j&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
         &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;scripts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;j&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;src&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;r&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;k&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;createElement&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getElementsByTagName&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;k&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;async&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;k&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;src&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;r&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;parentNode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;insertBefore&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;k&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;})(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;window&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;script&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;https://mc.yandex.ru/metrika/tag.js&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;ym&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

   &lt;span class=&quot;nb&quot;&gt;window&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;yaCounterInit&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;&amp;lt;!-- /Yandex.Metrika counter --&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;логика-запуска&quot;&gt;Логика запуска&lt;/h3&gt;

&lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;initYandexMetrika&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;typeof&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ym&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!==&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;undefined&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;window&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;yaCounterInit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;ym&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;12345678&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;init&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;clickmap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;trackLinks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;accurateTrackBounce&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;webvisor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
        &lt;span class=&quot;nb&quot;&gt;window&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;yaCounterInit&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// Проверяем сохранённое согласие&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;localStorage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getItem&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;cookies_accepted&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;initYandexMetrika&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getElementById&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;cookie-banner&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;style&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;display&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;block&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// Обработка согласия&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getElementById&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;accept-cookies&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;addEventListener&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;click&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;localStorage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;setItem&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;cookies_accepted&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getElementById&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;cookie-banner&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;style&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;display&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;none&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;initYandexMetrika&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;img src=&quot;https://bolotov.dev/blog/img/2025-05-30-goodbye-ga/screenlines-banner.png&quot; alt=&quot;Баннер согласия&quot; /&gt;
&lt;em&gt;Простой баннер внизу экрана&lt;/em&gt;&lt;/p&gt;

&lt;h2 id=&quot;важный-момент-про-noscript&quot;&gt;Важный момент про noscript&lt;/h2&gt;

&lt;p&gt;Яндекс.Метрика предлагает добавить такой код для пользователей без JavaScript:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;&amp;lt;!-- Не используйте это --&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;noscript&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&amp;gt;&amp;lt;img&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;https://mc.yandex.ru/watch/12345678&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;style=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;position:absolute; left:-9999px;&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;alt=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;/&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/noscript&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Проблема в том, что эта картинка загружается всегда, даже когда пользователь не давал согласия. Это нарушение принципа информированного согласия. Просто не добавляйте этот код.&lt;/p&gt;

&lt;h2 id=&quot;настройка-параметров&quot;&gt;Настройка параметров&lt;/h2&gt;

&lt;p&gt;Рекомендуемые настройки:&lt;/p&gt;

&lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nx&quot;&gt;ym&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;12345678&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;init&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;clickmap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;           &lt;span class=&quot;c1&quot;&gt;// Карта кликов&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;trackLinks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;         &lt;span class=&quot;c1&quot;&gt;// Отслеживание внешних ссылок&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;accurateTrackBounce&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;c1&quot;&gt;// Точный показатель отказов&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;webvisor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;          &lt;span class=&quot;c1&quot;&gt;// Лучше отключить для соблюдения GDPR&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;ecommerce&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;dataLayer&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;    &lt;span class=&quot;c1&quot;&gt;// Для интернет-магазинов&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;частые-проблемы&quot;&gt;Частые проблемы&lt;/h2&gt;

&lt;h3 id=&quot;данные-непоступают&quot;&gt;Данные не поступают&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Блокировщики рекламы.&lt;/strong&gt; AdBlock и подобные расширения блокируют счётчики. Для тестирования временно отключите их.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Проблемы с геолокацией.&lt;/strong&gt; Если ваш трафик идёт через зарубежные серверы (корпоративная сеть, спутниковый интернет), Метрика может некорректно определять местоположение.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Режим инкогнито.&lt;/strong&gt; В приватном режиме localStorage работает только до закрытия окна.&lt;/p&gt;

&lt;h3 id=&quot;проверка-работы&quot;&gt;Проверка работы&lt;/h3&gt;

&lt;ol&gt;
  &lt;li&gt;Откройте консоль разработчика (F12)&lt;/li&gt;
  &lt;li&gt;Перейдите на вкладку Network&lt;/li&gt;
  &lt;li&gt;Введите в фильтр &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mc.yandex&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;Обновите страницу и дайте согласие&lt;/li&gt;
  &lt;li&gt;Проверьте, что запросы отправляются&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;img src=&quot;https://bolotov.dev/blog/img/2025-05-30-goodbye-ga/dev_tool.png&quot; alt=&quot;Консоль разработчика&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;перенос-целей&quot;&gt;Перенос целей&lt;/h2&gt;

&lt;p&gt;Если использовали цели в Google Analytics, их нужно адаптировать:&lt;/p&gt;

&lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// Google Analytics&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;gtag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;purchase&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;currency&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;RUB&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// Яндекс.Метрика&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;ym&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;12345678&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;reachGoal&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;purchase&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;order_price&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;currency&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;RUB&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;чек-лист-после-миграции&quot;&gt;Чек-лист после миграции&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;Удалён код Google Analytics&lt;/li&gt;
  &lt;li&gt;Установлена Яндекс.Метрика&lt;/li&gt;
  &lt;li&gt;Настроен запуск после согласия&lt;/li&gt;
  &lt;li&gt;Удалён noscript-код&lt;/li&gt;
  &lt;li&gt;Проверена работа в разных браузерах&lt;/li&gt;
  &lt;li&gt;Обновлена политика конфиденциальности&lt;/li&gt;
  &lt;li&gt;Перенесены важные цели&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;итог&quot;&gt;Итог&lt;/h2&gt;

&lt;p&gt;Переход на Яндекс.Метрику — вынужденная, но не такая уж страшная мера. Основной функционал похож, есть свои интересные возможности вроде карты скроллинга и аналитики форм. Главное — правильно настроить с учётом требований о защите персональных данных.&lt;/p&gt;
</content>
  </entry>
  
  <entry>
    <title type="html">Как я&amp;nbsp;пытался зарегистрировать iOS-приложение в&amp;nbsp;реестре российского&amp;nbsp;ПО</title>
    <link href="https://bolotov.dev/blog/2025/05/28/reestr-po" rel="alternate" type="text/html"/>
    <id>https://bolotov.dev/blog/2025/05/28/reestr-po</id>
    <published>2025-05-28T00:00:00+00:00</published>
    <updated>2025-05-28T00:00:00+00:00</updated>
    
    


    <category term="notes" label="заметки"/>
    
    


    <category term="iosdev" label="iosdev"/>
    
    <summary type="html">История про бюрократию, загадочные формы и&amp;nbsp;почему я&amp;nbsp;решил подождать с&amp;nbsp;регистрацией</summary>
    
    <content type="html" xml:base="https://bolotov.dev/blog/2025/05/28/reestr-po">&lt;h2 id=&quot;счего-всё-началось&quot;&gt;С чего всё началось&lt;/h2&gt;

&lt;p&gt;У меня есть приложение &lt;a href=&quot;https://apps.apple.com/app/id6450484235&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;TwinKey&lt;/a&gt; — это такой российский аналог Google Authenticator для двухфакторной аутентификации. И вот подумал я: раз уж все говорят про импортозамещение, почему бы не зарегистрировать своё детище в официальном реестре отечественного ПО?&lt;/p&gt;

&lt;p&gt;Наивно полагал, что это будет что-то вроде: заполнил форму, приложил скриншоты, получил красивый сертификат. Спойлер: всё оказалось чуточку сложнее.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://bolotov.dev/blog/img/2025-05-28-reestr-po/reestr.png&quot; alt=&quot;Реестр&quot; /&gt;
&lt;em&gt;Заходя на подобные ресурсы, испытываю особую гордость за Страну&lt;/em&gt;&lt;/p&gt;

&lt;h2 id=&quot;авторизация-через-госуслуги-ноэто-неточно&quot;&gt;Авторизация через Госуслуги (но это не точно)&lt;/h2&gt;

&lt;p&gt;Начинается всё вроде бы логично — вход через Госуслуги. Думаю: «Отлично! Все мои данные там есть, сейчас всё само подтянется».&lt;/p&gt;

&lt;p&gt;Ага, как же! После успешного входа система радостно предлагает заполнить:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;ФИО (которое только что взяла с Госуслуг)&lt;/li&gt;
  &lt;li&gt;Паспорт (тоже оттуда)&lt;/li&gt;
  &lt;li&gt;Адрес (и он там есть!)&lt;/li&gt;
  &lt;li&gt;СНИЛС, ИНН и всё остальное (зачем вообще, если уже связали заявление и мой профиль?)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Получается, Госуслуги нужны просто чтобы убедиться, что ты — это ты и как аналог дорогой капчи. А потом всё равно перепиши всё руками. Логика уровня «покажи паспорт, а теперь перепиши его от руки в эту тетрадочку».&lt;/p&gt;

&lt;h2 id=&quot;техническая-документация-для-гуманитариев&quot;&gt;Техническая документация для гуманитариев&lt;/h2&gt;

&lt;p&gt;Дальше начинается самое интересное. Система просит описать техническую архитектуру. Звучит разумно, да? Но когда читаешь примеры и подсказки, понимаешь — форму составляли люди, которые последний раз видели код в фильме «Матрица».&lt;/p&gt;

&lt;h3 id=&quot;где-живёт-твой-код&quot;&gt;Где живёт твой код?&lt;/h3&gt;

&lt;p&gt;Мой любимый пункт: &lt;strong&gt;«Адрес нахождения технических средств хранения исходного текста»&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Ребята, это 2025 год! Мой код живёт:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;На MacBook (который ездит со мной везде)&lt;/li&gt;
  &lt;li&gt;На GitHub (это где-то в облаках)&lt;/li&gt;
  &lt;li&gt;В iCloud (ещё выше в облаках)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Что писать? «Россия, переносное устройство»? Или честно: «Сегодня код уехал вместе со мной на ТО в Ярославль, завтра будет дома, послезавтра — на природе отдохнёт»?&lt;/p&gt;

&lt;p&gt;И главное — нужен документ, подтверждающий этот адрес! Может, справку у дилера взять? «Подтверждаем, что Артём действительно сидел у нас с ноутбуком».&lt;/p&gt;

&lt;h3 id=&quot;про-хранение-кода&quot;&gt;Про хранение кода&lt;/h3&gt;

&lt;p&gt;Ещё просят &lt;strong&gt;«документацию с описанием технических средств хранения»&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Представляю, как пишу:&lt;/p&gt;
&lt;blockquote&gt;
  &lt;p&gt;Для хранения исходного кода программного обеспечения TwinKey используется персональный компьютер MacBook Pro 14″ 2021 года выпуска серийный номер XXX, оборудованный твердотельным накопителем емкостью 512 ГБ производства Samsung, файловая система APFS с включенным шифрованием FileVault 2 на базе алгоритма AES-256… Ещё есть GitHub — это такой сайт в интернете, где тоже можно код хранить. Он там хранится, наверное, на дисках, но уже не моих.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&quot;дайте-нам-ваше-приложение&quot;&gt;Дайте нам ваше приложение!&lt;/h2&gt;

&lt;p&gt;Следующий квест: предоставить «экземпляр программного обеспечения для проверки».&lt;/p&gt;

&lt;p&gt;Друзья, это iOS-приложение! Оно в App Store лежит! Хотите проверить — берите iPhone и качайте. Но нет, нужна инструкция по установке.&lt;/p&gt;

&lt;p&gt;Пришлось писать пошаговое руководство уровня:&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;Возьмите iPhone (это такой прямоугольник с яблоком сзади)&lt;/li&gt;
  &lt;li&gt;Найдите иконку App Store (синяя такая, с буквой А)&lt;/li&gt;
  &lt;li&gt;Нажмите на неё пальцем&lt;/li&gt;
  &lt;li&gt;В поиске наберите TwinKey&lt;/li&gt;
  &lt;li&gt;Нажмите «Загрузить»&lt;/li&gt;
  &lt;li&gt;Поздравляю, вы великолепны!&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;один-вполе-воин&quot;&gt;Один в поле воин&lt;/h2&gt;

&lt;p&gt;Особенно повеселил раздел про персонал и организационную структуру. У них там, видимо, представление, что любое ПО делает минимум отдел из 20 человек.&lt;/p&gt;

&lt;p&gt;А у меня структура простая:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;CEO/CTO/Разработчик/Тестировщик/Дизайнер/Менеджер&lt;/strong&gt;: я&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Отдел поддержки&lt;/strong&gt;: тоже я&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Служба безопасности&lt;/strong&gt;: опять я&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Отдел контроля качества&lt;/strong&gt;: и это я&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Юридический департамент&lt;/strong&gt;: ChatGPT (шутка, но только отчасти)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Пришлось креативить с формулировками. Вместо «делаю всё сам» писал что-то вроде «централизованная модель управления с полным циклом ответственности».&lt;/p&gt;

&lt;h2 id=&quot;эцп-привет-изпрошлого&quot;&gt;ЭЦП — привет из прошлого&lt;/h2&gt;

&lt;p&gt;Чтобы отправить все эти документы, нужна электронная подпись. Не просто подпись, а квалифицированная!&lt;/p&gt;

&lt;p&gt;Это значит:&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;Найти удостоверяющий центр (звучит как что-то из шпионского фильма)&lt;/li&gt;
  &lt;li&gt;Прийти туда лично с паспортом&lt;/li&gt;
  &lt;li&gt;Заплатить пару тысяч рублей&lt;/li&gt;
  &lt;li&gt;Получить флешку (в 2025 году)&lt;/li&gt;
  &lt;li&gt;Обнаружить, что у флешки разъём USB-A, а у твоего MacBook только USB-C&lt;/li&gt;
  &lt;li&gt;Искать переходник или хаб (ладно, есть у меня)&lt;/li&gt;
  &lt;li&gt;Установить программы с интерфейсом из Windows 98&lt;/li&gt;
  &lt;li&gt;Помолиться, чтобы всё заработало&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;И всё это чтобы подписать PDF-файл, который можно было бы отправить обычным email’ом. Но нет, нужна квалифицированная подпись! Потому что… потому что так надо!&lt;/p&gt;

&lt;h2 id=&quot;азачем-вообще&quot;&gt;А зачем вообще?&lt;/h2&gt;

&lt;p&gt;Пока разбирался со всеми этими требованиями, начал искать — а какие вообще плюсы даёт регистрация? Что получу, когда пройду все круги этого квеста?&lt;/p&gt;

&lt;p&gt;Внятного ответа так и не нашёл. Вроде как при госзакупках отечественное ПО в приоритете. Но мой аутентификатор вряд ли будут закупать министерства пачками.&lt;/p&gt;

&lt;p&gt;Может, какие-то льготы? Тоже непонятно. Продвижение? Сомнительно.&lt;/p&gt;

&lt;p&gt;Получается, регистрация нужна… чтобы быть зарегистрированным? Глубоко.&lt;/p&gt;

&lt;h2 id=&quot;что-витоге&quot;&gt;Что в итоге?&lt;/h2&gt;

&lt;p&gt;Посмотрел я на всю эту красоту и решил: а давайте-ка пока подождём. Поставил регистрацию на паузу по нескольким причинам:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Время&lt;/strong&gt;. Чтобы собрать все документы и правильно заполнить формы, нужно потратить кучу времени. А его лучше потратить на улучшение приложения.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Деньги&lt;/strong&gt;. ЭЦП стоит денег, продлевать её каждый год — тоже. И ради чего?&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Неопределённость&lt;/strong&gt;. Когда непонятно, какие преимущества получишь, сложно мотивировать себя проходить через все эти испытания.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Здравый смысл&lt;/strong&gt;. Иногда полезно остановиться и подумать: а оно мне надо?&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;мораль&quot;&gt;Мораль&lt;/h2&gt;

&lt;p&gt;С одной стороны, хорошо, что у нас есть реестр отечественного ПО. С другой — было бы здорово, если бы процедура регистрации учитывала реалии современной разработки.&lt;/p&gt;

&lt;p&gt;Знаете, система регистрации явно создавалась людьми, которые представляют разработку ПО как производство танков на заводе в Туле. У них в голове есть цеха с компьютерами, на которых висят инвентарные номера, есть журналы учёта исходного кода, и каждый файл проштампован печатью ОТК.&lt;/p&gt;

&lt;p&gt;Представьте, как было бы круто:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Зашёл через госуслуги — все данные подтянулись&lt;/li&gt;
  &lt;li&gt;Указал ссылку на GitHub — система сама всё проверила&lt;/li&gt;
  &lt;li&gt;Дал ссылку на App Store — не нужно писать инструкции&lt;/li&gt;
  &lt;li&gt;Подписал прямо в браузере — без флешек и КриптоПро&lt;/li&gt;
  &lt;li&gt;Получил сертификат и чёткий список преимуществ&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Но пока это только мечты. А я пока лучше займусь тем, что умею — буду делать хорошие приложения. Может, когда-нибудь и процедура регистрации станет такой же удобной, как современные приложения.&lt;/p&gt;

&lt;p&gt;А пока — качайте &lt;a href=&quot;https://apps.apple.com/app/id6450484235&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;TwinKey&lt;/a&gt;! Он хоть и не в реестре, но работает отлично. И для установки не нужна инструкция на 20 страниц 😊&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;em&gt;Эта статья написана с любовью к отечественному IT и надеждой на здравый смысл в государственных процедурах.&lt;/em&gt;&lt;/p&gt;
</content>
  </entry>
  
  <entry>
    <title type="html">Где разместить сайт: простые решения для разработчиков</title>
    <link href="https://bolotov.dev/blog/2025/04/30/hosting" rel="alternate" type="text/html"/>
    <id>https://bolotov.dev/blog/2025/04/30/hosting</id>
    <published>2025-04-30T00:00:00+00:00</published>
    <updated>2025-04-30T00:00:00+00:00</updated>
    
    


    <category term="webdev" label="webdev"/>
    
    <summary type="html">Обзор бесплатных вариантов хостинга для сайтов проектов без сложностей в настройке</summary>
    
    <content type="html" xml:base="https://bolotov.dev/blog/2025/04/30/hosting">&lt;p&gt;Каждый iOS-разработчик (и не только) рано или поздно сталкивается с необходимостью создания сайта для своего приложения. Это не просто дань моде — при публикации в App Store вы обязательно должны указать ссылку на политику конфиденциальности. К тому же сайт делает ваш проект более доступным для поисковых систем и потенциальных пользователей.&lt;/p&gt;

&lt;p&gt;Но что делать, если основная сфера ваших интересов — это само приложение, а сайт нужен лишь как дополнение? Тратить время на изучение тонкостей хостинга, настройку серверов и мониторинг нагрузки совсем не хочется. Особенно когда речь идёт о простом сайте с описанием, парой красивых скриншотов и, возможно, небольшим промо-видео. App Store требует обязательную ссылку на политику конфиденциальности. И пусть она ведёт на ваш сайт.&lt;/p&gt;

&lt;div class=&quot;image-gallery&quot; data-gallery-id=&quot;gallery-auto&quot;&gt;
  
  
  
  
  &lt;!-- Loading State --&gt;
  &lt;div class=&quot;gallery-loading&quot; data-images-count=&quot;2&quot;&gt;
    &lt;div class=&quot;gallery-loading-content&quot;&gt;
      &lt;div class=&quot;gallery-loading-spinner&quot;&gt;&lt;/div&gt;
      &lt;span class=&quot;gallery-loading-text&quot;&gt;Загрузка изображений...&lt;/span&gt;
      &lt;div class=&quot;gallery-loading-progress&quot;&gt;
        &lt;div class=&quot;gallery-loading-bar&quot;&gt;&lt;/div&gt;
      &lt;/div&gt;
      &lt;div class=&quot;gallery-loading-counter&quot;&gt;
        &lt;span class=&quot;gallery-loaded-count&quot;&gt;0&lt;/span&gt;/&lt;span class=&quot;gallery-total-count&quot;&gt;2&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Gallery Content (hidden by default) --&gt;
  &lt;div class=&quot;gallery-content&quot; style=&quot;opacity: 0; visibility: hidden;&quot;&gt;
    &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;2&quot;&gt;
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2025-04-30-hosting/privacy.png&quot; alt=&quot;Appstore Connect privacy&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease;&quot; /&gt;
        
      
        
        
          
          
          
          

          
          

          

          
          
          
          &lt;img src=&quot;https://bolotov.dev/blog/img/2025-04-30-hosting/marketing.png&quot; alt=&quot;Appstore Connect marketing&quot; loading=&quot;lazy&quot; fetchpriority=&quot;low&quot; data-gallery-image=&quot;&quot; data-gallery-id=&quot;gallery-auto&quot; style=&quot;opacity: 0; transition: opacity 0.3s ease;&quot; /&gt;
        
      
    &lt;/div&gt;
    
    &lt;!-- Navigation controls - always present for galleries --&gt;
    &lt;div class=&quot;gallery-controls&quot;&gt;
      &lt;button class=&quot;gallery-nav gallery-prev&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;prev&quot; aria-label=&quot;Предыдущее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;15 18 9 12 15 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
      &lt;button class=&quot;gallery-nav gallery-next&quot; data-gallery=&quot;gallery-auto&quot; data-direction=&quot;next&quot; aria-label=&quot;Следующее изображение&quot;&gt;
        &lt;svg width=&quot;24&quot; height=&quot;24&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;
          &lt;polyline points=&quot;9 18 15 12 9 6&quot;&gt;&lt;/polyline&gt;
        &lt;/svg&gt;
      &lt;/button&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  
  &lt;!-- Fallback for no-JS --&gt;
  &lt;noscript&gt;
    &lt;div class=&quot;gallery-fallback&quot;&gt;
      &lt;div class=&quot;image-gallery-inner&quot; data-count=&quot;2&quot;&gt;
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2025-04-30-hosting/privacy.png&quot; alt=&quot;Appstore Connect privacy&quot; /&gt;
          
        
          
          
            
            
            
            

            

            
            &lt;img src=&quot;https://bolotov.dev/blog/img/2025-04-30-hosting/marketing.png&quot; alt=&quot;Appstore Connect marketing&quot; /&gt;
          
        
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/noscript&gt;
&lt;/div&gt;

&lt;h2 id=&quot;домен-и-хостинг--разные-вещи&quot;&gt;Домен и хостинг — разные вещи&lt;/h2&gt;

&lt;p&gt;Прежде чем перейти к решениям, важно понять: &lt;strong&gt;покупка домена&lt;/strong&gt; и &lt;strong&gt;размещение сайта&lt;/strong&gt; — это два разных процесса, которые не обязательно должны быть связаны.&lt;/p&gt;

&lt;p&gt;Домен (например, &lt;a href=&quot;https://twinkey.app&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;twinkey.app&lt;/a&gt; или &lt;a href=&quot;https://calendar.red&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;calendar.red&lt;/a&gt;) вы можете приобрести в любом удобном регистраторе: &lt;a href=&quot;https://timeweb.com/services/domains/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;timeweb&lt;/a&gt;, &lt;a href=&quot;https://www.reg.ru/domain/new/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;REG.RU&lt;/a&gt;, &lt;a href=&quot;https://www.namecheap.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;namecheap&lt;/a&gt;, &lt;a href=&quot;https://www.spaceship.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;spaceship&lt;/a&gt; и других. А вот где будет физически размещён ваш сайт — это уже вопрос выбора хостинга.&lt;/p&gt;

&lt;p&gt;Хорошая новость — для простых проектов есть отличные бесплатные решения.&lt;/p&gt;

&lt;h2 id=&quot;github-pages-просто-по-гиковски&quot;&gt;GitHub Pages: просто по-гиковски&lt;/h2&gt;

&lt;p&gt;Если вы уже пользуетесь Git для хранения кода своих проектов, то GitHub Pages — идеальное решение. Именно на нём размещён и этот блог, который вы сейчас читаете.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Принцип работы:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;Создаёте репозиторий на GitHub с названием &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;username.github.io&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;Загружаете туда HTML, CSS и JavaScript файлы (или используете генератор статических сайтов вроде Jekyll)&lt;/li&gt;
  &lt;li&gt;GitHub автоматически публикует ваш сайт по адресу &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;username.github.io&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;img src=&quot;https://bolotov.dev/blog/img/2025-04-30-hosting/github_pages.png&quot; alt=&quot;Github Pages As Hosting&quot; /&gt;
&lt;em&gt;Бесплатный хостинг, правда с регистрацией и смс&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Если хотите использовать свой домен, это тоже не проблема — достаточно настроить DNS-записи и указать его в настройках репозитория.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://bolotov.dev/blog/img/2025-04-30-hosting/github_pages_2.png&quot; alt=&quot;Github Pages with custom domain&quot; /&gt;
&lt;em&gt;А если прописать A и CNAME DNS записи по инструкции, можно и свой домен подключить&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Преимущества GitHub Pages:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Абсолютно бесплатно для публичных репозиториев&lt;/li&gt;
  &lt;li&gt;Неограниченный трафик&lt;/li&gt;
  &lt;li&gt;Поддержка HTTPS из коробки&lt;/li&gt;
  &lt;li&gt;Интеграция с системой контроля версий&lt;/li&gt;
  &lt;li&gt;Автоматическое обновление сайта при изменении файлов&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Есть и ограничения: сайт должен быть статическим (никакого PHP или других серверных языков), а размер репозитория не должен превышать 1 ГБ.&lt;/p&gt;

&lt;h2 id=&quot;firebase-hosting-когда-нужно-чуть-больше&quot;&gt;Firebase Hosting: когда нужно чуть больше&lt;/h2&gt;

&lt;p&gt;Если вы как разработчик мобильных приложений уже используете Firebase для аналитики, уведомлений или базы данных, то Firebase Hosting станет логичным дополнением вашей инфраструктуры.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Особенности Firebase Hosting:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Интеграция с другими сервисами Firebase&lt;/li&gt;
  &lt;li&gt;Глобальная CDN от Google&lt;/li&gt;
  &lt;li&gt;HTTPS по умолчанию&lt;/li&gt;
  &lt;li&gt;Удобный CLI-интерфейс для деплоя&lt;/li&gt;
  &lt;li&gt;Возможность создания динамических приложений с Firebase Functions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Настройка тоже предельно проста:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Install Firebase CLI&lt;/span&gt;
npm &lt;span class=&quot;nb&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-g&lt;/span&gt; firebase-tools

&lt;span class=&quot;c&quot;&gt;# Login to your account&lt;/span&gt;
firebase login

&lt;span class=&quot;c&quot;&gt;# Initialize project&lt;/span&gt;
firebase init hosting

&lt;span class=&quot;c&quot;&gt;# Deploy your site&lt;/span&gt;
firebase deploy &lt;span class=&quot;nt&quot;&gt;--only&lt;/span&gt; hosting
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Что по-настоящему выделяет Firebase — возможность создания интерактивных сайтов без необходимости настройки бэкенда. Вы можете подключиться к Firestore прямо из JavaScript, добавить аутентификацию и даже использовать серверные функции для более сложной логики.&lt;/p&gt;

&lt;h2 id=&quot;яндексоблако-и-его-аналоги&quot;&gt;Яндекс.Облако и его аналоги&lt;/h2&gt;

&lt;p&gt;Стоит упомянуть и отечественные решения. Яндекс.Облако предлагает сервис Object Storage, который можно настроить для хостинга статических сайтов. Принцип похож на Amazon S3 — вы загружаете файлы в бакет и настраиваете его как веб-сайт.&lt;/p&gt;

&lt;p&gt;Преимущество в том, что ваш сайт будет размещён в российских дата-центрах, что положительно влияет на скорость загрузки для российских пользователей и соответствие законодательству.&lt;/p&gt;

&lt;p&gt;Сам не пользовался, но рассказать про это нужно.&lt;/p&gt;

&lt;h2 id=&quot;что-выбрать&quot;&gt;Что выбрать?&lt;/h2&gt;

&lt;p&gt;Мой опыт показывает, что:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;GitHub Pages&lt;/strong&gt; отлично подходит для личных сайтов, резюме, блогов и небольших проектов. Особенно если вы уже используете GitHub и знакомы с Markdown.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Firebase Hosting&lt;/strong&gt; — лучший выбор, если вы используете другие сервисы Firebase в своём приложении или планируете добавить интерактивность на сайт.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Яндекс.Облако&lt;/strong&gt; стоит рассмотреть, если важна скорость доступа в России и соответствие ФЗ-152.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;заключение&quot;&gt;Заключение&lt;/h2&gt;

&lt;p&gt;Современные облачные решения позволяют запустить сайт для вашего приложения буквально за считанные минуты. Главное преимущество описанных вариантов — они не только бесплатны для большинства случаев использования, но и автоматически масштабируются под любые нагрузки.&lt;/p&gt;

&lt;p&gt;Мой &lt;a href=&quot;https://twinkey.app&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;TwinKey&lt;/a&gt; использует Firebase, а этот блог работает на GitHub Pages — оба решения отлично справляются со своими задачами и не требуют практически никакого обслуживания. Это позволяет сосредоточиться на том, что действительно важно — разработке качественных приложений.&lt;/p&gt;
</content>
  </entry>
  
  <entry>
    <title type="html">Личный блог: испытание свободой</title>
    <link href="https://bolotov.dev/blog/2025/04/29/why-website" rel="alternate" type="text/html"/>
    <id>https://bolotov.dev/blog/2025/04/29/why-website</id>
    <published>2025-04-29T00:00:00+00:00</published>
    <updated>2025-05-03T10:13:00+00:00</updated>
    
    


    <category term="notes" label="заметки"/>
    
    <summary type="html">Свой сайт или чужая платформа: что удобнее для публикаций?</summary>
    
    <content type="html" xml:base="https://bolotov.dev/blog/2025/04/29/why-website">&lt;p&gt;Привет! Наконец-то решился начать вести блог на своём сайте. Казалось бы, в 2025 году все давно перебрались в телеграм-каналы и короткие видео. Но мне как разработчику захотелось вернуться к основам и создать что-то более личное и долговечное.&lt;/p&gt;

&lt;p&gt;Это не ностальгия по «старым добрым временам» (хотя куда без неё 😊). Просто хочется иметь место, где можно спокойно делиться чем-то без ограничений формата и алгоритмов социальных платформ.&lt;/p&gt;

&lt;h2 id=&quot;свобода-творчества-без-рамок&quot;&gt;Свобода творчества без рамок&lt;/h2&gt;

&lt;p&gt;Я пробовал писать статьи на Медиуме, Яндекс.Дзене, vc.ru и в телеграме. И всегда раздражал жёсткий формат. Особенно в телеграме, где каждый пост — это либо текст, либо картинка с подписью, либо видео, либо каша.&lt;/p&gt;

&lt;p&gt;А здесь, на сайте, можно делать всё, что захочется:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Вставить картинку прямо посреди абзаца, если она дополняет мысль&lt;/li&gt;
  &lt;li&gt;Организовать текст с помощью красивых заголовков&lt;/li&gt;
  &lt;li&gt;Показать код так, чтобы глаза не сломались при его чтении&lt;/li&gt;
  &lt;li&gt;Выстроить информацию в таблицы, когда это нужно&lt;/li&gt;
  &lt;li&gt;Проставить всякие &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;amp;nbsp;&lt;/code&gt; где хочется
А если захочется чего-то совсем необычного — можно вообще уйти от шаблона и сделать отдельную страницу с собственным дизайном. Когда ты сам себе хозяин, можно экспериментировать с подачей контента, не оглядываясь на ограничения платформы.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;овидео-ичтении&quot;&gt;О видео и чтении&lt;/h2&gt;

&lt;p&gt;Сегодня интернет переполнен видео. Shorts, reels, туториалы и стримы — создаётся ощущение, что текст вообще скоро исчезнет. И я понимаю почему: видео проще воспринимать, оно не требует особых усилий.&lt;/p&gt;

&lt;p&gt;Но есть одна интересная мысль. Когда мы читаем, наш мозг работает совсем иначе. Мы сами создаём образы, выстраиваем связи между идеями, контролируем скорость восприятия информации. Это более активный процесс, который помогает лучше запоминать и глубже понимать материал.&lt;/p&gt;

&lt;p&gt;Кроме того, у текста есть особая магия времени. Помните это чувство, когда находишь свои старые записи — школьные тетради, письма, заметки в блокноте? Перечитываешь и будто возвращаешься в тот момент, вспоминаешь, о чём думал тогда.&lt;/p&gt;

&lt;p&gt;И ещё один практический момент: по тексту работает поиск. В видео придётся перематывать туда-сюда, пытаясь вспомнить, на какой минуте была нужная информация.&lt;/p&gt;

&lt;p&gt;Не хочу сказать, что ролики — это плохо. У каждого формата своя аудитория и свои преимущества. Для демонстрации интерфейса приложения видео подходит идеально. Но когда речь идёт о технических деталях или размышлениях, текст даёт больше глубины и возможностей для анализа.&lt;/p&gt;

&lt;h2 id=&quot;машина-времени-вдействии&quot;&gt;Машина времени в действии&lt;/h2&gt;

&lt;p&gt;В соцсетях всё привязано к конкретному моменту: пост от вчерашнего дня уже считается «старым». На своём сайте можно:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Написать статью с любой датой — хоть из прошлого, хоть из будущего&lt;/li&gt;
  &lt;li&gt;Поделиться воспоминаниями о событиях прошлых лет, когда блога ещё не было&lt;/li&gt;
  &lt;li&gt;Обновить старую публикацию, добавив новые мысли или исправив устаревшие данные&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Классно же? Своя небольшая машина времени!&lt;/p&gt;

&lt;h2 id=&quot;гугл-тебя-найдёт&quot;&gt;Гугл тебя найдёт&lt;/h2&gt;

&lt;p&gt;Пока что это больше теория, чем практика. Блог только запустился, и посетителей из поиска ещё нет. Но я верю в долгосрочную перспективу.&lt;/p&gt;

&lt;p&gt;В телеграм-канале всё работало просто: написал пост, поделился ссылкой — люди прочитали. Через пару дней пост теряется, и шансов, что его кто-то найдёт, практически нет.&lt;/p&gt;

&lt;p&gt;С блогом должно быть иначе. Хорошая статья про SwiftUI или решение какой-то технической проблемы может приводить читателей месяцами и даже годами. Конечно, для этого нужно время и хорошие статьи: пока поисковики проиндексируют, пока статьи начнут ранжироваться… Но это как раз тот случай, когда попробуем играть вдолгую.&lt;/p&gt;

&lt;p&gt;Посмотрим, что получится. Надеюсь, этот раздел не придётся переписывать 😊&lt;/p&gt;

&lt;h2 id=&quot;сам-себе-хозяин&quot;&gt;Сам себе хозяин&lt;/h2&gt;

&lt;p&gt;На большинстве платформ мы всегда зависим от чужих правил и алгоритмов.&lt;/p&gt;

&lt;p&gt;А здесь полностью контролируем процесс:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Статья может быть любой длины&lt;/li&gt;
  &lt;li&gt;Никто не заблокирует контент из-за «нарушения правил»&lt;/li&gt;
  &lt;li&gt;Дизайн и оформление — только по вашему вкусу&lt;/li&gt;
  &lt;li&gt;Никаких внезапных глобальных обновлений, которые всё могут поломать!&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;инапоследок&quot;&gt;И напоследок…&lt;/h2&gt;

&lt;p&gt;Сайт — это как собственный дом в интернете. Ты можешь обустроить его по своему вкусу, принимать гостей и с годами делать его только лучше. А социальные платформы больше похожи на съемную квартиру — удобно, но правила устанавливает арендодатель.&lt;/p&gt;
</content>
  </entry>
  
  <entry>
    <title type="html">Как стать iOS-разработчиком</title>
    <link href="https://bolotov.dev/blog/2025/04/26/ios-dev-start" rel="alternate" type="text/html"/>
    <id>https://bolotov.dev/blog/2025/04/26/ios-dev-start</id>
    <published>2025-04-26T00:00:00+00:00</published>
    <updated>2025-04-26T00:00:00+00:00</updated>
    
    


    <category term="iosdev" label="iosdev"/>
    
    


    <category term="notes" label="заметки"/>
    
    <summary type="html">C чего начать, что потребуется и почему это того стоит</summary>
    
    <content type="html" xml:base="https://bolotov.dev/blog/2025/04/26/ios-dev-start">&lt;p&gt;Привет! Не знаю, как вы здесь оказались, но раз уж читаете эту статью, значит, вас заинтересовала нативная разработка приложений для iPhone и iPad. Отличный выбор!&lt;/p&gt;

&lt;h2 id=&quot;с-чего-всё-началось-у-меня&quot;&gt;С чего всё началось у меня&lt;/h2&gt;

&lt;p&gt;В 2014 году я случайно наткнулся на видео Алексея Скутаренко — русскоговорящего iOS разработчика, который, освоив эту профессию, уехал жить и работать в США. Это был один из тех моментов, когда сразу понимаешь: «вот оно!»&lt;/p&gt;

&lt;div class=&quot;youtube-container&quot;&gt;
    &lt;iframe title=&quot;Видео на YouTube&quot; src=&quot;https://www.youtube.com/embed/Z0IywGVakqQ&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture&quot; allowfullscreen=&quot;&quot;&gt;
    &lt;/iframe&gt;
  &lt;/div&gt;

&lt;p&gt;Тогда было крутое комьюнити и отличные бесплатные курсы. Сейчас курсы устарели, сообщество закрыли, но возможности стали шире.&lt;/p&gt;

&lt;h2 id=&quot;удовольствие-дорогое-но-оно-того-стоит&quot;&gt;Удовольствие дорогое, но оно того стоит&lt;/h2&gt;

&lt;p&gt;iOS-разработка — не самое дешёвое увлечение. И дело не только в технике.&lt;/p&gt;

&lt;h3 id=&quot;1-компьютер&quot;&gt;1. Компьютер&lt;/h3&gt;

&lt;p&gt;Вам обязательно понадобится Mac. Нет, не Windows с эмулятором, не хакинтош, не виртуалка. Настоящий Mac на чипе Apple Silicon.&lt;/p&gt;

&lt;p&gt;Помню, как покупал свой первый Macbook Air в 2014 году. Думал о хакинтошах, но решил не издеваться над собой и купил слабенький, но настоящий Mac. Разумеется, ни разу не пожалел.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://bolotov.dev/blog/img/2025-04-26-ios-dev-start/receipt.jpg&quot; alt=&quot;Чек на первый MacBook Air&quot; /&gt;
&lt;em&gt;И чек до сих пор ношу с собой в паспорте.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Сейчас мне максимально комфортно работать на MacBook Pro 14”. Не нужны широкие мониторы или несколько экранов. Куда приятнее быть мобильным: писать код в парке, кафе или на природе. Лучше быть готовым взять ноутбук с собой, чем иметь мощную, но стационарную станцию, к которой не захочется возвращаться.&lt;/p&gt;

&lt;h3 id=&quot;2-устройства-для-тестирования&quot;&gt;2. Устройства для тестирования&lt;/h3&gt;

&lt;p&gt;Симулятор в Xcode работает лучше, чем аналоги для Android, но он не заменит настоящее устройство. Идеально иметь хотя бы один iPhone или iPad для тестирования. И лучше не самый новый — так вы сможете проверить, как ваше приложение работает на устройствах с меньшей производительностью.&lt;/p&gt;

&lt;p&gt;Я начинал с тестирования на своем личном iPhone, потом появился iPad. Сейчас у нас несколько устройств разных поколений — это помогает убедиться, что всё работает везде.&lt;/p&gt;

&lt;h3 id=&quot;3-аккаунт-разработчика&quot;&gt;3. Аккаунт разработчика&lt;/h3&gt;

&lt;p&gt;Apple Developer Program стоит $99 в год. Без него вы не сможете:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Публиковать приложения в App Store&lt;/li&gt;
  &lt;li&gt;Полноценно тестировать их на физических устройствах&lt;/li&gt;
  &lt;li&gt;Использовать Push-уведомления&lt;/li&gt;
  &lt;li&gt;Работать с iCloud, In-App Purchase и другими API&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Сейчас оплата происходит так же просто, как подписка на iCloud.&lt;/p&gt;

&lt;h2 id=&quot;обучение&quot;&gt;Обучение&lt;/h2&gt;

&lt;p&gt;Если раньше было не так много качественных ресурсов, то сейчас их море. Разумеется, на английском. Если не знаете, всё равно попробуйте — удивитесь, но всё будет понятно. Из того, что советую:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://www.hackingwithswift.com/100/swiftui&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;100 Days of SwiftUI&lt;/a&gt;&lt;/strong&gt; — отличный старт для новичков. &lt;a href=&quot;https://x.com/twostraws&quot;&gt;Paul Hudson&lt;/a&gt; очень структурировано ведёт от основ к более сложным темам.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://cs193p.sites.stanford.edu/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;CS193p - Developing Apps for iOS&lt;/a&gt;&lt;/strong&gt; — университетский курс от Стэнфорда. Каждый год обновляется, сейчас полностью на SwiftUI. Тут уже поглубже и поакадемичнее.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://www.bigmountainstudio.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Big Mountain Studio&lt;/a&gt;&lt;/strong&gt; — отличный выбор когда уже есть основы. «SwiftUI Views Mastery» и «Core Data Mastery in SwiftUI» — их я прочитал от корки до корки (если так говорят про электронные книги).&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;почему-это-всё-таки-стоит-начать&quot;&gt;Почему это всё-таки стоит начать&lt;/h2&gt;

&lt;p&gt;Несмотря на все преграды, нативная iOS-разработка — одно из самых благодарных направлений. И подчеркну — именно нативная. Все эти кросплатформенные решения — редкостная ерунда, если вы хотите создавать действительно качественные приложения:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Качественная экосистема&lt;/strong&gt;. Apple создаёт хорошие инструменты, которые постоянно улучшаются. SwiftUI сейчас развивается невероятными темпами (и уже не сырой фреймворк).&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Монетизация&lt;/strong&gt;. Пользователи iOS больше платят за приложения. Даже небольшое приложение может приносить стабильный доход.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Удовлетворение&lt;/strong&gt;. Когда видишь своё приложение в App Store и получаешь первый отзыв — это непередаваемые ощущения.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Востребованность&lt;/strong&gt;. iOS-разработчики будут нужны всегда, пока существуют iPhone.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;что-дальше&quot;&gt;Что дальше?&lt;/h2&gt;

&lt;p&gt;Если эта статья не отпугнула вас, а наоборот — зажгла интерес, то вот мой совет: просто начните. Скачайте Xcode, создайте первый проект, посмотрите первые уроки из курса.&lt;/p&gt;

&lt;p&gt;Не пытайтесь сразу понять всё. iOS-разработка — это марафон, а не спринт. И помните: каждый разработчик когда-то написал свою первую строчку кода.&lt;/p&gt;

&lt;p&gt;P.S. И да, начинайте сразу с SwiftUI.&lt;/p&gt;
</content>
  </entry>
  
  <entry>
    <title type="html">Мнимая безопасность: усложняем простое</title>
    <link href="https://bolotov.dev/blog/2025/04/19/false-security" rel="alternate" type="text/html"/>
    <id>https://bolotov.dev/blog/2025/04/19/false-security</id>
    <published>2025-04-19T00:00:00+00:00</published>
    <updated>2025-04-19T00:00:00+00:00</updated>
    
    


    <category term="notes" label="заметки"/>
    
    <summary type="html">О том, как стремление к безопасности порой создаёт больше проблем, чем решает</summary>
    
    <content type="html" xml:base="https://bolotov.dev/blog/2025/04/19/false-security">&lt;p&gt;В IT-сфере постоянно наблюдаю интересный парадокс: решения, которые внедряются для повышения безопасности, на практике часто создают новые проблемы и даже снижают общий уровень защиты. Недавно столкнулся с типичным примером такого подхода.&lt;/p&gt;

&lt;h2 id=&quot;ситуация-изжизни&quot;&gt;Ситуация из жизни&lt;/h2&gt;

&lt;p&gt;Директору одной компании понадобился удалённый доступ к системе видеонаблюдения на планшете. Казалось бы, задача элементарная — современные видеорегистраторы поддерживают облачный доступ: устройству выдаётся специальный URL или происходит привязка к учётной записи в приложении.&lt;/p&gt;

&lt;p&gt;Но системный администратор решил пойти другим путём. Вместо настройки встроенного в регистратор удалённого доступа, он развернул VPN-подключение. Идея заключалась в том, чтобы директор мог «виртуально оказаться» в локальной сети предприятия и получать доступ к видеорегистратору как к обычному внутреннему ресурсу.&lt;/p&gt;

&lt;h2 id=&quot;почему-это-проблема&quot;&gt;Почему это проблема?&lt;/h2&gt;

&lt;p&gt;На первый взгляд, решение может показаться даже более надёжным — VPN же защищённое соединение, верно? Но на практике возникает как минимум две серьёзные проблемы:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Избыточные привилегии&lt;/strong&gt;. Подключаясь по VPN, директор получает доступ ко всей инфраструктуре локальной сети, хотя нужно только видеонаблюдение. Это нарушает один из базовых принципов информационной безопасности — принцип минимальных привилегий.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Снижение удобства использования&lt;/strong&gt;. Директору приходится постоянно включать и отключать VPN, что создаёт дополнительные сложности и повышает вероятность ошибки пользователя.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;почему-так-происходит&quot;&gt;Почему так происходит?&lt;/h2&gt;

&lt;p&gt;«Усложнение простого» обычно случается по нескольким причинам:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Недоверие к встроенным решениям&lt;/strong&gt;. Многие технические специалисты не доверяют готовым «облачным» решениям производителей, считая их небезопасными.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Профессиональная инерция&lt;/strong&gt;. «Мы всегда так делали, зачем менять?»&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Желание продемонстрировать экспертизу&lt;/strong&gt;. Иногда специалисты сознательно выбирают более сложное решение, чтобы подчеркнуть свою техническую компетентность.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;баланс-между-безопасностью-иудобством&quot;&gt;Баланс между безопасностью и удобством&lt;/h2&gt;

&lt;p&gt;Настоящее мастерство заключается не в создании максимально защищённой системы, а в нахождении золотой середины между безопасностью и удобством использования. В нашем примере оптимальным решением было бы:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Настроить облачный доступ к видеорегистратору с надёжной авторизацией (двухфакторная аутентификация, сложные пароли).&lt;/li&gt;
  &lt;li&gt;Ограничить доступ только функцией просмотра видео, без возможности изменения настроек.&lt;/li&gt;
  &lt;li&gt;Внедрить мониторинг подключений для отслеживания потенциальных проблем.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;выводы&quot;&gt;Выводы&lt;/h2&gt;

&lt;p&gt;Безопасность — это не только технические средства защиты, но и правильный баланс между защищённостью и функциональностью. Чрезмерное усложнение часто приводит к тому, что пользователи начинают искать обходные пути, полностью игнорируя защитные механизмы.&lt;/p&gt;
</content>
  </entry>
  
</feed>
