Home > Software engineering >  Swift: how to replace a completion handler with a Future in an element initializer?
Swift: how to replace a completion handler with a Future in an element initializer?

Time:07-19

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

CodePudding user response:

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)

CodePudding user response:

When you store a Cancellable using .store, the value returned will no longer be UIViewController, it is going to be Void.

You shouldn't use Future, instead use PassThroughSubject like so:

class MyViewController: UIViewController {
    private var startTime: Date = .now
    private var isOn = false
    var publisher = PassthroughSubject<TimeInterval, Never>()
    @IBAction func tapped() {
        if isOn {
            publisher.send(Date.now.timeIntervalSince(startTime))
        } else {
            startTime = .now
        }
        isOn.toggle()
    }

    init() {
        super.init(nibName: "MyViewController", bundle: .main)
    }
    
    required init?(coder: NSCoder) {
        fatalError()
    }
}
let viewController = MyViewController()
viewController
    .publisher
    .sink(receiveCompletion: { completion in
                
        }, receiveValue: { value in
                
        })
        .store(in: &subscriptions)
    .
window?.rootViewController = viewController
  • Related