Hallo! Heute werden wir gemeinsam von Grund auf ein wunderschön animiertes, UX-orientiertes Seitenmenü erstellen, es Schritt für Schritt aufbauen und Ihnen die besten bewährten Vorgehensweisen mit Ihnen teilen.
Wir werden einige Fragen beantworten, beginnend mit den Grundlagen bis hin zu verschiedenen Tipps und Tricks, die Sie für Ihr Produkt benötigen könnten, wobei wir die Codequalität berücksichtigen und dem SwiftUI-API-Stil folgen.
Das alles werden wir in dieser Artikelserie besprechen, indem wir die Komplexität der behandelten Themen schrittweise erhöhen:
So sieht das Ergebnis aus:
Bevor wir anfangen, sollten Sie das Anfangsprojekt aus unserem GitHub-Repository herunterladen, um den Code selbst zu analysieren. Er basiert auf dem Swift Package Manager und Voraussetzung ist, dass Sie grundlegende Erfahrungen damit haben. Sobald Sie das Paket heruntergeladen haben, öffnen Sie das "Example"-Projekt und wir können es loslegen.
Von Zeit zu Zeit müssen wir ein Menü oder einen anderen ergänzenden/navigatorischen Inhalt anzeigen, der vom vorderen/letzten Rand aus sichtbar sein soll. Und da wir das nicht immer wiederholen wollen, sollte unser Drawer so generisch sein, dass er an jeder beliebigen Stelle eingesetzt werden kann. Beginnen wir also damit, eine Drawer.swift
zu öffnen und sofort mit dem Code zu beginnen:
public struct Drawer<Menu: View, Content: View>: View {
@Binding private var isOpened: Bool
private let menu: Menu
private let content: Content
// MARK: - Init
public init(
isOpened: Binding<Bool>,
@ViewBuilder menu: () -> Menu,
@ViewBuilder content: () -> Content
) {
_isOpened = isOpened
self.menu = menu()
self.content = content()
}
// MARK: - Body
public var body: some View {
ZStack(alignment: .leading) {
content
if isOpened {
Color.clear
.onTapGesture {
if isOpened {
isOpened.toggle()
}
}
menu
.transition(.move(edge: .leading))
}
}
.animation(.spring(), value: isOpened)
}
}
Wir sind fast fertig mit den Grundlagen. Es ist erstaunlich, wie einfach und prägnant der Code dank SwiftUI ist. Sie können den folgenden Code entweder zu einer Vorschau oder zu einer ContentView.swift
in der Beispiel-App hinzufügen:
struct ContentView: View {
@State private var isOpened = true
var body: some View {
Drawer(
isOpened: $isOpened,
menu: {
ZStack {
Color.gray
Text("Menu")
}
.frame(width: 200)
},
content: {
ZStack {
Color.white
Button {
isOpened.toggle()
} label: {
Text("Open")
.foregroundColor(Color.secondary)
}
}
}
)
}
}
Nach der Ausführung werden Sie schnell feststellen, dass die App nicht auf Berührungen außerhalb eines Menüs reagiert und dass das Menü selbst verschwindet, wenn Sie auf eine Schaltfläche "Öffnen" klicken.
Zunächst einmal wollen wir ein ziemlich einfaches Problem beheben und verstehen: Color.clear
ist für Berührungen unsichtbar, was in gewisser Weisezu erwarten ist. Um dies zu lösen, gibt es Lösungen Work-arounds, wie zum Beispiel die Platzierung einer soliden Farbe mit 0.01 Opazität, aber wir werden einen dafür vorgesehenen View-Modifier verwenden - contentShape - der den tippbaren Bereich für ein View-Hit-Testing festlegt.
Lassen Sie uns auch das Problem mit dem versehentlichen Verschwinden beim Schließen des Menüs genauer untersuchen:
ZStack(alignment: .leading) {
content
if isOpened {
...
menu
.transition(.move(edge: .leading))
}
}
Die Sache ist die, dass ZStack die Unteransichten nach einer zIndex-Reihenfolge anordnet. Standardmäßig wird sie durch die Reihenfolge der deklarierten Ansichten abgeleitet. Sobald wir unser Menü aus dem ZStack
entfernen, gibt es keinen Grund, es an der Spitze zu platzieren, auch wenn es als das zuletzt deklarierte angegeben ist. Aber wir können erzwingen, dass die Ansicht selbst bei der Entfernung als oberste angeordnet wird, indem wir den zIndex-View-Modifier verwenden.
Hier ist unser aktualisierter Drawer
's Body:
public var body: some View {
ZStack(alignment: .leading) {
content
if isOpened {
Color.clear
.contentShape(Rectangle())
.onTapGesture {
if isOpened {
isOpened.toggle()
}
}
menu
.transition(.move(edge: .leading))
.zIndex(1)
}
}
.animation(.spring(), value: isOpened)
}
Jetzt, nach dem Erstellen und Starten, erhalten wir genau das, was wir erreichen wollten:
Okay, wir haben bisher wirklich grundlegende Interaktionen durchgeführt. Das ist jedoch offensichtlich nicht der Stand der Technik einer wiederverwertbaren Komponente. Um generisch ausreichend zu sein, müssen wir eine Kommunikation mit Geschwisterkomponenten etablieren, um Informationen darüber zu teilen, ob sie präsentiert werden oder nicht und - was genauso wichtig ist - um sie sich selbst entfernen zu lassen.
In der UIKit-Welt könnten Sie die übergeordnete Hierarchie durchlaufen, bis Sie den gewünschten View-Controller gefunden haben - so wie sich navigationController verhält.
In der SwiftUI-Welt ist dieses Muster nicht mehr anwendbar. Um dieses Problem zu lösen, verwendet Apple einen PresentationMode, der es dem präsentierten Kind erlaubt, sich selbst zu deaktivieren, unabhängig davon, wie tief es sich in der Hierarchie befindet - dieser wird von einem leistungsstarken System für Umgebungswerte verwaltet.
Leider können wir unsere eigene Instanz von PresentationMode aufgrund eines versteckten Initialisierers nicht instanziieren, aber wir sollten auf jeden Fall nachahmen, was Apple getan hat.
Es ist generell ein großartiger Ansatz, auf Augenhöhe mit einer eingebauten API zu bleiben, da andere Entwickler automatisch all ihr vorheriges Wissen und ihre Erfahrung darüber, wie ähnliche APIs funktionieren, einbringen können. Es versteht sich von selbst, wie ein solcher Ansatz das lokale Verständnis verbessert.
Das Ziel ist es, den folgenden Code innerhalb unseres Menüs oder aller nachfolgenden Geschwister weiter unten in der Hierarchie zuzulassen:
struct SiblingView: View {
@Environment(\.drawerPresentationMode) var presentationMode
var body: some View {
Button("Dismiss") {
presentationMode.wrappedValue.close()
}
}
}
Unser Präsentationsmodus wird ähnlich aussehen, aber etwas anders sein, um dem folgenden Bedarf gerecht zu werden: die Möglichkeit, das isOpened
-Flag von Drawer zu verstehen/kontrollieren. Lassen Sie uns einen Entwurf für eine API erstellen, die wir benötigen:
struct DrawerPresentationMode {
var isOpened: Bool
mutating func open()
mutating func close()
}
Das sieht ziemlich ähnlich aus wie das, was der Standard PresentationMode bietet, aber wie synchronisieren wir dies mit unserer Quelle der Wahrheit isOpened
, die vom Drawer
selbst verwaltet wird? Es gibt eine mächtige Fähigkeit in Binding selbst: die Fähigkeit, mit wenig bis gar keinem Aufwand von einem Typ auf einen anderen zu mappen.
Um dies zu implementieren, stellen wir fest, dass DrawerPresentationMode
auf einem einfachen Binding<Bool>
aufbaut, aber eine etwas ausgefeiltere API bietet:
public struct DrawerPresentationMode {
@Binding private var _isOpened: Bool
init(isOpened: Binding<Bool>) {
__isOpened = isOpened
}
public var isOpened: Bool {
_isOpened
}
public mutating func open() {
if !_isOpened {
_isOpened = true
}
}
public mutating func close() {
if _isOpened {
_isOpened = false
}
}
}
Jetzt, nachdem die Implementierung abgeschlossen ist, fügen wir das Mapping von Binding<Bool>
zu unserem DrawerPresentationMode
hinzu:
extension Binding where Value == Bool {
func mappedToDrawerPresentationMode() -> Binding<DrawerPresentationMode> {
Binding<DrawerPresentationMode>(
get: {
DrawerPresentationMode(isOpened: self)
},
set: { newValue in
self.wrappedValue = newValue.isOpened
}
)
}
}
Nachdem Sie sich mit der Implementierung von PresentationMode vertraut gemacht haben, können Sie nahezu die gleiche API wie Apple verwenden - genau das ist es, was wir anstreben - um all das übernommene Wissen für die Apple-Toolchain wiederzuverwenden.
Das Weitergeben dieses Bindings an die Geschwister des Drawers ist unkompliziert durch die Verwendung von EnvironmentValues:
extension DrawerPresentationMode {
static var placeholder: DrawerPresentationMode {
DrawerPresentationMode(isOpened: .constant(false))
}
}
private struct DrawerPresentationModeKey: EnvironmentKey {
static var defaultValue: Binding<DrawerPresentationMode> = .constant(.placeholder)
}
extension EnvironmentValues {
public var drawerPresentationMode: Binding<DrawerPresentationMode> {
get { self[DrawerPresentationModeKey.self] }
set { self[DrawerPresentationModeKey.self] = newValue }
}
}
Was bleibt, ist, diese Daten in den Body des Drawers weiterzugeben:
public var body: some View {
ZStack(alignment: .leading) {
content
...
}
}
.animation(.spring(), value: isOpened)
.environment(\.drawerPresentationMode, $isOpened.mappedToDrawerPresentationMode())
}
Jetzt haben wir alles, was wir brauchen, um endlich ein Menü zu implementieren.
Ein aufmerksamer Mensch könnte feststellen, dass es keinen Grund gibt, einen Wrapper um
Binding<Bool>
zu machen und das ist auch völlig richtig. Für unseren Zweck ist es ausreichend, Binding direkt zu übergeben. Dennoch lohnt es sich zu verstehen, wie die Dinge im Hintergrund ablaufen können.
Sie können die endgültige Version dieses Abschnitts auch von unserem Repo herunterladen.
Jetzt ist es an der Zeit, mit der Implementierung des Menüs zu beginnen. Lassen Sie uns noch einmal zusammenfassen, was zu tun ist, und das Ganze in eine Reihe von Funktionen unterteilen:
Bisher werden wir Folgendes Schritt für Schritt implementieren:
MenuItem
's mit Überschrift, mit Unterstützung des erweiterten und kompakten Zustands vor Ort.Als Ausgangspunkt können Sie den folgenden Commit verwenden oder dort weitermachen, wo wir im vorherigen Abschnitt aufgehört haben.
Bevor wir in die Benutzeroberfläche eintauchen, ist es sinnvoll zu klären, wie wir verschiedene Zustände verwalten können. Lassen Sie uns ein MenuAppearance
mit folgendem Inhalt deklarieren:
enum MenuAppearance: String {
case `default`, compact
mutating func toggle() {
switch self {
case .default:
self = .compact
case .compact:
self = .default
}
}
}
private struct MenuAppearanceEnvironmentKey: EnvironmentKey {
static var defaultValue: MenuAppearance = .default
}
extension EnvironmentValues {
var menuAppearance: MenuAppearance {
get {
self[MenuAppearanceEnvironmentKey.self]
}
set {
self[MenuAppearanceEnvironmentKey.self] = newValue
}
}
}
extension View {
func menuAppearance(_ appearance: MenuAppearance) -> some View {
environment(\.menuAppearance, appearance)
}
}
Da wir nur zwei Zustände zu berücksichtigen haben, passt enum
perfekt zu unseren Bedürfnissen. Der Hilfscode nach der enum-Deklaration ermöglicht es uns, den aktuellen Menüzustand genauso weiter unten in der Hierarchie zu übergeben, wie wir es zuvor mit der Drawer.isOpened
-Flag gemacht haben.
Das Coole daran ist, dass das Kind aufgrund des SwiftUI Layout-Modells auf ein gegebenes Menü-Erscheinungsbild vertrauen kann, um sein Layout anzupassen. Dies hat eine entscheidende Auswirkung auf die Vereinfachung des Codes: Grundsätzlich gibt es keine Präsentationstaste, um verschiedene Layoutgrößen zu verwalten - SwiftUI führt die richtige Größenänderung für uns durch.
Jetzt gehen wir zu den grundlegenden Bausteinen über. Wie oben erwähnt, bemühen wir uns, unseren Code so nah wie möglich an der eingebauten SwiftUI-API zu halten, nicht nur in Bezug auf Namensgebung und Semantik, sondern auch, um bestehende Komponenten und Konzepte zu nutzen.
Um ein besseres Verständnis davon zu bekommen, wovon ich spreche, schauen Sie hier und versuchen Sie, gemeinsame Ansichten zu finden:
Es ist offensichtlich, dass wir eine Art HStack mit konstantem Abstand und einem ähnlichen Verhalten beim Zusammenklappen des Menüs aufrecht erhalten müssen, um nur ein Icon anzuzeigen.
In der UIKit-Welt wäre das keine Katastrophe, sondern eine Aufgabe, über die man nachdenken muss. Aber das ist nicht der Fall, wenn es um SwiftUI geht. Auf den ersten Blick handelt es sich dabei um zwei separate Arten von Ansichten: Benutzerkopfzeile und eine Zelle, die ausgewählt werden können. In SwiftUI können jedoch beide durch eine einzelne Ansicht namens Label und ihren begleitenden LabelStyle beschrieben werden, der beschreibt, wie das Icon und der Titel präsentiert werden sollen.
Dies ist eine sehr leistugsvolle Idee, da in den meisten iOS-Apps viele Abstraktionen vorhanden sind, die perfekt als Paar aus Titel und Icon beschrieben werden können. SwiftUI hebt diese Idee auf die nächste Ebene, indem es uns ermöglicht, jeden Erscheinungsbild-Modifikator über den Stil zur Verfügung zu stellen.
Und da sowohl Icon als auch Titel generische Typen sind, ist es nicht überraschend, dass VStack aus Benutzerkopfzeile und einem Menüpunkt-Titel aus der Zelle beide als Titel dargestellt werden. Mit diesem Wissen im Hinterkopf können wir dieses Konzept sogar noch weiter in unseren Anwendungen skalieren, indem wir das Layout hier und da vereinfachen und wiederverwenden. In was für einer Welt wir nur leben! :)
Genug geredet, implementieren wir nun unseren MenuLabelStyle
:
struct MenuLabelStyle: LabelStyle {
@Environment(\.menuAppearance) var appearance
func makeBody(configuration: Configuration) -> some View {
HStack(spacing: 24) {
configuration.icon
.frame(width: 48, height: 48, alignment: .center)
if appearance == .default {
configuration.title
.typographyStyle(.title)
.transition(.move(edge: .trailing).combined(with: .opacity))
}
}
}
}
extension LabelStyle where Self == MenuLabelStyle {
static var menuLabel: Self {
MenuLabelStyle()
}
}
typographyStyle
ist nur ein praktischer Wrapper, um ein einheitliches Gestaltungssystem zu erreichen. Er kann in unserem Beispielcode gefunden werden.
Wie Sie sehen können, haben wir hier nur ein total einfaches Layout, das auf das Erscheinungsbild des Menüs reagiert, indem es den Titel der gegebenen Konfiguration ausblendet (siehe LabelStyleConfiguration für mehr Informationen).
Der einzige bemerkenswerte Modifikator hier ist die Übergang eines Titels, der unser Label ein-/ausblendet. Dies ist ein Teil unserer Animation. Sie können damit herumspielen, um die endgültigen Effekte zu sehen.
Wenn wir mit dem Label-Layout fertig sind, wie wäre es dann mit den Zellen? Das hier sind keine Zellen, sondern ein VStack mit Schaltflächen für jedes MenuItem, das wir haben. Und genau wie bei LabelStyle
bietet SwiftUI uns die Möglichkeit, das Aussehen einer Schaltfläche durch einen benutzerdefinierten ButtonStyle zu ändern. Dies ist ein gängiger Ansatz, bei dem die Zusammensetzung ihre Vorteile gegenüber der Vererbung ausspielt. Wir können buchstäblich jeden Stil in jeder beliebigen Kombination erweitern und zusammenstellen, und das ist umwerfend.
Hier und da finden Sie Implementierungen eines benutzerdefinierten Buttons mit Gesture Recognizers, der das Hervorheben und Auswählen handhabt, aber das ist eindeutig nicht der beabsichtigte Ansatz und ein eigentlich ein Work-around.
ButtonStyle
hingegen übergibt Ihnen nicht nur die Ansicht, die Sie anzeigen müssen, sondern gibt auch an, ob die Ansicht gedrückt ist oder nicht. Button hat gleichzeitig die Fähigkeit, das Hervorhebungsverhalten richtig zu verwalten, indem es ein iOS-weites Verhalten bereitstellt - versuchen Sie, Ihren Finger auf eine beliebige Systemschaltfläche zu drücken und zu bewegen, um zu sehen, was ich meine.
Um es kurz zu machen: ButtonStyle
sagt uns noch einmal, wie SwiftUI uns dazu bringen möchte, Ansichten zu erweitern und zu gestalten.
Lassen Sie mir die Implementierung eines MenuButtonStyle
zeigen:
struct MenuItemButtonStyle: ButtonStyle {
var isSelected: Bool = false
func makeBody(configuration: Configuration) -> some View {
configuration
.label
.labelStyle(.menuLabel)
.foregroundColor(configuration.isPressed || isSelected ? Color("color/selectionForeground") : Color("color/text"))
.animation(.spring(), value: isSelected != configuration.isPressed)
.frame(maxWidth: .infinity, minHeight: .contentHeight, alignment: .leading)
.contentShape(Rectangle())
}
}
Schauen Sie sich genauer an, was wir hier haben. Es sieht nicht so komplex aus, aber einige Zeilen benötigen eine Erklärung:
- Die animation
für isSelected != configuration.isPressed
gibt Ihrem benutzerdefinierten Button ein Systemaussehen und -gefühl. Wenn der Benutzer also ein Touch-Down-Event macht, erhalten Sie bei jedem visuellen Feedback eine schöne Animation. Denken Sie daran, dass visuelles Feedback wichtig ist, da der Benutzer keine Ahnung hat, was für ihn kommt.
- frame
- Dieser Modifikator ist etwas knifflig. Er lässt den Button auf jede Breite erweiterbar sein, sonst würde er schrumpfen, um seinen Inhalt perfekt zu umarmen. Später verwenden wir ihn innerhalb von VStack
, um vollbreite Tasten zu erhalten, obwohl ihre tatsächliche Inhaltsgröße viel kleiner sein könnte. Spielen Sie mit diesem Modifikator herum, um zu sehen, was passieren wird.
Frames zu verwalten ist ein weites Feld und wir empfehlen dringend ein Pro SwiftUI, um praktisches Wissen über das SwiftUI-Layout-System zu erwerben.
- contentShape
- gibt eine Ansicht zurück, die die gegebene Form für das Hit-Testing verwendet - Die Apple-Dokumentation sagt deutlich, was das ist. Um jedoch zu klären, was diese Funktion hier genau macht, müssen wir zum Frame-Modifikator oben zurückkehren. Denken Sie daran, der Button wird sich selbst schrumpfen, um seinen Inhalt perfekt zu umarmen. Daher hat er keinen Grund, Berührungen anderswo zu behandeln, als in seinem Icon und Titel. Aber unser Button sollte interaktiv innerhalb der vollen Breite des VStack sein und durch Anwenden dieses Modifikators zwingen wir dieses Verhalten explizit.
- isSelected
- und nicht zuletzt. ButtonStyle
und Button
behandeln den ausgewählten Status nicht und überlassen es Ihnen, wie Sie dieses Flag speichern und wie Sie es dem Benutzer präsentieren. Unsere Liste sollte das ausgewählte MenuItem
behalten, also übergeben wir dieses Flag explizit an unseren benutzerdefinierten Stil.
struct MenuItemList: View {
...
// MARK: - Body
var body: some View {
VStack(alignment: .leading) {
UserHeader(user: user)
.onTapGesture {
onUserSelection?()
}
Spacer()
.frame(height: .headerBottomSpacing)
itemsList
}
.animation(.spring(), value: selection)
}
@ViewBuilder
private var itemsList: some View {
VStack(alignment: .leading) {
ForEach(items) { item in
Button(
action: {
if selection == item {
selection = nil
} else {
selection = item
}
},
label: {
Label(item: item)
}
)
.buttonStyle(MenuItemButtonStyle(isSelected: item == selection))
}
}
}
}
Einige der Eigenschaften und Hilfscodes werden übersprungen. Es wird empfohlen, das GitHub-Repo zu durchsuchen, um den vollständigen Code zu erhalten.
Jetzt ist es an der Zeit, unseren ContentView
zu aktualisieren und unseren Code zu erstellen und auszuführen, um zu sehen, was wir haben:
struct ContentView: View {
@State var selection: MenuItem? = .dashboard
@State var appearance: MenuAppearance = .default
var body: some View {
MenuItemList(selection: $selection) {
withAnimation(.spring()) {
appearance.toggle()
}
}
.fixedSize(horizontal: true, vertical: false)
.menuAppearance(appearance)
}
}
Hier sind unsere Ergebnisse nach den endgültigen Änderungen an unserem ContentView:
Und das mit nur wenigen Ergänzungen, um alles zusammenzusetzen:
Während die endgültige Version des obigen Codes mit Hintergrund usw. hier gefunden werden kann, ist es an der Zeit, zum unterhaltsamen Teil 2 - Auswahl und benutzerdefinierte Übergänge - überzugehen.
12.05.2023
Revolutionierung der mobilen Entwicklung: Wie ChatGPT das Nutzererlebnis verbessertIn diesem Artikel werden wir das Thema der Einflussnahme der ChatGPT-Technologie auf das Benutzererlebnis behandeln und aufzeigen, wie sie genutzt werden kann, um dieses zu verbessern und neue Möglichkeiten für die mobile App-Entwicklung zu eröffnen.Weiterlesen17.01.2023
DER ULTIMATIVE LEITFADEN ZUR ERSTELLUNG EINES SEITENMENÜS IN SWIFTUI: TEIL 2. BENUTZERDEFINIERTE ÜBERGÄNGE UND ANIMATIONENHallo zusammen! In Teil 1 dieser Artikelserie haben wir uns damit beschäftigt, wie Sie mit Label, LabelStyle, ButtonStyle und anderen Ihre eigenen Komponenten im SwiftUI-Design erstellen. Außerdem konnten Sie erleben, wie einfach es ist, einen eigenen Container zu entwerfen und Präsentationsinformationen an Geschwister zu übergeben, so wie es Apple mit seinen Sheet und ähnlichen Präsentationscontainern macht.Weiterlesen23.08.2022
Flutter Benefit für Startup: Anwendungsentwicklung für plattformübergreifende ProjekteFlutter ist einer der am häufigsten verwendeten Stacks in der App-Entwicklung, insbesondere in plattformübergreifenden Umgebungen. Im Vergleich zu anderen nativen Apps, die unter Entwicklern verbreitet sind, gibt es hier einige Gründe, warum Sie Flutter für die Entwicklung Ihrer Apps wählen sollten.Weiterlesen