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

Citymobil

О переезде с Redis на Redis-cluster

18.08.2020 14:15:47 | Автор: admin


Приходя в продукт, который развивается больше десятка лет, совершенно не удивительно встретить в нем устаревшие технологии. Но что если через полгода вы должны держать нагрузку в 10 раз выше, а цена падений увеличится в сотни раз? В этом случае вам необходим крутой Highload Engineer. Но за неимением горничной такового, решать проблему доверили мне. В первой части статьи я расскажу, как мы переезжали с Redis на Redis-cluster, а во второй части дам советы, как начать пользоваться кластером и на что обратить внимание при эксплуатации.


Выбор технологии


Так ли плох отдельный Redis (standalone redis) в конфигурации 1 мастер и N слейвов? Почему я называю его устаревшей технологией?


Нет, Redis не так плох Однако, есть некоторые недочеты которые нельзя игнорировать.

  • Во-первых, Redis не поддерживает механизмы аварийного восстановления после падения мастера. Для решения этой проблемы мы использовали конфигурацию с автоматическим перекидыванием VIP-ов на новый мастер, сменой роли одного из слейвов и переключением остальных. Этот механизм работал, но назвать его надежным решением было нельзя. Во первых, случались ложные срабатывания, а во вторых, он был одноразовым, и после срабатывания требовались ручные действия по взводу пружины.


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



Какие есть варианты?


  • Самое дорогое и богатое решение Redis-Enterprise. Это коробочное решение с полной технической поддержкой. Несмотря на то, что оно выглядит идеальным с технической точки зрения, нам не подошло по идеологическим соображениям.
  • Redis-cluster. Из коробки есть поддержка аварийного переключения мастера и шардирования. Интерфейс почти не отличается от обычной версии. Выглядит многообещающе, про подводные камни поговорим далее.
  • Tarantool, Memcache, Aerospike и другие. Все эти инструменты делают примерно то же самое. Но у каждого есть свои недостатки. Мы решили не класть все яйца в одну корзину. Memcache и Tarantool мы используем для других задач, и, забегая вперед, скажу, что на нашей практике проблем с ними было больше.

Специфика использования


Давайте взглянем, какие задачи мы исторически решали Redisом и какую функциональность использовали:


  • Кеш перед запросами к удаленным сервисам вроде 2GIS | Golang
    GET SET MGET MSET "SELECT DB"
  • Кеш перед MYSQL | PHP
    GET SET MGET MSET SCAN "KEY BY PATTERN" "SELECT DB"
  • Основное хранилище для сервиса работы с сессиями и координатами водителей | Golang
    GET SET MGET MSET "SELECT DB" "ADD GEO KEY" "GET GEO KEY" SCAN

Как видите, никакой высшей математики. В чём же тогда сложность? Давайте разберем отдельно по каждому методу.


Метод Описание Особенности Redis-cluster Решение
GET SET Записать/прочитать ключ
MGET MSET Записать/прочитать несколько ключей Ключи будут лежать на разных нодах. Готовые библиотеки умеют делать Multi-операции только в рамках одной ноды Заменить MGET на pipeline из N GET операций
SELECT DB Выбрать базу, с которой будем работать Не поддерживает несколько баз данных Складывать всё в одну базу. Добавить к ключам префиксы
SCAN Пройти по всем ключам в базе Поскольку у нас одна база, проходить по всем ключам в кластере слишком затратно Поддерживать инвариант внутри одного ключа и делать HSCAN по этому ключу. Или отказаться совсем
GEO Операции работы с геоключом Геоключ не шардируется
KEY BY PATTERN Поиск ключа по паттерну Поскольку у нас одна база, будем искать по всем ключам в кластере. Слишком затратно Отказаться или поддерживать инвариант, как и в случае со SCAN-ом

Redis vs Redis-cluster


