├── LICENSE ├── README.md └── resources ├── surf.swiftformat ├── swiftlint.yml └── xcode_settings.bash /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Surf 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Surf Swift Style Guide 2 | 3 | ## Цели 4 | 5 | Этот стайлгайд создан с целью: 6 | 7 | * Облегчить чтение и понимание незнакомого кода 8 | * Облегчить поддержку кода 9 | * Уменьшить вероятность совершения простых ошибок кодинга 10 | * Снизить когнитивную нагрузку при кодинге 11 | * Сфокусировать обсуждения в Pull Request-ах на логике, а не на стиле 12 | 13 | Краткость кода не является основной целью. Код должен быть кратким только в том случае, если другие важные качества кода (такие как читаемость, простота и ясность) остаются равными или улучшаются. 14 | 15 | ## Принципы 16 | 17 | * Этот гайд являеся дополнением официальному [Swift API Design Guidelines](https://swift.org/documentation/api-design-guidelines/) 18 | * Мы стараемся сделать каждое правило проверяемым при помощи различных linter-ов 19 | 20 | ## Содержание 21 | 22 | - [Surf Swift Style Guide](#surf-swift-style-guide) 23 | - [Цели](#%D1%86%D0%B5%D0%BB%D0%B8) 24 | - [Принципы](#%D0%BF%D1%80%D0%B8%D0%BD%D1%86%D0%B8%D0%BF%D1%8B) 25 | - [Содержание](#%D1%81%D0%BE%D0%B4%D0%B5%D1%80%D0%B6%D0%B0%D0%BD%D0%B8%D0%B5) 26 | - [Xcode форматирование](#xcode-%D1%84%D0%BE%D1%80%D0%BC%D0%B0%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5) 27 | - [Именование](#%D0%B8%D0%BC%D0%B5%D0%BD%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5) 28 | - [Стиль](#%D1%81%D1%82%D0%B8%D0%BB%D1%8C) 29 | - [Функции](#%D1%84%D1%83%D0%BD%D0%BA%D1%86%D0%B8%D0%B8) 30 | - [Замыкания](#%D0%B7%D0%B0%D0%BC%D1%8B%D0%BA%D0%B0%D0%BD%D0%B8%D1%8F) 31 | - [Операторы](#%D0%BE%D0%BF%D0%B5%D1%80%D0%B0%D1%82%D0%BE%D1%80%D1%8B) 32 | - [Паттерны](#%D0%BF%D0%B0%D1%82%D1%82%D0%B5%D1%80%D0%BD%D1%8B) 33 | - [Организация файлов](#%D0%BE%D1%80%D0%B3%D0%B0%D0%BD%D0%B8%D0%B7%D0%B0%D1%86%D0%B8%D1%8F-%D1%84%D0%B0%D0%B9%D0%BB%D0%BE%D0%B2) 34 | - [Совместимость с Objective-C](#%D1%81%D0%BE%D0%B2%D0%BC%D0%B5%D1%81%D1%82%D0%B8%D0%BC%D0%BE%D1%81%D1%82%D1%8C-%D1%81-objective-c) 35 | 36 | ## Xcode форматирование 37 | 38 | _Вы можете добавить эти настройки воспользовавшись [этим скриптом](resources/xcode_settings.bash), как вариант, его вызов можно добавить в "Run Script" build phase._ 39 | 40 | * # **Каждая строка должна иметь максимальную длину в 120 символов.** [![SwiftLint: line_length](https://img.shields.io/badge/SwiftLint-line__length-217D89.svg)](https://github.com/realm/SwiftLint/blob/master/Rules.md#line-length) 41 | 42 | * # **Используйте 4 пробела для отступов.** 43 | 44 | * # **Строки не должны содержать пробелы в конце.** [![SwiftFormat: trailingSpace](https://img.shields.io/badge/SwiftFormat-trailingSpace-7B0051.svg)](https://github.com/nicklockwood/SwiftFormat/blob/master/Rules.md#trailingSpace) [![SwiftLint: trailing_whitespace](https://img.shields.io/badge/SwiftLint-trailing__whitespace-217D89.svg)](https://github.com/realm/SwiftLint/blob/master/Rules.md#trailing-whitespace) 45 | 46 | ## Именование 47 | 48 | * # **Используйте PascalCase для названий типов и протоколов, и lowerCamelCase для всего остального.** [![SwiftLint: type_name](https://img.shields.io/badge/SwiftLint-type__name-007A87.svg)](https://github.com/realm/SwiftLint/blob/master/Rules.md#type-name) 49 | 50 |
51 | 52 | ```swift 53 | protocol SpaceThing { 54 | // ... 55 | } 56 | 57 | class SpaceFleet: SpaceThing { 58 | 59 | enum Formation { 60 | // ... 61 | } 62 | 63 | class Spaceship { 64 | // ... 65 | } 66 | 67 | var ships: [Spaceship] = [] 68 | static let worldName = "Earth" 69 | 70 | func addShip(_ ship: Spaceship) { 71 | // ... 72 | } 73 | } 74 | 75 | let myFleet = SpaceFleet() 76 | ``` 77 | 78 |
79 | 80 | _Исключение: Вы можете поставить префикс подчеркивания перед приватным свойством если оно повторяет свойство или метод с одинаковым именем с более высоким уровнем доступа_ 81 | 82 |
83 | 84 | #### Почему? 85 | Есть некоторые случаи при которых повторение названия свойства или метода может быть проще для чтения и понимания, чем использование другого имени. 86 | 87 | Например: 88 | 89 | ```swift 90 | final class BiometricAuthenticator { 91 | 92 | static var canAuthenticate: Bool { 93 | return _canAuthenticate() 94 | } 95 | 96 | ... 97 | 98 | private static func _canAuthenticate() { 99 | ... 100 | } 101 | 102 | } 103 | ``` 104 | 105 |
106 | 107 | * # **Называйте булевые переменные в формате `isSpaceship`, `hasSpacesuit`, и т.п.** Так становится понятнее, что это именно Bool тип данных, а не какой-либо другой. 108 | 109 | * # **Акронимы в названиях (например `URL`) должны быть в верхнем регистре за исключением случаев, когда это начало названия которое должно быть в lowerCamelCase** 110 | 111 |
112 | 113 | ```swift 114 | // Неправильно 115 | class UrlValidator { 116 | 117 | func isValidUrl(_ URL: URL) -> Bool { 118 | // ... 119 | } 120 | 121 | func isUrlReachable(_ URL: URL) -> Bool { 122 | // ... 123 | } 124 | } 125 | 126 | let URLValidator = UrlValidator().isValidUrl(/* some URL */) 127 | 128 | // Правильно 129 | class URLValidator { 130 | 131 | func isValidURL(_ url: URL) -> Bool { 132 | // ... 133 | } 134 | 135 | func isURLReachable(_ url: URL) -> Bool { 136 | // ... 137 | } 138 | } 139 | 140 | let urlValidator = URLValidator().isValidURL(/* some URL */) 141 | ``` 142 | 143 |
144 | 145 | * # **Общая часть названия должна быть впереди, а более специфичная часть должна следовать за ней.** Значение "общая часть" зависит от конеткста, но должно примерно означать "то, что больше всего помогает вам сузить поиск нужного элемента." Самое главное, будьте последовательны с тем, как вы располагаете части имен. 146 | 147 |
148 | 149 | ```swift 150 | // Неправильно 151 | let rightTitleMargin: CGFloat 152 | let leftTitleMargin: CGFloat 153 | let bodyRightMargin: CGFloat 154 | let bodyLeftMargin: CGFloat 155 | 156 | // Правильно 157 | let titleMarginRight: CGFloat 158 | let titleMarginLeft: CGFloat 159 | let bodyMarginRight: CGFloat 160 | let bodyMarginLeft: CGFloat 161 | ``` 162 | 163 |
164 | 165 | * # **Включите подсказку о типе в имя, если в противном случае оно будет неоднозначным.** 166 | 167 |
168 | 169 | ```swift 170 | // Неправильно 171 | let title: String 172 | let cancel: UIButton 173 | 174 | // Правильно 175 | let titleText: String 176 | let cancelButton: UIButton 177 | ``` 178 | 179 |
180 | 181 | * # **Обработчики событий должны быть названы как предложения в настоящем времени.** Детали можно опустить, если они не нужны для ясности. 182 | 183 |
184 | 185 | ```swift 186 | // Неправильно 187 | class SomeViewController { 188 | 189 | private func didTapLogin() { 190 | // ... 191 | } 192 | 193 | private func didTapBookButton() { 194 | // ... 195 | } 196 | 197 | private func modelDidChange() { 198 | // ... 199 | } 200 | } 201 | 202 | // Правильно 203 | class SomeViewController { 204 | 205 | private func login() { 206 | // ... 207 | } 208 | 209 | private func handleBookButtonTap() { 210 | // ... 211 | } 212 | 213 | private func modelChanged() { 214 | // ... 215 | } 216 | } 217 | ``` 218 |
219 | 220 | * # **Названия протоколов должны явно отражать функциональное значение протоколов**. Если протокол описывает методы, реализующие действия самого объекта над другими объектами, лучше использовать Noun; если же он описывает действия, которые можно совершить над объектом, реализующим протокол, лучше использовать Adjective с суффиксом -able. 221 | 222 |
223 | 224 | 225 | ```swift 226 | // Неправильно 227 | protocol Presenter { 228 | func presentInView(view: UIView) 229 | } 230 | 231 | protocol AlertPresentable { 232 | func presentAlert(alert: Alert) 233 | } 234 | 235 | // Правильно 236 | protocol Presentable { 237 | func presentInView(view: UIView) 238 | } 239 | 240 | protocol AlertPresenter { 241 | func presentAlert(alert: Alert) 242 | } 243 | ``` 244 | 245 |
246 | 247 | ## Стиль 248 | 249 | * # **Не указываете типы там, где они легко могут быть выведены** 250 | 251 |
252 | 253 | ```swift 254 | // Неправильно 255 | let host: Host = Host() 256 | 257 | // Правильно 258 | let host = Host() 259 | ``` 260 | 261 | ```swift 262 | enum Direction { 263 | case left 264 | case right 265 | } 266 | 267 | func someDirection() -> Direction { 268 | // Неправильно 269 | return Direction.left 270 | 271 | // Правильно 272 | return .left 273 | } 274 | ``` 275 | 276 |
277 | 278 | * # **Условные операторы должны всегда вызывать `return` в следующей строке** [![SwiftLint: conditional_returns_on_newline](https://img.shields.io/badge/SwiftLint-trailing__comma-217D89.svg)](https://github.com/realm/SwiftLint/blob/master/Rules.md#conditional-returns-on-newline) 279 | 280 |
281 | 282 | ```swift 283 | // Неправильно 284 | guard true else { return } 285 | 286 | if true { return } 287 | 288 | // Правильно 289 | guard true else { 290 | return 291 | } 292 | 293 | if true { 294 | return 295 | } 296 | ``` 297 | 298 |
299 | 300 | * # **Не используйте `self` пока это не нужно для уточнения или пока того не требует язык.** Это правило не касается инициализаторов, там нужно использовать `self` всегда. [![SwiftFormat: redundantSelf](https://img.shields.io/badge/SwiftFormat-redundantSelf-7B0051.svg)](https://github.com/nicklockwood/SwiftFormat/blob/master/Rules.md#redundantSelf) 301 | 302 |
303 | 304 | ```swift 305 | final class Listing { 306 | 307 | init(capacity: Int, allowsPets: Bool) { 308 | // Правильно 309 | self.capacity = capacity 310 | self.isFamilyFriendly = !allowsPets 311 | } 312 | 313 | private let isFamilyFriendly: Bool 314 | private var capacity: Int 315 | 316 | private func increaseCapacity(by amount: Int) { 317 | // Неправильно 318 | self.capacity += amount 319 | self.save() 320 | 321 | // Правильно 322 | capacity += amount 323 | save() 324 | } 325 | } 326 | ``` 327 | 328 |
329 | 330 | * # **Следует избегать закрывающей запятой в массивах и словарях** [![SwiftLint: trailing_comma](https://img.shields.io/badge/SwiftLint-trailing__comma-217D89.svg)](https://github.com/realm/SwiftLint/blob/master/Rules.md#trailing-comma) 331 | 332 |
333 | 334 | ```swift 335 | // Неправильно 336 | let rowContent = [ 337 | listingUrgencyDatesRowContent(), 338 | listingUrgencyBookedRowContent(), 339 | listingUrgencyBookedShortRowContent(), 340 | ] 341 | 342 | // Правильно 343 | let rowContent = [ 344 | listingUrgencyDatesRowContent(), 345 | listingUrgencyBookedRowContent(), 346 | listingUrgencyBookedShortRowContent() 347 | ] 348 | ``` 349 | 350 |
351 | 352 | * # **Именуйте свойства в кортеже для большей ясности** Эмпирическое правило: если у вас есть более 3 полей, вы, вероятно, должны использовать структуру. 353 | 354 |
355 | 356 | ```swift 357 | // Неправильно 358 | func whatever() -> (Int, Int) { 359 | return (4, 4) 360 | } 361 | let thing = whatever() 362 | print(thing.0) 363 | 364 | // Правильно 365 | func whatever() -> (x: Int, y: Int) { 366 | return (x: 4, y: 4) 367 | } 368 | 369 | // Так тоже можно 370 | func whatever2() -> (x: Int, y: Int) { 371 | let x = 4 372 | let y = 4 373 | return (x, y) 374 | } 375 | 376 | let coord = whatever() 377 | coord.x 378 | coord.y 379 | ``` 380 | 381 |
382 | 383 | * # **Используйте конструкторы вместо Make() функций для CGRect, CGPoint, NSRange и других.** [![SwiftLint: legacy_constructor](https://img.shields.io/badge/SwiftLint-legacy__constructor-007A87.svg)](https://github.com/realm/SwiftLint/blob/master/Rules.md#legacy-constructor) 384 | 385 |
386 | 387 | ```swift 388 | // Неправильно 389 | let rect = CGRectMake(10, 10, 10, 10) 390 | 391 | // Правильно 392 | let rect = CGRect(x: 0, y: 0, width: 10, height: 10) 393 | ``` 394 | 395 |
396 | 397 | * # **Используйте современные Swift расширения методов вместо старых глобальных методов из Objective-C.** [![SwiftLint: legacy_cggeometry_functions](https://img.shields.io/badge/SwiftLint-legacy__cggeometry__functions-007A87.svg)](https://github.com/realm/SwiftLint/blob/master/Rules.md#legacy-cggeometry-functions) [![SwiftLint: legacy_constant](https://img.shields.io/badge/SwiftLint-legacy__constant-007A87.svg)](https://github.com/realm/SwiftLint/blob/master/Rules.md#legacy-constant) [![SwiftLint: legacy_nsgeometry_functions](https://img.shields.io/badge/SwiftLint-legacy__nsgeometry__functions-007A87.svg)](https://github.com/realm/SwiftLint/blob/master/Rules.md#legacy-nsgeometry-functions) 398 | 399 |
400 | 401 | ```swift 402 | // Неправильно 403 | var rect = CGRectZero 404 | var width = CGRectGetWidth(rect) 405 | 406 | // Правильно 407 | var rect = CGRect.zero 408 | var width = rect.width 409 | ``` 410 | 411 |
412 | 413 | * # **Ставьте двоеточие и пробел сразу после идентификатора.** [![SwiftLint: colon](https://img.shields.io/badge/SwiftLint-colon-007A87.svg)](https://github.com/realm/SwiftLint/blob/master/Rules.md#colon) 414 | 415 |
416 | 417 | ```swift 418 | // Неправильно 419 | var something : Double = 0 420 | 421 | // Правильно 422 | var something: Double = 0 423 | ``` 424 | 425 | ```swift 426 | // Неправильно 427 | class MyClass : SuperClass { 428 | // ... 429 | } 430 | 431 | // Правильно 432 | class MyClass: SuperClass { 433 | // ... 434 | } 435 | ``` 436 | 437 | ```swift 438 | // Неправильно 439 | var dict = [KeyType:ValueType]() 440 | var dict = [KeyType : ValueType]() 441 | 442 | // Правильно 443 | var dict = [KeyType: ValueType]() 444 | ``` 445 | 446 |
447 | 448 | * # **Ставьте пробел по обеим сторонам стрелки возвращаемого типа.** [![SwiftLint: return_arrow_whitespace](https://img.shields.io/badge/SwiftLint-return__arrow__whitespace-007A87.svg)](https://github.com/realm/SwiftLint/blob/master/Rules.md#returning-whitespace) 449 | 450 |
451 | 452 | ```swift 453 | // Неправильно 454 | func doSomething()->String { 455 | // ... 456 | } 457 | 458 | // Правильно 459 | func doSomething() -> String { 460 | // ... 461 | } 462 | ``` 463 | 464 | ```swift 465 | // Неправильно 466 | func doSomething(completion: ()->Void) { 467 | // ... 468 | } 469 | 470 | // Правильно 471 | func doSomething(completion: () -> Void) { 472 | // ... 473 | } 474 | ``` 475 | 476 |
477 | 478 | * # **Избегайте лишних скобок.** [![SwiftFormat: redundantParens](https://img.shields.io/badge/SwiftFormat-redundantParens-7B0051.svg)](https://github.com/nicklockwood/SwiftFormat/blob/master/Rules.md#redundantParens) 479 | 480 |
481 | 482 | ```swift 483 | // Неправильно 484 | if (userCount > 0) { ... } 485 | switch (someValue) { ... } 486 | let evens = userCounts.filter { (number) in number % 2 == 0 } 487 | let squares = userCounts.map() { $0 * $0 } 488 | 489 | // Правильно 490 | if userCount > 0 { ... } 491 | switch someValue { ... } 492 | let evens = userCounts.filter { number in number % 2 == 0 } 493 | let squares = userCounts.map { $0 * $0 } 494 | ``` 495 | 496 |
497 | 498 | * # **Опустите аргументы case, если они все без имени** [![SwiftLint: empty_enum_arguments](https://img.shields.io/badge/SwiftLint-empty__enum__arguments-007A87.svg)](https://github.com/realm/SwiftLint/blob/master/Rules.md#empty-enum-arguments) 499 | 500 |
501 | 502 | ```swift 503 | // Неправильно 504 | if case .done(_) = result { ... } 505 | 506 | switch animal { 507 | case .dog(_, _, _): 508 | ... 509 | } 510 | 511 | // Правильно 512 | if case .done = result { ... } 513 | 514 | switch animal { 515 | case .dog: 516 | ... 517 | } 518 | ``` 519 | 520 |
521 | 522 | ### Функции 523 | 524 | * # **Опускайте возвращаемый тип `Void`.** [![SwiftLint: redundant_void_return](https://img.shields.io/badge/SwiftLint-redundant__void__return-007A87.svg)](https://github.com/realm/SwiftLint/blob/master/Rules.md#redundant-void-return) 525 | 526 |
527 | 528 | ```swift 529 | // Неправильно 530 | func doSomething() -> Void { 531 | ... 532 | } 533 | 534 | // Правильно 535 | func doSomething() { 536 | ... 537 | } 538 | ``` 539 | 540 |
541 | 542 | ### Замыкания 543 | 544 | * # **Используйте возвращаемый тип `Void` вместо `()` в определении замыкания.** [![SwiftLint: void_return](https://img.shields.io/badge/SwiftLint-void__return-007A87.svg)](https://github.com/realm/SwiftLint/blob/master/Rules.md#void-return) 545 | 546 |
547 | 548 | ```swift 549 | // Неправильно 550 | func method(completion: () -> ()) { 551 | ... 552 | } 553 | 554 | // Правильно 555 | func method(completion: () -> Void) { 556 | ... 557 | } 558 | ``` 559 | 560 |
561 | 562 | * # **Именуйте неиспользуемые параметры замыкания как нижние подчеркивания (`_`).** [![SwiftLint: unused_closure_parameter](https://img.shields.io/badge/SwiftLint-unused__closure__parameter-007A87.svg)](https://github.com/realm/SwiftLint/blob/master/Rules.md#unused-closure-parameter) 563 | 564 |
565 | 566 | #### Почему? 567 | Это упрощает чтение, так становится очевидно какие параметры используются, а какие не используются. 568 | 569 | ```swift 570 | // Неправильно 571 | someAsyncThing() { argument1, argument2, argument3 in 572 | print(argument3) 573 | } 574 | 575 | // Правильно 576 | someAsyncThing() { _, _, argument3 in 577 | print(argument3) 578 | } 579 | ``` 580 | 581 |
582 | 583 | * # **Однострочные замыкания должны содержать по одному пробелу до и после каждой скобки, за исключением пробела между закрывающей скобкой и следующим оператором.** [![SwiftLint: closure_spacing](https://img.shields.io/badge/SwiftLint-closure__spacing-007A87.svg)](https://github.com/realm/SwiftLint/blob/master/Rules.md#closure-spacing) 584 | 585 |
586 | 587 | ```swift 588 | // Неправильно 589 | let evenSquares = numbers.filter {$0 % 2 == 0}.map { $0 * $0 } 590 | 591 | // Правильно 592 | let evenSquares = numbers.filter { $0 % 2 == 0 }.map { $0 * $0 } 593 | ``` 594 | 595 |
596 | 597 | ### Операторы 598 | 599 | * # **Инфиксные операторы должны отделятся одним пробелом с каждой стороны.** Предпочитайте скобки, чтобы визуально группировать выражения с большим количеством операторов, а не изменять ширину пробелов. Это правило не относится к операторам диапазона (например, `1...3`) и к префиксным или постфиксным операторам (например, `guest?` или `-1`). [![SwiftLint: operator_usage_whitespace](https://img.shields.io/badge/SwiftLint-operator__usage__whitespace-007A87.svg)](https://github.com/realm/SwiftLint/blob/master/Rules.md#operator-usage-whitespace) 600 | 601 |
602 | 603 | ```swift 604 | // Неправильно 605 | let capacity = 1+2 606 | let capacity = currentCapacity ?? 0 607 | let mask = (UIAccessibilityTraitButton|UIAccessibilityTraitSelected) 608 | let capacity=newCapacity 609 | let latitude = region.center.latitude - region.span.latitudeDelta/2.0 610 | 611 | // Правильно 612 | let capacity = 1 + 2 613 | let capacity = currentCapacity ?? 0 614 | let mask = (UIAccessibilityTraitButton | UIAccessibilityTraitSelected) 615 | let capacity = newCapacity 616 | let latitude = region.center.latitude - (region.span.latitudeDelta / 2.0) 617 | ``` 618 | 619 |
620 | 621 | ## Паттерны 622 | 623 | * # **Инициализируйте свойства в `init` где это возможно, а не используйте форс-анвраппинг.** Заметным исключением является UIViewController и его `view` свойство. [![SwiftLint: implicitly_unwrapped_optional](https://img.shields.io/badge/SwiftLint-implicitly__unwrapped__optional-007A87.svg)](https://github.com/realm/SwiftLint/blob/master/Rules.md#implicitly-unwrapped-optional) 624 | 625 |
626 | 627 | ```swift 628 | // Неправильно 629 | class MyClass: NSObject { 630 | 631 | var someValue: Int! 632 | 633 | init() { 634 | super.init() 635 | someValue = 5 636 | } 637 | 638 | } 639 | 640 | // Правильно 641 | class MyClass: NSObject { 642 | 643 | var someValue: Int 644 | 645 | init() { 646 | someValue = 0 647 | super.init() 648 | } 649 | 650 | } 651 | ``` 652 | 653 |
654 | 655 | * # **Избегайте выполнение любой значимой или времязатратной работы в `init()`.** Избегайте таких действий, как открытие соединения с базой данных, выполнение запросов в сеть, чтение большого объема данных с диска и т.п. Создайте метод вроде `start()` если вам нужно чтобы эти действия были выполнены до того как объект будет готов к использованию. 656 | 657 | * # **Выносите сложные наблюдатели свойств в методы.** Это уменьшает вложенность, отделяет сайд-эффекты от объявления и делает явным использование неявно передаваемых параметров, таких как `oldValue`. 658 | 659 |
660 | 661 | ```swift 662 | // Неправильно 663 | class TextField { 664 | var text: String? { 665 | didSet { 666 | guard oldValue != text else { 667 | return 668 | } 669 | 670 | // Куча побочных эффектов связанных с текстом 671 | } 672 | } 673 | } 674 | 675 | // Правильно 676 | class TextField { 677 | var text: String? { 678 | didSet { updateText(from: oldValue) } 679 | } 680 | 681 | private func updateText(from oldValue: String?) { 682 | guard oldValue != text else { 683 | return 684 | } 685 | 686 | // Куча побочных эффектов связанных с текстом 687 | } 688 | } 689 | ``` 690 | 691 |
692 | 693 | * # **Выносите сложные определения замыканий в методы**. Это уменьшает вложенность и сложность использования weak-self в блоках. Если необходимо сослаться на self в вызове замыкания, используйте `guard`, чтобы развернуть self на время вызова. 694 | 695 |
696 | 697 | ```swift 698 | // Неправильно 699 | class MyClass { 700 | 701 | func request(completion: () -> Void) { 702 | API.request { [weak self] response in 703 | if let strongSelf = self { 704 | // Processing and side effects 705 | } 706 | completion() 707 | } 708 | } 709 | } 710 | 711 | // Правильно 712 | class MyClass { 713 | 714 | func request(completion: () -> Void) { 715 | API.request { [weak self] response in 716 | guard let strongSelf = self else { 717 | return 718 | } 719 | strongSelf.doSomething(strongSelf.property) 720 | completion() 721 | } 722 | } 723 | 724 | func doSomething(nonOptionalParameter: SomeClass) { 725 | // Processing and side effects 726 | } 727 | } 728 | ``` 729 | 730 |
731 | 732 | * # **Используйте `guard` в начале скоупа.** 733 | 734 |
735 | 736 | #### Почему? 737 | Проще рассуждать о блоке кода, когда все операторы `guard` сгруппированы вверху, а не смешаны с бизнес-логикой. 738 | 739 |
740 | 741 | * # **Контроль доступа должен быть максимально строгим.** Предпочитайте использование `public` вместо `open` и `private` вместо `fileprivate` пока вам не понадобится это поведение. 742 | 743 |
744 | 745 | ```swift 746 | // Неправильно 747 | final class ViewController { 748 | 749 | @IBOutlet weak var tableView: UITableView! 750 | 751 | var models: [Model] = [] 752 | 753 | func reload() { 754 | // ... 755 | } 756 | 757 | } 758 | 759 | // Правильно 760 | final class ViewController { 761 | 762 | @IBOutlet private weak var tableView: UITableView! 763 | 764 | private var models: [Model] = [] 765 | 766 | private func reload() { 767 | // ... 768 | } 769 | 770 | } 771 | ``` 772 | 773 |
774 | 775 | * # **Избегайте глобальных функций где это возможно.** Предпочитайте методы в определениях типов. 776 | 777 |
778 | 779 | ```swift 780 | // Неправильно 781 | func age(of person, bornAt timeInterval) -> Int { 782 | // ... 783 | } 784 | 785 | func jump(person: Person) { 786 | // ... 787 | } 788 | 789 | // Правильно 790 | class Person { 791 | var bornAt: TimeInterval 792 | 793 | var age: Int { 794 | // ... 795 | } 796 | 797 | func jump() { 798 | // ... 799 | } 800 | } 801 | ``` 802 | 803 |
804 | 805 | * # **Предпочитайте выделять константы в закрытый enum.** Если константы должны быть открыты, сделайте их статичными внутри определения класса. 806 | 807 |
808 | 809 | ```swift 810 | public class MyClass { 811 | 812 | private enum Constants { 813 | static let privateValue = "private" 814 | } 815 | 816 | public static let publicValue = "public" 817 | 818 | func doSomething() { 819 | print(Constants.privateValue) 820 | print(MyClass.publicValue) 821 | } 822 | } 823 | ``` 824 | 825 |
826 | 827 | * # **Используйте `enum` без case для организации `public` или `internal` констант и функций в пространства имен.** Избегайте создания глобальных констант или функций. Не стесняйтесь вкладывать пространства имен, где это добавляет ясности. 828 | 829 |
830 | 831 | #### Почему? 832 | `enum`-ы без case хорошо работают как пространства имен так как они не могут быть созданы, что соответствует их назначению. 833 | 834 | ```swift 835 | enum Environment { 836 | 837 | enum Earth { 838 | static let gravity = 9.8 839 | } 840 | 841 | enum Moon { 842 | static let gravity = 1.6 843 | } 844 | } 845 | ``` 846 | 847 |
848 | 849 | * # **Используйте неизменяемые значения где это возможно.** Используйте `map` и `compactMap` вместо добавления в новую коллекцию. Используйте `filter` вмеcто удаления элементов из изменяемой коллекции. 850 | 851 |
852 | 853 | #### Почему? 854 | Изменяемые свойства увеличивают сложность, поэтому старайтесь держать их в максимально узкой области. 855 | 856 | ```swift 857 | // Неправильно 858 | var results = [SomeType]() 859 | for element in input { 860 | let result = transform(element) 861 | results.append(result) 862 | } 863 | 864 | // Правильно 865 | let results = input.map { transform($0) } 866 | ``` 867 | 868 | ```swift 869 | // Неправильно 870 | var results = [SomeType]() 871 | for element in input { 872 | if let result = transformThatReturnsAnOptional(element) { 873 | results.append(result) 874 | } 875 | } 876 | 877 | // Правильно 878 | let results = input.compactMap { transformThatReturnsAnOptional($0) } 879 | ``` 880 | 881 |
882 | 883 | * # **Классы должны быть `final`, если другого не требует логика.** 884 | 885 |
886 | 887 | #### Почему? 888 | Если класс должен быть переопределен, автор должен указать эту функциональность, опуская ключевое слово `final`. 889 | 890 | ```swift 891 | // Неправильно 892 | class SettingsRepository { 893 | // ... 894 | } 895 | 896 | // Правильно 897 | final class SettingsRepository { 898 | // ... 899 | } 900 | ``` 901 | 902 |
903 | 904 | * # **Никогда не используйте `default` case в `switch`.** 905 | 906 |
907 | 908 | #### Почему? 909 | Перечисление каждого case требует, чтобы разработчики и ревьюеры учитывали правильность каждого оператора switch при добавлении новых case. 910 | 911 | ```swift 912 | // Неправильно 913 | switch anEnum { 914 | case .a: 915 | // Do something 916 | default: 917 | // Do something else. 918 | } 919 | 920 | // Правильно 921 | switch anEnum { 922 | case .a: 923 | // Do something 924 | case .b, .c: 925 | // Do something else. 926 | } 927 | ``` 928 | 929 |
930 | 931 | * # **Проверьте значение nil вместо использования разворачивания, если значение не требуется.** [![SwiftLint: unused_optional_binding](https://img.shields.io/badge/SwiftLint-unused_optional_binding-007A87.svg)](https://github.com/realm/SwiftLint/blob/master/Rules.md#unused-optional-binding) 932 | 933 | ## Организация файлов 934 | 935 | * # **Сортируйте импорты по алфавиту и ставьте их после комментариев в заголовке файла.** Если одна из библиотек уже импортирует какие-то библиотеки необходимые в вашем модуле - их импорты можно опустить. [![SwiftFormat: sortedImports](https://img.shields.io/badge/SwiftFormat-sortedImports-7B0051.svg)](https://github.com/nicklockwood/SwiftFormat/blob/master/Rules.md#sortedImports) 936 | 937 |
938 | 939 | #### Почему? 940 | Стандартный метод организации помогает инженерам быстрее определить, от каких модулей зависит файл. 941 | 942 | ```swift 943 | // Неправильно 944 | 945 | // Copyright © 2018 Surf. All rights reserved. 946 | // 947 | import DLSPrimitives 948 | import Constellation 949 | import Epoxy 950 | 951 | import UIKit 952 | import Foundation 953 | 954 | // Правильно 955 | 956 | // Copyright © 2018 Surf. All rights reserved. 957 | // 958 | 959 | import Constellation 960 | import DLSPrimitives 961 | import Epoxy 962 | import UIKit 963 | ``` 964 | 965 |
966 | 967 | _Исключение: `@testable import` должны быть сгурппированы после обычных import и разделены пустой строкой._ 968 | 969 |
970 | 971 | ```swift 972 | // Неправильно 973 | 974 | // Copyright © 2018 Surf. All rights reserved. 975 | // 976 | 977 | import DLSPrimitives 978 | @testable import Epoxy 979 | import Foundation 980 | import Nimble 981 | import Quick 982 | 983 | // Правильно 984 | 985 | // Copyright © 2018 Surf. All rights reserved. 986 | // 987 | 988 | import DLSPrimitives 989 | import Foundation 990 | import Nimble 991 | import Quick 992 | 993 | @testable import Epoxy 994 | ``` 995 | 996 |
997 | 998 | * # **Ограничьте пустые вертикальные пробелы одной строкой.** [![SwiftLint: vertical_whitespace](https://img.shields.io/badge/SwiftLint-vertical__whitespace-007A87.svg)](https://github.com/realm/SwiftLint/blob/master/Rules.md#vertical-whitespace) 999 | 1000 | * # **Файлы должны заканчиваться новой строкой.** [![SwiftLint: trailing_newline](https://img.shields.io/badge/SwiftLint-trailing__newline-007A87.svg)](https://github.com/realm/SwiftLint/blob/master/Rules.md#trailing-newline) 1001 | 1002 | ## Совместимость с Objective-C 1003 | 1004 | * # **Старайтесь избегать наследования от NSObject.** Если ваш код должен быть использован каким-нибудь Objective-C кодом оберните его чтобы предоставить необходимую функциональность. Используйте `@objc` для отдельных функций и переменных вместо предоставления всего API класса при помощи `@objcMembers`. 1005 | 1006 |
1007 | 1008 | ```swift 1009 | class PriceBreakdownViewController { 1010 | 1011 | private let acceptButton = UIButton() 1012 | 1013 | private func setUpAcceptButton() { 1014 | acceptButton.addTarget( 1015 | self, 1016 | action: #selector(didTapAcceptButton), 1017 | forControlEvents: .TouchUpInside) 1018 | } 1019 | 1020 | @objc 1021 | private func didTapAcceptButton() { 1022 | // ... 1023 | } 1024 | } 1025 | ``` 1026 | 1027 |
1028 | -------------------------------------------------------------------------------- /resources/surf.swiftformat: -------------------------------------------------------------------------------- 1 | # options 2 | --self init-only # redundantSelf 3 | --importgrouping testable-bottom # sortedImports 4 | --trimwhitespace always # trailingSpace 5 | 6 | # rules 7 | --rules redundantParens,redundantSelf,sortedImports,trailingSpace -------------------------------------------------------------------------------- /resources/swiftlint.yml: -------------------------------------------------------------------------------- 1 | whitelist_rules: 2 | - attributes 3 | - class_delegate_protocol 4 | - closing_brace 5 | - closure_end_indentation 6 | - closure_parameter_position 7 | - closure_spacing 8 | - collection_alignment 9 | - colon 10 | - comma 11 | - conditional_returns_on_newline 12 | - control_statement 13 | - convenience_type 14 | - custom_rules 15 | - cyclomatic_complexity 16 | - discouraged_optional_boolean 17 | - duplicate_imports 18 | - empty_count 19 | - empty_parameters 20 | - empty_parentheses_with_trailing_closure 21 | - empty_string 22 | - explicit_init 23 | - file_length 24 | - first_where 25 | - force_cast 26 | - force_try 27 | - force_unwrapping 28 | - function_parameter_count 29 | - implicit_getter 30 | - implicitly_unwrapped_optional 31 | - inert_defer 32 | - large_tuple 33 | - last_where 34 | - leading_whitespace 35 | - legacy_cggeometry_functions 36 | - legacy_constant 37 | - legacy_constructor 38 | - legacy_hashing 39 | - legacy_nsgeometry_functions 40 | - line_length 41 | - literal_expression_end_indentation 42 | - mark 43 | - multiline_arguments 44 | - multiline_literal_brackets 45 | - notification_center_detachment 46 | - opening_brace 47 | - operator_usage_whitespace 48 | - redundant_discardable_let 49 | - redundant_optional_initialization 50 | - redundant_nil_coalescing 51 | - redundant_void_return 52 | - return_arrow_whitespace 53 | - shorthand_operator 54 | - statement_position 55 | - syntactic_sugar 56 | - todo 57 | - toggle_bool 58 | - trailing_comma 59 | - trailing_newline 60 | - trailing_semicolon 61 | - trailing_whitespace 62 | - unused_import 63 | - unused_optional_binding 64 | - unused_setter_value 65 | - vertical_whitespace 66 | - void_return 67 | - weak_delegate 68 | 69 | disabled_rules: # rule identifiers to exclude from running 70 | 71 | opt_in_rules: # some rules are only opt-in 72 | 73 | excluded: # paths to ignore during linting. Takes precedence over `included`. 74 | - fastlane 75 | - Pods 76 | - .bundle 77 | 78 | custom_rules: 79 | image_name_initialization: # Disable UIImage init from name 80 | included: ".*.swift" 81 | name: "Image initialization" 82 | regex: 'UIImage\(named:[^)]+\)' 83 | message: "Use UIImage(assetName: ) instead" 84 | severity: error 85 | 86 | open_iboutlets: 87 | included: ".*.swift" 88 | name: "IBOutlet opening" 89 | regex: "@IBOutlet ?(weak){0,1} var" 90 | message: "IBOutlet should be private or fileprivate" 91 | severity: error 92 | 93 | open_ibaction: 94 | included: ".*.swift" 95 | name: "IBAction opening" 96 | regex: "@IBAction func" 97 | message: "IBAction should be private or fileprivate" 98 | severity: error 99 | 100 | line_length: 120 101 | 102 | file_length: 103 | warning: 500 104 | error: 1200 105 | 106 | reporter: "xcode" # reporter type (xcode, json, csv, checkstyle, junit) 107 | -------------------------------------------------------------------------------- /resources/xcode_settings.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | defaults write com.apple.dt.Xcode DVTTextEditorTrimTrailingWhitespace -bool YES 6 | defaults write com.apple.dt.Xcode DVTTextEditorTrimWhitespaceOnlyLines -bool YES 7 | 8 | defaults write com.apple.dt.Xcode DVTTextIndentTabWidth -int 4 9 | defaults write com.apple.dt.Xcode DVTTextIndentWidth -int 4 10 | 11 | defaults write com.apple.dt.Xcode DVTTextPageGuideLocation -int 120 --------------------------------------------------------------------------------