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

Компания Apple анонсировала новый Retention Messaging API, который поможет разработчикам снизить отток подписчиков. Теперь можно гибко настраивать сообщения для пользователей, которые хотят отменить подписку.


⚠️ Что это дает?

С помощью данного API можно показывать 4 типа сообщений:

🔹 Текстовое: напоминание о преимуществах подписки.
🔹 Текст + изображение: визуальное убеждение.
🔹 Смена тарифа: предложение перейти на другой тарифный план.
🔹 Промо-оффер: скидка или спецусловия.

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


🤔 Как это работает?

🔸 Пользователь нажимает «Отменить подписку».
🔸 Система запрашивает у вашего сервера подходящее сообщение (или использует стандартное).
🔸 Пользователь видит предложение: оставить подписку, перейти на другой тариф или воспользоваться скидкой.


👨‍💻 Как подключить?

🔹 Загрузите тексты и изображения в App Store Connect.
🔹 Настройте дефолтные сообщения (текст, либо текст + картинка).
🔹 Реализуйте серверный endpoint для динамического выбора сообщений.


🔗 Официальная документация на сайте Apple


💡 Вывод:

Retention Messaging API — это не просто новая фича, а стратегический инструмент для монетизации. Вместо пассивного наблюдения за оттоком подписчиков вы получаете:

🔸 Контроль над последним touchpoint перед отменой.
🔸 Данные для анализа поведения пользователей.
🔸 Рычаг влияния на ключевое бизнес-метрики.

Теперь ваша реакция на отток — не постфактум, а в момент принятия пользователем решения.


➡️ Подписаться на канал
Мобильный трудоголик
Please open Telegram to view this post
VIEW IN TELEGRAM
26🔥19👍83
Очень интересный канал про IOS разработку 👉 @error_nil, в основном все в формате видеоуроков, много полезного материала, интересные обсуждения, XCode проекты и тд, советую подписаться 👍
👍104😁3🔥1🤔1
🔢 XCTest уходит в прошлое? Разбираем плюсы и минусы Swift Testing.

С выходом Swift Testing многие задумались о миграции с XCTest. Но так ли всё гладко, как обещает Apple? Давайте разберём реальные подводные камни и лайфхаки для перехода.


🤔 Чем Swift Testing отличается от XCTest?

1️⃣ Обнаружение тестов.
🔹 XCTest: использует XCTestCase + Objective-C runtime.
🔹 Swift Testing: макросы @Test + Swift ориентированный подход.

2️⃣ Выполнение тестов.
🔹 XCTest: выполняется последовательно (один за другим).
🔹 Swift Testing: выполняется параллельно (каждый тест в отдельной Task).

3️⃣ Синтаксис.
Разница в коде:

// XCTest  
func testLogin() { XCTAssertEqual(result, expected) }

