Using Combine-Future to Fetch Server Data

I could do it with completionHandler, but I'm trying to get server data with Combine. The following is what I have.

// UIViewController //

import UIKit
import Combine

class ViewController: UIViewController {
	// MARK: - Variables
	private var cancellableSet: Set<AnyCancellable> = []
	
	
	// MARK: - Life cycle
	override func viewDidLoad() {
		super.viewDidLoad()
		
		let urlStr = "https://api.github.com/repos/ReactiveX/RxSwift/events"
		let viewModel = ViewModel(urlStr: urlStr, waitTime: 2.0)
		viewModel.fetchData(urlText: viewModel.urlStr, timeInterval: viewModel.waitTime)
			.sink { completion in
				print("complete")
			} receiveValue: { dataSet in
				print("count: \(dataSet)")
			}
			.store(in: &cancellableSet)
		
		print("Yeah...")
	}
}

struct DataModel: Hashable, Decodable {
	let id: String
	let type: String
}

// ViewModel //

import UIKit
import Combine

class ViewModel: NSObject {
	var cancellables = [AnyCancellable]()
		
	var urlStr: String
	var waitTime: Double
	init(urlStr: String, waitTime: Double) {
		self.urlStr = urlStr
		self.waitTime = waitTime
	}
	
	
	func fetchData(urlText: String, timeInterval: Double) -> Future<[DataModel], Error> {
		return Future<[DataModel], Error> { [weak self] promise in
			guard let strongSelf = self else { return }
			if let url = URL(string: urlText) {
				var request = URLRequest(url: url)
				request.timeoutInterval = timeInterval
				let sessionConfiguration = URLSessionConfiguration.default
				let publisher = URLSession(configuration: sessionConfiguration).dataTaskPublisher(for: request)
				publisher.sink { completion in
					print("complete")
				} receiveValue: { (data: Data, response: URLResponse) in
					do {
						let dataModels = try JSONDecoder().decode([DataModel].self, from: data)
						promise(.success(dataModels))
					} catch {
						print("Error while parsing: \(error)")
						promise(.failure("Failure" as! Error))
					}
				}
				.store(in: &strongSelf.cancellables)
			} else {
				promise(.failure("Failure" as! Error))
			}
		}
	}
}

If I run it, I don't get an error. The app doesn't crash, either. The view controller doesn't deliver anything. What am I doing wrong? Muchos thankos.

I've removed the comment.

The following works.

import UIKit
import Combine

class ViewController: UIViewController {
	// MARK: - Variables
	private var cancellableSet: Set<AnyCancellable> = []


	override func viewDidLoad() {
		super.viewDidLoad()
		
		let _ = Future<[DataModel], Error> { [weak self] promise in
			guard let strongSelf = self else { return }
			let url = URL(string: "https://api.github.com/repos/ReactiveX/RxSwift/events")!
			URLSession.shared.dataTaskPublisher(for: url)
				.timeout(2.0, scheduler: DispatchQueue.global(qos: .background))
				.retry(3)
				.map { $0.data }
				.decode(type: [DataModel].self, decoder: JSONDecoder())
				.sink(receiveCompletion: { (completion) in
					print("I'm done: \(completion)")
				}, receiveValue: { dataModels in
					for model in dataModels {
						print("\(model.id) \(model.type)")
					}
					promise(.success(dataModels))
				})
				.store(in: &strongSelf.cancellableSet)
		}
	}
}

I wonder why it doesn't work if I use ViewModel?

I guess the following is better. But I'm not completely satisfied.

// ViewController //

import UIKit
import Combine

class ViewController: UIViewController {
	// MARK: - Variables
	var cancellable: AnyCancellable?
	private var cancellableSet: Set<AnyCancellable> = []
	
	
	// MARK: - Life cycle
	override func viewDidLoad() {
		super.viewDidLoad()
		
		
		let urlStr = "https://api.github.com/repos/ReactiveX/RxSwift/events"
		let viewModel = ViewModel(urlStr: urlStr, waitTime: 7.0)
		viewModel.fetchData(urlText: viewModel.urlStr, timeInterval: viewModel.waitTime)
			.sink { completion in
				print("Done!")
			} receiveValue: { dataModels in
				print("Count: \(dataModels.count)")
			}
			.store(in: &cancellableSet)
	}
}

// ViewModel //

import UIKit
import Combine

class ViewModel {
	var anycancellables = Set<AnyCancellable>()
	
	var urlStr: String
	var waitTime: Double
	init(urlStr: String, waitTime: Double) {
		self.urlStr = urlStr
		self.waitTime = waitTime
	}
	
	func fetchData(urlText: String, timeInterval: Double) -> Future<[DataModel], Error> {
		return Future<[DataModel], Error> { promise in
			let url = URL(string: urlText)!
			var request = URLRequest(url: url)
			request.timeoutInterval = timeInterval
			let sessionConfiguration = URLSessionConfiguration.default
			let session = URLSession(configuration: sessionConfiguration)
			session.dataTask(with: request) { data, response, error in
				if let error = error {
					print("error: \(error.localizedDescription)")
					promise(.failure("Failure" as! Error))
				}
				if let jsonData = data {
					do {
						let dataModels = try JSONDecoder().decode([DataModel].self, from: jsonData)
						promise(.success(dataModels))
					} catch {
						print("Error while parsing: \(error)")
					}
				}
			}.resume()
		}
	}
}

Why not just use async/await? I'm not sure why you're moving the http session time out 30 seconds to 7 seconds. It doesn't make the request any faster just creates problems if the call takes more than 7 seconds to complete.

func fetchData(from url: URL) async throws -> [DataModel] {
     let session = URLSession.shared
     let (data, _) = try await session.data(from: url)
     let decoder = JSONDecoder()
     decoder.keyDecodingStratergy = .converFromSnakeCase // <-- depends on JSON you might not need it
     return try decoder.decode([DataModel].self, from: data) 
}

View Controller

Task {
    let dataModel = try? await viewModel.fetchData(from: URL(string:"https://api.github.com/repos/ReactiveX/RxSwift/events")!)
    DispatchQueue.main.async { // give the data model to some other API on the main thread }
}

Accepted Answer

What am I doing wrong? 

In your code, you are making viewModel a local variable, so it will be released just after print("Yeah...").

Please try something like this:

class ViewController: UIViewController {
    // MARK: - Variables
    private var cancellableSet: Set<AnyCancellable> = []
    
    var viewModel: ViewModel! //<-
    
    // MARK: - Life cycle
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let urlStr = "https://api.github.com/repos/ReactiveX/RxSwift/events"
        viewModel = ViewModel(urlStr: urlStr, waitTime: 2.0) //<- No `let` here`
        viewModel.fetchData(urlText: viewModel.urlStr, timeInterval: viewModel.waitTime)
            .sink { completion in
                print("complete")
            } receiveValue: { dataSet in
                print("count: \(dataSet)")
            }
            .store(in: &cancellableSet)
        
        print("Yeah...")
    }
}

Thank you for your answer bro @OOPer

Using Combine-Future to Fetch Server Data
 
 
Q