Lær deg beste praksis for iOS ved å lage en enkel oppskriftsapp

Kilde: ChefStep

Innholdsfortegnelse

  • Starter
  • Xcode og Swift versjon
  • Minimum iOS-versjon å støtte
  • Organisering av Xcode-prosjektet
  • Oppskrifter appstruktur
  • Kodekonvensjoner
  • dokumentasjon
  • Merking av kodedeler
  • Kildekontroll
  • avhengig
  • Å komme inn i prosjektet
  • API
  • Start skjerm
  • App-ikon
  • Foringskode med SwiftLint
  • Typesikker ressurs
  • Vis meg koden
  • Designe modellen
  • Bedre navigasjon med FlowController
  • Auto Layout
  • Arkitektur
  • Massiv visningskontroller
  • Adgangskontroll
  • Lat egenskaper
  • Kodeutdrag
  • Nettverk
  • Hvordan teste nettverkskode
  • Implementering av hurtigbuffer for offline støtte
  • Hvordan teste Cache
  • Laster inn eksterne bilder
  • Gjør bildelasting mer praktisk for UIImageView
  • Generisk datakilde for UITableView og UICollectionView
  • Kontroller og visning
  • Håndtere ansvar med en barnevisningskontroller
  • Sammensetning og avhengighetsinjeksjon
  • App Transport Security
  • En tilpasset rullbar visning
  • Legger til søkefunksjonalitet
  • Forstå presentasjonskontekst
  • Fremsetter søkehandlinger
  • Testing av debouncing med omvendt forventning
  • Testing av brukergrensesnitt med UITests
  • Hovedtrådbeskyttelse
  • Måling av forestillinger og problemstillinger
  • Prototyping med lekeplass
  • Hvor du skal dra herfra

Jeg startet utviklingen av iOS da iOS 7 hadde blitt kunngjort. Og jeg har lært litt, gjennom arbeid, råd fra kolleger og iOS-samfunnet.

I denne artikkelen vil jeg dele mange gode fremgangsmåter ved å ta eksempelet på en enkel oppskrifts-app. Kildekoden er på GitHub-oppskrifter.

Appen er en tradisjonell masterdetaljerapplikasjon som viser frem en liste med oppskrifter sammen med detaljert informasjon.

Det er tusenvis av måter å løse et problem på, og hvordan et problem blir taklet avhenger også av personlig smak. Forhåpentligvis vil du lære noe nyttig gjennom denne artikkelen. Jeg lærte mye da jeg gjorde dette prosjektet.

Jeg har lagt til lenker til noen nøkkelord der jeg følte at ytterligere lesing ville være fordelaktig. Så definitivt sjekk dem ut. Eventuelle tilbakemeldinger er velkomne.

Så la oss komme i gang ...

Her er et høyt nivå oversikt over hva du skal bygge.

Starter

La oss bestemme for verktøyet og prosjektinnstillingene som vi bruker.

Xcode og Swift versjon

På WWDC 2018 introduserte Apple Xcode 10 med Swift 4.2. Imidlertid er Xcode 10 i skrivende stund fremdeles i beta 5. Så la oss holde oss til den stabile Xcode 9 og Swift 4.1. Xcode 4.2 har noen kule funksjoner - du kan leke med den gjennom denne fantastiske lekeplassen. Det introduserer ikke store endringer fra Swift 4.1, så vi kan enkelt oppdatere appen vår i nær fremtid om nødvendig.

Du bør stille Swift-versjonen i prosjektinnstillingen i stedet for målinnstillingene. Dette betyr at alle mål i prosjektet har samme Swift-versjon (4.1).

Minimum iOS-versjon å støtte

Fra sommeren 2018 er iOS 12 i offentlig beta 5, og vi kan ikke målrette iOS 12 uten Xcode 10. I dette innlegget bruker vi Xcode 9 og base SDK er iOS 11. Avhengig av krav og brukerbaser, noen apper trenger å støtte gamle iOS-versjoner. Selv om iOS-brukere har en tendens til å ta i bruk nye iOS-versjoner raskere enn de som bruker Android, er det noen som holder seg med gamle versjoner. I følge Apples råd må vi støtte de to siste versjonene, som er iOS 10 og iOS 11. Som målt av App Store 31. mai 2018, er det bare 5% av brukerne som bruker iOS 9 og tidligere.

Å målrette mot nye iOS-versjoner betyr at vi kan dra nytte av nye SDK-er, som Apple-ingeniører forbedrer hvert år. Apple-utviklernettstedet har en forbedret endringsloggvisning. Nå er det lettere å se hva som er lagt til eller modifisert.

For å avgjøre når vi skal slippe støtte for gamle iOS-versjoner, trenger vi analyser om hvordan brukerne bruker appen vår.

Organisering av Xcode-prosjektet

Når vi oppretter det nye prosjektet, velger du både “Inkluder enhetstester” og “Inkluder UI-tester” som det er en anbefalt praksis å skrive tester tidlig. De siste endringene i XCTest-rammeverket, spesielt i UI-tester, gjør testingen til en lek og er ganske stabil.

Før du legger til nye filer i prosjektet, ta en pause og tenk på strukturen til appen din. Hvordan ønsker vi å organisere filene? Vi har noen få alternativer. Vi kan organisere filer etter funksjon / modul eller rolle / typer. Hver har sine fordeler og ulemper, og jeg skal diskutere dem nedenfor.

Etter rolle / type:

  • Fordeler: Det er mindre tanker involvert om hvor du skal legge filer. Det er også lettere å bruke skript eller filtre.
  • Ulemper: Det er vanskelig å korrelere hvis vi ønsker å finne flere filer relatert til den samme funksjonen. Det vil også ta tid å organisere filer hvis vi ønsker å gjøre dem til gjenbrukbare komponenter i fremtiden.

Etter funksjon / modul

  • Fordeler: Det gjør alt modulært og oppmuntrer til komposisjon.
  • Ulemper: Det kan bli rotete når mange filer av forskjellige typer er samlet.

Forblir modulær

Personlig prøver jeg å organisere koden min etter funksjoner / komponenter så mye som mulig. Dette gjør det lettere å identifisere relatert kode å fikse, og legge til nye funksjoner enklere i fremtiden. Det svarer på spørsmålet "Hva gjør denne appen?" I stedet for "Hva er denne filen?" Her er en god artikkel angående dette.

En god tommelfingerregel er å holde seg konsekvent, uansett hvilken struktur du velger.

Oppskrifter appstruktur

Følgende er appstrukturen som vår oppskriftsapp bruker:

Kilde

Inneholder kildekodefiler, delt inn i komponenter:

  • Funksjoner: hovedfunksjonene i appen
  • Hjem: startskjermen, viser en liste med oppskrifter og et åpent søk
  • Liste: viser en liste over oppskrifter, inkludert omlasting av en oppskrift og viser en tom visning når en oppskrift ikke eksisterer
  • Søk: håndter søk og utspilling
  • Detalj: viser detaljinformasjon

Bibliotek

Inneholder kjernekomponentene i applikasjonen vår:

  • Flow: inneholder FlowController for å administrere flyter
  • Adapter: generisk datakilde for UICollectionView
  • Utvidelse: praktiske utvidelser for vanlige operasjoner
  • Modell: Modellen i appen, analysert fra JSON

Ressurs

Inneholder plist-, ressurs- og Storyboard-filer.

Kodekonvensjoner

Jeg er enig med de fleste av stilguidene i raywenderlich / swift-style-guide og github / swift-style-guide. Disse er enkle og rimelige å bruke i et Swift-prosjekt. Ta også en titt på de offisielle API designretningslinjene laget av Swift-teamet hos Apple om hvordan du kan skrive bedre Swift-koder.

Uansett hvilken stilguide du velger å følge, må kodeklarhet være det viktigste målet ditt.

Innrykk og tab-space-krigen er et følsomt tema, men igjen, det avhenger av smak. Jeg bruker innrykk i fire mellomrom i Android-prosjekter, og to mellomrom i iOS og React. I denne Oppskrifts-appen følger jeg konsekvent og lettfattelig innrykk, som jeg har skrevet om her og her.

dokumentasjon

God kode bør forklare seg tydelig, slik at du ikke trenger å skrive kommentarer. Hvis det er vanskelig å forstå en del av koden, er det greit å ta en pause og refaktorere den til noen metoder med beskrivende navn, så det er koden som er tydeligere å forstå. Imidlertid synes jeg at dokumentasjonsklasser og metoder også er bra for dine kolleger og fremtidig selv. I henhold til Swift API-designretningslinjer,

Skriv en dokumentasjonskommentar for hver erklæring. Innsikt oppnådd ved å skrive dokumentasjon kan ha stor innvirkning på designen din, så ikke legg den av.

Det er veldig enkelt å generere kommentarmal /// i Xcode med Cmd + Alt + /. Hvis du planlegger å refaktorere koden din til et rammeverk for å dele med andre i fremtiden, kan verktøy som jazzy generere dokumentasjon slik at andre kan følge med.

Merking av kodedeler

Bruken av MARK kan være nyttig for å skille kodeseksjoner. Den grupperer også funksjoner fint i navigasjonslinjen. Du kan også bruke utvidelsesgrupper, relaterte egenskaper og metoder.

For en enkel UIViewController kan vi definere følgende MARK:

// MERK: - Init
// MERK: - Vis livssyklus
// MARK: - Oppsett
// MERK: - Handling
// MERK: - Data

Kildekontroll

Git er et populært kildekontrollsystem akkurat nå. Vi kan bruke malen .gitignore-fil fra gitignore.io/api/swift. Det er både fordeler og ulemper ved å sjekke avhengighetsfiler (CocoaPods og Carthage). Det avhenger av prosjektet ditt, men jeg pleier ikke å begå avhengigheter (node_moduler, Kartago, Pods) i kildekontroll for ikke å rote kodebasen. Det gjør det også lettere å se gjennom forespørsler.

Hvorvidt du sjekker inn Pods-katalogen, Podfile og Podfile.lock skal alltid holdes under versjonskontroll.

Jeg bruker både iTerm2 for å utføre kommandoer og Source Tree for å se grener og iscenesettelse.

avhengig

Jeg har brukt tredjeparts rammer, og har også laget og bidratt til open source mye. Å bruke et rammeverk gir deg et løft i starten, men det kan også begrense deg mye i fremtiden. Det kan være noen trivielle forandringer som det er veldig vanskelig å jobbe rundt. Det samme skjer når du bruker SDK-er. Min preferanse er å velge aktive open source-rammer. Les kildekoden og sjekk rammene nøye, og rådfør med teamet ditt hvis du planlegger å bruke dem. Litt ekstra forsiktighet skader ikke.

I denne appen prøver jeg å bruke så få avhengigheter som mulig. Rett nok til å demonstrere hvordan man kan håndtere avhengigheter. Noen erfarne utviklere vil kanskje foretrekke Carthage, en avhengighetsansvarlig, da det gir deg full kontroll. Her velger jeg CocoaPods fordi den er enkel å bruke, og den har fungert utmerket så langt.

Det er en fil som heter .swift-versjon av verdi 4.1 i roten til prosjektet for å fortelle CocoaPods at dette prosjektet bruker Swift 4.1. Dette ser enkelt ut men tok meg ganske lang tid å finne ut av.

Å komme inn i prosjektet

La oss lage noen lanseringsbilder og ikoner for å gi prosjektet et pent utseende.

API

Den enkle måten å lære iOS-nettverk på er gjennom offentlige gratis API-tjenester. Her bruker jeg food2fork. Du kan registrere deg for en konto på http://food2fork.com/about/api. Det er mange andre fantastiske API-er i dette offentlige api-depotet.

Det er bra å oppbevare legitimasjonen din på et trygt sted. Jeg bruker 1Password for å generere og lagre passordene mine.

La oss spille med APIene før vi begynner å kode, for å se hvilke typer forespørsler de trenger og svar de returnerer. Jeg bruker Insomnia-verktøyet til å teste og analysere API-svar. Den er åpen kildekode, gratis og fungerer bra.

Start skjerm

Førsteinntrykket er viktig, det samme er lanseringsskjermen. Den foretrukne måten er å bruke LaunchScreen.storyboard i stedet for et statisk startbilde.

Hvis du vil legge til et lanseringsbilde til Asset Catalog, åpner du LaunchScreen.storyboard, legger UIImageView og fester det til kantene på UIView. Vi skal ikke feste bildet til det trygge området, da vi vil at bildet skal være i fullskjerm. Fjern også valg av marginer i begrensningene for automatisk oppsett. Angi contentMode for UIImageView som Aspect Fill slik at den strekker seg med riktig sideforhold.

Konfigurer layout i LaunchScreen.

App-ikon

En god praksis er å tilby alle nødvendige appikoner for hver enhet du støtter, og også for steder som Varsling, Innstillinger og Springbrett. Forsikre deg om at hvert bilde ikke har noen transparente piksler, ellers resulterer det i svart bakgrunn. Dette tipset er fra retningslinjer for menneskelig grensesnitt - appikon.

Hold bakgrunnen enkel og unngå åpenhet. Forsikre deg om at ikonet ditt er ugjennomsiktig, og ikke rot bakgrunnen. Gi den en enkel bakgrunn slik at den ikke overmann andre appikoner i nærheten. Du trenger ikke å fylle hele ikonet med innhold.

Vi må designe firkantede bilder med en størrelse som er større enn 1024 x 1024, slik at hver enkelt kan nedskalere til mindre bilder. Du kan gjøre dette for hånd, skript eller bruke denne lille appen IconGenerator som jeg har laget.

IconGenerator-appen kan generere ikoner for iOS i iPhone, iPad, macOS og watchOS-apper. Resultatet er AppIcon.appiconset som vi kan dra rett inn i Asset Catalog. Asset Catalog er veien å gå for moderne Xcode-prosjekter.

Foringskode med SwiftLint

Uansett hvilken plattform vi utvikler på, er det bra å ha en pinne for å håndheve konsistente konvensjoner. Det mest populære verktøyet for Swift-prosjekter er SwiftLint, laget av de fantastiske menneskene på Realm.

For å installere den, legger du pod 'SwiftLint', '~> 0,25' til Podfile. Det er også en god praksis å spesifisere versjonen av avhengighet slik at podinstallasjon ikke ved en tilfeldighet oppdateres til en større versjon som kan ødelegge appen din. Legg deretter til en .wiftlint.yml med den foretrukne konfigurasjonen. En eksempelkonfigurasjon finner du her.

Til slutt legger du til en ny Run Script-setning for å utføre hurtiglint etter kompilering.

Typesikker ressurs

Jeg bruker R.swift for å forvalte ressursene trygt. Det kan generere typesikre klasser for å få tilgang til font, lokaliserbare strenger og farger. Hver gang vi endrer navn på ressursfiler, får vi kompileringsfeil i stedet for et implisitt krasj. Dette forhindrer oss til å avlede ressurser som er aktivt i bruk.

imageView.image = R.image.notFound ()

Vis meg koden

La oss dykke inn i koden, begynner med modellen, flytkontrollere og serviceklasser.

Designe modellen

Det kan høres kjedelig ut, men klienter er bare en penere måte å representere API-svaret på. Modellen er kanskje den mest grunnleggende tingen, og vi bruker den mye i appen. Det spiller en så viktig rolle, men det kan være noen åpenbare feil relatert til misdannede modeller og antakelser om hvordan en modell skal analyseres som må vurderes.

Vi bør teste for hver modell av appen. Ideelt sett trenger vi automatisert testing av modeller fra API-svar i tilfelle modellen har endret seg fra backend.

Fra Swift 4.0, kan vi tilpasse modellen vår til Codable for enkelt å serialisere til og fra JSON. Modellen vår skal være uforanderlig:

struct Oppskrift: Kodbar {
  la utgiver: String
  la url: URL
  la sourceUrl: String
  la id: String
  la tittel: String
  la imageUrl: String
  la socialRank: Double
  la publisherUrl: URL
enum CodingKeys: String, CodingKey {
    saksforlegger
    sak url = "f2f_url"
    case sourceUrl = "source_url"
    case id = "oppskrift_id"
    sakstittel
    case imageUrl = "image_url"
    case socialRank = "social_rank"
    case publisherUrl = "publisher_url"
  }
}

Vi kan bruke noen testrammer hvis du liker fancy syntaks eller en RSpec-stil. Noen testrammer fra tredjeparter kan ha problemer. Jeg synes XCTest er god nok.

importer XCTest
@ testable import Oppskrifter
klasse OppskrifterTester: XCTestCase {
  func testParsing () kaster {
    la json: [String: Any] = [
      "utgiver": "To erter og poden deres",
      "f2f_url": "http://food2fork.com/view/975e33",
      "title": "No-Bake Chocolate Peanut Butter Pretzel Cookies",
      "source_url": "http://www.twopeasandtheirpod.com/no-bake-chocol-peanut-butter-pretzel-cookies/",
      "oppskrift_id": "975e33",
      "image_url": "http://static.food2fork.com/NoBakeChocolatPeanutButterPretzelCookies44147.jpg",
      "sosial_rank": 99.99999999999974,
      "publisher_url": "http://www.twopeasandtheirpod.com"
    ]
la data = prøv JSONSerialization.data (medJSONObject: json, alternativer: [])
    la dekoder = JSONDecoder ()
    la oppskrift = prøv dekoder.dekode (Oppskrift.selv, fra: data)
XCTAssertEqual (oppskrift.tittelen, "No-Bake Chocolate Peanut Butter Pretzel Cookies")
    XCTAssertEqual (oppskrift.id, "975e33")
    XCTAssertEqual (oppskrift.url, URL (streng: "http://food2fork.com/view/975e33")!)
  }
}

Bedre navigasjon med FlowController

Før brukte jeg Compass som en rutingmotor i prosjektene mine, men over tid har jeg funnet ut at det å skrive enkel rutingkode fungerer også.

FlowController brukes til å administrere mange UIViewController-relaterte komponenter til en felles funksjon. Det kan være lurt å lese FlowController og koordinator for andre brukssaker og for å få en bedre forståelse.