// Swift Testing
@Test func login() { #expect(result == expected) }



⚠️ Главные проблемы миграции.

1️⃣ Ассерты вне контекста Task.
Swift Testing требует, чтобы проверки (#expect) выполнялись только внутри Task.

// Старая схема (XCTest)
func testAsync() {
DispatchQueue.main.async {
XCTAssertEqual(result, 42)
}
}

// Неправильная миграция (упадёт в Swift Testing)
@Test func asyncTest() {
DispatchQueue.main.async {
#expect(result == 42) // Ошибка: вне контекста Task
}
}

// Правильная миграция
@Test func asyncTest() async { // 1. Объявляем async тест
let result = await withCheckedContinuation { continuation in
DispatchQueue.main.async {
continuation.resume(returning: 42) // 2. Возвращаем результат в контекст Task
}
}

#expect(result == 42) // 3. Проверяем УЖЕ внутри Task-контекста
}


Ключевые моменты:
🔸 Тест должен быть async — это автоматически создаёт контекст Task.
🔸 Все асинхронные операции, которые не поддерживают async/await оборачиваются в withCheckedContinuation.
🔸 #expect вызывается после получения результата, но внутри области теста.

2️⃣ Проблемы с общим состоянием.
Параллельное выполнение тестов могут вызвать гонки данных.

♣️ Пример:

actor Counter {
var count = 0

func increment() {
count += 1
}
}

@Test func testA() async {
await Counter.shared.increment()
#expect(await Counter.shared.count == 1) // Может упасть, если testB запустится раньше!
}

@Test func testB() async {
await Counter.shared.increment()
}


Решение:
Использование @Suite(.serialized) для последовательного выполнения:

@Suite(.serialized)
struct CriticalTests {
@Test func testA() {
}

@Test func testB() {
}
}



3️⃣ Наследование тестов не работает.
В XCTest можно было наследовать XCTestCase и автоматически получать все тесты родителя. В Swift Testing так не работает — тесты нужно дублировать.

Что делать:
🔸 Для простых сценариев использовать параметризованные тесты.
🔸 Для сложных, пока оставьте XCTest и мигрируйте постепенно.


💡 Вывод:
На Swift Testing стоит переходить если у вас современный проект, который использует Swift Concurrency и вам необходимо параллельное выполнение тестов.

Мой совет: Начинайте с гибридного подхода — подключайте Swift Testing для новых модулей, а старые тесты оставляйте в XCTest.


➡️ Подписаться на канал
Мобильный трудоголик
Please open Telegram to view this post
VIEW IN TELEGRAM
26👍193🔥2
👨‍💻 Пять фатальных ошибок в карьере разработчика.

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


1️⃣ Думать вместо делать.
«Сначала изучу весь SwiftUI, потом начну писать код» — знакомо?

Вспоминаю, как сам начинал: вместо того чтобы бесконечно изучать теорию, я просто пробовал писать код. Да, он был кривой, да, с ошибками, но это работало! Сейчас многие застревают на этапе «сначала выучу всё идеально, потом начну». Так не бывает. Лучший способ научиться: делать, ошибаться и исправлять.


2️⃣ Слушать не тех людей.
«Мой друг-джун сказал, что VIPER — это мусор».

Когда я только начинал, один «опытный» знакомый уверял меня, что Swift — это модно только для мажоров, а за Java будущее и он будет использоваться везде вечно. Хорошо, что я не поверил. Часто советы дают те, кто сам ещё мало что понимает. Ищите советов среди тех, кто реально добился результатов в вашей сфере.


3️⃣ Бояться критики.
«Мой код идеален, это тимлид ничего не понимает!»

Да, неприятно, когда говорят, что твой код плох. Но именно так и растёшь! Я научился воспринимать замечания не как личное оскорбление, а как бесплатный урок. Заводил даже отдельный блокнот, куда записывал все конструктивные замечания — очень помогает не наступать на те же грабли.


4️⃣ Лениться и откладывать на потом.
«Сегодня устал, завтра начну».

Сколько раз вы откладывали изучение новой технологии, потому что «еще есть время»? Или обещали себе дописать пет проект, но в итоге смотрели сериалы? Я сам через это проходил. Правда в том, что идеального момента не будет никогда. Каждый отложенный месяц — это упущенные возможности, проекты и даже зарплата. Технологии меняются быстрее, чем мы собираемся их изучить.


5️⃣ Бояться перемен.
«Лучше синица в руках, чем журавль в небе. Останусь там, где хоть что-то платят».

🔹 Страх сменить проект: «а если новый окажется хуже».
🔹 Боязнь попросить повышение: «а вдруг откажут».
🔹 Страх откликнуться на интересную вакансию: «там же нужно будет учить новое!».

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

Эти страхи кажутся разумными только в нашей голове. Со стороны они выглядят именно тем, чем и являются — отговорками, которые мешают нам расти.

В IT безопаснее прыгнуть в неизвестность, чем оставаться в зоне комфорта. Каждый раз, когда вы говорите «еще не готов», кто-то другой говорит «да» и получает ваши потенциальные бенефиты.


💡 Вывод:
Главное, что я понял: карьера строится не на гениальных озарениях, а на постоянных итерациях: попробовал, получил фидбек, исправил, повторил. Чем чаще этот цикл, тем быстрее рост.


➡️ Подписаться на канал
Мобильный трудоголик
Please open Telegram to view this post
VIEW IN TELEGRAM
26👍16👀2🔥1🗿11
🎨 Вышли официальные наборы компонентов дизайна iOS 26 и iPadOS 26.

Все обновленные компоненты и гайды доступны на сайте Apple.


🤔 Что внутри?

🔹 UI Kit (Figma, Sketch).
🔹 App Icon Template (Sketch, Photoshop, Illustrator, Figma).


➡️ Подписаться на канал
Мобильный трудоголик
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥18👍3👀1🗿11
🔢 Современные механизмы блокировок в Swift: Mutex и Synchronization Framework.

В Swift существует несколько способов защиты данных от гонки (data races): NSLock, DispatchSemaphore, последовательные очереди (DispatchQueue). Но на WWDC24 компания Apple представила новый Synchronization Framework, включающий современный Mutex – стандартизированную блокировку взаимного исключения.


🤔 В чем разница между Lock и Mutex?
🔸 Lock: общее название механизмов синхронизации (например, NSLock, os_unfair_lock).
🔸 Mutex: частный случай блокировки, гарантирующий, что только один поток может владеть ресурсом в данный момент.


Ключевая особенность Mutex:
🔹 Строгое владение – разблокировать может только тот поток, который заблокировал.
🔹 Поддержка Sendable, что делает его удобным для Swift Concurrency.


♣️ Пример использования.
Пример защиты счетчика:


final class Counter {
private let count = Mutex<Int>(0) // Обернули значение в Mutex

var currentCount: Int {
count.withLock { $0 } // Безопасное чтение
}

func increment() {
count.withLock { $0 += 1 } // Атомарная операция
}

func decrement() throws {
try count.withLock {
guard $0 > 0 else { throw Error.reachedZero }
$0 -= 1
}
}
}



🆚 Mutex vs Actor.
🔸 Actor: идеален для асинхронного доступа, но требует await.
🔸 Mutex: синхронный, подходит для legacy кода или когда нельзя использовать async/await.


⚠️ Когда выбирать Mutex?
🔸 Нужна мгновенная синхронная блокировка.
🔸 Работа с не Sendable типами (например NSBezierPath).
🔸 Интеграция с кодом, не поддерживающим Concurrency.


💡 Важно:
🔹 Доступен с iOS 18 и macOS 15.
🔹 Не заменяет Actor, а дополняет инструментарий для работы с многопоточностью.

Mutex из Synchronization Framework – это современный, безопасный и эффективный способ защиты данных в Swift.


➡️ Подписаться на канал
Мобильный трудоголик
Please open Telegram to view this post
VIEW IN TELEGRAM
👍21🔥1141🗿11
🔢 Принципы SOLID в Swift: полный гид с примерами.

Сегодня мы разберём 5 принципов SOLID, которые спасут ваш код от хаоса.

🤔 Что такое SOLID?

Это 5 принципов объектно-ориентированного дизайна, которые делают код:
🔸 Гибким к изменениям.
🔸 Понятным для других разработчиков.
🔸 Более тестируемым.

Акроним расшифровывается так:

1️⃣ S — Single Responsibility.

Один класс — одна зона ответственности. Пример:

// Неправильно. Класс делает все: логика, UI, сетевые запросы.
class ProfileViewController: UIViewController {
func loadUserData() {
URLSession.shared.dataTask(...) { [weak self] data in
let user = decode(data)
self?.nameLabel.text = user.name
self?.avatarImageView.image = UIImage(data: user.avatarData)
}.resume()
}
}

// Правильно. Разделяем ответственность.
class UserLoader {
func fetchUser() async throws -> User {
// Запросы к бд или серверу и логика обработки данных
}
}

class ProfileViewController: UIViewController {
private let loader = UserLoader()

func updateUI() async {
let user = try? await loader.fetchUser()
if let user {
nameLabel.text = user.name
avatarImageView.image = UIImage(data: user.avatarData)
}
}
}


2️⃣ O — Open-Closed.

Класс открыт для расширения, но закрыт для изменений. Пример:

// Неправильно. При добавлении нового типа ячейки придётся менять метод.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let item = data[indexPath.row]
if item is Photo {
return PhotoCell()
} else if item is Video {
return VideoCell() // Добавили новое условие
}
}

// Правильно. Один из вариантов правильного решения - решение через протокол. Новый тип ячейки = новая сущность, а не правки в коде.
protocol CellConfigurable {
static var reuseID: String { get }
func configure(with item: Any)
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let item = data[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: type(of: item).reuseID)
(cell as? CellConfigurable)?.configure(with: item)
return cell
}


3️⃣ L — Liskov Substitution.

Подклассы должны заменять родительские классы. Пример:

// Неправильно
class Bird {
func fly() {
print("Лететь!")
}
}

class Penguin: Bird {
override func fly() {
fatalError("Пингвины не летают!") // Ошибка
}
}

// Правильно
protocol Bird {
func move()
}

class Sparrow: Bird {
func move() {
print("Лететь!")
}
}

class Penguin: Bird {
func move() {
print("Плыть!")
}
}


4️⃣ I — Interface Segregation.

Клиенты не должны зависеть от интерфейсов, которые они не используют. Пример:

// Неправильно
protocol TableViewDelegate {
func didSelectRow(at indexPath: IndexPath)
func heightForRow(at indexPath: IndexPath) -> CGFloat
func willDisplayCell()
// ... 20+ методов
}

// Правильно: разделяем на мелкие протоколы, которые можно реализовывать при необходимости.
protocol TableViewSelectable {
func didSelectRow(at indexPath: IndexPath)
}

protocol TableViewSizable {
func heightForRow(at indexPath: IndexPath) -> CGFloat
}


5️⃣ D — Dependency Inversion.

Зависите от абстракций, а не от конкретных типов. Пример:

// Неправильно. Прямая зависимость от Firebase.
class AnalyticsService {
private let firebase = FirebaseAnalytics()

func track(event: String) {
firebase.log(event)
}
}

// Правильно. Гибкое решение.
protocol AnalyticsEngine {
func log(event: String)
}

class AnalyticsService {
private let engine: AnalyticsEngine // Зависим от протокола

init(engine: AnalyticsEngine) {
self.engine = engine
}

func track(event: String) {
engine.log(event)
}
}



💡 Вывод:
SOLID не просто теория, а практический инструмент для создания профессионального кода. Он требует больше времени на старте, но окупается с лихвой, так как упростит масштабируемость и тестирование проекта, позволит любому разработчику быстрее вникнуть в ваш код. Это как строить дом с фундаментом вместо хижины на песке.


➡️ Подписаться на канал
Мобильный трудоголик
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1814🔥111
👨‍💻 Тестировщики в мобильной разработке: почему без них не обойтись даже в стартапах.

Когда в команде появляется тестировщик, некоторые разработчики думают: «Ну вот, теперь будут придираться к каждой мелочи». Но на самом деле хороший QA — это не враг, а ваш главный союзник в создании качественного приложения и сейчас я расскажу почему.


1️⃣ Разработчик ≠ тестировщик.

Мы, разработчики, часто смотрим на код через призму «Как это должно работать», а тестировщики — через «Как это может сломаться».

Пример:
Вы добавили красивый анимационный переход между экранами. Всё работает на iPhone 15 Pro. Но тестировщик проверяет на старом iPhone 8 и обнаруживает, что анимация лагает. Без него этот баг мог бы улететь в прод.


2️⃣ Они экономят деньги компании.

Один пропущенный критический баг может:
🔹 Испортить первый опыт пользователей (и они удалят приложение).
🔹 Привести к отказу от подписки (если что-то сломалось в платежах).
🔹 Потребовать хотфиксов (а это внеурочные работы для разработчиков).

По данным исследований, исправление бага на этапе тестирования может быть в 5-10 раз дешевле, чем после релиза.


3️⃣ Они видят то, что вы не замечаете.

Разработчик проверяет «работает ли фича», а тестировщик — «можно ли её сломать».

Пример:
🔹 Приложение падает, если сменить язык системы во время загрузки данных.
🔹 Кнопка «Купить подписку» не нажимается, если нажать её слишком быстро.
🔹 На iPad интерфейс «поехал» из-за неправильных констрейнтов.


4️⃣ Они помогают улучшить UX.

Хороший тестировщик не просто ищет баги — он оценивает удобство использования.

Пример:
🔹 «Эта модалка появляется слишком часто — пользователи будут раздражаться»
🔹 «Нет индикатора загрузки — люди думают, что приложение зависло».
🔹 «Текст кнопки слишком длинный и обрезается на маленьких экранах».


5️⃣ Даже в маленькой команде они нужны.

Многие стартапы думают: «Мы же agile, сами всё проверим». Но:
🔹 Разработчики не успевают тестировать всё вручную.
🔹 Автоматические тесты не покрывают все сценарии.
🔹 Без QA страдает качество, а плохие отзывы в App Store снижают конверсию.


⚠️ Как правильно работать с тестировщиками?

🔸 Не воспринимайте замечания как личную критику: это про продукт, не про вас.
🔸 Всегда запрашивайте детали: если баг сложно воспроизвести, попросите скриншоты/видео.
🔸 Цените их мнение: иногда они предлагают реально полезные доработки UX.


💡 Вывод:

Тестировщик — это не «лишний человек» в команде, а гарантия качества. Чем раньше он найдёт проблему, тем дешевле её исправить.


➡️ Подписаться на канал
Мобильный трудоголик
Please open Telegram to view this post
VIEW IN TELEGRAM
22👍15🔥4🫡11
🔢 Паттерны проектирования в Swift: Синглтон.

Синглтон (Singleton) — это паттерн проектирования, который гарантирует, что у класса будет только один экземпляр на протяжении всего жизненного цикла приложения, и предоставляет к нему глобальную точку доступа.

Данный паттерн является одним из самых популярных и одновременно спорных паттернов в iOS разработке.

Вот как это работает на практике:
Когда вы создаёте синглтон, вы делаете его конструктор приватным, чтобы предотвратить создание дополнительных экземпляров извне, а единственный экземпляр храните в статическом свойстве. Swift автоматически обеспечивает ленивую инициализацию — объект создаётся только при первом обращении к этому свойству.

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


final class AppConfig {
static let shared = AppConfig()
private init() {}

var apiKey: String = "12345"
}


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

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


protocol ConfigService {
var apiKey: String { get }
}

final class ProdConfig: ConfigService {
static let shared = ProdConfig()
private init() {}

let apiKey = "prod_12345"
}

class TestConfig: ConfigService {
let apiKey = "test_67890"
}


Теперь в проде используем ProdConfig.shared, а в тестах можем подсунуть объект TestConfig.


⚠️ Главная проблема синглтонов:
Они создают неявные зависимости. Представьте, у вас 20 классов, использующих AppConfig.shared. Вдруг понадобится сделать разные конфиги для разных пользователей. Придется переписывать половину приложения.

Поэтому я использую синглтоны только для логгеров и аналитики. Для всего остального лучше подходит dependency injection, потому что это делает зависимости прозрачными и даёт гибкость в управлении объектами.

♣️ Пример:


protocol APIServiceProtocol {
func fetchData()
}

class APIService: APIServiceProtocol {
func fetchData() {
}
}

class DataManager {
let apiService: APIServiceProtocol

init(apiService: APIServiceProtocol) {
self.apiService = apiService
}

func loadData() {
apiService.fetchData()
}
}



💡 Вывод:
Синглтон — мощный инструмент, но при неправильном использовании может принести больше вреда, чем пользы. Главная его опасность в том, что он создаёт скрытые зависимости, которые со временем превращают код в запутанный клубок. Лучше всего его использовать только для объектов, которые действительно должны существовать в единственном экземпляре.


➡️ Подписаться на канал
Мобильный трудоголик
Please open Telegram to view this post
VIEW IN TELEGRAM
17🔥13👍2🤝11
🔢 SwiftUI: как отследить, что вызывает обновление View.

Когда View в SwiftUI неожиданно перерисовывается, это может сбивать с толку. Но есть простой способ понять, что именно вызвало обновление.


♣️ Добавьте в body вашей View:


var body: some View {
let _ = Self._printChanges()
// Ваш код
}


Данный код отобразит в консоли:

🔸 Какое свойство (@State, @ObservedObject и т.д.) вызвало обновление View.
🔸 Было ли обновление из-за изменения самой вьюхи.
🔸 Была ли смена её идентичности (@identity).


⚠️ Альтернативные способы отладки.

1️⃣ onAppear + print.


Text("Привет")
.onAppear {
print("View появилась")
}



2️⃣ Кастомный модификатор для вывода в консоль.


extension View {
func debugPrint(_ text: String) -> Self {
print(text)
return self
}
}

// Использование:
Circle()
.fill(.green)
.debugPrint("Круг отрисован")



💡 Вывод:

🔹 Self._printChanges() — лучший способ понять, почему перерисовывается View.
🔹 onAppear и кастомные модификаторы помогут отследить жизненный цикл вью.

Вот так просто можно понять, почему вьюхи в SwiftUI вдруг решили перерисоваться.


➡️ Подписаться на канал
Мобильный трудоголик
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1914🔥22👀1
👨‍💻 Неамбициозные разработчики — удобно, но смертельно для карьеры.

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

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


Почему стратегия «сиди и не высовывайся» проигрышная.

1️⃣ Знания устаревают — цена на рынке падает.
🔹 Сегодня SwiftUI и async/await — стандарт, а GCD уже называют «устаревшим».
🔹 Ценят боевой опыт, который принёс бизнесу деньги, а не пет проекты из 2018 года.

2️⃣ Зарплата и инфляция навыков.
Комфортная зона — это ловушка. Вот что происходит с зарплатой, когда ты отказываешься от роста:
🔹 Если не расти внутри компании, через 2-3 года окажешься ниже рынка.
🔹 Твой текущий стек дешевеет на 15-25% в год, пока рынок требует новых компетенций.
🔹 Сначала ты отказываешься от повышения — «мне и так норм». Потом компания замораживает индексацию — «кризис, потерпите».
🔹 HR давно ведут зарплатные матрицы. Твой «стабильный» грейд через 3 года сравняется с позицией джуниора.

3️⃣ Невидимость = риск.
🔹 ИТ — это эскалатор: если не идёшь вверх — скатываешься вниз.
🔹 Компании-лидеры рынка берут тех, кто решает сложные задачи, а не «стабильно тормозит».


🤔 Что же делать?
🔸 Разработчикам: постоянно прокачивайте экспертизу и смежные навыки.
🔸 Менеджерам: поощряйте рост — иначе через пару лет команда не сможет конкурировать.


💡 Вывод:
Выбор прост: остаться «удобным» и зависеть от настроений менеджмента или расти и диктовать свои условия.

ИТ любит тех, кто не стоит на месте!


➡️ Подписаться на канал
Мобильный трудоголик
Please open Telegram to view this post
VIEW IN TELEGRAM
🗿26👍1841🤔1👀1
🔢 Retain Cycle в Swift: как не попасть в тонкую ловушку.

Retain cycle — одна из самых коварных проблем в Swift. Даже опытные разработчики могут не заметить, как создают цикл удержания ссылок, который мешает освобождению памяти. Разберём пример, который выглядит безобидно, но содержит скрытую угрозу.


⚠️ Что такое Retain cycle?
Retain Cycle (цикл удержания сильных ссылок) в Swift — это ситуация, когда два или более объекта удерживают друг друга через сильные ссылки (strong), не позволяя ARC (Automatic Reference Counting) освободить память. Это приводит к утечкам памяти, так как объекты никогда не освобождаются.


Проблемный код.


class MyClass {
var task: Task<Void, Never>?

init() {
task = Task { [weak self] in
guard let self else { return }

repeat {
self.performSomeWork() // <- Цикл удержания!
} while !Task.isCancelled
}
}

func performSomeWork() {
}

deinit {
print("deinit") // Не выполнится
task?.cancel()
}
}



🤔 Что не так?
🔸 guard let self создаёт сильную ссылку на весь блок Task.
🔸 Цикл repeat работает бесконечно, удерживая selfdeinit никогда не вызовется.


Как исправить?

1️⃣ Изменить область видимости сильной ссылки.
Перенесите guard внутрь цикла, чтобы ссылка освобождалась после каждой итерации:


task = Task { [weak self] in
repeat {
guard let self else { return }
self.performSomeWork() // Сильная ссылка только здесь
} while !Task.isCancelled
}


2️⃣ Использовать слабую ссылку везде.
Если метод не требует обязательного self:


task = Task { [weak self] in
repeat {
self?.performSomeWork() // слабая ссылка
} while !Task.isCancelled
}


3️⃣ Вручную разорвать цикл.
Вызовите cancel() вне deinit, например, при закрытии экрана:


task?.cancel()
task = nil



⚠️ Чего лучше избегать?
Явный захват метода performSomeWork:


task = Task { [performSomeWork] in
repeat {
performSomeWork() // Метод всё равно содержит неявный self
} while !Task.isCancelled
}


Методы экземпляра неявно захватывают self, даже если переданы как замыкание.


🤔 Как отлавливать такие проблемы?
🔸 Тесты: Добавьте проверку на deinit с задержкой.
🔸 Instruments: Используйте Leaks и Allocations для поиска утечки памяти и цикличных ссылок.
🔸 Логирование: Добавьте print в deinit для критичных объектов.


💡 Вывод:
🔹 [weak self] — не панацея. Всегда анализируйте область видимости сильных ссылок.
🔹 Для бесконечных задач (repeat, while) используйте локальный guard или явный cancel.
🔹 Пишите тесты на освобождение памяти — это сэкономит часы отладки.


➡️ Подписаться на канал
Мобильный трудоголик
Please open Telegram to view this post
VIEW IN TELEGRAM
👍32🔥203🙏22😁1
🔢 Бекпорт onChange с oldValue для iOS 16 в SwiftUI.

Всем привет! Сегодня поделюсь полезным расширением для тех, кто поддерживает iOS 16 в своих проектах. В iOS 17 компания Apple добавила улучшенную версию onChange с доступом к старому значению, но как быть, если нужно поддерживать и более ранние версии?

Решение: backport-реализация, о которой расскажу ниже:


♣️ Создадим совместимый вариант valueChanged:


public extension View {
@available(iOS, deprecated: 17.0, message: "Используйте нативный onChange в iOS 17+")
@ViewBuilder func valueChanged<V>(
of value: V,
handler: @escaping (_ previous: V, _ current: V) -> Void
) -> some View where V: Equatable {
if #available(iOS 17, *) {
onChange(of: value, handler)
} else {
onChange(of: value) { [value] newValue in
handler(value, newValue)
}
}
}
}



