iOS Broadcast


Kanal geosi va tili: Rossiya, Ruscha


Подборка новостей и статей для iOS разработчиков.
Новости Kotlin и мультиплатформы @kotlin_broadcast
Новости Android @android_broadcast
Реклама и прочее @ab_manager

Связанные каналы  |  Похожие каналы

Kanal geosi va tili
Rossiya, Ruscha
Statistika
Postlar filtri


📱 Как тестировать навигацию в SwiftUI
Навигацию обычно тестируют примерно никак. Еще 3 года назад я рассказывал как можно в UIKit описывать навигацию декларативно и тестировать ее (Декларативная навигация в iOS-приложении). В SwiftUI тенденция тестировать навигацию, к моему удивлению, до сих не стала повсеместной. Хотя почти у всех есть NavigationStack, несколько маршрутов, deep links, sheet, ошибки и авторизация, и если подумать - навигация уже является частью бизнес-логики. В статье Azam Sharp интересный подход: не тестировать сам NavigationStack. Вместо этого вынести решение по навигации в отдельную логику. Например, вместо:
if isLoggedIn {
navigationPath.append(.home)
} else {
navigationPath.append(.login)
}
сделать так, чтобы ViewModel или router решал:
destination = .home
или:
destination = .login
А SwiftUI уже занимается отображением этого состояния. Тогда тесту вообще не нужен UI. Можно проверить:
🟢авторизованный пользователь → .home
🟢неавторизованный → .login
🟢после сохранения → .details
🟢ошибка → .error
🟢deep link → правильный маршрут

Основная идея - сделать навигацию обычным состоянием приложения. Например:
enum Route: Hashable {
case home
case profile
case settings
}
А NavigationStack просто отображает текущий маршрут. Это особенно хорошо работает с NavigationPath, потому что сам path можно рассматривать как данные. Изменился path → изменился navigation state → SwiftUI обновил UI.

после такого сдвига парадигмы тестировать становится намного проще. При этом не обязательно сразу строить огромный Router. Для небольшого приложения вполне может хватить @State или @Observable-модели. После того как Navigation становится обычными данными, тестировать становится намного проще. Ну а если вам идея понравится, советую пойти дальше и посмотреть мой доклад, фреймворк отлично подходит и для SwiftUI


🐥 Data Detector в iOS 26: ссылки прямо из обычного текста
В iOS 26 Apple добавила новый фреймворк - DataDetector, который умеет находить в тексте полезные сущности: ссылки, телефоны, адреса, даты и другие данные. Раньше для такого сценария часто приходилось самостоятельно разбирать строку, искать regex'ами URL и телефоны, а потом превращать найденные куски в ссылки. Совсем олдскульные разработчики вспомнят про NSDataDetector, но его далеко не так просто адаптировать для прямой работы со строкой в Swift.
DataDetector умеет работать не только с обычным String, но и с AttributedString, добавляя найденным данным соответствующие атрибуты.
То есть текст:
Позвони мне завтра в 15:00 или посмотри https://t.me/ios_broadcast

может автоматически превратиться в интерактивный текст с распознанными сущностями.
Особенно полезно для:
🟢чатов
🟢заметок
🟢email
🟢документов
🟢preview пользовательского текста

Это системный механизм, который знает гораздо больше о том, что именно находится в тексте. Основной плюс системного подхода: Data Detector учитывает локаль и системные форматы. Это сильно поможет при международном распространении приложения. Телефон, дата или адрес могут выглядеть совершенно по-разному в разных странах, и поддерживать все эти варианты самостоятельно довольно быстро становится неприятно.
Но DataDetector не превращает любой Text автоматически в интерактивный. Это именно инструмент для анализа текста и добавления metadata. А уже UI-слой решает, как эти данные отображать и что делать по нажатию.

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


📱 ContentBuilder - секрет ускорения проверки типов в SwiftUI
На сессии WWDC 2026 инженеры Apple представили новую функцию SwiftUI: ContentBuilder. Он представляет собой не что иное, как ViewBuilder с более широким охватом - API, которые раньше принимали ViewBuilder, ToolbarContentBuilder, или CommandsBuilder по отдельности, теперь это один конструктор.
Apple также утверждает, что это изменение значительно повышает производительность проверки типов. Давайте рассмотрим что же такое ContentBuilder и за счет чего достигается такая производительность.

