Вложенный тип - тип объявленный внутри класса или структуры (метода или замыкания).
В Свифте вложенные типы никак не делятся, для удобства разделим их по области видимости например как в Java, только с одной оговоркой, все вложенные типы в Свифте статические, то есть не имеют доступа к не статическим методам и полям внешнего типа.
Будем делить их на:
- Статические
- Локальные
- Анонимные
Анонимных типов в чистом виде нет, но «локальные типы» вкупе с замыканиями позволяют им быть
Статические вложенные типы доступны точно как статические поля:
class A {
class B {
}
}
let b = A.B()
Локальные типы объявляется внутри метода и соответственно видны только внутри метода:
class A {
func method() {
class B {
}
let b = B()
}
}
Анонимные типы не видены ниоткуда. Остановимся на этом поподробнее.
Имеем протокол Runnable с методом run:
protocol Runnable {
func run()
}
Имеем класс A с методом test который в качестве параметра принимает протокол Runnable:
class A {
func test(r: Runnable) {
r.run()
}
}
Методу test неважно что в него передадут, он только знает что это Runnable у которого можно вызвать run.
По этому в него вполне можно пережать замыкание которое будет возвращать Runnable, класс в этом замыкании и будет анонимный тип:
let a = A()
a.test(r: {
class R: Runnable {
func run() {
print("RUN")
}
}
return R()
}())
Рассмотрим пару примеров.
Пример первый - Builder
Представим что у нас есть какой-то сервер на который нам нужно отправлять данные. К примеру возьмем Google Tag Manager, SDK которого содержит метод send(dict: [String: String]) и в этот метод нам нужно передать словарь вида:
{"event":"value", "eventCategory":"value", "eventAction":"value", "eventLabel":"value"}
Передавать нужно часто, в зависимости от действий пользователя, например:
class ViewController: UIViewController {
override func viewDidAppear(_ animated: Bool) {
GTM.shared.send(dict: ["event": "MyEvent", "evantAction": "Visibility", "eventLabel": "Open", "eventCategory": "Main"])
}
override func viewDidDisappear(_ animated: Bool) {
GTM.shared.send(dict: ["event": "MyEvent", "evantAction": "Visibility", "eventLabel": "CLose", "eventCategory": "Main"])
}
@IBAction func click() {
GTM.shared.send(dict: ["event": "MyEvent", "evantAction": "Action", "eventLabel": "Click", "eventCategory": "Main"])
}
}
И так в каждом контроллере, а у нас их 100, легко можно ошибиться и вписать «CLose» вместо Close, да и выглядит как то не очень, загромождает код.
Для решения подобнго рода задач удобно использовать шаблон Builder.
Создадим Builder у которого будет несколько вложенных типов:
struct GTMEvent {
private(set) var dict = ["event": "MyEvent"]
enum Target {
case main
case detail
}
enum Visibility {
case open(from: Target)
case close(from: Target)
}
enum Action {
case click(from: Target)
}
private static func target(_ target: Target, dict: inout [String: String]) {
switch target {
case .main:
dict["eventCategory"] = "Main"
case .detail:
dict["eventCategory"] = "Detail"
}
}
static func action(_ action: Action) -> GTMEvent {
var event = GTMEvent()
event.dict["evantAction"] = "Action"
if case .click(let target) = action {
event.dict["eventLabel"] = "Click"
self.target(target, dict: &event.dict)
}
return event
}
static func visibility(_ visibility: Visibility) -> GTMEvent {
var event = GTMEvent()
event.dict["evantAction"] = "Visibility"
switch visibility {
case .open(let target):
event.dict["eventLabel"] = "Open"
self.target(target, dict: &event.dict)
case .close(let target):
event.dict["eventLabel"] = "Close"
self.target(target, dict: &event.dict)
}
return event
}
}
GTMEvent у которого словарь dict, который заполняется в зависимости от методов и параметров переданных в них.
Теперь создадим протокол с реализацией по умолчанию, который будет использовать GTMEvent:
protocol GTMEventSupport {}
extension GTMEventSupport {
func send(_ event: GTMEvent) {
GTM.shared.send(dict: event.dict)
}
}
Всё что осталось это расширить контроллеры с помощью этого протокола и можно с комфортом отсылать сообщения на сервер:
class ViewController: UIViewController, GTMEventSupport {
override func viewDidAppear(_ animated: Bool) {
send(.visibility(.open(from: .main)))
}
override func viewDidDisappear(_ animated: Bool) {
send(.visibility(.close(from: .main)))
}
@IBAction func click() {
send(.action(.click(from: .main)))
}
}
Удобство данного подхода я думаю очевидно.
Пример второй - Delegate
Второй пример слегка притянутый, но интересный в плане реализации. И так в качестве делегатов у нас будут выступать анонимные классы. Во избежании циклических ссылок, ссылки на делегаты всегда слабые (по крайней мере должны быть), нам это не подходит т.к. у нас будет одна ссылка на анонимный класс и если она будет слабой, этот класс выгрузится из памяти.
По этому создадим TextField подкласс UITextField у которого «сильный» superDelegate:
class TextField: UITextField {
var superDelegate: UITextFieldDelegate? { didSet { delegate = superDelegate } }
}
Во вью контроллере имеем несколько текстовых полей из которых будем заполнять форму:
class ViewController: UIViewController {
struct Form {
var name: String?
var email: String?
var password: String?
}
@IBOutlet weak var nameTextField: TextField!
@IBOutlet weak var emailTextField: TextField!
@IBOutlet weak var passTextField: TextField!
}
Создадим замыкание у которого в качестве параметра замыкание и возвращает UITextFieldDelegate:
let textFieldDidEndEditing = { (callback: @escaping (_ text: String) -> ()) -> UITextFieldDelegate in
class TextFieldDelegate: NSObject, UITextFieldDelegate {
var callBack: ((_ text: String) -> ())!
func textFieldDidEndEditing(_ textField: UITextField, reason: UITextFieldDidEndEditingReason) {
callBack(textField.text!)
}
}
let delegate = TextFieldDelegate()
delegate.callBack = callback
return delegate
}
В замыкании класс TextFieldDelegate который в методе textFieldDidEndEditing вызывает замыкание переданное в качестве параметра
Использовать достаточно просто:
var form = Form()
nameTextField.superDelegate = textFieldDidEndEditing { text in
form.name = text
print(form)
}
emailTextField.superDelegate = textFieldDidEndEditing { text in
form.email = text
print(form)
}
passTextField.superDelegate = textFieldDidEndEditing { text in
form.password = text
print(form)
}
При каждом обращении к textFieldDidEndEditing у нас новый делегат, который передает нам текст, который мы используем по своему усмотрению.
Весь класс выглядет так:
Вложенные типы помимо группировки по логическому соответствию, за счёт области видимости позволяют управлять уровнем доступа, ко всему прочему еще и улучшают читаемость кода. Весьма и весьма удобно, советую взять на вооружение