🤔 Как это работает?

🔸 На iOS 17+ использует нативный onChange.
🔸 На iOS 16 эмулирует поведение через захват текущего значения.
🔸 Сохраняет одинаковый интерфейс для всех версий.


Добавим дополнительную возможность: отслеживание переходов.

Создадим метод для отслеживания смены конкретного значения:


public extension View {
func trackTransition<V>(
of value: V,
from initialValue: V,
action: @escaping () -> Void
) -> some View where V: Equatable {
valueChanged(of: value) { old, new in
if old == initialValue && new != initialValue {
action()
}
}
}
}



♣️ Пример использования:


struct ContentView: View {
@State private var score: Int = 0

var body: some View {
VStack {
Text("Score: \(score)")
Button("Increase") { score += 1 }
}
.valueChanged(of: score) { old, new in
print("Score changed from \(old) to \(new)")
}
.trackTransition(of: score, from: 0) {
print("Score started changing from zero!")
}
}
}



Главное преимущество этого подхода - единый код для всех поддерживаемых версий iOS без дублирования логики.


➡️ Подписаться на канал
Мобильный трудоголик
Please open Telegram to view this post
VIEW IN TELEGRAM
28👍192🔥1🙏1
🔢 SwiftUI-Adapter: как поддерживать новые фичи SwiftUI и не терять совместимость.

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

