<?xml version="1.0" encoding="utf-8" ?><feed xmlns="http://www.w3.org/2005/Atom" xmlns:tt="http://teletype.in/" xmlns:opensearch="http://a9.com/-/spec/opensearch/1.1/"><title>Теплица | Код и Кейсы</title><subtitle>Это дополнение к Telegram-каналу «Теплица» — авторскому блогу о мобильной разработке.
Здесь мы с друзьями-разработчиками публикуем развернутые статьи.</subtitle><author><name>Теплица | Код и Кейсы</name></author><id>https://teletype.in/atom/greenhouse_mobile_dev</id><link rel="self" type="application/atom+xml" href="https://teletype.in/atom/greenhouse_mobile_dev?offset=0"></link><link rel="alternate" type="text/html" href="https://teletype.in/@greenhouse_mobile_dev?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=greenhouse_mobile_dev"></link><link rel="next" type="application/rss+xml" href="https://teletype.in/atom/greenhouse_mobile_dev?offset=10"></link><link rel="search" type="application/opensearchdescription+xml" title="Teletype" href="https://teletype.in/opensearch.xml"></link><updated>2026-07-08T19:10:45.003Z</updated><entry><id>greenhouse_mobile_dev:sprite_kit_intro</id><link rel="alternate" type="text/html" href="https://teletype.in/@greenhouse_mobile_dev/sprite_kit_intro?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=greenhouse_mobile_dev"></link><title>Введение в SpriteKit: учимся работать с движком на практике</title><published>2025-07-02T11:46:31.138Z</published><updated>2025-07-03T06:51:40.590Z</updated><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://img2.teletype.in/files/95/56/9556be39-125d-4f9a-91a5-da0231e354d1.png"></media:thumbnail><category term="i-os" label="iOS"></category><summary type="html">&lt;img src=&quot;https://img1.teletype.in/files/c8/c8/c8c85e78-906a-4930-8cd3-cfc3a4d3b542.png&quot;&gt;Всем привет ! «До этого мы никогда не писали игры. Но решили — пора!»  В этой статье мы воссоздали базовую механику Flappy Bird на SpriteKit: добавили персонажа, физику, гравитацию и управление.  Если ты iOS-разработчик и хочешь попробовать себя в геймдеве — загляни</summary><content type="html">
  &lt;figure id=&quot;pnNS&quot; class=&quot;m_column&quot;&gt;
    &lt;img src=&quot;https://img1.teletype.in/files/c8/c8/c8c85e78-906a-4930-8cd3-cfc3a4d3b542.png&quot; width=&quot;4800&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;p071&quot;&gt;Всем привет!&lt;/p&gt;
  &lt;p id=&quot;yTS3&quot;&gt;В этой статье мы хотим поговорить о нативной разработке мобильных игр на iOS. А точнее познакомить вас с игровым движком &lt;strong&gt;SpriteKit&lt;/strong&gt;. Разберём, что он умеет, зачем он нужен и как начать с ним работать. Вместе создадим простую игровую сцену, добавим персонажа, подключим физику и поэкспериментируем с гравитацией.&lt;/p&gt;
  &lt;p id=&quot;QKVv&quot;&gt;&lt;/p&gt;
  &lt;h2 id=&quot;2lYh&quot;&gt;Начало пути&lt;/h2&gt;
  &lt;p id=&quot;GUmD&quot;&gt;Мы с командой решили воссоздать классическую Flappy Bird — простую, но затягивающую игру, где игрок управляет птичкой, избегая столкновений с трубами. Несмотря на кажущуюся простоту, такая игра требует продуманной архитектуры: нужно обеспечить стабильную производительность и чёткое разделение логики. Важно было реализовать управление состояниями (меню, игра, проигрыш), физику столкновений, бесконечную прокрутку фона и генерацию труб.&lt;/p&gt;
  &lt;p id=&quot;YW77&quot;&gt;Читая абзац выше, может показаться, что мы отлично разбираемся в теме и всё у нас под контролем. Но на деле всё было иначе. Опыт? Впервые. План? Почти. Время на отрицание, гнев, торг и депрессию? Не завезли. Сразу «принятие» — и поехали делать игру.&lt;/p&gt;
  &lt;p id=&quot;5072&quot;&gt;К счастью, Apple предоставляет из коробки мощный инструмент для таких задач — &lt;strong&gt;SpriteKit&lt;/strong&gt;. Именно он стал нашим проводником в мир 2D-геймдева на iOS.&lt;/p&gt;
  &lt;p id=&quot;5WLz&quot;&gt;&lt;/p&gt;
  &lt;h2 id=&quot;Chk8&quot;&gt;Что такое SpriteKit?&lt;/h2&gt;
  &lt;p id=&quot;2HDi&quot;&gt;&lt;strong&gt;SpriteKit&lt;/strong&gt; — это фреймворк от Apple, который помогает создавать 2D-игры и анимации на iOS, macOS, tvOS и iPadOS. Проще говоря, это набор инструментов, позволяющий рисовать спрайты, двигать их, наделять физикой, проигрывать звуки и управлять столкновениями.&lt;/p&gt;
  &lt;p id=&quot;SrMi&quot;&gt;В SpriteKit все игровые элементы, такие как персонажи, враги, фоны и прочее называются &lt;strong&gt;спрайтами&lt;/strong&gt; (&lt;em&gt;sprites&lt;/em&gt;). Эти спрайты размещаются на &lt;strong&gt;сцене&lt;/strong&gt; (&lt;em&gt;scene&lt;/em&gt;), которая служит основным контейнером для всей игровой логики и визуальных объектов.&lt;/p&gt;
  &lt;p id=&quot;jP9t&quot;&gt;Рассмотрим основные компоненты фреймворка:&lt;/p&gt;
  &lt;ul id=&quot;9h6D&quot;&gt;
    &lt;li id=&quot;RrRb&quot;&gt;&lt;code&gt;SKScene&lt;/code&gt; — сцена, на которой разворачивается игра;&lt;/li&gt;
    &lt;li id=&quot;0O42&quot;&gt;&lt;code&gt;SKSpriteNode&lt;/code&gt; — графический элемент (например, персонаж или фон);&lt;/li&gt;
    &lt;li id=&quot;keTe&quot;&gt;&lt;code&gt;SKLabelNode&lt;/code&gt; — текст на экране;&lt;/li&gt;
    &lt;li id=&quot;Qbx3&quot;&gt;&lt;code&gt;SKAction&lt;/code&gt; — анимации и действия (движение, вращение и прочее);&lt;/li&gt;
    &lt;li id=&quot;wvqp&quot;&gt;&lt;code&gt;SKPhysicsBody&lt;/code&gt; — физическое тело для гравитации и столкновений.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;p5Ow&quot;&gt;&lt;/p&gt;
  &lt;h2 id=&quot;k4Sr&quot;&gt;Настройка Scene&lt;/h2&gt;
  &lt;p id=&quot;e9TD&quot;&gt;В SpriteKit все игровые объекты размещаются на сцене&lt;strong&gt;.&lt;/strong&gt; Это специальный класс (&lt;code&gt;SKScene&lt;/code&gt;), который управляет логикой игры, обрабатывает события (например, касания) и настраивает физику. Именно с неё начинается построение любой игры.&lt;/p&gt;
  &lt;p id=&quot;VmOO&quot;&gt;Давайте же перейдем к коду и создадим простую сцену. &lt;/p&gt;
  &lt;p id=&quot;GGa9&quot;&gt;&lt;/p&gt;
  &lt;p id=&quot;om5C&quot;&gt;&lt;strong&gt;Шаг 1: Создание сцены&lt;/strong&gt;&lt;/p&gt;
  &lt;p id=&quot;78An&quot;&gt;В Xcode выбираем &lt;strong&gt;New File From Template&lt;/strong&gt; → &lt;strong&gt;SpriteKit Scene&lt;/strong&gt;.&lt;br /&gt;Этот шаблон создаст &lt;code&gt;.sks&lt;/code&gt;-файл, визуальный редактор сцены, и вы сможете на нём размещать объекты.&lt;/p&gt;
  &lt;figure id=&quot;lPBg&quot; class=&quot;m_column&quot;&gt;
    &lt;img src=&quot;https://img4.teletype.in/files/f6/38/f6387208-cde1-494d-9a31-ff49aac2bbd4.png&quot; width=&quot;739&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;lrWv&quot;&gt;&lt;strong&gt;Шаг 2: Указание кастомного класса&lt;/strong&gt;&lt;/p&gt;
  &lt;p id=&quot;HNpH&quot;&gt;Открываем созданный &lt;code&gt;.sks&lt;/code&gt;-файл и в инспекторе справа и указываем имя вашего собственного класса сцены в поле &lt;strong&gt;Custom Class&lt;/strong&gt; — например, &lt;code&gt;GameScene&lt;/code&gt;.&lt;/p&gt;
  &lt;figure id=&quot;CANa&quot; class=&quot;m_original&quot;&gt;
    &lt;img src=&quot;https://img3.teletype.in/files/af/d1/afd1e0c5-8294-4636-b39a-c377a6f9c327.png&quot; width=&quot;258&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;CVgH&quot;&gt;&lt;strong&gt;Шаг 3: Реализация класса сцены&lt;/strong&gt;&lt;/p&gt;
  &lt;p id=&quot;8r82&quot;&gt;Теперь создаём Swift-файл с соответствующим именем (например, &lt;code&gt;GameScene.swift&lt;/code&gt;) и реализуйте свой класс, унаследовав его от &lt;code&gt;SKScene&lt;/code&gt;.&lt;/p&gt;
  &lt;pre id=&quot;AePV&quot; data-lang=&quot;swift&quot;&gt;final class GameScene: SKScene {

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

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

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

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

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

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

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

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

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

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

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

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

</content></entry><entry><id>greenhouse_mobile_dev:about_time_and_trustedtime_api</id><link rel="alternate" type="text/html" href="https://teletype.in/@greenhouse_mobile_dev/about_time_and_trustedtime_api?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=greenhouse_mobile_dev"></link><title>Немного о часах и TrustedTime API</title><published>2025-03-19T08:36:01.454Z</published><updated>2025-03-22T16:53:01.356Z</updated><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://img3.teletype.in/files/ac/d8/acd8fece-97eb-4539-8aa2-e1a1fae456cb.png"></media:thumbnail><category term="android" label="Android"></category><summary type="html">&lt;img src=&quot;https://img1.teletype.in/files/05/d3/05d3048f-16db-4a19-93b9-866fc39d7bc8.png&quot;&gt;Google недавно представил TrustedTime API. Я его изучила и подготовила статью о новом API, NTP, clock drift, timestamp. Погнали!</summary><content type="html">
  &lt;figure id=&quot;LbIC&quot; class=&quot;m_column&quot;&gt;
    &lt;img src=&quot;https://img1.teletype.in/files/05/d3/05d3048f-16db-4a19-93b9-866fc39d7bc8.png&quot; width=&quot;1200&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;z8qj&quot;&gt;Всем привет, дорогие друзья!&lt;/p&gt;
  &lt;p id=&quot;aa7Y&quot;&gt;Недавно Google представил новое API, которое позволяет получать текущее реальное время напрямую с &lt;code&gt;NTP&lt;/code&gt;-серверов Google, не используя системные часы устройства. В этой статье я хочу не просто показать, как его подключить, но и подробнее рассказать, что такое &lt;code&gt;NTP&lt;/code&gt;, как работает этот протокол и что такое &lt;code&gt;timestamp&lt;/code&gt; и &lt;code&gt;clock drift&lt;/code&gt;.&lt;/p&gt;
  &lt;p id=&quot;M7BD&quot;&gt;&lt;/p&gt;
  &lt;h2 id=&quot;089n&quot;&gt;NTP&lt;/h2&gt;
  &lt;p id=&quot;0FQ1&quot;&gt;&lt;code&gt;Network Time Protocol (NTP)&lt;/code&gt; — это сетевой протокол, предназначенный для синхронизации времени между устройствами. &lt;/p&gt;
  &lt;p id=&quot;q0sC&quot;&gt;Он работает по модели клиент-сервер: &lt;code&gt;NTP&lt;/code&gt;-клиенты запрашивают актуальное время у &lt;code&gt;NTP&lt;/code&gt;-серверов, которые получают его от высокоточных источников, таких как атомные часы или &lt;code&gt;GPS&lt;/code&gt;. Затем сервер передаёт точные данные клиенту, который обновляет своё время.&lt;/p&gt;
  &lt;p id=&quot;rk2D&quot;&gt;Для передачи используется протокол &lt;code&gt;User Datagram Protocol (UDP)&lt;/code&gt;, который обеспечивает быструю отправку данных без установления соединения. Соответственно, запрос на получение точного времени можно отправить через стандартные &lt;code&gt;IP&lt;/code&gt;-сети.&lt;/p&gt;
  &lt;p id=&quot;rXKk&quot;&gt;Вся система &lt;code&gt;NTP&lt;/code&gt; организована в иерархическую структуру с уровнями (&lt;code&gt;stratum&lt;/code&gt;), где каждый уровень определяет степень удалённости от эталонного источника времени.&lt;/p&gt;
  &lt;figure id=&quot;mhYp&quot; class=&quot;m_column&quot;&gt;
    &lt;img src=&quot;https://img3.teletype.in/files/ad/70/ad701aeb-33b6-41ad-958b-778c4621e499.png&quot; width=&quot;1226&quot; /&gt;
    &lt;figcaption&gt;Иерархия NTP системы&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;7UCw&quot;&gt;&lt;/p&gt;
  &lt;h2 id=&quot;KaHu&quot;&gt;Clock drift&lt;/h2&gt;
  &lt;p id=&quot;COel&quot;&gt;Возможно, вы замечали, что после длительного отключения смартфона его время может отличаться от других устройств. &lt;/p&gt;
  &lt;p id=&quot;5DXm&quot;&gt;Это явление называется &lt;code&gt;clock drift&lt;/code&gt; – расхождение системных часов устройства с точным временем из-за аппаратных или программных факторов.&lt;br /&gt;Для исправления данной проблемы производится синхронизация с эталонными источниками времени при помощи &lt;code&gt;NTP&lt;/code&gt;.&lt;/p&gt;
  &lt;figure id=&quot;055d&quot; class=&quot;m_column&quot;&gt;
    &lt;img src=&quot;https://img1.teletype.in/files/40/a2/40a2f5e5-fe8c-428d-b6e1-76e0a59c2117.png&quot; width=&quot;946&quot; /&gt;
    &lt;figcaption&gt;Графический пример Clock drift&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;6ctS&quot;&gt;&lt;/p&gt;
  &lt;h2 id=&quot;Mmx3&quot;&gt;Timestamp&lt;/h2&gt;
  &lt;p id=&quot;y0Gu&quot;&gt;При разработке функционала, где мы реализуем отображение времени заказа или операции, мы часто работаем с &lt;code&gt;timestamp&lt;/code&gt; (временной меткой). &lt;/p&gt;
  &lt;p id=&quot;haI9&quot;&gt;&lt;code&gt;Timestamp&lt;/code&gt;  — это числовое значение, указывающее на конкретный момент времени, зафиксированный с определенной точностью. &lt;/p&gt;
  &lt;p id=&quot;jRg3&quot;&gt;Данное значение обычно является количеством секунд или миллисекунд, прошедших с определенной даты отсчёта. Чаще всего в работе встречается &lt;code&gt;Unix Timestamp&lt;/code&gt;, где отсчёт начинается с 1 января 1970 года 00:00:00 UTC.&lt;/p&gt;
  &lt;p id=&quot;VkFy&quot;&gt;Timestamp может иметь различную точность: до секунды, миллисекунды, микросекунды или наносекунды. Поэтому, важно заранее обсудить точность с backend-разработчиком. Например, если вы разрабатываете чат поддержки и имеете разную точность с сервером, то могут встречаться ситуации с дублированием информации или некорректной сортировкой.&lt;/p&gt;
  &lt;p id=&quot;mbLX&quot;&gt;&lt;/p&gt;
  &lt;h2 id=&quot;KblS&quot;&gt;Для чего нужен TrustedTime API&lt;/h2&gt;
  &lt;p id=&quot;mIvJ&quot;&gt;Локальное время подвержено изменениям: в стране могут ввести или отменить перевод на летнее время, изменить часовой пояс для какой-то местности (или вовсе объединить в один часовой пояс всю страну).&lt;/p&gt;
  &lt;p id=&quot;RFYc&quot;&gt;Для владельцев устройств с Android версией ниже 10 такие изменения могут стать серьезной проблемой. До Android 10 обновления данных о часовых поясах зависели от системных обновлений, и чем старше устройство, тем ниже вероятность получения актуальной информации. В некоторых случаях, пользователи могут обновить часовые зоны вручную, но для этого часто требуется ROOT-доступ.&lt;/p&gt;
  &lt;p id=&quot;Em9T&quot;&gt;В итоге многие пользователи начинают вручную корректировать системное время, что делает его ненадёжным источником.&lt;/p&gt;
  &lt;p id=&quot;lRnc&quot;&gt;TrustedTime API позволяет приложениям получать точное и защищенное время (&lt;code&gt;Unix Timestamp&lt;/code&gt;), даже если системные часы устройства не являются достоверными.&lt;/p&gt;
  &lt;p id=&quot;FMLx&quot;&gt;&lt;/p&gt;
  &lt;h2 id=&quot;ZJv5&quot;&gt;Подключение TrustedTime API&lt;/h2&gt;
  &lt;p id=&quot;709P&quot;&gt;Ниже будет приведён упрощённый вариант реализации логики. &lt;br /&gt;Для начала необходимо добавить зависимость в &lt;code&gt;build.gradle&lt;/code&gt;:&lt;/p&gt;
  &lt;pre id=&quot;sxkj&quot; data-lang=&quot;kotlin&quot;&gt;dependencies {
    implementation(&amp;quot;com.google.android.gms:play-services-time:{version}&amp;quot;)
}&lt;/pre&gt;
  &lt;p id=&quot;BKf5&quot;&gt;&lt;/p&gt;
  &lt;p id=&quot;PTVU&quot;&gt;Далее, можно создать интерфейс и его реализацию, которые позволят инкапсулировать логику создания &lt;code&gt;TrustedTimeClient&lt;/code&gt; и получения времени:&lt;/p&gt;
  &lt;pre id=&quot;kVeK&quot; data-lang=&quot;kotlin&quot;&gt;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 -&amp;gt;
            TrustedTime.createClient(context)
                .addOnSuccessListener { client -&amp;gt;
                    trustedTimeClient = client
                    continuation.resume(client)
                }
                .addOnFailureListener { exception -&amp;gt;
                    continuation.resumeWithException(exception)
                }
        }
    }
}&lt;/pre&gt;
  &lt;p id=&quot;48hh&quot;&gt;Я использовала Koin при создании примера кода, поэтому файл DI может выглядеть следующим образом:&lt;/p&gt;
  &lt;pre id=&quot;jD08&quot; data-lang=&quot;kotlin&quot;&gt;val appModule = module {
    single&amp;lt;TimeService&amp;gt; {
        DefaultTimeService(
            context = androidContext(),
        )
    }

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

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

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

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

</content></entry><entry><id>greenhouse_mobile_dev:async_await_vs_completion_handler</id><link rel="alternate" type="text/html" href="https://teletype.in/@greenhouse_mobile_dev/async_await_vs_completion_handler?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=greenhouse_mobile_dev"></link><title>Почему async/await лучше completion handler?</title><published>2025-02-21T11:03:08.850Z</published><updated>2025-02-24T07:33:52.171Z</updated><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://img1.teletype.in/files/0d/52/0d525edc-8d6e-4866-8457-f1b583cc81ae.png"></media:thumbnail><category term="i-os" label="iOS"></category><summary type="html">&lt;img src=&quot;https://img4.teletype.in/files/32/23/3223e02f-fd94-450a-a1b8-c300b0a404ee.png&quot;&gt;Сегодня хотелось бы поговорить о преимуществах async/await, в сравнении с привычными нами способами работы с асинхронным кодом.</summary><content type="html">
  &lt;figure id=&quot;iP64&quot; class=&quot;m_column&quot;&gt;
    &lt;img src=&quot;https://img4.teletype.in/files/32/23/3223e02f-fd94-450a-a1b8-c300b0a404ee.png&quot; width=&quot;1200&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;J9yQ&quot;&gt;Всем привет, дорогие друзья!&lt;/p&gt;
  &lt;p id=&quot;UwwM&quot;&gt;Сегодня хотелось бы поговорить о преимуществах &lt;code&gt;async/await&lt;/code&gt;, в сравнении с привычными нами способами работы с асинхронным кодом. &lt;code&gt;async/await&lt;/code&gt; был представлен Apple на сессии WWDC 21, но к сожалению, не во всех проектах решаются перейти на данную технологию. Давайте узнаем как сделать наш код более читаемым, безопасным и надежным.&lt;/p&gt;
  &lt;p id=&quot;C4NR&quot;&gt;&lt;/p&gt;
  &lt;h2 id=&quot;tLu0&quot;&gt;Что такое async/await?&lt;/h2&gt;
  &lt;p id=&quot;X4L3&quot;&gt;&lt;code&gt;async/await&lt;/code&gt; – это современный способ работы с асинхронным кодом в Swift. Он делает код более читаемым, безопасным и удобным, заменяя сложные вложенные completion handlers или DispatchGroup.&lt;/p&gt;
  &lt;p id=&quot;hvUI&quot;&gt;Механизм &lt;code&gt;async/await&lt;/code&gt; основан на принципе структурированного параллелизма. Это означает, что асинхронный код выполняется последовательно в рамках своей области видимости, в отличие от замыканий, которые могут разрывать поток выполнения и усложнять его отслеживание.&lt;/p&gt;
  &lt;p id=&quot;IVfQ&quot;&gt;Пример асинхронного запроса через &lt;code&gt;completion handler&lt;/code&gt;:&lt;/p&gt;
  &lt;pre id=&quot;SDCx&quot; data-lang=&quot;swift&quot;&gt; // Service Layer
 		func fetchState(callback: @escaping (Result&amp;lt;GameShakeStateDataResponseModel, NetworkError&amp;gt;) -&amp;gt; Void) {
        let router = GameShakeRequestFactory.getState
        networkService.performRequest(router: router) {(result: Result&amp;lt;GameShakeStateDataResponseModel, NetworkError&amp;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)
            }
        }
    }
