Акции и промокоды Отзывы о школах

Разработка под watchOS: что нужно знать iOS-разработчику

#Блог

Apple Watch давно перестали быть просто модным аксессуаром — теперь это полноценная платформа со своими законами.

В этом материале — разбор всех этапов: от установки Xcode до публикации в App Store. Вы узнаете, какие особенности дизайна стоит учитывать, как использовать Digital Crown, зачем молиться перед работой с симулятором и как не получить отказ от Apple. Материал написан с юмором и практической пользой — пригодится каждому, кто хочет освоить разработку под часы.

Настройка разработочной среды

Итак, вы решили погрузиться в мир разработки для watchOS. Поздравляю! Теперь давайте разберемся, что вам понадобится, кроме нервов из стали и бесконечного запаса кофе.

Прежде всего – Mac (да-да, без него никак, можете даже не пытаться обойти эту «маленькую» деталь). Причем не просто Mac, а достаточно современный, способный запустить последнюю версию Xcode. И вот тут начинается самое интересное – наша любимая среда разработки Xcode, которая, как заботливая тёща, всегда найдет, к чему придраться.

Минимальный набор инструментов:

  • Mac с macOS последней (или около того) версии
  • Xcode (последняя стабильная версия из App Store – да, только оттуда)
  • Apple ID (и желательно платный аккаунт разработчика, если планируете выпускать приложения в релиз)
  • Симулятор watchOS (входит в комплект Xcode, слава яблочным богам)

Процесс настройки прост, как квантовая физика (шучу, на самом деле даже проще):

  1. Установите Xcode из App Store (приготовьтесь к долгой загрузке – отличный момент сходить и получить степень PhD)
  2. Запустите Xcode и согласитесь установить дополнительные компоненты (еще один повод сварить кофе)
  3. Создайте новый проект:
File -> New -> Project -> watchOS -> App

(и тут начинается самое веселое)

При создании проекта Xcode любезно создаст для вас два таргета – один для iOS-приложения (потому что ваши часы без него как рыба без воды), и один для watchOS. Это похоже на семейную пару – они вроде бы независимы, но попробуйте их разделить…

Важный момент, о котором часто забывают (и я в том числе, пока не набил достаточно шишек): убедитесь, что в настройках проекта выбраны правильные версии deployment target для обоих приложений. Иначе потом будете долго размышлять, почему ваше приложение отказывается запускаться на реальном устройстве, хотя в симуляторе все работает идеально.

И да, говоря о симуляторе – он прекрасен… когда работает. Но иногда (читай: довольно часто) вам придется перезапускать его, потому что он решил, что сегодня не его день. Считайте это тренировкой дзен-терпения.

А теперь самое важное – настройка сертификатов и профилей. О, эта захватывающая история достойна отдельного романа! Но если кратко: вам нужно будет танцевать с бубном в Developer Portal, создавая различные сертификаты, идентификаторы приложений и профили подготовки. И помните – одна ошибка в этом танце, и ваше приложение откажется устанавливаться на устройство с загадочной ошибкой, смысл которой понятен только древним эльфам из Apple.

Кстати, про версионность – держите под рукой таблицу совместимости версий watchOS и iOS. Потому что иногда выясняется, что ваше гениальное приложение требует watchOS 7, а у пользователя все еще стоит watchOS 6, и это уже не лечится простым обновлением.

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

Основы дизайна интерфейса

Итак, мы подошли к самому «увлекательному» этапу – созданию интерфейса для экрана размером с хорошую почтовую марку. Помните старую шутку про то, что размер не имеет значения? Так вот, в случае с watchOS это откровенная ложь.

Первое, что нужно понять (и принять всем сердцем) – создание UI для часов радикально отличается от разработки для iPhone. Тут не работает подход «давайте просто уменьшим все элементы». Если вы попробуете это сделать, пользователи будут тыкать в ваше приложение с точностью бегемота в посудной лавке.

Ключевые принципы (или «как не сделать пользователям больно»):

  1. Иерархия важнее красоты
// Правильно:
let mainLabel = WKInterfaceLabel()
mainLabel.setFont(.systemFont(ofSize: 20, weight: .bold))

