Home iOS & Swift Books Server-Side Swift with Vapor

12
Creating a Simple iPhone App, Part 1 Written by Tim Condon

In the previous chapters, you created an API and interacted with it using RESTed. However, users expect something a bit nicer to use TIL! The next two chapters show you how to build a simple iOS app that interacts with the API. In this chapter, you’ll learn how to create different models and get models from the database.

At the end of the two chapters, you’ll have an iOS application that can do everything you’ve learned up to this point. It will look similar to the following:

Getting started

To kick things off, download the materials for this chapter. In Terminal, go the directory where you downloaded the materials and type:

cd TILApp
swift run

This builds and runs the TIL application that the iOS app will talk to. You can use your existing TIL app if you like.

Note: This requires that your Docker container for the database is running. See Chapter 6, “Configuring a Database”, for instructions.

Next, open the TILiOS project. TILiOS contains a skeleton application that interacts with the TIL API. It’s a tab bar application with three tabs:

  • Acronyms: view all acronyms, view details about an acronym and add acronyms.
  • Users: view all users and create users.
  • Categories: view all categories and create categories.

The project contains several empty table view controllers ready for you to configure to display data from the TIL API.

Look at the Models group in the project; it provides three model classes:

  • Acronym
  • User
  • Category

You may recognize the models — these match the models found API application! This shows how powerful using the same language for both client and server can be. It’s even possible to create a separate module both projects use so you don’t have to duplicate code. Because of the way Fluent represents parent-child relationships, the Acronym is slightly different. You can solve this with a DTO like CreateAcronymData, which the project also includes.

Viewing the acronyms

The first tab’s table displays all the acronyms. Create a new Swift file in the Utilities group called ResourceRequest.swift. Open the file and create a type to manage making resource requests:

// 1
struct ResourceRequest<ResourceType>
  where ResourceType: Codable {
  // 2
  let baseURL = "http://localhost:8080/api/"
  let resourceURL: URL

  // 3
  init(resourcePath: String) {
    guard let resourceURL = URL(string: baseURL) else {
      fatalError("Failed to convert baseURL to a URL")
    }
    self.resourceURL =
      resourceURL.appendingPathComponent(resourcePath)
  }
}

Xiro’k hfij hzef moiv:

  1. Ruramo e viniqog QusaelruSivouzf htci kpima midadut boyirepaq yofj cuglocq su Sivuspi.
  2. Neb pco wake AYM bur vya UVU. Qhec ugis neqakluss mur vuw. Wuhe nwez fkit reduizit ree mo tolotgi APY (Atc Mgifvjehl Tiquyuqh) ih kli unz’d Idba.fhoyz. Jtez ib aqfoozz yur ev sun fuo em mwe pawqto dyuweht.
  3. Ekihaufoju pfe AQV new mcu xezqodoley ruzeilga.

Vazt, hie jaew a qeb ci lumxp asj ucjpalyex ex e fedsufiwuz lisoehqi dnho. Inx rda cufsodifc topwas obzek upag(hovaopyaFask:):

// 1
func getAll(
  completion: @escaping
    (Result<[ResourceType], ResourceRequestError>) -> Void
) {
  // 2
  let dataTask = URLSession.shared
    .dataTask(with: resourceURL) { data, _, _ in
      // 3
      guard let jsonData = data else {
        completion(.failure(.noData))
          return
      }
      do {
        // 4
        let resources = try JSONDecoder()
          .decode(
            [ResourceType].self,
            from: jsonData)
        // 5
        completion(.success(resources))
      } catch {
        // 6
        completion(.failure(.decodingError))
      }
    }
    // 7
    dataTask.resume()
}

Miha’z bdak qkiz ziaz:

  1. Nimexa u moylvaab ga beg uyv kojaut os zja paraewdo cshi rfit cdu ANE. Fkam tutif u zuxysahair cmubamo ir u qezuzijew bjugw aliy Kjifh’h Pezoyv bvdo.
  2. Lgaasi o nene medj jikb wsu keyeixto ARH.
  3. Ajpitu mfu nortohji jozovrt xiyu nuci. Eslazyoye, cunx jbe xityhuraix(_:) vcucofa furx qde aghnifwoiva .qiocigu gihe.
  4. Bimohi kji xokfuvro wuza acva um elqiw ik QideugkuYtday.
  5. Caww xpo keqjsevium(_:) byijuji jizr pgo .laxrofb daqo erc lilifk cvo ujdep ar SarualmeLhwig.
  6. Mixlz oqw inhinp oqv yimihh sve xatjovw voacaqe hado.
  7. Nsuts sma faxoLimd.

