Combine Decode parse valid values and continue

Aim:

I would like to know what is the preferred way to decode using Combine to parse only valid records and not provide default values for failures.

Questions:

  1. I get the desired output, but is this a reasonable approach or is there a better way to do it ?
  2. I couldn't find something like tryDecode (like tryMap), or could other more suitable operator be used to achieve this ?

Based on the WWDC example, I have modified it slightly.

Code:

I am returning nil when parsing fails and then use compactMap not sure if there is a better way to do it.

import Foundation
import Combine

//Model

public struct CarRecord : Codable {
    
    var name    : String
    var doors   : Int
}

//Notification

extension Notification.Name {
    public static let didPost = Notification.Name("didPost")
}

func postNotifications() {
    
    let d1 = #"{"name":"aaa","doors":2}"#.data(using: .utf8)!   //Good record
    let d2 = #"{"name":"ccc"}"#.data(using: .utf8)!             //Faulty record
    let d3 = #"{"name":"ddd","doors":5}"#.data(using: .utf8)!   //Good record
    
    let datas = [d1, d2, d3]

    for data in datas {
        
        NotificationCenter.default.post(name: .didPost,
                                        object: nil,
                                        userInfo: ["Car" : data])
    }
}

//Combine

let cancellable = NotificationCenter.default.publisher(for: .didPost)
    .compactMap { $0.userInfo?["Car"] as? Data }
    .flatMap { data in
        Just(data)
        .decode(type: CarRecord.self, decoder: JSONDecoder())
        .map { Optional($0) } //added
        .catch { _ in
            Just(nil) //modified
        }
    }
    .compactMap { $0 } //added
    .sink {
        print("attempt: \($0)")
    }


postNotifications()

Output:

attempt: CarRecord(name: "aaa", doors: 2)
attempt: CarRecord(name: "ddd", doors: 5)

You can also run compactMap directly,

let cancellable = NotificationCenter.default.publisher(for: .didPost)
    .compactMap { $0.userInfo?["Car"] as? Data }
    .compactMap { try? JSONDecoder().decode(CarRecord.self, from: $0) }
    .sink {
        print("attempt: \($0)")
}
1 Like

@Lantua Thanks a ton !!, this is so much cleaner.

Even if decoder throws an exception we are still safe because of try? and wouldn't be unsubscribed to the publisher. Really like this solution.

I guess I only thinking about the operators and completely missed the fact that we could decode directly with the operator.

@Lantua I am not sure if I am missing something but once I add .receive(on: RunLoop.main) the sink subscriber is not receiving values.

let cancellable6 = NotificationCenter.default.publisher(for: .didPost)
    .compactMap { $0.userInfo?["Car"] as? Data }
    .compactMap { try? JSONDecoder().decode(CarRecord.self, from: $0) }
    .receive(on: RunLoop.main)
    .sink {
        print("attempt6: \($0)")
}

I am not sure if the subscriber was cancelled

It's possible that you're running into the problem described in this thread:

1 Like

Thanks a lot @mayoff for pointing me to the relevant thread, I think I will reply to that thread with my example

Many Thanks to @Lantua @mayoff, the issue of .receive(on: RunLoop.main) not sending through values to the subscribers seems to be fixed in iOS 13.3 Beta.