SwiftUI using Combine. I cannot get json on api request

I'm studying SwiftUI, found a good example from youtube, but I don't get the expected working result. Here's my project on github.

ContentView:

struct ContentView: View {
    @ObservedObject var employeeViewModel = EmployeeViewModel()
    
    var body: some View {
        Form {
            Section {
                Button(action: {self.employeeViewModel.recevedEmployeeData()}) {
                    Text("Get employee name")
                }
            }
            Section {
                List(employeeViewModel.publishingNames, id:\.self) { publishedName in
                    Text("\(publishedName)")
                }
            }
        }
    }
}

EmployeeRoot:

// MARK: - EmployeeRoot
struct EmployeeRoot : Codable {
    
    let data : [EmployeeDatum]?
    let status : String?
    
    enum CodingKeys: String, CodingKey {
        case data = "data"
        case status = "status"
    }
    
    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        data = try values.decodeIfPresent([EmployeeDatum].self, forKey: .data)
        status = try values.decodeIfPresent(String.self, forKey: .status)
    }
    
}

// MARK: - Datum
struct EmployeeDatum : Codable {
    
    let id : String?
    let employeeName : String?
    let employeeSalary : String?
    let employeeAge : String?
    let profileImage : String?
    
    enum CodingKeys: String, CodingKey {
        case id = "id"
        case employeeName = "employee_name"
        case employeeSalary = "employee_salary"
        case employeeAge = "employee_age"
        case profileImage = "profile_image"
    }
    
    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        id = try values.decodeIfPresent(String.self, forKey: .id)
        employeeName = try values.decodeIfPresent(String.self, forKey: .employeeName)
        employeeSalary = try values.decodeIfPresent(String.self, forKey: .employeeSalary)
        employeeAge = try values.decodeIfPresent(String.self, forKey: .employeeAge)
        profileImage = try values.decodeIfPresent(String.self, forKey: .profileImage)
    }
}

EmployeeViewModel:

class EmployeeViewModel: ObservableObject {
    @Published var publishingNames: [String] = []
    @Published var employeeData: [EmployeeDatum] = []
    
    func recevedEmployeeData() {
        WebService.fetchApi()
            .sink(receiveCompletion: { completion in
                
                switch completion {
                case .finished:
                    print("finished!")
                case .failure(let error):
                    print("error: ", error)
                }
            }, receiveValue: { value in
                print("receiveValue?: ", value)
                self.employeeData = value
                for i in self.employeeData {
                    self.publishingNames = self.publishingNames + [i.employeeName!]
                }
            })
        
    }
}

WebService:

class WebService {
    
    enum ApiError: Error, LocalizedError {
        case unknown, apiError(reason: String)
    }
 
    static func fetchApi() -> AnyPublisher<[EmployeeDatum], Error> {
        let urlString = "http://dummy.restapiexample.com/api/v1/employees"
        guard let url = URL(string: urlString) else {
            fatalError("Invalid url")
        }
        
        return URLSession.shared.dataTaskPublisher(for: url)
            .tryMap { data, response -> Data in
                guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
                    throw ApiError.apiError(reason: "somethink went wrong")
                }
                return data
            }
            .decode(type: EmployeeRoot.self, decoder: JSONDecoder())
            .map { $0.data! }
            .receive(on: RunLoop.main)
            .eraseToAnyPublisher()
    }
}

Would you mind adding some context about what you expect to happen and the outcome you're getting? It's not very clear what issue you're running into.

As a result, I am not getting json. It is not displayed in my table.

You might want to check this Stack overflow answer.

dataTaskPublisher(for: urlRequest) will send values asynchronously. When program execution leaves your current scope, there are no more references to your pipeline and ARC destroys your pipeline before the network request has completed. Your pipeline returns a Cancellable.
Either assign that Cancellable directly to an instance variable or add the store(in:) operator to your pipeline.
CC BY-SA 4.0. — Gil Birman


Looking at your code, I would store the Cancellable in EmployeeViewModel since WebService only configures the publisher and does not own it.

...

private var networkRequestCancellable: AnyCancellable? = nil
    
func recevedEmployeeData() {
    networkRequestCancellable = WebService.fetchApi().sink

...                

thanks my friend, your topic helped me a lot. Yes, actually that was my problem with ARC. Sink now works as expected.

1 Like