Search code examples
swiftcombine

Updating several @Published properties in a single method


I have an observable view model with several published properties:

final class ProtectionViewModel: ObservableObject {
  @Published
  var showSomething = false
  @Published
  var showSomethingElse = false
}

The properties are updated similarly but their updates are triggered by different events. E.g.:

final class ProtectionViewModel: ObservableObject {

  // ...

  private func doSomething() {
    let someCondition = // ...
    if someCondition {
      showSomething = true
      Task {
        try? await Task.sleep(nanoseconds: 1_000_000_000)
        await MainActor.run { showSomething = false }
      }
    } else {
      showSomethingElse = true
      Task {
        try? await Task.sleep(nanoseconds: 1_000_000_000)
        await MainActor.run { showSomethingElse = false }
      }
    }
  }

}

There's an obvious code duplication there. Is there a way to extract a method that would accept a published property and change its wrapped value? Something like this:

// For the sake of example. The code doesn't compile.

private func doSomething() {
  let someCondition = // ...
  if someCondition {
    doSomething(with: showSomething)
  } else {
    doSomething(with: showSomethingElse)
  }
}

private func doSomething(with something: Published<Bool>) {
  something = true
  Task {
    try? await Task.sleep(nanoseconds: 1_000_000_000)
    await MainActor.run { something = false }
  }
}

Any piece of advice is appreciated.


Solution

  • Use key paths. Since you are dealing with properties of a class, the function can take a ReferenceWritableKeyPath<ProtectionViewModel, Bool>:

    private func doSomething(with something: ReferenceWritableKeyPath< ProtectionViewModel, Bool>) {
        self[keyPath: something] = true
        Task {
            try? await Task.sleep(nanoseconds: 1_000_000_000)
            await MainActor.run { self[keyPath: something] = false }
        }
    }
    

    Example usage:

    doSomething(with: \.showSomething)
    doSomething(with: \.showSomethingElse)
    

    Note that ProtectionViewModel should be MainActor for MainActor.run { self[keyPath: something] = false } to be thread safe.