Robinhood iOS Engineer Interview Experience — Full Onsite with a UIKit Card Carousel and an Order Retry Queue

Robinhood·iOS Engineer·Jul 2026
Onsiteeasy

I keep seeing people here asking for interview reports every day without ever posting their own. So I decided to do some good and post mine.

  1. iOS coding: build a project from zero to one. They give you a demo first, a card scroll view that you can swipe through. My solution:
import UIKit

// MARK: - Model
// Data for one card: color + title + subtitle
struct CardItem {
    let color: UIColor
    let title: String
    let subtitle: String
}

// MARK: - Custom cell: rounded card + title/subtitle in the bottom left
// The cell itself is full screen width (so paging lines up); the card you actually see is the inset cardView inside it
final class CardCell: UICollectionViewCell {
    private let cardView = UIView()
    private let titleLabel = UILabel()
    private let subtitleLabel = UILabel()

    // When the collection view creates a cell through dequeue, it calls init(frame:)
    override init(frame: CGRect) {
        super.init(frame: frame)
        cardView.layer.cornerRadius = 16
        titleLabel.font = .boldSystemFont(ofSize: 22)
        titleLabel.textColor = .white
        subtitleLabel.font = .systemFont(ofSize: 15)
        subtitleLabel.textColor = .white
        // Add subviews to contentView, not self
        contentView.addSubview(cardView)
        cardView.addSubview(titleLabel)
        cardView.addSubview(subtitleLabel)
    }

    required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }

    func configure(with item: CardItem) {
        cardView.backgroundColor = item.color
        titleLabel.text = item.title
        subtitleLabel.text = item.subtitle
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        // Inset the card by 16 on each side, so there is a 32 gap between two cards
        cardView.frame = contentView.bounds.insetBy(dx: 16, dy: 0)
        // Text sits in the bottom left of the card with 16 padding: place the subtitle first, the title goes on top of it
        let padding: CGFloat = 16
        let width = cardView.bounds.width - padding * 2
        subtitleLabel.frame = CGRect(x: padding, y: cardView.bounds.height - padding - 20,
                                     width: width, height: 20)
        titleLabel.frame = CGRect(x: padding, y: subtitleLabel.frame.minY - 28,
                                  width: width, height: 28)
    }
}

// MARK: - Reusable card carousel component (black box)
// From the outside you only need to: addChild it, set the frame, call setItems
final class ScrollCardViewController: UIViewController, UICollectionViewDataSource {
    private var items: [CardItem] = []
    private let cellID = "CardCell"

    private let layout: UICollectionViewFlowLayout = {
        let layout = UICollectionViewFlowLayout()
        layout.scrollDirection = .horizontal   // cells are laid out side by side horizontally
        layout.minimumLineSpacing = 0          // must be 0: paging moves one bounds width at a time
        return layout
    }()

    // lazy: created on first use, so the closure can use self (dataSource = self)
    private lazy var collectionView: UICollectionView = {
        let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
        cv.isPagingEnabled = true              // snap: always stops on exactly one card
        cv.showsHorizontalScrollIndicator = false
        cv.dataSource = self
        cv.register(CardCell.self, forCellWithReuseIdentifier: cellID)
        return cv
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        view.addSubview(collectionView)
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        collectionView.frame = view.bounds
        layout.itemSize = collectionView.bounds.size   // each cell is the same size as the collection view
    }

    // The only entry point for swapping data from outside
    func setItems(_ newItems: [CardItem]) {
        items = newItems
        collectionView.reloadData()
        collectionView.setContentOffset(.zero, animated: false)  // go back to the first card after swapping data
        if isAutoScrollOn {                                       // restart the timer from the first card after swapping the data set
            stopTimer()
            startTimer()
        }
    }

    // MARK: UICollectionViewDataSource
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        items.count
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellID, for: indexPath) as! CardCell
        cell.configure(with: items[indexPath.item])
        return cell
    }

    // MARK: Follow-up: auto paging (Timer, flip one card every 2 seconds)
    private var timer: Timer?
    private var isAutoScrollOn = false

    // The UISwitch outside calls this
    func setAutoScroll(_ on: Bool) {
        isAutoScrollOn = on
        on ? startTimer() : stopTimer()
    }

    private func startTimer() {
        guard timer == nil, items.count > 1 else { return }   // avoid creating it twice; no point flipping with only one card
        timer = Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { [weak self] _ in
            self?.scrollToNextCard()                          // weak self: the timer holds the closure strongly
        }
    }

    private func stopTimer() {
        timer?.invalidate()
        timer = nil
    }

    private func scrollToNextCard() {
        let width = collectionView.bounds.width
        let current = Int(round(collectionView.contentOffset.x / width))   // which page we are on
        let next = (current + 1) % items.count                             // after the last card, go back to the first
        collectionView.setContentOffset(CGPoint(x: CGFloat(next) * width, y: 0), animated: true)
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        stopTimer()                                           // stop when the page is gone
    }
}

// MARK: - Outer page: card carousel in the middle, 3 buttons at the bottom to switch data sets
final class ViewController: UIViewController {
    private let cardVC = ScrollCardViewController()
    private var buttons: [UIButton] = []
    private let autoScrollSwitch = UISwitch()

