4

I'm building a Swift-based iOS application that uses PromiseKit to handle promises (although I'm open to switching promise library if it makes my problem easier to solve). There's a section of code designed to handle questions about overwriting files.

I have code that looks approximately like this:

let fileList = [list, of, files, could, be, any, length, ...]

for file in fileList {
  if(fileAlreadyExists) {
    let overwrite = Promise<Bool> { fulfill, reject in
      let alert = UIAlertController(message: "Overwrite the file?")
      alert.addAction(UIAlertAction(title: "Yes", handler: { action in 
        fulfill(true)
      }
      alert.addAction(UIAlertAction(title: "No", handler: { action in 
        fulfill(false)
      }
    } else {
      fulfill(true)
    }
  }

  overwrite.then { result -> Promise<Void> in
    Promise<Void> { fulfill, reject in
      if(result) {
        // Overwrite the file
      } else {
        // Don't overwrite the file
      }
  }
}

However, this doesn't have the desired effect; the for loop "completes" as quickly as it takes to iterate over the list, which means that UIAlertController gets confused as it tries to overlay one question on another. What I want is for the promises to chain, so that only once the user has selected "Yes" or "No" (and the subsequent "overwrite" or "don't overwrite" code has executed) does the next iteration of the for loop happen. Essentially, I want the whole sequence to be sequential.

How can I chain these promises, considering the array is of indeterminate length? I feel as if I'm missing something obvious.

Edit: one of the answers below suggests recursion. That sounds reasonable, although I'm not sure about the implications for Swift's stack (this is inside an iOS app) if the list grows long. Ideal would be if there was a construct to do this more naturally by chaining onto the promise.

Andrew Ferrier
  • 16,664
  • 13
  • 47
  • 76

1 Answers1

0

One approach: create a function that takes a list of the objects remaining. Use that as the callback in the then. In pseudocode:

function promptOverwrite(objects) {
    if (objects is empty)
        return
    let overwrite = [...]  // same as your code
    overwrite.then {
        do positive or negative action
        // Recur on the rest of the objects
        promptOverwrite(objects[1:])
    }
}

Now, we might also be interested in doing this without recursion, just to avoid blowing the call stack if we have tens of thousands of promises. (Suppose that the promises don't require user interaction, and that they all resolve on the order of a few milliseconds, so that the scenario is realistic).

Note first that the callback—in the then—happens in the context of a closure, so it can't interact with any of the outer control flow, as expected. If we don't want to use recursion, we'll likely have to take advantage of some other native features.

The reason you're using promises in the first place, presumably, is that you (wisely) don't want to block the main thread. Consider, then, spinning off a second thread whose sole purpose is to orchestrate these promises. If your library allows to explicitly wait for a promise, just do something like

function promptOverwrite(objects) {
    spawn an NSThread with target _promptOverwriteInternal(objects)
}
function _promptOverwriteInternal(objects) {
    for obj in objects {
        let overwrite = [...]  // same as your code
        overwrite.then(...)    // same as your code
        overwrite.awaitCompletion()
    }
}

If your promises library doesn't let you do this, you could work around it by using a lock:

function _promptOverwriteInternal(objects) {
    semaphore = createSemaphore(0)
    for obj in objects {
        let overwrite = [...]  // same as your code
        overwrite.then(...)    // same as your code
        overwrite.always {
            semaphore.release(1)
        }
        semaphore.acquire(1)  // wait for completion
    }
}
wchargin
  • 15,589
  • 12
  • 71
  • 110
  • Apologies for the pseudocode—I don't know Swift. But this is a good question, and I think the solution is fairly language-agnostic. – wchargin Jun 20 '16 at 00:20
  • Essentially, you're suggesting to use recursion, I think. That's a reasonable idea if the list is short. I'm a bit worried about the Swift implications if it grows longer (I'm not enough of a Swift expert either to know if that would have serious implications). – Andrew Ferrier Jun 20 '16 at 00:21
  • 1
    @AndrewFerrier: Yes, it's recursive. But I'm guessing that a UIAlertController issues a prompt to the user. Surely you're not suggesting displaying thousands of prompts in a row, right? – wchargin Jun 20 '16 at 00:23
  • (To clarify, perhaps: there is no performance impact due to the recursion here. The only reason recursion could have an impact is if you blow the call stack, and the call stack generally handles thousands of calls. Even if you're ~100 calls in due to frameworks and GUI events, and each recursive call triggers a few frames due to promises, that would still mean you'd need many hundreds of prompts to blow the stack. And at that point you've got a serious UX issue! :) ) – wchargin Jun 20 '16 at 00:25
  • good point :) Getting a bit more theoretical now, but I'd still like to understand how this would happen if there wasn't UI interaction, though. – Andrew Ferrier Jun 20 '16 at 00:26
  • @AndrewFerrier: Sure, I'll entertain that. It's sounds feasible that you might want to chain tens of thousands of non-interactive promises in some extreme cases. See my edit. – wchargin Jun 20 '16 at 02:25