Привет, Хабр. Перевод подготовлен в рамках онлайн-курса "iOS Developer. Basic".
Приглашаем всех желающих на бесплатный двухдневный интенсив «Создание простейшего приложения без единой строчки кода». В первый день узнаем:1. Что такое XCode?
2. Как "рисуются экраны"
3. Добавим на экраны кнопки и поля ввода. Создадим экран авторизации.
4. Создадим второй экран нашего приложения и добавим переход на него из окна авторизации.
Зарегистрироваться можно здесь.
Добавьте в Swift свою собственную изюминку
Давайте будем честными. Фреймворки Swift и Apple не обладают всей функциональностью, необходимой при создании лучшего программного обеспечения для устройств Apple. К счастью, Swift поддерживает расширения, чтобы мы могли добавлять недостающие части, необходимые для более удобной работы.
Если вы новичок в Swift, пожалуйста, обратитесь к документации, чтобы узнать больше о Расширениях перед тем, как продолжить.
В этой статье я сосредоточусь на расширениях, которые добавляют дополнительные функциональные возможности к существующим типам. Расширения также могут добавлять функции по умолчанию для протоколов, добавлять ограничения для типов протоколов и многое другое.
При создании собственных расширений я бы порекомендовал создать несколько юнит-тестов для проверки их выполнения, чтобы удостовериться, что вы получаете желаемый результат.
Стремясь к тому, чтобы содержание ниже было относительно кратким, я не включил в описание наши юнит-тесты.
Вы можете найти Xcode Playground, используемый в этой статье, на моей странице GitHub.
Вот только 10 из многих расширений, которые мы используем в Livefront.
1. UIView — Ограничения
Добавление ограничений к UIView
import PlaygroundSupport
import UIKit
// Extension #1 - A helper method to add a view to another with top, left, bottom, and right constraints.
extension UIView {
/// Add a subview, constrained to the specified top, left, bottom and right margins.
///
/// - Parameters:
/// - view: The subview to add.
/// - top: Optional top margin constant.
/// - left: Optional left (leading) margin constant.
/// - bottom: Optional bottom margin constant.
/// - right: Optional right (trailing) margin constant.
///
func addConstrained(subview: UIView,
top: CGFloat? = 0,
left: CGFloat? = 0,
bottom: CGFloat? = 0,
right: CGFloat? = 0) {
subview.translatesAutoresizingMaskIntoConstraints = false
addSubview(subview)
if let top = top {
subview.topAnchor.constraint(equalTo: topAnchor, constant: top).isActive = true
}
if let left = left {
subview.leadingAnchor.constraint(equalTo: leadingAnchor, constant: left).isActive = true
}
if let bottom = bottom {
subview.bottomAnchor.constraint(equalTo: bottomAnchor, constant: bottom).isActive = true
}
if let right = right {
subview.trailingAnchor.constraint(equalTo: trailingAnchor, constant: right).isActive = true
}
}
}
// Implementation
class ViewController: UIViewController {
let newView = UIView()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBlue
newView.backgroundColor = .systemTeal
view.addConstrained(subview: newView, top: 50, left: 100, right: -100)
}
}
let viewController = ViewController()
PlaygroundPage.current.liveView = viewController
Вместо того, чтобы не забывать устанавливать translatesAutoresizingMaskIntoConstraints
в false
, добавлять отображение в родительское представление и устанавливать все индивидуальные ограничения, этот хелпер-метод выполнит все эти действия за вас. Этот метод позволяет установить верхние, передние, задние и нижние ограничения для родительского представления. Если вы опустите один из параметров ограничения, ограничение будет иметь нулевое значение, прикрепляя отображение к краю родительского представления. Чтобы полностью закрыть родительское представление, опустите все параметры ограничений.
2. Дата - Дата по всемирному координатному времени (UTC Date)
Создание объекта Date из строки в часовом поясе UTC
import Foundation
// Extension #2 - Create a date object from a date string with the UTC timezone.
//Inspired by: https://developer.apple.com/library/archive/qa/qa1480/_index.html
extension Date {
/// Returns a date from the provided string.
///
/// - Parameter utcString: The string used to create the date.
///
/// - Returns: A date from the provided string.
///
static func utcDate(from utcString: String) -> Date? {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(abbreviation: "UTC")!
return formatter.date(from: utcString)
}
}
// Implementation
let utcDateString = "2021-04-03T14:00:00.000Z"
let utcDate = Date.utcDate(from: utcDateString) //Playgrounds will show this in the machine's timezone.
print(utcDate!)
API REST обычно возвращает строку даты в часовом поясе UTC. Вышеуказанный статический метод позволяет преобразовать строку в объект Date
. Если у вас возникли проблемы с этим расширением в вашем собственном проекте, убедитесь, что dateFormat
соответствует формату строки даты, которую вы получаете.
3. String (Строка) — получение URL-адресов
Получить действительные URL-адреса из строки
import Foundation
// Extension #3 - Retrieves valid URLs from a given string.
//Credit - Thanks to Paul Hudson for the core functionality on this extension.
//Source - https://www.hackingwithswift.com/example-code/strings/how-to-detect-a-url-in-a-string-using-nsdatadetector
extension String {
/// Searches through a string to find valid URLs.
/// - Returns: An array of found URLs.
func getURLs() -> [URL] {
var foundUrls = [URL]()
guard let detector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) else {
return foundUrls
}
let matches = detector.matches(
in: self,
options: [],
range: NSRange(location: 0, length: self.utf16.count)
)
for match in matches {
guard let range = Range(match.range, in: self),
let retrievedURL = URL(string: String(self[range])) else { continue }
foundUrls.append(retrievedURL)
}
return foundUrls
}
}
// Implementation
let unfilteredString = "To get the best search results, go to https://www.google.com, www.duckduckgo.com, or www.bing.com"
let urls = unfilteredString.getURLs()
Этот хелпер-метод очень удобен, когда у вас есть несколько URL в заданной строке. Я бы настоятельно рекомендовал написать несколько юнит-тестов, чтобы убедиться, что этот метод извлекает предполагаемые URL-адреса для вашего конкретного JSON-ответа.
4. UIStackView — удаление представлений
Удаление всех subviews из UIStackView
import UIKit
// Extension #4 - Removes all views from a UIStackView.
extension UIStackView {
/// Removes all arranged subviews and their constraints from the view.
func removeAllArrangedSubviews() {
arrangedSubviews.forEach {
self.removeArrangedSubview($0)
NSLayoutConstraint.deactivate($0.constraints)
$0.removeFromSuperview()
}
}
}
// Implementation
let view1 = UIView()
let view2 = UIView()
let view3 = UIView()
let stackView = UIStackView()
//Add subviews to stackView
stackView.addArrangedSubview(view1)
stackView.addArrangedSubview(view2)
stackView.addArrangedSubview(view3)
//Confirm stackView contains 3 views
stackView.arrangedSubviews.count //3
//Remove views from stackView
stackView.removeAllArrangedSubviews()
//Confirm stackView doesn't contain any subviews now
stackView.arrangedSubviews.count //0
Есть несколько шагов, которые должны быть выполнены при удалении отображений из UIStackView
, такие как удаление их из самого отображения стека, деактивация любых ограничений, и полное удаление из родительского отображения. Этот хелпер-метод позаботится обо всех этих шагах за вас.
5. Bundle — Версия приложения и номер сборки
Получить версию приложения и номер сборки
import Foundation
// Extension #5 - retrieve the app version # and build #.
//Inspired by https://stackoverflow.com/questions/25965239/how-do-i-get-the-app-version-and-build-number-using-swift
extension Bundle {
/// Retrieve the app version # from Bundle
var releaseVersionNumber: String? {
return infoDictionary?["CFBundleShortVersionString"] as? String
}
/// Retrieve the build version # from Bundle
var buildVersionNumber: String? {
return infoDictionary?["CFBundleVersion"] as? String
}
}
// Implementation
let releaseVersionNumber = Bundle.main.releaseVersionNumber
let buildVersionNumber = Bundle.main.buildVersionNumber
Это одна из тех особенностей, которые должны быть включены в Bundle
. Вместо того, чтобы пытаться запомнить непонятный ключ словаря, эти вычисляемые свойства помогут в извлечении версии приложения и номера сборки. Многие приложения включают номер версии в меню настроек.
6. Календарь — предыдущий год
Определение прошлого года по типу Integer
import Foundation
// Extension #6 - Get the prior year as an integer
extension Calendar {
/// Returns the prior year as an integer.
///
/// - Returns: Returns last year's year as an integer.
func priorYear() -> Int {
guard let priorYear = date(byAdding: .year, value: -1, to: Date()) else {
return component(.year, from: Date()) - 1
}
return component(.year, from: priorYear)
}
}
//Implementation
let priorYearAsNumber = Calendar.current.priorYear()
Здесь все довольно прямолинейно. Метод вернет предыдущий год в виде Integer.
7. UIStackView — Удобство Init
Удобство Init (инициализация) чтобы упростить создание
import PlaygroundSupport
import UIKit
// Extension #7 - Make UIStackView creation a lot easier.
extension UIStackView {
/// `UIStackView` convenience initializer for creating a stack view with arranged subviews, an
/// axis and spacing.
///
/// - Parameters:
/// - alignment: The alignment of the arranged subviews perpendicular to the stack view’s
/// axis.
/// - arrangedSubviews: The subviews to arrange in the `UIStackView`.
/// - axis: The axis that the subviews should be arranged around.
/// - distribution: The distribution of the arranged views along the stack view’s axis.
/// - spacing: The spacing to place between each arranged subview. Defaults to 0.
///
convenience init(alignment: UIStackView.Alignment = .fill,
arrangedSubviews: [UIView],
axis: NSLayoutConstraint.Axis,
distribution: UIStackView.Distribution = .fill,
spacing: CGFloat = 0) {
arrangedSubviews.forEach { $0.translatesAutoresizingMaskIntoConstraints = false }
self.init(arrangedSubviews: arrangedSubviews)
self.alignment = alignment
self.axis = axis
self.distribution = distribution
self.spacing = spacing
}
}
// Implementation
let view1 = UIView()
view1.backgroundColor = .systemPink
let view2 = UIView()
view2.backgroundColor = .systemOrange
let view3 = UIView()
view3.backgroundColor = .systemTeal
let stackView = UIStackView(alignment: .leading,
arrangedSubviews: [view1, view2, view3],
axis: .vertical,
distribution: .fill,
spacing: 20)
let view = UIView(frame: CGRect(x: 0, y: 0, width: 500, height: 500))
view.backgroundColor = .systemBlue
view.addSubview(stackView)
stackView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
view1.heightAnchor.constraint(equalToConstant: 50),
view1.widthAnchor.constraint(equalToConstant: 150),
view2.heightAnchor.constraint(equalToConstant: 50),
view2.widthAnchor.constraint(equalToConstant: 150),
view3.heightAnchor.constraint(equalToConstant: 50),
view3.widthAnchor.constraint(equalToConstant: 150),
stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
])
PlaygroundPage.current.liveView = view
Запомнить, какие свойства нужно установить для UIStackView
, может быть непросто. Этот удобный инициализатор включает общие свойства в качестве своих параметров. Инициализатор также устанавливает translatesAutoresizingMaskIntoConstraints
в false
для каждого из представлений.
8. UIColor — Hex
Получение Hex (шестнадцатеричного) значения UIColor
import UIKit
// Extension #8 - generates a string with the hex color value.
//Inspired by: https://stackoverflow.com/a/26341062
extension UIColor {
// MARK: - Helper Functions
/// Returns the hex string for this `UIColor`. For example: `#FFFFFF` or `#222222AB` if the alpha value is included.
///
/// - Parameter includeAlpha: A boolean indicating if the alpha value should be included in the returned hex string.
///
/// - Returns: The hex string for this `UIColor`. For example: `#FFFFFF` or
/// `#222222AB` if the alpha value is included.
///
func hexString(includeAlpha: Bool = false) -> String {
let components = cgColor.components
let red: CGFloat = components?[0] ?? 0.0
let green: CGFloat = components?[1] ?? 0.0
let blue: CGFloat = components?[2] ?? 0.0
let alpha: CGFloat = components?[3] ?? 0.0
let hexString = String.init(
format: "#%02lX%02lX%02lX%02lX",
lroundf(Float(red * 255)),
lroundf(Float(green * 255)),
lroundf(Float(blue * 255)),
lroundf(Float(alpha * 255))
)
return includeAlpha ? hexString : String(hexString.dropLast(2))
}
}
// Implementation
let whiteColor = UIColor(displayP3Red: 1, green: 1, blue: 1, alpha: 1)
let whiteHexString = whiteColor.hexString() //#FFFFFF
let blackColor = UIColor(displayP3Red: 0, green: 0, blue: 0, alpha: 1)
let blackHexString = blackColor.hexString() //#000000
Этот метод извлекает шестнадцатеричное значение UIColor
и возвращает его в виде String
. Это может быть очень полезно, если вы хотите сохранить и запомнить значение цвета для пользователя. Таким образом, вам нужно сохранить только шестнадцатеричную строку вместо трех целочисленных значений RGB.
9. UIViewController — Темный режим
Проверьте, включен ли темный режим
import UIKit
// Extension #9
extension UIViewController {
/// Gets a flag indicating whether or not the UI is in dark mode.
public var isDarkMode: Bool {
if #available(iOS 12.0, *) {
return traitCollection.userInterfaceStyle == .dark
}
return false
}
}
UIColors
, такие как .label
, .systemBlue
и т.д., автоматически настраиваются, когда пользователь переключается между светлым и темным режимом, но вы может быть захотите добавить дополнительные функции, когда пользователь переключает внешний вид устройства. Это вычисляемое свойство позволит вам проверить, какой внешний вид активен, чтобы вы могли соответствующим образом отреагировать.
10. UICollectionView — Последний IndexPath
Получить последний indexPath для collectionView
import PlaygroundSupport
import UIKit
// Extension #10 - get the last valid indexPath in a UICollectionView.
extension UICollectionView {
/// Validates whether an `IndexPath` is a valid index path for an item in a collection view.
///
/// - Parameter indexPath: The index path to validate.
/// - Returns: `true` if the index path represents an item in the collection view or false
/// otherwise.
///
func isValid(_ indexPath: IndexPath) -> Bool {
guard indexPath.section < numberOfSections,
indexPath.item < numberOfItems(inSection: indexPath.section)
else {
return false
}
return true
}
/// Provides the last valid `indexPath` in the collection view.
/// - Parameter section: The section used to provide the last `indexPath`.
/// - Returns: the last valid `indexPath` in the collection view or nil if not a valid `indexPath`.
func lastIndexPath(in section: Int) -> IndexPath? {
let lastIndexPath = IndexPath(row: numberOfItems(inSection: section) - 1, section: section)
guard isValid(lastIndexPath) else { return nil }
return lastIndexPath
}
}
// Implementation
class CollectionViewController: UICollectionViewController {
let items = Array(1...100)
override func viewDidLoad() {
super.viewDidLoad()
collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "cell")
}
override func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return items.count
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
cell.backgroundColor = .systemBlue
return cell
}
}
let collectionViewController = CollectionViewController(collectionViewLayout: UICollectionViewFlowLayout())
let lastIndexPath = collectionViewController.collectionView.lastIndexPath(in: 0)
lastIndexPath?.section //0
lastIndexPath?.row //99
PlaygroundPage.current.liveView = collectionViewController
Наконец, в UICollectionView
добавлен метод, который возвращает последний допустимый indexPath
. Это еще одна из тех функций, которая, кажется, уже должна существовать в UIKit. Хотя это может быть достигнуто путем подсчета количества элементов в collectionView
и вычитания одного в контроллере представления; добавление его через расширение немного безопаснее.
Резюме
Я бы сказал, что практически невозможно создать проект без добавления хотя бы одного расширения. Добавление функциональности с помощью расширений делает Swift более мощным и позволяет создавать новые функции безопасным способом. Я советую вам поискать в Интернете "Расширения Swift" и получить удовлетворение от всех творческих решений, которые придумали наши коллеги-разработчики.
Не стесняйтесь поделиться своим любимым расширением (расширениями) в комментарии ниже.
Ресурсы:
Ограничения типов с помощью расширений
Узнать подробнее о курсе "iOS Developer. Basic"
Участвовать в интенсиве «Создание простейшего приложения без единой строчки кода»
storoj
Я не сразу врубился о каких ограничениях идёт речь...
API функции не очень понятен. Почему офсеты optional, но имеют дефолтное значение
0
, а неnil
? Предполагается, что по-умолчанию subview должна растягиваться на всю площадь родителя? На моей практике это очень редкий юзкейс, чтобы быть дефолтом.Для меня подобное поведение было бы неожиданным, я бы ожидал ровно обратного: по-умолчанию не происходит ничего, но если я задал офсет (в том числе нулевой) – то он применяется.
Про саму целесообразность введения подобного расширения можно порассуждать отдельно.
storoj
теперь врубился, статья-перевод