<?xml version="1.0" encoding="utf-8" ?><rss version="2.0" xmlns:tt="http://teletype.in/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:media="http://search.yahoo.com/mrss/"><channel><title>Теплица | Код и Кейсы</title><generator>teletype.in</generator><description><![CDATA[Это дополнение к Telegram-каналу «Теплица» — авторскому блогу о мобильной разработке.
Здесь мы с друзьями-разработчиками публикуем развернутые статьи.]]></description><image><url>https://img2.teletype.in/files/1c/0c/1c0c8a45-ecd7-4eeb-a893-4349fe590810.png</url><title>Теплица | Код и Кейсы</title><link>https://teletype.in/@greenhouse_mobile_dev</link></image><link>https://teletype.in/@greenhouse_mobile_dev?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=greenhouse_mobile_dev</link><atom:link rel="self" type="application/rss+xml" href="https://teletype.in/rss/greenhouse_mobile_dev?offset=0"></atom:link><atom:link rel="next" type="application/rss+xml" href="https://teletype.in/rss/greenhouse_mobile_dev?offset=10"></atom:link><atom:link rel="search" type="application/opensearchdescription+xml" title="Teletype" href="https://teletype.in/opensearch.xml"></atom:link><pubDate>Wed, 08 Jul 2026 19:10:57 GMT</pubDate><lastBuildDate>Wed, 08 Jul 2026 19:10:57 GMT</lastBuildDate><item><guid isPermaLink="true">https://teletype.in/@greenhouse_mobile_dev/sprite_kit_intro</guid><link>https://teletype.in/@greenhouse_mobile_dev/sprite_kit_intro?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=greenhouse_mobile_dev</link><comments>https://teletype.in/@greenhouse_mobile_dev/sprite_kit_intro?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=greenhouse_mobile_dev#comments</comments><dc:creator>greenhouse_mobile_dev</dc:creator><title>Введение в SpriteKit: учимся работать с движком на практике</title><pubDate>Wed, 02 Jul 2025 11:46:31 GMT</pubDate><media:content medium="image" url="https://img2.teletype.in/files/95/56/9556be39-125d-4f9a-91a5-da0231e354d1.png"></media:content><category>iOS</category><description><![CDATA[<img src="https://img1.teletype.in/files/c8/c8/c8c85e78-906a-4930-8cd3-cfc3a4d3b542.png"></img>Всем привет ! «До этого мы никогда не писали игры. Но решили — пора!»  В этой статье мы воссоздали базовую механику Flappy Bird на SpriteKit: добавили персонажа, физику, гравитацию и управление.  Если ты iOS-разработчик и хочешь попробовать себя в геймдеве — загляни]]></description><content:encoded><![CDATA[
  <figure id="pnNS" class="m_column">
    <img src="https://img1.teletype.in/files/c8/c8/c8c85e78-906a-4930-8cd3-cfc3a4d3b542.png" width="4800" />
  </figure>
  <p id="p071">Всем привет!</p>
  <p id="yTS3">В этой статье мы хотим поговорить о нативной разработке мобильных игр на iOS. А точнее познакомить вас с игровым движком <strong>SpriteKit</strong>. Разберём, что он умеет, зачем он нужен и как начать с ним работать. Вместе создадим простую игровую сцену, добавим персонажа, подключим физику и поэкспериментируем с гравитацией.</p>
  <p id="QKVv"></p>
  <h2 id="2lYh">Начало пути</h2>
  <p id="GUmD">Мы с командой решили воссоздать классическую Flappy Bird — простую, но затягивающую игру, где игрок управляет птичкой, избегая столкновений с трубами. Несмотря на кажущуюся простоту, такая игра требует продуманной архитектуры: нужно обеспечить стабильную производительность и чёткое разделение логики. Важно было реализовать управление состояниями (меню, игра, проигрыш), физику столкновений, бесконечную прокрутку фона и генерацию труб.</p>
  <p id="YW77">Читая абзац выше, может показаться, что мы отлично разбираемся в теме и всё у нас под контролем. Но на деле всё было иначе. Опыт? Впервые. План? Почти. Время на отрицание, гнев, торг и депрессию? Не завезли. Сразу «принятие» — и поехали делать игру.</p>
  <p id="5072">К счастью, Apple предоставляет из коробки мощный инструмент для таких задач — <strong>SpriteKit</strong>. Именно он стал нашим проводником в мир 2D-геймдева на iOS.</p>
  <p id="5WLz"></p>
  <h2 id="Chk8">Что такое SpriteKit?</h2>
  <p id="2HDi"><strong>SpriteKit</strong> — это фреймворк от Apple, который помогает создавать 2D-игры и анимации на iOS, macOS, tvOS и iPadOS. Проще говоря, это набор инструментов, позволяющий рисовать спрайты, двигать их, наделять физикой, проигрывать звуки и управлять столкновениями.</p>
  <p id="SrMi">В SpriteKit все игровые элементы, такие как персонажи, враги, фоны и прочее называются <strong>спрайтами</strong> (<em>sprites</em>). Эти спрайты размещаются на <strong>сцене</strong> (<em>scene</em>), которая служит основным контейнером для всей игровой логики и визуальных объектов.</p>
  <p id="jP9t">Рассмотрим основные компоненты фреймворка:</p>
  <ul id="9h6D">
    <li id="RrRb"><code>SKScene</code> — сцена, на которой разворачивается игра;</li>
    <li id="0O42"><code>SKSpriteNode</code> — графический элемент (например, персонаж или фон);</li>
    <li id="keTe"><code>SKLabelNode</code> — текст на экране;</li>
    <li id="Qbx3"><code>SKAction</code> — анимации и действия (движение, вращение и прочее);</li>
    <li id="wvqp"><code>SKPhysicsBody</code> — физическое тело для гравитации и столкновений.</li>
  </ul>
  <p id="p5Ow"></p>
  <h2 id="k4Sr">Настройка Scene</h2>
  <p id="e9TD">В SpriteKit все игровые объекты размещаются на сцене<strong>.</strong> Это специальный класс (<code>SKScene</code>), который управляет логикой игры, обрабатывает события (например, касания) и настраивает физику. Именно с неё начинается построение любой игры.</p>
  <p id="VmOO">Давайте же перейдем к коду и создадим простую сцену. </p>
  <p id="GGa9"></p>
  <p id="om5C"><strong>Шаг 1: Создание сцены</strong></p>
  <p id="78An">В Xcode выбираем <strong>New File From Template</strong> → <strong>SpriteKit Scene</strong>.<br />Этот шаблон создаст <code>.sks</code>-файл, визуальный редактор сцены, и вы сможете на нём размещать объекты.</p>
  <figure id="lPBg" class="m_column">
    <img src="https://img4.teletype.in/files/f6/38/f6387208-cde1-494d-9a31-ff49aac2bbd4.png" width="739" />
  </figure>
  <p id="lrWv"><strong>Шаг 2: Указание кастомного класса</strong></p>
  <p id="HNpH">Открываем созданный <code>.sks</code>-файл и в инспекторе справа и указываем имя вашего собственного класса сцены в поле <strong>Custom Class</strong> — например, <code>GameScene</code>.</p>
  <figure id="CANa" class="m_original">
    <img src="https://img3.teletype.in/files/af/d1/afd1e0c5-8294-4636-b39a-c377a6f9c327.png" width="258" />
  </figure>
  <p id="CVgH"><strong>Шаг 3: Реализация класса сцены</strong></p>
  <p id="8r82">Теперь создаём Swift-файл с соответствующим именем (например, <code>GameScene.swift</code>) и реализуйте свой класс, унаследовав его от <code>SKScene</code>.</p>
  <pre id="AePV" data-lang="swift">final class GameScene: SKScene {

    /// Вызывается один раз, когда сцена загружена из файла или кода.
    /// Здесь можно инициализировать свойства, подготовить объекты, которые не зависят от view.
    override func sceneDidLoad() {
        super.sceneDidLoad()
    }

    /// Вызывается, когда пользователь прикасается к экрану.
    /// Здесь можно обрабатывать начало касания — например, заставить персонажа прыгнуть.
    override func touchesBegan(_ touches: Set&lt;UITouch&gt;, with event: UIEvent?) {
        super.touchesBegan(touches, with: event)
    }

    /// Вызывается каждый кадр (обычно 60 раз в секунду).
    /// Здесь можно обновлять положение объектов, проверять столкновения, изменять состояние игры.
    override func update(_ currentTime: TimeInterval) {
        super.update(currentTime)
    }
}</pre>
  <p id="02Ln"></p>
  <p id="IiG0"><strong>Шаг 4: Контроллер запуска игры</strong></p>
  <p id="mwxJ">Затем нам необходимо создать обычный <strong>UIViewController</strong> и переопределить наше дефолтное view, на SKView</p>
  <p id="OhwR">Затем нам необходимо создать <code>UIViewController</code>, и переопределить у него <code>view</code>, чтобы она стала <code>SKView</code>.</p>
  <pre id="MZep" data-lang="swift">class GameViewController: UIViewController {

    override func loadView() {
        super.loadView()
        view = SKView()
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        startScene()
    }

    func startScene() {
        guard let scene = SKScene(fileNamed: &quot;GameScene&quot;) as? GameScene else { return }
        let skView = view as! SKView
        skView.presentScene(scene)
        skView.showsFPS = true
    }
}</pre>
  <p id="rA64">Готово! Базовая настройка позади. Теперь можно запустить проект и посмотреть, что у нас получилось.</p>
  <figure id="GomO" class="m_custom">
    <img src="https://img3.teletype.in/files/2f/84/2f846785-49e8-448a-908a-36243793370a.png" width="188.02288329519445" />
  </figure>
  <p id="2JxK">Скорее всего, вы увидите пустой экран. Не слишком вдохновляет, правда? Но если в правом нижнем углу появился индикатор FPS — значит всё подключено правильно и сцена действительно загружается. Поздравляю, это первый шаг!</p>
  <p id="JiW8">На самом деле вы можете убрать этот индикатор, если вам он мешает. Для этого просто удалите следующую строку: <code>skView.showsFPS = true</code> .</p>
  <p id="r3zY">Но иногда полезно знать о свойствах, которые нам предлагает SKView из коробки, так как это может существенно облегчить дебагинг игры. Советую ознакомиться с <a href="https://developer.apple.com/documentation/spritekit/skview" target="_blank">документацией по SKView</a>, чтобы узнать больше о его возможностях.</p>
  <p id="jfbo"></p>
  <h2 id="73Do">Добавляем объект на сцену</h2>
  <p id="64CP">Настройку мы завершили, теперь самое время перейти к делу и разместить первый объект на сцене.</p>
  <p id="RKUs"></p>
  <p id="cbfk"><strong>Выбор изображения</strong></p>
  <p id="KZ7y">В качестве объекта я использую изображение асыка из нашей игры, но вы можете выбрать любое изображение в формате <code>.png</code> и добавить его в <code>Assets.xcassets</code>.</p>
  <p id="JP7s"></p>
  <p id="Qe7v"><strong>Создаём собственный спрайт</strong></p>
  <p id="tO1I">Чтобы добавить объект на сцену и наделить его физическими свойствами, создадим отдельный класс, унаследованный от <code>SKSpriteNode</code>. Это позволит нам:</p>
  <ul id="X4Hl">
    <li id="AUWS">задать текстуру (изображение);</li>
    <li id="CFEu">управлять поведением объекта;</li>
    <li id="fLu6">добавить физику: столкновения, гравитацию, вращение и т.д.</li>
  </ul>
  <pre id="gFnD" data-lang="swift">class PlayerNode: SKSpriteNode {

    convenience init(with image: UIImage) {
        let texture = SKTexture(image: image)
        self.init(texture: texture)
    }
}</pre>
  <p id="cnNp">Как мы видим ничего сложного. Здесь мы просто создаём спрайт на основе картинки и используем её как текстуру. </p>
  <p id="Thss"></p>
  <p id="ZXcE"><strong>Добавляем объект на сцену</strong></p>
  <p id="XnG2">Теперь перейдём в нашу сцену и обновим её:</p>
  <pre id="scRK" data-lang="swift">final class GameScene: SKScene {
    
    private var player: PlayerNode!

    override func didMove(to view: SKView) {
        super.didMove(to: view)
        createPlayer()
    }

    private func createPlayer() {
        player = PlayerNode(with: UIImage(named: &quot;asyq-sprite&quot;)!)
        player.position = CGPoint(x: size.width / 2, y: size.height / 2)
        addChild(player)
    }
}</pre>
  <p id="n2jr">В функции <code>createPlayer()</code> мы создаём объект, передаём ему изображение, а затем устанавливаем позицию относительно центра экрана. </p>
  <p id="O1QP">Вызов происходит из <code>didMove(to view: SKView)</code> — это метод жизненного цикла, аналогичный <code>viewDidLoad()</code> в контроллере. Именно здесь обычно размещают начальные элементы сцены.</p>
  <p id="pRsm">Собираем и запускаем проект...</p>
  <figure id="Xh9Y" class="m_custom">
    <img src="https://img2.teletype.in/files/19/e1/19e1b610-0dac-43e1-b57e-4d2c13e4abbe.png" width="188.37757437070948" />
  </figure>
  <p id="NiQ8">Ну что ж, пока выглядит скромно, но всё работает!</p>
  <p id="CxV9"></p>
  <p id="urzO"><strong>Добавляем физику</strong></p>
  <p id="znDt">Предлагаю немного поиграться с возможностями движка и попробовать немного оживить нашу сцену при помощи физики. Вернёмся в <code>PlayerNode</code> и расширим его функциональность:</p>
  <pre id="HZNF" data-lang="swift">final class PlayerNode: SKSpriteNode {

    convenience init(with image: UIImage) {
        let texture = SKTexture(image: image)
        self.init(texture: texture)
        configureNode()
    }

    private func configureNode() {
        physicsBody = SKPhysicsBody()
    }
}</pre>
  <p id="K7Yt">И попробуем запустить наш проект снова и посмотрим что изменилось.</p>
  <figure id="pZK7" class="m_custom">
    <img src="https://img4.teletype.in/files/f9/20/f9201104-426b-46a7-9b6c-dc979db1e56d.gif" width="188.28146453089246" />
  </figure>
  <p id="AoYv">Уже что-то интересное, правда? Объект начал двигаться, хотя мы явно не писали код, который бы его перемещал. Почему так происходит?</p>
  <p id="mjqM"></p>
  <p id="Wk4A"><strong>Откуда берётся движение?</strong></p>
  <p id="37M7">Все классы, унаследованные от <code>SKScene</code>, содержат свойство <code>physicsWorld</code> — это движок физики, встроенный прямо в сцену. Он отвечает за гравитацию, столкновения, импульсы, трение и прочие физические эффекты.</p>
  <p id="S0Mz">Нас сейчас интересует его свойство: <code>physicsWorld.gravity</code>. По умолчанию значение равно <code>CGVector(dx: 0, dy: -9.8)</code> . Это примерное значение земного притяжения, объект начинает падать вниз по оси <code>y</code>, имитируя свободное падение на Земле (~9,8 м/с²). Именно поэтому наш объект сразу падает вниз относительно оси координат.</p>
  <p id="Y9hT">Попробуем поиграться с этим значением и изменим поведение. Например, сделаем так, будто объект падает не в воздухе, а в воде:</p>
  <pre id="xQyx" data-lang="swift">override func didMove(to view: SKView) {
    super.didMove(to: view)
    physicsWorld.gravity = CGVector(dx: 0, dy: -1.0)
    createPlayer()
}</pre>
  <p id="J5CG">Теперь падение стало заметно медленнее, создаётся ощущение вязкой среды.</p>
  <p id="7hgb"></p>
  <p id="izwL"><strong>Взаимодействие с объектом</strong></p>
  <p id="DhWz">Теперь хочется как-то взаимодействовать с нашим объектом. Например, чтобы он взлетал вверх при тапе по экрану. Для этого воспользуемся методом <code>touchesBegan</code>, который уже есть в <code>SKScene</code>:</p>
  <pre id="WsHH" data-lang="swift">override func touchesBegan(_ touches: Set&lt;UITouch&gt;, with event: UIEvent?) {
    super.touchesBegan(touches, with: event)
    player!.physicsBody!.applyImpulse(CGVector(dx: 0, dy: 45))
}</pre>
  <p id="nAne">При касании вызывается метод <code>touchesBegan</code> , где мы придаем импульс нашему объекту по оси y.</p>
  <p id="2Prs">Как видим, у узла нашего объекта также есть свойство <code>physicsBody</code>, которое мы можем настраивать по своему усмотрению. Через него можно задать массу, включить или отключить гравитацию, настроить вращение, а также указать маски для обработки столкновений с другими объектами на сцене.</p>
  <figure id="xrwZ" class="m_custom">
    <img src="https://img2.teletype.in/files/52/2d/522de2dd-7048-4885-b66f-9c63ad81cce1.gif" width="187.52173913043478" />
  </figure>
  <p id="3iHi">Теперь при каждом тапе наш герой взлетает вверх — получается простая, но уже играбельная механика.</p>
  <p id="ay3V"></p>
  <h2 id="Iyhl">Заключение </h2>
  <p id="GCy0">Вот и подошла к концу наша статья о SpriteKit. Мы познакомились с базовыми возможностями движка, создали сцену, добавили персонажа, подключили физику и поэкспериментировали с гравитацией.</p>
  <p id="qyJ3">Конечно, это лишь малая часть того, что умеет SpriteKit. В рамках одной статьи невозможно охватить всё: от анимации и столкновений до построения полноценной игры. Но если вам интересна тема нативной разработки игр на iOS, дайте знать в комментариях!</p>
  <p id="mXLo">Мы с радостью продолжим разбирать возможности SpriteKit: генерацию препятствий, бесконечную прокрутку, обработку столкновений и многое другое. А может, даже вместе напишем небольшую игру шаг за шагом.</p>
  <p id="wkwi">Спасибо за внимание и до встречи в следующих статьях!🤘</p>
  <p id="sSlg"></p>
  <h1 id="9B5g">Полезные ссылки:</h1>
  <ul id="QPPb">
    <li id="dHTq"><a href="https://developer.apple.com/documentation/spritekit/" target="_blank">Документация по SpriteKit для работы с 2D</a></li>
    <li id="sLzc"><a href="https://developer.apple.com/documentation/realitykit/" target="_blank">Документацией по RealityKit для работы с 3D</a></li>
  </ul>
  <hr />
  <p id="OYmd"><a href="https://t.me/greenhouse_mobile_dev" target="_blank">Telegram-канал Теплица</a></p>
  <p id="dYFl"><a href="mailto:greenhouse.mobile.dev@gmail.com" target="_blank">Обратная связь</a></p>

]]></content:encoded></item><item><guid isPermaLink="true">https://teletype.in/@greenhouse_mobile_dev/android_deep_linking</guid><link>https://teletype.in/@greenhouse_mobile_dev/android_deep_linking?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=greenhouse_mobile_dev</link><comments>https://teletype.in/@greenhouse_mobile_dev/android_deep_linking?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=greenhouse_mobile_dev#comments</comments><dc:creator>greenhouse_mobile_dev</dc:creator><title>Deep linking в Android: магия переходов</title><pubDate>Sun, 18 May 2025 18:00:20 GMT</pubDate><media:content medium="image" url="https://img2.teletype.in/files/57/27/5727df85-2d68-4fbe-a5a2-41df07a8ed29.png"></media:content><category>Android</category><description><![CDATA[<img src="https://img3.teletype.in/files/e6/45/e6456aee-6bd1-4510-9ae2-3fa9eb3e70c4.png"></img>Совсем недавно у нас в команде проводился небольшой workshop, посвященный навигации при deep linking. Из него родилась идея подготовить статью по этой теме. Давайте начнем!]]></description><content:encoded><![CDATA[
  <figure id="Ab9S" class="m_column">
    <img src="https://img3.teletype.in/files/e6/45/e6456aee-6bd1-4510-9ae2-3fa9eb3e70c4.png" width="4800" />
  </figure>
  <p id="DQBt">Всем привет, дорогие друзья!</p>
  <p id="H1Fp">Совсем недавно у нас в команде проводился небольшой workshop, посвященный навигации при deep linking. Из него родилась идея подготовить статью по этой теме. Давайте начнем!</p>
  <h2 id="jlTI">Deep link</h2>
  <p id="SxK8">Согласно официальной документации, <strong>deep link</strong> в Android — это URI-ссылка, которая может быть использована для навигации к конкретному месту внутри приложения, минуя стартовый экран.</p>
  <h3 id="UNNA">Назначение deep linking</h3>
  <p id="4PVj"><strong>Поддержка маркетинговых активностей</strong></p>
  <p id="KWkw">В рамках рекламных кампаний deep linking помогает привлекать пользователей к конкретным акциям, продуктам или функциям приложения.</p>
  <p id="xZgL"><strong>Связь с внешними сервисами</strong></p>
  <p id="FqHu">Deep linking позволяет обеспечить более удобное взаимодействие между приложением и внешними сервисами, создавая плавные и контекстные переходы.</p>
  <h2 id="eRCT">URI</h2>
  <p id="5rCo"><strong>URI (Uniform Resource Identifier)</strong> — это строка, которая идентифицирует ресурс в сети. </p>
  <p id="cloQ">При этом <code>URI</code> может указывать как на местоположение ресурса (<code>URL</code>), так и на его имя (<code>URN</code>). А может содержать и то и другое. То есть <code>URL</code> и <code>URN</code> — это частные случаи <code>URI</code>.</p>
  <p id="3O7J">Правила построения <code>URI</code> закреплены в <a href="https://datatracker.ietf.org/doc/html/rfc3986" target="_blank">RFC 3986</a>.</p>
  <figure id="TOGO" class="m_column">
    <img src="https://img3.teletype.in/files/62/0d/620d4c1b-5584-4377-90ef-c366cb8b2c8d.jpeg" width="4333" />
  </figure>
  <p id="kJK2"><code>URI</code> состоит из обязательных компонентов:</p>
  <ul id="IhtU">
    <li id="3OAa"><code>scheme</code> — определяет контекст / смысл идентификатора. Если схема зарегистрирована в IANA, то она считается официальной;</li>
    <li id="NSgG"><code>path</code> — иерархические данные для идентификации ресурса.</li>
  </ul>
  <p id="Otxe">И опциональных:</p>
  <ul id="o6Av">
    <li id="ylvs"><code>userinfo</code> — информация о том, кто взаимодействует с ресурсом;</li>
    <li id="MKlt"><code>host</code> — адрес расположения ресурса;</li>
    <li id="ijgU"><code>port</code> — точка взаимодействия с host;</li>
    <li id="S43z"><code>query</code> — неиерархические данные для идентификации ресурса;</li>
    <li id="gRF3"><code>fragment</code> — идентификация вторичного ресурса внутри исходного ресурса (якорь).</li>
  </ul>
  <h2 id="keF8">Web link</h2>
  <p id="Ftfm"><strong>Web link</strong> — это deep link, использующий схемы HTTP и HTTPS. </p>
  <p id="GGNp">В Android 12+ при нажатии на обычный web link содержимое всегда отображается в веб-браузере. </p>
  <p id="liTF">Если версия Android ниже 12, и приложения, установленные на устройстве, могут обрабатывать web link, пользователи, вместо перехода в браузер, увидят disambiguation dialog.</p>
  <p id="m25s"><strong>Disambiguation dialog или диалог разрешения неоднозначности</strong> — это системное окно в Android, которое появляется, когда несколько приложений могут обработать один и тот же <code>Intent</code> (например, ссылку).</p>
  <p id="bmRd">В нем отображается список приложений-кандидатов для обработки перехода по ссылке. Список формируется в зависимости от ссылки и <code>Intent-Filter</code>, установленных в приложениях.</p>
  <figure id="6bqP" class="m_custom">
    <img src="https://img3.teletype.in/files/ad/2b/ad2b9f5f-272b-4702-a77a-7b05bf6497f0.jpeg" width="191" />
  </figure>
  <h2 id="fZkw">Android app link</h2>
  <p id="s8fH">С Android 6.0 появилась возможность привязки веб-домена к приложению и верификации домена на стороне сервера. </p>
  <p id="vBMx">В случае успешной верификации, система автоматически открывала приложение, минуя disambiguation dialog.</p>
  <p id="X6hb">Именно с этой версии появилась поддержка Android app link.</p>
  <figure id="gIuo" class="m_custom">
    <img src="https://img4.teletype.in/files/74/fb/74fb6de4-4129-4a5d-bedb-fb775f4a9d4f.png" width="272" />
  </figure>
  <p id="pVlN"><strong>Android app link</strong> — это deep link, который также как и web link использует HTTP и HTTPS, но дополнительно содержит атрибут <code>autoVerify</code>. </p>
  <p id="NOaJ">Этот атрибут позволяет приложению назначить себя обработчиком по умолчанию для данного типа ссылки. Это позволяет открывать ссылку без вызова disambiguation dialog. </p>
  <p id="sK2j">Если пользователь не хочет, чтобы приложение было обработчиком по умолчанию, он может переопределить это поведение в настройках приложения.</p>
  <figure id="tX4v" class="m_custom">
    <img src="https://img1.teletype.in/files/8f/0c/8f0c8028-7ff5-474f-9b42-9fe7cc9e3f19.jpeg" width="190.99999999999997" />
  </figure>
  <h2 id="zpYR">Верификация домена для Android app link (если на проекте не используется AppsFlyer)</h2>
  <p id="CQ0O">Шаг 1. Добавить <code>Intent-Filter</code> в <code>AndroidManifest.xml</code> с флагом <code>autoVerify</code> и указанием информации о<code>https</code>-ссылке, которую необходимо обрабатывать:</p>
  <pre id="fbjg" data-lang="xml">&lt;intent-filter android:autoVerify=&quot;true&quot;&gt;
    &lt;action android:name=&quot;android.intent.action.VIEW&quot; /&gt;

    &lt;category android:name=&quot;android.intent.category.DEFAULT&quot; /&gt;
    &lt;category android:name=&quot;android.intent.category.BROWSABLE&quot; /&gt;

    &lt;data
        android:scheme=&quot;https&quot;
        android:host=&quot;your.domain.com&quot;
        android:pathPrefix=&quot;/yourpreffix&quot; /&gt;