Недавно я наткнулся на удобную Android-библиотеку, которая упрощает работу с разными версиями API и подумал: «Почему бы не сделать что-то подобное для SwiftUI?».

После этого родилась идея разработать инструмент, который избавит вас от бесконечных проверок #available и сделает код чище:

🔗 Ссылка на GitHub


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

Каждый раз, когда Apple выпускает новый модификатор в SwiftUI, нам приходится писать такие конструкции:

if #available(iOS 15.0, macOS 12.0, *) {
YourView()
.badge(5)
} else {
YourView()
}


SwiftUI-Adapter делает эту рутину за вас! Просто используйте единый синтаксис – проверки версий останутся под капотом:

YourView()
.adapter.badge(5)



Преимущества:

🔹 Не повлияет на производительность: все проверки производятся на этапе компиляции.
🔹 Чистая кодовая база: больше никаких #available в каждом втором файле.
🔹 Простота интеграции: добавляется за пару минут через SPM.
🔹 Открытый исходный код: полная прозрачность, возможность вносить правки и участвовать в развитии.


➡️ Подписаться на канал
Мобильный трудоголик
Please open Telegram to view this post
VIEW IN TELEGRAM
126👍164🔥22
👨‍💻 Крах IT-пузыря: как выжить в эпоху ИИ и сокращений.

Минцифры кричит о нехватке кадров, а компании массово режут штаты. ИИ пугает заменой разработчиков. Что на самом деле происходит?


⚠️ Корень проблем.

1️⃣ Пузырь лопнул.
🔹 2020-2022: нанимали «впрок», раздували команды.
🔹 2024: кредиты подорожали, инвесторы потребовали окупаемости.
🔹 Итог: 20% меньше вакансий, 26% больше резюме (данные с hh).

2️⃣ Театр менеджмента.
🔹 Раньше: 50 человек «перерисовывали кнопки», лишь бы были задачи.
🔹 Сейчас: берут только самых эффективных или обращаются к аутсорсу.

3️⃣ ИИ-революция.
🔹 Искусственный интеллект уже способен заменить низкоклассифицированных сотрудников.
🔹 Ожидается что к 2027 году ИИ будем способен полностью заменить сеньоров.


🤔 Как выжить?

1️⃣ Стать незаменимым.
🔹 Осваивать ИИ-инструменты (ChatCPT, Copilot, DeepSeek, Qwen Chat..).
🔹 Прокачивать продуктовое мышление.
🔹 Развивать soft skills.

2️⃣ Адаптироваться.
🔹 Мониторить реально востребованные навыки.
🔹 Развиваться до более высоких и незаменимых позиций.


💡 Вывод:
Рынок вовсе не умер, он повзрослел. Зарабатывать будут те, кто создаёт ценность, а не красивые отчёты.


➡️ Подписаться на канал
Мобильный трудоголик
Please open Telegram to view this post
VIEW IN TELEGRAM
🗿21👀137👍3🤯2
🔢 Dependency Injection: как правильно внедрять зависимости в SwiftUI.

Сегодня поговорим о Dependency Injection (DI) — мощный подход, который делает ваш код чище, тестируемым и гибким.


⚠️ Что такое Dependency Injection?

Это способ передачи зависимостей извне, вместо создания их внутри класса.

Без использования DI:


class MyViewModel {
private let service = MyService() // Зависимость создаётся внутри
}


С использованием DI:


class MyViewModel {
private let service: MyService

init(service: MyService) { // Зависимость передаётся извне
self.service = service
}
}



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

🔸 Тестируемость: можно подменять зависимости на моки.
🔸 Гибкость: легко менять реализации (например, для разных сборок).
🔸 Чистая архитектура: сущности не знают, как создаются их зависимости.
🔸 Масштабирование: удобное расширение функционала благодаря добавлению зависимостей.


Три способа внедрения зависимостей в SwiftUI:

1️⃣ Через инициализатор (Constructor Injection).


struct MyContentView: View {
let service: MyNetworkService

var body: some View {
Text(service.fetchText())
}
}

MyContentView(
service: MyNetworkService()
)


2️⃣ Через свойства (Property Injection).


class MyViewModel: ObservableObject {
var analytics: MyAnalyticsService? // Можно задать позже
}

let myViewModel = MyViewModel()
myViewModel.analytics = MyAnalyticsService()


3️⃣ Через .environmentObject (Environment).


@main
struct MyApp: App {
let service = MyNetworkService()

var body: some Scene {
WindowGroup {
MyContentView()
.environmentObject(service)
}
}
}

struct MyContentView: View {
@EnvironmentObject var service: MyNetworkService

var body: some View {
Text(service.fetchText())
}
}



💡 Вывод:

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


➡️ Подписаться на канал
Мобильный трудоголик
Please open Telegram to view this post
VIEW IN TELEGRAM
122👍115🔥3
🔢 @ViewBuilder: магия композиции в SwiftUI.

Всем привет! Сегодня разберём @ViewBuilder — один из ключевых механизмов SwiftUI, который делает наш код таким лаконичным и выразительным.


⚠️ Что такое @ViewBuilder?

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

VStack {
Text("Привет")

Button("Нажми меня") {
// Действие
}

Image(systemName: "apple.logo")
}


Вместо такого кошмара:

VStack(content: {
TupleView(
Text("Привет"),

Button("Нажми меня") {
// Действие
},

Image(systemName: "swift")
)
})



🤔 Как это работает?

1️⃣ Автоматическая упаковка:

@ViewBuilder преобразует список вьюх в TupleView (для 2-10 элементов) или Group (если их больше).

2️⃣ Поддержка условий:

Может обрабатывать if, if let, switch:

@ViewBuilder
func header(isLoggedIn: Bool) -> some View {
if isLoggedIn {
Text("Добро пожаловать!")
} else {
Button("Войти") {
// Действие
}
}
}


3️⃣ Ограничения:

🔹 Нельзя использовать for напрямую (нужен ForEach).
🔹 Максимум 10 вьюх в одном блоке (но это ограничение обходится).


Продвинутые фишки:

🔸 Комбинирование с @resultBuilder.

Можно создать свой билдер для специфичных задач:

@resultBuilder
struct GridBuilder {
static func buildBlock(_ items: AnyView...) -> [AnyView] {
items
}
}


🔸 Обход ограничения в 10 вьюх.

Для этого стоит использовать Group:

@ViewBuilder
func megaView() -> some View {
Group {
Text("1")
...
Text("11") // Работает!
}
}



💡 Вывод:

@ViewBuilder — это мощный инструмент SwiftUI, который превращает декларативный код в иерархию View, делая интерфейсы гибкими и читаемыми.


➡️ Подписаться на канал
Мобильный трудоголик
Please open Telegram to view this post
VIEW IN TELEGRAM
2218👍2🔥1🫡11
🔢 Result Builders в SwiftUI: как создать свой DSL.

Привет, друзья! В прошлый раз мы разбирали @ViewBuilder, а сегодня углубимся в его основу и поговорим про resultBuilder, который делает магию SwiftUI возможной.


⚠️ Что такое resultBuilder?

Result Builder (ранее назывался function builder) — это механизм, позволяющий:
🔹 Объединять элементы в единую структуру.
🔹 Создавать собственные мини-языки (DSL) прямо в Swift.
🔹 Писать декларативный код, как в SwiftUI.


🤔 Как работает в SwiftUI?

Когда вы пишете:

VStack {
Text("Привет")

Button("Нажми меня") {
// Действие
}
}


Под капотом @ViewBuilder (частный случай Result Builder) превращает это в TupleView.


Создаём свой Result Builder.

Сделаем билдер для объединения строк с отступами:

@resultBuilder
struct ParagraphBuilder {
// Объединяем строки через \n
static func buildBlock(_ components: String...) -> String {
components.map { "- \($0)" }.joined(separator: "\n")
}

// Поддержка if
static func buildOptional(_ component: String?) -> String {
component ?? ""
}
}

// Использование:
@ParagraphBuilder func makeList() -> String {
"Первый пункт"
"Второй пункт"

if true {
"Случайный пункт"
}
}

print(makeList())

/* Выведет:
- Первый пункт
- Второй пункт
- Случайный пункт
*/



🤔 Где ещё применяется?

1️⃣ Кастомные UI-билдеры.

Например, для декларативного описания таблиц:

@TableBuilder func buildTable() -> Table {
Row {
Cell("A1")
Cell("A2")
}
Row {
Cell("B1")
Cell("B2")
}
}


2️⃣ Конфигурация сетевых запросов:

@RequestBuilder func makeRequest() -> HTTPRequest {
Method(.get)
Path("users")
Query("page", "2")
}


3️⃣ Тестовые данные:

@TestDataBuilder func mockUser() -> User {
Name("Иван")
Age(30)

if isPremium {
Tier(.gold)
}
}



Ограничения:

🔸 Нет поддержки for-циклов (нужно реализовывать buildArray).
🔸 Сложная отладка (ошибки компиляции могут быть непонятными).
🔸 Лимит в 10 элементов (как у @ViewBuilder).


💡 Вывод:

Result Builder — это мощный инструмент Swift, позволяющий создавать собственные DSL-синтаксисы для декларативного описания логики. Он лежит в основе @ViewBuilder и открывает возможности для кастомных решений, но требует внимания к ограничениям вроде обработки условий и лимита элементов.


➡️ Подписаться на канал
Мобильный трудоголик
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1982🔥1🙏11
👨‍💻 Дисциплина — главный навык разработчика.

В мобильной разработке (да и в IT в целом) недостаточно просто уметь писать код. Без дисциплины даже талантливый разработчик быстро теряет эффективность. Вот почему это так важно:


1️⃣ Дедлайны не ждут.

В разработке релизы требуют четкого планирования:
🔹 Изучение ТЗ.
🔹 Разработка.
🔹 Ревью кода.
🔹 Подготовка сборки для тестирование.
🔹 Тестирование на разных устройствах.
🔹 Отправка релиза на проверку в Apple App Store.

Если откладывать задачи на потом — страдает качество, а команда начинает работать в авральном режиме.


2️⃣ Техдолг копится незаметно.

«Сейчас сделаю костыль, потом перепишу». Проходит месяц и проект превращается в легаси-монстра. Дисциплинированный разработчик:
🔹 Старается писать чистый код с первого раза.
🔹 Рефакторит по мере возможности.
🔹 Не допускает «временных решений», которые становятся постоянными.


3️⃣ Самообучение требует системы.

Swift и экосистема Apple обновляются каждый год. Если учиться урывками:
🔹 Пропускаешь важные изменения (Concurrency, SwiftUI, новых API).
🔹 Отстаешь от рынка (и зарплатных ожиданий).