// Неправильно:
let fancyLabel = WKInterfaceLabel()
fancyLabel.setFont(.systemFont(ofSize: 12, weight: .thin))
// Потому что никто это не прочитает, серьезно
  1. Каждый пиксель на счету (буквально)
  • Минимальная область касания: 44×44 точки (и это не я придумал, а Apple)
  • Отступы между элементами: минимум 8 точек
  • Текст: не меньше 15-16 пунктов (если не хотите, чтобы пользователи доставали лупу)

Работа со Storyboard в watchOS – это отдельный вид искусства. В отличие от iOS, где Auto Layout позволяет творить чудеса (и кошмары), здесь все гораздо проще и… сложнее одновременно. У нас есть groups (группы) – этакий аналог Stack View, только более ограниченный в возможностях:

// Пример создания группы программно
let group = WKInterfaceGroup()
group.setBackgroundColor(.black)
group.setCornerRadius(8)
// И молимся, чтобы все элементы внутри расположились как надо

Особое внимание стоит уделить навигации. В watchOS нет привычного Navigation Bar, зато есть page-based navigation (когда пользователь свайпает между экранами) и hierarchical navigation (когда мы проваливаемся глубже в интерфейс):

// Пример push-навигации
presentController(withName: "DetailInterface", context: someData)
// Надеемся, что пользователь найдет, как вернуться назад

А теперь о самом болезненном – анимациях. Да, они есть в watchOS, и да, их нужно использовать очень аккуратно. Батарея часов не бесконечна (хотя иногда хочется верить в обратное):

// Простая анимация
animate(withDuration: 0.3) {
self.image.setAlpha(0.5)
}
// Достаточно простая, чтобы не убить батарею

И помните о контексте использования! Люди смотрят на часы буквально секунды – если ваше приложение требует более 5 секунд на выполнение базовой операции, что-то пошло не так.

Отдельного упоминания заслуживает темная тема – точнее, её отсутствие как выбора. В watchOS темный интерфейс является стандартным и рекомендуемым, поскольку это экономит батарею на OLED-экранах. Хотя светлые интерфейсы также возможны, они используются реже из-за соображений энергоэффективности (каламбур о светлых мечтах все еще уместен).

В следующем подразделе мы глубже погрузимся в элементы управления интерфейсом, и вы узнаете, почему кнопка размером в 2 пикселя – это не самая лучшая идея…

размеры

Диаграмма показывает ключевые размеры элементов интерфейса для приложений на watchOS

Элементы управления интерфейсом

В мире watchOS каждый элемент управления – это отдельное произведение искусства (или головной боли, зависит от вашего настроения). Давайте разберем основные инструменты из нашего арсенала, с которыми предстоит работать.

Кнопки (WKInterfaceButton)

// Базовая кнопка - на первый взгляд все просто
let button = WKInterfaceButton()
button.setTitle("Нажми меня")
// Но есть нюанс - в watchOS кнопки могут быть группами!
button.setBackgroundGroup(someGroup)

Забавный факт: кнопки в watchOS могут содержать целые группы элементов. Это как матрешка, только в интерфейсе – можно создать кнопку, внутри которой будет изображение, текст и еще парочка элементов. Удобно? Да! Может ли это привести к хаосу? О да!

Таблицы (WKInterfaceTable)

// Создаем таблицу
table.setNumberOfRows(dataSource.count, withRowType: "CustomRow")

// Настраиваем каждую строку
for index in 0..

Таблицы в watchOS – это отдельный вид искусства. Здесь нет привычных делегатов и датасорсов, зато есть row controllers – отдельные контроллеры для каждой строки. И да, переиспользование ячеек тут тоже работает иначе (если вы привыкли к UITableView, приготовьтесь к культурному шоку).

Слайдеры и секвенсеры

// Слайдер - когда нужно выбрать значение
slider.setValue(0.5)

// Секвенсер - когда нужно показать набор изображений
sequencer.setImages([image1, image2, image3])
sequencer.startAnimating()
// Предупреждение: анимации едят батарею быстрее, чем я печеньки

Особое внимание стоит уделить Digital Crown – этой замечательной крутилке, которая может использоваться для навигации и управления:

// Обработка поворота Digital Crown
override func willActivate() {
super.willActivate()
crownSequencer.delegate = self
crownSequencer.focus()
}

func crownDidRotate(_ crownSequencer: WKCrownSequencer?, rotationalDelta: Double) {
// Здесь можно сделать что-то полезное
// Или просто подивиться точности показаний
}

Меню действий – еще одна особенность watchOS:

// Добавляем меню
addMenuItem(with: .resume, title: "Продолжить", action: #selector(resumeAction))
// Только не перестарайтесь с количеством пунктов!

И напоследок – пикер даты/времени, который в watchOS выглядит… специфически:

// Создаем пикер
let picker = WKInterfacePicker()
picker.setItems([item1, item2, item3])
// Надеемся, что пользователь оценит нашу заботу о его времени

Важно помнить несколько ключевых моментов:

  • Все элементы должны быть достаточно большими для удобного касания
  • Избегайте сложных жестов – на маленьком экране это просто пытка
  • Используйте Force Touch меню только для действительно важных функций
  • Помните о контексте – пользователь смотрит на часы секунды, а не минуты
кнопки

Картинка визуализирует ключевые UI-элементы, применяемые в разработке интерфейсов под watchOS. На ней отображены стилизованные иконки кнопки, таблицы, слайдера и пикера — основных инструментов для взаимодействия с пользователем

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

Программирование и логика приложения

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

Жизненный цикл приложения

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

class InterfaceController: WKInterfaceController {
override func awake(withContext context: Any?) {
super.awake(withContext: context)
// Первая возможность что-то сделать
// Аналог viewDidLoad, только круче
}

override func willActivate() {
super.willActivate()
// Экран будет показан
// Самое время запустить таймеры и анимации
}

override func didDeactivate() {
super.didDeactivate()
// "Пока-пока" - говорим экрану
// Чистим ресурсы, останавливаем таймеры
}
}

Обработка событий

В watchOS обработка событий – это отдельный вид искусства. Вот пример работы с кнопкой:

@IBAction func handleButton() {
// Пользователь нажал кнопку
// Надеемся, что он сделал это намеренно

if userHasEnoughBattery() {
doSomethingAmazing()
} else {
showLowBatteryWarning()
// И молимся, чтобы заряда хватило показать предупреждение
}
}

Асинхронные операции

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

func fetchData() {
// Начинаем показывать индикатор загрузки
setTitle("Загружаем...")

Task {
do {
let result = try await dataService.fetchSomeData()
// Обновляем UI в главном потоке
DispatchQueue.main.async {
self.updateInterface(with: result)
}
} catch {
// Что-то пошло не так
DispatchQueue.main.async {
self.showError("Упс! Кажется, интернет решил взять выходной")
}
}
}
}

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

А теперь самое важное – оптимизация. На часах каждый миллисекунд на вес золота:

// Плохо
func updateUI() {
// Обновляем всё и сразу
table.reloadData()
image.setImage(newImage)
label.setText(newText)
// И надеемся, что часы не взорвутся
}

// Хорошо
func updateUI() {
// Обновляем только то, что изменилось
if imageNeedsUpdate {
image.setImage(newImage)
}
if textNeedsUpdate {
label.setText(newText)
}
// Теперь можно спать спокойно
}

Работа с таймерами

Отдельная история – работа с таймерами. На часах они особенно коварны:

class TimerController: WKInterfaceController {
var timer: Timer?

func startTimer() {
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
self?.updateTime()
}
}

override func didDeactivate() {
super.didDeactivate()
timer?.invalidate()
timer = nil
// Забыть остановить таймер - верный путь к утечкам памяти
}
}

И помните о главном правиле watchOS: пользователь может в любой момент опустить руку, и приложение должно быть к этому готово. Это как внезапный звонок мамы – нужно уметь быстро все сохранить и свернуть.

В следующем подразделе мы поговорим о том, как организовать обмен данными между часами и телефоном – ещё одной захватывающей саге в мире watchOS разработки…

