Skip to content

Instantly share code, notes, and snippets.

@CBeloch
Created July 18, 2025 20:08
Show Gist options
  • Save CBeloch/3344fe59aebdd823af5921ba4967f66b to your computer and use it in GitHub Desktop.
Save CBeloch/3344fe59aebdd823af5921ba4967f66b to your computer and use it in GitHub Desktop.
import Foundation
// MARK: - LossySequence
/// LossySequence can be used as a replacement for a regular array,
/// where the given `Element` tends to fail decoding (e.g. because of an added enum type in the future etc).
///
/// Failed decodings will be skipped from the returned sequence. Errors can be found in the `errors` property
///
/// ```swift
/// struct NewsReponse {
/// @LossySequence let news: [News]
/// }
///
/// // decode data
///
/// response.news // only valid entities
/// response.$news.errors // contains all decoding errors
/// ```
@propertyWrapper
public struct LossySequence<Element> {
// MARK: Properties
/// The wrapped value
public var wrappedValue: [Element]
/// List of errors
public var errors: [Swift.Error] = []
/// Current index
private var currentIndex = 0
/// projected value to make this type accessible by $ prefix
public var projectedValue: LossySequence<Element> { return self }
// MARK: Initializers
/// Initialize a new ``LossySequence``
/// - Parameters:
/// - wrappedValue: The wrapped value
public init(
wrappedValue: [Element]
) {
self.wrappedValue = wrappedValue
}
}
// MARK: - Sequence
extension LossySequence: Sequence, IteratorProtocol {
public mutating func next() -> Element? {
// Safely grab the element at the current index
let model = self.wrappedValue.indices.contains(currentIndex)
? self.wrappedValue[currentIndex]
: nil
// advance current index
self.currentIndex += 1
return model
}
}
// MARK: - Decodable
extension LossySequence: Decodable where Element: Decodable {
public init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
// Ensure we have a count of contained elements in the container
guard let count = container.count else {
self.wrappedValue = .init()
return
}
var errors: [Swift.Error] = .init()
self.wrappedValue = try (0..<count).compactMap { _ in
do {
// Try to decode current index to Element
return try container.decode(Element.self)
} catch {
// Decoding failed, skip Element and advance
errors.append(error)
// Fallback Decodable to advance inside the container
_ = try container.decode(NothingCodable.self)
return nil
}
}
self.errors = errors
}
}
// MARK: - Encodable
extension LossySequence: Encodable where Element: Encodable {
public func encode(to encoder: Encoder) throws {
// Encode wrapped value
try self.wrappedValue.encode(to: encoder)
}
}
// MARK: - Equatable
extension LossySequence: Equatable where Element: Equatable {
public static func == (
lhs: LossySequence<Element>,
rhs: LossySequence<Element>
) -> Bool {
lhs.wrappedValue == rhs.wrappedValue
}
}
// MARK: - Hashable
extension LossySequence: Hashable where Element: Hashable {
public func hash(into hasher: inout Hasher) {
self.wrappedValue.hash(into: &hasher)
}
}
// MARK: - Codable
/// An empty object that will always succeed to decode
private struct NothingCodable: Codable {}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment