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
NSCameraUsageDescriptionmandatory) - 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)
}
Step 12. Universal link handling
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