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.
- 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)
}
}
- 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
*/
-
System design: photo library.
-
Project deep dive.
Discussion
Loading comments…