Det er AppFlowController som klarer å endre rootViewController. Foreløpig starter det RecipeFlowController.

vindu = UIWindow (ramme: UIScreen.main.bounds)
windows? .rootViewController = appFlowController
vinduet? .makeKeyAndVisible ()
appFlowController.start ()

RecipeFlowController administrerer (faktisk er det) UINavigationController, som håndterer å skyve HomeViewController, RecipesDetailViewController, SafariViewController.

sluttklasse RecipeFlowController: UINavigationController {
  /// Start flyten
  func start () {
    let service = RecipesService (nettverk: NetworkService ())
    let controller = HomeViewController (recipesService: service)
    viewControllers = [controller]
    controller.select = {[svak selv] oppskrift i
      self? .startDetail (oppskrift: oppskrift)
    }
  }
privat func startDetail (oppskrift: Oppskrift) {}
  privat func startWeb (url: URL) {}
}

UIViewController kan bruke delegat eller nedleggelse for å varsle FlowController om endringer eller neste skjermer i flyten. For delegater kan det være behov for å sjekke når det er to forekomster av samme klasse. Her bruker vi closeurefor enkelhet.

Auto Layout

Auto Layout har eksistert siden iOS 5, det blir bedre for hvert år. Selv om noen fremdeles har et problem med det, mest på grunn av forvirrende brytende begrensninger og ytelse, men personlig, synes jeg Auto Layout er bra nok.

Jeg prøver å bruke Auto Layout så mye som mulig for å lage et adaptivt brukergrensesnitt. Vi kan bruke biblioteker som Anchors for å gjøre erklærende og rask Auto Layout. I denne appen bruker vi imidlertid NSLayoutAnchor siden den er fra iOS 9. Koden nedenfor er inspirert av Constraint. Husk at Auto Layout i sin enkleste form innebærer å skifte oversettelserAutoresizingMaskIoTo Constraints og aktivere isActive begrensninger.

utvidelse NSLayoutConstraint {
  statisk funk-aktivering (_ begrensninger: [NSLayoutConstraint]) {
    begrensninger. For hver {
      ($ 0.firstItem som? UIView) ?. translatesAutoresizingMaskIntoConstraints = falsk
      $ 0.isActive = true
    }
  }
}

Det er faktisk mange andre layoutmotorer tilgjengelig på GitHub. For å få en forståelse av hvilken som vil være egnet å bruke, sjekk ut LayoutFrameworkBenchmark.

Arkitektur

Arkitektur er sannsynligvis det mest hypede og diskuterte emnet. Jeg er en fan av å utforske arkitekturer, du kan se flere innlegg og rammer om forskjellige arkitekturer her.

For meg definerer alle arkitekturer og mønstre roller for hvert objekt og hvordan man kobler dem sammen. Husk disse retningslinjene for valget av arkitektur:

  • innkapsler hva som varierer
  • favoriserer sammensetning fremfor arv
  • program for grensesnitt, ikke implementering

Etter å ha lekt med mange forskjellige arkitekturer, med og uten Rx, fant jeg ut at enkel MVC er god nok. I dette enkle prosjektet er det bare UIViewController med logikk innkapslet i helper Service klasser,

Massiv visningskontroller

Du har kanskje hørt folk spøke om hvor massiv UIViewController er, men i virkeligheten er det ingen massiv visningskontroller. Det er bare oss som skriver dårlig kode. Det er imidlertid måter å slanke det på.

I oppskriftsappen jeg bruker,

  • Tjeneste for å injisere i visningskontrolleren for å utføre en enkelt oppgave
  • Generisk visning for å flytte visning og kontrollerer erklæringen til visningslaget
  • Barnesynskontroller for å komponere kontroller av barnesyn for å bygge flere funksjoner

Her er en veldig god artikkel med 8 tips for å slanke store kontrollere.

Adgangskontroll

SWIFT-dokumentasjonen nevner at “tilgangskontroll begrenser tilgangen til deler av koden din fra kode i andre kildefiler og moduler. Denne funksjonen lar deg skjule implementeringsdetaljene til koden din, og spesifisere et foretrukket grensesnitt som den koden kan nås og brukes gjennom. "

Alt skal være privat og endelig som standard. Dette hjelper også kompilatoren. Når vi ser en offentlig eiendom, må vi søke etter den på tvers av prosjektet før vi gjør noe videre med det. Hvis eiendommen bare brukes i en klasse, gjør den privat, at vi ikke trenger å bry oss om den bryter andre steder.

Angi eiendommer som endelige der det er mulig.

sluttklasse HomeViewController: UIViewController {}

Erklær eiendommer som private eller i det minste private (sett).

sluttklasse RecipeDetailView: UIView {
  private let scrollableView = ScrollableView ()
  privat (sett) lat var imageView: UIImageView = self.makeImageView ()
}

Lat egenskaper

For egenskaper som er tilgjengelige på et senere tidspunkt, kan vi erklære dem som late og kan bruke stenging for rask konstruksjon.

sluttklasse RecipeCell: UICollectionViewCell {
  privat (sett) lat var containerView: UIView = {
    let view = UIView ()
    view.clipsToBounds = sant
    view.layer.cornerRadius = 5
    view.backgroundColor = Color.main.withAlphaComponent (0.4)
returvisning
  } ()
}

Vi kan også bruke make-funksjoner hvis vi planlegger å gjenbruke den samme funksjonen for flere egenskaper.

sluttklasse RecipeDetailView: UIView {
  privat (sett) lat var imageView: UIImageView = self.makeImageView ()
private func makeImageView () -> UIImageView {
    let imageView = UIImageView ()
    imageView.contentMode = .scaleAspectFill
    imageView.clipsToBounds = sant
    return imageView
  }
}

Dette samsvarer også med råd fra strebe etter flytende bruk.

Begynn navnene på fabrikkmetodene med “make”, for eksempel x.makeIterator ().

Kodeutdrag

Noen kodesyntax er vanskelig å huske. Vurder å bruke kodebiter for å automatisk generere kode. Dette støttes av Xcode og er den foretrukne måten av Apple-ingeniører når de demonstrerer.

hvis # tilgjengelig (iOS 11, *) {
  viewController.navigationItem.searchController = searchController
  viewController.navigationItem.hidesSearchBarWhenScrolling = falsk
} annet {
  viewController.navigationItem.titleView = searchController.searchBar
}

Jeg lagde en repo med noen nyttige Swift-utdrag som mange liker å bruke.

Nettverk

Nettverk i Swift er et slags løst problem. Det er kjedelige og feilutsatte oppgaver som å analysere HTTP-svar, håndtere forespørselskøer, håndtere parameterforespørsler. Jeg har sett feil om PATCH-forespørsler, HTC-metoder med lavere karakter, ... Vi kan bare bruke Alamofire. Det er ikke nødvendig å kaste bort tid her.

For denne appen, siden det er enkelt og å unngå unødvendige avhengigheter. Vi kan bare bruke URLSession direkte. En ressurs inneholder vanligvis URL, bane, parametere og HTTP-metoden.

struktur Ressurs {
  la url: URL
  la vei: streng?
  la httpMethod: String
  la parametere: [String: String]
}

En enkel nettverkstjeneste kan bare analysere Ressurs til URLRequest og fortelle URLSession å utføre

sluttklasse NetworkService: Networking {
  @discardableResult func hente (ressurs: Ressurs, fullføring: @escaping (Data?) -> Void) -> URLSessionTask? {
    vakt la forespørsel = makeRequest (ressurs: ressurs) annet {
      komplettering (null)
      tilbake null
    }
la oppgave = session.dataTask (med: forespørsel, fullføringHåndler: {data, _, feil i
      vakt la data = data, feil == intet annet {
        komplettering (null)
        komme tilbake
      }
komplettering (data)
    })
task.resume ()
    returoppgave
  }
}

Bruk avhengighetsinjeksjon. Tillat at innringeren spesifiserer URLSessionConfiguration. Her bruker vi Swift standardparameter for å tilby det vanligste alternativet.

init (konfigurasjon: URLSessionConfiguration = URLSessionConfiguration.default) {
  self.session = URLSession (konfigurasjon: konfigurasjon)
}

Jeg bruker også URLQueryItem som var fra iOS 8. Det gjør parsing av parametere for å spørre elementer fine og mindre kjedelige.

Hvordan teste nettverkskode

Vi kan bruke URLProtocol og URLCache for å legge til en stubbe for nettverkssvar, eller vi kan bruke rammer som Mockingjay som svømmer URLSessionConfiguration.

Selv foretrekker jeg å bruke protokollen for å teste. Ved å bruke protokollen, kan testen opprette en spott forespørsel for å gi en stub-respons.

protokoll Nettverk {
  @discardableResult func hente (ressurs: Ressurs, fullføring: @escaping (Data?) -> Void) -> URLSessionTask?
}
sluttklasse MockNetworkService: Networking {
  la data: Data
  init (filnavn: streng) {
    let bundle = Bundle (for: MockNetworkService.self)
    la url = bundle.url (forRessource: fileName, withExtension: "json")!
    self.data = prøv! Data (contentOf: url)
  }
func fetch (ressurs: Ressurs, fullføring: @escaping (Data?) -> Void) -> URLSessionTask? {
    komplettering (data)
    tilbake null
  }
}

Implementering av hurtigbuffer for offline støtte

Jeg pleide å bidra og bruker et bibliotek som heter Cache mye. Det vi trenger fra et godt cache-bibliotek er minne og disk-cache, minne for rask tilgang, disk for utholdenhet. Når vi lagrer, lagrer vi på både minne og disk. Når vi laster inn, hvis minnebufferen mislykkes, laster vi fra disken og oppdaterer deretter minnet igjen. Det er mange avanserte emner om cache som rensing, utløp, tilgangsfrekvens. Les om dem her.

I denne enkle appen er en hjemmekultert cache-tjenesteklasse nok og en god måte å lære hvordan cache fungerer. Alt i Swift kan konverteres til Data, så vi kan bare lagre Data i cache. Swift 4 Codable kan serialisere objekt til data.

Koden nedenfor viser oss hvordan du bruker FileManager for diskbuffer.

/// Lagre og last inn data i minne og diskbuffer
sluttklasse CacheService {
/// For å hente eller laste inn data i minnet
  privat letminne = NSCache  ()
/// Banen url som inneholder hurtigbufrede filer (mp3-filer og bildefiler)
  privat la diskPath: URL
/// For å sjekke fil eller katalog finnes i en spesifisert bane
  private let fileManager: FileManager
/// Forsikre deg om at alle operasjoner utføres serielt
  private let serialQueue = DispatchQueue (etikett: "Oppskrifter")
init (fileManager: FileManager = FileManager.default) {
    self.fileManager = fileManager
    gjør {
      la documentDirectory = prøv fileManager.url (
        for: .documentDirectory,
        i: .userDomainMask,
        passendeFor: null,
        opprette: sant
      )
      diskPath = documentDirectory.appendingPathComponent ("Oppskrifter")
      prøv createDirectoryIfNeeded ()
    } å fange {
      fatal feil()
    }
  }
func save (data: Data, nøkkel: String, fullføring: (() -> Void)? = null) {
    la tast = MD5 (nøkkel)
serialQueue.async {
      self.memory.setObject (data som NSData, forKey: tast som NSString)
      gjør {
        prøv data.write (til: self.filePath (key: key))
        ferdigstillelse? ()
      } å fange {
        print (feil)
      }
    }
  }
}

For å unngå misformede og veldig lange filnavn, kan vi hasj dem. Jeg bruker MD5 fra SwiftHash, noe som gir død enkel bruk-tast = MD5 (nøkkel).

Hvordan teste Cache

Siden jeg designer Cache-operasjoner for å være asynkrone, må vi bruke testforventning. Husk å tilbakestille tilstanden før hver test, slik at den forrige testtilstanden ikke forstyrrer gjeldende test. Forventningen i XCTestCase gjør testing av asynkron kode enklere enn noen gang.

class CacheServiceTests: XCTestCase {
  la service = CacheService ()
overstyre func setUp () {
    super.setUp ()
prøve? service.clear ()
  }
func testClear () {
    la forventning = self.expectation (beskrivelse: #function)
    la streng = "Hallo verden"
    la data = string.data (bruker: .utf8)!
service.save (data: data, key: "key", fullføring: {
      prøve? self.service.clear ()
      self.service.load (nøkkel: "nøkkel", fullføring: {
        XCTAssertNil ($ 0)
        expectation.fulfill ()
      })
    })
vent (for: [forventning], timeout: 1)
  }
}

Laster inn eksterne bilder

Jeg bidrar også til Imaginary så jeg vet litt om hvordan det fungerer. For eksterne bilder må vi laste ned og cache, og hurtigbuffertasten er vanligvis URL-en til det eksterne bildet.

La oss bygge en enkel ImageService basert på vår NetworkService og CacheService i den mottatte appen vår. I utgangspunktet er et bilde bare en nettverksressurs som vi laster ned og cacher. Vi foretrekker sammensetning, så vi inkluderer NetworkService og CacheService i ImageService.

/// Kontroller lokal cache, og hent eksternt bilde
sluttklasse ImageService {
private let networkService: Networking
  privat la cacheService: CacheService
  privat var oppgave: URLSessionTask?
init (networkService: Networking, cacheService: CacheService) {
    self.networkService = nettverkService
    self.cacheService = cacheService
  }
}

Vi har vanligvis UICollectionViewand UITableView-celler med UIImageView. Og siden celler gjenbrukes, må vi avbryte alle eksisterende forespørsleroppgaver før vi sender en ny forespørsel.

func hente (url: URL, fullføring: @escaping (UIImage?) -> Void) {
  // Avbryt eventuell eksisterende oppgave
  oppgaven? .cancel ()
// Prøv å laste inn fra hurtigbufferen
  cacheService.load (nøkkel: url.absoluteString, fullføring: {[svakt selv] cache-data i
    hvis la data = cachedData, la bilde = UIImage (data: data) {
      DispatchQueue.main.async {
        komplettering (bilde)
      }
    } annet {
      // Forsøk å be om fra nettverket
      la ressurs = ressurs (url: url)
      self? .task = self? .networkService.fetch (ressurs: ressurs, fullføring: {nettverksdata i
        hvis la data = nettverksdata, la bilde = UIImage (data: data) {
          // Lagre i hurtigbufferen
          self? .cacheService.save (data: data, nøkkel: url.absoluteString)
          DispatchQueue.main.async {
            komplettering (bilde)
          }
        } annet {
          print ("Feil ved lasting av bilde på \ (url)")
        }
      })
selv? formålet uttrykt? .resume ()
    }
  })
}

Gjør bildelasting mer praktisk for UIImageView

La oss legge til en utvidelse til UIImageView for å angi det eksterne bildet fra nettadressen. Jeg bruker tilknyttet objekt for å beholde denne ImageService og for å kansellere gamle forespørsler. Vi benytter godt av tilknyttet objekt for å knytte ImageService til UIImageView. Poenget er å avbryte den nåværende forespørselen når forespørselen utløses igjen. Dette er nyttig når bildevisningene gjengis i en rulleliste.

utvidelse UIImageView {
  func setImage (url: URL, plassholder: UIImage? = null) {
    hvis imageService == null {
      imageService = ImageService (networkService: NetworkService (), cacheService: CacheService ())
    }
self.image = plassholder
    self.imageService? .fetch (url: url, fullføring: {[svakt selv] -bilde i
      self? .image = image
    })
  }
privat var imageService: ImageService? {
    få {
      return objc_getAssociatedObject (self, & AssociateKey.imageService) som? ImageService
    }
    sett {
      objc_setAssociatedObject (
        selv,
        & AssociateKey.imageService,
        nyVerdi,
        objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC
      )
    }
  }
}

Generisk datakilde for UITableView og UICollectionView

Vi bruker UITableView og UICollectionView i nesten hver eneste app og utfører nesten samme ting gjentatte ganger.

  • vis oppdateringskontroll under lasting
  • last inn listen på nytt i tilfelle data
  • vis feil i tilfelle feil.

Det er mange innpakninger rundt UITableView og UICollection. Hver legger til et annet lag med abstraksjon, som gir oss mer kraft, men bruker begrensninger på samme tid.

I denne appen bruker jeg Adapter for å få en generisk datakilde, for å lage en type sikker samling. For til slutt alt vi trenger er å kartlegge fra modellen til cellene.

Jeg bruker også Upstream basert på denne ideen. Det er vanskelig å vikle rundt UITableView og UICollectionView, så mange ganger er det appspesifikt, så en tynn innpakning som Adapter er nok.

sluttklasse Adapter : NSObject,
UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
  var elementer: [T] = []
  var konfigurere: ((T, Cell) -> Void)?
  var velg: ((T) -> Gyldig)?
  var cellHeight: CGFloat = 60
}

Kontroller og visning

Jeg grøftet Storyboard på grunn av mange begrensninger og mange problemer. I stedet bruker jeg kode for å lage visninger og definere begrensninger. Det er ikke så vanskelig å følge. Det meste av kjeleplatkoden i UIViewController er for å lage visninger og konfigurere oppsettet. La oss flytte dem til utsikten. Du kan lese mer om det her.

/// Brukes til å skille mellom kontroller og visning
klasse BaseController : UIViewController {
  la rot = T ()
overstyre func loadView () {
    visning = rot
  }
}
sluttklasse RecipeDetailViewController: BaseController  {}

Håndtere ansvar med en barnevisningskontroller

View controller-beholderen er et kraftig konsept. Hver visningskontroller har en bekymringsseparasjon og kan komponeres sammen for å lage avanserte funksjoner. Jeg har brukt RecipeListViewController for å administrere UICollectionView og vise en liste med oppskrifter.

sluttklasse RecipeListViewController: UIViewController {
  privat (sett) var collectionView: UICollectionView!
  la adapter = Adapter  ()
  private let emptyView = EmptyView (tekst: "Ingen oppskrifter funnet!")
}

Det er HomeViewController som legger inn denne RecipeListViewController

/// Vis en liste med oppskrifter
sluttklasse HomeViewController: UIViewController {
/// Når en oppskrift blir valgt, velg
  var velg: ((Oppskrift) -> Gyldig)?
privat var refreshControl = UIRefreshControl ()
  private let recipesService: RecipesService
  private la searchComponent: SearchComponent
  private let recipeListViewController = RecipeListViewController ()
}

Sammensetning og avhengighetsinjeksjon

Jeg prøver å bygge komponenter og komponere kode når jeg kan. Vi ser at ImageService benytter seg av NetworkService og CacheService, og RecipeDetailViewController benytter seg av Recipe and RecipesService

Ideelt sett skal ikke objekter skape avhengigheter av seg selv. Avhengighetene skal opprettes utenfor og føres fra roten. I appen vår er roten AppDelegate og AppFlowController så avhengigheter bør starte herfra.

App Transportsikkerhet

Siden iOS 9, bør alle appene ta i bruk App Transport Security

App Transport Security (ATS) håndhever beste fremgangsmåter i sikre forbindelser mellom en app og baksiden. ATS forhindrer utilsiktet avsløring, gir sikker standardatferd og er enkel å adoptere; den er også på som standard i iOS 9 og OS X v10.11. Du bør adoptere ATS så snart som mulig, uavhengig av om du oppretter en ny app eller oppdaterer en eksisterende.

I appen vår oppnås noen bilder via en HTTP-forbindelse. Vi må ekskludere den fra sikkerhetsregelen, men bare for det domenet.

 NSAppTransportSecurity 

   NSExceptionDomains 
  
     food2fork.com 
    
       NSIncludesSubdomains 
      
       NSExceptionAllowsInsecureHTTPLoads 
      
    
  

En tilpasset rullbar visning

For detaljskjermbildet kan vi bruke UITableView og UICollectionView med forskjellige celletyper. Her skal visningene være statiske. Vi kan stable ved hjelp av UIStackView. For mer fleksibilitet kan vi bare bruke UIScrollView.

/// Vertikal layoutvisning ved hjelp av Auto Layout i UIScrollView
sluttklasse ScrollableView: UIView {
  private let scrollView = UIScrollView ()
  private let contentView = UIView ()
overstyre init (ramme: CGRect) {
    super.init (ramme: ramme)
scrollView.showsHorizontalScrollIndicator = falsk
    scrollView.alwaysBounceHorizontal = falsk
    addSubview (scrollView)
scrollView.addSubview (contentView)
NSLayoutConstraint.activate ([
      scrollView.topAnchor.constraint (likeTo: topAnchor),
      scrollView.bottomAnchor.constraint (likeTo: bottomAnchor),
      scrollView.leftAnchor.constraint (likeTo: leftAnchor),
      scrollView.rightAnchor.constraint (likeTo: rightAnchor),
contentView.topAnchor.constraint (likeTo: scrollView.topAnchor),
      contentView.bottomAnchor.constraint (likeTo: scrollView.bottomAnchor),
      contentView.leftAnchor.constraint (likeTo: leftAnchor),
      contentView.rightAnchor.constraint (likeTo: rightAnchor)
    ])
  }
}

Vi fester UIScrollView til kantene. Vi fester contentView venstre og høyre anker til meg selv, mens vi fester contentView topp- og bunnanker til UIScrollView.

Visningene i contentView har øvre og nedre begrensninger, så når de utvides utvider de også contentView. UIScrollView bruker Auto Layout-informasjon fra dette contentView for å bestemme innholdets størrelse. Slik brukes ScrollableView i RecipeDetailView.

scrollableView.setup (par: [
  ScrollableView.Par (visning: imageView, innsats: UIEdgeInsets (øverst: 8, venstre: 0, nederst: 0, høyre: 0)),
  ScrollableView.Par (visning: ingrediensHeaderView, innsats: UIEdgeInsets (øverst: 8, venstre: 0, nederst: 0, høyre: 0)),
  ScrollableView.Pair (visning: ingrediensLabel, innsats: UIEdgeInsets (øverst: 4, venstre: 8, nederst: 0, høyre: 0)),
  ScrollableView.Par (visning: infoHeaderView, innsats: UIEdgeInsets (øverst: 4, venstre: 0, nederst: 0, høyre: 0)),
  ScrollableView.Par (visning: instruksjonButton, innsats: UIEdgeInsets (øverst: 8, venstre: 20, nederst: 0, høyre: 20)),
  ScrollableView.Par (visning: originalButton, innsats: UIEdgeInsets (øverst: 8, venstre: 20, nederst: 0, høyre: 20)),
  ScrollableView.Par (visning: infoView, innsats: UIEdgeInsets (øverst: 16, venstre: 0, nederst: 20, høyre: 0))
])

Legger til søkefunksjonalitet

Fra iOS 8 og utover kan vi bruke UISearchController for å få en standard søkeopplevelse med søkefeltet og resultatkontrolleren. Vi innkapsler søkefunksjonaliteten i SearchComponent slik at den kan kobles til.

final class SearchComponent: NSObject, UISearchResultsUpdating, UISearchBarDelegate {
  la oppskrifter: Oppskrifter
  la searchController: UISearchController
  let recipeListViewController = RecipeListViewController ()
}

Fra iOS 11 er det en eiendom som heter searchController på UINavigationItem, noe som gjør det enkelt å vise søkefeltet på navigasjonsfeltet.

func add (til viewController: UIViewController) {
  hvis # tilgjengelig (iOS 11, *) {
    viewController.navigationItem.searchController = searchController
    viewController.navigationItem.hidesSearchBarWhenScrolling = falsk
  } annet {
    viewController.navigationItem.titleView = searchController.searchBar
  }
viewController.definesPresentationContext = true
}

I denne appen må vi deaktivere huderNavigationBarDuringPresentation for nå, siden det er ganske avlyttende. Forhåpentligvis blir det løst i fremtidige iOS-oppdateringer.

Forstå presentasjonskontekst

Å forstå presentasjonskontekst er avgjørende for presentasjon av visningskontroller. I søk bruker vi searchResultsController.

self.searchController = UISearchController (searchResultsController: oppskriftListViewController)

Vi må bruke defininesPresentationContext på kildevisningskontrolleren (visningskontrolleren der vi legger søkefeltet inn). Uten dette får vi searchResultsController til å bli presentert over fullskjerm !!!

Når du bruker gjeldendeContext- eller overCurrentContext-stil for å presentere en visningskontroller, kontrollerer denne egenskapen hvilken eksisterende visningskontroller i visningskontrollhierarkiet som faktisk dekkes av det nye innholdet. Når en kontekstbasert presentasjon oppstår, starter UIKit ved den nåværende visningskontrolleren og går opp i visningskontrollhierarkiet. Hvis den finner en visningskontroller hvis verdi for denne egenskapen er sann, ber den visningskontrolleren presentere den nye visningskontrolleren. Hvis ingen visningskontroller definerer presentasjonskonteksten, ber UIKit vinduets rotvisningskontroller om å håndtere presentasjonen.
Standardverdien for denne egenskapen er falsk. Noen systemtilførte visningskontrollere, for eksempel UINavigationController, endrer standardverdien til true.

Fremsetter søkehandlinger

Vi skal ikke utføre søkeforespørsler for hvert tasteslag brukeren skriver i søkefeltet. Derfor er det nødvendig med en slags throttling. Vi kan bruke DispatchWorkItem til å innkapsle handlingen og sende den til køen. Senere kan vi avbryte det.

final class Debouncer {
  privat la forsinkelse: TimeInterval
  private var workItem: DispatchWorkItem?
init (forsinkelse: TimeInterval) {
    self.delay = forsinkelse
  }
/// Utløs handlingen etter en viss forsinkelse
  func timeplan (action: @escaping () -> Void) {
    workItem? .cancel ()
    workItem = DispatchWorkItem (blokk: handling)
    DispatchQueue.main.asyncAfter (tidsfrist:. Nå () + forsinkelse, utfør: workItem!)
  }
}

Testing av debouncing med omvendt forventning

For å teste Debouncer kan vi bruke XCTest forventning i invertert modus. Les mer om det i Unit testing asynchronous Swift code.

For å sjekke at en situasjon ikke oppstår under testing, oppretter du en forventning som oppfylles når den uventede situasjonen oppstår, og setter dens inverterte eiendom til sann. Testen din vil mislykkes umiddelbart hvis den omvendte forventningen er oppfylt.
klasse DebouncerTests: XCTestCase {
  func testDebouncing () {
    la cancelExpectation = self.expectation (beskrivelse: "avbryt")
    cancelExpectation.isInverted = true
la completeExpectation = self.expectation (beskrivelse: "fullstendig")
    la debouncer = Debouncer (forsinkelse: 0.3)
debouncer.schedule {
      cancelExpectation.fulfill ()
    }
debouncer.schedule {
      completeExpectation.fulfill ()
    }
vent (for: [avbryt forventning, fullstendig forventning], tidsavbrudd: 1)
  }
}

Testing av brukergrensesnitt med UITests

Noen ganger kan liten refactoring ha stor effekt. En deaktivert knapp kan føre til ubrukelige skjermer etterpå. UITest hjelper deg med å sikre integritet og funksjonelle aspekter av appen. Test skal være erklærende. Vi kan bruke robotmønsteret.

klasse OppskrifterUITests: XCTestCase {
  var app: XCUIA-applikasjon!
  overstyre func setUp () {
    super.setUp ()
    continueAfterFailure = falsk
    app = XCUIA-applikasjon ()
  }
  func testScrolling () {
    app.launch ()
    la collectionView = app.collectionViews.element (boundBy: 0)
    collectionView.swipeUp ()
    collectionView.swipeUp ()
  }
  func testGoToDetail () {
    app.launch ()
    la collectionView = app.collectionViews.element (boundBy: 0)
    la firstCell = collectionView.cells.element (boundBy: 0)
    firstCell.tap ()
  }
}

Her er noen av artiklene mine angående testing.

  • Kjører UITests med Facebook-pålogging i iOS
  • Testing i hurtig med gitt når mønster

Hovedtrådbeskyttelse

Å få tilgang til brukergrensesnittet fra bakgrunnskøen kan føre til potensielle problemer. Tidligere trengte jeg å bruke MainThreadGuard, nå som Xcode 9 har Main Thread Checker, aktiverte jeg nettopp det i Xcode.

Hovedtrådkontrollen er et frittstående verktøy for Swift og C-språk som oppdager ugyldig bruk av AppKit, UIKit og andre API-er på en bakgrunnstråd. Å oppdatere brukergrensesnittet på en annen tråd enn hovedtråden er en vanlig feil som kan resultere i tapte brukeroppdateringer, visuelle feil, datakorrupsjoner og krasjer.

Måling av forestillinger og problemstillinger

Vi kan bruke instrumenter til å profilere appen grundig. For rask måling kan vi gå over til Debug Navigator-fanen og se CPU-, minne- og nettverksbruk. Sjekk ut denne kule artikkelen for å lære mer om instrumenter.

Prototyping med lekeplass

Lekeplass er den anbefalte måten å prototype og bygge apper på. På WWDC 2018 introduserte Apple Create ML som støtter Playground for å trene modell. Sjekk ut denne kule artikkelen for å lære mer om lekeplassdrevet utvikling i Swift.

Hvor du skal dra herfra

Takk for at du har klart det så langt. Jeg håper du har lært noe nyttig. Den beste måten å lære noe på er bare å gjøre det. Hvis du tilfeldigvis skriver den samme koden igjen og igjen, lager du den som en komponent. Hvis et problem gir deg vanskelig, kan du skrive om det. Del opplevelsen din med verden, du vil lære mye.

Jeg anbefaler å sjekke artikkelen Beste steder å lære iOS-utvikling for å lære mer om iOS-utvikling.

Hvis du har spørsmål, kommentarer eller tilbakemeldinger, ikke glem å legge dem til i kommentarene. Og hvis du syntes denne artikkelen var nyttig, ikke glem å klappe.

Hvis du liker dette innlegget, kan du vurdere å besøke de andre artiklene og appene mine