Что мы теряем и что получаем при переходе на кластер?


  • Недостатки: теряем функциональность нескольких баз.
    • Если мы хотим хранить в одном кластере логически не связанные данные, придется делать костыли в виде префиксов.
    • Теряем все операции по базе, такие как SCAN, DBSIZE, CLEAR DB и т.п.
    • Multi-операции стали значительно сложнее в реализации, потому что может требоваться обращение к нескольким нодам.
  • Достоинства:
    • Отказоустойчивость в виде аварийного переключения мастера.
    • Шардирования на стороне Redis.
    • Перенос данных между нодами атомарно и без простоев.
    • Добавление и перераспределение мощностей и нагрузок без простоев.

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


Подготовка к переезду


Начнем с требований к переезду:


  • Он должен быть бесшовным. Полная остановка сервиса на 5 минут нас не устраивает.
  • Он должен быть максимально безопасным и постепенным. Хочется иметь какой-то контроль над ситуацией. Бухнуть сразу всё и молиться над кнопкой отката мы не желаем.
  • Минимальные потери данных при переезде. Мы понимаем, что переехать атомарно будет очень сложно, поэтому допускаем некоторую рассинхронизацию между данными в обычном и кластерном Redis.

Обслуживание кластера


Перед самым переездом следует задуматься о том, а можем ли мы поддерживать кластер:


  • Графики. Мы используем Prometheus и Grafana для графиков загрузки процессоров, занятой памяти, количества клиентов, количества операций GET, SET, AUTH и т.п.
  • Экспертиза. Представьте, что завтра под вашей ответственностью будет огромный кластер. Если он сломается, никто, кроме вас, починить его не сможет. Если он начнет тормозить все побегут к вам. Если нужно добавить ресурсы или перераспределить нагрузку снова к вам. Чтобы не поседеть в 25, желательно предусмотреть эти случаи и проверить заранее, как технология поведет себя при тех или иных действиях. Поговорим об этом подробнее в разделе Экспертиза.
  • Мониторинги и оповещения. Когда кластер ломается, об этом хочется узнать первым. Тут мы ограничились оповещением о том, что все ноды возвращают одинаковую информацию о состоянии кластера (да, бывает и по-другому). А остальные проблемы быстрее заметить по оповещениям сервисов-клиентов Redis.

Переезд


Как будем переезжать:


  • В первую очередь, нужно подготовить библиотеку для работы с кластером. В качестве основы для версии на Gо мы взяли go-redis и немного изменили под себя. Реализовали Multi-методы через pipeline-ы, а также немного поправили правила повторения запросов. С версией для PHP возникло больше проблем, но в конечном счете мы остановились на php-redis. Недавно они внедрили поддержку кластера, и на наш взгляд она выглядит хорошо.
  • Далее нужно развернуть сам кластер. Делается это буквально в две команды на основе конфигурационного файла. Подробнее настройку обсудим ниже.
  • Для постепенного переезда мы используем dry-mode. Так как у нас есть две версии библиотеки с одинаковым интерфейсом (одна для обычной версии, другая для кластера), ничего не стоит сделать обертку, которая будет работать с отдельной версией и параллельно дублировать все запросы в кластер, сравнивать ответы и писать расхождения в логи (в нашем случае в NewRelic). Таким образом, даже если при выкатке кластерная версия сломается, наш production это не затронет.
  • Выкатив кластер в dry-режиме, мы можем спокойно смотреть на график расхождений ответов. Если доля ошибок медленно, но верно движется к некоторой небольшой константе, значит, всё хорошо. Почему расхождения всё равно есть? Потому что запись в отдельной версии происходит несколько раньше, чем в кластере, и за счет микролага данные могут расходиться. Осталось только посмотреть на логи расхождений, и если все они объяснимы неатомарностью записи, то можно идти дальше.
  • Теперь можно переключить dry-mode в обратную сторону. Писать и читать будем из кластера, а дублировать в отдельную версию. Зачем? В течение следующей недели хочется понаблюдать за работой кластера. Если вдруг выяснится, что в пике нагрузки есть проблемы, или мы что-то не учли, у нас всегда есть аварийный откат на старый код и актуальные данные в благодаря dry-mode.
  • Осталось отключить dry-mode и демонтировать отдельную версию.

Экспертиза


Сначала кратко об устройстве кластера.