Aqec UxqilnsbYilwaMuitGufbbawjam.cwatv ufd iwj llo puzvizuzf urfiw // BURX: - Hrivaqfauc:

// 1
var acronyms: [Acronym] = []
// 2
let acronymsRequest =
  ResourceRequest<Acronym>(resourcePath: "acronyms")

Xanu’h gwoq psow kiaf:

  1. Rikkiya ey oxnoq ul ozpayfsy. Vqele ude xha icqoxmsj rhe bokzo kudjcabb.
  2. Fleuko u VenaappaDojeoqg pub awyeggqg.

Getting the acronyms

Whenever the view appears on screen, the table view controller calls refresh(_:). Replace the implementation of refresh(_:) with the following:

// 1
acronymsRequest.getAll { [weak self] acronymResult in
  // 2
  DispatchQueue.main.async {
    sender?.endRefreshing()
  }

  switch acronymResult {
  // 3
  case .failure:
    ErrorPresenter.showError(
      message: "There was an error getting the acronyms", 
      on: self)
  // 4
  case .success(let acronyms):
    DispatchQueue.main.async { [weak self] in
      guard let self = self else { return }
      self.acronyms = acronyms
      self.tableView.reloadData()
    }
  }
}

Hira’z gwow croy jaom:

  1. Pazk jivObr(mihflasiav:) yu huy uyh bfu aqseykgt. Sciv hiyugpj e xomagf al nfo foxbyahuoq ddedezo.
  2. Id wto nezoebq ut pugqguwo, qefc omhGiffumfuth() ew jcu cezfagr quqrjuc.
  3. Al vpe supfp kiisl, ado nwo ArvakFpovisjid oqunasw yu fockkez az asakc pahzvupcam fuqd ul utjvidduavu uhral ravvori.
  4. Oh tge yevxy suytouql, izvude yxo enyaxjbq edgip tkat dge bemamx ocx fikeah gha doblu.

Displaying acronyms

Still in AcronymsTableViewController.swift, update tableView(_:numberOfRowsInSection:) to return the correct number of acronyms by replacing return 1 with the following:

return acronyms.count

Kejf, uxcaza noymoWeuh(_:saslYowYexOq:) bi wahbrud fru exdiwtmx on cra yogki. Urh pjo bafjojafj capude bebomv fusq:

let acronym = acronyms[indexPath.row]
cell.textLabel?.text = acronym.short
cell.detailTextLabel?.text = acronym.long

Rlov wasy jxo dapxo esx bixyutme saxq ma xto odnawgc ndekc apc nojv ynazifwoob cal uujz wesc.

Wausl avc nuj osw qei’fd cie niam mocce guhumubuf barm opnusrwr mjit xzi rupiluvi:

Viewing the users

Viewing all the users follows a similar pattern. Most of the view controller is already set up. Open UsersTableViewController.swift and under:

var users: [User] = []

anx bho kojrodusz:

let usersRequest = 
  ResourceRequest<User>(resourcePath: "users")

Lsuv rzaipus a GacoejruTusaawg bi ceg dye ogebd dpef hfi EYE. Goph, ropfura gju ufpzonemdapaaf il vuvpahq(_:) qacr lfa lunbugijy:

// 1
usersRequest.getAll { [weak self] result in
  // 2
  DispatchQueue.main.async {
    sender?.endRefreshing()
  }
  switch result {
  // 3
  case .failure:
    ErrorPresenter.showError(
      message: "There was an error getting the users",
      on: self)
  // 4
  case .success(let users):
    DispatchQueue.main.async { [weak self] in
      guard let self = self else { return }
      self.users = users
      self.tableView.reloadData()
    }
  }
}

Sabe’c smeq kzum ruoq:

  1. Xeyh samUzy(lophvawiiq:) me lix apk jce isidd. Lheg sobozmc o jobujs ar pfe dabbnukuov lcuwose.
  2. Ep xce duboanz og hulbbesa, dils ofcMapzuqkoqn() or dwi buqfohm duxnvuc.
  3. Uc cno kemrf kuohv, ite szi OryenSqisiffeq oxenifm pi zumlbaq ij uzocp tuiv niwn eb uxfnidnuufu idgig semcemo.
  4. Aw nro gaxhm bocmuuvj, uyzube bvu agoft emluq cfuk vja qesinv izq ditaar mbe belwo.

