Neben refreshable ist searchable ein weiterer Modifier, der von Apple in iOS 15 für die List hinzugefügt wurde. Ermöglicht wird mit diesem Modifier eine Suche einschließlich einem Eingabefeld, das oberhalb der Liste angezeigt wird. Bisher mussten Entwickler für diese Funktionalität noch auf Klasse des UIKit-Framework zurückgreifen. Jetzt geht es nativ mit SwiftUI. Benötigt wird lediglich ein String, der als Suchbegriff an das Eingabefeld gebunden wird. Die Position und der Platzhaltertext sind optional. Leider kann die Farbe des Suchfelds nicht angepasst werden. Für einen guten Kontrast ist es daher wichtig, zuvor eine passende Farbe für die Navigationsleiste zu wählen.
.searchable(
text: $searchText,
placement: .navigationBarDrawer(displayMode: .always),
prompt: "Search something...")
Gesucht oder gefiltert werden die Personen nach dieser Ergänzung in der View noch nicht. Aber es gibt jetzt einen Wert, auf dessen Änderungen reagiert werden muss. Es ist die Zeichenkette searchText. Welchen Wert diese Variable enthält, kann man sich anzeigen lassen, indem der Code um einen onChange-Modifier erweitert wird.
.searchable(
text: $searchText,
placement: .navigationBarDrawer(displayMode: .always),
prompt: "Search something...")
.onChange(of: searchText) { searchText in
print(searchText)
}
Hier der vollständige ContentView:
import SwiftUI
struct ContentView: View {
@State private var searchText = ""
@ObservedObject var repo = PersonsRepository(randomPersonsCount: 10)
init() {
let customAppearance = UINavigationBarAppearance()
// Backgroundcolor
customAppearance.backgroundColor = UIColor(red: 0.9, green: 0.9, blue: 1.0, alpha: 1)
// Font color for navigationBarTitleDisplayMode large
customAppearance.largeTitleTextAttributes = [.foregroundColor: UIColor.systemBlue]
// Font color for navigationBarTitleDisplayMode inline
customAppearance.titleTextAttributes = [.foregroundColor: UIColor.systemBlue]
UINavigationBar.appearance().standardAppearance = customAppearance
UINavigationBar.appearance().compactAppearance = customAppearance
UINavigationBar.appearance().scrollEdgeAppearance = customAppearance
}
var body: some View {
NavigationView {
List {
ForEach (self.repo.getSections(), id:\.self ) { section in
Section(header: SectionHeader(headlineText: section)) {
ForEach(self.repo.persons.filter {$0.company == section} , id: \.id) { person in
NavigationLink (destination: PersonDetailView(person: person))
{
PersonListCell(person: person)
.badge(Text(person.getAge())
.foregroundColor(Color.blue)
.font(.subheadline )
)
}
.swipeActions(edge: .leading , allowsFullSwipe: true) {
Button {
print("Sent Button")
} label: {
Label("Send", systemImage: "paperplane.fill")
}
.tint(.indigo)
Button {
print("Bookmark Button")
} label: {
Label("Bookmark", systemImage: "bookmark.circle")
}
.tint(.teal)
}
.swipeActions(edge: .trailing , allowsFullSwipe: true) {
Button {
withAnimation {
self.repo.delete(person: person)
}
} label: {
Label("Delete", systemImage: "trash.fill")
}
.tint(.red)
}
}
}
}
.listRowSeparatorTint( Color(red: 0.2, green: 0, blue: 1.0, opacity: 0.5) )
}.listStyle(PlainListStyle())
.refreshable {
await self.repo.addRandomPersonAsync()
}
.searchable(
text: $searchText,
placement: .navigationBarDrawer(displayMode: .always),
prompt: "Search something...")
.onChange(of: searchText) { searchText in
print(searchText)
}
.navigationTitle("List and People")
.navigationBarTitleDisplayMode(.large)
.navigationBarItems(trailing:
Button(action: {
self.repo.addRandomPerson()
}) {
Image(systemName: "plus.circle.fill").tint(Color(UIColor.systemBlue))
} )
}
}
}
Ob eine Person mit dem Suchtext übereinstimmt, kann am besten mit einer Methode überprüft werden, die direkt in der Klasse Person implementiert wird. Verglichen werden soll der Vornamen, der Nachname und die Firma, bei der die Person angestellt ist. Die Groß- und Kleinschreibung soll dabei keine Rolle spielen. Wird in einer Eigenschaft der Suchtext gefunden, gibt die Methode true zurück, ansonsten false. Ist der Suchtext leer, soll der Vergleich ebenfalls gültig sein, weil dann keine Einschränkungen angegeben wurden. Das ist auch beim Start der App der Fall. Ohne diese Bedingung wäre die View dann leer.
public func matchesFilter(searchText : String) -> Bool {
if searchText.isEmpty ||
self.firstName.range(of: searchText, options: .caseInsensitive) != nil ||
self.lastName.range(of: searchText, options: .caseInsensitive) != nil ||
self.company.range(of: searchText, options: .caseInsensitive) != nil {
return true
}
return false
}
Nachdem alle Vorbereitungen abgeschlossen sind, muss die View angepasst werden. Die angezeigten Personen sind jetzt von dem Suchtext abhängig. Die erste Änderung betrifft die Sektionen. Der getSections-Methode in der Klasse PersonsRepository muss der Suchtext übergeben werden, damit sie nur noch die Überschriften für Personen zurückgibt, die dem Filterkriterium entsprechen.
func getSections(searchText : String) -> [String] {
var sections = [String]()
for person in self.persons {
if person.matchesFilter(searchText: searchText) {
if !sections.contains(person.company) {
sections.append(person.company)
}
}
}
return sections.sorted()
}
Die letzte Änderung kann direkt im ContentView vorgenommen werden. Der inneren For-Schleife, welche die Personen zu einer Sektion liefert, muss eine weitere Filterbedingung hinzugefügt werden. Die Person muss nicht nur zur Sektion passen, auch der Suchtext muss übereinstimmen. Weil die getSections-Methode ebenfalls die matchesFilter-methode der Klasse Person verwendet, gibt es in der View keine Person ohne eine Sektion und auch keine Sektion, die keine Person enthält.
ForEach(self.repo.persons.filter {$0.company == section && $0.matchesFilter(searchText: self.searchText) } , id: \.id)
Hier der vollständige ContentView:
import SwiftUI
struct ContentView: View {
@State private var searchText = ""
@ObservedObject var repo = PersonsRepository(randomPersonsCount: 10)
init() {
let customAppearance = UINavigationBarAppearance()
// Backgroundcolor
customAppearance.backgroundColor = UIColor(red: 0.9, green: 0.9, blue: 1.0, alpha: 1)
// Font color for navigationBarTitleDisplayMode large
customAppearance.largeTitleTextAttributes = [.foregroundColor: UIColor.systemBlue]
// Font color for navigationBarTitleDisplayMode inline
customAppearance.titleTextAttributes = [.foregroundColor: UIColor.systemBlue]
UINavigationBar.appearance().standardAppearance = customAppearance
UINavigationBar.appearance().compactAppearance = customAppearance
UINavigationBar.appearance().scrollEdgeAppearance = customAppearance
}
var body: some View {
NavigationView {
List {
ForEach (self.repo.getSections(searchText: self.searchText) , id:\.self ) { section in
Section(header: SectionHeader(headlineText: section)) {
ForEach(self.repo.persons.filter {$0.company == section && $0.matchesFilter(searchText: self.searchText) } , id: \.id) { person in
NavigationLink (destination: PersonDetailView(person: person))
{
PersonListCell(person: person)
.badge(Text(person.getAge())
.foregroundColor(Color.blue)
.font(.subheadline )
)
}
.swipeActions(edge: .leading , allowsFullSwipe: true) {
Button {
print("Sent Button")
} label: {
Label("Send", systemImage: "paperplane.fill")
}
.tint(.indigo)
Button {
print("Bookmark Button")
} label: {
Label("Bookmark", systemImage: "bookmark.circle")
}
.tint(.teal)
}
.swipeActions(edge: .trailing , allowsFullSwipe: true) {
Button {
withAnimation {
self.repo.delete(person: person)
}
} label: {
Label("Delete", systemImage: "trash.fill")
}
.tint(.red)
}
}
}
}
.listRowSeparatorTint( Color(red: 0.2, green: 0, blue: 1.0, opacity: 0.5) )
}.listStyle(PlainListStyle())
.refreshable {
await self.repo.addRandomPersonAsync()
}
.searchable(
text: $searchText,
placement: .navigationBarDrawer(displayMode: .always),
prompt: "Search something...")
.onChange(of: searchText) { searchText in
print(searchText)
}
.navigationTitle("List and People")
.navigationBarTitleDisplayMode(.large)
.navigationBarItems(trailing:
Button(action: {
self.repo.addRandomPerson()
}) {
Image(systemName: "plus.circle.fill").tint(Color(UIColor.systemBlue))
} )
}
}
}
Geschrieben am: 29.08.2021 Technologien: SwiftUI, List