Рекомендую: выделять 2-3 часа в неделю на изучение нового и вести чек-лист актуальных технологий.


4️⃣ Баги любят хаос.

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


5️⃣ Карьера строится на привычках.

Разработчики, которые постоянно улучшают код, изучают новые технологии и следят за code style быстрее растут до мидлов/сеньоров и получают интересные офферы.


🤔 Как прокачать свою дисциплину?

🔸 Планируйте все свои задачи, в том числе и личные дела.
🔸 Разбивайте любую работу на этапы (не «сделать фичу», а «написать сетевой слой, сверстать UI, написать тесты»).
🔸 Автоматизируйте рутину (SwiftLint, CI/CD, юнит-тесты).


💡 Вывод:
Дисциплина ≠ скука. Это свобода от авралов, легаси-кода и профессионального застоя.


➡️ Подписаться на канал
Мобильный трудоголик
Please open Telegram to view this post
VIEW IN TELEGRAM
31🔥12👍3👀11
🐳 Container: Linux-контейнеры на macOS без Docker.

Один из самых интересных анонсов WWDC25Container, новый инструмент для работы с Linux-контейнерами прямо на macOS. Больше не нужен Docker — теперь можно собирать, запускать и деплоить контейнеры нативно, используя Swift и Virtualization.framework.


⚠️ Что умеет Container:

🔸 Собирает OCI-совместимые образы (как Docker).
🔸 Работает на Apple Silicon (M1/M2/M3 и новее).
🔸 Поддерживает кросс-компиляцию (например, под amd64 для Fly.io).
🔸 Интегрируется с удалёнными реестрами (Docker Hub, Fly.io и др.).


Сборка и деплой Vapor-приложения:

1️⃣ Установка.

Установим Container через Homebrew:

brew install container
container system start # Запускаем сервисы



2️⃣ Сборка образа.

Допустим, у нас есть Vapor-приложение. Container умеет читать Dockerfile:

container build --tag my-app --file Dockerfile .



3️⃣ Локальный запуск.


container run my-app


Проверим IP контейнера:


container list



4️⃣ Деплой на Fly.io

Сначала аутентифицируемся:


container registry login registry.fly.io


Собираем образ под amd64 (Fly.io не поддерживает arm):

container build --tag registry.fly.io/my-app:latest --arch amd64 --file Dockerfile .


Пушим и деплоим:

container images push registry.fly.io/my-app:latest
fly deploy --image registry.fly.io/my-app:latest



🤔 Зачем это iOS-разработчику?

🔹 Тестирование: быстро поднимать моки API, базы данных.
🔹 CI/CD: изолированные среды для SwiftLint, Fastlane.
🔹 Воспроизведение багов: запуск специфичных версий окружения.
🔹 Обучение: новички могут развернуть среду за минуты.


Текущие ограничения:

🔸 Только Apple Silicon.
🔸 Долгая сборка под amd64 (~15-20 минут).
🔸 Нет аналога Docker Compose (но уже есть сторонние решения).


💡 Вывод:

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

Container — это не просто замена Docker. Это шаг к универсальности Swift-разработки, где один язык и экосистема работают на всех уровнях — от iOS до бэкенда.


➡️ Подписаться на канал
Мобильный трудоголик
Please open Telegram to view this post
VIEW IN TELEGRAM
👍2811🔥3👀11
♠️ XML в мобильной разработке: почему он всё ещё актуален?

Привет, друзья! Сегодня поговорим о XML — формате, который десятилетиями остаётся фундаментом для обмена данными, несмотря на популярность JSON. В мобильной разработке он встречается чаще, чем кажется!


🤔 Где XML используется в iOS/Android?

1️⃣ Манифесты и конфиги:

🔸 AndroidManifest.xml (основной файл конфигурации приложений на Android).
🔸 info.plist (аналог в iOS, хотя формально это не XML, но схожая структура).


2️⃣ Локализация:

В Android строки хранятся в strings.xml — это стандартный подход для поддержки множества языков.


3️⃣ Верстка UI:

В Android XML до сих пор основной язык для разметки экранов (хотя Jetpack Compose постепенно меняет это).


4️⃣ Сетевые запросы:

Некоторые API (особенно в корпоративном секторе) до сих пор используют XML вместо JSON.


5️⃣ Базы данных:

Например, Firebase Realtime Database поддерживает экспорт/импорт данных в XML.


Плюсы XML для мобильных разработчиков:

🔹 Стандартизация: идеален для строгих форматов (например, банковские транзакции).
🔹 Валидация: можно проверить структуру через XSD-схемы.
🔹 Читаемость: вложенность тегов делает данные понятными для человека.
🔹 Поддержка: работает на любом устройстве и ОС без дополнительных библиотек.


Минусы XML:

🔸 Громоздкость: больше символов, чем в JSON.
🔸 Сложность парсинга: требует больше ресурсов (но XMLParser в iOS и XmlPullParser в Android решают эту проблему).
🔸 Не для всего: не подходит для высоконагруженных real-time-приложений.


💡 Вывод:

XML — не «устаревший» формат, а инструмент для конкретных задач. Он остаётся важным форматом в мобильной разработке, особенно для Android-манифестов, локализации и работы с legacy-API. Несмотря на популярность JSON, его строгая структура и валидация делают XML незаменимым в корпоративных и банковских решениях.


➡️ Подписаться на канал
Мобильный трудоголик
Please open Telegram to view this post
VIEW IN TELEGRAM
👍25👀12🗿21🔥1😁1