&lt;/intent-filter&gt;</pre>
  <p id="n6b7"></p>
  <p id="mTTN">Шаг 2. Сгенерировать файл ассоциации <code>assetlinks.json</code>. Для этого требуется доступ в Google Play Console.</p>
  <p id="cUIm">В разделе <strong>Привлечение пользователей</strong> имеется подраздел <strong>Ссылки на контент</strong>. </p>
  <figure id="C2Fu" class="m_column">
    <img src="https://img4.teletype.in/files/37/23/3723ce9b-e124-445b-a1d5-a87e2763843b.png" width="3024" />
  </figure>
  <p id="oJMM">После перехода в подраздел необходимо нажать на <strong>Добавить домен</strong> для открытия диалогового окна генерации <code>assetlinks.json</code>. </p>
  <p id="QqKQ">В открывшемся окне указывается <strong>domain</strong>, который ранее был указан в <code>AndroidManifest.xml</code>.</p>
  <figure id="yfjg" class="m_column">
    <img src="https://img3.teletype.in/files/6f/10/6f10dcd2-91c4-49fa-af69-9c2bfe43e106.png" width="1822" />
  </figure>
  <p id="LDut">После этого будет сгененирован файл ассоциации. </p>
  <p id="CyGx">Важно разместить данный файл согласно указанному адресу: <code>{your domain}/.well-known/assetlinks.json</code>. В противном случае, связь между приложением и доменом установлена не будет.</p>
  <figure id="9NCu" class="m_column">
    <img src="https://img2.teletype.in/files/11/ae/11ae9c10-6d99-4e15-97bf-302860329a75.png" width="1820" />
  </figure>
  <h2 id="7lUP">Верификация домена для Android app link (если на проекте используется AppsFlyer)</h2>
  <p id="IHZq">Если на проекте используется AppsFlyer и для deep link используется домен формата <code>yourbrand.onelink.me</code>, то <code>assetlinks.json</code> контролирует AppsFlyer. В этом случае, последовательность действий будет немного другой.</p>
  <p id="frWC">Шаг 1. Собрать все SHA-256 отпечатки подписи приложения, для которого требуется настройка Android app link.</p>
  <p id="0NLe">Шаг 2. Выбрать в <strong>AppsFlyer Dashboard</strong> нужное приложение для настройки на странице <strong>Apps</strong>. Затем, в левой навигационной панели необходимо выбрать пункт <strong>OneLink Management</strong>. На открывшейся странице нужно выбрать <strong>Edit OneLink template</strong>.</p>
  <figure id="Cojw" class="m_column">
    <img src="https://img3.teletype.in/files/e3/62/e362e05b-48a6-4d3d-a247-c26d93d6584f.png" width="1913" />
  </figure>
  <p id="D0Gj">Шаг 3. На экране редактирования шаблона кликнуть на <strong>Launch the app using App Links </strong>и указать все раннее собранные SHA-256.</p>
  <figure id="zwuo" class="m_column">
    <img src="https://img3.teletype.in/files/e8/64/e864b706-d0b7-48bf-82f9-e19b0022ba0f.png" width="1916" />
  </figure>
  <p id="N143">Шаг 4. Сохранить изменения, внесенные в шаблон.</p>
  <p id="8voy">Шаг 5. Скопировать на странице <strong>Edit template</strong>, полученный <code>Intent-Filter</code> и вставить в <code>AndroidManifest.xml</code>.</p>
  <p id="RMfN">Шаг 6. Добавить <a href="https://support.appsflyer.com/hc/ru/articles/360001250345-API-OneLink" target="_blank">OneLink API ключ</a> в приложение. Как правило, данный ключ запрашивается у администратора <strong>AppsFlyer Dashboard</strong>.</p>
  <p id="eqYS">На этом установка связи между приложением, использующим AppsFlyer и доменом, завершена.</p>
  <h2 id="V48r">Deferred deep link</h2>
  <p id="Jjpj">В завершении статьи стоит упомянуть о deferred deep linking. Это механизм, который позволяет открыть нужный экран внутри приложения, даже если приложение на момент клика по ссылке еще не установлено.</p>
  <figure id="D4sO" class="m_original">
    <img src="https://img2.teletype.in/files/d3/42/d342e400-4d73-4207-86af-685648a2b406.jpeg" width="670" />
  </figure>
  <p id="uTY3">Такого рода навигация бывает очень полезной при запуске различного рода рекламных компаний, но также требует дополнительной настройки со стороны приложения, исходя из документации сервиса deep linking, используемого на проекте. </p>
  <p id="wrBZ">Good coding and happy day!🤘</p>
  <p id="kuUg"></p>
  <h2 id="an3M">Полезные ссылки:</h2>
  <ul id="QPPb">
    <li id="dHTq"><a href="https://datatracker.ietf.org/doc/html/rfc3986" target="_blank">RFC 3986</a></li>
    <li id="uch7"><a href="https://www.iana.org/assignments/uri-schemes/uri-schemes.xhtml" target="_blank">Зарегистрированные схемы URI в IANA</a></li>
    <li id="sLzc"><a href="https://developer.android.com/training/app-links?hl=ru" target="_blank">Документация от Google</a></li>
    <li id="Xbc7"><a href="https://dev.appsflyer.com/hc/docs/dl_android_init_setup" target="_blank">Документация от AppsFlyer</a></li>
  </ul>
  <p id="37dc"></p>
  <hr />
  <p id="OYmd"><a href="https://t.me/greenhouse_mobile_dev" target="_blank">Telegram-канал Теплица</a></p>
  <p id="dYFl"><a href="mailto:greenhouse.mobile.dev@gmail.com" target="_blank">Обратная связь</a></p>

]]></content:encoded></item><item><guid isPermaLink="true">https://teletype.in/@greenhouse_mobile_dev/about_time_and_trustedtime_api</guid><link>https://teletype.in/@greenhouse_mobile_dev/about_time_and_trustedtime_api?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=greenhouse_mobile_dev</link><comments>https://teletype.in/@greenhouse_mobile_dev/about_time_and_trustedtime_api?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=greenhouse_mobile_dev#comments</comments><dc:creator>greenhouse_mobile_dev</dc:creator><title>Немного о часах и TrustedTime API</title><pubDate>Wed, 19 Mar 2025 08:36:01 GMT</pubDate><media:content medium="image" url="https://img3.teletype.in/files/ac/d8/acd8fece-97eb-4539-8aa2-e1a1fae456cb.png"></media:content><category>Android</category><description><![CDATA[<img src="https://img1.teletype.in/files/05/d3/05d3048f-16db-4a19-93b9-866fc39d7bc8.png"></img>Google недавно представил TrustedTime API. Я его изучила и подготовила статью о новом API, NTP, clock drift, timestamp. Погнали!]]></description><content:encoded><![CDATA[
  <figure id="LbIC" class="m_column">
    <img src="https://img1.teletype.in/files/05/d3/05d3048f-16db-4a19-93b9-866fc39d7bc8.png" width="1200" />
  </figure>
  <p id="z8qj">Всем привет, дорогие друзья!</p>
  <p id="aa7Y">Недавно Google представил новое API, которое позволяет получать текущее реальное время напрямую с <code>NTP</code>-серверов Google, не используя системные часы устройства. В этой статье я хочу не просто показать, как его подключить, но и подробнее рассказать, что такое <code>NTP</code>, как работает этот протокол и что такое <code>timestamp</code> и <code>clock drift</code>.</p>
  <p id="M7BD"></p>
  <h2 id="089n">NTP</h2>
  <p id="0FQ1"><code>Network Time Protocol (NTP)</code> — это сетевой протокол, предназначенный для синхронизации времени между устройствами. </p>
  <p id="q0sC">Он работает по модели клиент-сервер: <code>NTP</code>-клиенты запрашивают актуальное время у <code>NTP</code>-серверов, которые получают его от высокоточных источников, таких как атомные часы или <code>GPS</code>. Затем сервер передаёт точные данные клиенту, который обновляет своё время.</p>
  <p id="rk2D">Для передачи используется протокол <code>User Datagram Protocol (UDP)</code>, который обеспечивает быструю отправку данных без установления соединения. Соответственно, запрос на получение точного времени можно отправить через стандартные <code>IP</code>-сети.</p>
  <p id="rXKk">Вся система <code>NTP</code> организована в иерархическую структуру с уровнями (<code>stratum</code>), где каждый уровень определяет степень удалённости от эталонного источника времени.</p>
  <figure id="mhYp" class="m_column">
    <img src="https://img3.teletype.in/files/ad/70/ad701aeb-33b6-41ad-958b-778c4621e499.png" width="1226" />
    <figcaption>Иерархия NTP системы</figcaption>
  </figure>
  <p id="7UCw"></p>
  <h2 id="KaHu">Clock drift</h2>
  <p id="COel">Возможно, вы замечали, что после длительного отключения смартфона его время может отличаться от других устройств. </p>
  <p id="5DXm">Это явление называется <code>clock drift</code> – расхождение системных часов устройства с точным временем из-за аппаратных или программных факторов.<br />Для исправления данной проблемы производится синхронизация с эталонными источниками времени при помощи <code>NTP</code>.</p>
  <figure id="055d" class="m_column">
    <img src="https://img1.teletype.in/files/40/a2/40a2f5e5-fe8c-428d-b6e1-76e0a59c2117.png" width="946" />
    <figcaption>Графический пример Clock drift</figcaption>
  </figure>
  <p id="6ctS"></p>
  <h2 id="Mmx3">Timestamp</h2>
  <p id="y0Gu">При разработке функционала, где мы реализуем отображение времени заказа или операции, мы часто работаем с <code>timestamp</code> (временной меткой). </p>
  <p id="haI9"><code>Timestamp</code>  — это числовое значение, указывающее на конкретный момент времени, зафиксированный с определенной точностью. </p>
  <p id="jRg3">Данное значение обычно является количеством секунд или миллисекунд, прошедших с определенной даты отсчёта. Чаще всего в работе встречается <code>Unix Timestamp</code>, где отсчёт начинается с 1 января 1970 года 00:00:00 UTC.</p>
  <p id="VkFy">Timestamp может иметь различную точность: до секунды, миллисекунды, микросекунды или наносекунды. Поэтому, важно заранее обсудить точность с backend-разработчиком. Например, если вы разрабатываете чат поддержки и имеете разную точность с сервером, то могут встречаться ситуации с дублированием информации или некорректной сортировкой.</p>
  <p id="mbLX"></p>
  <h2 id="KblS">Для чего нужен TrustedTime API</h2>
  <p id="mIvJ">Локальное время подвержено изменениям: в стране могут ввести или отменить перевод на летнее время, изменить часовой пояс для какой-то местности (или вовсе объединить в один часовой пояс всю страну).</p>
  <p id="RFYc">Для владельцев устройств с Android версией ниже 10 такие изменения могут стать серьезной проблемой. До Android 10 обновления данных о часовых поясах зависели от системных обновлений, и чем старше устройство, тем ниже вероятность получения актуальной информации. В некоторых случаях, пользователи могут обновить часовые зоны вручную, но для этого часто требуется ROOT-доступ.</p>
  <p id="Em9T">В итоге многие пользователи начинают вручную корректировать системное время, что делает его ненадёжным источником.</p>
  <p id="lRnc">TrustedTime API позволяет приложениям получать точное и защищенное время (<code>Unix Timestamp</code>), даже если системные часы устройства не являются достоверными.</p>
  <p id="FMLx"></p>
  <h2 id="ZJv5">Подключение TrustedTime API</h2>
  <p id="709P">Ниже будет приведён упрощённый вариант реализации логики. <br />Для начала необходимо добавить зависимость в <code>build.gradle</code>:</p>
  <pre id="sxkj" data-lang="kotlin">dependencies {
    implementation(&quot;com.google.android.gms:play-services-time:{version}&quot;)
}</pre>
  <p id="BKf5"></p>
  <p id="PTVU">Далее, можно создать интерфейс и его реализацию, которые позволят инкапсулировать логику создания <code>TrustedTimeClient</code> и получения времени:</p>
  <pre id="kVeK" data-lang="kotlin">interface TimeService {

    suspend fun getCurrentTimeMillisOrNull(): Long?
}

