Karhin’s Blog
Apps, Design and Music
Later ↑

Самая большая проблема интерфейсов современных мессенджеров

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

А ведь знаете, я и сам своего рода разработчик мессенджеров. Раньше мы с Тимуром делали Ghostly.

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

В обычной презентации – это двухколоночный интерфейс со списком диалогов и активным чатом, а в компактной – обычный стек навигации. Взгляните на Telegram, Signal, Viber, Twitter, Instagram или веб-версию Ghostly.

Всем знакомый двухколоночный интерфейс.

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

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

  • Нельзя увидеть последнее сообщение от собеседника без открытия вкладки.
  • Вкладок становится слишком много, ведь список контактов постепенно в них переползает, если их не закрывать.
Интерфейсы ICQ разных лет, которые я нашёл на реддите.

Если помните, то и в браузерах когда-то все страницы открывались в отдельных окнах, но потом произошла революция вкладок, потому что десятки окон – это жесть! А теперь пытаются решить проблему с сотнями вкладок. Ох уж этот компуктер саенс.

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

  • Сразу видно с кем ты общался последний раз.
  • Сразу видно последнее сообщение в чате, а ещё статус доставки и прочтения.
  • Нет сотен вкладок и нет сотен окон.

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

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

Зачем это нужно?

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

Что вы делаете?

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

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

Как это пытается решить двухколоночный интерфейс?

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

Как можно решить эту проблему?

А я не знаю! Но знаю компромиссы, которые могут упростить жизнь.

На айпадах и десктопах есть поддержка окон. Она ещё есть в андроиде нативно, но на мобиле это работает отвратительно. Можно просто добавить возможность открывать чат в новом окне, как это было в старом добром 2007. Такая функция есть в iMessage, но нет в Telegram и большинстве мессенджеров.

iMessage позволяет открыть чат в отдельном окне.

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

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

DALL-E: я снова верю в технологии

DALL-E – это новая система на основе искуственного интеллекта, которая умеет преобразовывать текст на естественном языке в изображения.

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

Увидел и завизжал, как свинья!

Когда начал регистрироваться, немного расстроился. Будьте готовы к тому, что сервис не принимает белорусские карты и белорусские телефонные номера. А ещё может не пустить к себе с белорусского IP адреса.

Картинки по запросу The photorealistic cat eats pepperoni pizza and drinks beer at sunset.

115 запросов стоят 15$. На один запрос генерируются 4 изображения.

Единственный недостаток в том, что очень легко увлечься и потратить несколько десятков запросов на вариациии “кот читает газету” или “кот ест пиццу” в исполнении разных художников, потому что интересно же! Со мной так и было!

Кстати, попробуйте угадать, для какой картинки, какой художник брался за референc.

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

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

Уже очень давно не удивлялся технологиям. Но с DALL-E снова готов поверить, что что-то новое и классное появляется в IT. А не какая-то НФТ херня веб3.0.

Кстати, можете попробовать бесплатный Craiyon (бывший DALL-E mini), если не хотите ждать приглашение от OpenAI. Правда, над ним всем смеются, потому что там получаются криповые изображения. Я вас предупреждал.

Правильная реализация UIActivityViewController в SwiftUI

В iOS 16 добавлен ShareLink, который полностью решает проблему. Если вы собираетесь поддерживать версии iOS до 16, то можете читать дальше.

С помощью Activity View Controller можно предоставить пользователю функции для взаимодействия с контентом в других приложениях: копирования, публикации в социальные сети, отправки через мессенджеры и многие другие функции.

К сожалению, Apple до сих пор не представили нормального способа создания UIActivityViewController в контексте SwiftUI. Как и не сделали ещё больше десятка мелких, но необходимых вещей для базового приложения.

Самым простым и логичным решением кажется оборачивание в UIViewControllerRepresentable, как в топовом ответе на StackOverflow, и представление через .sheet().

struct ActivityViewController: UIViewControllerRepresentable {
    var activityItems: [Any]
    var applicationActivities: [UIActivity]? = nil
    
    func makeUIViewController(context: UIViewControllerRepresentableContext<ActivityViewController>) -> UIActivityViewController {
        return UIActivityViewController(activityItems: activityItems, applicationActivities: applicationActivities)
    }
    