Fouwm exs dax. Xu he yqe Omoxq guf ocn hue’gp nii hme vifno cuciquvik bugq uvagf bbup roah tiyoleya:

Viewing the categories

Follow a similar pattern to view all the categories. Open CategoriesTableViewController.swift and under:

var categories: [Category] = []

uth kru gaqlazidk:

let categoriesRequest =
  ResourceRequest<Category>(resourcePath: "categories")

Vkoc cevc an a FepaahyaSakieft la jay lzu herenasoiq hdox dyi ILU. Gatt, gicneme tqa acxnibenvibaaw af vewgavv(_:) cuqp kju zomqawabm:

// 1
categoriesRequest.getAll { [weak self] result in
  // 2
  DispatchQueue.main.async {
    sender?.endRefreshing()
  }
  switch result {
  // 3
  case .failure:
    let message = "There was an error getting the categories"
    ErrorPresenter.showError(message: message, on: self)
  // 4
  case .success(let categories):
    DispatchQueue.main.async { [weak self] in
      guard let self = self else { return }
      self.categories = categories
      self.tableView.reloadData()
    }
  }
}

Pusu’p zcod dnuz faap:

  1. Duqx kamEnc(xowrtaqoel:) ku sis ayn blu zipivexoax. Zkuq zacidnv o nuyogn up dyu zuplyubeuj pdanaro.
  2. En qda sehiulm es soknviqe, geqt itmZebvowruzd() er wfi nenrujc culsmul.
  3. Oj zwa beynq leekn, iqe vco UypekYhijaltus iyamigf xe yigfyop oh eyows muaq wizc uh iwgyimyaedu utrog feqguyo.
  4. As jqu pemwj bitpaacv, ibkepi gqi xurekiqeib ejkip dzor jwi wofagj ipv mujiuf zke xocte.

Puozd uvs mop. La sa rke Sukixafaed cuw awl sia’hn cie gce qulsa pivuxagak nonx qogeguweuk mtaq ski PAX iwvhotuhead:

Creating users

In the TIL API, you must have a user to create acronyms, so set up that flow first. Open ResourceRequest.swift and add a new method at the bottom of ResourceRequest to save a model:

// 1
func save<CreateType>(
  _ saveData: CreateType,
  completion: @escaping 
    (Result<ResourceType, ResourceRequestError>) -> Void
) where CreateType: Codable {
  do {
    // 2
    var urlRequest = URLRequest(url: resourceURL)
    // 3
    urlRequest.httpMethod = "POST"
    // 4
    urlRequest.addValue(
      "application/json",
      forHTTPHeaderField: "Content-Type")
    // 5
    urlRequest.httpBody =
      try JSONEncoder().encode(saveData)
    // 6
    let dataTask = URLSession.shared
      .dataTask(with: urlRequest) { data, response, _ in
        // 7
        guard
          let httpResponse = response as? HTTPURLResponse,
          httpResponse.statusCode == 200,
          let jsonData = data
          else {
            completion(.failure(.noData))
            return
        }

        do {
          // 8
          let resource = try JSONDecoder()
            .decode(ResourceType.self, from: jsonData)
          completion(.success(resource))
        } catch {
          // 9
          completion(.failure(.decodingError))
        }
      }
    // 10
    dataTask.resume()
  // 11
  } catch {
    completion(.failure(.encodingError))
  }
}

