Вложенные типы в Swift

swift

#1

Вложенный тип - тип объявленный внутри класса или структуры (метода или замыкания).

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

Будем делить их на:

  • Статические
  • Локальные
  • Анонимные

Анонимных типов в чистом виде нет, но «локальные типы» вкупе с замыканиями позволяют им быть :slight_smile:

Статические вложенные типы доступны точно как статические поля:

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 вызывает замыкание переданное в качестве параметра :slight_smile:

Использовать достаточно просто:

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 у нас новый делегат, который передает нам текст, который мы используем по своему усмотрению.

Весь класс выглядет так:

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


Вложенные типы в Swift (дополнение)