&lt;/pre&gt;
  &lt;p id=&quot;O5uW&quot;&gt;Если переписать код на &lt;code&gt;async/await&lt;/code&gt;:&lt;/p&gt;
  &lt;pre id=&quot;4X7b&quot; data-lang=&quot;swift&quot;&gt; // Service Layer 
 		func fetchState() async throws -&amp;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
            }
        }
    }
&lt;/pre&gt;
  &lt;p id=&quot;GHvk&quot;&gt;&lt;strong&gt;Основные составляющие кода:&lt;/strong&gt;&lt;/p&gt;
  &lt;ul id=&quot;v7w0&quot;&gt;
    &lt;li id=&quot;1Kco&quot;&gt;&lt;code&gt;async&lt;/code&gt; — объявляет асинхронную функцию;&lt;/li&gt;
    &lt;li id=&quot;ibIs&quot;&gt;&lt;code&gt;await&lt;/code&gt; — приостанавливает выполнение функций до завершения задачи;&lt;/li&gt;
    &lt;li id=&quot;gpJD&quot;&gt;&lt;code&gt;throws&lt;/code&gt; — указывает, что функция может выбрасывать ошибки;&lt;/li&gt;
    &lt;li id=&quot;073t&quot;&gt;&lt;code&gt;Task&lt;/code&gt; — запускает асинхронную операцию в синхронном коде.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;ClLm&quot;&gt;&lt;/p&gt;
  &lt;h2 id=&quot;sGuM&quot;&gt;Читаемость и простота кода&lt;/h2&gt;
  &lt;p id=&quot;DQYd&quot;&gt;Что хорошо во вложенности в &lt;code&gt;async/await&lt;/code&gt;- это то что ее нет.&lt;/p&gt;
  &lt;p id=&quot;ksz0&quot;&gt;В то время как в &lt;code&gt;completion handler&lt;/code&gt; одна из главных проблем — глубокая вложенность, также известная как callback hell. Когда одна асинхронная операция зависит от другой, &lt;code&gt;completion&lt;/code&gt; вложены друг в друга, усложняя чтение и поддержку.&lt;/p&gt;
  &lt;p id=&quot;ftR3&quot;&gt;Пример:&lt;/p&gt;
  &lt;pre id=&quot;vfhr&quot; data-lang=&quot;swift&quot;&gt;fetchData { result in
    switch result {
    case .success(let data):
        process(data) { processedData in
            save(processedData) { success in
                if success {
                    print(&amp;quot;Data saved&amp;quot;)
                }
            }
        }
    case .failure(let error):
        print(&amp;quot;Error: \\(error)&amp;quot;)
    }
}
&lt;/pre&gt;
  &lt;p id=&quot;RSvW&quot;&gt;С &lt;code&gt;async/await&lt;/code&gt; код остается линейным и чистым:&lt;/p&gt;
  &lt;ul id=&quot;bVBN&quot;&gt;
    &lt;li id=&quot;fb03&quot;&gt;Отсутствует вложенность;&lt;/li&gt;
    &lt;li id=&quot;Vcfu&quot;&gt;&lt;code&gt;do-catch&lt;/code&gt; охватывает все ошибки.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;pre id=&quot;DUPs&quot; data-lang=&quot;swift&quot;&gt;Task {
    do {
        let data = try await fetchData()
        let processedData = try await process(data)
        try await save(processedData)
        print(&amp;quot;Data saved&amp;quot;)
    } catch {
        print(&amp;quot;Error: \\(error)&amp;quot;)
    }
}
&lt;/pre&gt;
  &lt;p id=&quot;sNNI&quot;&gt;В данном примере наглядно виден принцип структурированного параллелизма. Код выполняется последовательно, строка за строкой. Мы можем ожидать асинхронную функцию до тех пор, пока она не вернет результат (или не выдаст ошибку) и не передаст его следующей функции.&lt;/p&gt;
  &lt;p id=&quot;mI8I&quot;&gt;&lt;/p&gt;
  &lt;h2 id=&quot;cpap&quot;&gt;Безопасность потоков&lt;/h2&gt;
  &lt;p id=&quot;f6we&quot;&gt;При использовании &lt;code&gt;async/await&lt;/code&gt;, большинство асинхронных операций (сетевые запросы, вычисления и т.д) выполняются в фоновом потоке. Однако при необходимости обновления UI, вам нужно явно указать переход на &lt;strong&gt;главный поток&lt;/strong&gt;. Иначе это может привести к сбою приложения. Так как вы пытаетесь изменить интерфейс из фонового потока.&lt;/p&gt;
  &lt;pre id=&quot;i0z9&quot; data-lang=&quot;swift&quot;&gt;  	func fetchState() async throws -&amp;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
            }
        }
    }&lt;/pre&gt;
  &lt;p id=&quot;HvSu&quot;&gt;Использование &lt;code&gt;@MainActor&lt;/code&gt; гарантирует безопасность при работе с асинхронным кодом. Что также помогает вам избежать проблем с потоками.&lt;/p&gt;
  &lt;pre id=&quot;z4eP&quot; data-lang=&quot;swift&quot;&gt;func fetchState() {
        Task {
            do {
                let offer = try await service.fetchState()
                await MainActor.run {
                   view?.updateUI(with: offer)
                }
            } catch {
                // handle error
            }
        }
 }
&lt;/pre&gt;
  &lt;p id=&quot;CjOr&quot;&gt;&lt;/p&gt;
  &lt;h2 id=&quot;Hn3R&quot;&gt;Обработка ошибок&lt;/h2&gt;
  &lt;p id=&quot;dIoK&quot;&gt;В &lt;code&gt;completion handler&lt;/code&gt; ошибки передаются через &lt;code&gt;Result&amp;lt;T, Error&amp;gt;&lt;/code&gt; или отдельный параметр &lt;code&gt;Error?&lt;/code&gt;. Однако, если забыть вызвать &lt;code&gt;completion&lt;/code&gt;, вызывающий код может зависнуть. Это приведет к бесконечной загрузке счетчика или неопределенному состоянию пользовательского интерфейса.&lt;/p&gt;
  &lt;p id=&quot;avzF&quot;&gt;В &lt;code&gt;async/await&lt;/code&gt; ошибки всегда требуют обработки:&lt;/p&gt;
  &lt;ul id=&quot;jwnD&quot;&gt;
    &lt;li id=&quot;S6QD&quot;&gt;Если функция объявлена как &lt;code&gt;throws&lt;/code&gt;, компилятор не позволит пропустить обработку;&lt;/li&gt;
    &lt;li id=&quot;o1AR&quot;&gt;Исключение сразу указывает на источник проблемы.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;V1Kr&quot;&gt;&lt;/p&gt;
  &lt;h2 id=&quot;8SO3&quot;&gt;Параллельность и производительность&lt;/h2&gt;
  &lt;p id=&quot;jVJx&quot;&gt;&lt;code&gt;async/await&lt;/code&gt; в Swift работает быстрее, чем старые подходы (замыкания, &lt;code&gt;DispatchGroup&lt;/code&gt;, &lt;code&gt;OperationQueue&lt;/code&gt;), потому как эффективнее управляет потоками и использует кооперативную многозадачность.&lt;/p&gt;
  &lt;ul id=&quot;OIQ1&quot;&gt;
    &lt;li id=&quot;qLTi&quot;&gt;Нет необходимости вручную управлять потоками или задачами, что предотвращает создание лишних потоков;&lt;/li&gt;
    &lt;li id=&quot;zGFb&quot;&gt;Обеспечена параллельная работа задач без блокировки главного потока. Например, когда операция ожидает завершения при запросах в сети или в работе с диском, остальные операции могут продолжать выполняться;&lt;/li&gt;
    &lt;li id=&quot;2AU6&quot;&gt;Минимизированы накладные расходы на переключение контекста.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;CHbi&quot;&gt;Говоря о параллельном выполнений задач, &lt;code&gt;async/await&lt;/code&gt; предлагает ряд инструментов (&lt;code&gt;async let&lt;/code&gt;, &lt;code&gt;TaskGroup&lt;/code&gt;,&lt;code&gt;Task.detached&lt;/code&gt;). В данной статье мы остановимся на &lt;code&gt;async let&lt;/code&gt; , в сравнений с &lt;code&gt;DispatchGroup&lt;/code&gt;.&lt;/p&gt;
  &lt;p id=&quot;u5D2&quot;&gt;Вспомним как выглядит код с &lt;code&gt;DispatchGroup&lt;/code&gt;:&lt;/p&gt;
  &lt;pre id=&quot;LojG&quot; data-lang=&quot;swift&quot;&gt;    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
        }
    }