class DefaultTimeService(private val context: Context) : TimeService {

    @Volatile
    private var trustedTimeClient: TrustedTimeClient? = null

    override suspend fun getCurrentTimeMillisOrNull(): Long? {
        val client = trustedTimeClient ?: createClient()

        return client.computeCurrentUnixEpochMillis()
    }

    private suspend fun createClient(): TrustedTimeClient {
        return suspendCancellableCoroutine { continuation -&gt;
            TrustedTime.createClient(context)
                .addOnSuccessListener { client -&gt;
                    trustedTimeClient = client
                    continuation.resume(client)
                }
                .addOnFailureListener { exception -&gt;
                    continuation.resumeWithException(exception)
                }
        }
    }
}</pre>
  <p id="48hh">Я использовала Koin при создании примера кода, поэтому файл DI может выглядеть следующим образом:</p>
  <pre id="jD08" data-lang="kotlin">val appModule = module {
    single&lt;TimeService&gt; {
        DefaultTimeService(
            context = androidContext(),
        )
    }

    viewModel {
        MainViewModel(
            timeService = get(),
        )
    }
}</pre>
  <p id="mmJ1"></p>
  <p id="DKjR">Класс <code>Application</code> в таком случае может выглядеть так:</p>
  <pre id="3khz" data-lang="kotlin">class MyApplication : Application() {

    override fun onCreate() {
        super.onCreate()
        startKoin {
            androidContext(this@MyApplication)
            modules(appModule)
        }
    }
}</pre>
  <p id="kOI0"></p>
  <p id="Cygj">Не забываем указать <code>Application</code> в <code>AndroidManifest</code>:</p>
  <pre id="RBhs" data-lang="xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;manifest xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    xmlns:tools=&quot;http://schemas.android.com/tools&quot;&gt;

    &lt;application
        android:name=&quot;.MyApplication&quot;
        //...</pre>
  <p id="Xndm"></p>
  <p id="5R7P">Теперь можно подключать сервис и использовать его. Например, так:</p>
  <pre id="3e3C" data-lang="kotlin">class MainViewModel(
    private val timeService: TimeService,
) : ViewModel() {
    private val _currentTime = MutableStateFlow&lt;Long?&gt;(null)
    val currentTime: StateFlow&lt;Long?&gt; = _currentTime.asStateFlow()

    fun updateCurrentTime() {
       viewModelScope.launch {
           _currentTime.update {
               timeService.getCurrentTimeMillisOrNull()
           }
       }
    }
}</pre>
  <h2 id="8KwS">Работа на устройствах без GMS</h2>
  <p id="fqbB">Если ваше приложение распространяется в Huawei AppGallery и может использоваться на устройствах без GMS, то стоит учитывать, что TrustedTime API не будет работать на таких устройствах.</p>
  <p id="FQIk">При попытке создать <code>TrustedTimeClient</code> вы получите исключение <code>ApiException</code> с кодом <code>17</code>. Я рекомендую добавить метод проверки наличия GMS на устройстве:</p>
  <pre id="stuQ" data-lang="kotlin">private fun Context.isGooglePlayServicesAvailable(): Boolean {
    return GoogleApiAvailability
        .getInstance()
        .isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS
}</pre>
  <p id="rLCO">и вызывать его в методах создания клиента и получение времени.</p>
  <p id="IC5i"></p>
  <h2 id="Fecr">Какие ещё методы есть у TrustedTimeClient</h2>
  <p id="qyuq">К сожалению, доступных методов на момент написания статьи не так много. Нам доступны:</p>
  <ul id="Dge7">
    <li id="9eDi"><code>getLatestTimeSignal()</code> — возвращает последний полученный <code>TimeSignal</code> или <code>null</code>, если внешний сигнал времени отсутствует. Позволяет получить информацию об ошибках в измерении времени и проследить погрешность;</li>
    <li id="Ox1q"><code>computeCurrentUnixEpochMillis()</code> — данный метод рассматривался ранее в статье.  Является заменой <code>System.currentTimeMillis()</code>, но без влияния системных часов устройства. Возвращает <code>значение в миллисекундах</code> с начала Unix-эпохи, основываясь на доверенном внешнем источнике, или возвращает <code>null</code>, если сигнал недоступен;</li>
    <li id="OsKM"><code>computeCurrentInstant()</code> — вычисляет <code>Instant</code> на основе последнего точного временного сигнала от внешнего источника или возвращает <code>null</code>, если сигнал недоступен. Является заменой для <code>Instant.now()</code> без влияния системных часов устройства;</li>
    <li id="addTimeSignalListener(com.google.android.gms.time.trustedtime.OnNewTimeSignalAvailableListener)"><code>addTimeSignalListener(listener: OnNewTimeSignalAvailableListener)</code> — метод для регистрации слушателя сигналов времени. После регистрации слушатель сразу получает последний сигнал времени, если он доступен. Стоит отметить, что повторная регистрация слушателя не будет выполняться, если слушатель в клиенте уже зарегистрирован;</li>
    <li id="kAYD"><code>removeTimeSignalListener(listener: OnNewTimeSignalAvailableListener)</code> — метод для удаления ранее зарегистрированного слушателя. В случае, если слушатель зарегистрирован не был, то вызов метода не порождает исключений;</li>
    <li id="XO16"><code>dispose()</code> — позволят освободить ресурсы клиента, когда он становится ненужным и отменяет регистрацию всех слушателей.</li>
  </ul>
  <p id="rKc1"></p>
  <p id="Xkui">На этом наш пост подошёл к концу. Good coding and happy day!🤘</p>
  <p id="UgBx"></p>
  <h1 id="9B5g">Полезные ссылки:</h1>
  <ul id="QPPb">
    <li id="dHTq"><a href="https://android-developers.googleblog.com/2025/02/trustedtime-api-introducing-reliable-approach-to-time-keeping-for-apps.html" target="_blank">Анонс TrustedTime API</a></li>
    <li id="sLzc"><a href="https://developers.google.com/android/reference/com/google/android/gms/time/package-summary" target="_blank">Документация по библиотеке time</a></li>
    <li id="iS44"><a href="https://source.android.com/docs/core/permissions/timezone-rules" target="_blank">Правила часового пояса Android</a></li>
  </ul>
  <hr />
  <p id="OYmd"><a href="https://t.me/greenhouse_mobile_dev" target="_blank">Telegram-канал Теплица</a></p>
  <p id="dYFl"><a href="mailto:greenhouse.mobile.dev@gmail.com" target="_blank">Обратная связь</a></p>

]]></content:encoded></item><item><guid isPermaLink="true">https://teletype.in/@greenhouse_mobile_dev/async_await_vs_completion_handler</guid><link>https://teletype.in/@greenhouse_mobile_dev/async_await_vs_completion_handler?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=greenhouse_mobile_dev</link><comments>https://teletype.in/@greenhouse_mobile_dev/async_await_vs_completion_handler?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=greenhouse_mobile_dev#comments</comments><dc:creator>greenhouse_mobile_dev</dc:creator><title>Почему async/await лучше completion handler?</title><pubDate>Fri, 21 Feb 2025 11:03:08 GMT</pubDate><media:content medium="image" url="https://img1.teletype.in/files/0d/52/0d525edc-8d6e-4866-8457-f1b583cc81ae.png"></media:content><category>iOS</category><description><![CDATA[<img src="https://img4.teletype.in/files/32/23/3223e02f-fd94-450a-a1b8-c300b0a404ee.png"></img>Сегодня хотелось бы поговорить о преимуществах async/await, в сравнении с привычными нами способами работы с асинхронным кодом.]]></description><content:encoded><![CDATA[
  <figure id="iP64" class="m_column">
    <img src="https://img4.teletype.in/files/32/23/3223e02f-fd94-450a-a1b8-c300b0a404ee.png" width="1200" />
  </figure>
  <p id="J9yQ">Всем привет, дорогие друзья!</p>
  <p id="UwwM">Сегодня хотелось бы поговорить о преимуществах <code>async/await</code>, в сравнении с привычными нами способами работы с асинхронным кодом. <code>async/await</code> был представлен Apple на сессии WWDC 21, но к сожалению, не во всех проектах решаются перейти на данную технологию. Давайте узнаем как сделать наш код более читаемым, безопасным и надежным.</p>
  <p id="C4NR"></p>
  <h2 id="tLu0">Что такое async/await?</h2>
  <p id="X4L3"><code>async/await</code> – это современный способ работы с асинхронным кодом в Swift. Он делает код более читаемым, безопасным и удобным, заменяя сложные вложенные completion handlers или DispatchGroup.</p>
  <p id="hvUI">Механизм <code>async/await</code> основан на принципе структурированного параллелизма. Это означает, что асинхронный код выполняется последовательно в рамках своей области видимости, в отличие от замыканий, которые могут разрывать поток выполнения и усложнять его отслеживание.</p>
  <p id="IVfQ">Пример асинхронного запроса через <code>completion handler</code>:</p>
  <pre id="SDCx" data-lang="swift"> // Service Layer
 		func fetchState(callback: @escaping (Result&lt;GameShakeStateDataResponseModel, NetworkError&gt;) -&gt; Void) {
        let router = GameShakeRequestFactory.getState
        networkService.performRequest(router: router) {(result: Result&lt;GameShakeStateDataResponseModel, NetworkError&gt;) in
            switch result {
            case .success(let model):
                callback(.success(model))
            case .failure(let error):
                callback(.failure(error))
            }
        }
    }
    
  // Presenter Layer
    func fetchState() {
        service.fetchState() { [weak self] result in
            guard let self else { return }
            switch result {
            case let .success(response):
                updateView(with: response.data)
            case let .failure(error):
                view?.showErrorToast(with: error.title)
            }
        }
    }
