UUID in state initializer

Update 1: I just saw that I missed an identical question here.

Update 2: Seems like the concept applies but the problem is not solved. I'll comment below.

——

Let's say I have a feature with an identifiable State that I use in an IdentifiedArray:

public struct ContentFeature: ReducerProtocol {
    
    public init() {}
    
    public struct State: Equatable, Identifiable {
        public let id: UUID
        public let content: Content
        
        public init(id: UUID = UUID(), _ content: Content) {
            self.id = id
            self.content = content
        }
    }
    public enum Action: Equatable {}
    
    public var body: some ReducerProtocol<State, Action> {
        Reduce { state, action in
            return .none
        }
    }
}

I derive the initial data from another state, so I would like to initialize it in that State's init. Is there a testable way to do that?

I cannot use @Dependency(\.uuid) uuid because that would use self before it's initialized.

The only thing I can think of is populating the array via an onAppear action in the reducer, but that seems to be quite a detour.

How do you guys handle this?

@mbrandonw pointed out the solution to an almost identical question in this thread: It's possible to use dependencies in eagerly evaluated closures.

I followed the solution in the other thread, put the closure at the top level and overwrote it from the initializer with a test value:

public let id: UUID = {
    @Dependency(\.uuid) var uuid: UUIDGenerator
    return uuid()
}()

public init(id: UUID? = nil, _ content: Content, folder: URL? = nil) {
    if let id {
        self.id = id
    }
    self.content = content
}

The problem with this is, that it runs on every initialization, producing "dependency not implemented" errors when creating the expected test result state.

(Identifiable forbids making it a lazy variable, a function cannot be executed from init and having it return a UUIDGenerator instead of a UUID makes State no longer conform to Equatable.)

What worked for me was putting the closure into the initializer:

public init(
    id: UUID = {
        @Dependency(\.uuid) var uuid: UUIDGenerator
        return uuid()
    }(),
    _ content: Content,
) {
    self.id = id
    self.content = content
}

Or in a static helper method:

public init(
    id: UUID = getUUID(),
    _ content: Content
) {
    self.id = id
    self.content = content
}

public static func getUUID() -> UUID {
    @Dependency(\.uuid) var uuid: UUIDGenerator
    return uuid()
}

This lets me test the parent feature.

Problem is with the parent of the parent where I don’t have direct access to the initializer anymore. So when I create an expected test state of the grandparent I would need to pipe the expected UUID of the grandchild through the object in the middle.

If I don’t do that my test doesn’t only fail because of the UUID mismatch, but also because of the not implemented UUID dependency (creating the expected state happens outside the store).

So my parent initializer looks like this (irrelevant parts omitted):

public init(
    paragraphUUIDs: [UUID]? = nil
) {
    let paragraphs = ((try? Content.parser.parse(entry.content)) ?? [])
    let finalParagraphs: [ContentFeature.State]
    if let paragraphUUIDs { // used in test
        finalParagraphs = zip(paragraphUUIDs, paragraphs)
            .map { (uuid, content) in
                ContentFeature.State(
                    id: uuid,
                    content
                )
            }
    } else { // used in production
        finalParagraphs = paragraphs
            .map { content in
                ContentFeature.State(
                    content
                )
            }
    }
    self.paragraphs = IdentifiedArrayOf(uniqueElements: finalParagraphs)
}

This passes the tests but it looks very hacky.