Мобильный трудоголик
1.33K subscribers
61 photos
9 videos
265 links
👨‍💻 Пишу простым языком об iOS разработке на Swift и мобильной разработке в целом.
🔹 Вошел в IT задолго до того как это стало мейнстримом.
---
‍Обо мне: https://t.me/hardworkerIT/3
Чат: @hardworkerChatIT
Канал про разработку и жизнь в ИТ: @itDenisov
Download Telegram
🔢 Управление временем жизни задач в Swift Concurrency.

Привет! Swift Concurrency предоставляет мощные инструменты для работы с асинхронными операциями, но понимание времени жизни задач - ключевой аспект, который часто упускают из виду. Давайте разберемся, как разные типы задач управляют своим жизненным циклом.


Structured Concurrency: автоматическое управление:

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


Примеры structured задач:

🔵async let в асинхронных функциях.
🔵withTaskGroup для параллельного выполнения.
🔵Модификатор .task в SwiftUI.

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, это останавливает только получение уведомлений, но не их генерацию. Источник продолжает создавать уведомления, даже если их никто не обрабатывает.

Чтобы полностью остановить поток данных, нужно управлять обеими сторонами:

🔵Задачей, которая читает данные (через cancel()).
🔵Источником данных (через соответствующий метод остановки).


Проверка отмены:

Системные 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
17👍136🙏2🔥11
🔢 Embedded Swift в Swift 6.3: выход за пределы iOS в мир микроконтроллеров.

Swift продолжает расширять границы применения. После успешного освоения серверной разработки, Linux и Windows, язык делает стратегический шаг в embedded-разработку. Теперь на Swift можно писать не только приложения для iPhone, но и программы для микроконтроллеров, датчиков и специализированных устройств.


Суть Embedded Swift:

Embedded Swift - это оптимизированная версия языка, созданная для работы в условиях крайне ограниченных ресурсов. Если обычный Swift полагается на операционную систему и runtime, то Embedded Swift работает напрямую с железом, где каждый байт памяти имеет значение.


Основные отличия от стандартного Swift:

🔵Автоматическое управление памятью (ARC) заменяется преимущественно ручным контролем.

🔵Отсутствуют механизмы рефлексии и динамической диспетчеризации.

🔵Стандартная библиотека сокращена до минимального набора.

🔵Запрещены weak и unowned ссылки из-за их накладных расходов.

🔵Сохранены ключевые возможности: структуры, классы, протоколы, дженерики, перечисления.


Пример:


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:

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

🔵Быстрое прототипирование IoT-устройств.

🔵Специализированные embedded-решения, требующие высокоуровневых абстракций.

🔵Разработка для игровых систем вроде консоли Playdate.


Новые языковые возможности для embedded-разработки:


// Прямая работа с разделами памяти
@section(".__DATA, .sensor_data")
@used
var sensorReadings: [Float] = []

// Упрощённая интеграция с C-кодом
@c(readSensorData)
func readSensorData() -> Float {
// Взаимодействие с аппаратным датчиком
return readFromHardware()
}



Сравнение с традиционными подходами:

Язык C/C++ предлагает максимальный контроль и минимальный размер кода, но требует глубокого знания низкоуровневых деталей. Rust обеспечивает безопасность памяти на уровне компилятора, но имеет крутую кривую обучения. Embedded Swift занимает промежуточную позицию - предоставляет более высокоуровневые абстракции, сохраняя приемлемую производительность и размер бинарного файла.


Значение для iOS-разработчиков:

🔵Улучшенная совместимость с C: атрибут @c делает взаимодействие безопаснее и предсказуемее.

🔵Расширенная диагностика: компилятор теперь лучше обнаруживает проблемы, специфичные для embedded-среды.

🔵Усовершенствованные инструменты отладки: LLDB получил улучшенную поддержку embedded-устройств.

🔵Глубокое понимание оптимизаций: опыт работы с ограниченными ресурсами улучшает общие навыки разработки.


🔗 Ссылка на подробную статью


💡 Вывод:

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

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


➡️ Подписаться на канал
Мобильный трудоголик
Please open Telegram to view this post
VIEW IN TELEGRAM
21👍11🔥52👏1🙏1
🔢 MainActorMessage и AsyncMessage: безопасные уведомления в Swift Concurrency.

