© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2022
A. NekrasovSwift Recipes for iOS Developershttps://doi.org/10.1007/978-1-4842-8098-0_3

3. Working with Strings

Alexander Nekrasov1  
(1)
Moscow, Russia
 

Strings are also data they’re arrays of bytes. But unlike simple Data, the String class knows how to interpret that data. To interpret data correctly, we need to know encoding. The most widespread options are UTF-8 and UTF-16, also known as Unicode.

UTF-8 uses one to four bytes to encode one character. Latin characters from the ASCII range use one byte. If the text contains only Latin characters, spaces, and standard punctuation symbols, ASCII and UTF-8 text strings are identical.

UTF-16 uses two of four bytes per character. As two bytes make 65536 combinations, they cover most of existing symbols. But not all of them. That’s why some symbols need four bytes.

We don’t need to know which encoding is used inside String or Character structures (yes, in Swift, they’re defined as structs, not classes). But knowing that it’s a String, we can get a lot of information about stored data compared to a Data object.

We know that text strings consist of characters, so we can get one of them. We can also get a range, which is called a Substring. And we don’t need to think how wide is one Character; Swift will calculate width in bytes automatically. We can remove whitespaces and/or newlines, we can make it lowercase or uppercase, and we can analyze its content and much more.

Analyzing String Content

Let’s suppose for a minute that we have a String object. We want to know what’s inside. Does it have Latin letters? Or numbers? Is it a strong password? A valid email?

UIKit offers two classes to get text input from the user: UITextField and UITextView . Both of them return data as a test. Even if we request a number from the user (their age, bank card number, a number they should guess) and choose an on-screen keyboard with numbers only, we’ll get a String (Recipe 3-1).
public extension StringProtocol {
    var containsOnlyDigits: Bool {
        let notDigits = NSCharacterSet.decimalDigits.inverted
        return rangeOfCharacter(from: notDigits, options: String.CompareOptions.literal, range: nil) == nil
    }
    var containsOnlyLetters: Bool {
        let notLetters = NSCharacterSet.letters.inverted
        return rangeOfCharacter(from: notLetters, options: String.CompareOptions.literal, range: nil) == nil
    }
    var containsIllegalCharacters: Bool {
        rangeOfCharacter(from: NSCharacterSet.illegalCharacters, options: String.CompareOptions.literal, range: nil) != nil
    }
    var containsOnlyPasswordAllowed: Bool {
        var allowedCharacters = CharacterSet()
        allowedCharacters.insert(charactersIn: "!"..."~")
        let forbiddenCharacters = allowedCharacters.inverted
        return rangeOfCharacter(from: forbiddenCharacters, options: String.CompareOptions.literal, range: nil) == nil
    }
    var isAlphanumeric: Bool {
        let notAlphanumeric = NSCharacterSet.decimalDigits.union(NSCharacterSet.letters).inverted
        return rangeOfCharacter(from: notAlphanumeric, options: String.CompareOptions.literal, range: nil) == nil
    }
    var containsLetters: Bool {
        rangeOfCharacter(from: NSCharacterSet.letters, options: String.CompareOptions.literal, range: nil) != nil
    }
    var containsDigits: Bool {
        rangeOfCharacter(from: NSCharacterSet.decimalDigits, options: String.CompareOptions.literal, range: nil) != nil
    }
    var containsUppercaseLetters: Bool {
        rangeOfCharacter(from: NSCharacterSet.uppercaseLetters, options: String.CompareOptions.literal, range: nil) != nil
    }
    var containsLowercaseLetters: Bool {
        rangeOfCharacter(from: NSCharacterSet.lowercaseLetters, options: String.CompareOptions.literal, range: nil) != nil
    }
    var containsNonAlphanumericCharacters: Bool {
        let notAlphanumeric = NSCharacterSet.decimalDigits.union(NSCharacterSet.letters).inverted
        return rangeOfCharacter(from: notAlphanumeric, options: String.CompareOptions.literal, range: nil) != nil
    }
}
Recipe 3-1

Checking String for Digits and Letters

These three extensions allow us to make fast (but simple) content analysis:
  • containsOnlyDigits returns true if String contains nothing but digits. No decimal separators, no spaces, no special characters. We can get similar results by casting String to Int, but without the potential overflow problem, and we handle negative numbers differently.

  • containsOnlyLetters returns true if String contains nothing but letters. Without modifications, it doesn’t have much use, but we’ll update it later to make it more powerful.

  • isAlphanumeric returns true if String contains letters and numbers. For example, a Firestore document ID is always alphanumeric, as well as the String representation of MD5 hashes.

As we’re discussing real-life examples, let’s talk about use cases. Registration forms may contain
  • First and last names

  • Email address

  • Password

  • Phone number

  • Gender

  • Date of birth or age

  • Credit card information

  • Address

When Apple reviewers see such a list on the registration screen, they reject the app. But any of these fields may be necessary for specific functionality.

We’ll leave address verification behind the scope, as it’s specific for every country, and discuss other fields. For all extensions described in the following, we’ll use names starting with isValid, for example, isValidName and isValidEmail, and write them as calculated properties.

Note

Using function or calculated property is always the programmer’s choice. There’s a popular opinion that calculated properties should be used when the function reflects a property of an object and doesn’t change it.

First, Last, and Other Names

Depending on your app’s region and your preferences, it can be one field for the full name, separate fields for the first and last names, or a more detailed set of fields, including middle name or patronymic. They can only contain letters, spaces, dashes, and maybe the dot symbol for titles like “Jr.” or “Mr.”. An example of name verification is shown in Recipe 3-2.
public extension StringProtocol {
    var isValidName: Bool {
        let allowedCharactgers = NSCharacterSet.letters.union(NSCharacterSet.whitespaces).union(CharacterSet(charactersIn: ".-"))
        let forbiddenCharacters = allowedCharactgers.inverted
        return rangeOfCharacter(from: forbiddenCharacters, options: String.CompareOptions.literal, range: nil) == nil
    }
}
Recipe 3-2

Name Verification

This extension merges three character sets:
  • Whitespaces

  • Letters

  • Characters . and -

It inverts the resulting set and verifies that no forbidden characters are found in the String.

Note

In May 2020, famous business magnate Elon Musk (PayPal, Tesla, SpaceX) and Canadian musician Grimes had a son who immediately got his share of fame because of a very unusual name – X Æ A-12. However, the Office of Vital Records in California requires that names contain only the 26 alphabetical characters of the English language (plus hyphens and apostrophes); thus, this name was rejected. The couple had to change the name to X AE A-XII, where X is the first name and AE A-XII is the middle name. Just like the administration of California, our extension would reject the first option and accept the final one.

Email Address

This topic always causes a lot of controversy . While some developers try to restrict email addresses to a problem-free territory, others want to give users as much freedom as possible – up to lack of verification on the client’s side. For example, is the email address root@localhost valid? It actually is. But when the user registers in an app, there’s not much use to it, as we can’t send any emails to it and it’s not unique.

We’ll discuss email verification in the context of user registration. And we will allow only emails with the following format: [email protected], where some-string may contain Latin letters, numbers, and the symbols ., _, -, %, and +; some-domain may contain Latin letters, numbers, -, and . for subdomains; and ext must have only Latin letters and be no shorter than two characters and no longer than 64.

Analysis can be done with regular expressions as shown in Recipe 3-3. We won’t discuss regular expressions in detail. In short, it’s a pattern. There’s standard syntax for regular expressions , which is universal for different platforms and languages. Regular expression for email verification looks like this: [A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,64}.

It’s not the only option when it comes to regular expressions. There are other options, but this expression works quite well and has been tested by hundreds of thousands of users. It can be used with a Firebase Auth framework, filtering unnecessary requests, but it’s even more useful if you have a custom back end.
extension StringProtocol {
    var isValidEmail: Bool {
        let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,64}"
        let emailTest = NSPredicate(format: "SELF MATCHES %@", emailRegEx)
        return emailTest.evaluate(with: self)
    }
}
Recipe 3-3

Email Verification

Password

How annoying it is when you try to set "12345678" as your password and the app or website you’re trying to set the password for won’t allow, isn’t it? Depending on the specifics of your app, the minimum allowed strength of a password should be different. If you’re writing an app for a financial service, the password should be strong. If it’s a personal calorie counter or an online video streaming service, you may be allowed to use a much more simple password. But in any case, there are passwords that should never be allowed, for example, "12345678".

The basic recommendations for passwords are as follows:
  • They should contain at least one capital and one lowercase letter.

  • They should contain at least one digit.

  • They should contain at least one special character (punctuation mark, percent sign, dollar sign, or similar).

  • They shouldn’t have spaces, new lines, or tabs.

The last recommendation is a standard for most services, though it’s not a strict rule. The previous recommendations are not so strict either, but it’s a good practice to force the user to fulfil at least two of three.

The extension from Recipe 3-4 will verify that
  • The string has only alphanumeric characters, plus some special characters (dot, comma, and others)

  • The string has at least one English letter

  • Two of these three conditions are met:
    • The string has both lowercase and capital letters.

    • The string has at least one numeric character.

    • The string has at least one allowed nonalphanumeric character.

public extension StringProtocol {
    var isValidPassword: Bool {
        if containsIllegalCharacters || !containsOnlyPasswordAllowed {
            return false
        }
        if !containsLetters {
            return false
        }
        var strength = 0
        if containsUppercaseLetters {
            strength += 1
        }
        if containsLowercaseLetters {
            strength += 1
        }
        if containsDigits {
            strength += 1
        }
        if containsNonAlphanumericCharacters {
            strength += 1
        }
        return strength >= 3
    }
}
Recipe 3-4

Password Validation

Phone Number

Phone number verification is another popular operation. Undoubtedly, the server should make its own verification. But to avoid unnecessary traffic, provide fast error handling, and protect the server from potentially harmful requests, it’s better to make the verification on the client’s side as well.

While email address has more flexibility, phone numbers are limited with digits, plus sign and separators. The length range is narrow, and it varies from country to country. We can make basic verifications with regular expressions, but there is a more efficient way that we will discuss in the following.

The PhoneNumberKit library allows to parse phone numbers, verify their validity, and convert local phone numbers to an international format. In Recipe 3-5 we use this library to verify phone number validity. Let’s add it to our Podfile:
pod 'PhoneNumberKit'
import PhoneNumberKit
public extension String {
    var isValidPhoneNumber: Bool {
        do {
            try phoneNumberKit.parse("+33 6 89 017383")
            return true
        }
        catch {
            return false
        }
    }
}
Recipe 3-5

Phone Number Verification

Gender

Gender selection shouldn’t be in the registration form unless it’s absolutely necessary. And if it is, try to make it optional. First, Apple requires your app to follow their Guidelines, which don’t allow asking for unnecessary personal data. Convincing Apple that knowing the user’s gender is necessary for the app functionality will be complicated. Second, it’s unclear which list of genders to present to the user. If you present a choice between male and female only, you can upset the LGBTQ+ community. They are potential clients as well, so that’s something to keep in mind.

If you still decided to add gender selection in your app, add it as a selection of two or more options without using the keyboard. This will make string analysis unnecessary.

Date of Birth

This field may be necessary if you have adult content. Age (or date of birth) selection can give you an idea of which content to show or hide. If your app sells food and drinks, you can hide alcohol and tobacco from minors (specific age restriction depends on the region).

The best practice is to add a date selector component to your UI, for example, UIDatePicker. Then you format the date using one of the date formatters matching your region. For example:
let selectedDate = Date()
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "MM/dd/yyyy"
dateFormatter.string(from: selectedDate)

Date formats vary from country to country. Your app should use the format that is commonly used in your region or offer several options depending on device settings.

If you need to work with user input and you don’t have an option to add a date picker, NSDataDetector comes to help (see Recipe 3-6).
public extension String {
    var asDate: Date? {
        let range = NSRange(location: 0, length: count)
        return try? NSDataDetector(types: NSTextCheckingResult.CheckingType.date.rawValue)
                .matches(in: self, range: range)
            .compactMap(.date)
            .first
    }
}
Recipe 3-6

Parsing Date from String

This extension will try to convert any user-typed string to a Date object, or return nil if it’s not possible .

Credit Card Information

If you want to add any paid functionalities to the app, you need to use In-App Purchases. Apple won’t allow you to use direct payment; otherwise, the app will be rejected.

On the other hand, if you sell goods or services that are not part of the app, you may need to get the user’s credit card number. Typical examples are food delivery apps, online shops, and booking services.

The most popular credit cards have these four components:
  • Number

  • Expiration month and year

  • Security code (CVC/CVV)

  • Name

Depending on the card type, the number varies from 16 to 19 digits. Usually, the number is separated by spaces after each four digits, but it’s not compulsory. For that reason, we will remove all spaces before verification.

When it comes to expiration month and year, everything is pretty straightforward. Month is a number from 1 to 12, and year has two or four digits. We’ll verify if the expiration date is during the current month of the current year or later.

The security code is always a number. It has three or four digits. As it can start with leading zeros, integer representation is a number between 0 and 9999.

The verification of names is identical to the verification of the user name on the registration form, so we’ll simply reuse our existing code. Full credit card verification code is shown in Recipe 3-7.
public struct CreditCard {
    public var number: String
    public var expMonth: Int
    public var expYear: Int
    public var securityCode: Int
    public var nameOnCard: String
    public init(number: String, expMonth: Int, expYear: Int, securityCode: Int, nameOnCard: String) {
        self.number = number
        self.expMonth = expMonth
        self.expYear = expYear
        self.securityCode = securityCode
        self.nameOnCard = nameOnCard
    }
    var isNumberValid: Bool {
        let numberProcessed = String(number.filter { !" ".contains($0) })
        return numberProcessed.containsOnlyDigits && numberProcessed.count >= 16 && numberProcessed.count <= 19
    }
    var isExpirationValid: Bool {
        if expMonth < 1 || expMonth > 12 {
            return false
        }
        let expYear4Digits = (expYear < 100) ? expYear + 2000 : expYear
        let calendar = Calendar(identifier: .gregorian)
        let monthToday = calendar.component(.month, from: Date())
        let yearToday = calendar.component(.year, from: Date())
        switch expYear4Digits {
        case ..<yearToday: return false
        case yearToday: return expMonth >= monthToday
        default: return true
        }
    }
    var isSecurityCodeValid: Bool {
        securityCode < 10000
    }
    var isNameValid: Bool {
        nameOnCard.isValidName
    }
    public var isCardValid: Bool {
        isNumberValid && isExpirationValid && isSecurityCodeValid && isNameValid
    }
}
Recipe 3-7

Credit Card Validation

Unlike most of our previous recipes, this one is a class, not an extension. A credit card number is a whole thing – for example, we validate the month and year together, so we won’t separate them into a group of extensions.

Before that, the validation structure needs to be filled with data. Use the isCardValid property to get the result as a Boolean value. If you need to know which component of a number is invalid, use isNameValid, isNumberValid, and other functions.

Note

