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.
Technical Support, ibanchecker.cash
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.
- 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.
- 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.
- 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.
Sources & References
- SWIFT IBAN Registry — Official IBAN country format specifications
- ISO 13616-1:2020 — International standard for IBAN structure
- ISO 7064 — MOD-97-10 check digit algorithm standard
Last updated: June 2026
Integrate IBAN validation via API
REST API with 1,000 free validations/month. JSON response with bank name, SEPA status, and BBAN breakdown.
How to Validate IBANs in JavaScript (API & Manual)
Two approaches: call the ibanchecker.cash API for full bank data, or implement MOD-97 validation yourself. Includes work...
IBAN Validation in Python: Complete Implementation with Regex and Check Digits
Two production-ready approaches: call the ibanchecker.cash API for bank metadata, or implement the MOD-97 algorithm your...
Building an IBAN Validation API: REST Endpoint Design and Edge Cases
Architect-level guide to IBAN API design — GET vs POST tradeoffs, error code standards, normalization contracts, KV cach...
IBAN Regex Patterns That Actually Work (And Why Simple Regex Isn't Enough)
Country-specific regex patterns for DE, GB, FR, NL plus a generic IBAN regex — and why regex is only step 1 of validatio...