</pre>
  <p id="O5uW">Если переписать код на <code>async/await</code>:</p>
  <pre id="4X7b" data-lang="swift"> // Service Layer 
 		func fetchState() async throws -&gt; GameShakeStateDataResponseModel {
        try await networkService.performRequestAsync(router: GameShakeRequestFactory.getState)
    }
        
  // Presenter Layer
    func fetchState() {
        Task {
            do {
                let offer = try await service.fetchState()
                // handle updateView
            } catch {
                // handle error
            }
        }
    }
</pre>
  <p id="GHvk"><strong>Основные составляющие кода:</strong></p>
  <ul id="v7w0">
    <li id="1Kco"><code>async</code> — объявляет асинхронную функцию;</li>
    <li id="ibIs"><code>await</code> — приостанавливает выполнение функций до завершения задачи;</li>
    <li id="gpJD"><code>throws</code> — указывает, что функция может выбрасывать ошибки;</li>
    <li id="073t"><code>Task</code> — запускает асинхронную операцию в синхронном коде.</li>
  </ul>
  <p id="ClLm"></p>
  <h2 id="sGuM">Читаемость и простота кода</h2>
  <p id="DQYd">Что хорошо во вложенности в <code>async/await</code>- это то что ее нет.</p>
  <p id="ksz0">В то время как в <code>completion handler</code> одна из главных проблем — глубокая вложенность, также известная как callback hell. Когда одна асинхронная операция зависит от другой, <code>completion</code> вложены друг в друга, усложняя чтение и поддержку.</p>
  <p id="ftR3">Пример:</p>
  <pre id="vfhr" data-lang="swift">fetchData { result in
    switch result {
    case .success(let data):
        process(data) { processedData in
            save(processedData) { success in
                if success {
                    print(&quot;Data saved&quot;)
                }
            }
        }
    case .failure(let error):
        print(&quot;Error: \\(error)&quot;)
    }
}
</pre>
  <p id="RSvW">С <code>async/await</code> код остается линейным и чистым:</p>
  <ul id="bVBN">
    <li id="fb03">Отсутствует вложенность;</li>
    <li id="Vcfu"><code>do-catch</code> охватывает все ошибки.</li>
  </ul>
  <pre id="DUPs" data-lang="swift">Task {
    do {
        let data = try await fetchData()
        let processedData = try await process(data)
        try await save(processedData)
        print(&quot;Data saved&quot;)
    } catch {
        print(&quot;Error: \\(error)&quot;)
    }
}
</pre>
  <p id="sNNI">В данном примере наглядно виден принцип структурированного параллелизма. Код выполняется последовательно, строка за строкой. Мы можем ожидать асинхронную функцию до тех пор, пока она не вернет результат (или не выдаст ошибку) и не передаст его следующей функции.</p>
  <p id="mI8I"></p>
  <h2 id="cpap">Безопасность потоков</h2>
  <p id="f6we">При использовании <code>async/await</code>, большинство асинхронных операций (сетевые запросы, вычисления и т.д) выполняются в фоновом потоке. Однако при необходимости обновления UI, вам нужно явно указать переход на <strong>главный поток</strong>. Иначе это может привести к сбою приложения. Так как вы пытаетесь изменить интерфейс из фонового потока.</p>
  <pre id="i0z9" data-lang="swift">  	func fetchState() async throws -&gt; GameShakeStateDataResponseModel {
        try await networkService.performRequestAsync(router: GameShakeRequestFactory.getState)
    }
        
    func fetchState() {
        Task {
            do {
                let offer = try await service.fetchState()
                self.view?.updateUI(with: offer) // CRASH!!! (Background thread)
            } catch {
                // handle error
            }
        }
    }</pre>
  <p id="HvSu">Использование <code>@MainActor</code> гарантирует безопасность при работе с асинхронным кодом. Что также помогает вам избежать проблем с потоками.</p>
  <pre id="z4eP" data-lang="swift">func fetchState() {
        Task {
            do {
                let offer = try await service.fetchState()
                await MainActor.run {
                   view?.updateUI(with: offer)
                }
            } catch {
                // handle error
            }
        }
 }