In this chapter, we discuss work with information, not UI, but sometimes, it may be useful to try code in an app, not in a playground. For this, you can use libraries for entering (scanning) and visualizing bank cards. In Figure 3-1, you can see visualization made by the CreditCardView library ( https://github.com/jboullianne/CreditCardView ). For scanning, you can use card.io ( https://github.com/card-io/card.io-iOS-SDK ).

An image of a typical VISA card contains a placeholder for the 16 digit card number. The card holder's name is Jean Marc Boullianne. The expiry date is 02 slash 30.

Figure 3-1

Credit card visualization in an iOS app

Base64 and Hex Encoding

Oftentimes, we need to present Data in the form of a String. We may need it for debug output, for passing it to the server, or for JSON integration. There are two common ways to do it: Base64 and hex encoding.

Base64 Encoding

Base64 is a way to represent binary data as a text string. It splits data into 6-bit fragments and encodes it using human-readable characters:
  • 26 uppercase Latin letters

  • 26 lowercase Latin letters

  • 10 digits

  • + (plus sign)

  • / (slash sign)

As we can only encode integer amount of bytes, and 1 byte equals 8 bits, the data length needs to be divisible by three. If it isn’t, padding is used. The equal sign (=) is used for padding when you need to add empty bits to the end of data. By adding one or two paddings, you can always get a number of bits, which is divisible by both 6 and 8.

The advantage of Base64 encoding is that characters used for encoding are basic ASCII characters, which means they’re represented the same in all 1-byte encodings.

Table 3-1 encodes the word "Swift" into Base64.
Table 3-1

Binary representation of the word “Swift”

S

w

i

f

t

0101 0011

0111 0111

0110 1001

0110 0110

0111 0100

Totally, we get 40 bytes, which is not divisible by 6, so we’ll need padding. Table 3-2 splits it into 6-bit fragments.
Table 3-2

Base64 representation of the word “Swift”

010100

110111

011101

101001

011001

100111

0100XX

U

3

d

p

Z

n

Q=

Result: “Swift” -> U3dpZnQ=.

Encoding ASCII text to Base64 doesn’t usually make sense, but using the same method, we can encode pictures, audio files, and basically anything to strings. It’s more compact than hex encoding, using two symbols to encode each byte, and it’s easily encodable and decodable. Typical examples of Base64-encoded binaries are email attachments. Recipe 3-8 shows how to decode from Base64 and encode to this format.
let base64String = "U3dpZnQ="
let data = Data(base64Encoded: base64String)!
// Check if we got correct text
let decodedString = String(data: data, encoding: .utf8)!
print(decodedString)
let base64Again = data.base64EncodedString()
print(base64Again)
Recipe 3-8

Encoding and Decoding Base64

In the preceding example, we decode Base64 String into Data using the optional constructor Data(base64Encoded:). In this case, we use force unwrapping because we know that it’s valid. Keep in mind that in production code, it’s not an advisable practice.

Data has a method base64EncodedString for encoding. These are standard Swift functions; no extra code is needed in this case .

Hex Encoding

Hex representation is more human-readable and more simple. We don’t need any paddings; each byte is represented with two hex symbols. A hex symbol is a number from 0 to 9 or a letter from A to F. Each hex symbol encodes 4 bytes of information.

Let’s repeat the "Swift" encoding string (Table 3-3).
Table 3-3

Binary representation of the word “Swift”

S

w

i

f

t

0101 0011

0111 0111

0110 1001

0110 0110

0111 0100

Table 3-4 shows how it looks in hex representation.
Table 3-4

Hex representation of the word “Swift”

0101

0011

0111

0111

0110

1001

0110

0110

0111

0100

5

3

7

7

6

9

6

6

7

4

As you can see, hex is longer than Base64. And in this particular case, it looks like a number. But with some practice, you can learn to read it. For example, S will always be encoded to 53, w to 77, and so on.

Note

In source code, hex numbers start with 0x to avoid confusion with decimal numbers. If hex is represented as a String, it’s wrapped in quotes but doesn’t have any prefixes.

Let’s switch to code. The extension from Recipe 3-9 offers a computed property turning Data to hex-encoded String and optional constructor doing the opposite .
public extension Data {
    init?(hexString: String) {
        if hexString.count % 2 != 0 {
            return nil
        }
        let allowedCharacters = CharacterSet(charactersIn: "01234567890abcdefABCDEF")
        if hexString.rangeOfCharacter(from: allowedCharacters.inverted, options: String.CompareOptions.literal, range: nil) != nil {
            return nil
        }
        self.init(capacity: hexString.count / 2)
        for i in stride(from: 0, to: hexString.count, by: 2) {
            let startIndex = hexString.index(hexString.startIndex, offsetBy: i)
            let endIndex = hexString.index(hexString.startIndex, offsetBy: i + 1)
            let hexPair = String(hexString[startIndex...endIndex])
            let num = UInt8(hexPair, radix: 16)!
            self.append(num)
        }
    }
    var hexString: String {
        map { String(format: "%02hhx", $0) }.joined()
    }
}
Recipe 3-9

Converting Hex String to Data and Back

Please note that the initializer is optional as not every String can be converted to Data. We also used force unwrapping because we made all the necessary checks in advance and we know that converting hexPair to UInt8 will never fail.

The following example converts a hex-encoded "Swift" string to readable text.
let hexSwift = "5377696674"
let hexData = Data(hexString: hexSwift)!
let hexString = String(data: hexData, encoding: .utf8)!
print(hexString)
Similarly, these conversions can be presented as String extensions (Recipe 3-10):
public extension String {
    init(hexFromData: Data) {
        append(contentsOf: hexFromData.map { String(format: "%02hhx", $0) }.joined())
    }
    func hexToData() -> Data? {
        if hexString.count % 2 != 0 {
            return nil
        }
        let allowedCharacters = CharacterSet(charactersIn: "01234567890abcdefABCDEF")
        if hexString.rangeOfCharacter(from: allowedCharacters.inverted, options: String.CompareOptions.literal, range: nil) != nil {
            return nil
        }
        var result = Data(capacity: self.count / 2)
        for i in stride(from: 0, to: hexString.count, by: 2) {
            let startIndex = hexString.index(hexString.startIndex, offsetBy: i)
            let endIndex = hexString.index(hexString.startIndex, offsetBy: i + 1)
            let hexPair = String(hexString[startIndex...endIndex])
            let num = UInt8(hexPair, radix: 16)!
            result.append(num)
        }
        return result
    }
}
Recipe 3-10

Converting Hex String to Data and Back (As String Extensions)

MD5, SHA, and Other Hashes

Hashes are useful when you need to convert data into a short string without the possibility to restore the original data. Hashes can’t be restored, but they can be compared. The same data always produces the same hash.

One of the most popular use cases is storing passwords. As you probably know, websites and apps that require registration never send you your password when you try to restore it – they offer to create a new one instead. It works this way because they don’t store your password as it’s unsafe. What they do is to store a hash of your password. When you type the same password during the next login, their hashes match, and the server returns a successful response. Said response is usually a session token. If you made a mistake at least in one character, the hash will be different, and the server will return an error. There’s a tiny chance that hashes will match, but it’s so small that we can ignore it.

Note

An MD5 hash is 128 bits long (16 bytes). It means there are 2128 unique hashes. The chance that two hashes will be accidentally the same is 1/2128, which is around 3 ∗ 10−39, or 3 ∗ 10−37%, or 0.0000000000003 yoctopercent.

Another common use of hashes is data verification. If you send a big portion of data (one song in an mp3 format contains millions of bytes), you may want to make sure it safely reaches the other end. The stack or protocols we use on the Internet guarantee it for us – they use hashes and control sequences under the hood. But sometimes, we need to verify the data ourselves. For example, to confirm that the data was downloaded completely after the app crashed or was terminated to save battery. For this purpose, we can calculate the hash of a file in advance and store it on a server. On an app side, we calculate the hash of a downloaded file; then we compare them. If hashes match, very likely it’s the same data. And as a hash is only 16 bytes long (in the case of MD5), it’s not a big trouble to verify it.

Now let’s see how we can calculate hashes, starting with the most popular one – MD5.

MD5 Hash

MD5 (message-digest algorithm, version 5) was originally used in cryptography, but nowadays, it has found usages in other areas. Cryptography, on the other hand, stopped widely using it because of its extensive vulnerabilities.

Any data can be encoded, but more often than not, it comes to Data and String objects. String needs to be converted to Data before a hash calculation can be made. The result is always a Data object, which can be printed using hex or Base64 representation.

Swift doesn’t provide native MD5 hashing implementation, but we can use the CommonCrypto library. Remember that if you’re using a production app, not a playground, you need to create a bridging header and include a CommonCrypto header:
#import <CommonCrypto/CommonCrypto.h>
Moreover, if you test the following code, you may get this warning:
'CC_MD5' was deprecated in iOS 13.0: This function is cryptographically broken and should not be used in security contexts. Clients should migrate to SHA256 (or stronger).
This means that Swift considers MD5 algorithm not suitable for cryptographic purposes, but it’s ok to use for data validation. As for SHA-256 and other algorithms, we’ll go over them later in this chapter. Recipe 3-11 shows how to calculate MD5 hash of Data and String.
import var CommonCrypto.CC_MD5_DIGEST_LENGTH
import func CommonCrypto.CC_MD5
import typealias CommonCrypto.CC_LONG
public extension String {
    var md5: Data? {
        data(using: .utf8)?.md5
    }
}
public extension Data {
    var md5: Data {
        let length = Int(CC_MD5_DIGEST_LENGTH)
        var hash = Data(count: length)
        _ = hash.withUnsafeMutableBytes { digestBytes -> UInt8 in
            self.withUnsafeBytes { messageBytes -> UInt8 in
                if let messageBytesBaseAddress = messageBytes.baseAddress, let digestBytesBlindMemory = digestBytes.bindMemory(to: UInt8.self).baseAddress {
                    let messageLength = CC_LONG(self.count)
                    CC_MD5(messageBytesBaseAddress, messageLength, digestBytesBlindMemory)
                }
                return 0
            }
        }
        return hash
    }
}
Recipe 3-11

Calculating an MD5 Hash

The preceding extension allows us to calculate MD5 hashes of both String and Data objects. In order to calculate a String hash, it gets a UTF-8 representation of it.

Note

Even though it’s a function, and not the simplest one, we present it as a computed property because, first, it doesn’t change the original object and, second, it’s a property or characteristic of it.

If your app targets iOS 13 or later, you can use CryptoKit as shown in Recipe 3-12.
import CryptoKit
public extension Data {
    var md5: Data {
        let bytes = Insecure.MD5.hash(data: self).map { $0 as UInt8 }
        return Data(bytes: bytes, count: bytes.count)
    }
}
Recipe 3-12

Calculating MD5 Using CryptoKit (iOS 13 or Later)

These two implementations return identical results. The difference is that the first one works in older versions of iOS while the second one is much shorter and clearer. It’s recommended to migrate to a new algorithm as soon as you drop support of iOS 12 and earlier.

Note

This extension doesn’t generate a warning, but it has the word “Insecure” in it. It should give the developer an idea that the usage of this function must be limited.

SHA Hashes

As we discussed earlier, MD5 is not suitable for security purposes. It’s too fast and easy. A good solution for password storing and other security purposes is using hashes from the SHA group. SHA stands for Secure Hash Algorithm. There are many algorithms in this group, but the most common ones are SHA-1 and SHA-256.

SHA-1 produces a 160-bit hash. Known since 1995, it’s considered not to be safe enough, yet much safer than MD5.

SHA-256 is a variant of the SHA-2 algorithm. It produces a 256-bit hash. It provides a good balance between performance and security, and it is recommended for security purposes. Calculation of SHA-1, SHA-256 and SHA-512 from Data and String is shown in Recipe 3-13.
public extension String {
    var sha1: Data? {
        data(using: .utf8)?.sha1
    }
    var sha256: Data? {
        data(using: .utf8)?.sha256
    }
    var sha512: Data? {
        data(using: .utf8)?.sha512
    }
}
public extension Data {
    var sha1: Data {
        let bytes = Insecure.SHA1.hash(data: self).map { $0 as UInt8 }
        return Data(bytes: bytes, count: bytes.count)
    }
    var sha256: Data {
        let bytes = SHA256.hash(data: self).map { $0 as UInt8 }
        return Data(bytes: bytes, count: bytes.count)
    }
    var sha512: Data {
        let bytes = SHA512.hash(data: self).map { $0 as UInt8 }
        return Data(bytes: bytes, count: bytes.count)
    }
}
Recipe 3-13

Calculating SHA Hashes

As you can see, since SHA-256, the insecure prefix disappears. Similarly, you can implement other algorithms .

Hashable Protocol and Swift Hashes

Swift offers its own hashing functionality. Any class, struct, or other language construct conforming to the Hashable protocol can be hashed. This functionality is used for Dictionary and Set. Instead of storing keys, Swift stores hashes. The Hashable protocol has a function hash(into:):
func hash(into hasher: inout Hasher) {
    hasher.combine(property1)
    hasher.combine(property2)
    // ... other properties
}

You don’t usually need to implement this function. If all members of your class or struct are hashable, you only need to declare conformance to the Hashable protocol.

Hashable classes should be also equatable (conforming to the Equatable protocol). It means that they should declare a function to check the equality of the two instances.

Note

Swift offers two different operators: == and ===. The first one is equality, and this operator is required to conform to the Equatable protocol. The second one is the identity operator, and it compares memory addresses of two classes. If two equatable classes are identical, it means they’re also equal, but if they’re equal, it doesn’t guarantee that they’re identical.

The algorithm under the hood of Swift Hasher is not the same in different Swift versions. We don’t need to know which exact algorithm is used; for us, that’s an abstraction. Trying to use the specifics of class implementation will cause problems if new versions of Swift change implementation again.

What’s more, generated hashes should never be used outside an app, or even stored somewhere. They’re used for comparing keys in structures like Set or Dictionary only. To send a hash to a server, you need to use one of the standard algorithms, like SHA or MD5.

Salt

As we discussed password hashing earlier, it’s important to give a mention to salt. Not the widespread seasoning, but the string or two of code that an app adds to the beginning and/or end of your password before hash calculation. Salt must be the same and shouldn’t change in new versions of an app.

Salt helps to keep your password private. If two different services use SHA-256 to store passwords, the same password will generate the same SHA-256, which will create a vulnerability. The server administrator will be able to use your password hash to log in to another service. Using salt is pretty straightforward. You can see how to use it in Recipe 3-14.

Note

It’s not recommended to use the same password for two different services, but as a developer, you should always assume that users will ignore this recommendation.

let password = "..."
let leftSalt = "..." // String constant, which should be the same in all versions of your app
let rightSalt = "..." // String constant, which should be the same in all versions of your app
let saltedPassword = "(leftSalt)(password)(rightSalt)"
let passwordHash = saltedPassword.sha256
Recipe 3-14

Calculating the Hash of a Password

This is a safe way to send a password to the server. On its side, the server script can add another salt and calculate another password hash.

While developing a security system, be creative. Add unexpected layers of protection. Hackers know perfectly well everything described here. Do more. Use a combination of different methods to confuse them and to stay one step ahead .

Integer Indexing

Swift doesn’t accept integer numbers as String indexes. For example, you can’t just get the 5th Character of a String. Apple explains it with internal encoding. As characters may have different lengths, indexing them with Int, like in C, may create confusion and index not a 5th character, but a 5th byte or 5th pair of bytes.

Unlike Int, String.Index gives context. Belonging to a particular String, it knows which Character it refers to and knows how wide it is.

In real life, we often need to get n-th letter in word or range with known indices like in our Recipe 3-9 in Section “Hex encoding”, converting hex String to Data.

The String extensions we’ll write in this section will add functionality to get a Character or a Substring from a String using Int-based subscripts or ranges.

Before we move on, there’s one detail that needs some attention. Swift allows two different structures: String and Substring. While String actually stores text data, Substring is only a reference to an existing String.

Substring can be easily converted to String:
let substring: Substring = ...
let string: String = String(substring)

At the same time, if you actively work with substrings, you may want to use StringProtocol in your functions instead of String. It will allow you to pass both String and Substring. If you don’t modify it, there’s no difference; you won’t need to convert one into another. But that decision has a price as well when you turn a Substring into a String, Swift copies all the data into a new memory fragment. It may not be important if you do it once or twice with short strings, but if you need an efficient app, you need to keep this in mind.

Recipe 3-15 shows how to add integer indexing to Strings in your project, but first let’s decide what we want:
let str = "Some string"
let substring = str[5...] // "string" word wrapped into Substring object
let firstChar = substring[0] // Character "S"
public extension String {
    subscript (i: Int) -> Character {
        return self[index(startIndex, offsetBy: i)]
    }
    subscript (bounds: CountableRange<Int>) -> Substring {
        let start = index(startIndex, offsetBy: bounds.lowerBound)
        let end = index(startIndex, offsetBy: bounds.upperBound)
        if end < start { return "" }
        return self[start..<end]
    }
    subscript (bounds: CountableClosedRange<Int>) -> Substring {
        let start = index(startIndex, offsetBy: bounds.lowerBound)
        let end = index(startIndex, offsetBy: bounds.upperBound)
        if end < start { return "" }
        return self[start...end]
    }
    subscript (bounds: CountablePartialRangeFrom<Int>) -> Substring {
        let start = index(startIndex, offsetBy: bounds.lowerBound)
        let end = index(endIndex, offsetBy: -1)
        if end < start { return "" }
        return self[start...end]
    }
    subscript (bounds: PartialRangeThrough<Int>) -> Substring {
        let end = index(startIndex, offsetBy: bounds.upperBound)
        if end < startIndex { return "" }
        return self[startIndex...end]
    }
    subscript (bounds: PartialRangeUpTo<Int>) -> Substring {
        let end = index(startIndex, offsetBy: bounds.upperBound)
        if end < startIndex { return "" }
        return self[startIndex..<end]
    }
}
Recipe 3-15

Integer Indexing of Strings

Localization

iOS has its own localization system. It automatically chooses the preferred language based on the user’s settings and localizes both storyboards and strings in the source code if you provide a localization for them.

NSLocalizedString Macro

To get a localized version of a string in the source code, you should use a macro :
NSLocalizedString(key, comment)

It’s available in any part of an iOS app. A key argument is an identifier of a string. Identifiers should be unique, and they can contain any symbol allowed in a Swift String object. A comment is used for generating files for translators. It’s allowed to keep it empty if you’re not generating a translation file with Xcode or genstrings utility.

Files with Translations

You should add files with a strings extension – for example, Localised.strings to provide a translation. You can either put them manually in directories corresponding to languages or do it using Xcode.

A strings file is a simple text file with key-value pairs. Pairs are separated with a semicolon. Key and value are separated with an equal sign. Both keys and values should be inside quotation marks:
"firstName": "First Name";
To avoid confusion, we use more precise identifiers, for example:
"auth.firstName.title": "First Name";
"auth.firstName.placeholder": "Enter your first name here";

LocalizedStringKey Struct

Localization in SwiftUI is usually done with LocalizedStringKey. It’s a struct, but it also conforms to protocol ExpressibleByStringLiteral, which means that we can assign a String to it.
let name: LocalizedStringKey = "name"
let text = Text(name)

Text in this code will get a value from translation.

LocalizedStringKey also conforms to ExpressibleByStringInterpolation. You can use it with () construction.
let name = "Alex"
Text("hello (name)")
Translation file should contain
"hello %@" = "Hello %@!";

Text view will be created with string “Hello Alex!”.

Syntactic Sugar

When you have a lot of localizable content , it can be annoying to type NSLocalizedString every time. Wouldn’t it look much better with an extension from Recipe 3-16?
public extension StringProtocol {
    var localised: String {
        NSLocalizedString(self, "")
    }
}
Recipe 3-16

Localized Strings

Now you can use it this way:
label.text = "auth.firstName.title".localised
Note

UI elements from storyboards or nibs can be localized without writing code. Mark a storyboard or nib file as localizable in Xcode Interface Builder. Xcode will generate the necessary files automatically, you’ll only need to change texts.

Summary

Old programming languages like C didn’t have a special type for strings, programmers used array of bytes to store them. It created a lot of potential bugs in code and made string analysis nearly impossible. Swift with its String type and extensions offers completely different approach.

In this chapter we saw how to check if a String has letters or digits, it has contains valid name, email or other data. We used String for storing hashes, verifying data and encrypting passwords. Finally, we talked about localization of iOS apps.

Until now we were discussing data processing, but didn’t discuss presentation. In the next chapter we’ll fix it and talk about user interface.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.191.208.181