Lær iOS bedste praksis ved at oprette en simpel opskriftsapp

Kilde: ChefStep

Indholdsfortegnelse

  • Kom godt i gang
  • Xcode og Swift version
  • Minimum iOS-version til support
  • Organisering af Xcode-projektet
  • Opskrifter app struktur
  • Kodekonventioner
  • Dokumentation
  • Markering af kodesektioner
  • Kildekontrol
  • Afhængigheder
  • At komme ind i projektet
  • API
  • Start skærm
  • App-ikon
  • Foringskode med SwiftLint
  • Type-sikker ressource
  • Vis mig koden
  • Design af modellen
  • Bedre navigation med FlowController
  • Auto-layout
  • Arkitektur
  • Massiv visningskontroller
  • Adgangskontrol
  • Lazy egenskaber
  • Kodestykker
  • Netværk
  • Sådan testes netværkskode
  • Implementering af cache til offline support
  • Sådan testes cache
  • Indlæser eksterne billeder
  • Gør billedindlæsning mere praktisk til UIImageView
  • Generisk datakilde til UITableView og UICollectionView
  • Kontrol og visning
  • Håndtering af ansvar med en barnesynscontroller
  • Injektion af sammensætning og afhængighed
  • App Transportsikkerhed
  • En brugerdefineret rullbar visning
  • Tilføjelse af søgefunktionalitet
  • Forståelse af præsentationskontekst
  • Afvisning af søgefunktioner
  • Test af debouncing med omvendt forventning
  • Test af brugergrænseflade med UITests
  • Hovedtrådbeskyttelse
  • Måling af forestillinger og emner
  • Prototype med legeplads
  • Hvor man skal hen herfra

Jeg startede iOS-udvikling, da iOS 7 var blevet annonceret. Og jeg har lært lidt gennem arbejde råd fra kolleger og iOS-samfundet.

I denne artikel vil jeg gerne dele en masse god praksis ved at tage eksemplet på en simpel opskriftsapp. Kildekoden findes på GitHub-opskrifter.

Appen er en traditionel masterdetaljer-applikation, der viser en liste med opskrifter sammen med deres detaljerede oplysninger.

Der er tusinder af måder at løse et problem på, og den måde, et problem løses, afhænger også af personlig smag. Forhåbentlig lærer du gennem denne artikel noget nyttigt - jeg lærte meget, da jeg gjorde dette projekt.

Jeg har tilføjet links til nogle nøgleord, hvor jeg mente, at yderligere læsning ville være fordelagtigt. Så bestemt tjek dem ud. Enhver feedback er velkommen.

Så lad os komme i gang ...

Her er en oversigt på højt niveau af, hvad du skal bygge.

Kom godt i gang

Lad os beslutte det værktøj og projektindstillinger, som vi bruger.

Xcode og Swift version

På WWDC 2018 introducerede Apple Xcode 10 med Swift 4.2. Imidlertid er Xcode 10 i skrivende stund stadig i beta 5. Så lad os holde os til den stabile Xcode 9 og Swift 4.1. Xcode 4.2 har nogle seje funktioner - du kan lege med den gennem denne fantastiske legeplads. Det introducerer ikke store ændringer fra Swift 4.1, så vi kan nemt opdatere vores app i den nærmeste fremtid, hvis det kræves.

Du skal indstille Swift-versionen i projektindstillingen i stedet for målindstillingerne. Dette betyder, at alle mål i projektet har den samme Swift-version (4.1).

Minimum iOS-version til support

Fra sommeren 2018 er iOS 12 i offentlig beta 5, og vi kan ikke målrette iOS 12 uden Xcode 10. I dette indlæg bruger vi Xcode 9, og basis-SDK er iOS 11. Afhængigt af krav og brugerbaser, nogle apps har brug for at understøtte gamle iOS-versioner. Selvom iOS-brugere har en tendens til at indføre nye iOS-versioner hurtigere end dem, der bruger Android, er der nogle, der forbliver hos gamle versioner. I henhold til Apples råd skal vi støtte de to seneste versioner, som er iOS 10 og iOS 11. Som målt i App Store den 31. maj 2018, er det kun 5% af brugerne, der bruger iOS 9 og tidligere.

Målretning af nye iOS-versioner betyder, at vi kan drage fordel af nye SDK'er, som Apple-ingeniører forbedrer hvert år. Apple-udviklerwebstedet har en forbedret visning af ændringsloggen. Nu er det lettere at se, hvad der er tilføjet eller ændret.

For at bestemme, hvornår support til gamle iOS-versioner skal slettes, skal vi ideelt analysere, hvordan brugere bruger vores app.

Organisering af Xcode-projektet

Når vi opretter det nye projekt, skal du vælge både "Inkluder enhedstests" og "Inkludere UI-tests", da det er en anbefalet praksis at skrive prøver tidligt. De seneste ændringer af XCTest-rammen, især i UI-test, gør testning til en leg og er temmelig stabil.

Før du tilføjer nye filer til projektet, skal du tage en pause og tænke over strukturen på din app. Hvordan vil vi organisere filerne? Vi har et par muligheder. Vi kan organisere filer efter funktion / modul eller rolle / typer. Hver har sine fordele og ulemper, og jeg vil diskutere dem nedenfor.

Efter rolle / type:

  • Fordele: Der er mindre tænkning involveret, hvor man skal placere filer. Det er også lettere at anvende scripts eller filtre.
  • Ulemper: Det er svært at korrelere, hvis vi ønsker at finde flere filer relateret til den samme funktion. Det vil også tage tid at omorganisere filer, hvis vi ønsker at gøre dem til genanvendelige komponenter i fremtiden.

Efter funktion / modul

  • Fordele: Det gør alt modulært og tilskynder til komposition.
  • Ulemper: Det kan blive rodet, når mange filer af forskellige typer er samlet sammen.

Forbliver modulopbygget