Всем привет! С выходом iOS 26 работа с уведомлениями в Swift Concurrency кардинально меняется. Старый добрый NotificationCenter, который годами служил верой и правдой, теперь выглядит как отголосок из прошлого - он не понимает модель акторов и постоянно генерирует предупреждения компилятора, но у Apple готово элегантное решение: протоколы MainActorMessage и AsyncMessage. Давайте разберемся, как они работают и почему это настоящий прорыв.


Проблемы старого подхода:

Традиционный NotificationCenter сталкивается с двумя фундаментальными проблемами в Swift Concurrency:

🔵Нарушение изоляции акторов: даже при подписке на главной очереди компилятор видит потенциальную гонку данных при вызове методов, помеченных @MainActor

🔵Отсутствие типобезопасности: постоянные приведения типов через as? создают хрупкий код, где ошибки обнаруживаются только в рантайме


MainActorMessage: гарантия выполнения на главном потоке:


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


// Старый подход
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
17🔥11👍6🙏21👏1
🔢 Параметризованные тесты: скрытые ловушки при переходе на Swift Testing.

Всем привет! Переход на 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
}


Последствия:

🔵Рефакторинг 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
👍1695🔥2🤔2
Forwarded from Кот Денисова
👨‍💻 Декомпозиция больших задач: как доводить проекты до конца.

Добрый день! У каждого из нас были проекты, которые начинались с энтузиазмом, а через месяц превращались в папку «забытые идеи». Существует подход, который помогает доводить дела до конца, даже самые сложные.


Проблема больших целей.

Когда вы ставите цель «сделать приложение для управления задачами», мозг воспринимает это как одну огромную неподъемную задачу. Гораздо эффективнее разбить ее на этапы, каждый из которых дает видимый результат.


Ключевые принципы:

🔹Декомпозиция на мелкие задачи: вместо «разработать таск-трекер» ставим цель «сделать форму для добавления задач». Вместо «разработать сайт» ставим «купить домен для сайта». Каждая задача должна занимать не больше 1-2 дней.

🔹Первый прототип за неделю: самый важный рубеж - получить работающий прототип как можно быстрее. Даже если он будет кривым и с ограниченной функциональностью.

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

🔹Без перфекционизма: не пытайтесь сделать идеально с первого раза. Сделайте удовлетворимый минимум и двигайтесь дальше.


Почему это работает?

🔹Мотивация: видимый прогресс поддерживает энтузиазм.
🔹Обратная связь: вы сразу отслеживаете как все работает и в правильном направлении движетесь или нет.
🔹Гибкость: легко поменять подход, если что-то идет не так.


💡 Вывод:

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

Этот подход стоит применять и в работе и в личных проектах. Он особенно полезен джуниорам, которые еще не научились оценивать объем работ.


➡️ Кот Денисова
Please open Telegram to view this post
VIEW IN TELEGRAM
👍17🔥93🙏1👀1
🔢 Swift Runtime: как скрытые процессы тормозят ваше приложение.

Друзья, сегодня разберем неочевидную проблему производительности в 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
17👍11🔥51👏1🫡1
🔢 Контекстные меню с превью в SwiftUI: показываем детали до действия.

Контекстные меню в 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 работает только в контекстных меню, вызванных долгим нажатием.

🔵Размер и layout preview можно гибко настраивать.

🔵Preview поддерживает анимации и переходы внутри себя.

🔵Может содержать любые SwiftUI вью, включая списки, сетки и интерактивные элементы.


Оптимизация производительности:

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


💡 Вывод:

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


➡️ Подписаться на канал
Мобильный трудоголик
Please open Telegram to view this post
VIEW IN TELEGRAM
17👍12🔥5👏211
🔢 Структуры в Swift: когда скорость копирования становится проблемой.

Структуры в 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 // Копируется всегда
}



Когда стоит рассмотреть переход к классам:

🔵Общие изменяемые состояния: когда несколько объектов должны ссылаться на одни и те же данные.

🔵Большие неизменяемые (immutable) данные: которые часто передаются по системе.

🔵Ресурсоемкие объекты: содержащие файловые дескрипторы, сетевые соединения, изображения.


// Вместо большой структуры:
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👍209👀52🔥1🙏1
🔢 Swift Concurrency: как проверить, на каком акторе выполняется ваш код.

Начиная с Swift 6 подход к отладке многопоточных приложений изменился. Вместо вопроса «на каком потоке выполняется код?» теперь нужно спрашивать «на каком акторе я нахожусь?». Это фундаментальный сдвиг, который требует новых инструментов отладки.


Почему Thread.isMainThread больше не актуален:

В Swift 6 при использовании async/await вы получите ошибку компиляции при попытке использовать Thread.isMainThread. Вместо этого система предлагает использовать аннотацию @MainActor. Это не ограничение, а переход к более безопасной модели - акторной изоляции.


Основной инструмент: MainActor.assertIsolated()

Для проверки выполнения на главном акторе используйте:

func updateUI() {
MainActor.assertIsolated("UI нужно обновлять в MainActor!")
titleLabel.text = "Данные загружены"
}


Что делает данный метод:

🔵В debug-сборках: вызывает краш приложения, если код выполняется не на главном акторе.

🔵В release-сборках: ничего не делает.

🔵Позволяет мгновенно обнаруживать ошибки во время разработки.

Когда ошибка должна приводить к крашу в любом случае, используйте MainActor.preconditionIsolated().


Анализ контекста выполнения в Xcode:

Когда происходит ошибка изоляции, в Xcode Debug Navigator отображается:

🔵Queue: com.apple.main-thread (serial) - выполнение на главном акторе.

🔵Queue: com.apple.root.user-initiated-qos.cooperative (concurrent) - выполнение на фоновом акторе.

Эта информация помогает понять текущий контекст выполнения без проверки потоков.


Что такое акторы:

Акторы в Swift - это не просто обертка над потоками. Это полноценная модель программирования, которая обеспечивает:

🔵Сериализацию доступа к данным.

🔵Компиляторные гарантии безопасности.

🔵Автоматическое управление жизненным циклом задач.


🔗 Ссылка на подробную статью


💡 Вывод:

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

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


➡️ Подписаться на канал
Мобильный трудоголик
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1452🔥11
🔢 Обратная сторона SwiftUI: почему сложная навигация все еще требует UIKit.

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


Фундаментальный разрыв - декларативное состояние vs императивная логика:

Навигация по своей природе императивна: «перейди туда», «вернись обратно», «покажи поверх», «закрой все». SwiftUI пытается описать это через состояние (@State, @Published), но сталкивается с проблемой композиции навигационных действий.

Рассмотрим реальный сценарий: пользователь получает push-уведомление -> должен открыться конкретный экран заказа -> но если пользователь не авторизован, нужно сначала показать экран входа -> после успешной авторизации продолжить исходный переход.

В UIKit это цепочка императивных команд. В SwiftUI возникает парадокс:

🔵Вы добавляете экран логина в состояние навигации.

🔵Но как система узнает, что авторизация завершена?

🔵Как вернуться к исходному «намерению» перейти к заказу после успешного входа?

🔵Где хранить это отложенное намерение, пока показывается логин?

Состояние описывает «что видно», но не «что нужно сделать» и «в какой последовательности». Именно этот разрыв между описанием интерфейса и логикой переходов становится основной болью при построении сложных навигационных потоков в SwiftUI.


Решение - гибридная архитектура:

Вместо попыток заставить SwiftUI делать то, для чего он не предназначен, эффективнее признать: навигация - это системная, платформозависимая задача. И использовать правильный инструмент для каждой части:

🔵Ядро навигации на UIKit: UINavigationController, модальные презентации, кастомные переходы.

🔵Контент экранов на SwiftUI: UIHostingController с SwiftUI вьюхами.

🔵Координаторы на Swift: для бизнес-логики переходов.


// Координатор на 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)
}
}



Почему это работает лучше:

🔵Полный контроль над анимациями и completion handlers.

🔵Единое состояние навигации через UINavigationController.viewControllers.

🔵Кастомные презентации через UIPresentationController.

🔵Глубокая интеграция с системными жестами.


Что SwiftUI делает хорошо:

🔵Быстрое прототипирование простых навигационных сценариев.

🔵NavigationStack для линейных потоков без кастомных компонентов.

🔵Вьюхи внутри экранов (где декларативный подход действительно силен).


🔗 Ссылка на подробную статью


💡 Вывод:

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


➡️ Подписаться на канал
Мобильный трудоголик
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥16👍95🤯1🙏1👀1
🔢 Как писать стабильные тесты для Swift Concurrency кода.

Тестирование многопоточного кода в 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) // Стабильно проходит
}



Преимущества подхода:


🔵Контроль приоритетов: можно проверять, что задачи создаются с правильными приоритетами.

🔵Отслеживание создания: можно вести лог созданных задач для assertions.

🔵Изоляция тестов: каждый тест работает с изолированным моком.

🔵Поддержка detached задач: паттерн легко расширяется для Task.detached.


Важное замечание:

Этот подход не заменяет async/await тесты, а дополняет их. Для кода, который уже использует async функции, лучше тестировать через await. Но для legacy-кода или ситуаций, где Task создается внутри синхронных методов, этот паттерн незаменим.


🔗 Ссылка на подробную статью


💡 Вывод:

Тестирование многопоточного кода требует особого подхода и абстракция TaskProvider предоставляет его. Она превращает плавающие тесты в детерминированные, давая полный контроль над выполнением асинхронных операций. Через инверсию зависимостей сохраняется чистота архитектуры, а случайные падения в CI/CD становятся историей.

Особенно ценен этот паттерн при работе с legacy-кодом, где синхронные и асинхронные вызовы соседствуют в процессе миграции на Swift Concurrency.


➡️ Подписаться на канал
Мобильный трудоголик
Please open Telegram to view this post
VIEW IN TELEGRAM
16👍11🔥41🙏1🤝1
Forwarded from Кот Денисова
👨‍💻 Долгосрочная стратегия в ИТ: почему нельзя гнаться за деньгами.

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


Как работает карьерный рост в ИТ:

Есть естественная последовательность, которую нельзя нарушать:

🔹Знания: изучение технологий и подходов.
🔹Навыки: применение знаний на практике.
🔹Окружение: работа с сильными специалистами.
🔹Ресурсы: достойная оплата и интересные проекты.
🔹Репутация: то, что нельзя купить за деньги.


Если попытаться прыгнуть на четвертый этап, минуя предыдущие, ничего не получится. Без знаний и навыков высокие зарплаты недолговечны.


Плохой пример:

Представьте разработчика, который в 25 лет выбрал компанию, предлагавшую зарплату на 40% выше рыночной. Проект казался стабильным, задачи комфортными. Но через три года оказалось, что:

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

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


Важно для развития:

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


💡 Вывод:

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


➡️ Кот Денисова
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1714😁6🔥2🗿2👀11
🔢 ARC в Swift: как работает автоматическое управление памятью.

Всем привет! Сегодня разберем одну из фундаментальных тем в 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:

🔵Не увеличивает счетчик ссылок.
🔵Автоматически становится nil при удалении объекта.
🔵Безопасны в использовании.

Unowned:

🔵Не увеличивает счетчик ссылок.
🔵Не становятся nil (вызовут краш при обращении к удаленному объекту).
🔵Используются, когда объект гарантированно существует.


🔗 Ссылка на подробную статью


💡 Вывод:

ARC автоматизирует управление памятью, но требует от разработчика понимания работы ссылок. Ключевое правило: weak и unowned ссылки не увеличивают счетчик ссылок, что позволяет разрывать циклы зависимостей. Используйте weak для безопасных связей и unowned только когда уверены в жизненном цикле объектов.

Про внутреннюю механику работы weak-ссылок через Side Tables подробно поговорим в будущих постах.


➡️ Подписаться на канал
Мобильный трудоголик
Please open Telegram to view this post
VIEW IN TELEGRAM
19👍114🙏1🤝1
🔢 Как диагностировать тормозящие List и ForEach в SwiftUI.

Работа со списками в SwiftUI часто превращается в борьбу с производительностью. Казалось бы, простой List или ForEach начинает лагать, приложение потребляет неоправданно много ресурсов, а причина остается неочевидной. Стандартные инструменты профилирования не всегда показывают корень проблемы, особенно когда дело касается динамического создания вьюх. Однако в SwiftUI существует малоизвестный параметр, который может стать вашим первым шагом в поиске проблемы.


Причина возникновения проблемы:

Проблема производительности в List и ForEach часто возникает из-за динамического количества вьюх. Когда SwiftUI не может предсказать, сколько элементов будет отрисовано, или когда структура списка динамически меняется, фреймфорк вынужден использовать более медленные пути рендеринга. Эти «медленные пути» могут существенно влиять на плавность скролла и общую отзывчивость интерфейса.


Параметр -LogForEachSlowPath:

Диагностический параметр -LogForEachSlowPath YES, передается как аргумент запуска приложения, заставляет SwiftUI логировать предупреждения каждый раз, когда обнаруживается неоптимальный сценарий работы с контейнерами списков. Это не флаг производительности, а исключительно инструмент диагностики, он не ускоряет приложение, но показывает, где именно возникают проблемы.


Как это работает:

Когда SwiftUI встречает ForEach или аналогичный контейнер, который производит неконстантное количество View (например, зависит от вычисляемого в рантайме значения), он переключается на менее эффективный режим работы. Активация флага -LogForEachSlowPath включает внутреннее логирование, которое сообщает о каждом таком переходе, указывая конкретное место в коде.