В первую очередь, Redis key-value хранилище. В качестве ключа используются произвольные строки. В качестве значений могут использоваться числа, строки и целые структуры. Последних великое множество, но для понимания общего устройства нам это не важно.
Следующий после ключей уровень абстракции слоты (SLOTS). Каждый ключ принадлежит одному из 16 383 слотов. Внутри каждого слота может быть сколько-угодно ключей. Таким образом, все ключи распадаются на 16 383 непересекающихся множеств.


Далее, в кластере должно быть N мастер-нод. Каждую ноду можно представлять как отдельный инстанс Redis, который знает всё о других нодах внутри кластера. Каждая мастер-нода содержит некоторое количество слотов. Каждый слот принадлежит только одной мастер-ноде. Все слоты нужно распределить между нодами. Если какие-то слоты не распределены, то хранящиеся в них ключи будут недоступны. Каждую мастер-ноду имеет смысл запускать на отдельной логической или физической машине. Также стоит помнить, что каждая нода работает только на одном ядре, и если вы хотите запустить на одной логической машине несколько экземпляров Redis, то убедитесь, что они будут работать на разных ядрах (мы не пробовали так делать, но в теории все должно работать). По сути, мастер-ноды обеспечивают обычное шардирование, и большее количество мастер-нод позволяет масштабировать запросы на запись и чтение.


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


Теперь поговорим об операциях, которые лучше бы уметь делать.


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


  • Первое и самое главное, что нам понадобится: операция cluster nodes. Она возвращает состояние кластера, показывает список нод, их роли, распределение слотов и т.п. Дополнительные сведения можно получить с помощью cluster info и cluster slots.
  • Хорошо бы уметь добавлять и удалять ноды. Для этого есть операции cluster meet и cluster forget. Обратите внимание, что cluster forget необходимо применить к КАЖДОЙ ноде, как к мастерам, так и к репликам. А cluster meet достаточно вызвать лишь на одной ноде. Такое различие может обескураживать, так что лучше узнать о нем до того, как запустили кластер в эксплуатацию. Добавление ноды безопасно выполняется в бою и никак не затрагивает работу кластера (что логично). Если же вы собираетесь удалить ноду из кластера, то следует убедиться, что на ней не осталось слотов (иначе вы рискуете потерять доступ ко всем ключам на этой ноде). Также не удаляйте мастер, у которого есть слейвы, иначе будет выполняться ненужное голосование за нового мастера. Если на нодах уже нет слотов, то это небольшая проблема, но зачем нам лишние выборе, если можно сначала удалить слейвы.
  • Если нужно насильно поменять местами мастер и слейв, то подойдет команда cluster failover. Вызывая её в бою, нужно понимать, что в течение выполнения операции мастер будет недоступен. Обычно переключение происходит менее, чем за секунду, но не атомарно. Можете рассчитывать, что часть запросов к мастеру в это время завершится с ошибкой.
  • Перед удалением ноды из кластера на ней не должно оставаться слотов. Перераспределять их лучше с помощью команды cluster reshard. Слоты будут перенесены с одного мастера, на другой. Вся операция может занимать несколько минут, это зависит от объема переносимых данных, однако процесс переноса безопасный и на работе кластера никак не сказывается. Таким образом, все данные можно перенести с одной ноды на другую прямо под нагрузкой, и не беспокоиться об их доступности. Однако есть и тонкости. Во-первых, перенос данных сопряжен с определенной нагрузкой на ноду получателя и отправителя. Если нода получателя уже сильно загружена по процессору, то не стоит нагружать её ещё и приемом новых данных. Во-вторых, как только на мастере-отправителе не останется ни одного слота, все его слейвы тут же перейдут к мастеру, на который эти слоты были перенесены. И проблема в том, что все эти слейвы разом захотят синхронизировать данные. И вам еще повезет, если это будет частичная, а не полная синхронизация. Учитывайте это, и сочетайте операции переноса слотов и отключения/переноса слейвов. Или же надейтесь, что у вас достаточный запас прочности.
  • Что делать, если при переносе вы обнаружили, что куда-то потеряли слоты? Надеюсь, эта проблема вас не коснется, но если что, есть операция cluster fix. Она худо-бедно раскидает слоты по нодам в случайном порядке. Рекомендую проверить её работу, предварительно удалив из кластера ноду с распределенными слотами. Поскольку данные в нераспределенных слотам и так недоступны, беспокоиться о проблемах с доступностью этих слотов уже поздно. В свою очередь на распределенные слоты операция не повлияет.
  • Еще одна полезная операция monitor. Она позволяет в реальном времени видеть весь список запросов, идущих на ноду. Более того, по ней можно сделать grep и узнать, есть ли нужный трафик.