    func updateUIViewController(_ uiViewController: UIActivityViewController, context: UIViewControllerRepresentableContext<ActivityViewController>) {}
}

Всё это хорошо работает на iPhone. Однако у этой реализации есть один большой недостаток: Activity View будет занимать весь экран iPad, а в macOS меню будет торчать не там, где нужно.

Представление Activity View курильщика в iPadOS и macOS.

Основная причина некорректного поведения в том, что в UIKit приложении для представления UIActivityViewController требуется указывать sourceView и sourceRect, что явно указано в документации. Приложение на iPad или macOS просто упало бы без этих атрибутов, но в SwiftUI поведение изменили.

On iPad, you must present the view controller in a popover. On iPhone and iPod touch, you must present it modally.

Представление Activity View здорового человека в iPadOS и macOS.

Если вы используете SwiftUI из UIKit, то просто передайте координаты и размеры вашей кнопки в UIViewController. Имея координаты, можно установить необходимую позицию для UIActivityViewController. Получить координаты для View можно через .overlay() и GeometryReader. Не очень красиво, но работает.

protocol MyUIViewDelegate: AnyObject {
    func shareButtonFrameChanged(frame: CGRect)
}

.overlay(GeometryReader { proxy -> AnyView in
    delegate?.shareButtonFrameChanged(
        frame: proxy.frame(in: CoordinateSpace.global)
    )
    return AnyView(Rectangle().fill(.clear))
})

Что делать, если вы хотите писать только на SwiftUI? Я попытался воссоздать нормальное поведение через popover. В iPadOS меню начало отображаться ровно по середине кнопки, но откорректировать его положение не получилось. В macOS поведение вообще не изменилось.

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

extension UIApplication {
    
    var keyWindow: UIWindow? {
        return UIApplication.shared.connectedScenes
            .filter { $0.activationState == .foregroundActive }
            .first(where: { $0 is UIWindowScene })
            .flatMap({ $0 as? UIWindowScene })?.windows
            .first(where: \.isKeyWindow)
    }
    
    var keyWindowPresentedController: UIViewController? {
        var viewController = self.keyWindow?.rootViewController
        if let presentedController = viewController as? UITabBarController {
            viewController = presentedController.selectedViewController
        }
        while let presentedController = viewController?.presentedViewController {
            if let presentedController = presentedController as? UITabBarController {
                viewController = presentedController.selectedViewController
            } else {
                viewController = presentedController
            }
        }
        return viewController
    }
    
}

Кнопку шаринга нужно разместить в Geometry Reader, чтобы получить её координаты. .overlay() уже не подойдёт, так как он будет изменять состояние View, но и код станет проще.

GeometryReader { proxy in
    Button(action: {
        let rect = proxy.frame(in: CoordinateSpace.global)
        let activityViewController = UIActivityViewController(activityItems: [URL(string: "https://karh.in/")!], applicationActivities: nil)
        if let vc = UIApplication.shared.keyWindowPresentedController {
            activityViewController.popoverPresentationController?.sourceView = vc.view
            activityViewController.popoverPresentationController?.sourceRect = rect
            vc.present(activityViewController, animated: true, completion: nil)
        }
    }, label: {
        Image(systemName: "square.and.arrow.up")
            .font(.system(size: 32))
    }).buttonStyle(.bordered)
}.frame(width: 48, height: 48)

Плохая новость: это может перестать работать в следующих версиях 🤡. Хорошая новость в том, что теперь Activity View будет появляться в нужном месте, а если даже поведение окон поменяют, то приложение не упадёт, а просто не отобразит меню.

Представление Activity View в SwiftUI здорового человека.

Полезные ссылки по теме:

Если вдруг есть какой-то более красивый способ правильно отобразить UIActivityViewController на всех платформах, то обязательно поделитесь в комментариях. Надеюсь, что на WWDC 2022 всё таки представят нормально решение.

Запускаем Chrome OS Flex

Google запустил открытое тестирование операционной системы Chrome OS Flex. Обещают, что будет работать даже на очень старом ПК или маке. Я не поверил и решил проверить: рассказываю про впечатления, как установить и небольшую проблему, с которой столкнулся. Если кратко про впечатления, то мне очень понравилась.

Минимальные системные требования:

  • 64-разрядный процессор Intel или AMD;
  • 4 гигабайта ОЗУ;
  • 16 гигабайтов постоянной памяти.

