DevPortfolio — Implementation Guide

Total estimated time: 60–80 hours, plus 1–2 weeks for App Store Review.

Day 1 — Project + TCA setup

Step 1. Create project

iOS app, iOS 17+. Add TCA via SwiftPM (https://github.com/pointfreeco/swift-composable-architecture).

Step 2. Capabilities

  • ARKit usage (Info.plist NSCameraUsageDescription mandatory)
  • CloudKit (for portfolio storage)
  • File Provider extension (for picking resume PDF)

Step 3. Root feature

@main struct DevPortfolioApp: App {
    let store = Store(initialState: AppFeature.State()) {
        AppFeature()._printChanges()  // dev only
    }
    var body: some Scene { WindowGroup { AppView(store: store) } }
}

Checkpoint: launch shows empty AppView, no crash.

Day 2 — Vision text extraction

Step 4. PDF → text

import PDFKit
import Vision

func extractText(from pdfURL: URL) async throws -> String {
    guard let doc = PDFDocument(url: pdfURL) else { throw ParseError.invalidPDF }
    var allText = ""
    for i in 0..<doc.pageCount {
        guard let page = doc.page(at: i), let image = page.thumbnail(of: CGSize(width: 1200, height: 1500), for: .mediaBox).cgImage else { continue }
        let request = VNRecognizeTextRequest()
        request.recognitionLevel = .accurate
        let handler = VNImageRequestHandler(cgImage: image)
        try handler.perform([request])
        let pageText = (request.results ?? [])
            .compactMap { $0.topCandidates(1).first?.string }
            .joined(separator: "\n")
        allText += pageText + "\n\n"
    }
    return allText
}

Checkpoint: pick a resume PDF, see extracted text logged to console.

Day 3–5 — Train CoreML classifier

Step 5. Collect training data

Sources:

  • Public anonymized resume datasets on Kaggle
  • Your own past resumes
  • 100+ paragraph samples per class minimum

Format as .csv:

text,label
"Senior iOS Engineer at Acme 2020-2024","experience"
"Built a custom networking layer with URLSession","experience"
"Swift, ARKit, CoreML","skill"
"BSc Computer Science Stanford 2018","education"
"PartyMode - an iOS app that...","project"

Step 6. Create ML project

Xcode → File → New → File → ML → Text Classifier. Drag your CSV. Train. Evaluate. Iterate on bad training examples. Export as .mlmodel.

Target accuracy: ≥ 80% on a held-out test set. If lower, the labeled data is too noisy or too small.

Step 7. Add the model to your Xcode target

Drag the .mlmodel file into Xcode. Xcode generates a Swift class.

import CoreML

public actor ResumeClassifier {
    private let model: ResumeClassifierMLModel  // Xcode-generated

    public init() throws {
        let config = MLModelConfiguration()
        self.model = try ResumeClassifierMLModel(configuration: config)
    }

    public func classify(_ text: String) throws -> (label: String, confidence: Double) {
        let input = ResumeClassifierMLModelInput(text: text)
        let output = try model.prediction(input: input)
        return (output.label, output.labelProbability[output.label] ?? 0)
    }
}

Checkpoint: feed a sample paragraph, get back a label + confidence.

Day 6 — Resume parser feature

Step 8. ResumeParserFeature reducer

@Reducer
public struct ResumeParserFeature {
    @ObservableState
    public struct State: Equatable {
        public var sourceURL: URL
        public var extractedText: String?
        public var classifiedParagraphs: [ClassifiedParagraph] = []
        public var isProcessing: Bool = false
    }

    public enum Action {
        case start
        case extractedText(String)
        case classified([ClassifiedParagraph])
        case delegate(Delegate)

        public enum Delegate {
            case parsingComplete(PortfolioFeature.State)
        }
    }

    @Dependency(\.pdfTextExtractor) var extractor
    @Dependency(\.resumeClassifier) var classifier

    public var body: some Reducer<State, Action> {
        Reduce { state, action in
            switch action {
            case .start:
                state.isProcessing = true
                let url = state.sourceURL
                return .run { send in
                    let text = try await extractor.extract(url)
                    await send(.extractedText(text))
                }
            case .extractedText(let text):
                state.extractedText = text
                return .run { send in
                    let paragraphs = text.split(separator: "\n\n").map(String.init)
                    let classified = try await withThrowingTaskGroup(of: ClassifiedParagraph.self) { group in
                        for p in paragraphs {
                            group.addTask { try await classifier.classify(p) }
                        }
                        var results: [ClassifiedParagraph] = []
                        for try await r in group { results.append(r) }
                        return results
                    }
                    await send(.classified(classified))
                }
            case .classified(let classified):
                state.classifiedParagraphs = classified
                let portfolio = PortfolioBuilder.build(from: classified)
                return .send(.delegate(.parsingComplete(portfolio)))
            case .delegate:
                return .none
            }
        }
    }
}

Checkpoint: pick a resume in the UI, see the AppFeature state transition into a populated PortfolioFeature.State within 3 s.

Day 7–8 — Portfolio UI

Step 9. PortfolioView

Profile header at top, then sections (Experience, Skills, Projects, Education). Each section a Section in a List with edit affordances.

Step 10. Persistence

Save PortfolioFeature.State to CloudKit private DB on every change. Use a simple CKRecord per portfolio with JSON-encoded Data for the body — we’re not querying fields, just round-tripping the whole structure.

Checkpoint: portfolio survives app restart. Confirmed via CloudKit Dashboard.

Day 9–10 — Sharing + QR codes

Step 11. Generate share URL

func makeShareURL(portfolioID: UUID) -> URL {
    URL(string: "https://yourorg.com/p/\(portfolioID.uuidString)")!
}

func makeQRCode(for url: URL) -> UIImage? {
    let data = url.absoluteString.data(using: .utf8)
    let filter = CIFilter.qrCodeGenerator()
    filter.setValue(data, forKey: "inputMessage")
    guard let output = filter.outputImage else { return nil }
    let scaled = output.transformed(by: CGAffineTransform(scaleX: 10, y: 10))
    let context = CIContext()
    guard let cg = context.createCGImage(scaled, from: scaled.extent) else { return nil }
    return UIImage(cgImage: cg)
}

Configure your domain with an apple-app-site-association file. Handle onOpenURL in the app to load the linked portfolio.

Checkpoint: share a QR. Scan with another phone. App opens or, if not installed, web fallback shows.

Day 11–13 — ARKit

Step 13. ARSessionController

Per architecture.md. Wrap ARView in UIViewRepresentable.

Step 14. Plane detection delegate

extension ARSessionController: ARSessionDelegate {
    public func session(_ session: ARSession, didAdd anchors: [ARAnchor]) {
        for anchor in anchors {
            if let plane = anchor as? ARPlaneAnchor, plane.alignment == .horizontal {
                Task { @MainActor in
                    self.placeNextProject(on: plane)
                }
            }
        }
    }
}

Step 15. Card placement & gestures

Place project cards spaced 30 cm apart on the detected plane. Add tap gesture to expand details. Pinch to scale.

Checkpoint: enter AR mode in a well-lit room. Within 5 s a horizontal plane is detected and 3 cards appear floating above the floor.

Day 14–15 — App Store submission

Step 16. Pre-submission

  • Run hardening-checklist.md
  • Screenshots: 6.7“, 6.1“, 5.5“, 12.9“ iPad
  • App Preview video (optional but improves conversion)
  • Privacy Nutrition Label
  • App Review Notes — include a sample resume PDF and a test AR-friendly room photo

Step 17. Submit and respond to Review

Common rejections for this kind of app:

  • Camera usage description vague → make it specific to AR
  • ML model not clearly working from screenshots → improve screenshot 1
  • AR mode doesn’t have a fallback for non-AR devices → add an alert

Step 18. Document the Review journey in the README

Take screenshots of every Apple Review email. Annotate. This becomes a powerful interview artifact — proves you’ve actually gone through the process.


Next: Hardening checklist