Vor einigen Wochen begann ich mit der Entwicklung einer iOS-App, mit der ich Features von SwiftUI ausprobieren wollten. Seit der Version 2 des Framework gab es viel zum Spielen. MatchedGeometryEffect, DatePicker, AppStorage und DisclosureGroups waren nur einige der Dinge, die ich mir ansehen wollte. Dazu einige andere Themen, wie Farbverläufe und Styles, mit denen ich bisher nur wenig Erfahrung hatte. Mein Konzept sah vor, für jede dieser Technologien eine eigene View zu erstellen. Die Auswahl, welche View angezeigt würde, sollte aus einer übergeordneten Ansicht, und dort aus einem List-Steuerelement, geschehen. Eine Navigation zwischen den verschiedenen Ansichten wäre kein Problem. Benötigt würde lediglich ein NavigationView und für jedes Ziel je einen NavigationLink. In der ersten Version der App hatte ich für jede View einen NavigationLink mit einer Beschreibung zum Thema direkt im Code des List-Steuerelements hinterlegt. Der Aufbau der Liste war somit statisch, es war aber auch nichts anders erforderlich. Immer, wenn ich ein Beispiel zu einem Thema vollendet hatte, fügte ich der List einen weiteren NavigationLink hinzu. In den ersten Tagen hat das gut funktioniert, doch dann stieß ich auf eine grundlegende Einschränkung von SwiftUI: Ein Element kann maximal zehn Unterelemente haben. Zumindest dann, wenn diese Hierarchie direkt im Code hinterlegt ist. Dann gilt es auch für ein List-Steuerelement. Einen elften NavigationLink konnte ich nicht hinzufügen. Die Entwicklungsumgebung meldete einen wenig aussagekräftigen Fehler. Ich benötigt einige Minuten, um herauszufinden, was das Problem war. Eine mögliche Lösung ist einfach. Hat ein Objekt mehr als zehn Unterobjekte, können diese in Gruppen (Group) zusammengefasst werden. Möglich wären dann zehn Gruppen mit jeweils zehn Objekten, sofern man die Hierarchie nicht noch zusätzlich erweitern möchte. Eine Group funktioniert innerhalb eines List-Steuerelementes. Auf der grafischen Oberfläche ist nicht zu erkennen, dass die einzelnen Einträge aus unterschiedlichen Gruppen stammen. Trotzdem war ich mit dieser Lösung nicht zufrieden. Meine Idee war, ein Array zu erzeugen, in dem die anzuzeigenden View und der zugehörige Beschreibungstext hinterlegt werden. Das würde der App zusätzlich die Möglichkeiten geben, die Themen alphabetisch sortiert anzuzeigen und nach einer Beschreibung zu suchen. Die Beschreibung war vom Typ String. Um eine beliebigen View zu referenzieren, kam der Typ Any zum Einsatz. Beide Variablen, zusammen mit einer ID, wurden in der Struktur SubViewData zusammengefasst.
struct SubViewData : Identifiable {
var view : Any
var viewTitle : String = ""
var id = UUID()
}
Das Array, mit Elementen von diesem Typ, wurde als Eigenschaft der Klasse SubViewRepository implementiert. Die Klasse verwendet ObservableObject und das Array wird mit dem Property Wrapper @Published ausgezeichnet. Durch diese beiden Erweiterungen entsteht die Möglichkeit, das Array zu beobachten und auf Änderungen automatisch zu reagieren. Somit würde das List-Steuerelement selbstständig aktualisiert, sollten dem Array weitere Elemente hinzugefügt werden. Zu Laufzeit dieser App kann das zwar nicht passieren sein, es ist trotzdem nie verkehrt, die Vorgehensweise zu üben.
class SubViewRepository : ObservableObject {
private var list = [SubViewData]()
init() {
var data = SubViewData(view: HapticView(), viewTitle: "Haptic View")
list.append(data)
data = SubViewData(view: AppStorageView(), viewTitle: "@AppStorage")
list.append(data)
data = SubViewData(view: AnimatedGridView(), viewTitle: "Animated Grid")
list.append(data)
}
}
Ebenfalls in der Klasse SubViewRepository umgesetzt wurde eine Filter-Methode. Ihr fällt die Aufgabe zu, ein gefiltertes und sortiertes Array zurückzugeben. Als Parameter erwartet die Methode einen Suchtext (searchQuery), der mit den Beschreibungen (viewTitle) der SubViewData-Strukturen verglichen wird, um die Elemente zu filtern. Die Groß- und Kleinschreibungen werden für den Vergleich nicht berücksichtig. Die Zeichenketten werden zuvor mit lowercased in Kleinbuchstaben umgewandelt. Ist der Suchtext leer, liefert die Methode sämtliche Elemente des SubViewData-Array zurück.
func Filter(searchQuery:String) -> [SubViewData]
{
var filteredItems = [SubViewData]()
if searchQuery.isEmpty {
filteredItems = self.list.sorted(by: { $0.viewTitle < $1.viewTitle})
} else {
for singleItem in self.list.sorted(by: { $0.viewTitle < $1.viewTitle}) {
if singleItem.viewTitle.lowercased().contains(searchQuery.lowercased())
{
filteredItems.append(singleItem)
}
}
}
return filteredItems
}
Als letzte Hürde muss aus der View, die in der SubViewData-Struktur als Typ Any referenziert werden, wieder eine View gemacht werden. Ein NavigationLink benötigt eine Ansicht, zu der er navigieren kann, mit einem beliebigen Objekt gibt er sich nicht zufrieden. Die Lösung des Problems liefert die Funktion AnyView(_fromValue), die den Typ Any wieder in eine View umwandelt. Die Beschreibung zum Thema wird innerhalb des NavigationLink als Text angezeigt.
List {
ForEach(repo.Filter(searchQuery: searchQuery), id:\.self.id) { item in
NavigationLink(destination: AnyView(_fromValue: item.view))
{
Text(item.viewTitle)
.font(.body)
.foregroundColor(Color.primary)
}
}
}
Geschrieben am: 05.01.2021 Technologien: SwiftUI, iOS