04 July 2022

Delegate 패턴

우리가 일반적으로 Delegate 패턴을 사용하여 작업을 수행하도록 하는 경우가 많습니다. Apple SDK에서도 Delegate 패턴으로 작성된 코드도 많습니다.

다음과 같이 Delegate 패턴으로 작성된 코드가 있습니다.

protocol ViewActionListener: AnyObject {
    func tapped()
    func refresh()
}

class SomeView {
    weak var listener: ViewActionListener?
    
    init() {}
    
    func tapped() {
        listener?.tapped()
    }
    func requestRefresh() {
        listener?.refresh()
    }
}

class ViewController: ViewActionListener {
    let view = SomeView()
    
    init() {
        view.listener = self
    }
    
    func tapped() {
        print("tapped")
    }

    func refresh() {
        print("refresh")
    }
}

위 코드는 View에서 수행된 액션을 ViewController에게 전달합니다. 그러면 ViewController에서 이를 처리하는 코드를 작성합니다.

ViewController는 ViewActionListener을 준수해야하며, 이를 구현해야 합니다. 그러나 함수가 많아졌습니다. 그래서 그런 함수들을 모아서 enum을 통해 함수 하나로 합칠 수 있습니다.

enum ViewAction {
    case tapped
    case refresh
}
protocol ViewActionListener: AnyObject {
    func send(action: ViewAction)
}

class SomeView {
    weak var listener: ViewActionListener?
    
    init() {}
    
    func tapped() {
        listener?.send(action: .tapped)
    }
    func requestRefresh() {
        listener?.send(action: .refresh)
    }
}

class ViewController: ViewActionListener {
    let view = SomeView()
    
    init() {
        view.listener = self
    }
    func send(action: ViewAction) {
        switch action {
        case .tapped:
            print("tapped")
        case .refresh:
            print("refresh")
        }
    } 
}

Concurrency

위에서 Delegate 패턴으로 비동기로 Action을 받는 코드입니다. 즉, Delegate 패턴으로 작성된 코드를 Concurrency 형태로 변환할 수 있습니다.

우선 기존 작성된 코드는 쉽게 바꿀 수 없습니다. 하지만, Action을 받아서 처리하는 곳은 Concurrency 형태로 변환할 수 있습니다.

먼저 ViewActionListener를 준수하는 ViewAdapter 클래스를 선언합니다. 이 클래스는 Delegate를 Closure로 바꿔주는 역할을 합니다.

class ViewController {
    final class Adapter: ViewActionListener {
        var handler: ((ViewAction) -> Void)?
        
        func send(action: ViewAction) {
            handler?(action)
        }
    }

    let view = SomeView()
    let adapter = Adapter()
    
    init() {
        view.listener = adapter

        adapter.handler = { action in
            switch action {
            case .tapped:
                print("tapped")
            case .refresh:
                print("refresh")
            }
        }
    }
}

AsyncStream

이제 handler 부분을 AsyncStream를 이용하여 for await in loop로 변환해봅시다.

class ViewController {
    private final class Adapter: ViewActionListener {
        var handler: ((ViewAction) -> Void)?
        
        func send(action: ViewAction) {
            handler?(action)
        }
    }

    let view = SomeView()
    private let adapter = Adapter()
    
    init() {
        view.listener = adapter

        Task { @MainActor [weak self] in
            guard let self = self else { return }

            for await action in self.viewActionEvents() {
                switch action {
                case .tapped:
                    print("tapped")
                case .refresh:
                    print("refresh")
                }
            }
        }
    }
    
    func viewActionEvents() -> AsyncStream<ViewAction> {
        let actions = AsyncStream(ViewAction.self) { [weak self] continuation in
            self?.adapter.handler = { action in
                continuation.yield(action)
            }
        }
        return actions
    }
}

AsyncSequence

위에서 AsyncStream을 이용하여 for await in loop으로 변환이 가능함을 알았으니, AsyncSequence 를 이용하여 구현할 수 있음을 의미합니다.

class ViewController {
    private final class Adapter: AsyncSequence, ViewActionListener {
        typealias Element = ViewAction

        private let continuation: AsyncStream<Element>.Continuation
        private let iterator: AsyncStream<Element>.Iterator

        init() {
            let (stream, continuation) = AsyncStream.makeStream(of: Element.self)
            self.continuation = continuation
            self.iterator = stream.makeAsyncIterator()
        }

        func makeAsyncIterator() -> AsyncStream<Element>.AsyncIterator {
            iterator
        }

        func send(action: ViewAction) {
            continuation.yield(action)
        }
    }

    let view = SomeView()
    private let adapter = Adapter()

    init() {
        view.listener = adapter
        Task { @MainActor [weak self] in
            guard let self = self else { return }

            for await action in self.adapter {
                switch action {
                case .tapped:
                    print("tapped")
                case .refresh:
                    print("refresh")
                }
            }
        }
    }
}

정리

Delegate 패턴을 Concurrency 형태로 변환하는 방법을 작성해보았습니다. 여러가지 방법으로 작성하긴 했지만, Closure 형태로 하는게 가장 간단해 보이긴 합니다.

추후에 위에 작성된 코드들보다 더 좋은 코드로 정리할 수 있도록 하겠습니다.