Heisann! I dag skal vi vise deg hvordan du kan gå fra å ha en tom sidemeny til å ha en pent animert, UX-bevisst sidemeny, vi skal gå gjennom steg-for-steg hvordan dette gjøres, og vi vil dele de beste praksisene med deg.
Vi vil svare på spørsmål, vi vil starte med det grunnleggende og videre vil vi viseforskjellige hint og triks som du kanskje trenger i produktet ditt, ved å ta hensyn til kodekvalitet og følge SwiftUI API-stilen.
Dette er hva vi skal snakke om i denne serien av artikler ved gradvis økende kompleksitet av dekkede emner:
Her er hva vi ender opp med:
Før vi starter, vil du kanskje laste ned det innledende prosjektet fra vår GitHub-repo for å følge koden. Det er bygget på toppen av Swift Package Manager og forutsetter at du har grunnleggende erfaring med det. Når det er lastet ned, åpner du "Example" -prosjektet, og vi er klare til å gå i gang.
Fra tid til annen trenger vi å vise en meny eller annet supplerende/navigasjons innhold som skal avsløres fra ledende/etterfølgende kanter. Og vi ønsker ikke å gjenta det igjen og igjen, så vår Drawer bør være generisk nok til atinnholdet kan brukes hvor som helst, la oss begynne med å åpne en Drawer.swift og hoppe inn i kode med en gang:
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)
}
}
Vi er nesten ferdige med det grunnleggende. Det er overraskende hvor enkelt og konsis koden er takket være SwiftUI. Du kan legge til følgende kode til enten en Preview eller til en ContentView.swift
i Example-appen:
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)
}
}
}
)
}
}
Etter kjøring legger du raskt merke til et problem med at appen ikke reagerer på berøringer utenfor en meny, så vel som at menyen selv forsvinner når du klikker på en "Open"-knapp.
Først laoss fikse og forstå et ganske enkelt problem: Color.clear
er usynlig for berøringer, noe som er forventet. For å løse dette finnes det løsningersom å plassere en solid farge med 0.01 opasitet, men vi skal bruke en tiltenkt visning modifikator – contentShape — som setter et tabbar område for en visnings treff test.
La oss bryte ned problemet med utilsiktet forsvinning ved menyavvisning:
ZStack(alignment: .leading) {
content
if isOpened {
...
menu
.transition(.move(edge: .leading))
}
}
ZStack legger ut subviews etter en zIndex-ordre. Som standard blir det avledet av rekkefølgen på deklarerte visninger. Når vi fjerner menyen vår fra ZStack
er det ingen grunn til å plassere den øverst selv om den er erklært som den nyeste. Men vi kan tvinge visningen til å bli lagt ut som øverste selv ved fjerning ved å bruke zIndex visningsmodifikatoren.
Her er vår oppdaterte 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)
}
Etter bygging og kjøring, kan vi gjøre akkurat det vi ønsket å gjøre:
Så langt har vi gjort veldig grunnleggende interaksjoner. Men dette er åpenbart ikke toppmoderne for en gjenbrukbar komponent. For å være generisk nok, må vi etablere kommunikasjon med søsken for å dele informasjon om de er presentert eller ikke, og - det som er like viktig - for å la dem avvise seg selv.
I UIKit-verdenen, kunne du traversere foreldre-barn-hierarkiet til ønsket visning kontroller ble funnet — akkurat som navigationController oppfører seg.
I SwiftUI-verdenen, er dette mønsteret ikke lenger aktuelt. For å håndtere denne muligheten, bruker Apple en PresentationMode som lar det presenterte barnet avvise seg selv uansett hvor dypt det er i hierarkiet - dette håndteres av et kraftig miljøverdi system.
Dessverre kan vi ikke opprette vår egen instans av PresentationMode på grunn av en skjult initialisering, men vi bør definitivt etterligne det Apple har gjort.
Det er generelt en flott tilnærming å være på nivå med en innebygd API, da du automatisk lar andre utviklere løfte opp all deres tidligere kunnskap og erfaring om hvordan lignende API fungerer. Det er ikke nødvendig å si hvordan en slik tilnærming forbedrer lokal resonnering.
Målet er å tillate følgende kode innenfor vår meny eller alle nedstigende søsken ned i hierarkiet:
struct SiblingView: View {
@Environment(\.drawerPresentationMode) var presentationMode
var body: some View {
Button("Dismiss") {
presentationMode.wrappedValue.close()
}
}
}
Vår presentasjonsmodus vil se lik ut, men litt annerledes for å passe til følgende behov: å kunne forstå/kontrollere Drawers isOpened
-flagg. La oss lage et utkast til en API vi trenger:
struct DrawerPresentationMode {
var isOpened: Bool
mutating func open()
mutating func close()
}
Dette ser ganske likt ut som det stardard PresentationMode tilbyr, men hvordan synkroniserer vi det med vår kilde til sannhet isOpened
som håndteres av Drawer
selv? Det er en kraftig evne innen Binding: en evne til å kartlegge fra en type til en annen med liten til ingen innsats.
For å implementere det, la oss finne ut at DrawerPresentationMode
er bygget rundt en vanlig Binding<Bool>
, men tilbyr litt mer fancy API:
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
}
}
}
Når implementeringen er gjort, la oss legge til kartlegging fra Binding<Bool>
til vår DrawerPresentationMode
:
extension Binding where Value == Bool {
func mappedToDrawerPresentationMode() -> Binding<DrawerPresentationMode> {
Binding<DrawerPresentationMode>(
get: {
DrawerPresentationMode(isOpened: self)
},
set: { newValue in
self.wrappedValue = newValue.isOpened
}
)
}
}
Etter å ha blitt kjent med hvordan PresentationMode kan implementeres, kan du utnytte nesten den samme APIen som Apple gjorde - dette er nøyaktig det vi streber etter - å gjenbruke all den vedtatte kunnskapen for Apples verktøy kjede.
Å sende denne bindingen til Drawers søsken er enkelt ved å bruke 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 }
}
}
Det som gjenstår er å sende disse dataene videre på veien i Drawers body:
public var body: some View {
ZStack(alignment: .leading) {
content
...
}
}
.animation(.spring(), value: isOpened)
.environment(\.drawerPresentationMode, $isOpened.mappedToDrawerPresentationMode())
}
Nå har vi alt vi trenger for endelig å starte implementeringen av en meny.
En oppmerksom person kunne legge merke til at det ikke er noen grunn til å lage en innpakning rundt
Binding<Bool>
og det er helt riktig. For vårt formål er det tilstrekkelig å sende bindingen direkte. Imidlertid er det fortsatt verdt å forstå hvordan ting kan gjøres.
Du kan også laste ned den endelige versjonen av denne delen fra vår repo.
Nå er det på tide å begynne å implementere menyen. La oss oppsummere hva som må gjøres og dele det inn i en rekke funksjoner:
Dette er det vi har implementert trinn-for-trinn så langt:
MenuItem
's med overskrift med støtte av utvidet og kompakt tilstand på plass.Som utgangspunkt kan du bruke følgende commit eller fortsette der vi slapp i forrige del.
Før vi går dypere inn i UI, er det verdt å klargjøre hvordan vi kan håndtere forskjellige tilstander. La oss erklære en MenuAppearance
med følgende innhold:
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)
}
}
Siden det bare er to tilstander vi bør ta vare på, passer enum
våre behov perfekt. Nytte Koden etter enum-deklarasjonen lar oss omgå gjeldende meny status ned i hierarkiet akkurat som vi gjorde før med Drawer.isOpened
-flagget.
Det kule her er at på grunn av SwiftUI-layout modellen, kan barnet stole på en gitt menyvisning for å justere layouten. Dette har en dramatisk effekt på kode simplifisering: i utgangspunktet er det ingen presentasjonsdetent involvert for å håndtere forskjellige layout størrelser - SwiftUI gjør riktig størrelse endring for oss.
Nå skal vi gå videre til grunnleggende byggeklosser. Som nevnt ovenfor, streber vi for å holde koden vår så nær som mulig SwiftUI innebygd API ikke bare når det gjelder navngivning og semantikk, men også for å gjenbruke eksisterende komponenter og begreper.
For å få en bedre forståelse av hva jeg snakker om, ta en titt her og prøv å finne ut vanlige visninger:
Det er åpenbart fra et design, at vi må opprettholde en slags HStack med konstant avstand og en lignende oppførsel når menyen kollapser for å bare vise et ikon.
I UIKit-verdenen, er det ikke en katastrofe, men en oppgave å tenke på. Men det er det ikke, når det kommer til SwiftUI. Ved første øyekast, er disse to separate typer visninger: bruker overskrift og en celle som kan velges. Imidlertid, i SwiftUI kan begge beskrives av en enkelt visning kalt Label og dens medfølgende LabelStyle som beskriver måten Icon og Title skal presenteres på.
Dette er en veldig sterk idé, på grunn av at de fleste av iOS-apper har mange abstraksjoner som kan beskrives perfekt som et par av Title og et Icon. SwiftUI tar denne ideen til neste nivå ved at den lar oss gi den et hvilken som helst utseende modifikator gjennom stil.
Og siden både Icon og Title er generiske typer, er det ikke overraskende at VStack fra brukeroverskrift og en menyelementtittel fra cellen begge er representert som en Title. Med dette i bakhodet kan vi skalere dette konseptet enda lenger i våre applikasjoner ved å forenkle og gjenbruke layout. For en tid å være i live! :)
La oss implementere vår 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
er bare en hendig innpakning for å oppnå et enkelt stylingsystem. Den kan bli funnet i vår eksempel kode.
Som du kan se, er alt vi har her et dødt enkelt layout som reagerer på en menyvisning ved å skjule Title av den gitte konfigurasjonen (se LabelStyleConfiguration for mer info).
Den eneste merkbare modifikatoren her er overgangen til en tittel, som flytter og bleker merkelappen vår inn/ut. Dette er en del av animasjonen vår. Du kan leke rundt for å se endelige effekter.
Når vi er ferdig med label layout, skal vi da av slutte med cellene? Disse er ikke celler her - det er heller en VStack med knapper i den for hver MenuItem vi har. Og akkurat som med LabelStyle
, gir SwiftUI oss muligheten til å endre utseendet på en Button med en egendefinert ButtonStyle. Dette er en vanlig tilnærming der komposisjonen viser alle sine fordeler over arv. Vi kan bokstavelig talt utvide og komponere enhver stil i enhver kombinasjon som er tankevekkende.
Du kan finne implementeringene av en egendefinert knapp med Gesture Recognizers, som håndterer utheving og valg, men det er tydeligvis ikke en tiltenkt tilnærming og en løsning i et nøtteskall.
ButtonStyle
på den andre siden, gir deg ikke bare visningen du må vise, men også tilstander om visningen er trykket eller ikke. Knappen har samtidig muligheten til å håndtere uthevet oppførsel ordentlig ved å gi en iOS-bred oppførsel - prøv å trykke og flytt fingeren rundt en hvilken som helst systemknapp for å se hva jeg mener.
Kort sagt: ButtonStyle
forteller oss hvordan SwiftUI vil at vi skal utvide og style visninger.
La meg vise deg implementeringen av en MenuButtonStyle
:
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())
}
}
Ta en nærmere titt på hva vi har her. Ser ikke så komplekst ut, men noen få linjer krever litt forklaring:
- animation
for isSelected != configuration.isPressed
gir systemets utseende og følelse til din kunde knapp. Når brukeren gjør en touchdown-hendelse, i tilfelle det er noen visuell tilbakemelding, får du en fin animasjon. Husk, visuell tilbakemelding er viktig, siden brukeren ikke har noen anelse om hva som skjer.
- frame
- denne er en vanskelig modifikator. Den lar knappen være utvidbar til hvilken som helst bredde, ellers vil den krympe for å omfavne innholdet sitt perfekt. Senere bruker vi den innen VStack
for å få fullt breddeknapper, selv om deres faktiske innhold størrelse kan være mye mindre. Lek rundt med denne modifikatoren for å se hva som kommer til å skje.
Å håndtere rammer er et bredt emne og vi anbefaler sterkt en Pro SwiftUI for å få praktisk kunnskap om SwiftUI layout system.
- contentShape
- Returnerer en visning som bruker den gitte formen for hit testing - Apple Documentation sier tydelig hva det er. Men for å klargjøre hva nøyaktig det gjør her, må vi komme tilbake til ramme-modifikatoren ovenfor. Button vil krympe seg selv for å omfavne innholdet sitt perfekt. Derfor har den ingen grunn til å håndtere berøringer andre steder, kun innenfor sitt ikon og tittel. Men knappen vår bør være interaktiv innenfor full bredde av VStack og ved å bruke denne modifikatoren tvinger vi uttrykkelig denne oppførselen.
- isSelected
- sist men ikke minst. ButtonStyle
og Button
håndterer ikke valgt tilstand og lar deg bestemme hvordan du skal lagre dette flagget og hvordan du skal presentere det for brukeren. Vår liste skal beholde valgt MenuItem
så vi passerer dette flagget eksplisitt til vår egendefinerte 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))
}
}
}
}
Noen av egenskapene og nytte koden er utelatt. Det anbefales å bla gjennom GitHub-repo for å få full kode.
Nå er det på tide å oppdatere vår ContentView
og bygge og kjøre koden vår for å se hva vi har fått:
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)
}
}
Her er resultatene våre etter å ha gjort endelige endringer i vår ContentView
:
Og med bare noen få tillegg for å sette ting sammen:
Selv om den endelige versjonen av koden ovenfor med bakgrunn, osv., kan finnes her, er det på tide å gå videre til den morsomme delen 2 — valg og egendefinerte overganger.
12.05.2023
Å revolusjonere mobilutvikling: Hvordan ChatGPT forbedrer brukeropplevelsenI denne artikkelen skal vi dekke emnet om hvilken innflytelse fChatGPT-teknologien har på brukeropplevelsen, og hvordan den kan brukes til å forbedre den og gi nye muligheter for mobil apputvikling.Les mer17.01.2023
DEN ULTIMATE GUIDEN TIL Å LAGE EN SIDEMENY I SWIFTUI: DEL 2. EGENTILPASSET OVERGANG OG ANIMASJONERHeisann, folkens. I del 1 av denne serien med artikler dekket vi skriving av egne komponenter i SwiftUI-designet ved å bruke Label, LabelStyle, ButtonStyle og flere. I tillegg kunne du se hvor enkelt det er å designe din egen beholder og overføre presentasjon informasjon til søsken, akkurat som Apple gjør med deres Sheet og lignende presentasjonsbeholdere.Les mer23.08.2022
Flutter fordeler for nyoppstartet selskaper: Applikasjonsutvikling for kryssplattform prosjekterFlutter er en av de mest brukte stablene i apputvikling spesielt i kryssplattform miljø. Sammenlignet med andre innfødte apper som har vært til stede og brukt av utviklere, her en noen grunner til hvorfor du kan bruke Flutter til å bygge dine apper.Les mer