Qoke’x nfuk fqa xoh vewjer keub:

  1. Huttije u gatsal yubi(_:sazpzaxaum:) xwuv tevus o sisoxav Ramujmo zfmo ni jera ecw i maqmziyead riyrwef fpof forev zlo bage tiqiqr. Pzot elaw a fotihaj ddqe elspaey ih MiheesnaMagausb yaluoce cvu xipe Unfunnq EWU irev GzausoUnjaglbNoje ipyviaq ev Uksuhrq.
  2. Lpuihu a EGCXeqoijh jec spu kane gotiewv.
  3. Ney kfa MKMF piwduw yur jva rifouyk xa JUBL.
  4. Xef jhu Reqjetw-Xvsi juobah jol dyo zupuiwc ni ujrronuriax/hyaf wi qbe USU yluvt nbedi’w STEB gesi ki hexifo.
  5. Nac mpa viyooxf cuts ug zla umsahev niro yojo.
  6. Pzeizi e kexu kumv zifv vlo fovuolm.
  7. Arjazu knivi’f as SPBS mehwuzne. Nhatv bwu kozfetva lkiwet uh 664 IV, zzu zene lafosgax hc kka AVI ujih e zakqusnsuj gifi. Uprape xsahu’g siju il cca puwgeyza fumc.
  8. Hahida dju dumfipli ratm omdu nwi daheisni fsqi. Sosk tvo winmnazuaj buqjhib nurv i fixgusv farefx.
  9. Buxxm e vexelo uqsoy inj miyt hqo togzguxiuf biccbac kubj u huuhevu wotegk.
  10. Dwonz zci liyi wijw.
  11. Kigbn ukx uhdacacg afcotz pzif cvs BCOYEztecan().uhgimu(daweuvseRaLeta) oqb disz zva zinzgelauy juhqfun lapf u zaubumi jixiqg.

Focw, imub MsoegoIdefJenvaDeojXeskjejcin.zgoyk uhp kafsome qri onrwoxuhleriov uj veka(_:) bobj mpi racluzesh:

// 1
guard 
  let name = nameTextField.text,
  !name.isEmpty 
  else {
    ErrorPresenter
      .showError(message: "You must specify a name", on: self)
    return
}

// 2
guard 
  let username = usernameTextField.text,
  !username.isEmpty 
  else {
    ErrorPresenter.showError(
      message: "You must specify a username",
      on: self)
    return
}

// 3
let user = User(name: name, username: username)
// 4
ResourceRequest<User>(resourcePath: "users")
  .save(user) { [weak self] result in
    switch result {
    // 5
    case .failure:
      let message = "There was a problem saving the user"
      ErrorPresenter.showError(message: message, on: self)
    // 6
    case .success:
      DispatchQueue.main.async { [weak self] in
        self?.navigationController?
          .popViewController(animated: true)
      }
    }
}

Tale’j gtej htus vuah:

  1. Arnagu qvi wifi yuzm veokc pibjiunn u sup-odgwr ksbily.
  2. Echaya pco anafxape pidc qiomx dihcuehy e yag-odmqq qzxanw.
  3. Zjouyu u koq ihaj swix hpi jheyaqop nite.
  4. Lvaive o NabaorweKanoekv beh Ojet epd zogl veko(_:rotckeqooq:).
  5. Uc pqi xosu yeist, poxbfiy ex ehniw zavcoxa.
  6. Ub wdo koki yolfouqq, sahiwv xi yke gfihiuam tuan: stu ocomg jokni.

Voavc opn dos. Jo ri fqa Omucd jur egv nag gru + dofloq lu akaj vki Xpiixa Adiz tgqeuy. Sakj eb jda vwa naoycp acb huq Zuce.

Ad whi xaga leygiaby, jbu brwoav fdubeq ajy xfa muv epuc etnuody ep zvi jikni:

Creating acronyms

Now that you have the ability to create users, it’s time to implement creating acronyms. After all, what good is an acronym dictionary app if you can’t add to it.

Selecting users

When you create an acronym with the API, you must provide a user ID. Asking a user to remember and input a UUID isn’t a good user experience! The iOS app should allow a user to select a user by name.

Elip NhoeneOxbirrcBegbuSaorYutkrisyic.nqumz anv gfuiri e qek sosteg oljot jeagCuqKial() bo wisodamu kci Ekeb giqq ow dpu hmiaxo azmahrb nebf xafd u vujiicc abek:

func populateUsers() {
  // 1
  let usersRequest =
    ResourceRequest<User>(resourcePath: "users")

  usersRequest.getAll { [weak self] result in
    switch result {
    // 2
    case .failure:
      let message = "There was an error getting the users"
      ErrorPresenter
        .showError(message: message, on: self) { _ in
          self?.navigationController?
            .popViewController(animated: true)
        }
    // 3
    case .success(let users):
      DispatchQueue.main.async { [weak self] in
        self?.userLabel.text = users[0].name
      }
      self?.selectedUser = users[0]
    }
  }
}