Для начала давайте вспомним как работает @ViewBuilder в SwiftUI:
@ViewBuilder
var content: some View {
Text("Hello")
Image(systemName: "star")
}
И SwiftUI как будто просто понимает, что делать с несколькими View, но никакой магии тут нет. @ViewBuilder это result builder, то есть специальный механизм Swift, который превращает декларативный код в обычные вызовы методов builder'а. То есть когда мы пишем:
if isLoading {
ProgressView()
} else {
ContentView()
}
SwiftUI не исполняет это как обычный if, а возвращающий разные типы. ViewBuilder строит из этого единую структуру, которую затем SwiftUI использует для своего view tree. За счет этого body может содержать несколько View, хотя обычная функция не может просто так вернуть несколько разные типы. ViewBuilder не является чем-то исключительно SwiftUI-ным, это обычный result builder из Swift. Поэтому похожий подход можно использовать и для собственных DSL.


За счет чего же достигается ускорение проверки типов?

Помимо typealias указания на ViewBuilder, реальным фактором повышения производительности стало изменение способа объявления API. Выигрыш обусловлен тем, что SwiftUI переработал инициализаторы общих компонентов и выходные типы builder-а.
public static func buildBlock(
_ content: Content
) -> Content

@available(iOS 27.0, macOS 27.0, *)
public static func buildBlock(
_ content: repeat each Content
) -> TupleContent

public static func buildIf(
_ content: Content?
) -> Content?

public static func buildEither(
first: TrueContent
) -> _ConditionalContent
Обратите внимание: ни одна из этих сигнатур не содержит Content: ViewContent: ToolbarContent или Content: Commands ограничений. Задача builder-а стала чисто структурной. Недавно представленный TupleContent является ключевым элементом дизайна. Его отличительная особенность заключается в том, что он принимает различные формы в зависимости от того, что могут делать его элементы.

Другими словами, TupleContent изначально не принадлежит ни к какому домену. Это View только в том случае, если каждый элемент, который он содержит, является View. когда все его элементы являются ToolbarContent, он является содержимым панели инструментов. Optional, _ConditionalContent, Group и ForEach используют один и тот же подход.
Таким образом, разработчик может сначала создать единую конкретную структуру:
TupleContent
Только когда сигнатура функции требует some View компилятор возвращается к проверке того, что и Text и Image соответствуют View.

Для Group новый интерфейс можно описать так:
public struct Group { ... }

extension Group {
public init(@ContentBuilder content: () -> Content)
}

extension Group: View where Content: View {}
extension Group: ToolbarContent where Content: ToolbarContent {}
extension Group: Commands where Content: Commands {}
Для доменов ContentBuilder унифицированы - View, ToolbarContent, Commands и т. д. — Group { ... } в обычном клиентском коде теперь предпочтительнее использовать эту универсальную точку входа в сборку без ограничений по доменам. Apple решила исправить междоменные вложенные общие компоненты, такие как Group, ForEach, и Section. Сократилось время проверки типов выражений, содержащих эти деревья перегрузок.


📱 Как заставить SwiftUI делать красивые
переносы

Обычно статьи про текст рассказывают то как быстро и точно рассчитать размер контейнера, но тут более редкая проблема - не красивые переносы слов. Случай, когда текст красиво выглядит почти на всех размерах экрана, а потом на каком-нибудь iPhone последняя строка внезапно состоит из одного-двух слов. Понятная логика, перенести это слово на предыдущую строку не понятно как описать без костылей вставки переносов в исходный текст, ведь стандартный Text не даёт нормального контроля над такими переносами.
В статье интересный подход: использовать AttributedString и управлять правилами переноса через paragraph style. В частности, можно задать lineBreakStrategy и использовать .pushOut, чтобы SwiftUI старался не оставлять слишком короткую последнюю строку.
Получается довольно приятная штука:
🟢не нужно вручную вставлять \n
🟢не нужно делать разные варианты текста для разных устройств
🟢перенос остаётся адаптивным
И это хороший пример того, как иногда проблема SwiftUI находится не в самом Text, а уровнем ниже, в типографике и AttributedString. При этом есть важный нюанс: Не стоит пытаться запретить любые короткие строки. Иногда перенос действительно является правильным, а попытка любой ценой выровнять текст только ухудшит результат.
Статья меня зацепила даже не самой идеей решения, а концепцией: типографику тоже можно сделать частью layout-поведения. И если текст выглядит странно только на некоторых размерах экрана, возможно, не нужно добавлять ещё один frame(). Сначала стоит посмотреть, какие правила переноса вообще используются.


