Play
Uncurrify
The world, in prices that feel familiar.
Built in 1 day

Menus, signs, receipts—just point and get the real cost in your currency. Stay immersed on your trip, with a clear view of your spending.
Backstory
In 2024, I traveled to Medellin, Colombia and fell in love — the food, the people, the culture, but there was one problem.
Understanding the prices.
At first, converting it, wasn’t a big deal.
But surrounded by foreign prices, and having to do it every single time, it quickly became one.
That's why the first night at the hotel I thought:
What if currency didn't exist and every price just felt
…familiar?
…familiar?
With no coding experience — "literally zero" — I built it that night.
Make it yours
Take the code, push it further, and turn it into something amazing.
// Be warned, this is my first time "EVER" and I mean "EVEEER" coding something import UIKit import AVFoundation import Vision class CameraViewController: UIViewController, AVCaptureVideoDataOutputSampleBufferDelegate { private var captureSession: AVCaptureSession! private var previewLayer: AVCaptureVideoPreviewLayer! private var request: VNRecognizeTextRequest! private var rdContainerView: UIView! private var rdLabel: UILabel! private var copLabel: UILabel! private var editLabel: UILabel! private var rectangleGuideView: UIView! private var lastRecognizedNumber: Double? override func viewDidLoad() { super.viewDidLoad() setupCamera() setupVision() setupRectangleGuide() setupLabels() addTapGestureToCOPLabel() addPressGestureToRDLabel() // Add gesture recognizer to RD$ label } private func setupCamera() { captureSession = AVCaptureSession() guard let videoCaptureDevice = AVCaptureDevice.default(for: .video) else { return } let videoInput: AVCaptureDeviceInput do { videoInput = try AVCaptureDeviceInput(device: videoCaptureDevice) } catch { return } if captureSession.canAddInput(videoInput) { captureSession.addInput(videoInput) } else { return } let dataOutput = AVCaptureVideoDataOutput() dataOutput.setSampleBufferDelegate(self, queue: DispatchQueue(label: "videoQueue")) if captureSession.canAddOutput(dataOutput) { captureSession.addOutput(dataOutput) } previewLayer = AVCaptureVideoPreviewLayer(session: captureSession) previewLayer.frame = view.layer.bounds previewLayer.videoGravity = .resizeAspectFill view.layer.addSublayer(previewLayer) DispatchQueue.global(qos: .userInitiated).async { self.captureSession.startRunning() } } private func setupVision() { request = VNRecognizeTextRequest(completionHandler: recognizeText) request.recognitionLevel = .accurate } private func setupRectangleGuide() { let guideHeight: CGFloat = 200.0 // Rectangle guide height // Rectangle guide view (border only) rectangleGuideView = UIView() rectangleGuideView.layer.borderColor = UIColor.white.cgColor rectangleGuideView.layer.borderWidth = 2.0 rectangleGuideView.backgroundColor = UIColor.clear rectangleGuideView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(rectangleGuideView) NSLayoutConstraint.activate([ rectangleGuideView.leadingAnchor.constraint(equalTo: view.leadingAnchor), rectangleGuideView.trailingAnchor.constraint(equalTo: view.trailingAnchor), rectangleGuideView.centerYAnchor.constraint(equalTo: view.centerYAnchor), rectangleGuideView.heightAnchor.constraint(equalToConstant: guideHeight) ]) } private func setupLabels() { // Container view for RD$ label to add padding rdContainerView = UIView() rdContainerView.backgroundColor = .black rdContainerView.layer.cornerRadius = 10 rdContainerView.clipsToBounds = true rdContainerView.translatesAutoresizingMaskIntoConstraints = false // RD$ label inside the container rdLabel = UILabel() rdLabel.textColor = .white rdLabel.font = UIFont.boldSystemFont(ofSize: 48) rdLabel.textAlignment = .center rdLabel.translatesAutoresizingMaskIntoConstraints = false rdContainerView.addSubview(rdLabel) view.addSubview(rdContainerView) // COP label below the rectangle, outside of it copLabel = UILabel() copLabel.textColor = UIColor(red: 0.1, green: 0.3, blue: 0.8, alpha: 1.0) copLabel.font = UIFont.systemFont(ofSize: 24) copLabel.backgroundColor = UIColor(red: 0.7, green: 0.85, blue: 1.0, alpha: 1.0) copLabel.textAlignment = .center copLabel.layer.cornerRadius = 10 copLabel.clipsToBounds = true copLabel.isUserInteractionEnabled = true copLabel.attributedText = NSAttributedString( string: "COP: 0.00", // Placeholder text attributes: [.underlineStyle: NSUnderlineStyle.single.rawValue] ) copLabel.translatesAutoresizingMaskIntoConstraints = false // Edit label below the COP label editLabel = UILabel() editLabel.text = "Editar" editLabel.textColor = .white editLabel.font = UIFont.systemFont(ofSize: 18) editLabel.textAlignment = .center editLabel.translatesAutoresizingMaskIntoConstraints = false view.addSubview(copLabel) view.addSubview(editLabel) NSLayoutConstraint.activate([ // RD$ container view constraints rdContainerView.centerXAnchor.constraint(equalTo: rectangleGuideView.centerXAnchor), rdContainerView.centerYAnchor.constraint(equalTo: rectangleGuideView.centerYAnchor), rdContainerView.widthAnchor.constraint(lessThanOrEqualTo: rectangleGuideView.widthAnchor, multiplier: 0.9), // Padding constraints inside the container view rdLabel.topAnchor.constraint(equalTo: rdContainerView.topAnchor, constant: 8), // Top padding rdLabel.leadingAnchor.constraint(equalTo: rdContainerView.leadingAnchor, constant: 16), // Side padding rdLabel.trailingAnchor.constraint(equalTo: rdContainerView.trailingAnchor, constant: -16), // Side padding rdLabel.bottomAnchor.constraint(equalTo: rdContainerView.bottomAnchor, constant: -8), // Bottom padding // COP label constraints copLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), copLabel.topAnchor.constraint(equalTo: rectangleGuideView.bottomAnchor, constant: 20), copLabel.widthAnchor.constraint(lessThanOrEqualTo: view.widthAnchor, multiplier: 0.9), // Edit label constraints editLabel.centerXAnchor.constraint(equalTo: copLabel.centerXAnchor), editLabel.topAnchor.constraint(equalTo: copLabel.bottomAnchor, constant: 5) ]) } private func addTapGestureToCOPLabel() { let tapGesture = UITapGestureRecognizer(target: self, action: #selector(copLabelTapped)) copLabel.addGestureRecognizer(tapGesture) } @objc private func copLabelTapped() { let alertController = UIAlertController(title: "Enter COP Value", message: nil, preferredStyle: .alert) alertController.addTextField { textField in textField.keyboardType = .decimalPad textField.placeholder = "Enter COP amount" } let confirmAction = UIAlertAction(title: "OK", style: .default) { [weak self] _ in if let textField = alertController.textFields?.first, let inputText = textField.text, let newCOPValue = Double(inputText) { self?.updateLabels(cop: newCOPValue, rd: newCOPValue / 69.70) } } let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil) alertController.addAction(confirmAction) alertController.addAction(cancelAction) present(alertController, animated: true, completion: nil) } private func addPressGestureToRDLabel() { let pressGesture = UILongPressGestureRecognizer(target: self, action: #selector(rdLabelPressed)) rdLabel.addGestureRecognizer(pressGesture) rdLabel.isUserInteractionEnabled = true } @objc private func rdLabelPressed(_ gesture: UILongPressGestureRecognizer) { if gesture.state == .began { rdLabel.isHidden = true } else if gesture.state == .ended || gesture.state == .cancelled { rdLabel.isHidden = false } } private func recognizeText(request: VNRequest, error: Error?) { guard let observations = request.results as? [VNRecognizedTextObservation] else { return } for observation in observations { if let text = observation.topCandidates(1).first?.string { if let sanitizedNumber = sanitizeRecognizedText(text) { let result = sanitizedNumber / 69.70 if sanitizedNumber != lastRecognizedNumber { lastRecognizedNumber = sanitizedNumber DispatchQueue.main.async { self.updateLabels(cop: sanitizedNumber, rd: result) } } break } } } } private func sanitizeRecognizedText(_ text: String) -> Double? { let sanitizedText = text.replacingOccurrences(of: " ", with: "") let regex = try! NSRegularExpression(pattern: "^\\d{1,3}(?:[.,]\\d{3})*(?:[.,]\\d+)?$") if regex.firstMatch(in: sanitizedText, options: [], range: NSRange(location: 0, length: sanitizedText.count)) != nil { var cleanedText = sanitizedText.replacingOccurrences(of: ".", with: "") cleanedText = cleanedText.replacingOccurrences(of: ",", with: "") if let lastSeparatorRange = cleanedText.range(of: "[.,](?=\\d+$)", options: .regularExpression) { cleanedText.replaceSubrange(lastSeparatorRange, with: ".") } return Double(cleanedText) } // Fallback return if regex match fails return Double(sanitizedText.replacingOccurrences(of: ",", with: ".")) } private func updateLabels(cop: Double, rd: Double) { // Format the numbers with commas let formatter = NumberFormatter() formatter.numberStyle = .decimal formatter.maximumFractionDigits = 2 let formattedCOP = formatter.string(from: NSNumber(value: cop)) ?? "\(cop)" let formattedRD = formatter.string(from: NSNumber(value: rd)) ?? "\(rd)" // Update RD$ label if RD$ is non-zero if rd > 0 { rdLabel.text = "RD$: \(formattedRD)" rdLabel.isHidden = false } else { rdLabel.isHidden = true } // Update COP label copLabel.attributedText = NSAttributedString( string: "COP: \(formattedCOP)", attributes: [.underlineStyle: NSUnderlineStyle.single.rawValue] ) UIView.animate(withDuration: 0.5) { self.rdLabel.alpha = 1.0 self.copLabel.alpha = 1.0 self.editLabel.alpha = 1.0 } } } // Extension for AVCaptureVideoDataOutputSampleBufferDelegate extension CameraViewController { func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return } // Calculate the cropping rectangle based on the rectangle guide let guideFrame = rectangleGuideView.frame let normalizedGuideRect = previewLayer.metadataOutputRectConverted(fromLayerRect: guideFrame) let requestHandler = VNImageRequestHandler(cvPixelBuffer: pixelBuffer, orientation: .up, options: [:]) let croppedRequest = VNRecognizeTextRequest(completionHandler: recognizeText) croppedRequest.regionOfInterest = normalizedGuideRect try? requestHandler.perform([croppedRequest]) } }