Avatar ·

Как проверить, содержит ли строка другую строку в Swift?

Всем привет! Я новичок в Swift и пишу своё первое приложение — простой поиск контактов. У меня есть массив строк с именами, и я хочу фильтровать его по введённому пользователем тексту в UISearchBar. Проблема в том, что я не могу понять, как правильно проверить, содержит ли строка какую-то подстроку. В документации нашёл метод `contains()`, но он, кажется, работает только для массивов, а не для строк. Или я путаю? Вот что я пробую: ```swift let searchText = "john" let name = "Johnny Appleseed" // Пытаюсь проверить, содержит ли name подстроку searchText if name.contains(searchText) { print("Найдено!") } ``` Но Xcode выдаёт ошибку: `Value of type 'String' has no member 'contains'`. Хотя в других языках (например, Python или JavaScript) я спокойно использовал `in` или `includes()`. Я читал, что в Swift есть `NSString` и можно использовать `range(of:)`, но это выглядит как-то громоздко. Может быть, есть более современный и "свифтовый" способ? И ещё: как сделать проверку регистронезависимой? Ведь "John" и "john" должны совпадать. Буду благодарен за примеры кода и объяснения! <<>>

Основное решение: метод `contains(_:)`

В Swift, начиная с версии 4.0, у типа String есть встроенный метод contains(_:). Ошибка, которую вы получили, скорее всего, связана с тем, что вы используете устаревшую версию Swift (до 4.0) или пытаетесь вызвать метод на NSString без явного приведения. Вот правильный синтаксис для современных версий Swift:

let name = "Johnny Appleseed"
let searchText = "john"

if name.contains(searchText) {
    print("Строка '\(name)' содержит '\(searchText)'")
} else {
    print("Строка '\(name)' НЕ содержит '\(searchText)'")
}
// Вывод: Строка 'Johnny Appleseed' НЕ содержит 'john' (из-за регистра)

Регистронезависимая проверка

Метод contains(_:) по умолчанию чувствителен к регистру. Чтобы сделать проверку регистронезависимой, приведите обе строки к одному регистру (например, к нижнему) с помощью lowercased():

if name.lowercased().contains(searchText.lowercased()) {
    print("Найдено! (регистронезависимо)")
} else {
    print("Не найдено")
}
// Вывод: Найдено! (регистронезависимо)

Альтернативный подход: метод `range(of:options:)`

Если вам нужен более тонкий контроль (например, поиск с учётом локали или поиск в начале/конце строки), используйте метод range(of:options:). Это особенно полезно для регистронезависимого поиска с учётом правил локали (например, буквы "ä" и "ae" в немецком):

let name = "Johnny Appleseed"
let searchText = "john"

if let range = name.range(of: searchText, options: .caseInsensitive) {
    print("Найдено в диапазоне: \(range)")
    // Вывод: Найдено в диапазоне: 0..<4
} else {
    print("Не найдено")
}

Обратите внимание: options: .caseInsensitive — это регистронезависимый поиск. Также можно комбинировать опции, например [.caseInsensitive, .diacriticInsensitive] для игнорирования диакритических знаков (é = e).

Получение индекса найденной подстроки

Если вам нужно не просто узнать, есть ли подстрока, но и получить её позицию (например, чтобы выделить текст жирным), используйте range(of:) и извлеките lowerBound:

if let range = name.range(of: searchText, options: .caseInsensitive) {
    let startIndex = range.lowerBound
    let endIndex = range.upperBound
    print("Подстрока с индекса \(startIndex) по \(endIndex)")
    // Можно получить саму подстроку:
    let foundString = name[range]
    print("Найденный фрагмент: \(foundString)") // "John"
}

Практический пример для поиска контактов

Вот как можно реализовать фильтрацию массива контактов с учётом регистра:

let contacts = ["Johnny Appleseed", "Jane Doe", "John Smith", "Alice Johnson"]
let searchText = "john"

let filteredContacts = contacts.filter { contact in
    contact.lowercased().contains(searchText.lowercased())
}
print(filteredContacts) // ["Johnny Appleseed", "John Smith", "Alice Johnson"]

Важные замечания

  • Производительность: Для больших массивов (10 000+ элементов) метод lowercased() создаёт новые строки, что может быть затратно. В таких случаях используйте range(of:options:) с опцией .caseInsensitive — это эффективнее.
  • Unicode: Swift отлично работает с Unicode. Строки типа "café" и "cafe\u{0301}" (e + комбинирующий акцент) будут считаться разными. Для поиска без учёта диакритики добавьте .diacriticInsensitive.
  • Пустая строка: Любая строка содержит пустую подстроку. Помните об этом при валидации ввода пользователя.

Рекомендую всегда использовать contains(_:) для простых проверок и range(of:options:) для более сложных сценариев. Это современный и идиоматичный Swift!

<<>>

Альтернативный подход с использованием `NSString` и `localizedStandardContains`

Если вы работаете с текстом, введённым пользователем, и хотите получить поведение, похожее на поиск в системных приложениях Apple (например, в Контактах или Почте), используйте метод localizedStandardContains. Этот метод автоматически выполняет регистронезависимый и диакритически нечувствительный поиск, а также учитывает текущую локаль пользователя:

import Foundation

let name = "Johnny Appleseed"
let searchText = "john"

if name.localizedStandardContains(searchText) {
    print("Найдено с учётом локали!")
} else {
    print("Не найдено")
}
// Вывод: Найдено с учётом локали!

Почему это удобно?

  • Не нужно вручную вызывать lowercased() или указывать опции.
  • Работает корректно с буквами разных алфавитов (например, "ß" = "ss" в немецком).
  • Этот метод используется в системном поиске iOS/macOS, поэтому пользователи ожидают именно такого поведения.

Пример с массивом контактов

let contacts = ["Johnny Appleseed", "José Garcia", "JOHN SMITH"]
let searchText = "jos"

let filtered = contacts.filter { $0.localizedStandardContains(searchText) }
print(filtered) // ["José Garcia", "JOHN SMITH"]

Обратите внимание: localizedStandardContains доступен только для строк (Swift String), не для NSString. Этот метод появился в Swift 3 и является рекомендуемым способом для пользовательского поиска.