🐥 @FocusState в SwiftUI
Давайте базовую тему - фокус в SwiftUI.  @FocusState позволяет хранить состояние фокуса прямо в SwiftUI и управлять им из кода. Например, можно описать поля формы: 🟡имя🟡email🟡пароль. После чего можно переключать фокус между ними после ввода. Это можно сделать через enum:
enum Field { case name, email, password }
А затем привязать его к focused(...)

В итоге логика становится довольно читаемой:
🟢открыть экран и сразу поставить фокус на поле
🟢нажать далее и перейти к следующему
🟢после ошибки вернуть пользователя к нужному полю
🟢скрыть клавиатуру, сбросив focus в nil

@FocusState лучше воспринимать именно как состояние UI, а не как часть бизнес-логики. Не нужно тащить информацию о том, какое поле сейчас активно, в модель или сервис. Фокус относится к интерфейсу. Но не стоит использовать несколько независимых @FocusState, когда все поля относятся к одной форме. Обычно один enum для всей формы получается проще и предсказуемее.

Магия @FocusState лежит не только в упрощении работы с формами, это подход по делегированию поведения системе. Это особенно актуально, если интерфейс по-настоящему кросплатформенный и работает не только на iOS/iPad, но и на mac или даже Apple TV. Вместо becomeFirstResponder, ссылок на UIKit и ручного управления клавиатурой достаточно описать: какое поле сейчас должно быть в фокусе и дальше SwiftUI сам занимается остальным.


🆓 Типобезопасные JSON в StructuredQueries
Оч интересное обновление вышло у библиотеки StructuredQueries. Сама библиотека разработана Point-Free предоставляет набор инструментов, которые позволяют вам писать типобезопасные, выразительные и компонуемые SQL-выражения на языке Swift. Но данное обновление расширяет DSL на JSON и JSONB ( это тип данных, который хранит JSON-документы в оптимизированном бинарном формате).

Элегантный DSL для указания типа для сериализации в таблице:
@Table struct Trip: Identifiable {
let id: UUID
var name = ""
@Column(as: Location.JSONRepresentation.self)
var location: Location
}
Вот так будет выглядеть запрос местоположения поездки, хранящейся в столбце JSON:
@Selection struct Location: Codable {
var latitude = 0.0
var longitude = 0.0
}
При применении макроса @Selection к Location структура типа становится видимой для builder-а запросов, и дает возможность перемещаться по JSON с помощью метода jsonExtract и привычного пути к ключу в Swift:
Trip.where {
$0.location
.jsonExtract(\.longitude) < 0
}
// Превращается в SQL
SELECT … FROM "trips"
WHERE json_extract(
"trips"."location",
'$."longitude"') < 0

Это означает, что вы получаете безопасность схемы для столбцов вашей таблицы, для полей внутри ваших JSON-данных и для извлекаемых значений. И эти выражения можно использовать в любом месте запроса: where, select, order в полях и т. д.
Вы также можете обновлять данные в формате JSON непосредственно в базе данных, не загружая их в память, используя jsonSet, jsonInsert, jsonAppend, jsonRemove и jsonReplace:
Profile.update {
$0.author = $0.author
.jsonSet(\.name, "Blob")
}
// превращается в
UPDATE "profiles"
SET "author" = json_set(
"profiles"."author",
'$."name"', 'Blob'
)

Profile.update {
$0.tags = $0.tags
.jsonAppend("new")
}
// превращается в
UPDATE "profiles"
SET "tags" = json_insert(
"profiles"."tags",
'$[#]', 'new'
)
Не сказал бы что работа с SQLite напрямую часто требуется, но во-первых это красиво 😀 . А Во-вторых можно взять на вооружение такой подход для создания своих DSL для работы с JSON

😺️ MR со всеми доработками и тестами


🐥 Почему @MainActor и протоколы в Swift 6 могут поссориться
В теории всё просто, есть протокол:
protocol ImageLoader { ... }

Реализация, которая работает с UI:
@MainActor final class ImageLoaderImpl: ImageLoader { ... }