Также стоит упомянуть о процедуре аварийного переключения мастера. Если коротко, то она есть, и, на мой взгляд, прекрасно работает. Однако не стоит думать, что если выдернуть шнур из розетки на машине с мастер-нодой, Redis тут же переключится и клиенты не заметят потери. На моей практике переключение происходит несколько секунд. В течение этого времени часть данных будет недоступна: обнаруживается недоступность мастера, ноды голосуют за нового, слейвы переключаются, данные синхронизируются. Лучший способ убедиться самостоятельно в том, что схема рабочая, это провести локальные учения. Поднимите кластер на своем ноутбуке, дайте минимальную нагрузку, сымитируйте падение (например, заблокировав порты), оцените скорость переключения. На мой взгляд, только поиграв таким образом день-два, можно быть уверенным в работе технологии. Ну, или понадеяться, что софт, которым пользуется половина интернета, наверняка работает.


Конфигурация


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


  • timeout 0
    Время, через которое закрываются неактивные соединения (в секундах). 0 не закрываются
    Не каждая наша библиотека умела корректно закрывать соединения. Отключив эту настройку, мы рискуем упереться в лимит по количеству клиентов. С другой стороны, если такая проблема есть, то автоматический разрыв потерянных соединений замаскирует её, и мы можем не заметить. Кроме того, не стоит включать эту настройку при использовании persist-соединений.
  • Save x y & appendonly yes
    Сохранение RDB-снепшота.
    Проблемы RDB/AOF мы подробно обсудим ниже.
  • stop-writes-on-bgsave-error no & slave-serve-stale-data yes
    Если включено, то при поломке RDB-снепшота мастер перестанет принимать запросы на изменение. Если соединение с мастером потеряно, то слейв может продолжать отвечать на запросы (yes). Или прекратит отвечать (no)
    Нас не устраивает ситуация, при которой Redis превращается в тыкву.
  • repl-ping-slave-period 5
    Через этот промежуток времени мы начнем беспокоиться о том, что мастер сломался и пора бы провести процедуру failovera.
    Придется вручную находить баланс между ложными срабатываниями и запуском failoverа. На нашей практике это 5 секунд.
  • repl-backlog-size 1024mb & epl-backlog-ttl 0
    Ровно столько данных мы можем хранить в буфере для отвалившейся реплики. Если буфер кончится, то придется полностью синхронизироваться.
    Практика подсказывает, что лучше поставить значение побольше. Причин, по которым реплика может начать отставать, предостаточно. Если она отстает, то, скорей всего, ваш мастер уже с трудом справляется, а полная синхронизация станет последней каплей.
  • maxclients 10000
    Максимальное количество единовременных клиентов.
    По нашему опыту, лучше поставить значение побольше. Redis прекрасно справляется с 10 тыс. соединений. Только убедитесь, что в системе достаточно сокетов.
  • maxmemory-policy volatile-ttl
    Правило, по которому удаляются ключи при достижения лимита доступной памяти.
    Тут важно не само правило, а понимание, как это будет происходить. Redis можно похвалить за умение штатно работать при достижении лимита памяти.

Проблемы RDB и AOF


Хотя сам Redis хранит всю информацию в оперативной памяти, также есть механизм сохранения данных на диск. А точнее, три механизма:


  • RDB-snapshot полный слепок всех данных. Устанавливается с помощью конфигурации SAVE X Y и читается как Сохранять полный снепшот всех данных каждые X секунд, если изменилось хотя бы Y ключей.
  • Append-only file список операций в порядке их выполнения. Добавляет новые пришедшие операции в файл каждые Х секунд или каждые Y операций.
  • RDB and AOF комбинация двух предыдущих.

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