    // Three hard-coded data sets
    private let datasets: [[CardItem]] = [
        [
            CardItem(color: .systemRed, title: "Red", subtitle: "Card 1"),
            CardItem(color: .systemGreen, title: "Green", subtitle: "Card 2"),
            CardItem(color: .systemBlue, title: "Blue", subtitle: "Card 3"),
        ],
        [
            CardItem(color: .systemOrange, title: "Orange", subtitle: "Card 1"),
            CardItem(color: .systemPurple, title: "Purple", subtitle: "Card 2"),
        ],
        [
            CardItem(color: .systemTeal, title: "Teal", subtitle: "Card 1"),
            CardItem(color: .systemPink, title: "Pink", subtitle: "Card 2"),
            CardItem(color: .systemIndigo, title: "Indigo", subtitle: "Card 3"),
            CardItem(color: .systemBrown, title: "Brown", subtitle: "Card 4"),
        ],
    ]

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemBackground

        // Child VC containment, three steps: addChild -> addSubview -> didMove
        addChild(cardVC)
        view.addSubview(cardVC.view)
        cardVC.didMove(toParent: self)
        cardVC.setItems(datasets[0])

        // 3 buttons, use tag to tell which data set each one is for
        for i in 0..<datasets.count {
            let button = UIButton(type: .system)
            button.setTitle("Dataset \(i + 1)", for: .normal)
            button.tag = i
            button.addTarget(self, action: #selector(didTapButton(_:)), for: .touchUpInside)
            view.addSubview(button)
            buttons.append(button)
        }

        autoScrollSwitch.addTarget(self, action: #selector(didToggleAutoScroll(_:)), for: .valueChanged)
        view.addSubview(autoScrollSwitch)
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        let safe = view.safeAreaLayoutGuide.layoutFrame

        // Card area: vertically centered, height 200, full width (the side margins come from the card insetting itself)
        let cardHeight: CGFloat = 200
        cardVC.view.frame = CGRect(x: 0, y: safe.midY - cardHeight / 2,
                                   width: view.bounds.width, height: cardHeight)

        // Switch goes right below the card area (UISwitch has a fixed size, only set the origin)
        autoScrollSwitch.frame.origin = CGPoint(x: 16, y: cardVC.view.frame.maxY + 16)

        // Buttons: stacked vertically from the bottom up
        let buttonHeight: CGFloat = 44
        for (i, button) in buttons.enumerated() {
            let fromBottom = CGFloat(buttons.count - i)
            button.frame = CGRect(x: 0, y: safe.maxY - fromBottom * buttonHeight,
                                  width: view.bounds.width, height: buttonHeight)
        }
    }

    @objc private func didTapButton(_ sender: UIButton) {
        cardVC.setItems(datasets[sender.tag])
    }

    @objc private func didToggleAutoScroll(_ sender: UISwitch) {
        cardVC.setAutoScroll(sender.isOn)
    }
}
  1. iOS coding: implement a function. This is what they gave me:
import Foundation

class NetworkManager {
    static let shared = NetworkManager()

    // Simulates sending order request to RH backend
    func sendOrder(order: Order, success: @escaping () -> Void, failure: @escaping () -> Void) {
        print("sendingOrder: \(order.identifier)")
        let requestDuration = Double.random(in: 0...1.0)
        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + requestDuration) {
            let rand = Int.random(in: 0...1)
            if rand == 0 {
                print("success: \(order.identifier)")
                success()
            } else {
                print("failure: \(order.identifier)")
                failure()
            }
        }
    }
}

public struct Order: Equatable { let identifier: String }

/*
 Do not edit above this line
 ================================================ */

public class OrderManager {

    /// **Do not change this method signature**
    public func placeOrder(_ order: Order) {
        // TODO: Implement placeOrder so that:
        //
        // 1. Orders are sent ONE AT A TIME.
        //    Do not send the next order until the current one has finished
        //    (finished = succeeded, or given up after 3 failures).
        //
        // 2. Orders are sent in the SAME ORDER placeOrder was called.
        //    placeOrder can be called again while an order is still in flight;
        //    the new order must wait its turn.
        //
        // 3. If an order FAILS, retry the SAME order.
        //
        // 4. If the same order fails 3 TIMES IN A ROW (3 sends total),
        //    give up on it and move on to the next order.
        //
        // Use NetworkManager.shared.sendOrder(order:success:failure:) to send.
        // You may add any properties / helper methods to this class.
    }
}

/*
 Examples
 =====================
 orderManager.placeOrder(Order(identifier: "SNAP"))
 orderManager.placeOrder(Order(identifier: "GOOG"))
 orderManager.placeOrder(Order(identifier: "HOOD"))
 orderManager.placeOrder(Order(identifier: "AAPL"))

 // Correct: each order is processed sequentially
 sendingOrder: SNAP → success: SNAP
 sendingOrder: GOOG → success: GOOG
 sendingOrder: HOOD → failure: HOOD
 sendingOrder: HOOD → success: HOOD
 sendingOrder: AAPL → success: AAPL

 // Correct: after failing an order three times in a row, skip to the next order
 sendingOrder: SNAP → failure: SNAP
 sendingOrder: SNAP → failure: SNAP
 sendingOrder: SNAP → failure: SNAP
 sendingOrder: GOOG → success: GOOG
 sendingOrder: HOOD → success: HOOD
 sendingOrder: AAPL → success: AAPL

 // Incorrect: not waiting for SNAP to finish before sending other orders
 // Incorrect: orders sent in a different order than placeOrder was called
 // Incorrect: not moving on after three failures in a row
 */
  1. System design: photo library.

  2. Project deep dive.

Published

Curated and edited by PracHub

Practice the questions from this interview

Discussion

Sign in to join the discussion. The author is notified of every comment.

Loading comments…

Interview at a glance

Company
Robinhood
Role
iOS Engineer
Rounds
Onsite
Difficulty
easy
Interview date
Jul 2026
Questions from this interview
3 questions

Related interview experiences

Explore more interview rounds, questions, and outcomes.

Browse interview experiences