Search code examples
swiftfuturecombine

Swift: how to replace a completion handler with a Future in an element initializer?


I am sorry if this sounds very beginner, but after looking at Apple doc as well as several tutorials I still struggle to understand how Combine's Future works.

I have this very simple code which stores the current date on first button tap, and prints the interval on the second one:

import UIKit

class MyViewController: UIViewController {
    private var startTime: Date = .now
    private var completion: (TimeInterval) -> Void = { _ in }
    private var isOn = false
    
    @IBAction func tapped() {
        if isOn {
            completion(Date.now.timeIntervalSince(startTime))
        } else {
            startTime = .now
        }
        isOn.toggle()
    }

    init(_ completion: @escaping (TimeInterval) -> Void) {
        super.init(nibName: "MyViewController", bundle: .main)
        self.completion = completion
    }
    
    required init?(coder: NSCoder) {
        fatalError()
    }
}

In my AppDelegate:

window?.rootViewController = MyViewController {
    print("Tapped with time interval: \($0)")
}

Now I would like to replace that completion handler with a Future, but I'm confused about what to do. I guess I have to create a function like this in my view controller:

func afterSecondTap() -> Future<TimeInterval, Error> {
    return Future { promise in
        // what to do here?
    }
}

And in the AppDelegate something like this:

window?.rootViewController = MyViewController()
    .afterSecondTap()
    .sink(receiveCompletion: { completion in
            
    }, receiveValue: { value in
            
    })
    .store(in: &subscriptions)

However this would not work because I get an error saying

Cannot assign value of type '()' to type 'UIViewController'

Thank you for helping me understand this


Solution

  • There are two issues here.

    First when you assign a rootViewController it expects UIViewController of some sort, as soon as you add .afterSecondTap() and the rest it changes the type. So let's start like this

    let controller = MyViewController()
    window?.rootViewController = controller
    

    Next let's see how we can publish time interval from the controller, I would think Future is not what you need:

    class MyViewController: UIViewController {
        
        private var startTime: Date = .now
        private var isOn = false
        
        var timeInterval: AnyPublisher<TimeInterval, Never> {
            _timeInterval
                .compactMap { $0 }
                .eraseToAnyPublisher()
        }
        private var _timeInterval = ConcurrentValueSubject<TimeInterval?, Never>(nil)
        
        @IBAction func tapped() {
            if isOn {
                _timeInterval.send(Date.now.timeIntervalSince(startTime)
            } else {
                startTime = .now
            }
            isOn.toggle()
        }
    
        init() {
            super.init(nibName: "MyViewController", bundle: .main)
        }
        
        required init?(coder: NSCoder) {
            fatalError()
        }
    }
    

    now you can do this:

    let controller = MyViewController()
    window?.rootViewController = controller
    controller
        .timeInterval
        .sink(
            receiveCompletion: { _ in },
            receiveValue: { _ in })
        .store(in: &subscriptions)