Во-первых, для сохранения RDB-снепшота требуется вызывать FORK. Если данных много, это может повесить весь Redis на период от нескольких миллисекунд до секунды. Кроме того, системе требуется выделить память под такой снепшот, что приводит к необходимости держать на логической машине двойной запас по оперативной памяти: если для Redis выделено 8 Гб, то на виртуалке с ним должно быть доступно 16.


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


Эти два пункта уже заставляют задуматься о том, а так ли нам нужны эти данные на диске, если и так всё дублируется слейвами. Потерять данные можно только при выходе из строя всех слейвов, а это проблема уровня пожар в ДЦ. В качестве компромисса можно предложить сохранять данные только на слейвах, но в этом случае нужно убедиться, что эти слейвы никогда не станут мастером при аварийном восстановлении (для этого есть настройка приоритета слейвов в их конфиге). Для себя мы в каждом конкретном случае думаем над тем, надо ли сохранять данные на диск, и чаще всего отвечаем нет.


Заключение


В заключении буду надеяться, что смог дать общее представление о работе redis-cluster-а тем, кто совсем о нем не слышал, а также обратил внимание на какие-то неочевидные моменты для тех, кто уже давно им пользуется.
Спасибо за уделенное время и, как обычно, комментарии по теме приветствуются.

Подробнее..

Настало время офигительных историй. Кастомные транзишены в iOS. 22

07.04.2021 12:05:29 | Автор: admin

В прошлой статье мы реализовали анимацию ZoomIn/ZoomOut для открытия и закрытия экрана с историями.

В этот раз мы прокачаем StoryBaseViewController и реализуем кастомные анимации при переходе между историями.

Навигация между историями

Давайте сделаем анимацию для переходов между историями.

enum TransitionOperation {    case push, pop}public class StoryBaseViewController: UIViewController {        // MARK: - Constants    private enum Spec {        static let minVelocityToHide: CGFloat = 1500                enum CloseImage {            static let size: CGSize = CGSize(width: 40, height: 40)            static var original: CGPoint = CGPoint(x: 24, y: 50)        }    }        // MARK: - UI components    private lazy var closeButton: UIButton = {        let button = UIButton(type: .custom)        button.setImage(#imageLiteral(resourceName: "close"), for: .normal)        button.addTarget(self, action: #selector(closeButtonAction(sender:)), for: .touchUpInside)        button.frame = CGRect(origin: Spec.CloseImage.original, size: Spec.CloseImage.size)        return button    }()        // MARK: - Private properties    // 1    private lazy var percentDrivenInteractiveTransition: UIPercentDrivenInteractiveTransition? = nil    private lazy var operation: TransitionOperation? = nil        // MARK: - Lifecycle    public override func loadView() {        super.loadView()        setupUI()    }    }extension StoryBaseViewController {        private func setupUI() {        // 2        let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:)))        panGestureRecognizer.delegate = self        view.addGestureRecognizer(panGestureRecognizer)        view.addSubview(closeButton)    }        @objc    private func closeButtonAction(sender: UIButton!) {        dismiss(animated: true, completion: nil)    }    }// MARK: UIPanGestureRecognizerextension StoryBaseViewController: UIGestureRecognizerDelegate {        @objc    func handlePanGesture(_ panGesture: UIPanGestureRecognizer) {        handleHorizontalSwipe(panGesture: panGesture)    }        // 3    private func handleHorizontalSwipe(panGesture: UIPanGestureRecognizer) {                let velocity = panGesture.velocity(in: view)        // 4 Отвечает за прогресс свайпа по экрану, в диапазоне от 0 до 1        var percent: CGFloat {            switch operation {            case .push:                return abs(min(panGesture.translation(in: view).x, 0)) / view.frame.width                            case .pop:                return max(panGesture.translation(in: view).x, 0) / view.frame.width                            default:                return max(panGesture.translation(in: view).x, 0) / view.frame.width            }        }                // 5        switch panGesture.state {        case .began:            // 6            percentDrivenInteractiveTransition = UIPercentDrivenInteractiveTransition()            percentDrivenInteractiveTransition?.completionCurve = .easeOut                        navigationController?.delegate = self            if velocity.x > 0 {                operation = .pop                navigationController?.popViewController(animated: true)            } else {                operation = .push                                let nextVC = StoryBaseViewController()                nextVC.view.backgroundColor = UIColor.random                navigationController?.pushViewController(nextVC, animated: true)            }                    case .changed:            // 7            percentDrivenInteractiveTransition?.update(percent)                    case .ended:            // 8            if percent > 0.5 || velocity.x > Spec.minVelocityToHide {                percentDrivenInteractiveTransition?.finish()            } else {                percentDrivenInteractiveTransition?.cancel()            }            percentDrivenInteractiveTransition = nil            navigationController?.delegate = nil                    case .cancelled, .failed:            // 9            percentDrivenInteractiveTransition?.cancel()            percentDrivenInteractiveTransition = nil            navigationController?.delegate = nil                    default:            break        }    }    }
  1. Чтобы наша анимация была интерактивной и следовала за движением пальца, мы создаем объект percentDrivenInteractiveTransition. А operation отвечает за тип перехода (push или pop).

