These are the lesson notes for this day. Its build-along tutorial and flash cards have not been written yet.
Drag and drop is built on the Transferable protocol, which describes how a type is converted to and from data. Make a view draggable with .draggable(item), and accept drops with .dropDestination(for:). The same protocol powers copy and paste and the share sheet (ShareLink). Haptics are declarative: .sensoryFeedback(.success, trigger: value) plays feedback each time value changes.
Coming from UIKit, Transferable replaces the plumbing around NSItemProvider, and .sensoryFeedback replaces UIFeedbackGenerator.
Let the user reorder trips by dragging, drag a trip onto a "Favorites" area, and feel a haptic when the drop succeeds.
After this lesson you should be able to answerTransferable describe about a type?“This waits 300 ms after the last keystroke before searching. How would you unit test it? I do not want a test suite that sleeps.”
@MainActor @Observable
final class SearchModel {
var query = "" { didSet { scheduleSearch() } }
private(set) var results: [String] = []
private var task: Task<Void, Never>?
private func scheduleSearch() {
task?.cancel()
task = Task {
try? await Task.sleep(for: .milliseconds(300))
guard !Task.isCancelled else { return }
results = await API.shared.search(query)
}
}
}
Where this comes from: swift-clocks: TestClock (package) · Testing asynchronous code — the standard senior question about time-dependent code: “how do you test this without waiting?”
As written it cannot be tested well. It hides two dependencies: time (Task.sleep uses the real clock) and the network (API.shared). Name them, then turn each into something the test can supply:
@MainActor @Observable
final class SearchModel {
var query = "" { didSet { scheduleSearch() } }
private(set) var results: [String] = []
private var task: Task<Void, Never>?
private let clock: any Clock<Duration> // seam 1: time
private let search: @MainActor (String) async -> [String] // seam 2: the network
init(clock: any Clock<Duration> = ContinuousClock(),
search: @escaping @MainActor (String) async -> [String]) {
self.clock = clock
self.search = search
}
private func scheduleSearch() {
task?.cancel()
task = Task {
try? await clock.sleep(for: .milliseconds(300))
guard !Task.isCancelled else { return }
results = await search(query)
}
}
}Now the test controls time. It types three characters, advances the clock to one millisecond before the 300 ms are up and proves that nothing has happened, then advances one more millisecond and proves that exactly one search ran, for the last keystroke. The whole test takes microseconds:
@Test @MainActor func debouncesToTheLastKeystroke() async {
let clock = TestClock()
var calls: [String] = []
let model = SearchModel(clock: clock) { query in
calls.append(query)
return [query]
}
model.query = "s"; model.query = "sw"; model.query = "swi"
await clock.advance(by: .milliseconds(299))
#expect(calls.isEmpty) // nothing yet: still inside the window
await clock.advance(by: .milliseconds(1))
#expect(calls == ["swi"]) // exactly one call, for the last keystroke
#expect(model.results == ["swi"])
}
What interviewers listen for: That you say "hidden dependency" before you write anything; that you assert the negative case (no call at 299 ms); and that no line of the test waits on real time.