Forwarded from Кот Денисова
Всем привет! Сегодня поговорим про собеседования в ИТ - это болезненный и несовершенный процессов. Каждая компания изобретает собственный велосипед, состоящий из хаотичных этапов, непрозрачных критериев и субъективных оценок. В результате кандидаты проходят 5 кругов ада, тратя нервы и время, а компании месяцами не могут закрыть вакансии, теряя сильных специалистов из-за выгорания в процессе найма.
Успех на собеседовании сегодня зависит не столько от реальных компетенций, сколько от умения «продать» себя и удачно угадать, что хочет услышать интервьюер. Абсурдность ситуации достигает пика, когда фронтендера заставляют решать задачи по алгоритмам сортировки, которые он никогда не применял в реальной работе.
Но есть и хорошие новости. Крупные игроки рынка начали осознавать проблему и пересматривать свой подход к найму разработчиков. Вот ключевые изменения, которые стоит отметить:
Устранение дублирования.
Раньше кандидат мог по 3-4 раза проходить одинаковые технические собеседования в разные команды. Теперь введена единая техническая секция, ее результат засчитывается при рассмотрении в любую команду. Это простое решение экономит недели времени для всех участников процесса.
Задачи, приближенные к реальности.
Вместо абстрактных алгоритмических головоломок кандидаты теперь решают прикладные задачи. Например, бэкендеру могут предложить спроектировать API для микросервиса или оптимизировать запрос к базе данных - это то, с чем он сталкивается ежедневно в работе.
Смещение фокуса на архитектурные навыки.
Важно не просто написать работающий код, а объяснить, как он поведет себя под нагрузкой, какие ресурсы потребует и как его масштабировать. Это гораздо ценнее знания специфических фреймворков, которые можно быстро изучить на проекте.
Мое мнение:
Эти изменения не просто «снижение планки». Это признак зрелости рынка. Компании наконец-то поняли, что эффективный найм - это не фильтрация через сито с заведомо мелкими ячейками, а поиск специалистов, релевантных реальным задачам.
Я сам сталкивался с ситуациями, когда сильного разработчика разворачивали из-за неудачи в задаче, которая не имеет отношения к реальным, рабочим задачам. Теперь такие кейсы должны уйти в прошлое.
Что ждет нас дальше?
Очевидно, что тренд на гуманизацию собеседований будет нарастать. Уже сейчас в некоторых компаниях:
Кандидаты становятся более разборчивыми и компании вынуждены подстраиваться, делая процесс найма комфортным и предсказуемым.
Остается надеяться, что вскоре мы окончательно избавимся от архаичных практик вроде заучивания алгоритмов и перейдем к содержательному диалогу о реальных навыках и опыте.
Эти изменения - важный шаг к адекватной оценке разработчиков. Постепенно рынок отказывается от абстрактных задач в пользу реальных навыков. Остается надеяться, что этот тренд станет новым стандартом для всей индустрии.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍15❤9🔥3👀2🤔1 1
Привет! Сегодня разберем паттерн Abstract Factory (Абстрактная фабрика) - мощный инструмент для создания семейств связанных объектов без привязки к конкретным типам.
Зачем это нужно:
Представьте, что вы разрабатываете приложение, которое должно работать с разными темами оформления (светлая/темная) или разными базами данных (SQLite/Realm). Abstract Factory позволяет создавать согласованные наборы объектов, которые гарантированно работают вместе.
Как это работает в Swift:
В отличие от Java/C++, в Swift нет абстрактных классов. Но мы можем использовать протоколы для той же цели.
Пример:
Абстрактные товары:
protocol Coffee {
func brew()
}
protocol Tea {
func steep()
}Конкретные товары:
// Для итальянской кофейни
struct Espresso: Coffee {
func brew() {
print("Варим эспрессо")
}
}
struct ItalianTea: Tea {
func steep() {
print("Завариваем итальянский травяной чай")
}
}
// Для английской кофейни
struct Cappuccino: Coffee {
func brew() {
print("Готовим капучино")
}
}
struct EnglishTea: Tea {
func steep() {
print("Завариваем английский черный чай")
}
}
Абстрактная фабрика:
protocol CafeFactory {
func makeCoffee() -> Coffee
func makeTea() -> Tea
}Конкретные фабрики:
struct ItalianCafeFactory: CafeFactory {
func makeCoffee() -> Coffee {
Espresso()
}
func makeTea() -> Tea {
ItalianTea()
}
}
struct EnglishCafeFactory: CafeFactory {
func makeCoffee() -> Coffee {
Cappuccino()
}
func makeTea() -> Tea {
EnglishTea()
}
}Использование фабрики:
final class CafeOrder {
private let factory: CafeFactory
init(factory: CafeFactory) {
self.factory = factory
}
func prepareCoffeeOrder() {
let coffee = factory.makeCoffee()
coffee.brew()
}
func prepareTeaOrder() {
let tea = factory.makeTea()
tea.steep()
}
}
let italianOrder = CafeOrder(factory: ItalianCafeFactory())
italianOrder.prepareCoffeeOrder()
italianOrder.prepareTeaOrder()
let englishOrder = CafeOrder(factory: EnglishCafeFactory())
englishOrder.prepareCoffeeOrder()
englishOrder.prepareTeaOrder()Преимущества:
🔹 Изоляция кода от конкретных классов.
🔹 Гарантия согласованности объектов.
🔹 Легкое добавление новых вариаций.
🔹 Упрощение тестирования.
Когда не стоит использовать:
🔹 Если у вас только один вариант продуктов.
🔹 Когда семейства объектов часто меняются.
🔹 В простых приложениях без сложной архитектуры.
Abstract Factory - отличный выбор для сложных приложений, где важна согласованность и гибкость архитектуры. Главное преимущество: способность легко добавлять новые семейства объектов без изменения существующего кода.
Мобильный трудоголик
Please open Telegram to view this post
VIEW IN TELEGRAM
Всем привет! Месяц назад 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 denoscription = String(describing: user) // 4 проверки протоколов!
Приведение типов
Операции as? при работе с протоколами оказались особенно затратными:
// Такой код может быть затратнее, чем кажется
if let convertible = value as? CustomStringConvertible {
print(convertible.denoscription)
}
// Приведение к конкретному классу работает значительно быстрее
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.noscript)
.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!")
noscriptLabel.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