Важно понимать:

Этот параметр лишь первый шаг диагностики. Он показывает симптомы, но не лечит болезнь. Частые причины проблемы:

🔵Динамическое вычисление количества элементов внутри ForEach.

🔵Использование нестабильных идентификаторов (id:).

🔵Сложная логика внутри замыканий ForEach.

🔵Частые изменения структуры данных во время скролла.


💡 Вывод:

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


➡️ Подписаться на канал
Мобильный трудоголик
Please open Telegram to view this post
VIEW IN TELEGRAM
2014👍7🔥21🙏1
🔢 Не создавайте Actor без причины: как избежать архитектурной ошибки.

С появлением Swift Concurrency многие разработчики стали добавлять actor везде, где видят асинхронность. Это как использовать танк для поездки в магазин за хлебом: мощно, но бессмысленно и создает больше проблем, чем решает.

Вот простой и эффективный чек-лист, который позволит определить, нужно использовать actor или нет:


У вас есть состояние, которое не является Sendable?

Sendable - это маркер того, что тип безопасен для использования в разных параллельных контекстах. Если все ваши данные (структуры, простые классы) уже соответствуют Sendable, то первое и важное основание для использования actor просто отсутствует.

🔵Пример не Sendable: класс с изменяемым (var) свойством, которое является ссылочным типом (например, другим не-Sendable классом) и не защищен блокировками.

🔵Пример Sendable: структура (struct) только с let свойствами простых типов (Int, String) или других Sendable типов.

Если у вас либо нет состояния, либо оно уже потокобезопасно (Sendable). actor для его защиты избыточен.


Операции с этим состоянием должны быть атомарными?

Атомарность означает, что промежуточное состояние объекта никогда не будет видно извне. Либо операция выполнена полностью, либо не выполнена совсем.

🔵Пример атомарной операции: списание суммы со счета и ее зачисление на другой счет в банковском приложении. Эти два действия должны быть одним неделимым целым.

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

Если ваши операции можно разбить на независимые шаги или они не требуют такой строгой изоляции, возможно, хватит обычной очереди (DispatchQueue) или async/await с корректным проектированием.


Эти операции не могут быть выполнены на уже существующем actor (например @MainActor)?

Часто проблема решается не созданием нового изолированного острова actor, а правильным использованием существующих.

🔵Проверьте: может ли ваша логика быть изолирована к @MainActor, если она касается UI? Или может ее стоит вынести в @concurrent функцию (Swift 6.2+), чтобы просто запустить в параллельном пуле потоков?

🔵Классическая ошибка: создавать отдельный actor только для того, чтобы выгрузить работу с основного потока. Для этого есть async let, Task.detached или аннотация @concurrent.

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


🔗 Ссылка на подробную статью


💡 Вывод:

Actor - это специальный и достаточно ресурсоемкий механизм, созданный для решения конкретной проблемы: строгой изоляции разделяемого изменяемого состояния. Его не следует применять автоматически для любой фоновой работы.

Прежде чем его использовать, необходимо честно ответить на три ключевых вопроса: есть ли у вас состояние, не являющееся потокобезопасным (Sendable), требуют ли операции с ним атомарности и действительно ли эту работу невозможно выполнить в рамках уже существующего контекста изоляции. Только утвердительные ответы на все три пункта оправдывают введение actor. Во всех остальных сценариях стоит отдавать предпочтение более легким инструментам, таким как async/await, проектирование с Sendable типами, классические DispatchQueue или новая аннотация @concurrent.


➡️ Подписаться на канал
Мобильный трудоголик
Please open Telegram to view this post
VIEW IN TELEGRAM
19👍107🔥2👏1🙏1
🍏 🏋️ 🪟 Мое приложение для планирования и концентрации - TaskFocus.

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

В приложении TaskFocus можно легко создавать задачи, фиксировать заметки, использовать таймер фокусировки с фоновыми звуками и анализировать свою продуктивность с помощью наглядной статистики.


Разработка:

Изначально разработка планировалась под iOS, но для охвата всех пользователей я принял решение выбрать кроссплатформенный фреймворк Flutter. Это позволило выпустить приложение одновременно на iOS, Android, macOS и Windows. Серверную часть для синхронизации данных также разрабатывал самостоятельно на PHP.


Ключевые возможности:

🔵Задач с настройкой отображения списка.
🔵Возможность добавления заметок к задаче.
🔵Удобный поиск по дате и тексту задач или заметок.
🔵Режим фокусировки с выбором фоновых звуков для концентрации.
🔵Детальная статистика по выполнению задач и затраченному времени.
🔵Синхронизация данных и настроек между устройствами.
🔵Экспорт задач и статистики в Excel.
🔵Выбор темы оформления.
🔵Возможность подключения премиум подписки.


📊 На данный момент приложение имеет более 19 тысяч скачиваний в магазинах приложений.

📱 📱 📱 Приложение доступно в магазинах приложений


➡️ Подписаться на канал
Мобильный трудоголик
Please open Telegram to view this post
VIEW IN TELEGRAM
13👍8🔥4👏1👀1
🔢 Swift: как defer наконец-то подружился с асинхронностью.

Иногда самые полезные изменения в языке - не громкие нововведения, а устранение небольших, но раздражающих неудобств. Те, что заставляют писать лишний код, нарушать логику или искать обходные пути. Именно таким изменением стало принятие 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 сохраняется, но теперь с поддержкой асинхронности:

🔵Гарантия выполнения: блок defer выполнится при любом выходе из функции (успех, ошибка, возврат).

🔵Ожидание завершения: если внутри defer есть await, функция дождется его завершения, прежде чем вернуть управление.

🔵Ограничение: использовать await в defer можно только внутри async-контекста.


Почему это важно:

Раньше для асинхронной очистки приходилось либо дублировать код на всех путях выхода, либо использовать Task { }, который не гарантировал завершения операции. Теперь очистка ресурсов (закрытие сетевых соединений, сброс состояния, отмена операций) становится такой же простой и надежной, как и в синхронном коде.


🔗 Ссылка на документацию


💡 Вывод:

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


➡️ Подписаться на канал
Мобильный трудоголик
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1913🔥42👏1🙏1👀1
This media is not supported in your browser
VIEW IN TELEGRAM
🔢 iOS 26: официальный способ прикрепить плавающие View над TabBar.

Многие годы разработчики 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
🔥1710👍31🙏1👀1
🔨 Xcode - это не среда разработки, это квест на выживание.

Существует инструмент, который ежедневно определяет границы возможного для миллионов разработчиков. И часто эти границы оказываются не там, где мы ожидаем. Речь не об аппаратных ограничениях или сложности алгоритмов, а о среде, в которой рождаются приложения для экосистемы 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
👍179👀4🗿2🔥1🤯1
🔢 Захват ссылок в Swift: когда [weak self] становится ловушкой.

Всем привет! Наткнулся на интересную статью об отладке памяти в iOS. Автор разбирает реальный кейс, где мелкий UI-баг оказался верхушкой айсберга серьезной проблемы.

С чего все началось:

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


Расследование по шагам:

Автор методично искал причину. Сначала: классические print-ы по всему стеку вызовов. Оказалось, что обработчик deep link вызывался дважды, и что важнее: от двух разных экземпляров координатора в памяти.

Ключевая зацепка: проблема воспроизводилась только после выхода и повторного входа в аккаунт. Это намекало, что старые объекты не убирались из памяти.


Масштаб проблемы:

Используя Memory Graph Debugger, автор обнаружил шокирующую вещь: в системе оставались жить целые копии части приложения, со всеми координаторами, сервисами и вью-моделями. Каждый перелогин добавлял новый слой в память.


Найденный виновник:

Проблема скрывалась в, казалось бы, корректном коде:


.sink(receiveValue: {
Task { [weak self] in // weak здесь не спасает!
// логика
}
})


Ошибка была в том, что [weak self] стоял во внутреннем замыкании Task, в то время как внешнее замыкание .sink захватывало self сильно по умолчанию.


Решение в одну строку:

Исправление оказалось минимальным, но критически важным:


.sink(receiveValue: { [weak self] in // weak должен быть здесь!
Task {
// логика
}
})



💡 Вывод:

Эта история наглядно показывает, как механическая, но невнимательная расстановка [weak self] может создать иллюзию безопасности, скрывая реальную утечку памяти. Мы часто действуем по шаблону, добавляя слабый захват в асинхронные блоки, забывая, что корень проблемы может находиться уровнем выше - в замыкании, которое инициирует эту асинхронную операцию. Ключевой урок здесь в том, что борьба с retain cycles требует не ритуала, а понимания полного контекста захвата, особенно при работе с цепочками и вложенными замыканиями.


➡️ Подписаться на канал
Мобильный трудоголик
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥20115🤝2👍1🙏1