</pre>
  <p id="CjOr"></p>
  <h2 id="Hn3R">Обработка ошибок</h2>
  <p id="dIoK">В <code>completion handler</code> ошибки передаются через <code>Result&lt;T, Error&gt;</code> или отдельный параметр <code>Error?</code>. Однако, если забыть вызвать <code>completion</code>, вызывающий код может зависнуть. Это приведет к бесконечной загрузке счетчика или неопределенному состоянию пользовательского интерфейса.</p>
  <p id="avzF">В <code>async/await</code> ошибки всегда требуют обработки:</p>
  <ul id="jwnD">
    <li id="S6QD">Если функция объявлена как <code>throws</code>, компилятор не позволит пропустить обработку;</li>
    <li id="o1AR">Исключение сразу указывает на источник проблемы.</li>
  </ul>
  <p id="V1Kr"></p>
  <h2 id="8SO3">Параллельность и производительность</h2>
  <p id="jVJx"><code>async/await</code> в Swift работает быстрее, чем старые подходы (замыкания, <code>DispatchGroup</code>, <code>OperationQueue</code>), потому как эффективнее управляет потоками и использует кооперативную многозадачность.</p>
  <ul id="OIQ1">
    <li id="qLTi">Нет необходимости вручную управлять потоками или задачами, что предотвращает создание лишних потоков;</li>
    <li id="zGFb">Обеспечена параллельная работа задач без блокировки главного потока. Например, когда операция ожидает завершения при запросах в сети или в работе с диском, остальные операции могут продолжать выполняться;</li>
    <li id="2AU6">Минимизированы накладные расходы на переключение контекста.</li>
  </ul>
  <p id="CHbi">Говоря о параллельном выполнений задач, <code>async/await</code> предлагает ряд инструментов (<code>async let</code>, <code>TaskGroup</code>,<code>Task.detached</code>). В данной статье мы остановимся на <code>async let</code> , в сравнений с <code>DispatchGroup</code>.</p>
  <p id="u5D2">Вспомним как выглядит код с <code>DispatchGroup</code>:</p>
  <pre id="LojG" data-lang="swift">    func fetchOffers() {
        let dispatchGroup = DispatchGroup()
        dispatchGroup.enter()
        forYouService.fetch() { [weak self] result in
            // ...
            dispatchGroup.leave()
        }
        dispatchGroup.enter()
        loyaltyStateService.fetchLoyaltyState() { [weak self] result in
            // ...
            dispatchGroup.leave()
        }
        
        dispatchGroup.enter()
        gameShakeService.fetchGameRules { [weak self] result in
             // ...
		         dispatchGroup.leave()
        }
        
        dispatchGroup.notify(queue: .main) { [weak self] in
            // handle result
        }
    }
</pre>
  <p id="7NaP"><code>DispatchGroup</code> требует:</p>
  <ul id="ihWn">
    <li id="bjZ0">Явного вызова <code>enter()</code> и <code>leave()</code>, что потенциально повышает вероятность ошибок (например, забыв вызвать <code>leave()</code>);</li>
    <li id="fG6r">Отслеживания завершения через <code>notify()</code>, что добавляет дополнительные элементы синхронизации;</li>
    <li id="Vfqy">Ручной обработки ошибок в каждом замыкании.</li>
  </ul>
  <p id="ZMDJ"><code>async let</code> — это способ параллельного выполнения асинхронных операций.</p>
  <p id="U4Du">Он позволяет запустить несколько независимых задач одновременно и затем дождаться их завершения без блокировки. Пример кода:</p>
  <pre id="6J0I" data-lang="swift"> Task {
	 async let forYouData = fetchForYou()
   async let loyaltyStateData = fetchLoyaltyState()
   async let gameRulesData = fetchGameRules()

   let (forYouResponse, loyaltyStateResponse, gameRulesResponse) = await (forYouData, loyaltyStateData, gameRulesData)
   // handle result
 }</pre>
  <p id="VMak"><code>async let</code> упрощает работу:</p>
  <ul id="nbgL">
    <li id="P54B">Синхронизация скрывается за механизмами асинхронных задач;</li>
    <li id="FziX">Система сама управляет потоками;</li>
    <li id="EjWo">Ошибки можно обрабатывать централизованно через <code>do-catch;</code></li>
    <li id="No9B">Код остается лаконичным и удобным для поддержки.</li>
  </ul>
  <p id="KGKC"></p>
  <h2 id="fVyq">Итоги</h2>
  <p id="PONg">Подводя итоги, хочется отметить следующие преимущества:</p>
  <ul id="kTNZ">
    <li id="vWpi">Мы получаем простой и линейный код, где отсутствуют вложенность;</li>
    <li id="WuG6">Мы получаем асинхронный код, который выглядит как синхронный, за счет чего упрощается понимание и дебагинг кода;</li>
    <li id="8erO">У нас есть четкое разделение между потоками, что снижает количество возникаемых проблем с многопоточностью;</li>
    <li id="0sIe">У нас есть надежная отладка ошибок;</li>
    <li id="mFdV">Также <code>async/await</code> дает современные способы работы с параллельными задачами.</li>
  </ul>
  <p id="9uNe">В результате мы получаем чистый, надежный и безопасный код.</p>
  <p id="lMWO"></p>
  <h1 id="9B5g">Полезные ссылки:</h1>
  <ul id="QPPb">
    <li id="dHTq"><a href="https://developer.apple.com/videos/play/wwdc2021/10132/" target="_blank">Презентация async/await на WWDC 21</a></li>
    <li id="Z3Oj"><a href="https://wwdcnotes.com/documentation/wwdcnotes/wwdc21-10132-meet-asyncawait-in-swift" target="_blank">Текстовая версия WWDC записи</a></li>
    <li id="kvam"><a href="https://www.avanderlee.com/swift/async-await/" target="_blank">Cтатья с описание адаптаций async/await в проекте</a></li>
    <li id="bJT7"><a href="https://github.com/swiftlang/swift-evolution/blob/main/proposals/0296-async-await.md" target="_blank">Эволюция async/await</a></li>
  </ul>
  <hr />
  <p id="OYmd"><a href="https://t.me/greenhouse_mobile_dev" target="_blank">Telegram-канал Теплица</a></p>
  <p id="dYFl"><a href="mailto:greenhouse.mobile.dev@gmail.com" target="_blank">Обратная связь</a></p>

]]></content:encoded></item><item><guid isPermaLink="true">https://teletype.in/@greenhouse_mobile_dev/alternative_icons_for_ios_and_android</guid><link>https://teletype.in/@greenhouse_mobile_dev/alternative_icons_for_ios_and_android?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=greenhouse_mobile_dev</link><comments>https://teletype.in/@greenhouse_mobile_dev/alternative_icons_for_ios_and_android?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=greenhouse_mobile_dev#comments</comments><dc:creator>greenhouse_mobile_dev</dc:creator><title>Динамическое изменение иконки приложения</title><pubDate>Tue, 17 Dec 2024 06:43:45 GMT</pubDate><media:content medium="image" url="https://img3.teletype.in/files/66/48/66484e48-c43d-4004-802e-b32fffea6a74.png"></media:content><category>Android &amp;amp; iOS</category><description><![CDATA[<img src="https://img2.teletype.in/files/da/15/da15e8bd-e48c-478c-8f04-0d653962f779.png"></img>Итак, 2024 год подходит к концу. Пришло время подведения итогов, финальных релизов и обновления иконки для праздничного настроения. Стоит отметить, что наша команда не раз задумывалась о динамическом изменении иконки, независимо от релизного поезда. Мы реализовали данный подход, набили шишек и решили поделиться своим опытом.]]></description><content:encoded><![CDATA[
  <figure id="Sjn3" class="m_column">
    <img src="https://img2.teletype.in/files/da/15/da15e8bd-e48c-478c-8f04-0d653962f779.png" width="1200" />
  </figure>
  <p id="7uca">Всем привет, дорогие друзья!</p>
  <p id="JJUY">Итак, 2024 год подходит к концу. Пришло время подведения итогов, финальных релизов и обновления иконки для праздничного настроения. Стоит отметить, что наша команда не раз задумывалась о динамическом изменении иконки, независимо от релизного поезда. Мы реализовали данный подход, набили шишек и решили поделиться своим опытом.</p>
  <p id="Yh6O">О чём статья:</p>
  <ul id="utBp">
    <li id="BoYc"><a href="#ACdQ">Постановка задачи</a></li>
    <li id="zsLG"><a href="#xzRQ">Реализация динамического переключения иконки на iOS</a></li>
    <li id="MFkv"><a href="#1G1Z">Реализация динамического переключения иконки на Android</a></li>
    <li id="Lyxd"><a href="#zcS8">На что опираться для переключения иконки</a></li>
  </ul>
  <p id="tqhX"></p>
  <h2 id="ACdQ">Изменение иконки приложения без обновления версии приложения в сторах</h2>
  <p id="pLwR">С таким заголовком задачи встретилась наша команда в начале спринта. Основная идея состояла в отрыве зависимости от релизного поезда.</p>
  <p id="Nbkf">Гипотеза заключалась в том, что это позволит снизить риски отсутствия обновления иконки перед каким-либо значимым праздником или событием:</p>
  <ul id="AO74">
    <li id="mqtb">шанс отмены релиза крайне низкий, но никогда не равен нулю;</li>
    <li id="A6nn">даты старта или завершения релиза могут не подходить для предстоящего события;</li>
    <li id="vJdc">пользователь может не обновлять приложение долгое время и ходить с новогодним дизайном даже летом.</li>
  </ul>
  <p id="AhQ9"></p>
  <p id="dAMA">Если вы столкнулись с такой задачей или вас посещала мысль о воплощении такой идеи, но лапки так и не дошли, то делимся своим опытом и шагами для реализации.</p>
  <p id="lCEf"></p>
  <h2 id="xzRQ">Реализация динамического переключения иконки на iOS</h2>
  <p id="NamL">Механизм реализации альтернативных иконок стал доступен с iOS 10.3 и идеально подходит для сезонных акций, тематических обновлений или других целей.</p>
  <p id="rvY7"></p>
  <h3 id="dmwB">Шаг 1: Подготовка иконок</h3>
  <p id="jucp">Для начала подготовьте альтернативные иконки в двух размерах: <strong>120px</strong> и</p>
  <p id="2QcJ"><strong>180px</strong>. Назовите их следующим образом: </p>
  <ul id="kXYX">
    <li id="xDW0"><code>IconName@2x</code> для 120px иконки;</li>
    <li id="bCSD"><code>IconName@3x</code> для 180px иконки.</li>
  </ul>
  <p id="qcaO">Создайте директорию в вашем проекте, где будут хранится файлы изображений.</p>
  <figure id="aLfF" class="m_original">
    <img src="https://img2.teletype.in/files/11/ae/11aef36a-d9c0-4a0d-b4e6-d92b9c0cc80a.png" width="536" />
  </figure>
  <h3 id="3wE7">Шаг 2: Настройка Info.plist</h3>
  <p id="3T05">Добавьте в ваш <code>Info.plist</code> ключ <code>CFBundleIcons</code> и его под-ключ <code>CFBundleAlternateIcons</code>. Пример конфигурации:</p>
  <figure id="6EeM" class="m_column">
    <img src="https://img2.teletype.in/files/55/55/5555876e-2233-456a-9613-d0bd4ca81b59.png" width="1468" />
  </figure>
  <p id="VEgD">Здесь мы добавили две альтернативные иконки: <strong>Winter</strong> и <strong>Spring</strong>. Чтобы обращаться к ним, используйте значение ключа <code>key</code>. Название иконки должно совпадать с именем файла, но без суффиксов <code>@2x</code> и <code>@3x</code> .</p>
  <p id="z6BM"></p>
  <h3 id="jSh8">Шаг 3: Создание AppIconManager</h3>
  <p id="MR7i">Для удобной работы создадим синглтон <code>AppIconManager</code>. Пример реализации:</p>
  <figure id="cWP8" class="m_column">
    <img src="https://img1.teletype.in/files/06/87/06873bdb-9971-4650-99b2-4a62f3992bb8.png" width="1816" />
  </figure>
  <p id="UKQB"><strong>Разберем ключевые моменты:</strong></p>
  <ol id="4ses">
    <li id="KqHf"><code>supportsAlternateIcons</code> — проверяет, поддерживает ли приложение смену иконок;</li>
    <li id="Bhcj"><code>alternateIconName</code> — возвращает текущую альтернативную иконку;</li>
    <li id="hE4n"><code>setAlternateIconName</code> — устанавливает новую иконку, используя название ключа <code>key</code> из <code>Info.plist</code>.</li>
  </ol>
  <p id="WZEZ"><strong>Примечание</strong>: Если вы хотите вернуть стандартную иконку, передайте <code>nil</code> в параметр <code>setAlternateIconName</code>.<br /> </p>
  <h3 id="NXOB">Шаг 4: Использование AppIconManager</h3>
  <p id="UIuQ">Теперь достаточно одной строки кода, чтобы сменить иконку:</p>
  <figure id="qkwh" class="m_column">
    <img src="https://img2.teletype.in/files/11/88/11883997-60ab-4296-aba0-60f3216a2120.png" width="1816" />
  </figure>
  <h3 id="ChsU">Результат</h3>
  <p id="XIIa"></p>
  <figure id="b6L4" class="m_custom">
    <img src="https://img3.teletype.in/files/6c/2c/6c2cb2fe-a7a3-4ec1-8efd-202d17fadd9c.gif" width="204.65429588205393" />
  </figure>
  <h3 id="myRr">Как убрать системное уведомление при смене иконки</h3>
  <p id="ThhY">По умолчанию, iOS уведомляет пользователя о смене иконки. Apple не предоставляет официального способа отключить это сообщение, но есть способ его обойти. Для этого обновим функцию <code>setAppIcon</code>:</p>
  <figure id="zLuw" class="m_column">
    <img src="https://img1.teletype.in/files/48/94/4894c0f4-4f3c-458b-b23b-4cde928ec653.png" width="2260" />
  </figure>
  <p id="n6zL"><strong>ВАЖНО! Почему такой вариант небезопасен?</strong></p>
  <p id="GVR2">Не рекомендуем использовать данное решение как минимум по трем причинам:</p>
  <ol id="yuHt">
    <li id="OLnN">Данное решение противоречит релизной политике Apple и может быть отклонено тестировщиком на этапе проверки в AppStoreConnect;</li>
    <li id="1dAI">Небезопасный вызов метода. <code>unsafeBitCast</code> предполагает, что сигнатура функции совпадает с <code>setAlternateIconName</code>. Если есть расхождение, это может привести к повреждению памяти или сбоям;</li>
    <li id="NOHj">Поскольку <code>_setAlternateIconName:completionHandler:</code> не документирован, его поведение может измениться в будущих версиях iOS. Это может привести в лучшем случае к некорректному выполнению функции, в худшем к крашу приложения.</li>
  </ol>
  <p id="LhYo">Поэтому используйте это решение на свой страх и риск 😅</p>
  <p id="U7Mi"></p>
  <h2 id="1G1Z">Реализация динамического переключения иконки на Android</h2>
  <p id="mQsP">В Android иконка приложения указывается в файле <code>AndroidManifest.xml</code> и для её переопределения лучше использовать <a href="https://developer.android.com/guide/topics/manifest/activity-alias-element" target="_blank">activity-alias</a>, который служит для создания &quot;ссылок&quot; на существующую <code>Activity</code>. </p>
  <p id="q9mJ"></p>
  <h3 id="T9gc">Шаг 1: Подготовка иконок</h3>
  <p id="3qU8">На данном этапе подготовка иконок происходит аналогично процессу создания иконки приложения по умолчанию, стоит лишь переименовать ресурсы согласно типу альтернативной иконки <code>ic_winter</code> и <code>ic_spring</code>. </p>
  <p id="XNPW">Если вы никогда не сталкивались с процессом смены иконки приложения, то стоит <a href="https://developer.android.com/codelabs/basic-android-kotlin-compose-training-change-app-icon#0" target="_blank">ознакомиться с уроком от Google по данной части</a>.</p>
  <p id="twsZ"></p>
  <h3 id="WczF">Шаг 2: Настройка activity-alias в AndroidManifest.xml</h3>
  <p id="SOsH">Ключевыми атрибутами <code>activity-alias</code> являются:</p>
  <ul id="1Y22">
    <li id="K4G1"><code>android:name</code> — имя <code>alias</code>-компонента;</li>
    <li id="iXZ0"><code>android:targetActivity</code> — целевая <code>Activity</code>, которую можно открыть через <code>alias</code>;</li>
    <li id="37r3"><code>android:enabled</code> — флаг позволяющий управлять состоянием <code>alias</code>;</li>
    <li id="R3uV"><code>android:icon</code> — задаёт иконку для <code>alias</code>;</li>
    <li id="Wvj1"><code>android:label</code> — задаёт название для <code>alias</code>.</li>
  </ul>
  <p id="tOlb"></p>
  <p id="XsdX">В <code>AndroidManifest.xml</code> связываем <code>alias</code> каждого типа с <code>Activity</code>, которая позволяет открыть приложение:</p>
  <figure id="inET" class="m_column">
    <img src="https://img4.teletype.in/files/b3/ac/b3aca447-56d6-4d49-af59-ee73a80ad2ea.png" width="1700" />
  </figure>
  <h3 id="tGS9">Шаг 3: Добавим немного красоты</h3>
  <p id="MjRG">Для удобства на следующем шаге, можно вынести имя <code>alias</code>-компонентов в отдельный файл, создав классы <code>Activity</code> с идентичным именем из <code>AndroidManifest.xml</code>:</p>
  <figure id="jF2r" class="m_column">
    <img src="https://img1.teletype.in/files/00/9f/009fb84f-5e99-42d7-8f3f-d2aa09c9f0f5.png" width="1700" />
  </figure>
  <p id="1WUN">После, подготовить enum-класс с типами:</p>
  <figure id="EuwU" class="m_column">
    <img src="https://img1.teletype.in/files/48/65/486589cd-f898-4efd-9f65-dbed5fe5c398.png" width="1700" />
  </figure>
  <h3 id="PyRs">Шаг 4: Измените активный activity-alias с помощью PackageManager</h3>
  <p id="3hmE">Создаём метод, который включает нужный <code>alias</code> и отключает остальные.</p>
  <figure id="vZiC" class="m_column">
    <img src="https://img1.teletype.in/files/8e/fe/8efe9a2a-e46d-4c41-b7e6-0ec71d5c829d.png" width="2020" />
  </figure>
  <h3 id="oLZ7">Результат</h3>
  <figure id="lQWW" class="m_custom">
    <img src="https://img3.teletype.in/files/2e/09/2e0928d9-4530-4545-99a2-0a120696d09e.gif" width="204.84615384615387" />
  </figure>
  <h3 id="V83M">Подводные камни</h3>
  <p id="h2Yj">Настоятельно рекомендуем заранее продумать и предложить дизайнеру отобразить AlertDialog или BottomSheet по аналогии с iOS, чтобы предупредить пользователя о смене иконки приложения. Это связано с тем, что приложение будет закрыто на Android 13 и выше, несмотря на указанный <code>PackageManager.DONT_KILL_APP</code>.</p>
  <p id="Yrnv">Реализация AlertDialog или BottomSheet позволит создать безопасную обвязку для поставленной задачи и не вызывать метод смены конфигурации в <code>onStop</code>. Выполнение логики смены конфигурации в методах жизненного цикла может повлечь за собой баги. </p>
  <p id="lgZY">Например, для SingleActivity приложений при смене языка потребуется осуществить пересоздание <code>Activity</code>, тем самым вызвав <code>onStop</code> , что приведёт к уничтожению экземпляра приложения, если иконка не соответствует ожидаемой.</p>
  <p id="Wj7A"></p>
  <h2 id="zcS8">На что опираться для переключения иконки</h2>
  <p id="RxYY">Здесь возможны различные варианты. </p>
  <p id="oz7m">Если в вашей компании существует механизм feature-toggle и соответствующее API - оно может подойти для данной задачи. </p>
  <p id="JgPn">Если такого механизма нет, можно обратить внимание на <a href="https://firebase.google.com/docs/remote-config" target="_blank">Firebase Remote Config</a>. </p>
  <h2 id="QJ7S"></h2>
  <p id="Xkui">На этом наш пост подошёл к концу. Всем праздничного настроения и высокого Crash-free во время выходных. Good coding and happy day!🤘</p>
  <p id="hVQW"></p>
  <h1 id="9B5g">Полезные ссылки:</h1>
  <ul id="QPPb">
    <li id="Sklg"><a href="https://developer.apple.com/documentation/xcode/configuring_your_app_to_use_alternate_app_icons" target="_blank">Документация по альтернативным иконкам для iOS</a></li>
    <li id="ZGj8"><a href="https://developer.android.com/guide/topics/manifest/manifest-intro" target="_blank">Документация по AndroidManifest.xml</a></li>
    <li id="aRUt"><a href="https://developer.android.com/guide/topics/manifest/activity-alias-element" target="_blank">Документация по activity-alias</a></li>
    <li id="GFUQ"><a href="https://firebase.google.com/docs/remote-config" target="_blank">Документация по Firebase Remote Config</a></li>
  </ul>
  <hr />
  <p id="OYmd"><a href="https://t.me/greenhouse_mobile_dev" target="_blank">Telegram-канал Теплица</a></p>
  <p id="dYFl"><a href="mailto:greenhouse.mobile.dev@gmail.com" target="_blank">Обратная связь</a></p>

]]></content:encoded></item><item><guid isPermaLink="true">https://teletype.in/@greenhouse_mobile_dev/text_field_in_jetpack_compose</guid><link>https://teletype.in/@greenhouse_mobile_dev/text_field_in_jetpack_compose?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=greenhouse_mobile_dev</link><comments>https://teletype.in/@greenhouse_mobile_dev/text_field_in_jetpack_compose?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=greenhouse_mobile_dev#comments</comments><dc:creator>greenhouse_mobile_dev</dc:creator><title>Как улучшить UX приложения при работе с полями ввода</title><pubDate>Sun, 21 Jul 2024 13:27:24 GMT</pubDate><media:content medium="image" url="https://img4.teletype.in/files/3e/12/3e125436-4d15-4c10-a202-44d3cf72beff.png"></media:content><category>Android</category><description><![CDATA[<img src="https://img1.teletype.in/files/00/40/00409d6e-9d5d-41f9-b85e-d45cc8a94ffe.png"></img>При работе с полями ввода пришлось преодолеть множество граблей, что позволило создать статью про опытом решения проблем с UX]]></description><content:encoded><![CDATA[
  <figure id="uAt8" class="m_column">
    <img src="https://img1.teletype.in/files/00/40/00409d6e-9d5d-41f9-b85e-d45cc8a94ffe.png" width="1200" />
  </figure>
  <p id="SaBp">Всем привет дорогие друзья! </p>
  <p id="H1Fp">Сегодня хотелось бы поговорить о наболевшем, о пользовательском опыте на экранах с полями ввода. Как правило, именно с полей ввода начинается знакомство пользователя с приложением. Стоит отметить, что ответственность за UX лежит не только на дизайнере, но и на разработчике. Начнём с настроек клавиатуры.</p>
  <p id="jVxj"></p>
  <h2 id="jlTI">Типы клавиатур</h2>
  <p id="TNg4">В Android доступно несколько типов клавиатуры для осуществления первичной фильтрации вводимых данных. Ниже будут приведены типы, доступные для Jetpack Compose.</p>
  <p id="qeKq"></p>
  <h3 id="At2t">Буквенные клавиатуры</h3>
  <p id="sqXN">К буквенным можно отнести клавиатуры с типом:</p>
  <ul id="JGVn">
    <li id="xzYU">Text (стандартная текстовая клавиатура);</li>
    <li id="79yZ">Ascii (клавиатура позволяет ограничить ввод только ASCII символов);</li>
    <li id="ULuJ">Password (клавиатура позволяет вводить различные знаки препинания и цифры, при открытии клавиатура переключится на латинский алфавит);</li>
    <li id="wn1U">Email (клавиатура для ввода электронного адреса, имеет вспомогательные кнопки в нижнем ряду для ускорения ввода данных);</li>
    <li id="qojo">Uri (клавиатура для ввода URI, имеет вспомогательные кнопки в нижнем ряду для ускорения ввода данных).</li>
  </ul>
  <p id="Voak"></p>
  <figure id="3op3" class="m_column">
    <img src="https://img4.teletype.in/files/33/c4/33c4776a-c3a0-4f30-8725-97e274e1605a.png" width="1080" />
    <figcaption>Примеры отображения буквенных клавиатур</figcaption>
  </figure>
  <p id="vkkv"></p>
  <h3 id="5duZ">Числовые клавиатуры</h3>
  <p id="IMVI">К числовым можно отнести клавиатуры с типом:</p>
  <ul id="JGVn">
    <li id="Q286">Number (клавиатура для ввода цифр и ряда символов);</li>
    <li id="X2Dz">NumberPassword (клавиатура для ввода цифровых паролей, в отличие от Number не позволяет ввести ничего кроме цифр);</li>
    <li id="EZnn">Phone (клавиатура для ввода номера телефона и USSD-команд);</li>
    <li id="v3IX">Decimal (клавиатура для ввода десятичных дробей).</li>
  </ul>
  <figure id="SzVl" class="m_column">
    <img src="https://img2.teletype.in/files/9d/02/9d020396-39d6-4249-bfc9-303665f84325.png" width="1080" />
    <figcaption>Примеры отображения числовых клавиатур</figcaption>
  </figure>
  <p id="hchF"></p>
  <h3 id="CuUH">Указание типа клавиатуры в Jetpack Compose</h3>
  <p id="aMCc">Указать тип клавиатуры для поля ввода можно при помощи класса <code>KeyboardOptions</code>.</p>
  <pre id="iQLk" data-lang="kotlin">TextField(
    keyboardOptions = KeyboardOptions.Default.copy(
            keyboardType = KeyboardType.Text, // Указываем тип клавиатуры    
    ),    
    ...
)</pre>
  <p id="YT62"></p>
  <p id="qleF">Помимо типа клавиатуры класс <code>KeyboardOptions</code> позволяет настраивать такие параметры как:</p>
  <ul id="e1u3">
    <li id="GIDI"><code>capitalization</code> - сообщает клавиатуре, следует ли автоматически использовать заглавные буквы в словах и предложениях;</li>
    <li id="qqzI"><code>autoCorrect</code> - сообщает клавиатуре, следует ли выполнять автоисправление для введёного текста;</li>
    <li id="FzVm"><code>imeAction</code> - сообщает клавиатуре о необходимости изменить внешний вид вспомогательной кнопки на клавиатуре. Об этом поговорим чуть позднее, в рамках данной статьи.</li>
  </ul>
  <p id="8BxW"></p>
  <h3 id="0Awf">Пароли и типы клавиатур</h3>
  <p id="kAzG">Если вы когда-нибудь видели поведение клавиатуры при записи экрана на iOS, во время ввода пароля, то для Android стоит снизить ожидания.</p>
  <figure id="n72l" class="m_custom">
    <img src="https://img2.teletype.in/files/10/88/1088f571-14aa-4645-898a-7a6fabb74595.jpeg" width="192.63061137238137" />
    <figcaption>Скрытие содержимого клавиатуры и поля ввода при записи экрана на iOS</figcaption>
  </figure>
  <p id="TA6U">При использовании XML для полей ввода указывался параметр <code>android:inputType=&quot;textPassword&quot;</code>, который включал маску для вводимых символов.</p>
  <figure id="MzCS" class="m_custom">
    <img src="https://img3.teletype.in/files/e3/57/e35769fb-b748-4cb0-99ef-7e7c9ca3a306.jpeg" width="193.46153846153857" />
    <figcaption>Отсутствие скрытия содержимого клавиатуры и поля ввода при записи экрана на Android</figcaption>
  </figure>
  <p id="FV2R">При использовании Jetpack Compose это работает по другому. Для создания маски-пароля требуется передать <code>PasswordVisualTransformation()</code> в  <code>visualTransformation</code>. Установка <code>keyboardType</code> в этом случае лишь подготовит клавиатуру к вводу пароля.</p>
  <pre id="e27w" data-lang="kotlin">TextField(
    visualTransformation = PasswordVisualTransformation(), // Указываем маску для поля ввода пароля    
    keyboardOptions = KeyboardOptions.Default.copy(
            keyboardType = KeyboardType.Password, // Указываем тип клавиатуры    
    ),    
   ...
)</pre>
  <p id="iMlw"></p>
  <p id="UBDX">Однако, если требуется повысить уровень безопасности экрана ввода пароля, рекомендуется использовать <a href="https://developer.android.com/reference/android/view/WindowManager.LayoutParams#FLAG_SECURE" target="_blank">SECURE_FLAG</a>. Данный флаг предотвратит запись видео или создание скриншота на экране.</p>
  <p id="GUuS"></p>
  <h2 id="VbD0">Типы действий на клавиатуре</h2>
  <p id="8ICF">На клавиатуре имеется <strong>кнопка действия (ImeAction)</strong>. </p>
  <figure id="etcw" class="m_retina">
    <img src="https://img1.teletype.in/files/c7/9d/c79d3f5a-1703-4342-b5e7-bdbc4aa402f0.jpeg" width="540" />
    <figcaption>ImeAction на клавиатуре в Android</figcaption>
  </figure>
  <p id="j5eA">Иногда использование данной кнопки является более удобным путём для пользователя, так как она находится в нижней части экрана. Например, не потребуется тянуться пальцем в верхнюю часть экрана.<br />Клавиатура поддерживает следующие действия:</p>
  <p id="HSGF"><strong>Default</strong></p>
  <p id="eY2I">Система самостоятельно выберет действие для клавиатуры. Как правило это <code>None</code> или <code>Done</code>.</p>
  <p id="0Q7O"><strong>None</strong></p>
  <p id="n9Lu">При клике на данное действие курсор перейдёт на новую строку.</p>
  <p id="Gr8E"><strong>Go</strong></p>
  <p id="dOPK">При клике на действие предполагается переход куда-либо. Например, переход по введенному URL.</p>
  <p id="anCC"><strong>Search</strong></p>
  <p id="1nHB">При клике на действие предполагается выполнение поискового запроса по введённому значению.</p>
  <p id="ZwuZ"><strong>Send</strong></p>
  <p id="qyMc">При клике на действие предполагается отправка текста.</p>
  <p id="Wtjt"><strong>Previous</strong></p>
  <p id="jAQd">При клике на действие предполагается перевод фокуса на предыдущее поле ввода, если оно есть.</p>
  <p id="xJMf"><strong>Next</strong></p>
  <p id="GVN7">При клике на действие предполагается перевод фокуса на следующее поле ввода, если оно есть.</p>
  <p id="fgFw"><strong>Done</strong></p>
  <p id="rQUj">При клике на действие предполагается выполнение какого-либо завершающего действия (закрытие клавиатуры/отображение прогресса и отправка запроса).</p>
  <p id="E7KE"></p>
  <p id="p80j">Важно отметить, что иконки на <code>ImeAction</code> могут отличаться в зависимости от выбранной системной клавиатуры. </p>
  <p id="UrMr"></p>
  <h3 id="d2Za">Указание ImeAction в Jetpack Compose</h3>
  <p id="Gz1A">Указание <code>ImeAction</code>, как говорилось ранее, осуществляется с помощью класса <code>KeyboardOptions</code>.</p>
  <pre id="mVoV" data-lang="kotlin">TextField(
    keyboardOptions = KeyboardOptions.Default.copy(       
            imeAction = ImeAction.Search, // Указываем тип кнопки действия    
    ),    
    keyboardActions = KeyboardActions {
            // Указываем код, который требуется выполнить при клике на ImeAction    
    },    
    ...
)</pre>
  <p id="1m9B"></p>
  <h2 id="kLiA">Размещение контента над клавиатурой и другие проблемы, связанные с этим вопросом</h2>
  <p id="BIf1">На StackOverflow можно найти множество способов решения различных проблем, связанных с размещением контента над клавиатурой. Ниже я приведу примеры кейсов, с которыми мы сталкивались на наших проектах, и поделюсь решениями, к которым мы пришли.</p>
  <p id="fhpx"></p>
  <h3 id="QdEV"><strong>Перекрытие контента прижатой к низу кнопкой при открытой клавиатуре</strong></h3>
  <p id="mP3z">Периодически в дизайне можно встретить ситуацию, когда поле ввода находится в верхней части экрана, а кнопка прижата к низу экрана и при открытии клавиатуры требуется поднять кнопку, перекрывая контент кнопкой.</p>
  <figure id="ephW" class="m_custom">
    <img src="https://img3.teletype.in/files/6e/d5/6ed5aeea-1c53-4e3b-b424-aa764b3aceb0.gif" width="193.37500000000003" />
    <figcaption>Пример перекрытия контента при открытии клавиатуры</figcaption>
  </figure>
  <p id="tF7b">В данном случае мы рекомендуем установить значение <code>adjustResize</code> для параметра <code>windowSoftInputMode</code> для Activity. Например, через Manifest.<br />Если в проекте используется <code>BottomSheetDialogFragment</code>, для него значение устанавливается отдельно через тему, так как данный класс имеет свой <code>Window</code>. Дополнительно, стоит указывать метод <code>imePadding</code> , который добавляет <code>Spacer</code> равный размеру клавиатуры для корректного подъема контента.</p>
  <p id="ww7H">Верстка с использованием Jetpack Compose при этом может выглядеть следующим образом:</p>
  <pre id="iBNu" data-lang="kotlin">Box(modifier = Modifier.fillMaxSize()) {
    Column(modifier = Modifier.fillMaxSize()) {
            SomeHeader()        
            SomeBody()    
    }
        
    OverlayFooter()
}</pre>
  <p id="dwqq"></p>
  <h3 id="4KYG">Деформация контента при использовании adjustResize</h3>
  <p id="ToLi">Помните, что когда вы работаете с <code>adjustResize</code>, при недостатке места на экране вёрстка может начать деформироваться.</p>
  <figure id="sz8x" class="m_custom">
    <img src="https://img2.teletype.in/files/9d/40/9d40eaf5-685b-43d0-b7b7-8d0fdf7957e7.jpeg" width="193.46153846153848" />
    <figcaption>Деформация кнопки при недостаточном количестве места для контента при открытии клавиатуры</figcaption>
  </figure>
  <p id="pL1D">Не забывайте проверять экраны с полями ввода на эмуляторах или физических устройствах с разной диагональю и настройками масштабирования контента в системе. </p>
  <p id="N1hg">При необходимости добавляйте для контейнера возможность вертикального скролла. Это позволит избежать деформации верстки при недостатке экранного пространства.</p>
  <figure id="JP6u" class="m_custom">
    <img src="https://img3.teletype.in/files/65/1a/651a79e4-5de7-4dfc-bff5-cb8c0ad78791.gif" width="192.95312500000003" />
    <figcaption>Экран с возможностью скрола при недостаточном количестве места для контента при открытии клавиатуры</figcaption>
  </figure>
  <p id="l6U3"></p>
  <h3 id="Lz07">Поднятие контента над клавиатурой в BottomSheetDialogFragment</h3>
  <p id="B711">Достаточно часто в дизайне можно встретить боттомшит с полем ввода и кнопкой. Как правило, дизайнеры просят, чтобы при открытии клавиатуры весь контент включая кнопку поднимался над клавиатурой.</p>
  <figure id="uzr1" class="m_custom">
    <img src="https://img1.teletype.in/files/c7/b3/c7b324f3-2b35-490d-97fa-3af7f154ca6d.jpeg" width="193.15384615384616" />
  </figure>
  <p id="VaT0">В этом случае, для BottomSheetDialogFragment следует указать <code>adjustResize</code> через тему. Это сработает на устройствах большинства производителей. </p>
  <p id="ueYz">Однако для некоторых устройств Huawei для кнопки стоит указать параметр <a href="https://developer.android.com/reference/kotlin/androidx/compose/foundation/relocation/BringIntoViewRequester" target="_blank">BringIntoViewRequester</a>, который позволит проскролить контент до элемента, тем самым поднимет кнопку над клавиатурой.</p>
  <pre id="tC91" data-lang="kotlin">val bringIntoViewRequester = remember { BringIntoViewRequester() }
