Bygge væskegrænseflader

Sådan opretter du naturlige bevægelser og animationer på iOS

På WWDC 2018 præsenterede Apple-designere et foredrag med titlen “Designing Fluid Interfaces”, der forklarede designens begrundelse bag gestusgrænsefladen på iPhone X.

Apples WWDC18-præsentation “Designing Fluid Interfaces”

Det er min foretrukne WWDC-tale nogensinde - jeg kan varmt anbefale det.

Foredraget gav nogle tekniske vejledninger, hvilket er usædvanligt for en designpræsentation, men det var pseudokode, hvilket efterlod en masse ukendte.

Nogle Swift-lignende kode fra præsentationen.

Hvis du prøver at implementere disse ideer, vil du måske bemærke et kløft mellem inspiration og implementering.

Mit mål er at bygge bro over dette kløft ved at give eksempler på arbejdskode om ethvert større emne i præsentationen.

De otte (8) grænseflader, vi vil oprette. Knapper, fjedre, brugerdefinerede interaktioner og mere!

Her er en oversigt over, hvad vi vil dække:

  1. Et kort resumé af "Design Fluid Interfaces" -talen.
  2. Otte væskegrænseflader, designteorien bag dem og koden til opbygning af dem.
  3. Ansøgninger til designere og udviklere.

Hvad er fluidgrænseflader?

En fluidgrænseflade kan også kaldes "hurtig", "glat", "naturlig" eller "magisk". Det er en friktionsfri oplevelse, der bare føles "rigtig".

WWDC-præsentationen taler om fluidgrænseflader som "en udvidelse af dit sind" og "en udvidelse af den naturlige verden". En grænseflade er flydende, når den opfører sig efter den måde folk tænker på, ikke den måde maskiner tænker.

Hvad gør dem flydende?

Fluidgrænseflader er lydhør, interruptible og omdirigerbare. Her er et eksempel på swip-to-go-home-gestus på iPhone X:

Apps kan lukkes under deres lanceringsanimation.

Grænsefladen reagerer øjeblikkeligt på brugerens input, kan stoppes på ethvert tidspunkt i processen og kan endda ændre kurs midtvejs.

Hvorfor er vi interesserede i væskegrænseflader?

  1. Flydende grænseflader forbedrer brugerens oplevelse, hvilket får enhver interaktion til at føle sig hurtig, let og meningsfuld.
  2. De giver brugeren en følelse af kontrol, som skaber tillid med din app og dit brand.
  3. De er svære at opbygge. En fluidgrænseflade er vanskelig at kopiere og kan være en konkurrencefordel.

Grænsefladerne

For resten af ​​dette indlæg vil jeg vise dig, hvordan man bygger otte (8) grænseflader, der dækker alle de vigtigste emner i præsentationen.

Ikoner, der repræsenterer de otte (8) grænseflader, vi vil bygge.

Grænseflade nr. 1: Kalkulatorknap

Dette er en knap, der efterligner knappernes opførsel i iOS-lommeregner-appen.

Nøglefunktioner

  1. Fremhæver øjeblikkeligt ved berøring.
  2. Kan tappes hurtigt, selv når der er mid-animation.
  3. Brugeren kan trykke ned og trække uden for knappen for at annullere hanen.
  4. Brugeren kan røre ned, trække udenfor, trække tilbage ind og bekræfte hanen.

Designteori

Vi vil have knapper, der føles lydhøre, som anerkender for brugeren, at de er funktionelle. Derudover ønsker vi, at handlingen skal annulleres, hvis brugeren beslutter imod deres handling, efter at de har rørt ned. Dette giver brugerne mulighed for at tage hurtigere beslutninger, da de kan udføre handlinger parallelt med tanke.

Slides fra WWDC-præsentationen, der viser, hvordan bevægelser parallelt med tanke gør handlinger hurtigere.

Kritisk kode

Det første trin til at oprette denne knap er at bruge en UIControl underklasse, ikke en UIB-knap underklasse. En UIB-knap ville fungere fint, men da vi tilpasser interaktionen, har vi ikke brug for nogen af ​​dens funktioner.

LommeregnerKnap: UIControl {
    offentlig var-værdi: Int = 0 {
        didSet {label.text = “\ (værdi)”}
    }
    privat doven var etiket: UILabel = {...} ()
}

Dernæst bruger vi UIControlEvents til at tildele funktioner til de forskellige berøringsinteraktioner.

addTarget (self, action: #selector (touchDown), for: [.touchDown, .touchDragEnter])
addTarget (self, action: #selector (touchUp), for: [.touchUpInside, .touchDragExit, .touchCancel])

Vi grupperer touchDown og touchDragEnter begivenheder i en enkelt "begivenhed" kaldet touchDown, og vi kan gruppere touchUpInside, touchDragExit og touchCancel begivenheder i en enkelt begivenhed kaldet touchUp.

(For en beskrivelse af alle tilgængeligeUIControlEvents, se dokumentationen.)

Dette giver os to funktioner til at håndtere animationerne.

privat var animator = UIViewPropertyAnimator ()
@objc private func touchDown () {
    animator.stopAnimation (sand)
    backgroundColor = fremhævet Color
}
@objc privat func touchUp () {
    animator = UIViewPropertyAnimator (varighed: 0,5, kurve: .easeOut, animationer: {
        self.backgroundColor = self.normalColor
    })
    animator.startAnimation ()
}

På touchDown annullerer vi om nødvendigt den eksisterende animation og indstiller øjeblikkeligt farven til den fremhævede farve (i dette tilfælde en lysegrå).

Ved touchUp opretter vi en ny animator og starter animationen. Brug af en UIViewPropertyAnimator gør det nemt at annullere højdepunktanimationen.

(Side note: Dette er ikke den nøjagtige opførsel af knapperne i iOS-lommeregner-appen, som tillader et berøring, der startede i en anden knap for at aktivere det, hvis berøringen blev trukket inden i knappen. I de fleste tilfælde en knap som den ene Jeg oprettede her er den tilsigtede opførsel for iOS-knapper.)

Grænseflade nr. 2: Springanimationer

Denne grænseflade viser, hvordan en fjederanimation kan oprettes ved at specificere en "dæmpning" (hopp) og "respons" (hastighed).

Nøglefunktioner

  1. Bruger “designvenlige” parametre.
  2. Intet begreb af animationens varighed.
  3. Let afbrydes.

Designteori

Fjedre laver fantastiske animationsmodeller på grund af deres hurtighed og naturlige udseende. En forårsanimation starter utroligt hurtigt og bruger det meste af sin tid gradvist på at nærme sig sin endelige tilstand. Dette er perfekt til at skabe grænseflader, der føles lydhøre - de springer til liv!

Et par ekstra påmindelser ved design af forårsanimationer:

  1. Fjedre behøver ikke at være fjedrende. Brug af en dæmpningsværdi på 1 vil skabe en animation, der langsomt kommer til hvile uden nogen hoppe. De fleste animationer skal bruge en dæmpningsværdi på 1.
  2. Prøv at undgå at tænke over varigheden. I teorien kommer en fjeder aldrig helt til hvile, og at tvinge en varighed på foråret kan få den til at føle sig unaturlig. Spil i stedet med dæmpnings- og responsværdierne, indtil det føles rigtigt.
  3. Afbrydelse er kritisk. Fordi fjedre tilbringer så meget af deres tid tæt på deres endelige værdi, kan brugere tro, at animationen er afsluttet og vil prøve at interagere med den igen.

Kritisk kode

I UIKit kan vi oprette en fjederanimation med en UIViewPropertyAnimator og et UISpringTimingParameters-objekt. Desværre er der ingen initialisator, der bare tager en dæmpning og respons. Det tætteste vi kan komme er UISpringTimingParameters initialisator, der tager en masse, stivhed, dæmpning og starthastighed.

UISpringTimingParameters (masse: CGFloat, stivhed: CGFloat, dæmpning: CGFloat, initialVelocity: CGVector)

Vi vil gerne oprette en bekvemmelighedsinitialisator, der tager en dæmpning og reaktion, og kortlægger den til den krævede masse, stivhed og dæmpning.

Med lidt fysik kan vi udlede de ligninger, vi har brug for:

Løsning til fjederkonstanten og dæmpningskoefficient.

Med dette resultat kan vi oprette vores egne UISpringTimingParameters med nøjagtigt de parametre, vi ønsker.

udvidelse UISpringTimingParameters {
    bekvemmelighedsinit (dæmpning: CGFloat, respons: CGFloat, initialVelocity: CGVector = .zero) {
        lad stivhed = pow (2 * .pi / respons, 2)
        lad fugt = 4 * .pi * dæmpning / respons
        self.init (masse: 1, stivhed: stivhed, dæmpning: fugtig, initialVelocity: initialVelocity)
    }
}

Sådan specificerer vi forårsanimationer til alle andre grænseflader.

Fysikken bag forårsanimationer

Vil du gå dybere på forårsanimationer? Tjek dette utrolige indlæg af Christian Schnorr: Demystifying UIKit Spring Animations.

Efter at have læst hans indlæg klikkede foråret animationer endelig på mig. Kæmpe råb til Christian for at hjælpe mig med at forstå matematikken bag disse animationer og for at lære mig, hvordan man løser andenordens differentialligninger.

Grænseflade nr. 3: Lommelygtsknap

En anden knap, men med meget forskellig opførsel. Dette efterligner opførslen fra lommelygtsknappen på låseskærmen på iPhone X.

Nøglefunktioner

  1. Kræver en forsætlig gestus med 3D-berøring.
  2. Bounciness antyder den krævede gestus.
  3. Haptisk feedback bekræfter aktivering.

Designteori

Apple ønskede at oprette en knap, der var let og hurtigt tilgængelig, men som ikke kunne udløses ved et uheld. At kræve krafttryk for at aktivere lommelygten er et godt valg, men mangler råd og feedback.

For at løse disse problemer er knappen fjedrende og vokser, efterhånden som brugeren anvender kraft og antyder den krævede gestus. Derudover er der to separate vibrationer af haptisk feedback: en, når den krævede mængde kraft påføres, og en anden, når knappen aktiveres, når kraften reduceres. Disse haptikere efterligner opførslen af ​​en fysisk knap.

Kritisk kode

For at måle mængden af ​​kraft, der påføres knappen, kan vi bruge det UITouch-objekt, der leveres i berøringshændelser.

tilsidesætte func touches Flyttet (_ touches: Indstil , med begivenhed: UIEvent?) {
    super.touchesMoved (berører, med: begivenhed)
    vagt lad røre = rører. først {return}
    lad kraft = touch.force / touch.maximumPossibleForce
    lad skala = 1 + (maxBredde / minBredde - 1) * kraft
    transform = CGAffineTransform (skalaX: skala, y: skala)
}

Vi beregner en skalaetransformation baseret på den aktuelle kraft, så knappen vokser med stigende tryk.

Da knappen kunne trykkes, men endnu ikke er aktiveret, er vi nødt til at holde styr på knappens aktuelle tilstand.

enum ForceState {
    sag nulstilles, aktiveret, bekræftet
}
private let resetForce: CGFloat = 0.4
privat letaktiveringForce: CGFloat = 0.5
private letfirmationForce: CGFloat = 0,49

At have bekræftelsesstyrken være lidt lavere end aktiveringskraften forhindrer brugeren i at hurtigt aktivere og deaktivere knappen ved hurtigt at krydse krafttærsklen.

For haptisk feedback kan vi bruge UIKits feedbackgeneratorer.

private let activationFeedbackGenerator = UIImpactFeedbackGenerator (stil: .light)
privat letfirmationFeedbackGenerator = UIImpactFeedbackGenerator (stil: .medium)

Til slut kan vi til de hoppende animationer bruge en UIViewPropertyAnimator med de brugerdefinerede UISpringTimingParameters initialiseringer, vi oprettede før.

lad params = UISpringTimingParameters (dæmpning: 0.4, respons: 0.2)
lad animator = UIViewPropertyAnimator (varighed: 0, timingParametre: params)
animator.addAnimations {
    self.transform = CGAffineTransform (skalaX: 1, y: 1)
    self.backgroundColor = self.isOn? self.onColor: self.offColor
}
animator.startAnimation ()

Grænseflade 4: Gummibånd

Gummibånd opstår, når en visning modstår bevægelse. Et eksempel er, når en rullevisning når slutningen af ​​dens indhold.

Nøglefunktioner

  1. Interface er altid lydhør, også når en handling er ugyldig.
  2. De-synkroniseret berøringssporing indikerer en grænse.
  3. Bevægelsesmængden mindskes længere fra grænsen.

Designteori

Gummibånd er en fantastisk måde at kommunikere ugyldige handlinger på, mens den stadig giver brugeren en følelse af kontrol. Det indikerer let en afgrænsning og trækker dem tilbage til en gyldig tilstand.

Kritisk kode

Heldigvis er gummibånding let at implementere.

offset = pow (offset, 0,7)

Ved at bruge en eksponent mellem 0 og 1 flyttes visningens forskydning mindre, jo længere den er væk fra sin hvileposition. Brug en større eksponent til mindre bevægelse og en mindre eksponent til mere bevægelse.

For en lidt mere sammenhæng implementeres denne kode normalt i en UIPanGestureRecognizer-tilbagekald, når berøringen bevæger sig. Offset kan beregnes med deltaet mellem de aktuelle og originale berøringssteder, og forskydningen kan anvendes med en translationstransformation.

var offset = touchPoint.y - originalTouchPoint.y
offset = offset> 0? pow (offset, 0,7): -pow (-offset, 0,7)
view.transform = CGAffineTransform (oversættelseX: 0, y: offset)

Bemærk: Sådan udfører Apple ikke gummibånd med elementer som rullevisninger. Jeg kan godt lide denne metode på grund af dens enkelhed, men der er mere komplekse funktioner til forskellige opførsler.

Interface nr. 5: Accelerationspause

For at se app-switcher på iPhone X, skubber brugeren op fra bunden af ​​skærmen og holder pause midtvejs. Denne grænseflade skaber denne opførsel igen.

Nøglefunktioner

  1. Pause beregnes ud fra gestusens acceleration.
  2. Hurtigere stop resulterer i en hurtigere reaktion.
  3. Ingen timere.

Designteori

Fluidgrænseflader skal være hurtige. En forsinkelse fra en timer, selvom den er kort, kan få en grænseflade til at føles træg.

Denne grænseflade er især cool, fordi dens reaktionstid er baseret på brugerens bevægelse. Hvis de hurtigt pauser, reagerer grænsefladen hurtigt. Hvis de langsomt pauser, reagerer det langsomt.

Kritisk kode

For at måle acceleration kan vi spore de seneste værdier for pan-gestusens hastighed.

private var-hastigheder = [CGFloat] ()
privat func-spor (hastighed: CGFloat) {
    if velocities.count 

Denne kode opdaterer hastighedsarrayet til altid at have de sidste syv hastigheder, der bruges til at beregne accelerationen.

For at bestemme, om accelerationen er stor nok, kan vi måle forskellen mellem den første hastighed i vores array mod den aktuelle hastighed.

hvis abs (hastighed)> 100 || abs (offset) <50 {return}
let ratio = abs (firstRecordedVelocity - hastighed) / abs (firstRecordedVelocity)
hvis forhold> 0,9 {
    pauseLabel.alpha = 1
    feedbackGenerator.impactOccurred ()
    hasPaused = sandt
}

Vi kontrollerer også for at sikre, at bevægelsen har en minimal forskydning og hastighed. Hvis bevægelsen har mistet mere end 90% af sin hastighed, betragter vi den som pause.

Min implementering er ikke perfekt. I min testning ser det ud til at fungere temmelig godt, men der er en mulighed for en bedre heuristik til at måle acceleration.

Grænseflade nr. 6: Belønningsmoment

En skuffe med åbne og lukkede tilstande, der har hoppe baseret på bevægelsens hastighed.

Nøglefunktioner

  1. Hvis du banker på skuffen, åbnes den uden hopp.
  2. Når man klikker på skuffen, åbnes den med sprette.
  3. Interaktiv, afbrydelig og reversibel.

Designteori

Denne skuffe viser begrebet givende momentum. Når brugeren skubber en visning med hastighed, er det meget mere tilfredsstillende at animere visningen med hopp. Dette får interface til at føle sig levende og sjov.

Når skuffen er tappet, animeres den uden hoppe, hvilket føles passende, da et tryk ikke har nogen fart i en bestemt retning.

Når du designer tilpassede interaktioner, er det vigtigt at huske, at grænseflader kan have forskellige animationer til forskellige interaktioner.

Kritisk kode

For at forenkle logikken ved at tappe versus panorering kan vi bruge en brugerdefineret bevægelsesgenkendelsesunderklasse, der straks indtastes i begyndt tilstand ved touch-down.

klasse InstantPanGestureRecognizer: UIPanGestureRecognizer {
    tilsidesætte func touchesBegan (_ touches: Indstil , med begivenhed: UIEvent) {
        super.touchesBegan (rører, med: begivenhed)
        self.state = .began
    }
}

Dette giver også brugeren mulighed for at trykke på skuffen under dens bevægelse for at sætte den på pause, svarende til at tappe på en rullevisning, der i øjeblikket ruller. For at håndtere vandhaner kan vi kontrollere, om hastigheden er nul, når bevægelsen slutter og fortsætte animationen.

hvis yVelocity == 0 {
    animator.continueAnimation (medTimingParameters: nul, varighedFaktor: 0)
}

For at håndtere en bevægelse med hastighed skal vi først beregne dens hastighed i forhold til den samlede resterende forskydning.

lad fractionRemaining = 1 - animator.fractionComplete
let distanceRemaining = fractionRemaining * closedTransform.ty
hvis distanceRemaining == 0 {
    animator.continueAnimation (medTimingParameters: nul, varighedFaktor: 0)
    pause
}
lad relativeVelocity = abs (yVelocity) / distanceRemaining

Vi kan bruge denne relative hastighed til at fortsætte animationen med de tidsindstillede parametre, der inkluderer en smule bounciness.

lad timingParameters = UISpringTimingParameters (dæmpning: 0,8, respons: 0,3, initialVelocity: CGVector (dx: relativeVelocity, dy: relativeVelocity))
lad newDuration = UIViewPropertyAnimator (varighed: 0, timingParameters: timingParameters) .duration
lad DurationFactor = CGFloat (newDuration / animator.duration)
animator.continueAnimation (withTimingParameters: timingParameters, durationFactor: durationFactor)

Her opretter vi en ny UIViewPropertyAnimator til at beregne den tid animationen skal tage, så vi kan give den korrekte varighedFaktor, når animationen fortsættes.

Der er mere kompleksiteter relateret til at vende animationen, som jeg ikke vil dække her. Hvis du vil lære mere, skrev jeg en fuld tutorial til denne komponent: Bygge bedre iOS-app-animationer.

Interface nr. 7: FaceTime PiP

En gendannelse af brugergrænsefladen billede-i-billede i iOS FaceTime-appen.

Nøglefunktioner

  1. Let, luftigt samspil.
  2. Projekteret position er baseret på UIScrollViews decelerationsgrad.
  3. Kontinuerlig animation, der respekterer gestens oprindelige hastighed.

Kritisk kode

Vores slutmål er at skrive noget lignende.

lad params = UISpringTimingParameters (dæmpning: 1, respons: 0.4, initialVelocity: relativeInitialVelocity)
lad animator = UIViewPropertyAnimator (varighed: 0, timingParametre: params)
animator.addAnimations {
    self.pipView.center = nærmesteCornerPosition
}
animator.startAnimation ()

Vi vil gerne oprette en animation med en indledende hastighed, der matcher hastigheden af ​​panbevægelsen og animere pipen til det nærmeste hjørne.

Lad os først beregne den oprindelige hastighed.

For at gøre dette er vi nødt til at beregne en relativ hastighed baseret på den aktuelle hastighed, aktuelle position og målposition.

lad relativeInitialVelocity = CGVector (
    dx: relativeVelocity (forVelocity: hastighed.x, fra: pipView.center.x, til: nærmesteCornerPosition.x),
    dy: relativeVelocity (forVelocity: hastighed.y, fra: pipView.center.y, til: nærmesteCornerPosition.y)
)
func relativeVelocity (forVelocity hastighed: CGFloat, fra nuværende værdi: CGFloat, til targetValue: CGFloat) -> CGFloat {
    vagt nuværende værdi - targetValue! = 0 andet {return 0}
    returhastighed / (targetValue - currentValue)
}

Vi kan opdele hastigheden i dens x- og y-komponenter og bestemme den relative hastighed for hver.

Lad os derefter beregne hjørnet for PiP at animere til.

For at få vores interface til at føle sig naturlig og let, planlægger vi PiP's endelige position baseret på dens aktuelle bevægelse. Hvis PiP skulle glide og stoppe, hvor skulle den da lande?

let decelerationRate = UIScrollView.DecelerationRate.normal.rawValue
lad hastighed = genkender.velocitet (i: visning)
lad projectedPosition = CGPoint (
    x: pipView.center.x + projekt (initialVelocity: hastighed.x, decelerationRate: decelerationRate),
    y: pipView.center.y + projekt (initialVelocity: velocity.y, decelerationRate: decelerationRate)
)
lad nærmesteCornerPosition = nærmesteCorner (til: projectedPosition)

Vi kan bruge decelerationsgraden for en UIScrollView til at beregne denne hvileposition. Dette er vigtigt, fordi det refererer til brugerens muskelhukommelse til rulning. Hvis en bruger ved, hvor langt en visning ruller, kan de bruge den forudgående viden til intuitivt at gætte, hvor meget kraft der er behov for for at flytte PiP til det ønskede mål.

Denne decelerationshastighed er også ganske generøs, hvilket får interaktionen til at føles let - kun en lille flick er nødvendig for at sende PiP flyvende hele vejen over skærmen.

Vi kan bruge den projektionsfunktion, der er leveret i ”Designing Fluid Interfaces” -talen til at beregne den endelige projicerede position.

/// tilbagelagt afstand efter deceleration til nulhastighed med en konstant hastighed.
func-projekt (initialVelocity: CGFloat, decelerationRate: CGFloat) -> CGFloat {
    return (initialVelocity / 1000) * decelerationRate / (1 - decelerationRate)
}

Det sidste stykke, der mangler, er logikken for at finde det nærmeste hjørne baseret på den projicerede position. For at gøre dette kan vi løbe gennem alle hjørnepositioner og finde den, der har den mindste afstand til den projicerede landingsposition.

func nærmesteCorner (til punkt: CGPoint) -> CGPoint {
    var minDistance = CGFloat.greatestFiniteMagnitude
    var nærmeste placering = CGPoint.zero
    til position i pipPositioner {
        lad afstand = point.distance (mod: position)
        hvis afstand 

For at opsummere den endelige implementering: Vi bruger UIScrollViews retardationshastighed til at projicere pipens bevægelse til dens endelige hvileposition og beregne den relative hastighed for at føre det hele ind i UISpringTimingParameters.

Grænseflade 8: Rotation

Anvendelse af koncepterne fra PiP-interface til en rotationsanimation.

Nøglefunktioner

  1. Bruger projektion til at respektere gestusens hastighed.
  2. Ender altid i en gyldig retning.

Kritisk kode

Koden her ligner meget den forrige PiP-interface. Vi vil bruge de samme byggesten, undtagen at bytte den nærmesteCorner-funktion til en nærmeste vinkel-funktion.

func-projekt (...) {...}
func relativeVelocity (...) {...}
func nærmeste vinkel (...) {...}

Når det er tid til endelig at oprette UISpringTimingParameters, er vi forpligtet til at bruge en CGVector til den oprindelige hastighed, selvom vores rotation kun har en dimension. I alle tilfælde, hvor den animerede egenskab kun har en dimension, skal du indstille dx-værdien til den ønskede hastighed og sætte dy-værdien til nul.

let timingParameters = UISpringTimingParameters (
    dæmpning: 0,8,
    svar: 0,4,
    initialVelocity: CGVector (dx: relativeInitialVelocity, dy: 0)
)

Internt ignorerer animator dy-værdien og bruger dx-værdien til at oprette timingskurven.

Prøv det selv!

Disse grænseflader er meget sjovere på en rigtig enhed. Hvis du selv vil lege med disse grænseflader, er demo-appen tilgængelig på GitHub.

Demo-appen til væskegrænseflader, tilgængelig på GitHub!

Praktiske applikationer

For designere

  1. Tænk på grænseflader som flydende medier til udtryk, ikke samlinger af statiske elementer.
  2. Overvej animationer og bevægelser tidligt i designprocessen. Layoutværktøjer som Sketch er fantastiske, men tilbyder ikke enhedens fulde udtryk.
  3. Prototype med udviklere. Få designindstillede udviklere til at hjælpe dig med at prototype animationer, bevægelser og haptik.

For udviklere

  1. Anvend tipene fra disse grænseflader på dine egne brugerdefinerede komponenter. Tænk på, hvordan de kan kombineres på nye og interessante måder.
  2. Lær dine designere om nye muligheder. Mange er ikke opmærksomme på den fulde kraft af 3D-touch, haptics, bevægelser og forårsanimationer.
  3. Prototype med designere. Hjælp dem med at se deres design på en ægte enhed og oprette værktøjer, der hjælper dem med at designe mere effektivt.

Hvis du nød dette indlæg, skal du forlade nogle klapper.

Du kan klappe op til 50 gange, så kom / klik!

Del venligst indlægget med din iOS-designer / iOS-udviklervenner på dit valgte sociale medieudgang.

Hvis du kan lide denne slags ting, skal du følge mig på Twitter. Jeg sender kun tweets i høj kvalitet. twitter.com/nathangitter

Tak til David Okun for at have revideret udkast til dette indlæg.