&lt;/pre&gt;
  &lt;p id=&quot;7NaP&quot;&gt;&lt;code&gt;DispatchGroup&lt;/code&gt; требует:&lt;/p&gt;
  &lt;ul id=&quot;ihWn&quot;&gt;
    &lt;li id=&quot;bjZ0&quot;&gt;Явного вызова &lt;code&gt;enter()&lt;/code&gt; и &lt;code&gt;leave()&lt;/code&gt;, что потенциально повышает вероятность ошибок (например, забыв вызвать &lt;code&gt;leave()&lt;/code&gt;);&lt;/li&gt;
    &lt;li id=&quot;fG6r&quot;&gt;Отслеживания завершения через &lt;code&gt;notify()&lt;/code&gt;, что добавляет дополнительные элементы синхронизации;&lt;/li&gt;
    &lt;li id=&quot;Vfqy&quot;&gt;Ручной обработки ошибок в каждом замыкании.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;ZMDJ&quot;&gt;&lt;code&gt;async let&lt;/code&gt; — это способ параллельного выполнения асинхронных операций.&lt;/p&gt;
  &lt;p id=&quot;U4Du&quot;&gt;Он позволяет запустить несколько независимых задач одновременно и затем дождаться их завершения без блокировки. Пример кода:&lt;/p&gt;
  &lt;pre id=&quot;6J0I&quot; data-lang=&quot;swift&quot;&gt; Task {
	 async let forYouData = fetchForYou()
   async let loyaltyStateData = fetchLoyaltyState()
   async let gameRulesData = fetchGameRules()

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

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

</content></entry><entry><id>greenhouse_mobile_dev:text_field_in_jetpack_compose</id><link rel="alternate" type="text/html" href="https://teletype.in/@greenhouse_mobile_dev/text_field_in_jetpack_compose?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=greenhouse_mobile_dev"></link><title>Как улучшить UX приложения при работе с полями ввода</title><published>2024-07-21T13:27:24.897Z</published><updated>2024-07-22T08:41:56.133Z</updated><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://img4.teletype.in/files/3e/12/3e125436-4d15-4c10-a202-44d3cf72beff.png"></media:thumbnail><category term="android" label="Android"></category><summary type="html">&lt;img src=&quot;https://img1.teletype.in/files/00/40/00409d6e-9d5d-41f9-b85e-d45cc8a94ffe.png&quot;&gt;При работе с полями ввода пришлось преодолеть множество граблей, что позволило создать статью про опытом решения проблем с UX</summary><content type="html">
  &lt;figure id=&quot;uAt8&quot; class=&quot;m_column&quot;&gt;
    &lt;img src=&quot;https://img1.teletype.in/files/00/40/00409d6e-9d5d-41f9-b85e-d45cc8a94ffe.png&quot; width=&quot;1200&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;SaBp&quot;&gt;Всем привет дорогие друзья! &lt;/p&gt;
  &lt;p id=&quot;H1Fp&quot;&gt;Сегодня хотелось бы поговорить о наболевшем, о пользовательском опыте на экранах с полями ввода. Как правило, именно с полей ввода начинается знакомство пользователя с приложением. Стоит отметить, что ответственность за UX лежит не только на дизайнере, но и на разработчике. Начнём с настроек клавиатуры.&lt;/p&gt;
  &lt;p id=&quot;jVxj&quot;&gt;&lt;/p&gt;
  &lt;h2 id=&quot;jlTI&quot;&gt;Типы клавиатур&lt;/h2&gt;
  &lt;p id=&quot;TNg4&quot;&gt;В Android доступно несколько типов клавиатуры для осуществления первичной фильтрации вводимых данных. Ниже будут приведены типы, доступные для Jetpack Compose.&lt;/p&gt;
  &lt;p id=&quot;qeKq&quot;&gt;&lt;/p&gt;
  &lt;h3 id=&quot;At2t&quot;&gt;Буквенные клавиатуры&lt;/h3&gt;
  &lt;p id=&quot;sqXN&quot;&gt;К буквенным можно отнести клавиатуры с типом:&lt;/p&gt;
  &lt;ul id=&quot;JGVn&quot;&gt;
    &lt;li id=&quot;xzYU&quot;&gt;Text (стандартная текстовая клавиатура);&lt;/li&gt;
    &lt;li id=&quot;79yZ&quot;&gt;Ascii (клавиатура позволяет ограничить ввод только ASCII символов);&lt;/li&gt;
    &lt;li id=&quot;ULuJ&quot;&gt;Password (клавиатура позволяет вводить различные знаки препинания и цифры, при открытии клавиатура переключится на латинский алфавит);&lt;/li&gt;
    &lt;li id=&quot;wn1U&quot;&gt;Email (клавиатура для ввода электронного адреса, имеет вспомогательные кнопки в нижнем ряду для ускорения ввода данных);&lt;/li&gt;
    &lt;li id=&quot;qojo&quot;&gt;Uri (клавиатура для ввода URI, имеет вспомогательные кнопки в нижнем ряду для ускорения ввода данных).&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;Voak&quot;&gt;&lt;/p&gt;
  &lt;figure id=&quot;3op3&quot; class=&quot;m_column&quot;&gt;
    &lt;img src=&quot;https://img4.teletype.in/files/33/c4/33c4776a-c3a0-4f30-8725-97e274e1605a.png&quot; width=&quot;1080&quot; /&gt;
    &lt;figcaption&gt;Примеры отображения буквенных клавиатур&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;vkkv&quot;&gt;&lt;/p&gt;
  &lt;h3 id=&quot;5duZ&quot;&gt;Числовые клавиатуры&lt;/h3&gt;
  &lt;p id=&quot;IMVI&quot;&gt;К числовым можно отнести клавиатуры с типом:&lt;/p&gt;
  &lt;ul id=&quot;JGVn&quot;&gt;
    &lt;li id=&quot;Q286&quot;&gt;Number (клавиатура для ввода цифр и ряда символов);&lt;/li&gt;
    &lt;li id=&quot;X2Dz&quot;&gt;NumberPassword (клавиатура для ввода цифровых паролей, в отличие от Number не позволяет ввести ничего кроме цифр);&lt;/li&gt;
    &lt;li id=&quot;EZnn&quot;&gt;Phone (клавиатура для ввода номера телефона и USSD-команд);&lt;/li&gt;
    &lt;li id=&quot;v3IX&quot;&gt;Decimal (клавиатура для ввода десятичных дробей).&lt;/li&gt;
  &lt;/ul&gt;
  &lt;figure id=&quot;SzVl&quot; class=&quot;m_column&quot;&gt;
    &lt;img src=&quot;https://img2.teletype.in/files/9d/02/9d020396-39d6-4249-bfc9-303665f84325.png&quot; width=&quot;1080&quot; /&gt;
    &lt;figcaption&gt;Примеры отображения числовых клавиатур&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;hchF&quot;&gt;&lt;/p&gt;
  &lt;h3 id=&quot;CuUH&quot;&gt;Указание типа клавиатуры в Jetpack Compose&lt;/h3&gt;
  &lt;p id=&quot;aMCc&quot;&gt;Указать тип клавиатуры для поля ввода можно при помощи класса &lt;code&gt;KeyboardOptions&lt;/code&gt;.&lt;/p&gt;
  &lt;pre id=&quot;iQLk&quot; data-lang=&quot;kotlin&quot;&gt;TextField(
    keyboardOptions = KeyboardOptions.Default.copy(
            keyboardType = KeyboardType.Text, // Указываем тип клавиатуры    
    ),    
    ...
)&lt;/pre&gt;
  &lt;p id=&quot;YT62&quot;&gt;&lt;/p&gt;
  &lt;p id=&quot;qleF&quot;&gt;Помимо типа клавиатуры класс &lt;code&gt;KeyboardOptions&lt;/code&gt; позволяет настраивать такие параметры как:&lt;/p&gt;
  &lt;ul id=&quot;e1u3&quot;&gt;
    &lt;li id=&quot;GIDI&quot;&gt;&lt;code&gt;capitalization&lt;/code&gt; - сообщает клавиатуре, следует ли автоматически использовать заглавные буквы в словах и предложениях;&lt;/li&gt;
    &lt;li id=&quot;qqzI&quot;&gt;&lt;code&gt;autoCorrect&lt;/code&gt; - сообщает клавиатуре, следует ли выполнять автоисправление для введёного текста;&lt;/li&gt;
    &lt;li id=&quot;FzVm&quot;&gt;&lt;code&gt;imeAction&lt;/code&gt; - сообщает клавиатуре о необходимости изменить внешний вид вспомогательной кнопки на клавиатуре. Об этом поговорим чуть позднее, в рамках данной статьи.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;8BxW&quot;&gt;&lt;/p&gt;
  &lt;h3 id=&quot;0Awf&quot;&gt;Пароли и типы клавиатур&lt;/h3&gt;
  &lt;p id=&quot;kAzG&quot;&gt;Если вы когда-нибудь видели поведение клавиатуры при записи экрана на iOS, во время ввода пароля, то для Android стоит снизить ожидания.&lt;/p&gt;
  &lt;figure id=&quot;n72l&quot; class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://img2.teletype.in/files/10/88/1088f571-14aa-4645-898a-7a6fabb74595.jpeg&quot; width=&quot;192.63061137238137&quot; /&gt;
    &lt;figcaption&gt;Скрытие содержимого клавиатуры и поля ввода при записи экрана на iOS&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;TA6U&quot;&gt;При использовании XML для полей ввода указывался параметр &lt;code&gt;android:inputType=&amp;quot;textPassword&amp;quot;&lt;/code&gt;, который включал маску для вводимых символов.&lt;/p&gt;
  &lt;figure id=&quot;MzCS&quot; class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://img3.teletype.in/files/e3/57/e35769fb-b748-4cb0-99ef-7e7c9ca3a306.jpeg&quot; width=&quot;193.46153846153857&quot; /&gt;
    &lt;figcaption&gt;Отсутствие скрытия содержимого клавиатуры и поля ввода при записи экрана на Android&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;FV2R&quot;&gt;При использовании Jetpack Compose это работает по другому. Для создания маски-пароля требуется передать &lt;code&gt;PasswordVisualTransformation()&lt;/code&gt; в  &lt;code&gt;visualTransformation&lt;/code&gt;. Установка &lt;code&gt;keyboardType&lt;/code&gt; в этом случае лишь подготовит клавиатуру к вводу пароля.&lt;/p&gt;
  &lt;pre id=&quot;e27w&quot; data-lang=&quot;kotlin&quot;&gt;TextField(
    visualTransformation = PasswordVisualTransformation(), // Указываем маску для поля ввода пароля    
    keyboardOptions = KeyboardOptions.Default.copy(
            keyboardType = KeyboardType.Password, // Указываем тип клавиатуры    
    ),    
   ...
)&lt;/pre&gt;
  &lt;p id=&quot;iMlw&quot;&gt;&lt;/p&gt;
  &lt;p id=&quot;UBDX&quot;&gt;Однако, если требуется повысить уровень безопасности экрана ввода пароля, рекомендуется использовать &lt;a href=&quot;https://developer.android.com/reference/android/view/WindowManager.LayoutParams#FLAG_SECURE&quot; target=&quot;_blank&quot;&gt;SECURE_FLAG&lt;/a&gt;. Данный флаг предотвратит запись видео или создание скриншота на экране.&lt;/p&gt;
  &lt;p id=&quot;GUuS&quot;&gt;&lt;/p&gt;
  &lt;h2 id=&quot;VbD0&quot;&gt;Типы действий на клавиатуре&lt;/h2&gt;
  &lt;p id=&quot;8ICF&quot;&gt;На клавиатуре имеется &lt;strong&gt;кнопка действия (ImeAction)&lt;/strong&gt;. &lt;/p&gt;
  &lt;figure id=&quot;etcw&quot; class=&quot;m_retina&quot;&gt;
    &lt;img src=&quot;https://img1.teletype.in/files/c7/9d/c79d3f5a-1703-4342-b5e7-bdbc4aa402f0.jpeg&quot; width=&quot;540&quot; /&gt;
    &lt;figcaption&gt;ImeAction на клавиатуре в Android&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;j5eA&quot;&gt;Иногда использование данной кнопки является более удобным путём для пользователя, так как она находится в нижней части экрана. Например, не потребуется тянуться пальцем в верхнюю часть экрана.&lt;br /&gt;Клавиатура поддерживает следующие действия:&lt;/p&gt;
  &lt;p id=&quot;HSGF&quot;&gt;&lt;strong&gt;Default&lt;/strong&gt;&lt;/p&gt;
  &lt;p id=&quot;eY2I&quot;&gt;Система самостоятельно выберет действие для клавиатуры. Как правило это &lt;code&gt;None&lt;/code&gt; или &lt;code&gt;Done&lt;/code&gt;.&lt;/p&gt;
  &lt;p id=&quot;0Q7O&quot;&gt;&lt;strong&gt;None&lt;/strong&gt;&lt;/p&gt;
  &lt;p id=&quot;n9Lu&quot;&gt;При клике на данное действие курсор перейдёт на новую строку.&lt;/p&gt;
  &lt;p id=&quot;Gr8E&quot;&gt;&lt;strong&gt;Go&lt;/strong&gt;&lt;/p&gt;
  &lt;p id=&quot;dOPK&quot;&gt;При клике на действие предполагается переход куда-либо. Например, переход по введенному URL.&lt;/p&gt;
  &lt;p id=&quot;anCC&quot;&gt;&lt;strong&gt;Search&lt;/strong&gt;&lt;/p&gt;
  &lt;p id=&quot;1nHB&quot;&gt;При клике на действие предполагается выполнение поискового запроса по введённому значению.&lt;/p&gt;
  &lt;p id=&quot;ZwuZ&quot;&gt;&lt;strong&gt;Send&lt;/strong&gt;&lt;/p&gt;
  &lt;p id=&quot;qyMc&quot;&gt;При клике на действие предполагается отправка текста.&lt;/p&gt;
  &lt;p id=&quot;Wtjt&quot;&gt;&lt;strong&gt;Previous&lt;/strong&gt;&lt;/p&gt;
  &lt;p id=&quot;jAQd&quot;&gt;При клике на действие предполагается перевод фокуса на предыдущее поле ввода, если оно есть.&lt;/p&gt;
  &lt;p id=&quot;xJMf&quot;&gt;&lt;strong&gt;Next&lt;/strong&gt;&lt;/p&gt;
  &lt;p id=&quot;GVN7&quot;&gt;При клике на действие предполагается перевод фокуса на следующее поле ввода, если оно есть.&lt;/p&gt;
  &lt;p id=&quot;fgFw&quot;&gt;&lt;strong&gt;Done&lt;/strong&gt;&lt;/p&gt;
  &lt;p id=&quot;rQUj&quot;&gt;При клике на действие предполагается выполнение какого-либо завершающего действия (закрытие клавиатуры/отображение прогресса и отправка запроса).&lt;/p&gt;
  &lt;p id=&quot;E7KE&quot;&gt;&lt;/p&gt;
  &lt;p id=&quot;p80j&quot;&gt;Важно отметить, что иконки на &lt;code&gt;ImeAction&lt;/code&gt; могут отличаться в зависимости от выбранной системной клавиатуры. &lt;/p&gt;
  &lt;p id=&quot;UrMr&quot;&gt;&lt;/p&gt;
  &lt;h3 id=&quot;d2Za&quot;&gt;Указание ImeAction в Jetpack Compose&lt;/h3&gt;
  &lt;p id=&quot;Gz1A&quot;&gt;Указание &lt;code&gt;ImeAction&lt;/code&gt;, как говорилось ранее, осуществляется с помощью класса &lt;code&gt;KeyboardOptions&lt;/code&gt;.&lt;/p&gt;
  &lt;pre id=&quot;mVoV&quot; data-lang=&quot;kotlin&quot;&gt;TextField(
    keyboardOptions = KeyboardOptions.Default.copy(       
            imeAction = ImeAction.Search, // Указываем тип кнопки действия    
    ),    
    keyboardActions = KeyboardActions {
            // Указываем код, который требуется выполнить при клике на ImeAction    
    },    
    ...
)&lt;/pre&gt;
  &lt;p id=&quot;1m9B&quot;&gt;&lt;/p&gt;
  &lt;h2 id=&quot;kLiA&quot;&gt;Размещение контента над клавиатурой и другие проблемы, связанные с этим вопросом&lt;/h2&gt;
  &lt;p id=&quot;BIf1&quot;&gt;На StackOverflow можно найти множество способов решения различных проблем, связанных с размещением контента над клавиатурой. Ниже я приведу примеры кейсов, с которыми мы сталкивались на наших проектах, и поделюсь решениями, к которым мы пришли.&lt;/p&gt;
  &lt;p id=&quot;fhpx&quot;&gt;&lt;/p&gt;
  &lt;h3 id=&quot;QdEV&quot;&gt;&lt;strong&gt;Перекрытие контента прижатой к низу кнопкой при открытой клавиатуре&lt;/strong&gt;&lt;/h3&gt;
  &lt;p id=&quot;mP3z&quot;&gt;Периодически в дизайне можно встретить ситуацию, когда поле ввода находится в верхней части экрана, а кнопка прижата к низу экрана и при открытии клавиатуры требуется поднять кнопку, перекрывая контент кнопкой.&lt;/p&gt;
  &lt;figure id=&quot;ephW&quot; class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://img3.teletype.in/files/6e/d5/6ed5aeea-1c53-4e3b-b424-aa764b3aceb0.gif&quot; width=&quot;193.37500000000003&quot; /&gt;
    &lt;figcaption&gt;Пример перекрытия контента при открытии клавиатуры&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;tF7b&quot;&gt;В данном случае мы рекомендуем установить значение &lt;code&gt;adjustResize&lt;/code&gt; для параметра &lt;code&gt;windowSoftInputMode&lt;/code&gt; для Activity. Например, через Manifest.&lt;br /&gt;Если в проекте используется &lt;code&gt;BottomSheetDialogFragment&lt;/code&gt;, для него значение устанавливается отдельно через тему, так как данный класс имеет свой &lt;code&gt;Window&lt;/code&gt;. Дополнительно, стоит указывать метод &lt;code&gt;imePadding&lt;/code&gt; , который добавляет &lt;code&gt;Spacer&lt;/code&gt; равный размеру клавиатуры для корректного подъема контента.&lt;/p&gt;
  &lt;p id=&quot;ww7H&quot;&gt;Верстка с использованием Jetpack Compose при этом может выглядеть следующим образом:&lt;/p&gt;
  &lt;pre id=&quot;iBNu&quot; data-lang=&quot;kotlin&quot;&gt;Box(modifier = Modifier.fillMaxSize()) {
    Column(modifier = Modifier.fillMaxSize()) {
            SomeHeader()        
            SomeBody()    
    }
        
    OverlayFooter()
}&lt;/pre&gt;
  &lt;p id=&quot;dwqq&quot;&gt;&lt;/p&gt;
  &lt;h3 id=&quot;4KYG&quot;&gt;Деформация контента при использовании adjustResize&lt;/h3&gt;
  &lt;p id=&quot;ToLi&quot;&gt;Помните, что когда вы работаете с &lt;code&gt;adjustResize&lt;/code&gt;, при недостатке места на экране вёрстка может начать деформироваться.&lt;/p&gt;
  &lt;figure id=&quot;sz8x&quot; class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://img2.teletype.in/files/9d/40/9d40eaf5-685b-43d0-b7b7-8d0fdf7957e7.jpeg&quot; width=&quot;193.46153846153848&quot; /&gt;
    &lt;figcaption&gt;Деформация кнопки при недостаточном количестве места для контента при открытии клавиатуры&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;pL1D&quot;&gt;Не забывайте проверять экраны с полями ввода на эмуляторах или физических устройствах с разной диагональю и настройками масштабирования контента в системе. &lt;/p&gt;
  &lt;p id=&quot;N1hg&quot;&gt;При необходимости добавляйте для контейнера возможность вертикального скролла. Это позволит избежать деформации верстки при недостатке экранного пространства.&lt;/p&gt;
  &lt;figure id=&quot;JP6u&quot; class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://img3.teletype.in/files/65/1a/651a79e4-5de7-4dfc-bff5-cb8c0ad78791.gif&quot; width=&quot;192.95312500000003&quot; /&gt;
    &lt;figcaption&gt;Экран с возможностью скрола при недостаточном количестве места для контента при открытии клавиатуры&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;l6U3&quot;&gt;&lt;/p&gt;
  &lt;h3 id=&quot;Lz07&quot;&gt;Поднятие контента над клавиатурой в BottomSheetDialogFragment&lt;/h3&gt;
  &lt;p id=&quot;B711&quot;&gt;Достаточно часто в дизайне можно встретить боттомшит с полем ввода и кнопкой. Как правило, дизайнеры просят, чтобы при открытии клавиатуры весь контент включая кнопку поднимался над клавиатурой.&lt;/p&gt;
  &lt;figure id=&quot;uzr1&quot; class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://img1.teletype.in/files/c7/b3/c7b324f3-2b35-490d-97fa-3af7f154ca6d.jpeg&quot; width=&quot;193.15384615384616&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;VaT0&quot;&gt;В этом случае, для BottomSheetDialogFragment следует указать &lt;code&gt;adjustResize&lt;/code&gt; через тему. Это сработает на устройствах большинства производителей. &lt;/p&gt;
  &lt;p id=&quot;ueYz&quot;&gt;Однако для некоторых устройств Huawei для кнопки стоит указать параметр &lt;a href=&quot;https://developer.android.com/reference/kotlin/androidx/compose/foundation/relocation/BringIntoViewRequester&quot; target=&quot;_blank&quot;&gt;BringIntoViewRequester&lt;/a&gt;, который позволит проскролить контент до элемента, тем самым поднимет кнопку над клавиатурой.&lt;/p&gt;
  &lt;pre id=&quot;tC91&quot; data-lang=&quot;kotlin&quot;&gt;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 = &amp;quot;Отправить&amp;quot;)
      }
)
      