val isFocused = remember { mutableStateOf(false) }
TextField(
    modifier = Modifier
            .fillMaxWidth()        
            .onFocusChanged{ isFocused.value = it.isFocused },
    ...        
)

Button(
    onClick = { 
           // Выполнить действие по клику    
    },    
    modifier = Modifier
            .fillMaxWidth()
            .bringIntoViewRequester(bringIntoViewRequester),
    ) {
        Text(text = &quot;Отправить&quot;)
      }
)
      
LaunchedEffect(key1 = isFocused.value) {
    if (isFocused.value) {
            bringIntoViewRequester.bringIntoView()    
    }
}</pre>
  <p id="XVlt"></p>
  <h3 id="ekdp">Некорректная позиция handle в полях ввода внутри BottomSheetDialogFragment</h3>
  <p id="kOv3">Капелька, которая появляется при выделении или фокусе на тексте называется handle. Если вы встречались с поведением, как на скриншоте ниже, то у меня есть для вас крутая новость - это можно пофиксить.</p>
  <figure id="bw40" class="m_custom">
    <img src="https://img4.teletype.in/files/3b/8e/3b8eed9b-8836-433a-aa9b-16320ebde19a.gif" width="192.94531249999997" />
    <figcaption>Некорректная позиция handle при открытии клавиатуры на боттомшите</figcaption>
  </figure>
  <p id="7Ma4">Данный баг связан с некорректной передачей координат местоположения поля ввода на экране в метод отрисовки handle. </p>
  <p id="9Zin">Данное поведение можно исправить разместив контент боттомшита внутри Box:</p>
  <pre id="QCHs" data-lang="kotlin">Box(
    modifier = Modifier.fillMaxSize(),
    contentAlignment = Alignment.BottomCenter,
) {
    // Контент боттомшита
}</pre>
  <p id="TMIS"></p>
  <p id="Xkui">На этом наш пост подошёл к концу. Надеюсь, мой опыт работы с полями ввода поможет вам снизить количество граблей во время разработки функционала. Помните, что разработчик тоже влияет на UX приложения. </p>
  <p id="gRIC">Good coding and happy day!🤘</p>
  <p id="Rxul"></p>
  <h1 id="9B5g">Полезные ссылки:</h1>
  <ul id="QPPb">
    <li id="dHTq"><a href="https://developer.android.com/reference/kotlin/androidx/compose/ui/text/input/ImeAction" target="_blank">Страница об ImeAction</a></li>
    <li id="sLzc"><a href="https://developer.android.com/reference/kotlin/androidx/compose/ui/text/input/KeyboardType" target="_blank">Страница о KeyboardType</a></li>
    <li id="Xbc7"><a href="https://developer.android.com/develop/ui/views/touch-and-input/keyboard-input/visibility" target="_blank">Документация по обработке системной клавиатуры</a></li>
  </ul>
  <hr />
  <p id="OYmd"><a href="https://t.me/greenhouse_mobile_dev" target="_blank">Telegram-канал Теплица</a></p>
  <p id="dYFl"><a href="mailto:greenhouse.mobile.dev@gmail.com" target="_blank">Обратная связь</a></p>

]]></content:encoded></item><item><guid isPermaLink="true">https://teletype.in/@greenhouse_mobile_dev/integration_xcmetrics</guid><link>https://teletype.in/@greenhouse_mobile_dev/integration_xcmetrics?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=greenhouse_mobile_dev</link><comments>https://teletype.in/@greenhouse_mobile_dev/integration_xcmetrics?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=greenhouse_mobile_dev#comments</comments><dc:creator>greenhouse_mobile_dev</dc:creator><title>Интеграция XCMetrics</title><pubDate>Wed, 10 Jul 2024 07:53:34 GMT</pubDate><media:content medium="image" url="https://img1.teletype.in/files/4f/c4/4fc4d38f-40f0-4333-94da-3aee02202b4b.png"></media:content><category>iOS</category><description><![CDATA[<img src="https://img2.teletype.in/files/d1/ec/d1ec67b7-223b-47db-84e9-f451c9f3ccfd.png"></img>С ростом сложности современных мобильных приложений, эффективность процесса сборки становится критически важной для разработчиков. Каждая секунда, потраченная на сборку, увеличивает общее время разработки, затягивает тестирование и выпуск обновлений. Именно здесь на помощь приходит XCMetrics — мощный инструмент для анализа времени сборки iOS проектов]]></description><content:encoded><![CDATA[
  <figure id="6jLP" class="m_column">
    <img src="https://img2.teletype.in/files/d1/ec/d1ec67b7-223b-47db-84e9-f451c9f3ccfd.png" width="2400" />
  </figure>
  <h2 id="HqHN">Что такое XCMetrics?</h2>
  <p id="wvxG">XCMetrics — инструмент, разработанный инженерами Spotify, предназначенный для обработки и отправки метрик сборок Xcode. </p>
  <p id="XYdG">По своей сути, XCMetrics является «оберткой» над XCLogParser, который анализирует логи Xcode и xcodebuild, представленные в формате xcactivitylog.</p>
  <p id="d4dQ"></p>
  <h2 id="G16A">Для чего нужен XCMetrics?</h2>
  <p id="8OkA">XCMetrics используется для отправки логов сборок на ваш сервер, что позволяет аккумулировать обширные данные. </p>
  <p id="OCMR">Это открывает возможности для глубокого анализа, сравнения и понимания факторов, влияющих на скорость сборки вашего проекта. </p>
  <p id="HJzF">С помощью XCMetrics вы можете более точно определить узкие места и улучшить процесс сборки, что в конечном итоге приведет к повышению эффективности разработки.</p>
  <p id="H9Pl"></p>
  <h2 id="lAKP">Как интегрировать XCMetrics в проект?</h2>
  <p id="njHH">Интеграцию XCMetrics условно можно разделить на 3 этапа:</p>
  <ul id="rlJc">
    <li id="CFSh">Разворачивание backend-части</li>
    <li id="tqew">Интеграция в iOS проекте</li>
    <li id="hkP1">Подключение плагинов в backstage</li>
  </ul>
  <p id="zzF9"></p>
  <h2 id="ZTlY">Интеграция XCMetrics в iOS проекте</h2>
  <h3 id="aj93">Подготовка executable файлов</h3>
  <p id="snlO">Для начала необходимо склонить <a href="https://github.com/spotify/XCMetrics.git" target="_blank">XCMetrics проект</a>. Далее компилируем executable файл XCMetrics. Для этого в терминале переходим в склоненный проект и запускаем команду</p>
  <pre id="Oqob">swift build --product XCMetrics -c release</pre>
  <p id="cdZ2">Скомпилировнный файл будет находится по пути <strong>XCMetrics/.build/arm64-apple-macosx/release</strong> и будет наименован как XCMetrics</p>
  <figure id="A4OB" class="m_column">
    <img src="https://img1.teletype.in/files/02/28/0228d860-c3da-4bfb-963f-5d8932a3d086.jpeg" width="1280" />
  </figure>
  <p id="4wdx">Альтернативный вариант - скачать <a href="https://github.com/spotify/XCMetrics/releases" target="_blank">готовые скомпилированные executable файлы</a>.</p>
  <p id="g1cq">Далее копируем скомпилированный файл, а также файл XCMetricsLauncher из root-папки XСMetrics и вставляем в root-папку вашего проекта. </p>
  <p id="9d5X">На этом предварительная работа закончена. Теперь можем внести изменения в самом Xcode.</p>
  <h3 id="9n4R"></h3>
  <h3 id="rZPp">Внесение изменений в Xcode</h3>
  <p id="wHm7">Для того, чтобы после каждого билда вашего проекта отправлялись метрики необходимо добавить post action скрипт. Выбираем нужную вам схему в Xcode → Edit Scheme → Build → Post-Actions. </p>
  <p id="4peF">Сюда добавляем следующий скрипт:</p>
  <pre id="j592">../XCMetrics/XCMetricsLauncher ../XCMetrics/XCMetrics --name &#x60;Наименование вашей схемы&#x60; --buildDir ${BUILD_DIR} --serviceURL &#x60;https://example.com&#x60;