  2. Добавляем наш жест во view.

  3. Реализуем обработчик нажатия/свайпа.

  4. percent отвечает за прогресс свайпа по экрану в диапазоне от 0 до 1.

  5. В зависимости от состояния жеста конфигурируем наши свойства.

  6. Как только начинается новый жест, создаем свежий экземпляр UIPercentDrivenInteractiveTransition и сообщаем делегату navigationControllerа, что мы самостоятельно его реализуем (реализация будет ниже). Если направление свайпа положительное, то мы сохраняем в переменную operation значение.pop, и сообщаем navigationControllerу, что мы начали процесс перехода с анимацией .navigationController?.popViewController(animated: true). Аналогично делаем для.push-перехода.

  7. Когда наш свайп уже активен, мы передаем его прогресс в percentDrivenInteractiveTransition.

  8. Если мы просвайпили более половины экрана, или это было сделано с скоростью более 1500, то мы завершаем наш переход percentDrivenInteractiveTransition?.finish(). В противном случае отменяем переход. При этом необходимо очистить percentDrivenInteractiveTransition и navigationController?.delegate.

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

Сейчас при начале свайпа нужно сообщить navigationControllerу, что мы реализуем делегат navigationController?.delegate = self. Но мы этого так и не сделали. Самое время:

// MARK: UINavigationControllerDelegate    extension StoryBaseViewController: UINavigationControllerDelegate {        // 1    public func navigationController(        _ navigationController: UINavigationController,        animationControllerFor operation: UINavigationController.Operation,        from fromVC: UIViewController,        to toVC: UIViewController    ) -> UIViewControllerAnimatedTransitioning? {                switch operation {        case .push:            return StoryBaseAnimatedTransitioning(operation: .push)                    case .pop:            return StoryBaseAnimatedTransitioning(operation: .pop)                    default:            return nil        }    }        // 2    public func navigationController(        _ navigationController: UINavigationController,        interactionControllerFor animationController: UIViewControllerAnimatedTransitioning    ) -> UIViewControllerInteractiveTransitioning? {            return percentDrivenInteractiveTransition    }    }
  1. Этот метод возвращает аниматор для соответствующего перехода.