LaunchedEffect(key1 = isFocused.value) {
    if (isFocused.value) {
            bringIntoViewRequester.bringIntoView()    
    }
}&lt;/pre&gt;
  &lt;p id=&quot;XVlt&quot;&gt;&lt;/p&gt;
  &lt;h3 id=&quot;ekdp&quot;&gt;Некорректная позиция handle в полях ввода внутри BottomSheetDialogFragment&lt;/h3&gt;
  &lt;p id=&quot;kOv3&quot;&gt;Капелька, которая появляется при выделении или фокусе на тексте называется handle. Если вы встречались с поведением, как на скриншоте ниже, то у меня есть для вас крутая новость - это можно пофиксить.&lt;/p&gt;
  &lt;figure id=&quot;bw40&quot; class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://img4.teletype.in/files/3b/8e/3b8eed9b-8836-433a-aa9b-16320ebde19a.gif&quot; width=&quot;192.94531249999997&quot; /&gt;
    &lt;figcaption&gt;Некорректная позиция handle при открытии клавиатуры на боттомшите&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;7Ma4&quot;&gt;Данный баг связан с некорректной передачей координат местоположения поля ввода на экране в метод отрисовки handle. &lt;/p&gt;
  &lt;p id=&quot;9Zin&quot;&gt;Данное поведение можно исправить разместив контент боттомшита внутри Box:&lt;/p&gt;
  &lt;pre id=&quot;QCHs&quot; data-lang=&quot;kotlin&quot;&gt;Box(
    modifier = Modifier.fillMaxSize(),
    contentAlignment = Alignment.BottomCenter,
) {
    // Контент боттомшита
}&lt;/pre&gt;
  &lt;p id=&quot;TMIS&quot;&gt;&lt;/p&gt;
  &lt;p id=&quot;Xkui&quot;&gt;На этом наш пост подошёл к концу. Надеюсь, мой опыт работы с полями ввода поможет вам снизить количество граблей во время разработки функционала. Помните, что разработчик тоже влияет на UX приложения. &lt;/p&gt;
  &lt;p id=&quot;gRIC&quot;&gt;Good coding and happy day!🤘&lt;/p&gt;
  &lt;p id=&quot;Rxul&quot;&gt;&lt;/p&gt;
  &lt;h1 id=&quot;9B5g&quot;&gt;Полезные ссылки:&lt;/h1&gt;
  &lt;ul id=&quot;QPPb&quot;&gt;
    &lt;li id=&quot;dHTq&quot;&gt;&lt;a href=&quot;https://developer.android.com/reference/kotlin/androidx/compose/ui/text/input/ImeAction&quot; target=&quot;_blank&quot;&gt;Страница об ImeAction&lt;/a&gt;&lt;/li&gt;
    &lt;li id=&quot;sLzc&quot;&gt;&lt;a href=&quot;https://developer.android.com/reference/kotlin/androidx/compose/ui/text/input/KeyboardType&quot; target=&quot;_blank&quot;&gt;Страница о KeyboardType&lt;/a&gt;&lt;/li&gt;
    &lt;li id=&quot;Xbc7&quot;&gt;&lt;a href=&quot;https://developer.android.com/develop/ui/views/touch-and-input/keyboard-input/visibility&quot; target=&quot;_blank&quot;&gt;Документация по обработке системной клавиатуры&lt;/a&gt;&lt;/li&gt;
  &lt;/ul&gt;
  &lt;hr /&gt;
  &lt;p id=&quot;OYmd&quot;&gt;&lt;a href=&quot;https://t.me/greenhouse_mobile_dev&quot; target=&quot;_blank&quot;&gt;Telegram-канал Теплица&lt;/a&gt;&lt;/p&gt;
  &lt;p id=&quot;dYFl&quot;&gt;&lt;a href=&quot;mailto:greenhouse.mobile.dev@gmail.com&quot; target=&quot;_blank&quot;&gt;Обратная связь&lt;/a&gt;&lt;/p&gt;

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

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

</content></entry></feed>