ibanchecker.cash
Developer ResourcesJune 13, 2026 · 9 min read

IBAN Validation in Swift (iOS/macOS) — Implementation Guide

IBAN validation in Swift: full MOD-97 struct with chunked integer arithmetic, UITextField real-time feedback extension, async/await URLSession integration with the ibanchecker.cash API, and XCTest coverage.

Share

IBAN validation in Swift is required for any iOS or macOS application that collects international bank account numbers — mobile banking apps, payment wallets, invoice tools, and business utilities. This guide covers implementing the full ISO 13616 MOD-97 algorithm as a Swift struct, adding a UITextField extension for real-time form feedback, and calling the ibanchecker.cash REST API via URLSession to retrieve bank metadata.

What Are the Three Validation Layers You Must Implement?

Every IBAN validator must apply three checks in order. Stopping at layer 1 or 2 will accept IBANs with real structural errors.

  1. Format: The string must begin with two uppercase letters (country code), followed by exactly two digits (check digits), followed by alphanumeric BBAN characters only. Strip spaces and dashes before validating.
  2. Length: Each country has a fixed IBAN length defined by the SWIFT IBAN Registry. German IBANs are 22 characters; French IBANs are 27; Turkish IBANs are 26. Any deviation is an error.
  3. MOD-97: Move the first four characters to the end, replace every letter A–Z with its two-digit numeric equivalent (A=10, B=11 … Z=35), interpret the full string as an integer, and compute its value mod 97. The IBAN is valid if and only if the result equals 1.

How Do You Implement the MOD-97 Algorithm in Swift?

Swift integers are bounded, so the numeric string from step 3 — which can be up to 34 digits after letter expansion — exceeds UInt64.max. Process the MOD-97 in chunks to stay within standard integer bounds.

import Foundation

enum IBANValidationError: Error, LocalizedError {
  case invalidFormat
  case unknownCountry(String)
  case wrongLength(expected: Int, actual: Int)
  case invalidCheckDigits

  var errorDescription: String? {
    switch self {
    case .invalidFormat:
      return "Invalid format: must start with a country code followed by two digits"
    case .unknownCountry(let code):
      return "Unknown country code: \(code)"
    case .wrongLength(let expected, let actual):
      return "Wrong length: expected \(expected), got \(actual)"
    case .invalidCheckDigits:
      return "Invalid check digits (MOD-97 failed)"
    }
  }
}

struct IBANValidator {
  static let lengths: [String: Int] = [
    "AD": 24, "AE": 23, "AL": 28, "AT": 20, "AZ": 28,
    "BA": 20, "BE": 16, "BG": 22, "BH": 22, "BR": 29,
    "BY": 28, "CH": 21, "CR": 22, "CY": 28, "CZ": 24,
    "DE": 22, "DK": 18, "DO": 28, "EE": 20, "EG": 29,
    "ES": 24, "FI": 18, "FO": 18, "FR": 27, "GB": 22,
    "GE": 22, "GI": 23, "GL": 18, "GR": 27, "GT": 28,
    "HR": 21, "HU": 28, "IE": 22, "IL": 23, "IQ": 23,
    "IS": 26, "IT": 27, "JO": 30, "KW": 30, "KZ": 20,
    "LB": 28, "LC": 32, "LI": 21, "LT": 20, "LU": 20,
    "LV": 21, "LY": 25, "MC": 27, "MD": 24, "ME": 22,
    "MK": 19, "MR": 27, "MT": 31, "MU": 30, "NL": 18,
    "NO": 15, "PK": 24, "PL": 28, "PS": 29, "PT": 25,
    "QA": 29, "RO": 24, "RS": 22, "SA": 24, "SC": 31,
    "SE": 24, "SI": 19, "SK": 24, "SM": 27, "ST": 25,
    "SV": 28, "TL": 23, "TN": 24, "TR": 26, "UA": 29,
    "VA": 22, "VG": 24, "XK": 20,
  ]

  static func normalize(_ raw: String) -> String {
    raw.trimmingCharacters(in: .whitespaces)
      .replacingOccurrences(of: " ", with: "")
      .replacingOccurrences(of: "-", with: "")
      .uppercased()
  }

  static func validate(_ raw: String) throws {
    let iban = normalize(raw)

    let formatRegex = try! NSRegularExpression(pattern: "^[A-Z]{2}[0-9]{2}[A-Z0-9]+$")
    let range = NSRange(iban.startIndex..., in: iban)
    guard formatRegex.firstMatch(in: iban, range: range) != nil else {
      throw IBANValidationError.invalidFormat
    }

    let country = String(iban.prefix(2))
    guard let expected = lengths[country] else {
      throw IBANValidationError.unknownCountry(country)
    }
    guard iban.count == expected else {
      throw IBANValidationError.wrongLength(expected: expected, actual: iban.count)
    }

    let rearranged = String(iban.dropFirst(4)) + String(iban.prefix(4))
    let numeric = rearranged.unicodeScalars.map { scalar -> String in
      let value = scalar.value
      return value >= 65 ? String(value - 55) : String(UnicodeScalar(value)!)
    }.joined()

    var remainder = 0
    for char in numeric {
      remainder = (remainder * 10 + Int(String(char))!) % 97
    }

    guard remainder == 1 else {
      throw IBANValidationError.invalidCheckDigits
    }
  }

  static func isValid(_ raw: String) -> Bool {
    (try? validate(raw)) != nil
  }