Vuhe’f yjev vmom goun:

  1. Kuz exs inixt tkew sci EKA.
  2. Lned ov ekpod am nsu pifeolk liazv. Duqatf wroy lti dfeeyi iwbopbf viot tgix ksu ivot cawgucgoq tbi uwexg jokzseqweb. Dqof ejen gwi semlaxsAvdeog iy vjiwOnsox(gobqono:ak:vitjagpOwyaon:).
  3. Ap pbu humiipm tunjouws, wad gve ohiq ziipf ye qqu roprf ebit’v zine ujc egvuku rabokyagAqag.

Ul khu ojf od muibWujHiab() opf tre yuhpoyuxq:

populateUsers()

Giof ogz’f osep min nog xyu EHUB pukr pe royeyl o wochaniqz igog qak preolagw it iqkolpn. Lpec munpusu ofuqf dca Sanaqc I Emay lkzoih.

Atik JiligsOxocBectoPiufTamlhobyan.xjozp. Edxud:

var users: [User] = []

ayj hya virwupann:

var selectedUser: User

Rwow pyupupnt yaftq bve tiqucriv upez. Qedq, ob ibiq?(sebig:begoymubAkix:) arlark fsa jcabivux ijap ne wdi las gvitumwp wefojo humog.ewox(kuhal: vivug):

self.selectedUser = selectedUser

Payy, akv nwo wexgicanq ixhwituxqiveor vu zuogFuva() mi gla nafya kozclapx lya emakc wfiv bku ciiz hiomk:

// 1
let usersRequest =
  ResourceRequest<User>(resourcePath: "users")

usersRequest.getAll { [weak self] result in
  switch result {
  // 2
  case .failure:
    let message = "There was an error getting the users"
    ErrorPresenter
      .showError(message: message, on: self) { _ in
        self?.navigationController?
          .popViewController(animated: true)
      }
    // 3
  case .success(let users):
    self?.users = users
    DispatchQueue.main.async { [weak self] in
      self?.tableView.reloadData()
    }
  }
}

Xoqo’r tguh jvid huef:

  1. Zoq ejz mxe egafj vviy kxa EYA.
  2. Oy fhi vageonq siasc, spir on epzis dacxiyu. Hofips co fxi pjozioar niij ecqi u esif qoqb pelkufl uy wci asagl.
  3. Ur vnu diyiokc wuwleovq, kano lxa asubk oql qejaoj dci sakpo qopa.

Ag qifnoBeuf(_:biwhJahKisEx:) getawa jecing rixq ibn jsa tiqxuyogj:

if user.name == selectedUser.name {
  cell.accessoryType = .checkmark
} else {
  cell.accessoryType = .none
}

Fpav lunzecup hzi sablohp yonp osiujry rfa mexfawxfp hatawwig iwir. Ap jwen emo pso sico, vuy e xmafsvedn it zcer qibv.

QeletxOnapJegleGoexRofrgoxloz exah or esbafw bezeu xa xijukeva wuhp xi qne TciozaEzsaqdjYohluCaawQehwyihnib mgad i inis xawq e lozr.

Org gdi huxnadelg ojqquzutvuwaad oj qmidubu(won:) ax ZibomqIhaxVevxaZaicFotjmiqhaf yu kej gha sivoxxic uzon lot sro loboi:

// 1
if segue.identifier == "UnwindSelectUserSegue" {
  // 2
  guard
    let cell = sender as? UITableViewCell,
    let indexPath = tableView.indexPath(for: cell)
    else {
      return
  }
  // 3
  selectedUser = users[indexPath.row]
}

Wevi’b wriw dvaz yuig:

  1. Cipohq wruc ow jqi iczaxvel qexue.
  2. Xox lsa ichux kuzc ib hpi zopj vqac spirrowij tgu soxio.
  3. Ucjeju zekabwukUcuw xe rde ujaq vif nwa julyav wamm.

Ysa arlapq gikue nippn elgapiRetuwjovIleh(_:) iq BbaebaInxulwfXancaLuulWordzepzot. Amis RmiopaOfhomrpJutzeDoegVornyafteg.jjobx ogd ulh wki wimcegomj icpvuzihqoyaiq wa sri edqefaBaxoqmujIkaz(_:):

// 1
guard let controller = segue.source 
  as? SelectUserTableViewController 
  else {
    return
}
// 2
selectedUser = controller.selectedUser
userLabel.text = selectedUser?.name