  2. Возвращаем объект типа UIPercentDrivenInteractiveTransition, который отвечает за прогресс интерактивного перехода.

Аниматор

Наконец-то реализуем аниматор, который непосредственно отвечает за поведение перехода.

Нам необходимы два метода делегата, отвечающие за продолжительность анимации и сам переход.

class StoryBaseAnimatedTransitioning: NSObject {        private enum Spec {        static let animationDuration: TimeInterval = 0.3        static let cornerRadius: CGFloat = 10        static let minimumScale = CGAffineTransform(scaleX: 0.85, y: 0.85)    }        private let operation: TransitionOperation        init(operation: TransitionOperation) {        self.operation = operation    }    }extension StoryBaseAnimatedTransitioning: UIViewControllerAnimatedTransitioning {        // http://fusionblender.net/swipe-transition-between-uiviewcontrollers/    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {                /// 1 Получаем view-контроллеры, которые будем анимировать.        guard            let fromViewController = transitionContext.viewController(forKey: .from),            let toViewController = transitionContext.viewController(forKey: .to)        else {            return        }                /// 2 Получаем доступ к представлению, на котором происходит анимация (которое участвует в переходе).        let containerView = transitionContext.containerView        containerView.backgroundColor = UIColor.clear                /// 3 Закругляем углы наших view при переходе.        fromViewController.view.layer.masksToBounds = true        fromViewController.view.layer.cornerRadius = Spec.cornerRadius        toViewController.view.layer.masksToBounds = true        toViewController.view.layer.cornerRadius = Spec.cornerRadius                /// 4 Отвечает за актуальную ширину containerView        // Swipe progress == width        let width = containerView.frame.width        /// 5 Начальное положение fromViewController.view (текущий видимый VC)        var offsetLeft = fromViewController.view.frame        /// 6 Устанавливаем начальные значения для fromViewController и toViewController        switch operation {        case .push:            offsetLeft.origin.x = 0            toViewController.view.frame.origin.x = width            toViewController.view.transform = .identity                    case .pop:            offsetLeft.origin.x = width            toViewController.view.frame.origin.x = 0            toViewController.view.transform = Spec.minimumScale        }                /// 7 Перемещаем toViewController.view над/под fromViewController.view, в зависимости от транзишена        switch operation {        case .push:            containerView.insertSubview(toViewController.view, aboveSubview: fromViewController.view)                    case .pop:            containerView.insertSubview(toViewController.view, belowSubview: fromViewController.view)        }                // Так как мы уже определили длительность анимации, то просто обращаемся к ней        let duration = self.transitionDuration(using: transitionContext)                UIView.animate(withDuration: duration, delay: 0, options: .curveEaseIn, animations: {                    /// 8. Выставляем финальное положение view-контроллеров для анимации и трансформируем их.            let moveViews = {                toViewController.view.frame = fromViewController.view.frame                fromViewController.view.frame = offsetLeft            }            switch self.operation {            case .push:                moveViews()                toViewController.view.transform = .identity                fromViewController.view.transform = Spec.minimumScale                            case .pop:                toViewController.view.transform = .identity                fromViewController.view.transform = .identity                moveViews()            }                    }, completion: { _ in                        ///9.  Убираем любые возможные трансформации и скругления            toViewController.view.transform = .identity            fromViewController.view.transform = .identity                        fromViewController.view.layer.masksToBounds = true            fromViewController.view.layer.cornerRadius = 0            toViewController.view.layer.masksToBounds = true            toViewController.view.layer.cornerRadius = 0                 /// 10. Если переход был отменен, то необходимо удалить всё то, что успели сделать. То есть необходимо удалить toViewController.view из контейнера.            if transitionContext.transitionWasCancelled {                toViewController.view.removeFromSuperview()            }                        containerView.backgroundColor = .clear            /// 11. Сообщаем transitionContext о состоянии операции            transitionContext.completeTransition(!transitionContext.transitionWasCancelled)        })            }        // 12. Время длительности анимации    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {        return Spec.animationDuration    }
  1. Получаем view-контроллеры, которые будем анимировать.

  2. Получаем доступ к представлению containerView, на котором происходит анимация (участвующее в переходе).

  3. Закругляем углы наших view при переходе.

  4. width отвечает при анимации за актуальную ширину containerView.

  5. offsetLeft начальное положение fromViewController.

  6. Конфигурируем начальное положение для экранов.

  7. Перемещаем toViewController.view над/под fromViewController.view, в зависимости от перехода.

  8. Выставляем финальное положение view-контроллеров для анимации и трансформируем их.

  9. Убираем любые возможные трансформации и скругления.

  10. Если переход был отменен, то необходимо удалить всё то, что успели сделать. То есть необходимо удалить toViewController.view из контейнера.

  11. Сообщаем transitionContext о состоянии перехода.