Но Swift 6 начинает задавать неприятный вопрос: а сам протокол изолирован от Main Actor или нет?
И вот тут начинается самое интересное. @MainActor на конкретной реализации не означает автоматически, что любой код, который работает с протоколом, тоже находится на Main Actor.
Для компилятора протокол может использоваться совершенно независимо от конкретной реализации.
В результате вполне невинный код начинает упираться в ошибки concurrency:
🟡Вызов main-actor метода из nonisolated контекста
🟡Несоответствие требований протокола и actor isolation
🟡Необходимость добавлять @MainActor туда, где раньше его вообще не было
Особенно часто это всплывает при dependency injection.
Например, ViewModel хранит зависимость как:
let loader: ImageLoader
а конкретно переданный ImageLoaderImpl является @MainActor.
Swift должен гарантировать, что контракт ImageLoader сам по себе безопасен. И здесь есть несколько вариантов:
➡️ Можно сделать весь протокол @MainActor
➡️ Можно изолировать только отдельные методы
➡️ Можно использовать nonisolated, если конкретная операция действительно не зависит от Main Actor
И вот последний вариант особенно важно не использовать просто для того, чтобы "заткнуть компилятор". Если метод реально работает с состоянием, которое должно жить на Main Actor, nonisolated проблему не решает. Он просто заставляет вас взять ответственность за безопасность на себя.

Попробую подытожить:
@MainActor — это не просто атрибут класса. Это часть контракта, и при работе через протокол этот контракт должен быть виден там, где он действительно нужен. Поэтому при проектировании Swift 6 API стоит заранее решить:
🟢Весь сервис main-actor isolated?
🟢Только часть его методов?
🟢Протокол вообще должен знать про actor isolation?
🟢Можно ли использовать dependency без Main Actor?
Это особенно важно для архитектуры с ViewModel + protocols + dependency injection. Это со SwiftUI как раз очень распространено. Раньше можно было сказать:
«Ну эта реализация всё равно работает на Main Actor».

Swift 6 отвечает:
«Мне всё равно. Я проверяю контракт».

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


🎯 Новый уровень работы с фото и видео
Рассматриваем новый фреймворк — Media Intelligence. Если совсем коротко, это API, которое позволяет приложению понимать содержимое фото и видео, а не просто отображать их. Например, определить:
🟣что происходит в кадре
🟣какие объекты есть на изображении
🟣какие сцены встречаются в видео
🟣где начинается нужный момент
🟣какие фрагменты похожи друг на друга

Apple постепенно начинает повышать уровень абстракции при работе с AI и делиться внутренними моделями. Появляется абстракция, благодаря которой Apple сможет подменять реализацию. Гораздо важнее становится какую задачу нужно решить. Раньше для поиска нужного момента в видео приходилось строить собственный пайплайн, теперь система сама может помочь понять, что находится в медиаконтенте. Новые API работают локально, без необходимости встраивать огромные модели в приложение. Очень похоже что все то что уже обкатали в команде Photos становится доступно всем, как только API удалось стабилизировать.

Полезные ссылки:
iOS 27: Media Intelligence Framework
Media Intelligence Framework Documentation


🐥 Swift 6 стал заметно дружелюбнее к Concurrency
Продолжаем эксперименты с многопоточностью в Swift 6, в этой области мы точно движемся в правильном направлении. Одна из главных претензий к Swift 6 звучала примерно так: Я просто включил Swift 6, а компилятор решил переписать половину моего проекта. Sendable, @MainActor, изоляции, десятки новых предупреждений... И вроде всё правильно, но начать миграцию было больно. Именно поэтому Apple представила Approachable Concurrency. Идея очень простая. Вместо того чтобы сразу требовать идеальный concurrency-код, компилятор позволяет включать проверки постепенно.
Мы получили преимущества новой модели, но без ощущения, что проект нужно переписать за один раз.

Многие типы из UIKit и SwiftUI уже аннотированы так, что с ними стало проще работать, часть проверок теперь включается только тогда, когда они действительно приносят пользу. Но Approachable Concurrency — это не способ избежать миграции, а способ сделать её управляемой. Если в коде есть потенциальные race condition или нарушение изоляции акторов, рано или поздно их всё равно придётся исправить.

Радует что в этой области Apple приняли тот факт, что iOS-приложения не всегда пишутся с нуля и нужно заботиться и о разработчиках которые поддерживают свои приложения годами.

Гайды от Apple по миграции:
Embracing Swift Concurrency
Enabling Complete Concurrency Checking

9 ta oxirgi post ko‘rsatilgan.