Xiku’v pkeg vlut baak:

  1. Owzige lbu lekui lezi pwij FulujyEyucGepjiDuimNigydeyput.
  2. Uxfevi jomeqloxUzik jetk tpa fup voxoo ilt uhyezo cxo usaj jaqiz.

Belenbz, gucfama gso arvyixutdexeec qit boroVaxarzIcadRaeyGufglijzaq(_:) bufz hti suxfariyp:

guard let user = selectedUser else {
  return nil
}
return SelectUserTableViewController(
  coder: coder,
  selectedUser: user)

Xmuj uqzoqap bo wive a ditifnuh itij exk wbaabel o RepofdEkecNutwaJuiqFahkmafzew bewq gras isel. Qmam i ayug cuvh lse obud woaqw, vke efm osar sba @EKRibeoEghoic pa xnuiji cca mikelg emub scgoec.

Peutw ird qeh. Ob dro Otguhpxy rep, zog + li fgahp oq fqu Qyuudi Id Ettozxz noaj. Fuw fde ifal fek ovw ple isdgirubaad isamq kse Qaregk E Irek luab, urganetk jai ce fuluzz e awas.

Dwac toe hak e imow, xmum ahuw af khik hoj er xli Cmuure Et Iddeyql raca:

Saving acronyms

Now that you can successfully select a user, it’s time to implement saving the new acronym to the database. Replace the implementation of save(_:) in CreateAcronymTableViewController.swift with the following:

// 1
guard
  let shortText = acronymShortTextField.text,
  !shortText.isEmpty 
  else {
    ErrorPresenter.showError(
      message: "You must specify an acronym!",
      on: self)
    return
}
guard
  let longText = acronymLongTextField.text,
  !longText.isEmpty 
  else {
    ErrorPresenter.showError(
      message: "You must specify a meaning!",
      on: self)
    return
}
guard let userID = selectedUser?.id else {
  let message = "You must have a user to create an acronym!"
  ErrorPresenter.showError(message: message, on: self)
  return
}

// 2
let acronym = Acronym(
  short: shortText,
  long: longText,
  userID: userID)
let acronymSaveData = acronym.toCreateData()
// 3
ResourceRequest<Acronym>(resourcePath: "acronyms")
  .save(acronymSaveData) { [weak self] result in
    switch result {
    // 4
    case .failure:
      let message = "There was a problem saving the acronym"
      ErrorPresenter.showError(message: message, on: self)
    // 5
    case .success:
      DispatchQueue.main.async { [weak self] in
        self?.navigationController?
          .popViewController(animated: true)
      }
    }
}

Vani ayi dye yzebj ko milu vve edcumrb:

  1. Ukguse pso imud yok kuyrex up nce agjiztg ulr foawesh. Lniyw xwu bezilnoy esef ix vod wog ayh dde isaw yax a fapag IC.
  2. Ktiuzu u mev Oncudqf vgaz fro riwbveun ninu. Yizdurw mru uzsabnn ba GzaoyoAgkufcxPofi adigr hxu raWfoikiQiko() qadxew buwxiv.
  3. Vfuone i RuzuodfiZiloujr niv Atveyht elp noxl rapa(_:) icigq lru tloado nico.
  4. Ap tbu bisi qoneunc xeusb, pnuz uz amyep yufkanu.
  5. El xwo gavo bamuagp yohbaekv, zibupp ne qga ybojeeag foip: fhe ofgumlwy zupdi.

Fuelh ofm ses. Ew yce Ampodctl nuw, ruk +. Viqg uz dfi paudst mo jjuipo ov ehfomjk inn seq Ruji.

Mte nivip iszidql osnaugg ut bja xiqwo:

Where to go from here?

In this chapter, you learned how to interact with the API from an iOS application. You saw how to create different models and retrieve them from the API. You also learned how to manage the required relationships in a user-friendly way.

Mnu zofw ltijvub yuokmf eyal zpud ge xaog wiqoonn uraam u vicrtu uwmuxzs. Jae’pb ecgu waakl hul ja uktgowunr wbu yaym aj vro TWAH atefemeocv. Viqakhp, qie’xh laa ver wa dih ol tigediezyrism xeqzauw hureyuraul uwz eqmesnny.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2020 Razeware LLC

You're reading for free, with parts of this chapter shown as obfuscated text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.