Работа с данными

Давайте поговорим о том, как заставить ваши часы и iPhone общаться друг с другом. Это похоже на налаживание отношений между двумя упрямыми родственниками – требует терпения, правильного подхода и иногда небольшой магии.

Watch Connectivity Framework

Основной инструмент для коммуникации – Watch Connectivity Framework. Вот как это работает:

class DataManager: NSObject, WCSessionDelegate {
let session = WCSession.default

override init() {
super.init()
// Проверяем, поддерживается ли сессия
if WCSession.isSupported() {
session.delegate = self
session.activate()
// Скрестим пальцы, что активация пройдет успешно
}
}

func session(_ session: WCSession, activationDidCompleteWith state: WCSessionActivationState, error: Error?) {
if let error = error {
print("Упс! Что-то пошло не так: \(error.localizedDescription)")
// Время плакать и отлаживать
}
}
}

Передача данных

Есть несколько способов передачи данных (потому что одного было бы слишком просто):

// 1. Интерактивная передача сообщений
func sendMessage() {
let message = ["key": "value"]
session.sendMessage(message, replyHandler: { reply in
print("Ура! Получили ответ: \(reply)")
}, errorHandler: { error in
print("Что-то пошло не так: \(error)")
})
}

// 2. Передача данных в фоне
func transferData() {
let data = try? JSONEncoder().encode(someObject)
let transfer = session.transferFile(URL(fileURLWithPath: "path"), metadata: nil)
// Теперь ждем и молимся
}

// 3. Обновление контекста приложения
func updateApplicationContext() {
do {
try session.updateApplicationContext(["lastUpdate": Date()])
} catch {
print("Не смогли обновить контекст. Классика!")
}
}

Хранение данных

Для локального хранения данных у нас есть несколько опций:

// UserDefaults - для небольших данных
class SettingsManager {
static let shared = SettingsManager()

func saveSettings(_ settings: Settings) {
UserDefaults.standard.set(settings.encoded, forKey: "userSettings")
// Надеемся, что данные сохранятся
}
}

// File System - для больших объемов
func saveFile() {
let fileManager = FileManager.default
let documentsPath = fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0]
let filePath = documentsPath.appendingPathComponent("data.json")

do {
try data.write(to: filePath)
} catch {
print("Файл решил жить своей жизнью: \(error)")
}
}

Оптимизация передачи данных

Важные моменты для оптимизации:

  • Минимизируйте объем передаваемых данных:
// Плохо
struct HugeData: Codable {
let entireDatabase: [String: Any] // Зачем нам вообще это передавать?
}

// Хорошо
struct OptimizedData: Codable {
let lastUpdateTimestamp: TimeInterval
let changedRecordsOnly: [String: Any]
}
  • Используйте правильный метод передачи:
// Для срочных маленьких данных
session.sendMessage(["urgent": true], replyHandler: nil, errorHandler: nil)

// Для больших данных в фоне
session.transferFile(fileURL, metadata: nil)

Помните: батарея часов – это ограниченный ресурс, и каждая передача данных её расходует. Поэтому подходите к этому вопросу как к планированию бюджета – экономно и с умом.

В следующем разделе мы поговорим о том, как все это протестировать и не сойти с ума в процессе…

Тестирование и отладка

Тестирование приложений для watchOS – это особый вид приключения, где каждый день открываешь что-то новое (обычно баг, который не должен был существовать в принципе). Давайте разберем, как сделать этот процесс менее болезненным.

Симулятор против реальности

// Проверка, работаем ли мы в симуляторе
#if targetEnvironment(simulator)
print("Мы в матрице!")
#else
print("Это реальный мир, и тут все работает иначе")
#endif

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

  • Производительности (в симуляторе все летает)
  • Батареи (которой в симуляторе просто нет)
  • Сетевых задержек (попробуйте посимулировать плохой WiFi – будет весело)

Отладка с помощью логирования

class DebugLogger {
static func log(_ message: String, file: String = #file, function: String = #function, line: Int = #line) {
#if DEBUG
print("📱 [WATCH] \(file.split(separator: "/").last ?? "") - \(function):\(line) -> \(message)")
#endif
}
}