</pre>
  <p id="8jXX">Давайте разберем содержание скрипта:</p>
  <ul id="WES6">
    <li id="IXrG"><strong>../XCMetrics/XCMetricsLauncher ../XCMetrics/XCMetrics —</strong> путь до файлов XCMetrics и XCMetricsLauncher</li>
    <li id="JuCv"><strong>--name —</strong> сюда вставляем наименование вашей схемы, либо название проекта</li>
    <li id="1E6w"><strong>--buildDir</strong> ${BUILD_DIR} — путь до логов ваших билдов в Derived Data</li>
    <li id="IwwQ"><strong>--serviceURL</strong> <code>https://example.com</code> — адрес, по которому будут отправляться метрики</li>
  </ul>
  <p id="2mi8">Помимо этого существуют и другие дополнительные параметры, с которыми можно ознакомиться в документации XCMetrics. На этом работа с XCMetrics в Xcode закончена и теперь после каждого билда вашего проекта соответствующие логи будут передаваться на ваш сервер.</p>
  <p id="IJnw"></p>
  <h2 id="Nbul">Локальный Backend и тестирование</h2>
  <p id="iM9V">В рамках XCMetrics, инженеры Spotify предлагают готовый Docker образ XCMetrics. С помощью него мы сможем развернуть backend составляющую локально и протестировать отправку метрик.</p>
  <ul id="GSvk">
    <li id="ksRV">Для начала необходимо установить <a href="https://docs.docker.com/docker-for-mac/" target="_blank">Docker</a>.</li>
    <li id="PeqQ">В проекте XCMetrics (который вы ранее склонили) имеется готовый конфигурационный файл docker-compose, необходимый для развертывания инстансов Redis и Postgres. Запускаем команду и получаем готовый к работе backend XCMetrics. <code>docker-compose -f docker-compose-local.yml up </code></li>
  </ul>
  <p id="2tQZ">Так как теперь мы имеем полностью готовый к использованию локальный backend, можно приступить к тестированию отправки метрик. Заменяем <code>—serviceUrl ****</code> в ранее добавленном в Xcode post-action скрипте на <code>http://localhost:8080/v1/metrics</code> Можно билдить проект и тестировать отправку метрик.</p>
  <p id="N0KD">Запросы XCMetrics можно отследить используя различные снифферы, наподобие Proxyman.</p>
  <p id="TkuI">Проанализировать содержание метрик можно используя инструменты для работы с PostgreSQL, наподобие Postico. Креды для работы с базой Postgres XCMetrics хранятся в файле <code>docker-compose-local.yml.</code></p>
  <p id="Y25g"></p>
  <h2 id="7ZEP">XCMetrics и Fastlane</h2>
  <p id="wNhq">XCMetrics является отличным решеним для отправки метрик в рамках CI. Для этого достаточно добавить в ваш lane вызов sh-скрипта. Пример скрипта использующийся в проектах Tele2/Altel.</p>
  <pre id="Fbgf">sh(&quot;../XCMetrics/XCMetricsLauncher ../XCMetrics/XCMetrics --name &#x27;Название вашей схемы&#x27; --buildDir &#x27;#{ENV[&quot;PWD&quot;]}/DerivedData/Logs/Build&#x27; --isCI true --serviceURL &lt;https://example.com&gt;&quot;)

</pre>
  <p id="npee">Давайте разберем отличия от post-action скрипта в Xcode:</p>
  <ul id="4OMG">
    <li id="7ID3">Изменен параметр —buildDir, т.к. в рамках CI мы используем кастомные пути для DerivedData. Файлы c расширением .xcactivitylog хранятся именно в DerivedData/Logs/Build.</li>
    <li id="iaDy">Добавлен параметр —isCI true для разграничения метрик в backstage.</li>
  </ul>
  <p id="vNJo"></p>
  <h2 id="Xbur">Backstage</h2>
  <p id="YGUQ">При подключении плагинов Xcmetrics к вашему Backstage - вы сможете видеть наглядно информацию по вашим сборкам с инфографикой и детальной информацией по каждой отдельной сборке</p>
  <figure id="Rgkc" class="m_column">
    <img src="https://img1.teletype.in/files/8f/e1/8fe118c3-3b75-4d23-a289-85e23ba386d0.png" width="1280" />
  </figure>
  <p id="l4Gd"></p>
  <h1 id="9B5g">Полезные ссылки:</h1>
  <ul id="QPPb">
    <li id="dHTq"><a href="https://xcmetrics.io/docs/getting-started.html" target="_blank">Документация XCMetrics</a></li>
    <li id="Xbc7"><a href="https://github.com/spotify/XCMetrics/blob/main/docs/How%20to%20Deploy%20Backend.md" target="_blank">Документация по backend XCMetrics</a></li>
    <li id="fHXS"><a href="https://www.youtube.com/watch?v=6p8kveO1m00" target="_blank">Доклад инженеров Spotify на iOS Conf</a></li>
  </ul>
  <p id="5hFZ"></p>
  <hr />
  <p id="OYmd"><a href="https://t.me/greenhouse_mobile_dev" target="_blank">Telegram-канал Теплица</a></p>
  <p id="dYFl"><a href="mailto:greenhouse.mobile.dev@gmail.com" target="_blank">Обратная связь</a></p>

]]></content:encoded></item></channel></rss>