  12. Указываем длительность анимации.

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

Весь исходный код можете скачать тут. Буду рад вашим комментариям и замечаниям!

Подробнее..

Вас заметили! App Tracking Transparency (ATT) для iOS14.5

05.05.2021 12:08:12 | Автор: admin

Недавно вышла iOS 14.5, а чуть ранее Apple предупредила разработчиков, что начиная с этой версии ОС необходимо поддерживать фреймворк AppTrackingTransparency, который позволяет получить доступ к IDFA.

IDFA (The Device Identifier For Advertisers) это уникальный случайный идентификационный номер, который используется рекламными сетями для распознавания вашего устройства. Этот идентификатор позволяет подобрать для вас максимально точную рекламу. Также на его основе работают многие сервисы аналитики.

Если пользователь запретит доступ к своему идентификатору, тот будет состоять из нулей, что не позволит идентифицировать ваше устройство:

Zeroed out IDFA: 00000000-0000-0000-0000-000000000000

Помимо требования Apple поддерживать AppTrackingTransparency, модераторы с 26 апреля отклоняют все обновления приложений, которые не запрашивают доступ к идентификатору. Давайте посмотрим, как соблюдать новое требование.

Как быть дружелюбнее?

По предварительным результатам исследования AppsFlyer, не менее 40 % пользователей готовы делиться IDFA. Чтобы увеличить этот показатель, перед отображением запроса можно показать свой информационный экран, объясняющий, для чего это необходимо.

Руководство Apple: https://developer.apple.com/design/human-interface-guidelines/ios/app-architecture/accessing-user-data/

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

Нам в Ситимобил показалось, что нужно быть честнее и дружелюбнее. Необходимо объяснить пользователям, что давая согласие они сделают только лучше и нам, и себе; Win-win, так сказать. Поэтому мы сделали информационный экран, в котором только после согласия пользователя отображаем системное окно с запросом IDFA.

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

Техническая реализация

Реализация необходимой и достаточной поддержки AppTrackingTransparency не займет у вас много времени. Для начала вам необходимо в info.plist добавить параметр:

<key>NSUserTrackingUsageDescription</key><string>Можно ли использовать данные о вашей активности? Если вы разрешите, реклама Ситимобила на сайтах и в других приложениях будет более актуальной.</string>

Если ваше приложение поддерживает несколько языков, не забудьте про локализацию!

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

private func requestTrackingAuthorization() {    guard #available(iOS 14, *) else { return }    ATTrackingManager.requestTrackingAuthorization { _ in        DispatchQueue.main.async { [weak self] in            // self?.router.close() or nothing to do        }    }}

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

import AppTrackingTransparency

Запустите приложение и взгляните на получившийся запрос. Теперь ваше приложение готово для обновления.

Пограничные случаи

В примере выше мы запросили права, но не проконтролировали, дал ли пользователь свое согласие. Это можно проверить через AuthorizationStatus, возвращаемый методом requestTrackingAuthorization.

private func requestTrackingAuthorization() {    guard #available(iOS 14, *) else { return }    ATTrackingManager.requestTrackingAuthorization { status in        DispatchQueue.main.async {            switch status {            case .authorized:                let idfa = ASIdentifierManager.shared().advertisingIdentifier                print("Пользователь разрешил доступ. IDFA: ", idfa)            case .denied, .restricted:                print("Пользователь запретил доступ.")            case .notDetermined:                print("Пользователь ещё не получил запрос на авторизацию.")            @unknown default:                break            }        }    }}
  • Функция requestTrackingAuthorization(completionHandler:) отобразит запрос на права только когда у авторизации отслеживания будет статус .notDetermined. После установки статуса вызов функции просто запустит обработчик без отображения запроса.

  • Обратите внимание, что обработчик функции запускается не в основном потоке. Учтите это, если работаете с UI.

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

func goToAppSettings() {    guard let url = URL(string: UIApplication.openSettingsURLString),          UIApplication.shared.canOpenURL(url)    else {        return    }    UIApplication.shared.open(url, options: [:], completionHandler: nil)}

Теперь точно всё. Был ли у вас интересный опыт с внедрением ATT? Буду рад вашим комментариям и отзывам!

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru