07 June 2024

Swift에서는 Protocol을 제외한 대부분의 타입은 타입 내부에서 다른 타입을 정의할 수 있었습니다.

struct Parent {
    class ChildClass {} // ✅
    struct ChildStruct {} // ✅
    enum ChildEnum {} // ✅
    protocol ChildProtocol {} // ❌
}

이로 인해 Protocol 이름은 길어질 수밖에 없었습니다.

protocol ParentChildProtocol {}

이는 Swift 5.10 이전까지 Protocol을 사용하던 방식이었습니다. 그러나 Swift 5.10부터는 중첩 프로토콜을 사용할 수 있게 되었습니다. SE-0404 Allow Protocols to be Nested in Non-Generic Contexts 덕분입니다.

이제 Struct, Class, Enum과 같이 Protocol도 타입 내부에서 정의할 수 있게 되었습니다. 중첩 프로토콜을 사용하면 타입의 구조를 더욱 조직화하고 캡슐화할 수 있습니다.

// FileName : RootInterface.swift
enum Root {}

extension Root {
    protocol Serviceable {
        func doSomething()
    }

    protocol Interactable {
        var service: Serviceable { get }
        func doSomething()
    }
}

// FileName : RootImplement.swift
extension Root {
    struct Service: Serviceable {
        func doSomething() {
            print("Service did something")
        }
    }

    struct Interactor: Interactable {
        let service: any Serviceable
        init(service: some Serviceable) {
            self.service = service
        }

        func doSomething() {
            service.doSomething()
        }
    }
}

// 중첩 프로토콜과 구현 타입을 사용하는 예시
let service = Root.Service()
let interactor = Root.Interactor(service: service)
interactor.doSomething() // "Service did something" 출력

RIBs

RIBs 아키텍처는 Router, Interactor, Builder, Presenter 등으로 구성됩니다. 각 구성 요소는 Protocol로 추상화합니다.

/// FileName : LoggedInBuilder.swift
protocol LoggedInDependency: Dependency {}
protocol LoggedInBuildable: Buildable {
    func build(withListener listener: LoggedInListener) -> LoggedInRouting
}

/// FileName : LoggedInteractor.swift
protocol LoggedInRouting: ViewableRouting {}
protocol LoggedInPresentable: Presentable {
    var listener: LoggedInPresentableListener? { get set }
}
protocol LoggedInListener: AnyObject {}

/// FileName : LoggedRouter.swift
protocol LoggedInInteractable: Interactable {
    var router: LoggedInRouting? { get set }
    var listener: LoggedInListener? { get set }
}
protocol LoggedInViewControllable: ViewControllable {}

/// FileName : LoggedViewController.swift
protocol LoggedInPresentableListener {}

중첩 프로토콜이 도입되기 전에는 위와 같이 Protocol 이름이 길어질 수밖에 없었습니다. 하지만, 중첩 프로토콜을 사용한다면 네임스페이스 역할을 해주는 타입 아래에 코드를 조직화할 수 있게 되었습니다.

다음은 LoggedInRIB에 중첩 프로토콜을 적용한 코드입니다.

/// FileName: LoggedIn.swift
enum LoggedIn {}

/// FileName: LoggedIn+Builder.swift
extension LoggedIn {
    protocol Dependency: RIBs.Dependency {}

    protocol Buildable: RIBs.Buildable {
        func build(withListener listener: Listener) -> RIBs.Routing
    }
}

extension LoggedIn {
    final class Component: RIBs.Component<Dependency> {}

    final class Builder: RIBs.Builder<Dependency>, Buildable {
        override init(dependency: Dependency) {
            super.init(dependency: dependency)
        }

        func build(withListener listener: Listener) -> RIBs.Routing {
            let component = Component(dependency: dependency)
            let viewController = ViewController()
            let interactor = Interactor(presenter: viewController)
            interactor.listener = listener
            return Router(interactor: interactor, viewController: viewController)
        }
    }
}

/// FileName: LoggedIn+Interactor.swift
extension LoggedIn {
    protocol Routing: RIBs.ViewableRouting {}
    protocol Presentable: RIBs.Presentable {
        var listener: PresentableListener? { get set }
    }

    protocol Listener: AnyObject {}
}

extension LoggedIn {
    final class Interactor: PresentableInteractor<Presentable>, Interactable, PresentableListener {
        weak var router: Routing?
        weak var listener: Listener?
        // TODO: Add additional dependencies to constructor. Do not perform any logic
        // in constructor.
        override init(presenter: Presentable) {
            super.init(presenter: presenter)
            presenter.listener = self
        }

        override func didBecomeActive() {
            super.didBecomeActive()
            // TODO: Implement business logic here.
        }

        override func willResignActive() {
            super.willResignActive()
            // TODO: Pause any business logic.
        }
    }
}

/// FileName : LoggedIn+Router.swift
extension LoggedIn {
    protocol Interactable: RIBs.Interactable {
        var router: Routing? { get set }
        var listener: Listener? { get set }
    }

    protocol ViewControllable: RIBs.ViewControllable {}
}

extension LoggedIn {
    final class Router: ViewableRouter<Interactable, ViewControllable>, Routing {
        // TODO: Constructor inject child builder protocols to allow building children.
        override init(interactor: Interactable, viewController: ViewControllable) {
            super.init(interactor: interactor, viewController: viewController)
            interactor.router = self
        }
    }
}

/// FileName : LoggedIn+ViewController.swift
extension LoggedIn {
    protocol PresentableListener: AnyObject {}
}

extension LoggedIn {
    final class ViewController: UIViewController, Presentable, ViewControllable {
        weak var listener: PresentableListener?
    }
}

정리

  • Swift 5.10 이전에는 프로토콜은 네임스페이스 기능을 제공하지 못했으나, Swift 5.10 이후부터는 중첩 프로토콜을 지원하여 코드를 더욱 구조화 및 캡슐화할 수 있게 되었습니다.

참고자료