Установка

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

Для установки нужна флешка минимум 8 гигабайтов, установленный гугл хром с расширением Chromebook Recovery Utility. Расширение работает только на маке и виндоус, на линуксе записать не получилось.

Втыкаем флешку, запускаем расширение, выбираем производителя Google Chrome OS Flex, продукт Chrome OS Flex (Developer-Unstable), ждём несколько десятков минут загрузку и запись операционной системы.

Не без приколов, конечно.

Перезагружаем компьютер, загружаемся с USB. Я запускал операционную систему на старом Lenovo Y510P и у меня возник прикол: чёрный экран после загрузочного с логотипом Google Chrome. Система издавала какие-то звуки, но на экране пустота: сделал верный вывод, что проблема с GPU. Уже думал, что пора бросить это дело, но вспомнил, что в моём старике целых три видеокарты. Одну видеокарту можно достать, что я и сделал.

Вот это технологии делали в 2013 году.

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

Теперь это не виндоноут, а миленький хромбук.

Решил, что нужно продолжить тестирование и нашёл ноутбук динозавра с ещё более впечатляющими характеристиками – HP 450 G2. У этого компьютера как раз минимальные требования для этой операционной системы. Думаю, что вы представляете, как он работает на последней виндоус (никак).

Как тебе такое, Сатья Наделла?

Впечатления

Первые впечатления обманчивые, но мне понравилось. Камера, микрофон, Wi-Fi, Bluetooth и другая периферия заработали прямо из коробки. На обычном линуксе пришлось бы еще два часа возиться и искать нужные пакеты.

Нарядный лаунчпад, как в макос.

Самое главное – она реально быстрая. Даже на обычном жестком диске, на старом процессоре и с 4 или 8 гигабайтами оперативной памяти. После виндоус, где приложения могут запускаться по 30 секунд или минуте на такой конфигурации, как глоток свежего воздуха.

Все приложения в Chrome OS – это веб-приложения, поэтому у меня возник логичный вопрос, как кодить. Оказалось, что всё проще простого и можно запускать линукс приложения через Crostini. Я не попробовал запустить Android Studio или VS Code, потому что под рукой только стик на 16 гигабайт, но кажется, что с этим не будет проблем.

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

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

А зачем это всё, когда есть убунту/дебиан/аниме? Давно хотел написать про то, почему я не использую линукс на основном компьютере, но люблю его на серверах и в эмбеддед. За компьютером хочется работать, а не заставлять его работать. К сожалению, в большинстве дистрибутивов наоборот и, скорее всего, так и будет. Гугл снова выпустил дистрибутив лучше бубунты.

Закрытый показ Pepperoni Engine

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

Весь движок разработан на языке TypeScript. Сегодня я запускаю его в бету и опубликовал весь исходный код в приватный репозиторий. Если вы не боитесь JavaScript, Docker, Postgres и Redis, то его можно попробовать прямо сейчас. Если боитесь, то постараюсь помочь разобраться; Тимур сегодня впервые установил Docker и запустил движок локально за 10 минут.

Почему мой движок клёвый?

  • Он оптимизирован для SEO. Есть поддержка Sitemap, Yandex.Turbo, OpenGraph, JSON-LD и некоторых других технологий.
  • Свой собственный язык для разметки статей, похожий на Markdown. Благодаря собственному языку и его парсеру, можно приводить статьи к любому формату, даже для нативного отображения в мобильных приложениях.
  • Система древовидных комментариев с возможностью анонимного комментирования.
  • Авторизация через Телеграм, мгновенные оповещения для пользователей и администраторов сайта через него же.
  • Черновики, статические страницы, категории, теги, счётчики просмотров, рекомендации и всё остальное, как у всех.
  • Стандартная тема поддерживает ночной и дневной режимы, адаптивная, набирает много попугаев в Google Lighthouse.
  • Если меня пнуть и убедить, сделаю нужную фичу. Ну либо вы сами её сделаете.

Для того, чтобы получить доступ к исходному коду, добавляйтесь в группу в Телеграме. После я отправлю вам приглашение в GitLab.

Конфигурация движка ещё немного гиковая (через JSON), но надеюсь, что скоро решим эту проблему. Уже не гиковая. Когда причешем весь код, скорее всего, опубликуем его публично.

Earlier ↓