Personligt prøver jeg at organisere min kode efter funktioner / komponenter så meget som muligt. Dette gør det lettere at identificere den relaterede kode, der skal rettes, og at tilføje nye funktioner lettere i fremtiden. Det svarer på spørgsmålet "Hvad gør denne app?" I stedet for "Hvad er denne fil?" Her er en god artikel vedrørende dette.

En god tommelfingerregel er at forblive konsistent, uanset hvilken struktur du vælger.

Opskrifter app struktur

Følgende er den appstruktur, som vores opskriftsapp bruger:

Kilde

Indeholder kildekodefiler, opdelt i komponenter:

  • Funktioner: de vigtigste funktioner i appen
  • Hjem: startskærmen, der viser en liste med opskrifter og en åben søgning
  • Liste: viser en liste med opskrifter, inklusive genindlæsning af en opskrift og viser en tom visning, når en opskrift ikke findes
  • Søgning: håndter søgning og afvisning
  • Detalje: viser detaljerede oplysninger

Bibliotek

Indeholder kernekomponenterne i vores applikation:

  • Flow: indeholder FlowController til styring af strømme
  • Adapter: generisk datakilde til UICollectionView
  • Udvidelse: praktiske udvidelser til almindelige operationer
  • Model: Modellen i appen, analyseret fra JSON

ressource

Indeholder plist-, ressource- og Storyboard-filer.

Kodekonventioner

Jeg er enig med de fleste af stilguiderne i raywenderlich / swift-style-guide og github / swift-style-guide. Disse er enkle og rimelige at bruge i et Swift-projekt. Se også de officielle API-designretningslinjer foretaget af Swift-teamet hos Apple om, hvordan man skriver bedre Swift-kode.

Uanset hvilken stilguide du vælger at følge, skal kodeklarhed være dit vigtigste mål.

Indrykket og tab-space-krigen er et følsomt emne, men igen afhænger det af smag. Jeg bruger fire mellemrum indrykkelse i Android-projekter og to mellemrum i iOS og React. I denne Opskrifts-app følger jeg konsekvent og let at begrunde indrykning, som jeg har skrevet om her og her.

Dokumentation

God kode skal forklare sig selv tydeligt, så du ikke behøver at skrive kommentarer. Hvis det er svært at forstå en del af koden, er det godt at tage en pause og refaktorere den til nogle metoder med beskrivende navne, så det er kodenummeret er mere klar at forstå. Jeg synes dog, at dokumentation af klasser og metoder også er godt for dine kolleger og fremtidige selv. I henhold til Swift API-designretningslinjer

Skriv en dokumentationskommentar for hver erklæring. Indblik, der opnås ved at skrive dokumentation, kan have en dybtgående indflydelse på dit design, så lad det ikke komme af.

Det er meget let at generere kommentarskabelon /// i Xcode med Cmd + Alt + /. Hvis du planlægger at refaktorere din kode til en ramme, der skal deles med andre i fremtiden, kan værktøjer som jazzy generere dokumentation, så andre kan følge med.

Markering af kodesektioner

Brug af MARK kan være nyttigt til at adskille kodesektioner. Det grupperer også funktioner fint i navigationslinjen. Du kan også bruge udvidelsesgrupper, relaterede egenskaber og metoder.

For en simpel UIViewController kan vi muligvis definere følgende MARK'er:

// MARK: - Init
// MARK: - Se livscyklus
// MARK: - Opsætning
// MÆRK: - Handling
// MARK: - Data

Kildekontrol

Git er et populært kildekontrolsystem lige nu. Vi kan bruge skabelonen .gitignore-fil fra gitignore.io/api/swift. Der er både fordele og ulemper ved at kontrollere afhængighedsfiler (CocoaPods og Carthage). Det afhænger af dit projekt, men jeg plejer ikke at begå afhængigheder (node_moduler, Carthage, Pods) i kildekontrol for ikke at rod på kodebasen. Det gør det også lettere at gennemgå Pull-anmodninger.

Uanset om du tjekker i Pods-biblioteket, skal Podfile og Podfile.lock altid holdes under versionskontrol.

Jeg bruger både iTerm2 til at udføre kommandoer og Source Tree for at se grene og iscenesættelse.

Afhængigheder

Jeg har brugt tredjepartsrammer og også lavet og bidraget til open source meget. Brug af en ramme giver dig et løft i starten, men det kan også begrænse dig meget i fremtiden. Der kan være nogle trivielle ændringer, som det er meget svært at arbejde på. Den samme ting sker, når du bruger SDK'er. Jeg foretrækker at vælge aktive open source-rammer. Læs kildekoden og tjek rammer grundigt, og konsulter med dit team, hvis du planlægger at bruge dem. Lidt ekstra forsigtighed skader ikke.

I denne app prøver jeg at bruge så få afhængigheder som muligt. Lige nok til at demonstrere, hvordan man styrer afhængigheder. Nogle erfarne udviklere foretrækker muligvis Carthage, en afhængighedsadministrator, da det giver dig fuld kontrol. Her vælger jeg CocoaPods, fordi det er let at bruge, og det har fungeret godt indtil videre.

Der er en fil kaldet .swift-version af værdi 4.1 i roden af ​​projektet for at fortælle CocoaPods, at dette projekt bruger Swift 4.1. Dette ser simpelt ud, men det tog mig lang tid at finde ud af.

At komme ind i projektet

Lad os lave nogle lanceringsbilleder og ikoner for at give projektet et pænt look.

API

Den nemme måde at lære iOS-netværk på er gennem offentlige gratis API-tjenester. Her bruger jeg food2fork. Du kan registrere dig for en konto på http://food2fork.com/about/api. Der er mange andre fantastiske API'er i dette offentlige api-arkiv.

Det er godt at opbevare dine legitimationsoplysninger på et sikkert sted. Jeg bruger 1Password til at generere og gemme mine adgangskoder.

Før vi begynder at kode, lad os lege med API'erne for at se, hvilke slags anmodninger de har brug for, og svar, de returnerer. Jeg bruger Insomnia-værktøjet til at teste og analysere API-svar. Det er open source, gratis og fungerer godt.

Start skærm

Det første indtryk er vigtigt, det samme er startskærmen. Den foretrukne måde er at bruge LaunchScreen.storyboard i stedet for et statisk startbillede.

For at tilføje et lanceringsbillede til Asset Catalog, skal du åbne LaunchScreen.storyboard, tilføje UIImageView og fastgøre det på kanterne af UIView. Vi bør ikke fastlægge billedet til det sikre område, da vi ønsker, at billedet skal være i fuld skærm. Fjern også markering af margener i begrænsningerne til automatisk layout. Indstil indholdsmoden for UIImageView som aspektfyld, så den strækker sig med det korrekte billedformat.

Konfigurer layout i LaunchScreen.

App-ikon

En god praksis er at give alle de nødvendige appikoner til hver enhed, du understøtter, og også til steder som Underretning, Indstillinger og Springboard. Sørg for, at hvert billede ikke har nogen gennemsigtige pixels, ellers resulterer det i en sort baggrund. Dette tip kommer fra retningslinjer for menneskelig grænseflade - appikon.

Hold baggrunden enkel og undgå gennemsigtighed. Sørg for, at dit ikon er uigennemsigtigt, og lad ikke rodet komme i baggrunden. Giv det en simpel baggrund, så det ikke overgår andre appikoner i nærheden. Du behøver ikke at udfylde hele ikonet med indhold.

Vi er nødt til at designe firkantede billeder med en størrelse, der er større end 1024 x 1024, så hver er i stand til at nedskalere til mindre billeder. Du kan gøre dette i hånden, script eller bruge denne lille IconGenerator-app, som jeg lavede.

IconGenerator-appen kan generere ikoner til iOS i iPhone, iPad, macOS og watchOS-apps. Resultatet er AppIcon.appiconset, som vi kan trække lige ind i Asset Catalog. Asset Catalog er vejen til moderne Xcode-projekter.

Foringskode med SwiftLint

Uanset hvilken platform vi udvikler på, er det godt at have en længer til at håndhæve konsistente konventioner. Det mest populære værktøj til Swift-projekter er SwiftLint, lavet af de fantastiske mennesker på Realm.

For at installere det skal du tilføje pod 'SwiftLint', '~> 0,25' til Podfilen. Det er også en god praksis at specificere versionen af ​​afhængigheder, så podinstallationen ikke ved en fejltagelse opdateres til en større version, der kan ødelægge din app. Tilføj derefter en .wiftlint.yml med din foretrukne konfiguration. En prøvekonfiguration findes her.

Til sidst skal du tilføje en ny Run Script-sætning for at udføre hurtiglint efter kompilering.

Type-sikker ressource

Jeg bruger R.swift til sikkert at administrere ressourcer. Det kan generere typesikre klasser for at få adgang til font, lokaliserbare strenge og farver. Hver gang vi ændrer navne på ressourcefiler, får vi kompileringsfejl i stedet for et implicit crash. Dette forhindrer os i at aflede ressourcer, der er aktivt i brug.

imageView.image = R.image.notFound ()

Vis mig koden

Lad os dykke ned i koden, startende med modellen, flowcontrollere og serviceklasser.

Design af modellen

Det lyder måske kedeligt, men klienter er bare en pænere måde at repræsentere API-svaret på. Modellen er måske den mest basale ting, og vi bruger den meget i appen. Det spiller en så vigtig rolle, men der kan være nogle åbenlyse fejl relateret til misdannede modeller og antagelser om, hvordan en model skal analyseres, der skal overvejes.

Vi bør teste for alle modeller af appen. Ideelt set har vi brug for automatisk test af modeller fra API-svar, i tilfælde af at modellen er ændret fra backend.

Fra Swift 4.0 kan vi tilpasse vores model til Codable for let at serialisere til og fra JSON. Vores model skal være uforanderlig:

struct Opskrift: Kodelig {
  lad udgiver: String
  lad url: URL
  lad sourceUrl: String
  lad id: streng
  lad titel: String
  lad imageUrl: String
  lad socialRank: Double
  lad publisherUrl: URL
enum CodingKeys: String, CodingKey {
    sagudgiver
    sag url = "f2f_url"
    sag sourceUrl = "source_url"
    case id = "recept_id"
    sagstitel
    case imageUrl = "image_url"
    sag socialRank = "social_rank"
    case publisherUrl = "publisher_url"
  }
}

Vi kan bruge nogle testrammer, hvis du kan lide fancy syntaks eller en RSpec-stil. Nogle tredjeparts testrammer kan have problemer. Jeg finder XCTest god nok.

import XCTest
@ testable import Opskrifter
klasse Opskrifttest: XCTestCase {
  func testParsing () kaster {
    lad json: [String: Any] = [
      "udgiver": "To ærter og deres pod",
      "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/",
      "recept_id": "975e33",
      "image_url": "http://static.food2fork.com/NoBakeChocolatPeanutButterPretzelCookies44147.jpg",
      "social_rank": 99.99999999999974,
      "publisher_url": "http://www.twopeasandtheirpod.com"
    ]
lad data = prøv JSONSerialization.data (medJSONObject: json, indstillinger: [])
    lad dekoder = JSONDecoder ()
    lad opskrift = prøv dekoder.decode (Recipe.self, fra: data)
XCTAssertEqual (recept.title, "No-Bake Chokolade Jordnøddesmør Pretzel Cookies")
    XCTAssertEqual (recept.id, "975e33")
    XCTAssertEqual (recept.url, URL (streng: "http://food2fork.com/view/975e33")!)
  }
}

Bedre navigation med FlowController

Tidligere brugte jeg Compass som en routingmotor i mine projekter, men over tid har jeg fundet ud af, at det at skrive enkel routingkode fungerer.

FlowController bruges til at administrere mange UIViewController-relaterede komponenter til en fælles funktion. Det kan være nødvendigt at læse FlowController og koordinator til andre brugssager og for at få en bedre forståelse.

Der er AppFlowController, der administrerer at ændre rootViewController. For nu starter den RecipeFlowController.

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

RecipeFlowController administrerer (faktisk er det) UINavigationController, der håndterer at skubbe HomeViewController, RecipesDetailViewController, SafariViewController.

sidste klasse RecipeFlowController: UINavigationController {
  /// Start flowet
  func start () {
    let service = RecipesService (netværk: NetworkService ())
    let controller = HomeViewController (recipesService: service)
    viewControllers = [controller]
    controller.select = {[svag selv] opskrift i
      self? .startDetail (opskrift: opskrift)
    }
  }
privat func startDetail (opskrift: Opskrift) {}
  privat func startWeb (url: URL) {}
}

UIViewController kan bruge delegeret eller lukning til at underrette FlowController om ændringer eller næste skærme i flowet. For delegerede kan der være behov for at kontrollere, hvornår der er to tilfælde af samme klasse. Her bruger vi lukning for enkelhed.

Auto-layout

Auto-layout har eksisteret siden iOS 5, det bliver bedre for hvert år. Selvom nogle mennesker stadig har et problem med det, mest på grund af forvirrende brudende begrænsninger og ydeevne, men personligt synes jeg, Auto Layout er godt nok.

Jeg prøver at bruge Auto Layout så meget som muligt for at lave et adaptivt brugergrænseflade. Vi kan bruge biblioteker som Ankre til at gøre erklærende og hurtig Auto Layout. Men i denne app bruger vi bare NSLayoutAnchor, da den er fra iOS 9. Koden nedenfor er inspireret af Constraint. Husk, at Auto Layout i sin enkleste form involverer at skifte translaterAutoresizingMaskIo til Constraints og aktivere isActive begrænsninger.

udvidelse NSLayoutConstraint {
  statisk func-aktivering (_ begrænsninger: [NSLayoutConstraint]) {
    begrænsninger. For hver {
      ($ 0.firstItem som? UIView) ?. translatesAutoresizingMaskIntoConstraints = falsk
      $ 0.isActive = sandt
    }
  }
}

Der er faktisk mange andre layoutmotorer tilgængelige på GitHub. For at få en fornemmelse af, hvilken man vil være egnet til at bruge, skal du tjekke LayoutFrameworkBenchmark.

Arkitektur

Arkitektur er sandsynligvis det mest hypede og diskuterede emne. Jeg er fan af at udforske arkitekturer, du kan se flere indlæg og rammer om forskellige arkitekturer her.

For mig definerer alle arkitekturer og mønstre roller for hvert objekt, og hvordan man forbinder dem. Husk disse vejledende principper for dit valg af arkitektur:

  • indkapsle hvad der varierer
  • favoriserer sammensætning frem for arv
  • program til interface, ikke til implementering

Efter at have spillet rundt med mange forskellige arkitekturer, med og uden Rx, fandt jeg ud af, at enkel MVC er god nok. I dette enkle projekt er der bare UIViewController med logik indkapslet i hjælper Service klasser,

Massiv visningskontroller

Du har muligvis hørt folk joke om, hvor massiv UIViewController er, men i virkeligheden er der ingen massiv visningskontroller. Det er bare os, der skriver dårlig kode. Der er dog måder at slanke det på.

I opskriftsappen, som jeg bruger,

  • Service til at injicere i visningskontrolleren for at udføre en enkelt opgave
  • Generisk visning for at flytte visning og kontrollerer erklæring til visningslaget
  • Børnesynscontroller til at komponere børnesynscontrollere for at bygge flere funktioner

Her er en meget god artikel med 8 tip til slanke store controllere.

Adgangskontrol

SWIFT-dokumentationen nævner, at ”adgangskontrol begrænser adgangen til dele af din kode fra kode i andre kildefiler og moduler. Denne funktion giver dig mulighed for at skjule implementeringsdetaljerne for din kode og specificere en foretrukken grænseflade, gennem hvilken den kode kan åbnes og bruges. ”

Alt skal være privat og endeligt som standard. Dette hjælper også kompilatoren. Når vi ser en offentlig ejendom, er vi nødt til at søge efter den på tværs af projektet, før vi gør noget videre med det. Hvis ejendommen kun bruges i en klasse, betyder det at det er privat, at vi ikke behøver at pleje, hvis den går i stykker andetsteds.

Angiv ejendomme som endelige, hvor det er muligt.

sidste klasse HomeViewController: UIViewController {}

Erklær ejendomme som private eller i det mindste private (sæt).

sidste klasse RecipeDetailView: UIView {
  private let scrollableView = ScrollableView ()
  privat (sæt) doven var imageView: UIImageView = self.makeImageView ()
}

Lazy egenskaber

For egenskaber, der kan fås adgang til på et senere tidspunkt, kan vi erklære dem som dovne og kan bruge lukning til hurtig konstruktion.

sidste klasse RecipeCell: UICollectionViewCell {
  privat (sæt) doven var containerView: UIView = {
    let view = UIView ()
    view.clipsToBounds = sandt
    view.layer.cornerRadius = 5
    view.backgroundColor = Color.main.withAlphaComponent (0.4)
returvisning
  } ()
}

Vi kan også bruge make-funktioner, hvis vi planlægger at genbruge den samme funktion til flere egenskaber.

sidste klasse RecipeDetailView: UIView {
  privat (sæt) doven var imageView: UIImageView = self.makeImageView ()
privat func makeImageView () -> UIImageView {
    let imageView = UIImageView ()
    imageView.contentMode = .scaleAspectFill
    imageView.clipsToBounds = sandt
    return imageView
  }
}

Dette matcher også råd fra stræben efter flydende brug.

Begynd navne på fabriksmetoder med "make", for eksempel x.makeIterator ().

Kodestykker

Nogle kodesyntaxer er vanskelige at huske. Overvej at bruge kodestykker til automatisk generering af kode. Dette understøttes af Xcode og er den foretrukne måde af Apple-ingeniører, når de demonstrerer.

hvis #tilgængelig (iOS 11, *) {
  viewController.navigationItem.searchController = searchController
  viewController.navigationItem.hidesSearchBarWhenScrolling = false
} andet {
  viewController.navigationItem.titleView = searchController.searchBar
}

Jeg lavede en repo med nogle nyttige Swift-kodestykker, som mange nyder at bruge.

Netværk

Netværk i Swift er slags et løst problem. Der er kedelige og fejlagtige opgaver som at analysere HTTP-svar, håndtere anmodningskøer, håndtere parameterforespørgsler. Jeg har set bugs om PATCH-anmodninger, HTC-metoder med lavere karakter, ... Vi kan bare bruge Alamofire. Her er det ikke nødvendigt at spilde tid.

For denne app, da den er enkel og at undgå unødvendige afhængigheder. Vi kan bare bruge URLSession direkte. En ressource indeholder normalt URL, sti, parametre og HTTP-metoden.

struktur Ressource {
  lad url: URL
  lad sti: streng?
  lad httpMethod: String
  lad parametre: [String: String]
}

En simpel netværkstjeneste kan bare analysere Ressource til URLRequest og fortæller URLSession at udføre

sidste klasse NetworkService: Networking {
  @discardableResult func hente (ressource: Ressource, færdiggørelse: @escaping (Data?) -> Void) -> URLSessionTask? {
    guard let request = makeRequest (resource: resource) andet {
      færdiggørelse (nul)
      returnere nul
    }
lad opgave = session.dataTask (med: anmodning, komplet håndtering: {data, _, fejl i
      vagt lad data = data, fejl == intet andet {
        færdiggørelse (nul)
        Vend tilbage
      }
færdiggørelse (data)
    })
task.resume ()
    return opgave
  }
}

Brug afhængighedsinjektion. Tillad, at den, der ringer op, angiver URLSessionConfiguration. Her bruger vi Swift-standardparameteren til at give den mest almindelige mulighed.

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

Jeg bruger også URLQueryItem, som stammer fra iOS 8. Det gør, at parametre til analyse af forespørgsler er mindre og mindre trættende.

Sådan testes netværkskode

Vi kan bruge URLProtocol og URLCache til at tilføje en stub til netværkssvar, eller vi kan bruge rammer som Mockingjay, der svirker URLSessionConfiguration.

Selv foretrækker jeg at bruge protokollen til at teste. Ved at bruge protokollen kan testen oprette en mock-anmodning om at give en stub-respons.

protokol Netværk {
  @discardableResult func hente (ressource: Ressource, færdiggørelse: @escaping (Data?) -> Void) -> URLSessionTask?
}
sidste klasse MockNetworkService: Netværk {
  lad data: Data
  init (filnavn: streng) {
    let bundle = Bundle (for: MockNetworkService.self)
    lad url = bundle.url (forResource: fileName, withExtension: "json")!
    self.data = prøv! Data (contentOf: url)
  }
func hente (ressource: Ressource, færdiggørelse: @escaping (Data?) -> Void) -> URLSessionTask? {
    færdiggørelse (data)
    returnere nul
  }
}

Implementering af cache til offline support

Jeg plejede at bidrage og bruge et bibliotek, der hedder Cache meget. Det, vi har brug for fra et godt cache-bibliotek, er hukommelse og disk-cache, hukommelse til hurtig adgang, disk for vedvarende. Når vi gemmer, gemmer vi på både hukommelse og disk. Når vi indlæser, hvis hukommelsescache mislykkes, indlæses vi fra disken og opdaterer derefter hukommelsen igen. Der er mange avancerede emner om cache som rensning, udløb, adgangsfrekvens. Læs om dem her.

I denne enkle app er en hjemmekultureret cache-serviceklasse nok og en god måde at lære, hvordan cache fungerer. Alt i Swift kan konverteres til data, så vi kan bare gemme data i cache. Swift 4 Codable kan serialisere objekt til data.

Koden herunder viser os, hvordan du bruger FileManager til diskcache.

/// Gem og indlæst data i hukommelse og diskcache
slutklasse CacheService {
/// For at hente eller indlæse data i hukommelsen
  privat lethukommelse = NSCache  ()
/// Stien url, der indeholder cache-filer (mp3-filer og billedfiler)
  private let diskPath: URL
/// Til kontrol af fil eller bibliotek findes i en specificeret sti
  private let fileManager: FileManager
/// Sørg for, at al operation udføres serielt
  private let serialQueue = DispatchQueue (etiket: "Opskrifter")
init (fileManager: FileManager = FileManager.default) {
    self.fileManager = fileManager
    gør {
      let documentDirectory = prøv fileManager.url (
        til: .documentDirectory,
        i: .userDomainMask,
        passendeFor: nul,
        oprette: sandt
      )
      diskPath = documentDirectory.appendingPathComponent ("Opskrifter")
      prøv createDirectoryIfNeeded ()
    } fangst {
      fatal fejl()
    }
  }
func save (data: Data, nøgle: String, færdiggørelse: (() -> Void)? = nul) {
    lad nøgle = MD5 (nøgle)
serialQueue.async {
      self.memory.setObject (data som NSData, forKey: nøgle som NSString)
      gør {
        prøv data.write (til: self.filePath (key: key))
        færdiggørelse?()
      } fangst {
        print (fejl)
      }
    }
  }
}

For at undgå misdannede og meget lange filnavne, kan vi hash dem. Jeg bruger MD5 fra SwiftHash, som giver død simpel brug let tast = MD5 (nøgle).

Sådan testes cache

Da jeg designer Cache-operationer til at være asynkrone, er vi nødt til at bruge testforventning. Husk at nulstille tilstanden før hver test, så den forrige testtilstand ikke forstyrrer den aktuelle test. Forventningen i XCTestCase gør test af asynkron kode lettere end nogensinde.

klasse CacheServiceTests: XCTestCase {
  let service = CacheService ()
tilsidesætte func setUp () {
    super.setUp ()
prøve? service.clear ()
  }
func testClear () {
    lad forventning = self.expectation (beskrivelse: #function)
    let string = "Hej verden"
    lad data = string.data (bruger: .utf8)!
service.save (data: data, nøgle: "nøgle", færdiggørelse: {
      prøve? self.service.clear ()
      self.service.load (nøgle: "nøgle", færdiggørelse: {
        XCTAssertNil ($ 0)
        expectation.fulfill ()
      })
    })
vent (til: [forventning], timeout: 1)
  }
}

Indlæser eksterne billeder

Jeg bidrager også til Imaginary, så jeg ved lidt om, hvordan det fungerer. For fjernbilleder er vi nødt til at downloade og cache den, og cache-nøglen er normalt URL-adressen til det fjerne billede.

Lad os oprette en simpel ImageService baseret på vores NetworkService og CacheService i vores modtagelige app. Grundlæggende er et billede bare en netværksressource, som vi downloader og cache. Vi foretrækker sammensætning, så vi vil inkludere NetworkService og CacheService i ImageService.

/// Kontroller lokal cache, og hent fjernbillede
sidste klasse ImageService {
private let networkService: Netværk
  private let cacheService: CacheService
  privat var opgave: URLSessionTask?
init (netværkService: Netværk, cacheService: CacheService) {
    self.networkService = netværksservice
    self.cacheService = cacheService
  }
}

Vi har normalt UICollectionViewand UITableView-celler med UIImageView. Og da celler genbruges, er vi nødt til at annullere enhver eksisterende anmodningsopgave, inden vi fremsætter en ny anmodning.

func hentning (url: URL, færdiggørelse: @escaping (UIImage?) -> Void) {
  // Annuller eksisterende opgave, hvis nogen
  opgave? .cancel ()
// Prøv indlæsning fra cache
  cacheService.load (nøgle: url.absoluteString, færdiggørelse: {[svag selv] cacheData i
    hvis lad data = cacheedata, lad billede = UIImage (data: data) {
      DispatchQueue.main.async {
        færdiggørelse (billede)
      }
    } andet {
      // Prøv at anmode om fra netværket
      lad ressource = ressource (url: url)
      self? .task = self? .networkService.fetch (ressource: ressource, færdiggørelse: {netværksdata i
        hvis lad data = netværksData, så lad billede = UIImage (data: data) {
          // Gem i cache
          self? .cacheService.save (data: data, nøgle: url.absoluteString)
          DispatchQueue.main.async {
            færdiggørelse (billede)
          }
        } andet {
          print ("Fejl ved indlæsning af billede på \ (url)")
        }
      })
selv? .task? .resume ()
    }
  })
}

Gør billedindlæsning mere praktisk til UIImageView

Lad os tilføje en udvidelse til UIImageView for at indstille fjernbilledet fra URL'en. Jeg bruger et tilknyttet objekt til at beholde denne ImageService og for at annullere gamle anmodninger. Vi bruger god tilknyttet objekt til at knytte ImageService til UIImageView. Pointen er at annullere den aktuelle anmodning, når anmodningen udløses igen. Dette er praktisk, når billedvisningerne gengives i en rulleliste.

udvidelse UIImageView {
  func setImage (url: URL, pladsholder: UIImage? = nul) {
    hvis imageService == nul {
      imageService = ImageService (networkService: NetworkService (), cacheService: CacheService ())
    }
self.image = pladsholder
    self.imageService? .fetch (url: url, færdiggørelse: {[svagt selv] billede i
      selv? .billede = billede
    })
  }
privat var imageService: ImageService? {
    få {
      return objc_getAssociatedObject (self, & AssociateKey.imageService) som? ImageService
    }
    sæt {
      objc_setAssociatedObject (
        selv,
        & AssociateKey.imageService,
        NEWVALUE,
        objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC
      )
    }
  }
}

Generisk datakilde til UITableView og UICollectionView

Vi bruger UITableView og UICollectionView i næsten hver app og udfører næsten det samme gentagne gange.

  • vis opdateringskontrol under indlæsning
  • genindlæs listen i tilfælde af data
  • Vis fejl i tilfælde af fejl.

Der er mange indpakninger omkring UITableView og UICollection. Hver tilføjer et andet lag med abstraktion, som giver os mere magt, men anvender begrænsninger på samme tid.

I denne app bruger jeg Adapter til at få en generisk datakilde for at oprette en type sikker samling. Fordi i sidste ende alt vi har brug for er at kortlægge fra modellen til cellerne.

Jeg bruger også Upstream baseret på denne idé. Det er svært at ombryde UITableView og UICollectionView, ligesom mange gange er det appspecifikt, så en tynd indpakning som adapter er nok.

sidste klasse Adapter : NSObject,
UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
  var elementer: [T] = []
  var konfigurere: ((T, celle) -> ugyldig)?
  var vælg: ((T) -> Annulleres)?
  var cellHøjde: CGFloat = 60
}

Kontrol og visning

Jeg grøftede Storyboard på grund af mange begrænsninger og mange problemer. I stedet bruger jeg kode til at lave synspunkter og definere begrænsninger. Det er ikke så svært at følge. Det meste af kedelplade-koden i UIViewController er til at oprette visninger og konfigurere layoutet. Lad os flytte dem til visningen. Du kan læse mere om det her.

/// Bruges til at adskille mellem controller og visning
klasse BaseController : UIViewController {
  lad rod = T ()
tilsidesætte func loadView () {
    visning = rod
  }
}
sidste klasse RecipeDetailViewController: BaseController  {}

Håndtering af ansvar med en barnesynscontroller

View controller-containeren er et magtfuldt koncept. Hver visningskontroller har en adskillelse af bekymring og kan sammensættes for at skabe avancerede funktioner. Jeg har brugt RecipeListViewController til at administrere UICollectionView og vise en liste med opskrifter.

sidste klasse RecipeListViewController: UIViewController {
  privat (sæt) var collectionView: UICollectionView!
  let adapter = Adapter  ()
  private let emptyView = EmptyView (tekst: "Ingen opskrifter fundet!")
}

Der er HomeViewController, der integrerer denne RecipeListViewController

/// Vis en liste med opskrifter
sidste klasse HomeViewController: UIViewController {
/// Når du får en opskrift, vælg
  var vælg: ((Opskrift) -> Annulleres)?
privat var refreshControl = UIRefreshControl ()
  private let recipesService: RecipesService
  private let searchComponent: SearchComponent
  private let recipeListViewController = RecipeListViewController ()
}

Injektion af sammensætning og afhængighed

Jeg prøver at opbygge komponenter og komponere kode, når jeg kan. Vi ser, at ImageService gør brug af NetworkService og CacheService, og RecipeDetailViewController bruger Recipe and RecipesService

Ideelt set bør objekter ikke skabe afhængigheder af sig selv. Afhængighederne skal oprettes udenfor og sendes fra rod. I vores app er roden AppDelegate og AppFlowController, så afhængigheder bør starte herfra.

App Transportsikkerhed

Siden iOS 9 skal alle apps anvende App Transport Security

App Transport Security (ATS) håndhæver bedste praksis i de sikre forbindelser mellem en app og dens bagenden. ATS forhindrer utilsigtet videregivelse, giver sikker standardadfærd og er let at vedtage; det er også tændt som standard i iOS 9 og OS X v10.11. Du skal vedtage ATS så hurtigt som muligt, uanset om du opretter en ny app eller opdaterer en eksisterende.

I vores app opnås nogle billeder via en HTTP-forbindelse. Vi er nødt til at ekskludere den fra sikkerhedsreglen, men kun for dette domæne.

 NSAppTransportSecurity 

   NSExceptionDomains 
  
     food2fork.com 
    
       NSIncludesSubdomains 
      
       NSExceptionAllowsInsecureHTTPLoads 
      
    
  

En brugerdefineret rullbar visning

For detaljeskærmen kan vi bruge UITableView og UICollectionView med forskellige celletyper. Her skal visningerne være statiske. Vi kan stakke ved hjælp af UIStackView. For mere fleksibilitet kan vi bare bruge UIScrollView.

/// Lodret layoutvisning ved hjælp af Auto Layout i UIScrollView
sidste klasse ScrollableView: UIView {
  private let scrollView = UIScrollView ()
  private let contentView = UIView ()
tilsidesætte init (ramme: CGRect) {
    super.init (ramme: ramme)
scrollView.showsHorizontalScrollIndicator = falsk
    scrollView.alwaysBounceHorizontal = falsk
    addSubview (scrollView)
scrollView.addSubview (contentView)
NSLayoutConstraint.activate ([
      scrollView.topAnchor.constraint (lige til: topAnchor),
      scrollView.bottomAnchor.constraint (ligeTo: bottomAnchor),
      scrollView.leftAnchor.constraint (lige til: leftAnchor),
      scrollView.rightAnchor.constraint (ligeTo: rightAnchor),
contentView.topAnchor.constraint (lige til: scrollView.topAnchor),
      contentView.bottomAnchor.constraint (lige til: scrollView.bottomAnchor),
      contentView.leftAnchor.constraint (lige til: leftAnchor),
      contentView.rightAnchor.constraint (ligeTo: rightAnchor)
    ])
  }
}

Vi fastgør UIScrollView til kanterne. Vi fastgør indholdsvisningen til venstre og højre anker til mig selv, mens vi fastgør indholdet Vis øverste og nederste anker til UIScrollView.

Visningerne i contentView har øvre og nederste begrænsninger, så når de udvides, udvider de også contentView. UIScrollView bruger Auto Layout-oplysninger fra dette contentView til at bestemme dets indholdsstørrelse. Sådan bruges ScrollableView i RecipeDetailView.

scrollableView.setup (par: [
  ScrollableView.Pair (visning: imageView, indsættelse: UIEdgeInsets (øverst: 8, venstre: 0, nederst: 0, højre: 0)),
  ScrollableView.Pair (visning: ingrediensHeaderView, indsættelse: UIEdgeInsets (øverst: 8, venstre: 0, bund: 0, højre: 0)),
  ScrollableView.Pair (visning: ingrediensLabel, indsættelse: UIEdgeInsets (øverst: 4, venstre: 8, bund: 0, højre: 0)),
  ScrollableView.Pair (visning: infoHeaderView, indsættelse: UIEdgeInsets (øverst: 4, venstre: 0, bund: 0, højre: 0)),
  ScrollableView.Pair (visning: instruction Button, inset: UIEdgeInsets (øverst: 8, venstre: 20, bund: 0, højre: 20)),
  ScrollableView.Pair (visning: originalButton, indsættelse: UIEdgeInsets (øverst: 8, venstre: 20, bund: 0, højre: 20)),
  ScrollableView.Pair (visning: infoView, indsættelse: UIEdgeInsets (øverst: 16, venstre: 0, bund: 20, højre: 0))
])

Tilføjelse af søgefunktionalitet

Fra iOS 8 og fremover kan vi bruge UISearchController til at få en standard søgeoplevelse med søgefeltet og resultatorenheden. Vi indkapsler søgefunktionalitet i SearchComponent, så den kan være tilsluttet.

final class SearchComponent: NSObject, UISearchResultsUpdating, UISearchBarDelegate {
  lad opskrifterService: RecipesService
  lad searchController: UISearchController
  let recipeListViewController = RecipeListViewController ()
}

Fra iOS 11 er der en egenskab kaldet searchController på UINavigationItem, hvilket gør det nemt at vise søgefeltet på navigationslinjen.

func tilføj (til viewController: UIViewController) {
  hvis #tilgængelig (iOS 11, *) {
    viewController.navigationItem.searchController = searchController
    viewController.navigationItem.hidesSearchBarWhenScrolling = false
  } andet {
    viewController.navigationItem.titleView = searchController.searchBar
  }
viewController.definesPresentationContext = true
}

I denne app er vi nødt til at deaktivere huderNavigationBarDuringPresentation i øjeblikket, da det er ret buggy. Forhåbentlig bliver det løst i fremtidige iOS-opdateringer.

Forståelse af præsentationskontekst

At forstå præsentationskontekst er afgørende for visning af controller-præsentation. I søgning bruger vi searchResultsController.

self.searchController = UISearchController (searchResultsController: receptListViewController)

Vi er nødt til at bruge defininesPresentationContext på kildevisningskontrolleren (visningskontrolleren, hvor vi tilføjer søgefeltet til). Uden dette får vi searchResultsController til at blive præsenteret på fuld skærm !!!

Når du bruger aktuelleContext- eller overCurrentContext-stil til at præsentere en visningskontroller, kontrollerer denne egenskab, hvilken eksisterende visningskontroller i dit visningskontrolhierarki der faktisk dækkes af det nye indhold. Når der sker en kontekstbaseret præsentation, starter UIKit ved den præsenterende visningskontroller og går op i visningskontrolhierarkiet. Hvis den finder en visningskontroller, hvis værdi for denne egenskab er sand, beder den den visningscontroller om at præsentere den nye visningskontroller. Hvis ingen visningskontroller definerer præsentationskonteksten, beder UIKit vinduet om rodvisningscontroller om at håndtere præsentationen.
Standardværdien for denne egenskab er falsk. Nogle systembehandlede visningskontrollere, såsom UINavigationController, ændrer standardværdien til sand.

Afvisning af søgefunktioner

Vi skal ikke udføre søgeanmodninger for hvert tasteslag, som brugeren skriver i søgefeltet. Derfor er der brug for en slags throttling. Vi kan bruge DispatchWorkItem til at indkapsle handlingen og sende den til køen. Senere kan vi annullere det.

sidste klasse Debouncer {
  privat letforsinkelse: TimeInterval
  privat var workItem: DispatchWorkItem?
init (forsinkelse: TimeInterval) {
    self.delay = forsinkelse
  }
/// Udløs handlingen efter en vis forsinkelse
  func tidsplan (handling: @escaping () -> Void) {
    workItem? .cancel ()
    workItem = DispatchWorkItem (blok: handling)
    DispatchQueue.main.asyncAfter (deadline:. Nu () + forsinkelse, udfør: workItem!)
  }
}

Test af debouncing med omvendt forventning

For at teste Debouncer kan vi bruge XCTest-forventning i inverteret tilstand. Læs mere om det i Enhedstestning af asynkron Swift-kode.

For at kontrollere, at en situation ikke opstår under test, skal du oprette en forventning, der er opfyldt, når den uventede situation opstår, og indstil dens isInverterede egenskab til sand. Din test mislykkes øjeblikkeligt, hvis den omvendte forventning er opfyldt.
klasse DebouncerTests: XCTestCase {
  func testDebouncing () {
    lad cancelExpectation = self.expectation (beskrivelse: "annullere")
    cancelExpectation.isInverted = sandt
lad completeExpectation = self.expectation (beskrivelse: "complete")
    lad debouncer = Debouncer (forsinkelse: 0.3)
debouncer.schedule {
      cancelExpectation.fulfill ()
    }
debouncer.schedule {
      completeExpectation.fulfill ()
    }
vent (til: [annullere forventning, komplet forventning], timeout: 1)
  }
}

Test af brugergrænseflade med UITests

Undertiden kan små refactoring have en stor effekt. En deaktiveret knap kan føre til ubrugelige skærme bagefter. UITest hjælper med at sikre integritet og funktionelle aspekter af appen. Test skal være erklærende. Vi kan bruge robotmønsteret.

klasse OpskrifterUITests: XCTestCase {
  var app: XCUIAansøgning!
  tilsidesætte func setUp () {
    super.setUp ()
    continueAfterFailure = falsk
    app = XCUIA-applikation ()
  }
  func testScrolling () {
    app.launch ()
    lad collectionView = app.collectionViews.element (boundBy: 0)
    collectionView.swipeUp ()
    collectionView.swipeUp ()
  }
  func testGoToDetail () {
    app.launch ()
    lad collectionView = app.collectionViews.element (boundBy: 0)
    lad firstCell = collectionView.cells.element (boundBy: 0)
    firstCell.tap ()
  }
}

Her er nogle af mine artikler vedrørende test.

  • Kører UITests med Facebook-login i iOS
  • Test i hurtigt med givet når derefter mønster

Hovedtrådbeskyttelse

Adgang til UI fra baggrundskøen kan føre til potentielle problemer. Tidligere havde jeg brug for at bruge MainThreadGuard, nu hvor Xcode 9 har Main Thread Checker, aktiverede jeg netop det i Xcode.

Hovedtrådskontrollen er et selvstændigt værktøj til Swift- og C-sprog, der registrerer ugyldig brug af AppKit, UIKit og andre API'er på en baggrundstråd. Opdatering af brugergrænseflade på en anden tråd end hovedtråden er en almindelig fejl, der kan resultere i ubesvarede UI-opdateringer, visuelle defekter, datakorruktioner og nedbrud.

Måling af forestillinger og emner

Vi kan bruge instrumenter til at profilere appen grundigt. For hurtig måling kan vi gå over til fanen Debug Navigator og se CPU, hukommelse og netværksbrug. Tjek denne seje artikel for at lære mere om instrumenter.

Prototype med legeplads

Legeplads er den anbefalede måde at prototype og bygge apps på. På WWDC 2018 introducerede Apple Create ML, der understøtter Playground til at træne model. Tjek denne seje artikel for at lære mere om legepladsdrevet udvikling i Swift.

Hvor man skal hen herfra

Tak for at have nået det så langt. Jeg håber du har lært noget nyttigt. Den bedste måde at lære noget på er bare at gøre det. Hvis du tilfældigvis skriver den samme kode igen og igen, skal du oprette den som en komponent. Hvis et problem giver dig en hård tid, skal du skrive om det. Del din oplevelse med verden, du lærer meget.

Jeg anbefaler, at du tjekker artiklen Bedste steder at lære iOS-udvikling for at lære mere om iOS-udvikling.

Hvis du har spørgsmål, kommentarer eller feedback, skal du ikke glemme at tilføje dem i kommentarerne. Og hvis du fandt denne artikel nyttig, skal du ikke glemme at klappe.

Hvis du kan lide dette indlæg, kan du overveje at besøge mine andre artikler og apps