// Использование
DebugLogger.log("Что-то пошло не так, но мы хотя бы знаем где именно")

Unit-тестирование

Да, для watchOS тоже можно (и нужно) писать юнит-тесты:

class WatchDataManagerTests: XCTestCase {
var dataManager: DataManager!

override func setUp() {
super.setUp()
dataManager = DataManager()
}

func testDataSync() {
// Создаем ожидание
let expectation = XCTestExpectation(description: "Data sync completion")

dataManager.syncData { result in
switch result {
case .success:
// Ура, все работает!
expectation.fulfill()
case .failure(let error):
XCTFail("Синхронизация не удалась: \(error)")
}
}

// Ждем выполнения, но не вечно
wait(for: [expectation], timeout: 5.0)
}
}

Профилирование производительности

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

func measurePerformance() {
let start = CFAbsoluteTimeGetCurrent()

// Ваш код здесь
heavyOperation()

let end = CFAbsoluteTimeGetCurrent()
DebugLogger.log("Операция заняла \(end - start) секунд")
// Если больше 0.1 секунды - пора оптимизировать
}

Отладка коммуникации с iPhone

extension WCSession {
func debugSendMessage(_ message: [String: Any]) {
sendMessage(message, replyHandler: { reply in
DebugLogger.log("✅ Получен ответ: \(reply)")
}, errorHandler: { error in
DebugLogger.log("❌ Ошибка отправки: \(error)")
})
}
}

Чеклист тестирования

  • Проверка на разных размерах часов:
// Программная адаптация под размер экрана
if WKInterfaceDevice.current().screenBounds.size.width > 184 {
// 44mm/45mm логика
} else {
// 40mm/41mm логика
}
  • Тестирование при разных уровнях заряда
  • Проверка поведения при потере соединения с iPhone
  • Тестирование прерываний (входящие уведомления, звонки)

Советы по отладке

  • Используйте breakpoints с условиями:
// Точка останова сработает только при определенном условии
if dataSize > maxAllowedSize {
// Поставьте breakpoint здесь
}
  • Логируйте важные события жизненного цикла:
override func willActivate() {
super.willActivate()
DebugLogger.log("📱 Контроллер активируется")
}
  • Не забывайте про Edge Cases:
  1. Отсутствие интернета
  2. Полный диск
  3. Разряженная батарея
  4. Медленное соединение

В следующем разделе мы поговорим о том, как выпустить ваше детище в большой мир через App Store…

Публикация и поддержка приложения

А теперь самое интересное – как выпустить ваше приложение в свет и не получить отказ от Apple по 47 различным причинам (да, такое тоже бывает).

Подготовка к публикации

Первым делом нужно убедиться, что ваше приложение соответствует всем требованиям Apple:

// Проверка критических ошибок
func validateAppForSubmission() {
assert(appIcon != nil, "Без иконки никуда! Apple это не оценит")
assert(appName.count <= 30, "Название слишком длинное - Apple любит краткость")
assert(hasPrivacyPolicy, "Политика конфиденциальности обязательна!")
}

Чеклист перед отправкой

  • Метаданные приложения:
struct AppMetadata {
let appName: String
let description: String  // До 4000 символов
let keywords: String // До 100 символов
let supportUrl: URL
let marketingUrl: URL?   // Необязательно, но желательно
}
  • Скриншоты:
  1. 44mm Apple Watch
  2. 40mm Apple Watch (если поддерживается)
  3. Promotional Art (желательно)
  • Технические требования:
// Проверка основных требований
func technicalRequirements() -> Bool {
return hasValidBundleId &&
hasValidProvisioningProfile &&
hasValidCertificates &&
supportsLatestWatchOS &&
hasNoHardcodedIPs // Да, Apple это проверяет!
}

Процесс публикации

  • Архивация и подписание:
// Настройка build settings
let buildSettings = [
"DEVELOPMENT_TEAM": "YOUR_TEAM_ID",
"CODE_SIGN_STYLE": "Manual",
"PROVISIONING_PROFILE_SPECIFIER": "WatchProfile"
]
  • Загрузка в App Store Connect:
// Проверка версии и сборки
func validateVersionAndBuild() {
let currentVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString")
let buildNumber = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion")

guard currentVersion != previousVersion else {
fatalError("Забыли обновить версию!")
}
}

Поддержка после релиза

  • Мониторинг:
class CrashReporter {
static func logError(_ error: Error) {
// Отправляем в вашу систему аналитики
Analytics.logError(error)
// И молимся, чтобы это не случалось часто
}
}
  • Обновления:
func checkForUpdates() {
if let newWatchOSVersion = WKInterfaceDevice.current().systemVersion {
// Проверяем совместимость
if needsUpdate(for: newWatchOSVersion) {
scheduleUpdate()
}
}
}

Советы по поддержке

  1. Регулярно проверяйте отзывы пользователей
  2. Быстро реагируйте на критические баги
  3. Следите за обновлениями watchOS
  4. Поддерживайте актуальную документацию

Частые причины отказа

  1. Некорректная работа с пользовательскими данными
  2. Проблемы с производительностью
  3. Несоответствие гайдлайнам Human Interface Guidelines
  4. Недостаточное описание функциональности
// Пример проверки согласия на использование данных
func checkPrivacyConsent() -> Bool {
guard let userConsent = UserDefaults.standard.bool(forKey: "PrivacyConsent") else {
requestPrivacyConsent()
return false
}
return userConsent
}

Финальные рекомендации

  1. Тестируйте на реальных устройствах
  2. Документируйте все изменения
  3. Поддерживайте канал обратной связи с пользователями
  4. Регулярно обновляйте приложение

В следующем, заключительном разделе, мы подведем итоги и поговорим о будущем разработки под watchOS…

Заключение

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

Ключевые выводы

  1. Размер имеет значение
// Правило большого пальца для watchOS
struct WatchOSDesignRules {
static let minimumTapTargetSize: CGFloat = 44.0
static let maximumScreenContent = "Меньше чем вы думаете"
static let perfectAppDescription = "Делает одну вещь, но делает её хорошо"
}
  1. Производительность критична
  • Каждая миллисекунда на счету
  • Батарея – ваш главный враг
  • Оптимизация – не опция, а необходимость
  1. Пользовательский опыт превыше всего
// Золотое правило watchOS разработки
func userExperience() -> Bool {
return isSimple &&
isFast &&
isUseful &&
doesntKillBattery
}

Взгляд в будущее

Платформа watchOS продолжает развиваться, и мы можем ожидать:

  • Новые API и возможности
  • Улучшенную производительность
  • Более тесную интеграцию с другими устройствами Apple
  • Новые сценарии использования

Последние советы

  1. Постоянно учитесь:
func stayUpdated() {
followWWDC()
readDocumentation()
experimentWithNewAPIs()
// И не забывайте про кофе
}
  1. Участвуйте в сообществе:
  • Делитесь опытом
  • Задавайте вопросы
  • Помогайте другим
  1. Не бойтесь экспериментировать:
func tryNewThings() {
if ideaSeemsCrazy && mightActuallyWork {
goForIt()
}
}

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

И помните: хорошее watchOS приложение – это не уменьшенная версия iPhone приложения, а продуманный инструмент, созданный специально для этой платформы.

// Финальное напутствие
func partingWords() -> String {
return """
Создавайте с умом,
Тестируйте тщательно,
Оптимизируйте безжалостно,
И никогда не забывайте, что
ваше приложение будет носить кто-то на руке.
"""
}

Удачи в разработке, и пусть ваши приложения никогда не крашатся (ну, или хотя бы делают это красиво)!

Читайте также
инструменты веб-разработки
#Блог

Какие инструменты используют веб-разработчики?

Веб-разработка — это не только код, но и выбор правильных инструментов. Узнайте, как редакторы кода, фреймворки, препроцессоры и системы контроля версий помогают создавать современные сайты. Разбираемся, что выбрать начинающим и профессиональным разработчикам.

Категории курсов
Отзывы о школах