Всем привет! Месяц назад Apple провела митап с глубоким разбором производительности SwiftUI. Сегодня обсудим самое важное, что вы могли пропустить.
Суть проблемы: SwiftUI не «тормозит» сам по себе. Проблемы начинаются, когда логика обновлений UI выходит из-под контроля. Каждая лишняя перерисовка View - это работа для системы, которая копится и превращается в лаги.
Что важно знать:
@StateObject для разделяемых данных. Класс с @Observable дает точный контроль: вью обновится только если она читает конкретное измененное свойство. Это резко снижает количество нежелательных перерисовок.@State или запись в Environment в таких местах запускает каскад проверок и перерисовок по всему дереву вьюх. Вместо этого выносите часто меняющиеся данные в отдельные, минимальные по объему вьюхи или передавайте их через Observable‑классы - это ограничит зону обновления только теми компонентами, которые действительно зависят от этих данных.Для достижения плавного интерфейса недостаточно проектировать только внешний вид, необходимо сознательно проектировать поток обновлений данных. Ключевая задача - сделать так, чтобы каждое изменение состояния затрагивало минимально необходимый набор вьюх, а не вызывало каскадную перерисовку.
Как проверить: используйте Instruments с шаблоном SwiftUI. Ваш главный ориентир не общее время выполнения, а столбец Updates. Если видите, что какая-либо вьюха обновляется десятки раз без визуальных изменений - вы нашли точку для немедленной оптимизации.
Мобильный трудоголик
Please open Telegram to view this post
VIEW IN TELEGRAM
2👍18❤9 4 2🙏1
Привет! Swift Concurrency предоставляет мощные инструменты для работы с асинхронными операциями, но понимание времени жизни задач - ключевой аспект, который часто упускают из виду. Давайте разберемся, как разные типы задач управляют своим жизненным циклом.
Structured Concurrency: автоматическое управление:
Structured Concurrency - это фундаментальный принцип, который связывает время жизни задачи с областью ее создания. Когда область видимости завершается, система автоматически отменяет все связанные с ней задачи.
Примеры structured задач:
struct ProfileView: View {
@State private var user: User?
var body: some View {
VStack {
// Задача автоматически отменится при исчезновении View
.task {
user = await loadUserData()
}
}
}
}Преимущество structured подхода - предсказуемость. Вы можете быть уверены, что при выходе из области видимости все дочерние задачи будут корректно отменены.
Unstructured задачи: ручное управление:
Когда вы создаете задачу через Task {} вне structured контекста, вы берете на себя ответственность за ее жизненный цикл. Такие задачи выполняются независимо и требуют явной отмены.
class DataLoader {
private var loadingTask: Task<User, Error>?
func loadUser() {
loadingTask = Task {
try await fetchUser()
}
}
func cancelLoading() {
loadingTask?.cancel()
}
}Важно понимать: вызов cancel() не останавливает задачу мгновенно. Он лишь устанавливает флаг isCancelled, который ваша асинхронная логика должна проверять.
Detached задачи: полная независимость:
Task.detached создает полностью изолированную задачу, которая не наследует контекст родителя, ни приоритета, ни актора, ни состояния отмены. Используйте их осторожно, только когда действительно нужна полная независимость от контекста вызова.
Работа с долгоживущими операциями:
Особого внимания требуют задачи, которые могут выполняться бесконечно долго, например, обработка AsyncStream:
class NotificationService {
private var listenerTask: Task<Void, Never>?
func startListening() {
listenerTask = Task {
for await notification in await notificationStream() {
process(notification)
}
}
}
func stopListening() {
listenerTask?.cancel()
}
}Когда вы отменяете задачу listenerTask, это останавливает только получение уведомлений, но не их генерацию. Источник продолжает создавать уведомления, даже если их никто не обрабатывает.
Чтобы полностью остановить поток данных, нужно управлять обеими сторонами:
Проверка отмены:
Системные API типа URLSession или Task.sleep автоматически проверяют отмену, но в своем коде вам нужно делать это явно:
func processLargeDataset() async throws {
for item in dataset {
try Task.checkCancellation() // Вызывает CancellationError
// или
if Task.isCancelled { return }
await process(item)
}
}Правильное управление временем жизни задач - это фундамент стабильных и эффективных асинхронных приложений. Рекомендуется использовать Structured Concurrency по умолчанию для максимальной предсказуемости поведения. Unstructured задачи стоит применять в тех случаях, когда вам нужен полный контроль над временем жизни операции. Detached задачи подходят только для полностью независимой работы, не связанной с контекстом вызова.
Мобильный трудоголик
Please open Telegram to view this post
VIEW IN TELEGRAM
Swift продолжает расширять границы применения. После успешного освоения серверной разработки, Linux и Windows, язык делает стратегический шаг в embedded-разработку. Теперь на Swift можно писать не только приложения для iPhone, но и программы для микроконтроллеров, датчиков и специализированных устройств.
Суть Embedded Swift:
Embedded Swift - это оптимизированная версия языка, созданная для работы в условиях крайне ограниченных ресурсов. Если обычный Swift полагается на операционную систему и runtime, то Embedded Swift работает напрямую с железом, где каждый байт памяти имеет значение.
Основные отличия от стандартного Swift:
Пример:
import EmbeddedHal
// Настройка пинов как в embedded-проектах
let ledPin = DigitalOutputPin(pin: 13)
let sensorPin = AnalogInputPin(pin: 0)
@main
struct DeviceController {
static func main() async {
while true {
// Чтение данных с датчика
let sensorValue = sensorPin.read()
// Логика управления на основе показаний
if sensorValue > threshold {
ledPin.write(true)
await Task.sleep(milliseconds: 500)
ledPin.write(false)
}
await Task.sleep(seconds: 1)
}
}
}
Области применения Embedded Swift:
Новые языковые возможности для embedded-разработки:
// Прямая работа с разделами памяти
@section(".__DATA, .sensor_data")
@used
var sensorReadings: [Float] = []
// Упрощённая интеграция с C-кодом
@c(readSensorData)
func readSensorData() -> Float {
// Взаимодействие с аппаратным датчиком
return readFromHardware()
}
Сравнение с традиционными подходами:
Язык C/C++ предлагает максимальный контроль и минимальный размер кода, но требует глубокого знания низкоуровневых деталей. Rust обеспечивает безопасность памяти на уровне компилятора, но имеет крутую кривую обучения. Embedded Swift занимает промежуточную позицию - предоставляет более высокоуровневые абстракции, сохраняя приемлемую производительность и размер бинарного файла.
Значение для iOS-разработчиков:
@c делает взаимодействие безопаснее и предсказуемее.Embedded Swift представляет собой не просто расширение функциональности языка, а развитие Swift как универсальной платформы для разработки. От мобильных приложений до серверных решений, от настольных программ до микроконтроллерных систем - один язык начинает охватывать все больше областей применения.
Для разработчиков iOS это открывает возможность войти в мир embedded-разработки, используя уже знакомые инструменты и паттерны. Можно создавать программы для физических устройств, не начиная с изучения совершенно новых языков программирования.
Мобильный трудоголик
Please open Telegram to view this post
VIEW IN TELEGRAM
Всем привет! С выходом iOS 26 работа с уведомлениями в Swift Concurrency кардинально меняется. Старый добрый NotificationCenter, который годами служил верой и правдой, теперь выглядит как отголосок из прошлого - он не понимает модель акторов и постоянно генерирует предупреждения компилятора, но у Apple готово элегантное решение: протоколы MainActorMessage и AsyncMessage. Давайте разберемся, как они работают и почему это настоящий прорыв.
Проблемы старого подхода:
Традиционный NotificationCenter сталкивается с двумя фундаментальными проблемами в Swift Concurrency:
@MainActorMainActorMessage: гарантия выполнения на главном потоке:
Новый протокол решает первую проблему кардинально - он явно указывает, что уведомление должно обрабатываться на главном акторе. Посмотрим на разницу:
// Старый подход
NotificationCenter.default.addObserver(
forName: UIApplication.didBecomeActiveNotification,
object: nil,
queue: .main,
using: { [weak self] _ in
self?.updateUI() // Warning: Call to main actor-isolated method
}
)
// Новый подход
private var observationToken: NotificationCenter.ObservationToken?
// Подписываемся и сохраняем token
observationToken = NotificationCenter.default.addObserver(
of: UIApplication.self,
for: .didBecomeActive
) { [weak self] message in
self?.updateUI() // Без предупреждений
}
// Отмена подписки
deinit {
observationToken?.cancel()
}
AsyncMessage: гибкость для фоновых задач:
Для сценариев, где не требуется главный поток, идеально подходит AsyncMessage. Он доставляет уведомления асинхронно, сохраняя при этом строгую типизацию:
struct DataSyncMessage: NotificationCenter.AsyncMessage {
typealias Subject = SyncResult
let result: Subject
}
// Отправка из любого контекста
let message = DataSyncMessage(result: syncResult)
NotificationCenter.default.post(message)
Внедрение MainActorMessage и AsyncMessage - это не просто следование трендам, а повышение качества кода. Мы получаем не только технические преимущества в виде компиляторных проверок и строгой типизации, но и совершенно иной уровень уверенности в своем коде.
Мобильный трудоголик
Please open Telegram to view this post
VIEW IN TELEGRAM
Всем привет! Переход на Swift Testing - это не просто смена синтаксиса, а изменение парадигмы тестирования. Особенно это касается параметризованных тестов, которые кажутся идеальным решением для замены множества похожих проверок. Но за кажущейся простотой скрываются риски, способные превратить ваши тесты в формальность вместо реального инструмента контроля качества.
Подробнее о проблема:
Проблема 1 - иллюзия покрытия: параметризованные тесты создают ложное ощущение полноты проверок. Рассмотрим классический пример:
@Test(arguments: UserRole.allCases)
func testAccess(role: UserRole) {
#expect(system.hasAccess(role) == true)
}
Что не так с этим подходом:
Проблема 2 - зависимость от порядка и структуры данных: использование CaseIterable для автоматической генерации тестовых данных создает хрупкие зависимости:
enum PaymentMethod: CaseIterable {
case card, applePay, googlePay // Порядок имеет значение!
}
@Test(arguments: PaymentMethod.allCases)
func testPaymentProcessing(method: PaymentMethod) {
// Тест зависит от порядка элементов в enum
}
Последствия:
Проблема 3 - смешивание тестовой логики и проверок: параметризованные тесты часто приводят к появлению условной логики внутри проверок:
@Test(arguments: ProductCategory.allCases)
func testPricing(category: ProductCategory) {
if category == .premium {
#expect(calculatePrice(category) >= 1000)
} else {
#expect(calculatePrice(category) < 1000)
}
}
Что здесь происходит:
Параметризованные тесты в Swift Testing - мощный инструмент, но не панацея. Их слепое применение может привести к обратному эффекту: вместо улучшения покрытия и читаемости вы получите хрупкие, сложные в поддержке проверки, которые маскируют реальные проблемы.
Ключевой принцип: параметризуйте только то, что действительно является вариациями одного и того же сценария. Если тестовые кейсы имеют разную природу, требования или критичность - лучше оставить их отдельными.
Мобильный трудоголик
Please open Telegram to view this post
VIEW IN TELEGRAM
👍16 9❤5🔥2🤔2
Forwarded from Кот Денисова
Добрый день! У каждого из нас были проекты, которые начинались с энтузиазмом, а через месяц превращались в папку «забытые идеи». Существует подход, который помогает доводить дела до конца, даже самые сложные.
Проблема больших целей.
Когда вы ставите цель «сделать приложение для управления задачами», мозг воспринимает это как одну огромную неподъемную задачу. Гораздо эффективнее разбить ее на этапы, каждый из которых дает видимый результат.
Ключевые принципы:
Почему это работает?
Главный враг больших проектов не сложность, а потеря мотивации. Когда вы неделями не видите результата, легко забросить все.
Этот подход стоит применять и в работе и в личных проектах. Он особенно полезен джуниорам, которые еще не научились оценивать объем работ.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍17🔥9❤3🙏1👀1
Друзья, сегодня разберем неочевидную проблему производительности в Swift, о которой мало кто знает. Речь пойдет о том, как простые операции вроде String(describing:) или as? могут серьезно замедлять работу приложения.
В Swift Runtime есть метод:
swift_conformsToProtocolMaybeInstantiateSuperclasses
который выполняет проверки соответствия типов протоколам. В больших приложениях с десятками тысяч типов эти проверки превращаются в линейный поиск по огромному массиву.
Пример где мы теряем производительность:
String(describing:) и String(reflecting:)
Кажется безобидным, но:
struct User {
let id: Int
let name: String
}
let user = User(id: 1, name: "John")
let description = String(describing: user) // 4 проверки протоколов!
Приведение типов
Операции as? при работе с протоколами оказались особенно затратными:
// Такой код может быть затратнее, чем кажется
if let convertible = value as? CustomStringConvertible {
print(convertible.description)
}
// Приведение к конкретному классу работает значительно быстрее
if let viewController = object as? UIViewController {
}
Дженерики с ограничениями
// Плохо для производительности
class Cache<T: Codable & Sendable> {
}
// Лучше
class Cache<T> {
let encode: (T) -> Data
let decode: (Data) -> T
init(encode: @escaping (T) -> Data, decode: @escaping (Data) -> T) {
self.encode = encode
self.decode = decode
}
}
Производительность Swift-приложений - это не только вопросы асинхронности и сложных алгоритмов. Самые значительные улучшения часто скрываются в понимании внутренних механизмов Swift Runtime и оптимизации, казалось бы, элементарных операций.
Мобильный трудоголик
Please open Telegram to view this post
VIEW IN TELEGRAM
Контекстные меню в SwiftUI это мощный инструмент для скрытия дополнительных действий и функций. Однако они часто страдают от недостатка контекста: пользователь не всегда понимает, к чему именно относится действие, пока не нажмет на него. Начиная с iOS 16, SwiftUI предлагает решение этой проблемы с помощью модификатора preview, который позволяет показывать детальную информацию прямо в меню, прежде чем пользователь сделает выбор.
Базовый пример - уточнение действия:
Рассмотрим сценарий с просмотром документа. Без preview пользователь видит только действия, но не понимает, какой именно документ будет затронут:
DocumentCard(document: report)
.contextMenu {
Button("Переименовать", systemImage: "pencil") {
rename()
}
Button("Создать копию", systemImage: "doc.on.doc") {
duplicate()
}
Button("Удалить", systemImage: "trash", role: .destructive) {
delete()
}
}
С добавлением preview меню становится самодостаточным:
DocumentCard(document: report)
.contextMenu {
Button("Переименовать", systemImage: "pencil") {
rename()
}
Button("Создать копию", systemImage: "doc.on.doc") {
duplicate()
}
Button("Удалить", systemImage: "trash", role: .destructive) {
delete()
}
} preview: {
// Preview показывает, с каким документом работаем
VStack(alignment: .leading, spacing: 8) {
Text(report.title)
.font(.headline)
HStack {
Label("\(report.pageCount) стр.", systemImage: "doc.text")
Label(report.formattedDate, systemImage: "calendar")
}
.font(.caption)
.foregroundStyle(.secondary)
if let previewText = report.preview {
Text(previewText)
.font(.caption)
.lineLimit(3)
.foregroundStyle(.secondary)
}
}
.padding()
.frame(width: 300)
}
Расширенный сценарий - интерактивный preview:
Модификатор preview поддерживает не только статический контент, но и интерактивные элементы, что открывает дополнительные возможности:
Image(uiImage: selectedPhoto)
.resizable()
.aspectRatio(contentMode: .fit)
.contextMenu {
Button("Редактировать", systemImage: "slider.horizontal.3") {
editPhoto()
}
Button("Поделиться", systemImage: "square.and.arrow.up") {
sharePhoto()
}
Button("Удалить", systemImage: "trash", role: .destructive) {
deletePhoto()
}
} preview: {
// Preview с возможностью масштабирования
ZoomableImageView(image: selectedPhoto)
.frame(height: 300)
.cornerRadius(12)
}
Важные детали:
Оптимизация производительности:
Поскольку preview рендерится в момент появления меню, стоит избегать тяжелых операций в его инициализации. Для динамических данных лучше использовать отложенную загрузку или кэширование.
Модификатор preview для контекстных меню - это не просто косметическое улучшение, а фундаментальное изменение подхода к проектированию контекстных действий. Он позволяет превратить меню из простого списка команд в информативный интерфейсный элемент, который показывает пользователю ровно ту информацию, которая нужна для принятия осознанного решения.
Мобильный трудоголик
Please open Telegram to view this post
VIEW IN TELEGRAM
Структуры в Swift - это основа современной разработки под iOS. Они гарантируют безопасность потоков, предсказуемость и чистоту кода. Однако многие разработчики забывают, что за кажущейся простотой value types скрываются важные компромиссы, особенно когда речь заходит о производительности. В этом посте разберем, как неправильное использование структур может незаметно замедлить ваше приложение, и как этого избежать.
Основной принцип структур - копирование при каждом присваивании или передаче. Это прекрасно работает для простых типов вроде Int, String или небольших struct. Но представьте сценарий, где ваша структура содержит несколько больших массивов данных или вложенные коллекции:
struct AnalyticsReport {
var userEvents: [UserEvent] // 10 000 элементов
var sessionLogs: [SessionLog] // 5 000 элементов
var metadata: [String: Any]
var userId: UUID
var createdAt: Date
}
Каждый раз при передаче такого отчета между функциями Swift создает полную копию всех данных. Если UserEvent и SessionLog - тоже структуры, то происходит глубокое рекурсивное копирование. В таком сценарии затраты памяти и процессорного времени могут быть колоссальными.
Где возникают скрытые копии:
@escaping замыканиях.Пример опасного сценария:
func processReport(_ report: AnalyticsReport) -> ProcessedReport {
// Здесь уже создана полная копия report
let filtered = report.userEvents.filter { $0.isImportant }
}
// Где-то в коде:
let report = AnalyticsReport(...) // Большой объект
for processor in processors {
let result = processReport(report) // Копия на каждой итерации!
}
Оптимизация Copy-on-Write (COW) - не панацея:
Многие рассчитывают на встроенную оптимизацию Copy-on-Write, но она работает только для стандартных типов (Array, Dictionary, String) и требует определенных условий. Ваши кастомные структуры не получают COW автоматически.
struct LargeData {
var items: [String] // Имеет COW
var customBuffer: UnsafeMutableRawPointer // Копируется всегда
var nestedStruct: AnotherStruct // Копируется всегда
}
Когда стоит рассмотреть переход к классам:
// Вместо большой структуры:
struct HeavyConfiguration {
var rules: [Rule] // 1000+ правил
var templates: [Template]
var settings: [String: Any]
}
// Можно использовать гибридный подход:
final class ConfigurationStorage {
var rules: [Rule]
var templates: [Template]
var settings: [String: Any]
}
struct LightweightConfiguration {
let id: UUID
let version: String
let storage: ConfigurationStorage // Общая ссылка
}
Практическое правило: Если ваша структура превышает 1-2 КБ в размере или содержит больше 10-15 свойств, задумайтесь об оптимизации. Используйте Instruments и Time Profiler для измерения реального влияния.
Структуры - мощный инструмент, но их следует использовать осознанно. Слепая вера в то, что «структуры всегда быстрее классов», может привести к обратному результату. Ключ к производительности - понимание стоимости копирования и умение выбирать правильную абстракцию для каждой задачи.
Иногда небольшое количество контролируемых мутабельных состояний через классы дает больше преимуществ, чем повсеместное использование иммутабельных структур. Баланс между безопасностью и производительностью - вот что отличает опытного Swift-разработчика.
Мобильный трудоголик
Please open Telegram to view this post
VIEW IN TELEGRAM
1👍20❤9👀5 2🔥1🙏1
Начиная с Swift 6 подход к отладке многопоточных приложений изменился. Вместо вопроса «на каком потоке выполняется код?» теперь нужно спрашивать «на каком акторе я нахожусь?». Это фундаментальный сдвиг, который требует новых инструментов отладки.
Почему Thread.isMainThread больше не актуален:
В Swift 6 при использовании async/await вы получите ошибку компиляции при попытке использовать Thread.isMainThread. Вместо этого система предлагает использовать аннотацию
@MainActor. Это не ограничение, а переход к более безопасной модели - акторной изоляции.Основной инструмент: MainActor.assertIsolated()
Для проверки выполнения на главном акторе используйте:
func updateUI() {
MainActor.assertIsolated("UI нужно обновлять в MainActor!")
titleLabel.text = "Данные загружены"
}Что делает данный метод:
Когда ошибка должна приводить к крашу в любом случае, используйте MainActor.preconditionIsolated().
Анализ контекста выполнения в Xcode:
Когда происходит ошибка изоляции, в Xcode Debug Navigator отображается:
Эта информация помогает понять текущий контекст выполнения без проверки потоков.
Что такое акторы:
Акторы в Swift - это не просто обертка над потоками. Это полноценная модель программирования, которая обеспечивает:
Переход от потоков к акторам в Swift - это эволюция подхода к многопоточности. Вместо ручного управления потоками и синхронизацией можно полагаться на систему акторов, которая обеспечивает безопасность на уровне компилятора.
Использование MainActor.assertIsolated() и аналогичных методов для кастомных акторов как основных инструменты отладки не только помогает находить ошибки на этапе разработки, но и заставит задуматься о правильном проектировании изоляции данных в вашем приложении.
Мобильный трудоголик
Please open Telegram to view this post
VIEW IN TELEGRAM
👍14 5❤2🔥1 1
SwiftUI обещал революцию: декларативный синтаксис, живые превью, кроссплатформенность. Но когда речь заходит о построении сложной навигационной архитектуры для приложений, декларативный рай оборачивается императивным адом. Особенно остро это чувствуется в проектах с требовательными дизайнами, глубокими диплинками и кастомными UI-компонентами. Давайте разберем, где SwiftUI показывает свои границы и какие стратегии помогают эти границы расширить.
Фундаментальный разрыв - декларативное состояние vs императивная логика:
Навигация по своей природе императивна: «перейди туда», «вернись обратно», «покажи поверх», «закрой все». SwiftUI пытается описать это через состояние (
@State, @Published), но сталкивается с проблемой композиции навигационных действий.Рассмотрим реальный сценарий: пользователь получает push-уведомление -> должен открыться конкретный экран заказа -> но если пользователь не авторизован, нужно сначала показать экран входа -> после успешной авторизации продолжить исходный переход.
В UIKit это цепочка императивных команд. В SwiftUI возникает парадокс:
Состояние описывает «что видно», но не «что нужно сделать» и «в какой последовательности». Именно этот разрыв между описанием интерфейса и логикой переходов становится основной болью при построении сложных навигационных потоков в SwiftUI.
Решение - гибридная архитектура:
Вместо попыток заставить SwiftUI делать то, для чего он не предназначен, эффективнее признать: навигация - это системная, платформозависимая задача. И использовать правильный инструмент для каждой части:
// Координатор на Swift управляет UIKit навигацией
class OrderCoordinator {
private let navigationController: UINavigationController
func showOrder(id: String, context: NavigationContext) {
if !context.isAuthenticated {
showAuth { [weak self] success in
if success { self?.showOrder(id: id, context: context) }
}
return
}
let swiftUIView = OrderDetailView(orderId: id)
let hostingController = UIHostingController(rootView: swiftUIView)
navigationController.pushViewController(hostingController, animated: true)
}
}
Почему это работает лучше:
Что SwiftUI делает хорошо:
SwiftUI - отличный инструмент для построения UI, но навигация остается его ахиллесовой пятой. Вместо того чтобы бороться с системой, пытаясь заставить декларативный подход описывать императивную логику, эффективнее признать: некоторые задачи по-прежнему лучше решаются старыми, проверенными методами.
Мобильный трудоголик
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥16👍9 5🤯1🙏1👀1
Тестирование многопоточного кода в Swift всегда было не без проблем. С появлением Swift Concurrency ситуация стала лучше, но не идеальной. Самые проблемные - плавающие тесты, которые то проходят, то падают без видимых причин. Корень проблемы в недетерминированном выполнении асинхронных операций, особенно при использовании Task {}. Сейчас разберем, как заставить такие тесты работать предсказуемо.
Почему обычные тесты с Task не работают:
Когда мы создаем Task внутри синхронного метода, тест не ждет его завершения:
// Плавающий тест
func testAsyncOperation() {
let service = DataService()
service.startProcessing() // Внутри создается Task
XCTAssertTrue(service.isCompleted) // Может упасть
}
Тест завершится раньше, чем Task выполнится, потому что Task {} запускает асинхронную операцию, которая живет своей жизнью.
Решение - абстракция над Task:
Ключевая идея - создать протокол TaskProvider, который инкапсулирует создание задач:
protocol TaskProvider {
func task<T>(priority: TaskPriority?, operation: @escaping () async -> T) -> Task<T, Never>
}
struct DefaultTaskProvider: TaskProvider {
func task<T>(priority: TaskPriority?, operation: @escaping () async -> T) -> Task<T, Never> {
Task(priority: priority, operation: operation)
}
}
// Мок для тестов
class MockTaskProvider: TaskProvider {
private var tasks: [Task<Void, Never>] = []
func task<T>(priority: TaskPriority?, operation: @escaping () async -> T) -> Task<T, Never> {
let task = Task(priority: priority) {
await operation()
}
tasks.append(task as! Task<Void, Never>)
return task
}
// Ждем завершения всех созданных задач
func waitForAllTasks() async {
for task in tasks {
await task.value
}
}
}Использование в коде:
Меняем зависимость в нашем сервисе:
class DataService {
private let taskProvider: TaskProvider
init(taskProvider: TaskProvider = DefaultTaskProvider()) {
self.taskProvider = taskProvider
}
func startProcessing() {
taskProvider.task(priority: .medium) {
// Долгая асинхронная операция
await self.processData()
}
}
}Стабильный тест:
Теперь тест может дождаться выполнения всех задач:
func testAsyncOperation() async {
let mockProvider = MockTaskProvider()
let service = DataService(taskProvider: mockProvider)
service.startProcessing()
await mockProvider.waitForAllTasks() // Ждем завершения
XCTAssertTrue(service.isCompleted) // Стабильно проходит
}Преимущества подхода:
Важное замечание:
Этот подход не заменяет async/await тесты, а дополняет их. Для кода, который уже использует async функции, лучше тестировать через await. Но для legacy-кода или ситуаций, где Task создается внутри синхронных методов, этот паттерн незаменим.
Тестирование многопоточного кода требует особого подхода и абстракция TaskProvider предоставляет его. Она превращает плавающие тесты в детерминированные, давая полный контроль над выполнением асинхронных операций. Через инверсию зависимостей сохраняется чистота архитектуры, а случайные падения в CI/CD становятся историей.
Особенно ценен этот паттерн при работе с legacy-кодом, где синхронные и асинхронные вызовы соседствуют в процессе миграции на Swift Concurrency.
Мобильный трудоголик
Please open Telegram to view this post
VIEW IN TELEGRAM
Forwarded from Кот Денисова
Сейчас многие в ИТ стремятся к быстрым деньгам: переходят в компании, где платят больше, но не дают развития. Кажется, это логичный выбор, но на самом деле это тупиковый путь.
Как работает карьерный рост в ИТ:
Есть естественная последовательность, которую нельзя нарушать:
Если попытаться прыгнуть на четвертый этап, минуя предыдущие, ничего не получится. Без знаний и навыков высокие зарплаты недолговечны.
Плохой пример:
Представьте разработчика, который в 25 лет выбрал компанию, предлагавшую зарплату на 40% выше рыночной. Проект казался стабильным, задачи комфортными. Но через три года оказалось, что:
Такой разработчик оказался в ловушке: текущая зарплата все еще высока, но профессионального роста нет, а сменить работу страшно - новые технологии уже не освоить за две недели.
Важно для развития:
Высокая зарплата в ИТ - это следствие экспертизы, а не ее причина. Сначала станьте ценным специалистом, а финансовый успех придет естественным образом.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍17❤14😁6🔥2🗿2👀1 1
Всем привет! Сегодня разберем одну из фундаментальных тем в iOS-разработке: управление памятью с помощью ARC. Понимание этого механизма критически важно для создания стабильных приложений без утечек памяти.
Что такое ARC:
Automatic Reference Counting - это система автоматического подсчета ссылок, встроенная в Swift. В отличие от ручного управления памятью, ARC самостоятельно отслеживает, когда объекты больше не нужны, и освобождает занимаемую ими память.
Пример:
class User {
let name: String
init(name: String) {
self.name = name
}
}
var user1: User? = User(name: "Иван") // Счетчик ссылок: 1
var user2: User? = user1 // Счетчик ссылок: 2
user1 = nil // Счетчик ссылок: 1
user2 = nil // Счетчик ссылок: 0, память освобождена
Главная проблема: циклы сильных ссылок.
Самая распространенная ошибка: создание retain cycles, когда два объекта держат друг друга сильными ссылками и не могут быть освобождены.
Пример:
class Profile {
var settings: Settings?
}
class Settings {
var profile: Profile?
}
let profile = Profile() // Счетчик ссылок: 1
let settings = Settings() // Счетчик ссылок: 1
profile.settings = settings // Счетчик ссылок: 2, Profile держит Settings
settings.profile = profile // Счетчик ссылок: 2, Settings держит Profile
// Оба объекта держат друг-друга и не будут освобождены!
Решение: weak и unowned ссылки.
Weak и unowned ссылки не увеличивают счетчик ссылок объекта.
Weak ссылки:
class Profile {
weak var settings: Settings?
}
class Settings {
weak var profile: Profile?
}
let profile = Profile() // Счетчик ссылок: 1
let settings = Settings() // Счетчик ссылок: 1
profile.settings = settings // Счетчик ссылок не увеличивается
settings.profile = profile // Счетчик ссылок не увеличивается
// Теперь объекты могут быть освобождены
Unowned ссылки:
class CreditCard {
unowned let owner: Customer
init(owner: Customer) {
self.owner = owner
}
}
class Customer {
let name: String
init(name: String) {
self.name = name
}
}
let customer = Customer(name: "Artem") // Счетчик ссылок: 1
let card = CreditCard(owner: customer) // Счетчик ссылок: 1 (не изменился!)
// Теперь объекты могут быть освобождены
Разница между weak и unowned:
Weak:
Unowned:
ARC автоматизирует управление памятью, но требует от разработчика понимания работы ссылок. Ключевое правило: weak и unowned ссылки не увеличивают счетчик ссылок, что позволяет разрывать циклы зависимостей. Используйте weak для безопасных связей и unowned только когда уверены в жизненном цикле объектов.
Про внутреннюю механику работы weak-ссылок через Side Tables подробно поговорим в будущих постах.
Мобильный трудоголик
Please open Telegram to view this post
VIEW IN TELEGRAM
Работа со списками в SwiftUI часто превращается в борьбу с производительностью. Казалось бы, простой List или ForEach начинает лагать, приложение потребляет неоправданно много ресурсов, а причина остается неочевидной. Стандартные инструменты профилирования не всегда показывают корень проблемы, особенно когда дело касается динамического создания вьюх. Однако в SwiftUI существует малоизвестный параметр, который может стать вашим первым шагом в поиске проблемы.
Причина возникновения проблемы:
Проблема производительности в List и ForEach часто возникает из-за динамического количества вьюх. Когда SwiftUI не может предсказать, сколько элементов будет отрисовано, или когда структура списка динамически меняется, фреймфорк вынужден использовать более медленные пути рендеринга. Эти «медленные пути» могут существенно влиять на плавность скролла и общую отзывчивость интерфейса.
Параметр -LogForEachSlowPath:
Диагностический параметр -LogForEachSlowPath YES, передается как аргумент запуска приложения, заставляет SwiftUI логировать предупреждения каждый раз, когда обнаруживается неоптимальный сценарий работы с контейнерами списков. Это не флаг производительности, а исключительно инструмент диагностики, он не ускоряет приложение, но показывает, где именно возникают проблемы.
Как это работает:
Когда SwiftUI встречает ForEach или аналогичный контейнер, который производит неконстантное количество View (например, зависит от вычисляемого в рантайме значения), он переключается на менее эффективный режим работы. Активация флага -LogForEachSlowPath включает внутреннее логирование, которое сообщает о каждом таком переходе, указывая конкретное место в коде.
Важно понимать:
Этот параметр лишь первый шаг диагностики. Он показывает симптомы, но не лечит болезнь. Частые причины проблемы:
Диагностика производительности в SwiftUI требует системного подхода, и параметр -LogForForEachSlowPath - это ценный инструмент в арсенале разработчика. Он позволяет быстро локализовать проблемные участки кода, которые используют неоптимальные пути отрисовки. Однако важно помнить, что это лишь инструмент выявления симптомов, настоящее решение требует анализа архитектуры, стабилизации данных и оптимизации самих вьюх.
Мобильный трудоголик
Please open Telegram to view this post
VIEW IN TELEGRAM
❤20 14👍7🔥2✍1🙏1
С появлением Swift Concurrency многие разработчики стали добавлять actor везде, где видят асинхронность. Это как использовать танк для поездки в магазин за хлебом: мощно, но бессмысленно и создает больше проблем, чем решает.
Вот простой и эффективный чек-лист, который позволит определить, нужно использовать actor или нет:
У вас есть состояние, которое не является Sendable?
Sendable - это маркер того, что тип безопасен для использования в разных параллельных контекстах. Если все ваши данные (структуры, простые классы) уже соответствуют Sendable, то первое и важное основание для использования actor просто отсутствует.
Если у вас либо нет состояния, либо оно уже потокобезопасно (Sendable). actor для его защиты избыточен.
Операции с этим состоянием должны быть атомарными?
Атомарность означает, что промежуточное состояние объекта никогда не будет видно извне. Либо операция выполнена полностью, либо не выполнена совсем.
Если ваши операции можно разбить на независимые шаги или они не требуют такой строгой изоляции, возможно, хватит обычной очереди (DispatchQueue) или async/await с корректным проектированием.
Эти операции не могут быть выполнены на уже существующем actor (например
@MainActor)?Часто проблема решается не созданием нового изолированного острова actor, а правильным использованием существующих.
@MainActor, если она касается UI? Или может ее стоит вынести в @concurrent функцию (Swift 6.2+), чтобы просто запустить в параллельном пуле потоков?@concurrent.Вы не исчерпали возможности существующих механизмов изоляции. Новый actor добавит накладные расходы на переключение контекстов без реальной необходимости.
Actor - это специальный и достаточно ресурсоемкий механизм, созданный для решения конкретной проблемы: строгой изоляции разделяемого изменяемого состояния. Его не следует применять автоматически для любой фоновой работы.
Прежде чем его использовать, необходимо честно ответить на три ключевых вопроса: есть ли у вас состояние, не являющееся потокобезопасным (Sendable), требуют ли операции с ним атомарности и действительно ли эту работу невозможно выполнить в рамках уже существующего контекста изоляции. Только утвердительные ответы на все три пункта оправдывают введение actor. Во всех остальных сценариях стоит отдавать предпочтение более легким инструментам, таким как async/await, проектирование с Sendable типами, классические DispatchQueue или новая аннотация
@concurrent.Мобильный трудоголик
Please open Telegram to view this post
VIEW IN TELEGRAM
В начале 2022 года я решил разработать приложение TaskFocus - планировщик задач со встроенным фокусированием. Мне нужен был единый инструмент, который бы работал на всех моих устройствах и помогал не только фокусироваться на задачах, но и учитывать затраченное на них время.
Разработка:
Изначально разработка планировалась под iOS, но для охвата всех пользователей я принял решение выбрать кроссплатформенный фреймворк Flutter. Это позволило выпустить приложение одновременно на iOS, Android, macOS и Windows. Серверную часть для синхронизации данных также разрабатывал самостоятельно на PHP.
Ключевые возможности:
Мобильный трудоголик
Please open Telegram to view this post
VIEW IN TELEGRAM
❤13👍8🔥4👏1👀1
Иногда самые полезные изменения в языке - не громкие нововведения, а устранение небольших, но раздражающих неудобств. Те, что заставляют писать лишний код, нарушать логику или искать обходные пути. Именно таким изменением стало принятие SE-0493, которое разрешает использовать await внутри defer. На первый взгляд это техническая деталь. На деле - значимое упрощение для написания чистого и надежного асинхронного кода.
Это небольшое, но важное изменение устраняет давнюю проблему: раньше defer не позволял делать await, что мешало писать чистый и надежный асинхронный код для гарантированной очистки ресурсов.
Что изменилось:
Теперь в async-функции вы можете писать так:
func loadData() async throws {
let resource = try await acquireResource()
defer {
await resource.release()
}
try await work(with: resource)
}
Как это работает:
Ключевая механика defer сохраняется, но теперь с поддержкой асинхронности:
Почему это важно:
Раньше для асинхронной очистки приходилось либо дублировать код на всех путях выхода, либо использовать Task { }, который не гарантировал завершения операции. Теперь очистка ресурсов (закрытие сетевых соединений, сброс состояния, отмена операций) становится такой же простой и надежной, как и в синхронном коде.
Принятие SE-0493 - это пример зрелой эволюции языка. Вместо введения сложных новых концепций, Swift устраняет конкретное, давно назревшее несоответствие между синхронными конструкциями языка и его асинхронной парадигмой. Это изменение делает асинхронный код не только безопаснее, но и чище.
Мобильный трудоголик
Please open Telegram to view this post
VIEW IN TELEGRAM
👍19 13🔥4❤2👏1🙏1👀1
This media is not supported in your browser
VIEW IN TELEGRAM
Многие годы разработчики iOS испытывали смешанные чувства при виде таких интерфейсов, как мини-плеер в приложении Apple Music или постоянная панель действий в Podcasts. С одной стороны - это удобный и интуитивный паттерн, с другой - его реализация всегда была головной болью, требующей неочевидных трюков с safeAreaInsets и ручной подгонкой размеров. С выходом iOS 26 эта эпоха подошла к концу. Apple официально представила API для создания нижних аксессуаров в UITabBarController, и это одно из тех изменений, которое кардинально упрощает жизнь.
Прощайте, костыли. Здравствуй, декларативность:
До iOS 26 интеграция любого статичного или плавающего элемента поверх таббара была упражнением в нетривиальной геометрии. Разработчики были вынуждены вручную управлять additionalSafeAreaInsets, отслеживать повороты устройства, адаптировать layout под разные состояния навигации (например, появление клавиатуры) и гарантировать, что кастомная вью не перекроет таббар и его элементы. Новый API bottomAccessory решает эту проблему радикально простым и элегантным способом. Все что требуется - это создать экземпляр UITabAccessory, передав ему ваше кастомное представление, и установить его в свойство контроллера.
let miniPlayerView = MiniPlayerView() // Кастомная вью
let accessory = UITabAccessory(contentView: miniPlayerView)
tabBarController.bottomAccessory = accessory
Система берет на себя всю ответственность за позиционирование, анимации при появлении/скрытии (через метод setBottomAccessory(_:animated:)) и корректное взаимодействие с жестами. Это переход от императивного «как это разместить» к декларативному «что я хочу показать».
Гармония с поведением таббара - новый уровень интеграции:
Инновация не ограничивается простым добавлением вью. Apple обеспечила глубокую интеграцию аксессуара с обновленным поведением самого UITabBar. Теперь таббар может автоматически сворачиваться в компактный вид (например, при скролле контента), и аксессуар реагирует на это изменение согласованно. Поведением управляет свойство tabBarMinimizeBehavior, которое предлагает гибкие опции: от автоматического решения системой до явных триггеров вроде скролла вниз или вверх.
Адаптивный интерфейс через tabAccessoryEnvironment:
Одной из самых тонких проблем при создании подобных элементов была адаптация их внешнего вида к разным состояниям. Нужно ли показывать полный заголовок трека или только иконку? Как изменить layout при компактном таббаре? Для этого Apple ввела новый trait - UITabAccessory.Environment. Теперь вью может запросить у traitCollection текущее окружение (.regular, .inline, .none) и кардинально изменить свой вид или внутреннюю композицию.
contentLayoutGuide - надежный фундамент для контента:
Чтобы окончательно устранить ручные расчеты отступов, Apple добавила в UITabBarController новый UILayoutGuide - contentLayoutGuide. Привязка контента основного вью-контроллера к этому guide, автоматически гарантирует, что он всегда будет располагаться в корректной области: выше системных элементов нижней части интерфейса. Этот guide динамически обновляет свои размеры при изменениях состояния таббара, появлении клавиатуры или изменении ориентации.
Введение bottomAccessory в iOS 26 - это не просто добавление еще одного свойства в API. Это значительный шаг вперед в философии UI-разработки под iOS. Apple признает популярные пользовательские паттерны и предоставляет для них первоклассные, системные инструменты, заменяя годы накопленных хаков и обходных приемов. Это снижает порог входа для создания сложных интерфейсов, повышает стабильность приложений и позволяет разработчикам сосредоточиться на логике и дизайне, а не на борьбе с фреймворком.
Мобильный трудоголик
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥17 10👍3❤1🙏1👀1
Существует инструмент, который ежедневно определяет границы возможного для миллионов разработчиков. И часто эти границы оказываются не там, где мы ожидаем. Речь не об аппаратных ограничениях или сложности алгоритмов, а о среде, в которой рождаются приложения для экосистемы Apple. Это история не столько о багах, сколько о философии, которая превращает процесс создания в перманентное преодоление.
Иллюзия контроля:
Современная разработка строится на предсказуемости: четких ошибках, логичном рабочем процессе, последовательной документации. Xcode систематически нарушает этот контракт. Возьмем классическую ошибку SwiftUI: «The compiler is unable to type-check this expression in reasonable time». Это не ошибка в вашем коде- это капитуляция системы. Компилятор не говорит, где проблема; он предлагает вам угадывать, разбивая выражения наугад. Это эквивалент того, как если бы строительный кран останавливался со словами «что-то тяжело», не указывая на проблемную балку.
Архитектура как наследие:
Погружение в структуру проекта - это путешествие в прошлое. Файл project.pbxproj - это не просто конфигурация, это артефакт эпохи, когда понятия «человеческий формат» считались роскошью. Merge-конфликты в этом файле - это квест, где наградой становится возможность просто открыть проект. Существование инструментов вроде xcodegen - не доказательство гибкости экосистемы, а молчаливое признание провала.
Церемония подписи кода:
Каждый раз при запуске сборки начинается один и тот же ритуал. Система безопасности macOS требует пароль для связки ключей, чтобы подписать приложение вашим сертификатом. Но вместо одного запроса вы получаете целую серию одинаковых окон: одно для сертификата, другое для приватного ключа, третье для профиля. Они наслаиваются друг на друга, затемняя экран, и вы вынуждены по несколько раз подряд вводить один и тот же пароль.
Документация как мираж:
Попытка следовать официальным руководствам - это часто путь через зеркало. Sandbox-аккаунт для тестирования покупок должен появляться в настройках симулятора, но его нет. Вы вводите пароль снова и получаете загадочное «Password reuse not available for account». Форумы разработчиков разделены: половина утверждает, что в симуляторе это не работает, другая половина - что работает. Правда где-то посередине, но ее приходится устанавливать методом проб и ошибок, а не чтением документации.
Закрытость как система:
Баг-трекер Apple - это черный ящик. Отправив отчет, вы не получаете обратной связи, не видите обсуждений, не можете узнать, воспроизводится ли проблема у других. Это создает вакуум, где каждый разработчик вынужден в одиночку бороться с проблемами, которые могут быть системными. Экосистема, где тысячи профессионалов тратят время на повторное открытие одних и тех же багов - это не экосистема, а лабиринт без карты.
Монополия на инструменты:
Отсутствие реальных альтернатив - ключевой момент. AppCode от JetBrains был похоронен, а настройка Neovim с xcode-build-server остается увлечением для энтузиастов. CLI-инструменты плохо документированы, что делает автоматизацию и CI/CD не естественным процессом, а подвигом. Fastlane существует не для расширения возможностей, а для преодоления фундаментальных недостатков.
Xcode - это не просто IDE. Это культурный код, который формирует мышление разработчика. Он учит не углубляться в суть проблем, а выполнять ритуалы: перезагрузить, очистить Derived Data, пересоздать проект. Опасность такого подхода в том, что он формирует поколение разработчиков, которые воспринимают непредсказуемость как норму. Apple создала выдающиеся продукты, но инструменты для их создания остаются парадоксом: они одновременно и мост и барьер.
Мобильный трудоголик
Please open Telegram to view this post
VIEW IN TELEGRAM
❤20👍13👀5🤔2🤯1🤝1
Forwarded from Кот Денисова
Распространенное мнение, что ИТ - это сфера, в которой можно строить карьеру из любой точки мира, лишь отчасти соответствует действительности. Несмотря на рост популярности удаленной работы, физическое местоположение продолжает играть значительную роль в карьерном пути разработчика. Вот ключевые причины, почему это так:
Концентрация возможностей в крупных центрах.
Крупные города и технологические кластеры остаются центрами притяжения ИТ-индустрии. Здесь сосредоточены штаб-квартиры компаний, венчурные фонды, исследовательские центры и специализированные образовательные учреждения. Такая концентрация создает среду, где проще найти работу, соответствующую растущим амбициям и квалификации. Кроме того, высокая конкуренция среди работодателей в таких регионах часто приводит к более выгодным условиям труда и уровню дохода.
Роль профессионального сообщества и нетворкинга.
Карьерный рост в ИТ редко происходит изолированно. Значительная часть возможностей возникает благодаря профессиональным связям, как слабым, так и сильным. Участие в конференциях, митапах и воркшопах позволяет не только быть в курсе последних трендов, но и устанавливать контакты, которые в будущем могут трансформироваться в предложения о сотрудничестве, партнерстве или трудоустройстве. В удаленном формате такие спонтанные, но важные взаимодействия происходят значительно реже.
Доступ к уникальным проектам и инновациям.
Многие передовые проекты, особенно на ранних стадиях, требуют тесного взаимодействия внутри команды и с инвесторами. Это особенно характерно для стартапов, где скорость и гибкость имеют критическое значение. Находясь вблизи таких экосистем, разработчики получают более легкий доступ к участию в инновационных инициативах, что может ускорить их профессиональное развитие.
Влияние на формирование личного бренда.
Хотя цифровое присутствие - важная часть современной профессиональной репутации, офлайн-активность по-прежнему имеет вес. Публичные выступления, участие в жюри хакатонов или менторство в локальных образовательных программах усиливают узнаваемость и авторитет специалиста внутри профессионального сообщества. В условиях большого города такие возможности встречаются чаще и имеют более широкий охват.
Несмотря на то, что удаленная работа расширила границы возможного для ИТ-специалистов, географический фактор продолжает влиять на траекторию карьеры. Осознанный выбор места жительства с учетом развитости местной технологической среды может стать стратегическим решением, открывающим доступ к уникальным возможностям для профессионального роста.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍17❤9👀4🗿2🔥1🤯1