  static func formatted(_ raw: String) -> String {
    let iban = normalize(raw)
    return stride(from: 0, to: iban.count, by: 4).map { i -> String in
      let start = iban.index(iban.startIndex, offsetBy: i)
      let end = iban.index(start, offsetBy: min(4, iban.count - i))
      return String(iban[start..<end])
    }.joined(separator: " ")
  }
}

How Do You Add Real-Time IBAN Validation to a UITextField?

Extend UITextField with an IBAN-aware target-action that validates as the user types and shows inline feedback. The local MOD-97 check is synchronous and runs on every keystroke; the API call for bank metadata fires only once the local check passes.

import UIKit

class IBANTextField: UITextField {
  private let feedbackLabel: UILabel = {
    let label = UILabel()
    label.font = .systemFont(ofSize: 13)
    label.translatesAutoresizingMaskIntoConstraints = false
    return label
  }()

  override init(frame: CGRect) {
    super.init(frame: frame)
    addTarget(self, action: #selector(textChanged), for: .editingChanged)
  }

  required init?(coder: NSCoder) {
    super.init(coder: coder)
    addTarget(self, action: #selector(textChanged), for: .editingChanged)
  }

  @objc private func textChanged() {
    guard let text = text, !text.isEmpty else {
      feedbackLabel.text = nil
      return
    }

    if IBANValidator.isValid(text) {
      feedbackLabel.text = "✓ Valid IBAN"
      feedbackLabel.textColor = UIColor.systemGreen
      fetchBankInfo(for: text)
    } else {
      feedbackLabel.text = "Invalid IBAN"
      feedbackLabel.textColor = UIColor.systemRed
    }
  }

  private func fetchBankInfo(for iban: String) {
    IBANCheckerAPI.validate(iban: iban) { result in
      DispatchQueue.main.async {
        if let bankName = result?.bankName {
          self.feedbackLabel.text = "✓ \(bankName)"
        }
      }
    }
  }
}

How Do You Call the ibanchecker.cash API with URLSession?

Use URLSession with async/await (iOS 15+) or a completion handler for earlier deployment targets. The ibanchecker.cash API accepts JSON POST requests and returns structured validation results including bank name and BIC.

import Foundation

struct IBANValidationResult: Codable {
  let valid: Bool
  let iban: String?
  let country: String?
  let bankName: String?
  let bic: String?
  let formatted: String?
  let error: String?
}

struct IBANCheckerAPI {
  static let baseURL = URL(string: "https://ibanchecker.cash")!

  static func validate(iban: String) async throws -> IBANValidationResult {
    let url = baseURL.appendingPathComponent("/api/v1/validate")
    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    request.httpBody = try JSONEncoder().encode(["iban": iban])

    let (data, _) = try await URLSession.shared.data(for: request)
    return try JSONDecoder().decode(IBANValidationResult.self, from: data)
  }

  static func validateBulk(ibans: [String]) async throws -> [IBANValidationResult] {
    let url = baseURL.appendingPathComponent("/api/v1/validate/bulk")
    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    request.httpBody = try JSONEncoder().encode(["ibans": ibans])

    let (data, _) = try await URLSession.shared.data(for: request)
    let response = try JSONDecoder().decode([String: [IBANValidationResult]].self, from: data)
    return response["results"] ?? []
  }
}

// Usage
Task {
  let result = try await IBANCheckerAPI.validate(iban: "DE89370400440532013000")
  if result.valid {
    print("Bank: \(result.bankName ?? "Unknown")")
    print("BIC:  \(result.bic ?? "Unknown")")
  }
}

How Do You Write Unit Tests for the Swift IBAN Validator?

import XCTest

final class IBANValidatorTests: XCTestCase {
  let validIBANs = [
    "DE89370400440532013000",
    "GB29NWBK60161331926819",
    "FR7630006000011234567890189",
    "NL91ABNA0417164300",
    "BE68539007547034",
    "TR330006100519786457841326",
  ]

  let invalidIBANs = [
    "DE89370400440532013001",
    "GB29NWBK6016133192681",
    "NOTANIBAN",
    "",
  ]

  func testValidIBANs() {
    for iban in validIBANs {
      XCTAssertTrue(IBANValidator.isValid(iban), "Expected valid: \(iban)")
    }
  }

  func testInvalidIBANs() {
    for iban in invalidIBANs {
      XCTAssertFalse(IBANValidator.isValid(iban), "Expected invalid: \(iban)")
    }
  }

  func testNormalizesSpaces() {
    XCTAssertTrue(IBANValidator.isValid("DE89 3704 0044 0532 0130 00"))
  }

  func testNormalizesLowercase() {
    XCTAssertTrue(IBANValidator.isValid("de89370400440532013000"))
  }

  func testFormatting() {
    let formatted = IBANValidator.formatted("DE89370400440532013000")
    XCTAssertEqual(formatted, "DE89 3704 0044 0532 0130 00")
  }

  func testUnknownCountryThrows() {
    XCTAssertThrowsError(try IBANValidator.validate("XX89370400440532013000")) { error in
      XCTAssertTrue(error is IBANValidationError)
    }
  }
}

For production iOS apps, combine the local validator (synchronous, no network) with the ibanchecker.cash API (async, returns bank metadata). Check the pricing page for rate limits and API key requirements.

Last updated: June 2026

Validate an IBAN instantly

Free IBAN checker — MOD-97 verification, bank lookup, and SEPA status across 84 countries.

Open IBAN Checker →

Related Articles