iOSProgramming:TheBigNerdRanchGuidebyChristianKeurandAaronHillegass
Copyright©2016BigNerdRanch,LLCAllrightsreserved.PrintedintheUnitedStatesofAmerica.Thispublicationisprotectedbycopyright,andpermissionmustbeobtainedfromthepublisherpriortoanyprohibitedreproduction,storageinaretrievalsystem,ortransmissioninanyformorbyanymeans,electronic,mechanical,photocopying,recording,orlikewise.Forinformationregardingpermissions,contact
BigNerdRanch,LLC200ArizonaAveNEAtlanta,GA30307(770)817-6373http://www.bignerdranch.com/book-comments@bignerdranch.comThe10-gallonhatwithpropellerlogoisatrademarkofBigNerdRanch,LLC.ExclusiveworldwidedistributionoftheEnglisheditionofthisbookby
PearsonTechnologyGroup800East96thStreetIndianapolis,IN46240USAhttp://www.informit.comTheauthorsandpublisherhavetakencareinwritingandprintingthisbookbutmakenoexpressedorimpliedwarrantyofanykindandassumenoresponsibilityforerrorsoromissions.Noliabilityisassumedforincidentalorconsequentialdamagesinconnectionwithorarisingoutoftheuseoftheinformationorprogramscontainedherein.AppStore,Apple,Cocoa,CocoaTouch,Finder,Instruments,iCloud,iPad,iPhone,iPod,iPodtouch,iTunes,Keychain,Mac,MacOS,Multi-Touch,Objective-C,OSX,Quartz,Retina,Safari,andXcodearetrademarksofApple,Inc.,registeredintheU.S.andothercountries.Manyofthedesignationsusedbymanufacturersandsellerstodistinguishtheirproductsareclaimedastrademarks.Wherethosedesignationsappearinthisbook,andthepublisherwasawareofatrademarkclaim,thedesignationshavebeenprintedwithinitialcapitallettersorinallcapitals.PrintISBN-10013468964X
PrintISBN-13978-0134689647
Sixthedition,firstprinting,December2016ReleaseK.6.1.1
AcknowledgmentsWhileournamesappearonthecover,manypeoplehelpedmakethisbookareality.Wewouldliketotakethischancetothankthem.
FirstandforemostwewouldliketothankJoeConwayforhisworkontheearliereditionsofthisbook.Heauthoredthefirstthreeeditionsandcontributedgreatlytothefourtheditionaswell.Manyofthewordsinthisbookarestillhis,andforthat,weareverygrateful.JuanPabloClaudewrotesomeofthecontentandcontributedhisexpertiseandopinionstomakethisbookevenbetter.Hisworkisgreatlyappreciated.Acoupleotherpeoplewentaboveandbeyondwiththeirhelponthisbook.TheyareMikeyWardandChrisMorris.TheotherinstructorswhoteachtheiOSBootcampfedusanever-endingstreamofsuggestionsandcorrections.TheyareBenScheirman,BolotKerimbaev,BrianHardy,ChrisMorris,JJManton,JohnGallagher,JonathanBlocksom,JosephDixon,JuanPabloClaude,MarkDalrymple,MattBezark,MattMathias,MikeZornek,MikeyWard,PouriaAlmassi,RobertEdwards,RodStrougo,ScottRitchie,StepChristopher,ThomasWard,TJUsiyan,TomHarrington,andZacharyWaldowski.Theseinstructorswereoftenaidedbytheirstudentsinfindingbookerrata,somanythanksareduetoallthestudentswhoattendtheiOSBootcamp.ThankstoalloftheemployeesatBigNerdRanchwhohelpedreviewthebook,providedsuggestions,andfounderrata.Ourtirelesseditor,ElizabethHoladay,tookourdistractedmumblingsandmadethemintoreadableprose.AnnaBentleyandSimonePaymentjumpedintoprovidecopyeditingandproofing.EllieVolckhausendesignedthecover.(Thephotoisofthebottombracketofabicycleframe.)ChrisLoperatIntelligentEnglish.comdesignedandproducedtheprintandebookversionsofthebook.TheamazingteamatPearsonTechnologyGrouppatientlyguidedusthroughthebusinessendofbookpublishing.
Thefinalandmostimportantthanksgoestoourstudents,whosequestionsinspiredustowritethisbookandwhosefrustrationsinspiredustomakeitclearandcomprehensible.
TableofContentsIntroduction
PrerequisitesWhatHasChangedintheSixthEdition?OurTeachingPhilosophyHowtoUseThisBookUsinganeBookHowThisBookIsOrganizedStyleChoicesTypographicalConventionsNecessaryHardwareandSoftware
1.ASimpleiOSApplicationCreatinganXcodeProjectModel-View-ControllerDesigningQuizInterfaceBuilderBuildingtheInterface
CreatingviewobjectsConfiguringviewobjectsRunningonthesimulatorAbriefintroductiontoAutoLayoutMakingconnections
CreatingtheModelLayerImplementingactionmethodsLoadingthefirstquestion
BuildingtheFinishedApplicationApplicationIconsLaunchScreen
2.TheSwiftLanguageTypesinSwiftUsingStandardTypes
InferringtypesSpecifyingtypesLiteralsandsubscriptingInitializersPropertiesInstancemethods
OptionalsSubscriptingdictionaries
LoopsandStringInterpolationEnumerationsandtheSwitchStatement
EnumerationsandrawvaluesExploringApple’sSwiftDocumentation
3.ViewsandtheViewHierarchyViewBasicsTheViewHierarchyCreatingaNewProjectViewsandFrames
CustomizingthelabelsTheAutoLayoutSystem
ThealignmentrectangleandlayoutattributesConstraintsAddingconstraintsinInterfaceBuilderIntrinsiccontentsizeMisplacedviewsAddingmoreconstraints
BronzeChallenge:MoreAutoLayoutPractice4.TextInputandDelegation
TextEditingKeyboardattributesRespondingtotextfieldchangesDismissingthekeyboard
ImplementingtheTemperatureConversionNumberformatters
DelegationConformingtoaprotocolUsingadelegateMoreonprotocols
BronzeChallenge:DisallowAlphabeticCharacters5.ViewControllers
TheViewofaViewControllerSettingtheInitialViewControllerUITabBarController
TabbaritemsLoadedandAppearingViews
AccessingsubviewsInteractingwithViewControllersandTheirViewsSilverChallenge:DarkModeFortheMoreCurious:RetinaDisplay
6.ProgrammaticViewsCreatingaViewProgrammaticallyProgrammaticConstraints
AnchorsActivatingconstraintsLayoutguides
MarginsExplicitconstraints
ProgrammaticControlsBronzeChallenge:AnotherTabSilverChallenge:User ’sLocationGoldChallenge:DroppingPinsFortheMoreCurious:NSAutoresizingMaskLayoutConstraint
7.LocalizationInternationalization
FormattersBaseinternationalizationPreparingforlocalization
LocalizationNSLocalizedStringandstringstables
BronzeChallenge:AnotherLocalizationFortheMoreCurious:Bundle’sRoleinInternationalizationFortheMoreCurious:ImportingandExportingasXLIFF
8.ControllingAnimationsBasicAnimations
ClosuresAnotherLabelAnimationCompletionAnimatingConstraintsTimingFunctionsBronzeChallenge:SpringAnimationsSilverChallenge:LayoutGuides
9.DebuggingABuggyProjectDebuggingBasics
InterpretingconsolemessagesFixingthefirstbugCavemandebugging
TheXcodeDebugger:LLDBSettingbreakpointsSteppingthroughcodeTheLLDBconsole
10.UITableViewandUITableViewControllerBeginningtheHomepwnerApplicationUITableViewController
SubclassingUITableViewControllerCreatingtheItemClass
CustominitializersUITableView’sDataSource
Givingthecontrolleraccesstothestore
ImplementingdatasourcemethodsUITableViewCells
CreatingandretrievingUITableViewCellsReusingUITableViewCells
ContentInsetsBronzeChallenge:SectionsSilverChallenge:ConstantRowsGoldChallenge:CustomizingtheTable
11.EditingUITableViewEditingModeAddingRowsDeletingRowsMovingRowsDisplayingUserAlertsDesignPatternsBronzeChallenge:RenamingtheDeleteButtonSilverChallenge:PreventingReorderingGoldChallenge:ReallyPreventingReordering
12.SubclassingUITableViewCellCreatingItemCellExposingthePropertiesofItemCellUsingItemCellDynamicCellHeightsDynamicType
RespondingtouserchangesBronzeChallenge:CellColors
13.StackViewsUsingUIStackView
ImplicitconstraintsStackviewdistributionNestedstackviewsStackviewspacing
SeguesHookingUptheContentPassingDataAroundBronzeChallenge:MoreStackViews
14.UINavigationControllerUINavigationControllerNavigatingwithUINavigationControllerAppearingandDisappearingViewsDismissingtheKeyboard
EventhandlingbasicsDismissingbypressingtheReturnkeyDismissingbytappingelsewhere
UINavigationBarAddingbuttonstothenavigationbar
BronzeChallenge:DisplayingaNumberPadSilverChallenge:ACustomUITextFieldGoldChallenge:PushingMoreViewControllers
15.CameraDisplayingImagesandUIImageView
AddingacamerabuttonTakingPicturesandUIImagePickerController
Settingtheimagepicker ’ssourceTypeSettingtheimagepicker ’sdelegatePresentingtheimagepickermodallyPermissionsSavingtheimage
CreatingImageStoreGivingViewControllersAccesstotheImageStoreCreatingandUsingKeysWrappingUpImageStoreBronzeChallenge:EditinganImageSilverChallenge:RemovinganImageGoldChallenge:CameraOverlayFortheMoreCurious:NavigatingImplementationFiles
//MARK:16.Saving,Loading,andApplicationStates
ArchivingApplicationSandbox
ConstructingafileURLNSKeyedArchiverandNSKeyedUnarchiver
LoadingfilesApplicationStatesandTransitionsWritingtotheFilesystemwithDataErrorHandlingBronzeChallenge:PNGFortheMoreCurious:ApplicationStateTransitionsFortheMoreCurious:ReadingandWritingtotheFilesystemFortheMoreCurious:TheApplicationBundle
17.SizeClassesModifyingTraitsforaSpecificSizeClassBronzeChallenge:StackedTextFieldandLabels
18.TouchEventsandUIResponderTouchEventsCreatingtheTouchTrackerApplicationCreatingtheLineStruct
Structs
ValuetypesvsreferencetypesCreatingDrawViewDrawingwithDrawViewTurningTouchesintoLines
Handlingmultipletouches@IBInspectableSilverChallenge:ColorsGoldChallenge:CirclesFortheMoreCurious:TheResponderChainFortheMoreCurious:UIControl
19.UIGestureRecognizerandUIMenuControllerUIGestureRecognizerSubclassesDetectingTapswithUITapGestureRecognizerMultipleGestureRecognizersUIMenuControllerMoreGestureRecognizers
UILongPressGestureRecognizerUIPanGestureRecognizerandsimultaneousrecognizers
MoreonUIGestureRecognizerSilverChallenge:MysteriousLinesGoldChallenge:SpeedandSizePlatinumChallenge:ColorsFortheMoreCurious:UIMenuControllerandUIResponderStandardEditActions
20.WebServicesStartingthePhotoramaApplicationBuildingtheURL
FormattingURLsandrequestsURLComponents
SendingtheRequestURLSession
ModelingthePhotoJSONData
JSONSerializationEnumerationsandassociatedvaluesParsingJSONdata
DownloadingandDisplayingtheImageDataTheMainThreadBronzeChallenge:PrintingtheResponseInformationSilverChallenge:FetchRecentPhotosfromFlickrFortheMoreCurious:HTTP
21.CollectionViewsDisplayingtheGridCollectionViewDataSourceCustomizingtheLayout
CreatingaCustomUICollectionViewCellDownloadingtheImageData
ExtensionsImagecaching
NavigatingtoaPhotoSilverChallenge:UpdatedItemSizesGoldChallenge:CreatingaCustomLayout
22.CoreDataObjectGraphsEntities
ModelingentitiesTransformableattributesNSManagedObjectandsubclasses
NSPersistentContainerUpdatingItems
InsertingintothecontextSavingchanges
UpdatingtheDataSourceFetchrequestsandpredicates
BronzeChallenge:PhotoViewCountFortheMoreCurious:TheCoreDataStack
NSManagedObjectModelNSPersistentStoreCoordinatorNSManagedObjectContext
23.CoreDataRelationshipsRelationshipsAddingTagstotheInterfaceBackgroundTasksSilverChallenge:Favorites
24.AccessibilityVoiceOver
TestingVoiceOverAccessibilityinPhotorama
25.AfterwordWhattoDoNextShamelessPlugs
Index
IntroductionAsanaspiringiOSdeveloper,youfacethreemajortasks:
YoumustlearntheSwiftlanguage.SwiftistherecommendeddevelopmentlanguageforiOS.ThefirsttwochaptersofthisbookaredesignedtogiveyouaworkingknowledgeofSwift.Youmustmasterthebigideas.Theseincludethingslikedelegation,archiving,andtheproperuseofviewcontrollers.Thebigideastakeafewdaystounderstand.Whenyoureachthehalfwaypointofthisbook,youwillunderstandthesebigideas.Youmustmastertheframeworks.TheeventualgoalistoknowhowtouseeverymethodofeveryclassineveryframeworkiniOS.Thisisaprojectforalifetime:TherearehundredsofclassesandthousandsofmethodsavailableiniOS,andAppleaddsmoreclassesandmethodswitheveryreleaseofiOS.Inthisbook,youwillbeintroducedtoeachofthesubsystemsthatmakeuptheiOSSDK,butyouwillnotstudyeachonedeeply.Instead,ourgoalistogetyoutothepointwhereyoucansearchandunderstandApple’sreferencedocumentation.
WehaveusedthismaterialmanytimesatouriOSbootcampsatBigNerdRanch.ItiswelltestedandhashelpedthousandsofpeoplebecomeiOSdevelopers.Wesincerelyhopethatitprovesusefultoyou.
Prerequisites
ThisbookassumesthatyouarealreadymotivatedtolearntowriteiOSapps.WewillnotspendanytimeconvincingyouthattheiPhone,iPad,andiPodtoucharecompellingpiecesoftechnology.Wealsoassumethatyouhavesomeexperienceprogrammingandknowsomethingaboutobject-orientedprogramming.Ifthisisnottrue,youshouldprobablystartwithSwiftProgramming:TheBigNerdRanchGuide.
WhatHasChangedintheSixthEdition?
AllofthecodeinthisbookhasbeenupdatedforSwift3.0,whichwasamajorupdatetotheSwiftlanguage.Throughoutthebook,youwillseehowtouseSwift’scapabilitiesandfeaturestowritebetteriOSapplications.WehavecometoloveSwiftatBigNerdRanchandbelieveyouwill,too.OtheradditionsincludenewchaptersondebuggingandaccessibilityandimprovedcoverageofCoreData.WehavealsoupdatedvariouschapterstousethetechnologiesandAPIsintroducediniOS10.ThiseditionassumesthatthereaderisusingXcode8.1orlaterandrunningapplicationsonaniOS10orlaterdevice.Besidestheseobviouschanges,wemadethousandsoftinyimprovementsthatwereinspiredbyquestionsfromourreadersandourstudents.Everychapterofthisbookisjustalittlebetterthanthecorrespondingchapterfromthefifthedition.
OurTeachingPhilosophy
ThisbookwillteachyoutheessentialconceptsofiOSprogramming.Atthesametime,youwilltypeinalotofcodeandbuildabunchofapplications.Bytheendofthebook,youwillhaveknowledgeandexperience.However,alltheknowledgeshouldnot(and,inthisbook,willnot)comefirst.Thatisthetraditionalwayoflearningwehaveallcometoknowandhate.Instead,wetakealearn-while-doingapproach.Developmentconceptsandactualcodinggotogether.HereiswhatwehavelearnedovertheyearsofteachingiOSprogramming:
Wehavelearnedwhatideaspeoplemustgrasptogetstartedprogramming,andwefocusonthatsubset.Wehavelearnedthatpeoplelearnbestwhentheseconceptsareintroducedastheyareneeded.Wehavelearnedthatprogrammingknowledgeandexperiencegrowbestwhentheygrowtogether.Wehavelearnedthat“goingthroughthemotions”ismuchmoreimportantthanitsounds.Manytimeswewillaskyoutostarttypingincodebeforeyouunderstandit.Werealizethatyoumayfeellikeatrainedmonkeytypinginabunchofcodethatyoudonotfullygrasp.Butthebestwaytolearncodingistofindandfixyourtypos.Farfrombeingadrag,thisbasicdebuggingiswhereyoureallylearntheinsandoutsofthecode.Thatiswhyweencourageyoutotypeinthecodeyourself.Youcouldjustdownloadit,butcopyingandpastingisnotprogramming.Wewantbetterforyouandyourskills.
Whatdoesthismeanforyou,thereader?Tolearnthiswaytakessometrust–andweappreciateyours.Italsotakespatience.Asweleadyouthroughthesechapters,wewilltrytokeepyoucomfortableandtellyouwhatishappening.However,therewillbetimeswhenyouwillhavetotakeourwordforit.(Ifyouthinkthiswillbugyou,keepreading–wehavesomeideasthatmighthelp.)Donotgetdiscouragedifyourunacrossaconceptthatyoudonotunderstandrightaway.Rememberthatweareintentionallynotprovidingalltheknowledgeyouwilleverneedallatonce.Ifaconceptseemsunclear,wewilllikelydiscussitinmoredetaillaterwhenitbecomesnecessary.Andsomethingsthatarenotclearatthebeginningwillsuddenlymakesensewhenyouimplementthemthefirst(orthetwelfth)time.Peoplelearndifferently.Itispossiblethatyouwilllovehowwehandoutconceptsonanas-neededbasis.Itisalsopossiblethatyouwillfinditfrustrating.Incaseofthelatter,herearesomeoptions:
Takeadeepbreathandwaititout.Wewillgetthere,andsowillyou.Checktheindex.Wewillletitslideifyoulookaheadandreadthroughamoreadvanceddiscussionthatoccurslaterinthebook.ChecktheonlineAppledocumentation.Thisisanessentialdevelopertool,andyouwillwantplentyofpracticeusingit.Consultitearlyandoften.IfSwiftorobject-orientedprogrammingconceptsaregivingyouahardtime(orifyou
thinktheywill),youmightconsiderbackingupandreadingourSwiftProgramming:TheBigNerdRanchGuide.
HowtoUseThisBook
ThisbookisbasedontheclassweteachatBigNerdRanch.Assuch,itwasdesignedtobeconsumedinacertainmanner.Setyourselfareasonablegoal,like,“Iwilldoonechaptereveryday.”Whenyousitdowntoattackachapter,findaquietplacewhereyouwillnotbeinterruptedforatleastanhour.Shutdownyouremail,yourTwitterclient,andyourchatprogram.Thisisnotatimeformultitasking;youwillneedtoconcentrate.Dotheactualprogramming.Youcanreadthroughachapterfirst,ifyoulike.Butthereallearningcomeswhenyousitdownandcodeasyougo.Youwillnotreallyunderstandtheideauntilyouhavewrittenaprogramthatusesitand,perhapsmoreimportantly,debuggedthatprogram.Acoupleoftheexercisesrequiresupportingfiles.Forexample,inthefirstchapteryouwillneedaniconforyourQuizapplication,andwehaveoneforyou.Youcandownloadtheresourcesandsolutionstotheexercisesfromwww.bignerdranch.com/solutions/iOSProgramming6ed.zip.Therearetwotypesoflearning.WhenyoulearnaboutthePeloponnesianWar,youaresimplyaddingdetailstoascaffoldingofideasthatyoualreadyunderstand.Thisiswhatwewillcall“EasyLearning.”Yes,learningaboutthePeloponnesianWarcantakealongtime,butyouareseldomflummoxedbyit.LearningiOSprogramming,ontheotherhand,is“HardLearning,”andyoumayfindyourselfquitebaffledattimes,especiallyinthefirstfewdays.Inwritingthisbook,wehavetriedtocreateanexperiencethatwilleaseyouoverthebumpsinthelearningcurve.Herearetwothingsyoucandotomakethejourneyeasier:
FindsomeonewhoalreadyknowshowtowriteiOSapplicationsandwillansweryourquestions.Inparticular,gettingyourapplicationontoadevicethefirsttimeisusuallyveryfrustratingifyouaredoingitwithoutthehelpofanexperienceddeveloper.Getenoughsleep.Sleepypeopledonotrememberwhattheyhavelearned.
UsinganeBook
IfyouarereadingthisbookonaKindleorKindleforiPad,wewanttopointoutthatreadingthecodemaybetrickyattimes.Longerlinesofcodewillwraptoasecondlinedependingonyourselectedfontsize.ThisbothersusbecausewearereallyconscientiousatBigNerdRanchaboutthewayourcodeappearsonthepage.Clearvisualpatternsincodemakethatcodeeasiertounderstand.Thelongestlinesofcodeinthisbookare86monospacecharacters,likethisone.cell=tableView.dequeueReusableCell(withIdentifier:"UITableViewCell",for:idxPath1)
YoucanplaywithyourKindle’ssettingstofindthebestforviewinglongcodelines.Whenyougettothepointwhereyouareactuallytypingincode,wesuggestopeningthebookonyourMacinKindleforMac,afreeapplicationyoucandownloadfromAmazon.com.Maketheapplicationwindowlargeenoughsothatyoucanseethecodewithnowrappinglines.Youwillalsobeabletoseethefiguresinfulldetail.
HowThisBookIsOrganized
Inthisbook,eachchapteraddressesoneormoreideasofiOSdevelopmentthroughdiscussionandhands-onpractice.Formorecodingpractice,mostchaptersincludechallengeexercises.Weencourageyoutotakeonatleastsomeofthese.TheyareexcellentforfirmingupyourgraspoftheconceptsintroducedinthechapterandformakingyouamoreconfidentiOSprogrammer.Finally,mostchaptersconcludewithoneortwoFortheMoreCurioussectionsthatexplaincertainconsequencesoftheconceptsthatwereintroducedearlier.Chapter1introducesyoutoiOSprogrammingasyoubuildanddeployatinyapplicationcalledQuiz.YouwillgetyourfeetwetwithXcodeandtheiOSsimulatoralongwithallthestepsforcreatingprojectsandfiles.ThechapterincludesadiscussionofModel-View-ControllerandhowitrelatestoiOSdevelopment.Chapter2providesanoverviewofSwift,includingbasicsyntax,types,optionals,initialization,andhowSwiftisabletointeractwiththeexistingiOSframeworks.Youwillalsogetexperienceworkinginaplayground,Xcode’sprototypingtool.InChapter3,youwillfocusontheiOSuserinterfaceasyoulearnaboutviewsandtheviewhierarchyandcreateanapplicationcalledWorldTrotter.Chapter4introducesdelegation,animportantiOSdesignpattern.YouwillalsoaddatextfieldtoWorldTrotter.InChapter5,youwillexpandWorldTrotterandlearnaboutusingviewcontrollersformanaginguserinterfaces.Youwillgetpracticeworkingwithviewsandviewcontrollersaswellasnavigatingbetweenscreensusingatabbar.InChapter6,youwilllearnhowtomanageviewsandviewcontrollersincode.YouwilladdasegmentedcontroltoWorldTrotterthatwillletyouswitchbetweenvariousmaptypes.Chapter7introducestheconceptsandtechniquesofinternationalizationandlocalization.YouwilllearnaboutLocale,stringstables,andBundleasyoulocalizepartsofWorldTrotter.InChapter8,youwilllearnaboutandadddifferenttypesofanimationstotheQuizprojectthatyoucreatedinChapter1.Chapter9willwalkyouthroughsomeofthetoolsatyourdisposalfordebugging–findingandfixingissuesinyourapplication.Chapter10introducesthelargestapplicationinthebook–Homepwner.(“Homepwner”isnotatypo;youcanfindthedefinitionof“pwn”atwww.wiktionary.org.)Thisapplicationkeepsarecordofyouritemsincaseoffireorothercatastrophe.Homepwnerwilltakeeightchapterstocomplete.InChapter10–Chapter12,youwillworkwithtables.Youwilllearnabouttableviews,theirviewcontrollers,andtheirdatasources.Youwilllearnhowtodisplaydatainatable,howtoallowtheusertoeditthetable,andhowtoimprovetheinterface.Chapter13introducesstackviews,whichwillhelpyoucreatecomplexinterfaceseasily.YouwilluseastackviewtoaddanewscreentoHomepwnerthatdisplaysanitem’sdetails.
Chapter14buildsonthenavigationexperiencegainedinChapter5.YouwilluseUINavigationControllertogiveHomepwneradrill-downinterfaceandanavigationbar.Chapter15introducesthecamera.YouwilltakepicturesanddisplayandstoreimagesinHomepwner.InChapter16,youwilladdpersistencetoHomepwner,usingarchivingtosaveandloadtheapplicationdata.InChapter17,youwilllearnaboutsizeclasses,andyouwillusethesetoupdateHomepwner’sinterfacetoscalewellacrossvariousscreensizes.InChapter18andChapter19,youwillcreateadrawingapplicationnamedTouchTrackertolearnabouttouchevents.YouwillseehowtoaddmultitouchcapabilityandhowtouseUIGestureRecognizertorespondtoparticulargestures.Youwillalsogetexperiencewiththefirstresponderandresponderchainconceptsandmorepracticeusingstructuresanddictionaries.Chapter20introduceswebservicesasyoucreatethePhotoramaapplication.ThisapplicationfetchesandparsesJSONdatafromaserverusingURLSessionandJSONSerialization.InChapter21,youwilllearnaboutcollectionviewsasyoubuildaninterfaceforPhotoramausingUICollectionViewandUICollectionViewCell.InChapter22andChapter23,youwilladdpersistencetoPhotoramausingCoreData.YouwillstoreandloadimagesandassociateddatausinganNSManagedObjectContext.Chapter24willwalkyouthroughmakingyourapplicationsaccessibletomorepeoplebyaddingVoiceOverinformation.
StyleChoices
Thisbookcontainsalotofcode.Wehaveattemptedtomakethatcodeandthedesignsbehinditexemplary.Wehavedoneourbesttofollowtheidiomsofthecommunity,butattimeswehavewanderedfromwhatyoumightseeinApple’ssamplecodeorcodeyoumightfindinotherbooks.Inparticular,youshouldknowupfrontthatwenearlyalwaysstartaprojectwiththesimplesttemplateproject:thesingleviewapplication.Whenyourappworks,youwillknowitisbecauseofyourefforts–notbecauseofbehaviorbuiltintothetemplate.
TypographicalConventions
Tomakethisbookeasiertoread,certainitemsappearincertainfonts.Classes,types,methods,andfunctionsappearinabold,fixed-widthfont.Classesandtypesstartwithcapitalletters,andmethodsandfunctionsstartwithlowercaseletters.Forexample,“IntheloadView()methodoftheRexViewControllerclass,createaconstantoftypeString.”Variables,constants,andfilenamesappearinafixed-widthfontbutarenotbold.Soyouwillsee,“InViewController.swift,addavariablenamedfidoandinitializeitto"Rufus".”Applicationnames,menuchoices,andbuttonnamesappearinagraysansseriffont.(IfyourKindledeviceorappdoesnothaveasansseriffont,theywillappearingrayinthebodyfont.)Forexample,“OpenXcodeandselectNewProject...fromtheFilemenu.SelectSingleViewApplicationandthenclickNext.”Allcodeblocksareinafixed-widthfont.Codethatyouneedtotypeinisbold;codethatyouneedtodeleteisstruckthrough.Forexample,inthefollowingcode,youwoulddeletethelineimportFoundationandtypeinthetwolinesbeginning@IBOutlet.Theotherlinesarealreadyinthecodeandareincludedtoletyouknowwheretoaddthenewlines.importFoundation
importUIKit
classViewController:UIViewController{
@IBOutletvarquestionLabel:UILabel!
@IBOutletvaranswerLabel:UILabel!
}
NecessaryHardwareandSoftware
Tobuildtheapplicationsinthisbook,youmusthaveXcode8.1,whichrequiresaMacrunningmacOSElCapitanversion10.11.4orlater.Xcode,Apple’sIntegratedDevelopmentEnvironment,isavailableontheAppStore.XcodeincludestheiOSSDK,theiOSsimulator,andotherdevelopmenttools.YoushouldjointheAppleDeveloperProgram,whichcosts$99/year,because:
Downloadingthelatestdevelopertoolsisfreeformembers.Youcannotputanappinthestoreuntilyouareamember.
Ifyouaregoingtotakethetimetoworkthroughthisentirebook,membershipintheAppleDeveloperProgramisworththecost.Gotodeveloper.apple.com/programs/ios/tojoin.WhataboutiOSdevices?MostoftheapplicationsyouwilldevelopinthefirsthalfofthebookareforiPhone,butyouwillbeabletorunthemonaniPad.OntheiPadscreen,iPhoneapplicationsappearinaniPhone-sizedwindow.NotacompellinguseofiPad,butthatisOKwhenyouarestartingwithiOS.Intheearlychapters,youwillbefocusedonlearningthefundamentalsoftheiOSSDK,andthesearethesameacrossiOSdevices.Laterinthebook,youwillseehowtomakeapplicationsrunnativelyonbothiOSdevicefamilies.Excitedyet?Good.Let’sgetstarted.
1ASimpleiOSApplication
Inthischapter,youaregoingtowriteaniOSapplicationnamedQuiz.Thisapplicationwillshowaquestionandthenrevealtheanswerwhentheusertapsabutton.Tappinganotherbuttonwillshowtheuseranewquestion(Figure1.1).Figure1.1Yourfirstapplication:Quiz
WhenyouarewritinganiOSapplication,youmustanswertwobasicquestions:HowdoIgetmyobjectscreatedandconfiguredproperly?(Example:“IwantabuttonherethatsaysNextQuestion.”)HowdoImakemyapprespondtouserinteraction?(Example:“Whentheusertapsthebutton,Iwantthispieceofcodetobeexecuted.”)
Mostofthisbookisdedicatedtoansweringthesequestions.Asyougothroughthisfirstchapter,youwillprobablynotunderstandeverythingthatyouaredoing,andyoumayfeelridiculousjustgoingthroughthemotions.Butgoingthroughthe
motionsisenoughfornow.Mimicryisapowerfulformoflearning;itishowyoulearnedtospeak,anditishowyouwillstartiOSprogramming.Asyoubecomemorecapable,youwillexperimentandchallengeyourselftodocreativethingsontheplatform.Fornow,goaheadanddowhatweshowyou.Thedetailswillbeexplainedinlaterchapters.
CreatinganXcodeProject
OpenXcodeand,fromtheFilemenu,selectNew→Project....(IfXcodeopenstoawelcomescreen,selectCreateanewXcodeproject.)Anewworkspacewindowwillappearandasheetwillslidedownfromitstoolbar.Atthetop,findtheiOSsectionandthentheApplicationarea(Figure1.2).Youareofferedseveralapplicationtemplatestochoosefrom.SelectSingleViewApplication.Figure1.2Creatingaproject
ThisbookwascreatedforXcode8.1.ThenamesofthesetemplatesmaychangewithnewXcodereleases.IfyoudonotseeaSingleViewApplicationtemplate,usethesimplest-soundingtemplate.YoucanalsovisittheBigNerdRanchforumforthisbookatforums.bignerdranch.comforhelpworkingwithnewerversionsofXcode.ClickNextand,inthenextsheet,enterQuizfortheProductName(Figure1.3).Theorganizationnameandidentifierarerequiredtocontinue.YoucanuseBigNerdRanchoranyorganizationnameyouwouldlike.Fortheorganizationidentifier,youcanusecom.bignerdranchorcom.yourcompanynamehere.FromtheLanguagepop-upmenu,chooseSwift,andfromtheDevicespop-upmenu,chooseUniversal.MakesurethattheUseCoreDatacheckboxisunchecked.
Figure1.3Configuringanewproject
ClickNextand,inthefinalsheet,savetheprojectinthedirectorywhereyouplantostoretheexercisesinthisbook.ClickCreatetocreatetheQuizproject.YournewprojectopensintheXcodeworkspacewindow(Figure1.4).
Figure1.4Xcodeworkspacewindow
Thelefthandsideoftheworkspacewindowisthenavigatorarea.Thisareadisplaysdifferentnavigators–toolsthatshowyoudifferentpartsofyourproject.Youcanopenanavigatorbyselectingoneoftheiconsinthenavigatorselector,whichisthebarjustabovethenavigatorarea.Thenavigatorcurrentlyopenistheprojectnavigator.Theprojectnavigatorshowsyouthefilesthatmakeupaproject(Figure1.5).Youcanselectoneofthesefilestoopenitintheeditorareatotherightofthenavigatorarea.Thefilesintheprojectnavigatorcanbegroupedintofolderstohelpyouorganizeyourproject.Afewgroupshavebeencreatedbythetemplateforyou.Youcanrenamethem,ifyouwant,oraddnewones.Thegroupsarepurelyfortheorganizationoffilesanddonotcorrelatetothefilesysteminanyway.
Figure1.5Quizapplication’sfilesintheprojectnavigator
Model-View-Controller
Beforeyoubeginyourapplication,let’sdiscussakeyconceptinapplicationarchitecture:Model-View-Controller,orMVC.MVCisadesignpatternusediniOSdevelopment.InMVC,everyinstancebelongstoeitherthemodellayer,theviewlayer,orthecontrollerlayer.(Layerheresimplyreferstooneormoreobjectsthattogetherfulfillarole.)
Themodellayerholdsdataandknowsnothingabouttheuserinterface,orUI.InQuiz,themodelwillconsistoftwoorderedlistsofstrings:oneforquestionsandanotherforanswers.Usually,instancesinthemodellayerrepresentrealthingsintheworldoftheuser.Forexample,whenyouwriteanappforaninsurancecompany,yourmodelwillalmostcertainlycontainacustomtypecalledInsurancePolicy.Theviewlayercontainsobjectsthatarevisibletotheuser.Examplesofviewobjects,orviews,arebuttons,textfields,andsliders.Viewobjectsmakeupanapplication’sUI.InQuiz,thelabelsshowingthequestionandanswerandthebuttonsbeneaththemareviewobjects.Thecontrollerlayeriswheretheapplicationismanaged.Controllerobjects,orcontrollers,arethemanagersofanapplication.Controllersconfiguretheviewsthattheuserseesandmakesurethattheviewandmodelobjectsstaysynchronized.Ingeneral,controllerstypicallyhandle“Andthen?”questions.Forexample,whentheuserselectsanitemfromalist,thecontrollerdetermineswhattheuserseesnext.
Figure1.6showstheflowofcontrolinanapplicationinresponsetouserinput,suchastheusertappingabutton.Figure1.6MVCpattern
Noticethatmodelsandviewsdonottalktoeachotherdirectly;controllerssitsquarelyinthemiddleofeverything,receivingmessagesanddispatchinginstructions.
DesigningQuiz
YouaregoingtowritetheQuizapplicationusingtheMVCpattern.Hereisabreakdownoftheinstancesyouwillbecreatingandworkingwith:
Themodellayerwillconsistoftwoinstancesof[String].TheviewlayerwillconsistoftwoinstancesofUILabelandtwoinstancesofUIButton.ThecontrollerlayerwillconsistofaninstanceofViewController.
TheseinstancesandtheirrelationshipsarelaidoutinthediagramforQuizshowninFigure1.7.Figure1.7ObjectdiagramforQuiz
Figure1.7isthebigpictureofhowthefinishedQuizapplicationwillwork.Forexample,whentheNextQuestionbuttonistapped,itwilltriggeramethodinViewController.Amethodisalotlikeafunction–alistofinstructionstobeexecuted.Thismethodwillretrieveanewquestionfromthearrayofquestionsandaskthetoplabeltodisplaythatquestion.ItisOKifthisdiagramdoesnotmakesenseyet–itwillbytheendofthechapter.Referbacktoitasyoubuildtheapptoseehowitistakingshape.YouaregoingtobuildQuizinsteps,startingwiththevisualinterfacefortheapplication.
InterfaceBuilder
YouareusingtheSingleViewApplicationtemplatebecauseitisthesimplesttemplatethatXcodeoffers.Still,thistemplatehasasignificantamountofmagicinthatsomecriticalcomponentshavealreadybeensetupforyou.Fornow,youwilljustusethesecomponents,withoutattemptingtogainadeepunderstandingofhowtheywork.Therestofthebookwillbeconcernedwiththosedetails.Intheprojectnavigator,clickonceontheMain.storyboardfile.Xcodewillopenitsgraphic-styleeditorcalledInterfaceBuilder.InterfaceBuilderdividestheeditorareaintotwosections:thedocumentoutline,onthelefthandside,andthecanvas,ontheright.ThisisshowninFigure1.8.Ifwhatyouseeinyoureditorareadoesnotmatchthefigure,youmayhavetoclickontheShowDocumentOutlinebutton.(Ifyouhaveadditionalareasshowing,donotworryaboutthem.)Youmayalsohavetoclickonthedisclosuretrianglesinthedocumentoutlinetorevealcontent.Figure1.8InterfaceBuildershowingMain.storyboard
TherectanglethatyouseeintheInterfaceBuildercanvasiscalledasceneandrepresentstheonly“screen”orviewyourapplicationhasatthistime(rememberthatyouusedthesingleviewapplicationtemplatetocreatethisproject).Inthenextsection,youwilllearnhowtocreateaUIforyourapplicationusingInterfaceBuilder.InterfaceBuilderletsyoudragobjectsfromalibraryontothecanvastocreateinstancesandalsoletsyouestablishconnectionsbetweenthoseobjectsandyourcode.Theseconnectionscanresultincodebeingcalledbyauserinteraction.AcrucialfeatureofInterfaceBuilderisthatitisnotagraphicalrepresentationofcodecontainedinotherfiles.InterfaceBuilderisanobjecteditorthatcancreateinstancesofobjectsandmanipulatetheirproperties.Whenyouaredoneeditinganinterface,itdoesnotgeneratecodethatcorrespondstotheworkyouhavedone.A.storyboardfileisanarchiveofobjectinstancestobeloadedintomemorywhennecessary.
BuildingtheInterface
Let’sgetstartedonyourinterface.YouhaveselectedMain.storyboardtorevealitssinglesceneinthecanvas(Figure1.9).Figure1.9ThesceneinMain.storyboard
Tostart,makesureyoursceneissizedforiPhone7.Atthebottomofthecanvas,findtheViewasbutton.ItwilllikelysaysomethinglikeViewas:iPhone7(wChR).(ThewChRwillnotmakesenserightnow;wewillexplainitinChapter17.)IfitsaysiPhone7already,thenyouareallset.Ifnot,clickontheViewasbuttonandselectthefourthdevicefromtheleft,whichcorrespondstoiPhone7(Figure1.10).
Figure1.10ViewingthesceneforiPhone7
Itistimetoaddyourviewobjectstothatblankslate.
Creatingviewobjects
MakesurethattheutilityareawithinXcode’swindowisvisible.Youmayneedtoclickontherightmostbuttonofthe controlinthetop-rightcornerofthewindow.Theutilityareaistotherightoftheeditorareaandhastwosections:theinspectorandthelibrary.Thetopsectionistheinspector,whichdisplayssettingsforafileorobjectthatisselectedintheeditorarea.Thebottomsectionisthelibrary,whichlistsitemsthatyoucanaddtoafileorproject.Atthetopofeachsectionintheutilityareaisaselectorfordifferentinspectorsandlibraries(Figure1.11).
Figure1.11Xcodeutilityarea
Yourapplicationinterfacerequiresfourviewobjects:twobuttonstoacceptuserinputandtwotextlabelstodisplayinformation.Toaddthem,firstmakesureyoucanseetheobjectlibrary,asshowninFigure1.11,byselectingthe tabfromthelibraryselector.Theobjectlibrarycontainstheobjectsthatyoucanaddtoastoryboardfiletocomposeyourinterface.FindtheLabelobjectbyeitherscrollingdownthroughthelistorbyusingthesearchbaratthebottomofthelibrary.Selectthisobjectinthelibraryanddragitontotheviewobjectonthecanvas.Dragthelabelaroundthecanvasandnoticethedashedbluelinesthatappearwhenthelabelisnearthecenterofthecanvas(Figure1.12).Theseguidelineswillhelpyoulayoutyourinterface.
Figure1.12Addingalabeltothecanvas
Usingtheguidelines,positionthelabelinthehorizontalcenteroftheviewandnearthetop,asshowninFigure1.12.Eventually,thislabelwilldisplayquestionstotheuser.Dragasecondlabelontotheviewandpositionitinthehorizontalcenter,closertothemiddle.Thislabelwilldisplayanswers.Next,findButtonintheobjectlibraryanddragtwobuttonsontotheview.Positiononebeloweachlabel.YouhavenowaddedfourviewobjectstotheViewController’sUI.Noticethattheyalsoappearinthedocumentoutline.YourinterfaceshouldlooklikeFigure1.13.Figure1.13BuildingtheQuizinterface
Configuringviewobjects
Nowthatyouhavecreatedtheviewobjects,youcanconfiguretheirattributes.Someattributesofaview,likesize,position,andtext,canbechangeddirectlyonthecanvas.Forexample,youcanresizeanobjectbyselectingitinthecanvasorthedocumentoutlineandthendraggingitscornersandedgesinthecanvas.Beginbyrenamingthelabelsandbuttons.Double-clickoneachlabelandreplacethetextwith???.Thendouble-clicktheupperbuttonandchangeitsnametoNextQuestion.RenamethelowerbuttontoShowAnswer.TheresultsareshowninFigure1.14.
Figure1.14Renamingthelabelsandbuttons
Youmayhavenoticedthatbecauseyouhavechangedthetextinthelabelsandbuttons,andthereforetheirwidths,theyarenolongerneatlycenteredinthescene.Clickoneachofthemanddragtocenterthemagain,asshowninFigure1.15.Figure1.15Centeringthelabelsandbuttons
Runningonthesimulator
TotestyourUI,youaregoingtorunQuizonXcode’siOSsimulator.ToprepareQuiztorunonthesimulator,findthecurrentschemepop-upmenuontheXcodetoolbar(Figure1.16).Figure1.16iPhone7schemeselected
IfitsayssomethinggenericlikeiPhone7,thentheprojectissettorunonthesimulatorandyouaregoodtogo.IfitsayssomethinglikeChristian'siPhone,thenclickandchooseiPhone7fromthepop-upmenu.TheiPhone7schemewillbeyoursimulatordefaultthroughoutthisbook.Clickthetriangularplaybuttoninthetoolbar.Thiswillbuild(compile)andthenruntheapplication.YouwillbedoingthisoftenenoughthatyoumaywanttolearnandusethekeyboardshortcutCommand-R.Afterthesimulatorlaunchesyouwillseethattheinterfacehasalltheviewsyouadded,neatlycenteredasyouconfiguredtheminInterfaceBuilder.Nowgobacktothecurrentschemepop-upmenuandselectiPhone7Plusasyoursimulatorofchoice.Runtheapplicationagainandyouwillnoticethatwhiletheviewsyouaddedarestillpresent,theyarenotcenteredastheywereoniPhone7.Thisisbecausethelabelsandbuttonscurrentlyhaveafixedpositiononascreen,andtheydonotremaincenteredonthemainview.Tocorrectthisproblem,youwilluseatechnologycalledAutoLayout.
AbriefintroductiontoAutoLayout
Asofnow,yourinterfacelooksniceintheInterfaceBuildercanvas.ButiOSdevicescomeinevermorescreensizes,andapplicationsareexpectedtosupportallscreensizesandorientations–andperhapsmorethanonedevicetype.Youneedtoguaranteethatthelayoutofviewobjectswillbecorrectregardlessofthescreensizeororientationofthedevicerunningtheapplication.ThetoolforthistaskisAutoLayout.AutoLayoutworksbyspecifyingpositionandsizeconstraintsforeachviewobjectinascene.Theseconstraintscanberelativetoneighboringviewsortocontainerviews.Acontainerviewisjustaviewobjectthat,asthenamesuggests,containsanotherview.Forexample,takealookatthedocumentoutlineforMain.storyboard(Figure1.17).
Figure1.17Documentlayoutwithacontainerview
YoucanseeinthedocumentoutlinethatthelabelsandbuttonsyouaddedareindentedwithrespecttoaViewobject.Thisviewobjectisthecontainerofthelabelsandbuttons,andtheobjectscanbepositionedandsizedrelativetothisview.TobeginspecifyingAutoLayoutconstraints,selectthetoplabelbyclickingoniteitheronthecanvasorinthedocumentoutline.Atthebottomofthecanvas,noticetheAutoLayoutmenus,showninFigure1.18.Figure1.18TheAutoLayoutmenus
Withthetoplabelstillselected,clickonthe icontorevealtheAlignmenushowninFigure1.19.
Figure1.19Centeringthetoplabelinthecontainer
WithintheAlignmenu,checktheHorizontallyinContainercheckboxtocenterthelabelinthecontainer.ThenclicktheAdd1Constraintbutton.Thisconstraintguaranteesthatonanysizescreen,inanyorientation,thelabelwillbecenteredhorizontally.Nowyouneedtoaddmoreconstraintstocenterthelowerlabelandthebuttonswithrespecttothetoplabelandtolockthespacingbetweenthem.SelectthefourviewsbyCommand-clickingonthemoneafteranotherandthenclickonthe icontoopentheAddNewConstraintsmenushowninFigure1.20.
Figure1.20Addingconstraintstocenterandfixthespacingbetweenviews
Clickontheredverticaldashedsegmentnearthetopofthemenu.Whenyouclickonthesegment,itwillbecomesolidred(showninFigure1.20),indicatingthatthedistanceofeachviewispinnedtoitsnearesttopneighbor.Also,checktheAlignboxandthenselectHorizontalCentersfromthepop-upmenu.ForUpdateFrames,makesurethatyouhaveItemsofNewConstraintsselected.Finally,clickontheAdd7Constraintsbuttonatthebottomofthemenu.Ifyoumadeanymistakeswhileaddingconstraints,youmayseeredororangeconstraintsandframesonthecanvasinsteadofthecorrectbluelines.Ifthatisthecase,youwillwanttocleartheexistingconstraintsandgothroughthestepsaboveagain.Toclearconstraints,firstselectthebackground(container)view.Thenclickthe icontoopentheResolveAutoLayoutIssuesmenu.SelectClearConstraintsundertheAllViewsinViewControllersection(Figure1.21).Thiswillclearawayanyconstraintsthatyouhaveaddedandgiveyouafreshstartonaddingtheconstraintsbackin.
Figure1.21Clearingconstraints
AutoLayoutcanbeadifficulttooltomaster,andthatiswhyyouarestartingtouseitinthefirstchapterofthisbook.Bystartingearly,youwillhavemorechancestouseitandgetusedtoitscomplexity.Also,dealingwithproblemsbeforethingsgettoocomplicatedwillhelpyoudebuglayoutissueswithconfidence.Toconfirmthatyourinterfacebehavescorrectly,buildandruntheapplicationontheiPhone7Plussimulator.Afterconfirmingthattheinterfacelookscorrect,buildandruntheapplicationontheiPhone7simulator.Thelabelsandbuttonsshouldbecenteredonboth.
Makingconnections
Aconnectionletsoneobjectknowwhereanotherobjectisinmemorysothatthetwoobjectscancommunicate.TherearetwokindsofconnectionsthatyoucanmakeinInterfaceBuilder:outletsandactions.Anoutletisareferencetoanobject.Anactionisamethodthatgetstriggeredbyabuttonorsomeotherviewthattheusercaninteractwith,likeasliderorapicker.Let’sstartbycreatingoutletsthatreferencetheinstancesofUILabel.TimetoleaveInterfaceBuilderandwritesomecode.
Declaringoutlets
Intheprojectnavigator,findandselectthefilenamedViewController.swift.TheeditorareawillchangefromInterfaceBuildertoXcode’scodeeditor.InViewController.swift,startbydeletinganycodethatthetemplateaddedbetweenclassViewController:UIViewController{andthefinalbrace,sothatthefile
lookslikethis:importUIKit
classViewController:UIViewController{
}
(Forsimplicity,wewillnotshowthelineimportUIKitagainforthisfile.)Next,addthefollowingcodethatdeclarestwoproperties.(Throughoutthisbook,newcodeforyoutoaddwillbeshowninbold.Codeforyoutodeletewillbestruckthrough.)Donotworryaboutunderstandingthecodeorpropertiesrightnow;justgetitin.classViewController:UIViewController{
@IBOutletvarquestionLabel:UILabel!
@IBOutletvaranswerLabel:UILabel!
}
ThiscodegiveseveryinstanceofViewControlleranoutletnamedquestionLabelandanoutletnamedanswerLabel.TheviewcontrollercanuseeachoutlettoreferenceaparticularUILabelobject(i.e.,oneofthelabelsinyourview).The@IBOutletkeywordtellsXcodethatyouwillconnecttheseoutletstolabelobjectsusingInterfaceBuilder.
Settingoutlets
Intheprojectnavigator,selectMain.storyboardtoreopenInterfaceBuilder.YouwantthequestionLabeloutlettopointtotheinstanceofUILabelatthetopoftheUI.Inthedocumentoutline,findtheViewControllerScenesectionandtheViewControllerobjectwithinit.Inyourcase,theViewControllerstandsinforaninstanceofViewController,whichistheobjectresponsibleformanagingtheinterfacedefinedinMain.storyboard.Control-drag(orright-clickanddrag)fromtheViewControllerinthedocumentoutlinetothetoplabelinthescene.Whenthelabelishighlighted,releasethemouseandkeyboard;ablackpanelwillappear.SelectquestionLabeltosettheoutlet,asshowninFigure1.22.
Figure1.22SettingquestionLabel
(IfyoudonotseequestionLabelintheconnectionspanel,double-checkyourViewController.swiftfilefortypos.)Now,whenthestoryboardfileisloaded,theViewController’squestionLabeloutletwillautomaticallyreferencetheinstanceofUILabelatthetopofthescreen,whichwillallowtheViewControllertotellthelabelwhatquestiontodisplay.SettheanswerLabeloutletthesameway:Control-dragfromtheViewControllertothebottomUILabelandselectanswerLabel(Figure1.23).
Figure1.23SettinganswerLabel
Noticethatyoudragfromtheobjectwiththeoutletthatyouwanttosettotheobjectthatyouwantthatoutlettopointto.Youroutletsareallset.Thenextconnectionsyouneedtomakeinvolvethetwobuttons.
Definingactionmethods
WhenaUIButtonistapped,itcallsamethodonanotherobject.Thatobjectiscalledthetarget.Themethodthatistriggerediscalledtheaction.Thisactionisthenameofthemethodthatcontainsthecodetobeexecutedinresponsetothebuttonbeingtapped.Inyourapplication,thetargetforbothbuttonswillbetheinstanceofViewController.Eachbuttonwillhaveitsownaction.Let’sstartbydefiningthetwoactionmethods:showNextQuestion(_:)andshowAnswer(_:).ReopenViewController.swiftandaddthetwoactionmethodsaftertheoutlets.classViewController:UIViewController{
@IBOutletvarquestionLabel:UILabel!
@IBOutletvaranswerLabel:UILabel!
@IBActionfuncshowNextQuestion(_sender:UIButton){
}
@IBActionfuncshowAnswer(_sender:UIButton){
}
}
Youwillfleshoutthesemethodsafteryoumakethetargetandactionconnections.The@IBActionkeywordtellsXcodethatyouwillbemakingtheseconnectionsinInterfaceBuilder.
Settingtargetsandactions
SwitchbacktoMain.storyboard.Let’sstartwiththeNextQuestionbutton.YouwantitstargettobeViewControlleranditsactiontobeshowNextQuestion(_:).Tosetanobject’starget,youControl-dragfromtheobjecttoitstarget.Whenyoureleasethemouse,thetargetisset,andapop-upmenuappearsthatletsyouselectanaction.SelecttheNextQuestionbuttoninthecanvasandControl-dragtotheViewControllerinthedocumentoutline.WhentheViewControllerishighlighted,releasethemousebuttonandchooseshowNextQuestion:underSentEventsinthepop-upmenu,asshowninFigure1.24.Figure1.24SettingNextQuestiontarget/action
NowfortheShowAnswerbutton.SelectthebuttonandControl-dragfromthebuttontotheViewController.ChooseshowAnswer:fromthepop-upmenu.
Summaryofconnections
TherearenowfiveconnectionsbetweentheViewControllerandtheviewobjects.YouhavesetthepropertiesanswerLabelandquestionLabeltoreferencethelabelobjects–twoconnections.TheViewControlleristhetargetforbothbuttons–twomore.Theproject’stemplatemadeoneadditionalconnection:TheviewpropertyofViewControllerisconnectedtotheViewobjectthatrepresentsthebackgroundoftheapplication.Thatmakesfive.Youcanchecktheseconnectionsintheconnectionsinspector.SelecttheViewControllerinthedocumentoutline.Then,intheutilitiesarea,clickthe tabtorevealtheconnectionsinspector(Figure1.25).Figure1.25Checkingconnectionsintheconnectionsinspector
Yourstoryboardfileiscomplete.Theviewobjectshavebeencreatedandconfiguredandallthenecessaryconnectionshavebeenmadetothecontrollerobject.Let’smoveontocreatingandconnectingyourmodelobjects.
CreatingtheModelLayer
ViewobjectsmakeuptheUI,sodeveloperstypicallycreate,configure,andconnectviewobjectsusingInterfaceBuilder.Thepartsofthemodellayer,ontheotherhand,aretypicallysetupincode.Intheprojectnavigator,selectViewController.swift.Addthefollowingcodethatdeclarestwoarraysofstringsandaninteger.classViewController:UIViewController{
@IBOutletvarquestionLabel:UILabel!
@IBOutletvaranswerLabel:UILabel!
letquestions:[String]=[
"Whatis7+7?",
"WhatisthecapitalofVermont?",
"Whatiscognacmadefrom?"
]
letanswers:[String]=[
"14",
"Montpelier",
"Grapes"
]
varcurrentQuestionIndex:Int=0
...
}
Thearraysareorderedlistscontainingquestionsandanswers.Theintegerwillkeeptrackofwhatquestiontheuserison.Noticethatthearraysaredeclaredusingtheletkeyword,whereastheintegerisdeclaredusingthevarkeyword.Aconstantisdenotedwiththeletkeyword;itsvaluecannotchange.Thequestionsandanswersarraysareconstants.Thequestionsandanswersinthisquizwillnotchangeand,infact,cannotbechangedfromtheirinitialvalues.Avariable,ontheotherhand,isdenotedbythevarkeyword;itsvalueisallowedtochange.YoumadethecurrentQuestionIndexpropertyavariablebecauseitsvaluemustbeabletochangeastheusercyclesthroughthequestionsandanswers.
Implementingactionmethods
Nowthatyouhavequestionsandanswers,youcanfinishimplementingtheactionmethods.InViewController.swift,updateshowNextQuestion(_:)andshowAnswer(_:)....
@IBActionfuncshowNextQuestion(_sender:UIButton){
currentQuestionIndex+=1
ifcurrentQuestionIndex==questions.count{
currentQuestionIndex=0
}
letquestion:String=questions[currentQuestionIndex]
questionLabel.text=question
answerLabel.text="???"
}
@IBActionfuncshowAnswer(_sender:UIButton){
letanswer:String=answers[currentQuestionIndex]
answerLabel.text=answer
}
...
Loadingthefirstquestion
Justaftertheapplicationislaunched,youwillwanttoloadthefirstquestionfromthearrayanduseittoreplacethe???placeholderinthequestionLabellabel.AgoodwaytodothisisbyoverridingtheviewDidLoad()methodofViewController.(“Override”meansthatyouareprovidingacustomimplementationforamethod.)AddthemethodtoViewController.swift.classViewController:UIViewController{
...
overridefuncviewDidLoad(){
super.viewDidLoad()
questionLabel.text=questions[currentQuestionIndex]
}
}
Allthecodeforyourapplicationisnowcomplete!
BuildingtheFinishedApplication
BuildandruntheapplicationontheiPhone7simulator,asyoudidearlier.Ifbuildingturnsupanyerrors,youcanviewthemintheissuenavigatorbyselectingthetabinthenavigatorarea(Figure1.26).Figure1.26Issuenavigatorwithexampleerrorsandwarnings
Clickonanyerrororwarningintheissuenavigatortobetakentothefileandthelineofcodewheretheissueoccurred.Findandfixanyproblems(i.e.,codetypos!)bycomparingyourcodewiththecodeinthischapter.Thentryrunningtheapplicationagain.Repeatthisprocessuntilyourapplicationcompiles.Afteryourapplicationhascompiled,itwilllaunchintheiOSsimulator.PlayaroundwiththeQuizapplication.YoushouldbeabletotaptheNextQuestionbuttonandseeanewquestioninthetoplabel;tappingShowAnswershouldshowtherightanswer.Ifyourapplicationisnotworkingasexpected,double-checkyourconnectionsinMain.storyboard.YouhavebuiltaworkingiOSapp!Takeamomenttobaskintheglory.OK,enoughbasking.Yourappworks,butitneedssomespitandpolish.
ApplicationIcons
WhilerunningQuiz,selectHardware→Homefromthesimulator ’smenu.YouwillseethatQuiz’siconisaboring,defaulttile.Let’sgiveQuizabettericon.AnapplicationiconisasimpleimagethatrepresentstheapplicationontheiOSHomescreen.Differentdevicesrequiredifferent-sizedicons,someofwhichareshowninTable1.1.Table1.1Applicationiconsizesbydevice
Device Applicationiconsizes
5.5-inchiPhone 180x180pixels(@3x)
4.7-inchand4.0-inchiPhone 120x120pixels(@2x)
7.9-inchand9.7-inchiPad 152x152pixels(@2x)
12.9-inchiPad 167x167pixels(@2x)
Wehavepreparedaniconimagefile(size120x120)fortheQuizapplication.Youcandownloadthisicon(alongwithresourcesforotherchapters)fromwww.bignerdranch.com/solutions/iOSProgramming6ed.zip.UnzipiOSProgramming6ed.zipandfindtheQuiz-120.pngfileinthe0-Resources/ProjectAppIconsdirectoryoftheunzippedfolder.Youaregoingtoaddthisicontoyourapplicationbundleasaresource.Ingeneral,therearetwokindsoffilesinanapplication:codeandresources.Code(likeViewController.swift)isusedtocreatetheapplicationitself.Resourcesarethingslikeimagesandsoundsthatareusedbytheapplicationatruntime.Intheprojectnavigator,findAssets.xcassets.SelectthisfiletoopenitandthenselectAppIconfromtheresourcelistonthelefthandside(Figure1.27).
Figure1.27ShowingtheAssetCatalog
ThispanelistheAssetCatalog,whereyoucanmanagealloftheimagesthatyourapplicationwillneed.DragtheQuiz-120.pngfilefromFinderontothe2xslotoftheiPhoneAppsection(Figure1.28).Thiswillcopythefileintoyourproject’sdirectoryonthefilesystemandaddareferencetothatfileintheAssetCatalog.(YoucanControl-clickonafileintheAssetCatalogandselecttheoptiontoShowinFindertoconfirmthis.)Figure1.28AddingtheappicontotheAssetCatalog
Buildandruntheapplicationagain.Switchtothesimulator ’sHomescreeneitherbyclickingHardware→Home,asyoudidbefore,orbyusingthekeyboardshortcutCommand-Shift-H.Youshouldseethenewicon.(Ifyoudonotseetheicon,deletetheapplicationandthenbuildandrunagaintoredeployit.Todothis,theeasiestoptionistoresetthesimulatorbyclickingSimulator→ResetContentandSettings....Thiswillremoveallapplicationsandresetthesimulatortoitsdefaultsettings.Youshouldseetheappiconthenexttimeyouruntheapplication.)
LaunchScreen
Anotheritemyoushouldsetfortheprojectisthelaunchimage,whichappearswhileanapplicationisloading.ThelaunchimagehasaspecificroleiniOS:ItconveystotheuserthattheapplicationisindeedlaunchinganddepictstheUIthattheuserwillinteractwithoncetheapplicationloads.Therefore,agoodlaunchimageisacontent-lessscreenshotoftheapplication.Forexample,theClockapplication’slaunchimageshowsthefourtabsalongthebottom,allintheunselectedstate.Oncetheapplicationloads,thecorrecttabisselectedandthecontentbecomesvisible.(Keepinmindthatthelaunchimageisreplacedaftertheapplicationhaslaunched;itdoesnotbecomethebackgroundimageoftheapplication.)AneasywaytoaccomplishthisistoallowXcodetogeneratethepossiblelaunchscreenimagesforyouusingalaunchscreenfile.Opentheprojectsettingsbyclickingonthetop-levelQuizintheprojectnavigator.UnderAppIconsandLaunchImages,chooseMain.storyboardfromtheLaunchScreenFiledropdown(Figure1.29).LaunchimageswillnowbegeneratedfromMain.storyboard.Figure1.29Settingthelaunchscreenfile
Itisdifficulttoseetheresultsofthischange,becausethelaunchimageistypicallyshownforonlyashorttime.However,itisagoodpracticetosetthelaunchimageeventhoughitsroleissobrief.Congratulations!Youhavewrittenyourfirstapplicationandevenaddedsomedetailstomakeitpolished.YouwillreturntoQuizlaterinthebook.ThenextchaptercoverssomebasicsofSwifttoprepareyouformorecoding.
2TheSwiftLanguage
SwiftisanewlanguagethatAppleintroducedin2014.ItreplacesObjective-CastherecommendeddevelopmentlanguageforiOSandMac.Inthischapter,youaregoingtofocusonthebasicsofSwift.Youwillnotlearneverything,butyouwilllearnenoughtogetstarted.Then,asyoucontinuethroughthebook,youwilllearnmoreSwiftwhileyoulearniOSdevelopment.SwiftmaintainstheexpressivenessofObjective-Cwhileintroducingasyntaxthatissafer,succinct,andreadable.Itemphasizestypesafetyandaddsadvancedfeaturessuchasoptionals,generics,andsophisticatedstructuresandenumerations.Mostimportantly,Swiftallowstheuseofthesenewfeatureswhilerelyingonthesametested,elegantiOSframeworksthatdevelopershavebuiltuponforyears.IfyouknowObjective-C,thenthechallengeisrecastingwhatyouknow.Itmayseemawkwardatfirst,butwehavecometoloveSwiftatBigNerdRanchandbelieveyouwill,too.IfyoudonotthinkyouwillbecomfortablepickingupSwiftatthesametimeasiOSdevelopment,youmaywanttostartwithSwiftProgramming:TheBigNerdRanchGuideorApple’sSwifttutorials,whichyoucanfindatdeveloper.apple.com/swift.Butifyouhavesomeprogrammingexperienceandarewillingtolearn“onthejob,”youcanstartyourSwifteducationhereandnow.
TypesinSwift
Swifttypescanbearrangedintothreebasicgroups:structures,classes,andenumerations(Figure2.1).Allthreecanhave:
properties–valuesassociatedwithatypeinitializers–codethatinitializesaninstanceofatypeinstancemethods–functionsspecifictoatypethatcanbecalledonaninstanceofthattypeclassorstaticmethods–functionsspecifictoatypethatcanbecalledonthetypeitself
Figure2.1Swiftbuildingblocks
Swift’sstructures(or“structs”)andenumerations(or“enums”)aresignificantlymorepowerfulthaninmostlanguages.Inadditiontosupportingproperties,initializers,andmethods,theycanalsoconformtoprotocolsandcanbeextended.Swift’simplementationoftypically“primitive”typessuchasnumbersandBooleanvaluesmaysurpriseyou:Theyareallstructures.Infact,alloftheseSwifttypesarestructures:
Numbers: Int,Float,Double
Boolean: Bool
Text: String,Character
Collections:Array<Element>,Dictionary<Key:Hashable,Value>,Set<Element:Hashable>
Thismeansthatstandardtypeshaveproperties,initializers,andmethodsoftheirown.Theycanalsoconformtoprotocolsandbeextended.Finally,akeyfeatureofSwiftisoptionals.Anoptionalallowsyoutostoreeitheravalueofaparticulartypeornovalueatall.YouwilllearnmoreaboutoptionalsandtheirroleinSwiftlaterinthischapter.
UsingStandardTypes
Inthissection,youaregoingtoexperimentwithstandardtypesinanXcodeplayground.Aplaygroundletsyouwritecodeandseetheresultswithouttheoverheadofmanuallyrunninganapplicationandcheckingtheoutput.InXcode,selectFile→New→Playground....Youcanacceptthedefaultnameforthisfile;youwillonlybeherebriefly.MakesuretheplatformisiOS(Figure2.2).Figure2.2Configuringaplayground
ClickNextandsavethisfileinaconvenientplace.Whenthefileopens,noticethattheplaygroundisdividedintotwosections(Figure2.3).Thelargerwhiteareatotheleftistheeditorwhereyouwritecode.Thegraycolumnontherightisthesidebar.Theplaygroundcompilesandexecutesyourcodeaftereverylineandshowstheresultsinthesidebar.Figure2.3Aplayground
Intheexamplecode,thevarkeyworddenotesavariable,sothevalueofstrcanbechangedfromitsinitialvalue.Typeinthecodebelowtochangethevalueofstr,andyouwillseetheresultsappearinthesidebartotheright.varstr="Hello,playground""Hello,playground"
str="Hello,Swift""Hello,Swift"
(Noticethatweareshowingsidebarresultstotherightofthecodeforthebenefitofreaderswhoarenotactivelydoingtheexercise.)Theletkeyworddenotesaconstantvalue,whichcannotbechanged.InyourSwiftcode,youshoulduseletunlessyouexpectthevaluewillneedtochange.Addaconstanttothemix:varstr="Hello,playground""Hello,playground"
str="Hello,Swift""Hello,Swift"
letconstStr=str"Hello,Swift"
BecauseconstStrisaconstant,attemptingtochangeitsvaluewillcauseanerror.varstr="Hello,playground""Hello,playground"
str="Hello,Swift""Hello,Swift"
letconstStr=str"Hello,Swift"
constStr="Hello,world"
Anerrorappears,indicatedbytheredsymboltotheleftoftheoffendingline.Clickthesymboltogetmoreinformationabouttheerror.Inthiscase,theerrorreadsCannotassigntovalue:'constStr'isa'let'constant.Anerrorintheplaygroundcodewillpreventyoufromseeinganyfurtherresultsinthesidebar,soyouusuallywanttoaddressitrightaway.RemovethelinethatattemptstochangethevalueofconstStr.varstr="Hello,playground""Hello,playground"
str="Hello,Swift""Hello,Swift"
letconstStr=str"Hello,Swift"
constStr="Hello,world"
Inferringtypes
Atthispoint,youmayhavenoticedthatneithertheconstStrconstantnorthestrvariablehasaspecifiedtype.Thisdoesnotmeantheyareuntyped!Instead,thecompilerinferstheirtypesfromtheinitialvalues.Thisiscalledtypeinference.YoucanfindoutwhattypewasinferredusingXcode’sQuickHelp.Option-clickonconstStrintheplaygroundtoseetheQuickHelpinformationforthisconstant,showninFigure2.4.
Figure2.4constStrisoftypeString
Option-clickingtorevealQuickHelpwillworkforanysymbol.
Specifyingtypes
Ifyourconstantorvariablehasaninitialvalue,youcanrelyontypeinference.Ifaconstantorvariabledoesnothaveaninitialvalueorifyouwanttoensurethatitisacertaintype,youcanspecifythetypeinthedeclaration.Addmorevariableswithspecifiedtypes:varstr="Hello,playground""Hello,playground"
str="Hello,Swift""Hello,Swift"
letconstStr=str"Hello,Swift"
varnextYear:Int
varbodyTemp:Float
varhasPet:Bool
Notethatthesidebardoesnotreportanyresultsbecausethesevariablesdonotyethavevalues.Let’sgooverthesenewtypesandhowtheyareused.
NumberandBooleantypes
ThemostcommontypeforintegersisInt.Thereareadditionalintegertypesbasedonwordsizeandsignedness,butApplerecommendsusingIntunlessyoureallyhaveareasontousesomethingelse.Forfloating-pointnumbers,Swiftprovidesthreetypeswithdifferentlevelsofprecision:Floatfor32-bitnumbers,Doublefor64-bitnumbers,andFloat80for80-bitnumbers.ABooleanvalueisexpressedinSwiftusingthetypeBool.ABool’svalueiseithertrueorfalse.
Collectiontypes
TheSwiftstandardlibraryoffersthreecollections:arrays,dictionaries,andsets.Anarrayisanorderedcollectionofelements.ThearraytypeiswrittenasArray<T>,whereTisthetypeofelementthatthearraywillcontain.Arrayscancontainelementsofanytype:astandardtype,astructure,oraclass.Addavariableforanarrayofintegers:varhasPet:Bool
vararrayOfInts:Array<Int>
Arraysarestronglytyped.Onceyoudeclareanarrayascontainingelementsof,say,Int,youcannotaddaStringtoit.Thereisashorthandsyntaxfordeclaringarrays:Youcansimplyusesquarebracketsaroundthetypethatthearraywillcontain.UpdatethedeclarationofarrayOfIntstousetheshorthand:varhasPet:Bool
vararrayOfInts:Array<Int>
vararrayOfInts:[Int]
Adictionaryisanunorderedcollectionofkey-valuepairs.Thevaluescanbeofanytype,includingstructuresandclasses.Thekeyscanbeofanytypeaswell,buttheymustbeunique.Specifically,thekeysmustbehashable,whichallowsthedictionarytoguaranteethatthekeysareuniqueandtoaccessthevalueforagivenkeymoreefficiently.BasicSwifttypessuchasInt,Float,Character,andStringareallhashable.LikeSwiftarrays,Swiftdictionariesarestronglytypedandcanonlycontainkeysandvaluesofthedeclaredtype.Forexample,youmighthaveadictionarythatstorescapitalcitiesbycountry.Thekeysforthisdictionarywouldbethecountrynames,andthevalueswouldbethecitynames.Bothkeysandvalueswouldbestrings,andyouwouldnotbeabletoaddakeyorvalueofanyothertype.Addavariableforsuchadictionary:vararrayOfInts:[Int]
vardictionaryOfCapitalsByCountry:Dictionary<String,String>
Thereisashorthandsyntaxfordeclaringdictionaries,too.UpdatedictionaryOfCapitalsByCountrytousetheshorthand:vararrayOfInts:[Int]
vardictionaryOfCapitalsByCountry:Dictionary<String,String>
vardictionaryOfCapitalsByCountry:[String:String]
Asetissimilartoanarrayinthatitcontainsanumberofelementsofacertaintype.However,setsareunordered,andthemembersmustbeuniqueaswellashashable.Theunorderednessofsetsmakesthemfasterwhenyousimplyneedtodeterminewhethersomethingisamemberofaset.Addavariableforaset:varwinningLotteryNumbers:Set<Int>
Unlikearraysanddictionaries,setsdonothaveashorthandsyntax.
Literalsandsubscripting
Standardtypescanbeassignedliteralvalues,orliterals.Forexample,strisassignedthevalueofastringliteral.Astringliteralisformedwithdoublequotes.ContrasttheliteralvalueassignedtostrwiththenonliteralvalueassignedtoconstStr:varstr="Hello,playground""Hello,playground"
str="Hello,Swift""Hello,Swift"
letconstStr=str"Hello,Swift"
Addtwonumberliteralstoyourplayground:letnumber=4242
letfmStation=91.191.1
Arraysanddictionariescanbeassignedliteralvaluesaswell.Thesyntaxforcreatingliteralarraysanddictionariesresemblestheshorthandsyntaxforspecifyingthesetypes.letcountingUp=["one","two"]["one","two"]
letnameByParkingSpace=[13:"Alice",27:"Bob"][13:"Alice",27:"Bob"]
Swiftalsoprovidessubscriptingasshorthandforaccessingarrays.Toretrieveanelementinanarray,youprovidetheelement’sindexinsquarebracketsafterthearrayname.letcountingUp=["one","two"]["one","two"]
letsecondElement=countingUp[1]"two"
...
Noticethatindex1retrievesthesecondelement;anarray’sindexalwaysstartsat0.Whensubscriptinganarray,besurethatyouareusingavalidindex.Attemptingtoaccessanout-of-boundsindexresultsinatrap.Atrapisaruntimeerrorthatstopstheprogrambeforeitgetsintoanunknownstate.Subscriptingalsoworkswithdictionaries–moreonthatlaterinthischapter.
Initializers
Sofar,youhaveinitializedyourconstantsandvariablesusingliteralvalues.Indoingso,youcreatedinstancesofaspecifictype.Aninstanceisaparticularembodimentofatype.Historically,thistermhasbeenonlyusedwithclasses,butinSwiftitisusedtodescribestructuresandenumerations,too.Forexample,theconstantsecondElementholdsaninstanceofString.Anotherwayofcreatinginstancesisbyusinganinitializeronthetype.Initializersareresponsibleforpreparingthecontentsofanewinstanceofatype.Whenaninitializerisfinished,theinstanceisreadyforaction.Tocreateanewinstanceusinganinitializer,youusethetypenamefollowedbyapairofparenthesesand,ifrequired,arguments.Thissignature–thecombinationoftypeandarguments–correspondstoaspecificinitializer.Somestandardtypeshaveinitializersthatreturnemptyliteralswhennoargumentsaresupplied.Addanemptystring,anemptyarray,andanemptysettoyourplayground.letemptyString=String()""
letemptyArrayOfInts=[Int]()0elements
letemptySetOfFloats=Set<Float>()0elements
Othertypeshavedefaultvalues:
letdefaultNumber=Int()0
letdefaultBool=Bool()false
Typescanhavemultipleinitializers.Forexample,StringhasaninitializerthatacceptsanIntandcreatesastringbasedonthatvalue.letnumber=4242
letmeaningOfLife=String(number)"42"
Tocreateaset,youcanusetheSetinitializerthatacceptsanarrayliteral:letavailableRooms=Set([205,411,412]){412,205,411}
Floathasseveralinitializers.Theparameter-lessinitializerreturnsaninstanceofFloatwiththedefaultvalue.Thereisalsoaninitializerthatacceptsafloating-pointliteral.letdefaultFloat=Float()0.0
letfloatFromLiteral=Float(3.14)3.14
Ifyouusetypeinferenceforafloating-pointliteral,thetypedefaultstoDouble.Createthefollowingconstantwithafloating-pointliteral:leteasyPi=3.143.14
UsetheFloatinitializerthatacceptsaDoubletocreateaFloatfromthisDouble:leteasyPi=3.143.14
letfloatFromDouble=Float(easyPi)3.14
Youcanachievethesameresultbyspecifyingthetypeinthedeclaration.leteasyPi=3.143.14
letfloatFromDouble=Float(easyPi)3.14
letfloatingPi:Float=3.143.14
Properties
Apropertyisavalueassociatedwithaninstanceofatype.Forexample,StringhasthepropertyisEmpty,whichisaBoolthattellsyouwhetherthestringisempty.Array<T>hasthepropertycount,whichisthenumberofelementsinthearrayasanInt.Accessthesepropertiesinyourplayground:letcountingUp=["one","two"]["one","two"]
letsecondElement=countingUp[1]"two"
countingUp.count2
...
letemptyString=String()
emptyString.isEmptytrue
Instancemethods
Aninstancemethodisafunctionthatisspecifictoaparticulartypeandcanbecalledonaninstanceofthattype.Tryouttheappend(_:)instancemethodfromArray<T>.YouwillfirstneedtochangeyourcountingUparrayfromaconstanttoavariable.letcountingUp=["one","two"]
varcountingUp=["one","two"]["one","two"]
letsecondElement=countingUp[1]"two"
countingUp.count
countingUp.append("three")["one","two","three"]
Theappend(_:)methodacceptsanelementofthearray’stypeandaddsittotheendofthearray.Wewilldiscussmethods,includingnaming,inChapter3.
Optionals
Swifttypescanbeoptional,whichisindicatedbyappending?toatypename.varanOptionalFloat:Float?
varanOptionalArrayOfStrings:[String]?
varanOptionalArrayOfOptionalStrings:[String?]?
Anoptionalletsyouexpressthepossibilitythatavariablemaynotstoreavalueatall.Thevalueofanoptionalwilleitherbeaninstanceofthespecifiedtypeornil.Throughoutthisbook,youwillhavemanychancestouseoptionals.Whatfollowsisanexampletogetyoufamiliarwiththesyntaxsothatyoucanfocusontheuseoftheoptionalslater.Imagineagroupofinstrumentreadings:varreading1:Float
varreading2:Float
varreading3:Float
Sometimes,aninstrumentmightmalfunctionandnotreportareading.Youdonotwantthismalfunctionshowingupas,say,0.0.Youwantittobesomethingcompletelydifferentthattellsyoutocheckyourinstrumentortakesomeotheraction.Youcandothisbydeclaringthereadingsasoptionals.Addthesedeclarationstoyourplayground.varreading1:Float?nil
varreading2:Float?nil
varreading3:Float?nil
Asanoptionalfloat,eachreadingcancontaineitheraFloatornil.Ifnotgivenaninitialvalue,thenthevaluedefaultstonil.Youcanassignvaluestoanoptionaljustlikeanyothervariable.Assignfloating-pointliteralstothereadings:reading1=9.89.8
reading2=9.29.2
reading3=9.79.7
However,youcannotusetheseoptionalfloatslikenon-optionalfloats–eveniftheyhavebeenassignedFloatvalues.Beforeyoucanreadthevalueofanoptionalvariable,youmustaddressthepossibilityofitsvaluebeingnil.Thisiscalledunwrappingtheoptional.Youaregoingtotryouttwowaysofunwrappinganoptionalvariable:optionalbindingandforcedunwrapping.Youwillimplementforcedunwrappingfirst.Thisisnotbecauseitisthebetteroption–infact,itisthelesssafeone.Butimplementingforcedunwrappingfirstwillletyouseethedangersandunderstandwhyoptionalbindingistypicallybetter.Toforciblyunwrapanoptional,youappenda!toitsname.First,tryaveragingthereadingsasiftheywerenon-optionalvariables:reading1=9.89.8
reading2=9.29.2
reading3=9.79.7
letavgReading=(reading1+reading2+reading3)/3
Thisresultsinanerrorbecauseoptionalsrequireunwrapping.Forciblyunwrapthereadingstofixtheerror:letavgReading=(reading1+reading2+reading3)/3
letavgReading=(reading1!+reading2!+reading3!)/39.566667
Everythinglooksfine,andyouseethecorrectaverageinthesidebar.Butadangerlurksinyourcode.Whenyouforciblyunwrapanoptional,youtellthecompilerthatyouaresurethattheoptionalwillnotbenilandcanbetreatedasifitwereanormalFloat.Butwhatifyouarewrong?Tofindout,commentouttheassignmentofreading3,whichwillreturnittoitsdefaultvalue,nil.reading1=9.89.8
reading2=9.29.2
reading3=9.7
//reading3=9.7
Younowhaveanerror.Xcodemayhaveopeneditsdebugareaatthebottomoftheplaygroundwithinformationabouttheerror.Ifitdidnot,selectView→DebugArea→ShowDebugArea.Theerrorreads:fatalerror:unexpectedlyfoundnilwhileunwrappinganOptionalvalue
Ifyouforciblyunwrapanoptionalandthatoptionalturnsouttobenil,itwillcauseatrap,stoppingyourapplication.Asaferwaytounwrapanoptionalisoptionalbinding.Optionalbindingworkswithinaconditionalif-letstatement:Youassigntheoptionaltoatemporaryconstantofthecorrespondingnon-optionaltype.Ifyouroptionalhasavalue,thentheassignmentisvalidandyouproceedusingthenon-optionalconstant.Iftheoptionalisnil,thenyoucanhandlethatcasewithanelseclause.Changeyourcodetouseanif-letstatementthattestsforvalidvaluesinallthreereadings.letavgReading=(reading1!+reading2!+reading3!)/3
ifletr1=reading1,
letr2=reading2,
letr3=reading3{
letavgReading=(r1+r2+r3)/3
}else{
leterrorString="Instrumentreportedareadingthatwasnil."
}
reading3iscurrentlynil,soitsassignmenttor3fails,andthesidebarshowstheerrorstring.Toseetheothercaseinaction,restorethelinethatassignsavaluetoreading3.Nowthatallthreereadingshavevalues,allthreeassignmentsarevalid,andthesidebarupdatestoshowtheaverageofthethreereadings.
Subscriptingdictionaries
Recallthatsubscriptinganarraybeyonditsboundscausesatrap.Dictionariesaredifferent.Theresultofsubscriptingadictionaryisanoptional:
letnameByParkingSpace=[13:"Alice",27:"Bob"][13:"Alice",27:"Bob"]
letspace13Assignee:String?=nameByParkingSpace[13]"Alice"
letspace42Assignee:String?=nameByParkingSpace[42]nil
Ifthekeyisnotinthedictionary,theresultwillbenil.Aswithotheroptionals,itiscommontouseif-letwhensubscriptingadictionary:letspace13Assignee:String?=nameByParkingSpace[13]
ifletspace13Assignee=nameByParkingSpace[13]{
print("Key13isassignedinthedictionary!")
}
LoopsandStringInterpolation
Swifthasallthecontrolflowstatementsthatyoumaybefamiliarwithfromotherlanguages:if-else,while,for,for-in,repeat-while,andswitch.Eveniftheyarefamiliar,however,theremaybesomedifferencesfromwhatyouareaccustomedto.ThekeydifferencebetweenthesestatementsinSwiftandinC-likelanguagesisthatwhileenclosingparenthesesarenotnecessaryonthesestatements’expressions,Swiftdoesrequirebracesonclauses.Additionally,theexpressionsforifandwhile-likestatementsmustevaluatetoaBool.SwiftdoesnothavethetraditionalC-styleforloopthatyoumightbeaccustomedto.Instead,youcanaccomplishthesamethingalittlemorecleanlyusingSwift’sRangetypeandthefor-instatement:letrange=0..<countingUp.count
foriinrange{
letstring=countingUp[i]
//Use'string'
}
Themostdirectroutewouldbetoenumeratetheitemsinthearraythemselves:forstringincountingUp{
//Use'string'
}
Whatifyouwantedtheindexofeachiteminthearray?Swift’senumerated()functionreturnsasequenceofintegersandvaluesfromitsargument:for(i,string)incountingUp.enumerated(){
//(0,"one"),(1,"two")
}
Whatarethoseparentheses,youask?Theenumerated()functionreturnsasequenceoftuples.Atupleisanorderedgroupingofvaluessimilartoanarray,excepteachmembermayhaveadistincttype.Inthisexamplethetupleisoftype(Int,String).Wewillnotspendmuchtimeontuplesinthisbook;theyarenotusediniOSAPIsbecauseObjective-Cdoesnotsupportthem.However,theycanbeusefulinyourSwiftcode.Anotherapplicationoftuplesisinenumeratingthecontentsofadictionary:letnameByParkingSpace=[13:"Alice",27:"Bob"]
for(space,name)innameByParkingSpace{
letpermit="Space\(space):\(name)"
}
Didyounoticethatcuriousmarkupinthestringliteral?ThatisSwift’sstringinterpolation.Expressionsenclosedbetween\(and)areevaluatedandinsertedintothestringatruntime.Inthisexampleyouareusinglocalvariables,butanyvalidSwiftexpression,suchasamethodcall,canbeused.Toseethevaluesofthepermitvariableforeachiterationoftheloop,firstclickonthecircularShowResultindicatoratthefarrightendoftheresultssidebarforthelineletpermit="Space\(space):\(name)".Youwillseethecurrentvalueofpermitunderthecode.Control-clickontheresultandselectValueHistory(Figure2.5).Thiscanbeveryusefulfor
visualizingwhatishappeninginyourplaygroundcode’sloops.Figure2.5UsingtheValueHistorytoseetheresultsofstringinterpolation
EnumerationsandtheSwitchStatement
Anenumerationisatypewithadiscretesetofvalues.Defineanenumdescribingpies:enumPieType{
caseapple
casecherry
casepecan
}
letfavoritePie=PieType.apple
Swifthasapowerfulswitchstatementthat,amongotherthings,isgreatformatchingonenumvalues:letname:String
switchfavoritePie{
case.apple:
name="Apple"
case.cherry:
name="Cherry"
case.pecan:
name="Pecan"
}
Thecasesforaswitchstatementmustbeexhaustive:Eachpossiblevalueoftheswitchexpressionmustbeaccountedfor,whetherexplicitlyorviaadefault:case.UnlikeinC,Swiftswitchcasesdonotfallthrough–onlythecodeforthecasethatismatchedisexecuted.(Ifyouneedthefall-throughbehaviorofC,youcanexplicitlyrequestitusingthefallthroughkeyword.)Switchstatementscanmatchonmanytypes,evenranges:letmacOSVersion:Int=...
switchmacOSVersion{
case0...8:
print("Abigcat")
case9:
print("Mavericks")
case10:
print("Yosemite")
case11:
print("ElCapitan")
case12:
print("Sierra")
default:
print("Greetings,peopleofthefuture!What'snewin10.\(macOSVersion)?")
}
Formoreontheswitchstatementanditspatternmatchingcapabilities,seetheControlFlowsectioninApple’sTheSwiftProgrammingLanguageguide.(Moreonthatinjustamoment.)
Enumerationsandrawvalues
Swiftenumscanhaverawvaluesassociatedwiththeircases:
enumPieType:Int{
caseapple=0
casecherry
casepecan
}
Withthetypespecified,youcanaskaninstanceofPieTypeforitsrawValueandtheninitializetheenumtypewiththatvalue.Thisreturnsanoptional,sincetherawvaluemaynotcorrespondwithanactualcaseoftheenum,soitisagreatcandidateforoptionalbinding.letpieRawValue=PieType.pecan.rawValue
//pieRawValueisanIntwithavalueof2
ifletpieType=PieType(rawValue:pieRawValue){
//Gotavalid'pieType'!
}
TherawvalueforanenumisoftenanInt,butitcanbeanyintegerorfloating-pointnumbertypeaswellastheStringandCharactertypes.Whentherawvalueisanintegertype,thevaluesautomaticallyincrementifnoexplicitvalueisgiven.ForPieType,onlytheapplecaseisgivenanexplicitvalue.ThevaluesforcherryandpecanareautomaticallyassignedarawValueof1and2,respectively.Thereismoretoenumerations.Eachcaseofanenumerationcanhaveassociatedvalues.YouwilllearnmoreaboutassociatedvaluesinChapter20.
ExploringApple’sSwiftDocumentation
ToexploreApple’sdocumentationonSwift,startatdeveloper.apple.com/swift.Herearetwoparticularresourcestolookfor.Wesuggestbookmarkingthemandvisitingthemwhenyouwanttoreviewaparticularconceptordigalittledeeper.TheSwiftProgrammingLanguageThisguidedescribesmanyfeaturesofSwift.Itstartswiththebasicsandincludesexamplecodeandlotsofdetail.ItalsocontainsthelanguagereferenceandformalgrammarofSwift.SwiftStandardLibraryReferenceThestandardlibraryreferencelaysoutthedetailsofSwifttypes,protocols,andglobal(orfree)functions.YourhomeworkistobrowsethroughtheTypessectionoftheSwiftStandardLibraryReferenceandthesectionsofTheSwiftProgrammingLanguageguideonTheBasics,StringsandCharacters,andCollectionTypes.Solidifywhatyoulearnedinthischapterandbecomefamiliarwiththeinformationtheseresourcesoffer.Ifyouknowwheretofindthedetailswhenyouneedthem,thenyouwillfeellesspressuretomemorizethem–lettingyoufocusoniOSdevelopmentinstead.
3ViewsandtheViewHierarchy
Overthenextfivechapters,youaregoingtobuildanapplicationnamedWorldTrotter.Whenitiscomplete,thisappwillconvertvaluesbetweendegreesFahrenheitanddegreesCelsius.Inthischapter,youwilllearnaboutviewsandtheviewhierarchythroughcreatingWorldTrotter’sUI.Attheendofthischapter,yourappwilllooklikeFigure3.1.Figure3.1WorldTrotter
Let’sstartwithalittlebitofthetheorybehindviewsandtheviewhierarchy.
ViewBasics
RecallfromChapter1thatviewsareobjectsthatarevisibletotheuser,likebuttons,textfields,andsliders.Viewobjectsmakeupanapplication’sUI.Aview:
isaninstanceofUIVieworoneofitssubclassesknowshowtodrawitselfcanhandleevents,liketouchesexistswithinahierarchyofviewswhoserootistheapplication’swindow
Let’slookattheviewhierarchyingreaterdetail.
TheViewHierarchy
EveryapplicationhasasingleinstanceofUIWindowthatservesasthecontainerforalltheviewsintheapplication.UIWindowisasubclassofUIView,sothewindowisitselfaview.Thewindowiscreatedwhentheapplicationlaunches.Oncethewindowiscreated,otherviewscanbeaddedtoit.Whenaviewisaddedtothewindow,itissaidtobeasubviewofthewindow.Viewsthataresubviewsofthewindowcanalsohavesubviews,andtheresultisahierarchyofviewobjectswiththewindowatitsroot(Figure3.2).Figure3.2Anexampleviewhierarchyandtheinterfacethatitcreates
Oncetheviewhierarchyiscreated,itwillbedrawntothescreen.Thisprocesscanbebrokenintotwosteps:
Eachviewinthehierarchy,includingthewindow,drawsitself.Itrendersitselftoitslayer,whichyoucanthinkofasabitmapimage.(ThelayerisaninstanceofCALayer.)Thelayersofalltheviewsarecompositedtogetheronthescreen.
Figure3.3depictsanotherexampleviewhierarchyandthetwodrawingsteps.
Figure3.3Viewsrenderthemselvesandthenarecompositedtogether
ForWorldTrotter,youaregoingtocreateaninterfacecomposedofdifferentviews.TherewillbefourinstancesofUILabelandoneinstanceofUITextFieldthatwillallowtheusertoenteratemperatureinFahrenheit.Let’sgetstarted.
CreatingaNewProject
InXcode,selectFile→New→Project...(orusethekeyboardshortcutCommand-Shift-N).UndertheiOSsectionatthetop,choosetheSingleViewApplicationtemplateunderApplicationandclickNext.EnterWorldTrotterfortheproductname.MakesurethatSwiftisselectedfromtheLanguagedropdownandthatUniversalisselectedfromtheDevicesdropdown.AlsomakesuretheUseCoreDataboxisunchecked(Figure3.4).ClickNextandthenCreateonthefollowingscreen.Figure3.4ConfiguringWorldTrotter
ViewsandFrames
Whenyouinitializeaviewprogrammatically,youuseitsinit(frame:)designatedinitializer.Thismethodtakesoneargument,aCGRect,thatwillbecometheview’sframe,apropertyonUIView.varframe:CGRect
Aview’sframespecifiestheview’ssizeanditspositionrelativetoitssuperview.Becauseaview’ssizeisalwaysspecifiedbyitsframe,aviewisalwaysarectangle.ACGRectcontainsthemembersoriginandsize.TheoriginisastructureoftypeCGPointandcontainstwoCGFloatproperties:xandy.ThesizeisastructureoftypeCGSizeandhastwoCGFloatproperties:widthandheight(Figure3.5).Figure3.5CGRect
Whentheapplicationislaunched,theviewfortheinitialviewcontrollerisaddedtotheroot-levelwindow.ThisviewcontrollerisrepresentedbytheViewControllerclassdefinedinViewController.swift.WewilldiscusswhataviewcontrollerisinChapter5,butfornow,itissufficienttoknowthataviewcontrollerhasaviewandthattheviewassociatedwiththemainviewcontrollerfortheapplicationisaddedasasubviewofthewindow.BeforeyoucreatetheviewsforWorldTrotter,youaregoingtoaddsomepracticeviewsprogrammaticallytoexploreviewsandtheirpropertiesandseehowtheinterfacesforapplicationsarecreated.
OpenViewController.swiftanddeleteanymethodsthatthetemplatecreated.Yourfileshouldlooklikethis:importUIKit
classViewController:UIViewController{
}
(UIKit,whichyoualsosawinChapter1,isaframework.Aframeworkisacollectionofrelatedclassesandresources.TheUIKitframeworkdefinesmanyoftheUIelementsthatyouruserssee,aswellasotheriOS-specificclasses.Youwillbeusingafewdifferentframeworksasyougothroughthisbook.)Rightaftertheviewcontroller ’sviewisloadedintomemory,itsviewDidLoad()methodiscalled.Thismethodgivesyouanopportunitytocustomizetheviewhierarchy,soitisagreatplacetoaddyourpracticeviews.InViewController.swift,overrideviewDidLoad().CreateaCGRectthatwillbetheframeofaUIView.Next,createaninstanceofUIViewandsetitsbackgroundColorpropertytoblue.Finally,addtheUIViewasasubviewoftheviewcontroller ’sviewtomakeitpartoftheviewhierarchy.(Muchofthiswillnotlookfamiliar.Thatisfine.Wewillexplainmoreafteryouenterthecode.)classViewController:UIViewController{
overridefuncviewDidLoad(){
super.viewDidLoad()
letfirstFrame=CGRect(x:160,y:240,width:100,height:150)
letfirstView=UIView(frame:firstFrame)
firstView.backgroundColor=UIColor.blue
view.addSubview(firstView)
}
}
TocreateaCGRect,youuseitsinitializerandpassinthevaluesfororigin.x,origin.y,size.width,andsize.height.TosetthebackgroundColor,youusetheUIColorclasspropertyblue.ThisisacomputedpropertythatinitializesaninstanceofUIColorthatisconfiguredtobeblue.ThereareanumberofUIColorclasspropertiesforcommoncolors,suchasgreen,black,andclear.Buildandruntheapplication(Command-R).YouwillseeabluerectanglethatistheinstanceofUIView.BecausetheoriginoftheUIView’sframeis(160,240),therectangle’stop-leftcorneris160pointstotherightand240pointsdownfromthetop-leftcornerofitssuperview.Theviewstretches100pointstotherightand150pointsdownfromitsorigin,inaccordancewithitsframe’ssize(Figure3.6).
Figure3.6WorldTrotterwithoneUIView
Notethatthesevaluesareinpoints,notpixels.Ifthevalueswereinpixels,thentheywouldnotbeconsistentacrossdisplaysofdifferentresolutions(i.e.,Retinaversusnon-Retina).Apointisarelativeunitofameasure;itwillbeadifferentnumberofpixelsdependingonhowmanypixelsareinthedisplay.Sizes,positions,lines,andcurvesarealwaysdescribedinpointstoallowfordifferencesindisplayresolution.Figure3.7representstheviewhierarchythatyouhavecreated.
Figure3.7Currentviewhierarchy
EveryinstanceofUIViewhasasuperviewproperty.Whenyouaddaviewasasubviewofanotherview,theinverserelationshipisautomaticallyestablished.Inthiscase,theUIView’ssuperviewistheUIWindow.Let’sexperimentwiththeviewhierarchy.First,inViewController.swift,createanotherinstanceofUIViewwithadifferentframeandbackgroundcolor.overridefuncviewDidLoad(){
super.viewDidLoad()
letfirstFrame=CGRect(x:160,y:240,width:100,height:150)
letfirstView=UIView(frame:firstFrame)
firstView.backgroundColor=UIColor.blue
view.addSubview(firstView)
letsecondFrame=CGRect(x:20,y:30,width:50,height:50)
letsecondView=UIView(frame:secondFrame)
secondView.backgroundColor=UIColor.green
view.addSubview(secondView)
}
Buildandrunagain.Inadditiontothebluerectangle,youwillseeagreensquarenearthetop-leftcornerofthewindow.Figure3.8showstheupdatedviewhierarchy.
Figure3.8Updatedviewhierarchywithtwosubviewsassiblings
NowyouaregoingtoadjusttheviewhierarchysothatoneinstanceofUIViewisasubviewoftheotherUIViewinsteadoftheviewcontroller ’sview.InViewController.swift,addsecondViewasasubviewoffirstView....
letsecondView=UIView(frame:secondFrame)
secondView.backgroundColor=UIColor.green
view.addSubview(secondView)
firstView.addSubview(secondView)
Yourviewhierarchyisnowfourlevelsdeep,asshowninFigure3.9.
Figure3.9OneUIViewasasubviewoftheother
Buildandruntheapplication.NoticethatsecondView’spositiononthescreenhaschanged(Figure3.10).Aview’sframeisrelativetoitssuperview,sothetop-leftcornerofsecondViewisnowinset(20,30)pointsfromthetop-leftcorneroffirstView.
Figure3.10WorldTrotterwithnewhierarchy
(IfthegreeninstanceofUIViewlookssmallerthanitdidpreviously,thatisjustanopticalillusion.Itssizehasnotchanged.)Nowthatyouhaveseenthebasicsofviewsandtheviewhierarchy,youcanstartworkingontheinterfaceforWorldTrotter.Insteadofbuildinguptheinterfaceprogrammatically,youwilluseInterfaceBuildertovisuallylayouttheinterface,asyoudidinChapter1.InViewController.swift,startbyremovingyourpracticecode.overridefuncviewDidLoad(){
super.viewDidLoad()
letfirstFrame=CGRect(x:160,y:240,width:100,height:150)
letfirstView=UIView(frame:firstFrame)
firstView.backgroundColor=UIColor.blue
view.addSubview(firstView)
letsecondFrame=CGRect(x:20,y:30,width:50,height:50)
letsecondView=UIView(frame:secondFrame)
secondView.backgroundColor=UIColor.green
firstView.addSubview(secondView)
}
Nowlet’saddsomeviewstotheinterfaceandsettheirframes.OpenMain.storyboard.Atthebottomofthecanvas,makesuretheViewasbuttonisconfiguredtodisplayaniPhone7device.Fromtheobjectlibrary,dragfiveinstancesofUILabelontothecanvas.SettheirtexttomatchFigure3.11.Asshown,spacethemoutverticallyonthetophalfoftheinterfaceandcenterthemhorizontally.Figure3.11Addinglabelstotheinterface
SelectthetoplabelsoyoucanseeitsframeinInterfaceBuilder.Openitssizeinspector–thefifthtabintheutilitiesarea.(ThekeyboardshortcutsfortheutilitiestabsareCommand-Optionplusthetabnumber.Thesizeinspectoristhefifthtab,soitskeyboardshortcutis
Command-Option-5.)UndertheViewsection,findFrameRectangle.(Ifyoudonotseeit,youmightneedtoselectitfromtheShowpop-upmenu.)Thevaluesshownaretheview’sframe,andtheydictatethepositionoftheviewonscreen(Figure3.12).Figure3.12Viewframevalues
BuildandruntheapplicationontheiPhone7simulator.TheinterfaceonthesimulatorwilllookidenticaltotheinterfacethatyoulaidoutinInterfaceBuilder.
Customizingthelabels
Let’smaketheinterfacelookalittlebitbetterbycustomizingtheviewproperties.InMain.storyboard,selectthebackgroundview.Opentheattributesinspectorandgivetheappanewbackgroundcolor:FindandclicktheBackgrounddropdownandclickOther....Selectthesecondtab(theColorSliderstab)andchooseRGBSlidersfromthedropdown.Intheboxnearthebottom,enteraHexColor#ofF5F4F1(Figure3.13).Thiswillgivethebackgroundawarmgraycolor.
Figure3.13Changingthebackgroundcolor
Youcancustomizeattributescommontoselectedviewssimultaneously.Youwillusethistogivemanyofthelabelsalargerfontsizeaswellasaburntorangetextcolor.SelectthetoptwoandbottomtwolabelsbyCommand-clickingtheminthedocumentoutline.Makesuretheattributesinspectorisopenandupdatethetextcolor:UndertheLabelsection,findColorandopenthepop-upmenu.SelecttheColorSliderstabagainandenteraHexColor#ofE15829.Nowlet’supdatethefont.Selectthe212and100labels.UndertheLabelsectionintheattributesinspector,findFontandclickonthetexticonnexttothecurrentfont.Inthepopoverthatappears,maketheSize70(Figure3.14).Selecttheremainingthreelabels.OpentheirFontpop-upandmaketheSize36.
Figure3.14Customizingthelabels’font
Nowthatthefontsizeislarger,thetextnolongerfitswithintheboundsofthelabel.Youcouldresizethelabelsmanually,butthereisaneasierway.Selectthetoplabelonthecanvas.FromXcode’sEditormenu,selectSizetoFitContent(Command-=).Thiswillresizethelabeltoexactlyfititstextcontents.Repeattheprocessfortheotherfourlabels.(Youcanselectallfourlabelstoresizethemallatonce.)Nowmovethelabelssothattheyareagainnicelyalignedverticallyandcenteredhorizontally(Figure3.15).
Figure3.15Updatingthelabelframes
BuildandruntheapplicationontheiPhone7simulator.NowbuildandruntheapplicationontheiPhone7Plussimulator.Noticethatthelabelsarenolongercentered–instead,theyappearshiftedslightlytotheleft.Youhavejustseentwoofthemajorproblemswithabsoluteframes.First,whenthecontentschange(likewhenyouchangedthefontsize),theframesdonotautomaticallyupdate.Second,theviewdoesnotlookequallygoodondifferentsizesofscreens.Ingeneral,youshouldnotuseabsoluteframesforyourviews.Instead,youshoulduseAutoLayouttoflexiblycomputetheframesforyoubasedonconstraintsthatyouspecifyforeachview.Forexample,whatyoureallywantforWorldTrotterisforthelabelstoremainthesamedistancefromthetopofthescreenandtoremainhorizontallycenteredwithintheirsuperview.Theyshouldalsoupdateifthefontortextofthelabelschange.Thisiswhatyouwillaccomplishinthenextsection.
TheAutoLayoutSystem
Beforeyoucanfixthelabelstohavethemlayoutflexibly,youneedtolearnalittletheoryabouttheAutoLayoutsystem.AsyousawinChapter1,absolutecoordinatesmakeyourlayoutfragilebecausetheyassumethatyouknowthesizeofthescreenaheadoftime.UsingAutoLayout,youcandescribethelayoutofyourviewsinarelativewaythatenablestheirframestobedeterminedatruntimesothattheframes’definitionscantakeintoaccountthescreensizeofthedevicethattheapplicationisrunningon.
Thealignmentrectangleandlayoutattributes
TheAutoLayoutsystemisbasedonthealignmentrectangle.Thisrectangleisdefinedbyseverallayoutattributes(Figure3.16).Figure3.16Layoutattributesdefininganalignmentrectangleofaview
Width/Height
Thesevaluesdeterminethealignmentrectangle’ssize.Top/BottomLeft/Right
Thesevaluesdeterminethespacingbetweenthegivenedgeofthealignmentrectangleandthealignmentrectangleofanotherviewinthehierarchy.CenterXCenterY
Thesevaluesdeterminethecenterpointofthealignmentrectangle.FirstBaselineLastBaseline
Thesevaluesarethesameasthebottomattributeformost,butnotall,views.Forexample,UITextFielddefinesitsbaselinesasthebottomofthetextitdisplaysratherthanthebottomofthealignmentrectangle.Thiskeeps“descenders”(thepartsofletterslike“g”and“p”thatdescendbelowthebaseline)frombeingobscuredbyaviewrightbelowthetextfield.Formultilinetextlabelsandtextviews,thefirstandlastbaselinerefertothefirstandlastlineoftext.Inallothersituations,thefirstandlastbaselinearethesame.
LeadingTrailing
Thesevaluesarelanguage-specificattributes.Ifthedeviceissettoalanguagethatreadslefttoright(e.g.,English),thentheleadingattributeisthesameastheleftattributeandthetrailingattributeisthesameastherightattribute.Ifthelanguagereadsrighttoleft(e.g.,Arabic),thentheleadingattributeisontherightandthetrailingattributeisontheleft.InterfaceBuilderautomaticallyprefersleadingandtrailingoverleftandright,and,ingeneral,youshouldaswell.Bydefault,everyviewhasanalignmentrectangle,andeveryviewhierarchyusesAutoLayout.Thealignmentrectangleisverysimilartotheframe.Infact,thesetworectanglesareoftenthesame.Whereastheframeencompassestheentireview,thealignmentrectangleonlyencompassesthecontentthatyouwishtouseforalignmentpurposes.Figure3.17showsanexamplewheretheframeandthealignmentrectanglearedifferent.Figure3.17Framevsalignmentrectangle
Youcannotdefineaview’salignmentrectangledirectly.Youdonothaveenoughinformation(likescreensize)todothat.Instead,youprovideasetofconstraints.Takentogether,theseconstraintsenablethesystemtodeterminethelayoutattributes,andthusthealignmentrectangle,foreachviewintheviewhierarchy.
Constraints
Aconstraintdefinesaspecificrelationshipinaviewhierarchythatcanbeusedtodeterminealayoutattributeforoneormoreviews.Forexample,youmightaddaconstraintlike,“Theverticalspacebetweenthesetwoviewsshouldalwaysbe8points,”or,“Theseviewsmustalwayshavethesamewidth.”Aconstraintcanalsobeusedtogiveaviewafixedsize,like,“Thisview’sheightshouldalwaysbe44points.”Youdonotneedaconstraintforeverylayoutattribute.Somevaluesmaycomedirectlyfromaconstraint;otherswillbecomputedbythevaluesofrelatedlayoutattributes.Forexample,ifaview’sconstraintssetitsleftedgeanditswidth,thentherightedgeisalreadydetermined(leftedge+width=rightedge,always).Asageneralruleofthumb,youneedatleasttwoconstraintsperdimension(horizontalandvertical).If,afteralloftheconstraintshavebeenconsidered,thereisstillanambiguousormissing
valueforalayoutattribute,thentherewillbeerrorsandwarningsfromAutoLayoutandyourinterfacewillnotlookasyouexpectonalldevices.Debuggingtheseproblemsisimportant,andyouwillgetsomepracticelaterinthischapter.Howdoyoucomeupwithconstraints?Let’sseehow,usingthelabelsthatyouhavelaidoutonthecanvas.First,describewhatyouwanttheviewtolooklikeindependentofscreensize.Forexample,youmightsaythatyouwantthetoplabeltobe:
8pointsfromthetopofthescreencenteredhorizontallyinitssuperviewaswideandastallasitstext
ToturnthisdescriptionintoconstraintsinInterfaceBuilder,itwillhelptounderstandhowtofindaview’snearestneighbor.Thenearestneighboristheclosestsiblingviewinthespecifieddirection(Figure3.18).Figure3.18Nearestneighbor
Ifaviewdoesnothaveanysiblingsinthespecifieddirection,thenthenearestneighborisitssuperview,alsoknownasitscontainer.Nowyoucanspellouttheconstraintsforthelabel:
1. Thelabel’stopedgeshouldbe8pointsawayfromitsnearestneighbor(whichisitscontainer–theviewoftheViewController).
2. Thelabel’scentershouldbethesameasitssuperview’scenter.3. Thelabel’swidthshouldbeequaltothewidthofitstextrenderedatitsfontsize.4. Thelabel’sheightshouldbeequaltotheheightofitstextrenderedatitsfontsize.
Ifyouconsiderthefirstandfourthconstraints,youcanseethatthereisnoneedtoexplicitlyconstrainthelabel’sbottomedge.Itwillbedeterminedfromtheconstraintsonthelabel’stopedgeandthelabel’sheight.Similarly,thesecondandthirdconstraintstogetherdeterminethelabel’srightandleftedges.Nowthatyouhaveaplanforthetoplabel,youcanaddtheseconstraints.ConstraintscanbeaddedusingInterfaceBuilderorincode.ApplerecommendsthatyouaddconstraintsusingInterfaceBuilderwheneverpossible,andthatiswhatyouwilldohere.However,ifyourviewsarecreatedandconfiguredprogrammatically,thenyoucanaddconstraintsincode.InChapter6,youwillpracticethatapproach.
AddingconstraintsinInterfaceBuilder
Let’sgetstartedconstrainingthattoplabel.Selectthetoplabelonthecanvas.Inthebottom-rightcornerofthecanvas,findtheAutoLayoutconstraintmenus(Figure3.19).Figure3.19AutoLayoutconstraintmenus
Clickthe icon(thefourthfromtheleft)torevealtheAddNewConstraintsmenu.Thismenushowsyouthecurrentsizeandpositionofthelabel.AtthetopoftheAddNewConstraintsmenuarefourvaluesthatdescribethelabel’scurrentspacingfromitsnearestneighboronthecanvas.Forthislabel,youareonlyinterestedinthetopvalue.Toturnthisvalueintoaconstraint,clickthetopredstrutseparatingthevaluefromthesquareinthemiddle.Thestrutwillbecomeasolidredline.Inthemiddleofthemenu,findthelabel’sWidthandHeight.ThevaluesnexttoWidthandHeightindicatethecurrentcanvasvalues.Toconstrainthelabel’swidthandheighttothecurrentcanvasvalues,checktheboxesnexttoWidthandHeight.ThebuttonatthebottomofthemenureadsAdd3Constraints.Clickthisbutton.Atthispoint,youhavenotspecifiedenoughconstraintstofullydeterminethealignmentrectangle.Theredoutlinearoundthelabelindicatesthatitsalignmentrectangleisincompletelydefined,andInterfaceBuilderwillhelpyoudeterminewhattheproblemis.Inthetop-rightcornerofInterfaceBuilder,noticetheyellowwarningsign(Figure3.20).Clickonthisicontorevealtheissue:Horizontalpositionisambiguousfor"212".
Figure3.20Horizontalambiguity
Youhaveaddedtwoverticalconstraints(atopedgeconstraintandaheightconstraint),butyouhaveonlyaddedonehorizontalconstraint(awidthconstraint).Havingonlyoneconstraintmakesthehorizontalpositionofthelabelambiguous.Youwillfixthisissuebyaddingacenteralignmentconstraintbetweenthelabelanditssuperview.Withthetoplabelstillselected,clickthe icon(thethirdiconfromtheleft)torevealtheAlignmenu.Ifyouhavemultipleviewsselected,thismenuwillallowyoutoalignattributesamongtheviews.Becauseyouhaveonlyselectedonelabel,theonlyoptionsyouaregivenaretoaligntheviewwithinitscontainer.IntheAlignmenu,checkHorizontallyinContainer(donotclickAdd1Constraintyet).Onceyouaddthisconstraint,therewillbeenoughconstraintstofullydeterminethealignmentrectangle.Toensurethattheframeofthelabelmatchestheconstraintsspecified,opentheUpdateFramespop-upmenufromtheAlignmenuandselectItemsofNewConstraints.Thiswillrepositionthelabeltomatchtheconstraintsthathavebeenadded.NowclickonAdd1Constrainttoaddthecenteringconstraintandrepositionthelabel.Thelabel’sconstraintsareallbluenowthatthealignmentrectangleforthelabelisfullyspecified.Additionally,thewarningatthetop-rightcornerofInterfaceBuilderisnowgone.BuildandruntheapplicationontheiPhone7simulatorandtheiPhone7Plussimulator.Thetoplabelwillremaincenteredinbothsimulators.
Intrinsiccontentsize
Althoughthetoplabel’spositionisflexible,itssizeisnot.Thisisbecauseyouhaveaddedexplicitwidthandheightconstraintstothelabel.Ifthetextorfontweretochange,youwouldbeinthesamepositionyouwereinearlier.Thesizeoftheframeisabsolute,sotheframewouldnothugtothecontent.Thisiswheretheintrinsiccontentsizeofaviewcomesintoplay.Youcanthinkoftheintrinsiccontentsizeasthesizethataview“wants”tobe.Forlabels,thissizeisthesizeofthetextrenderedatthegivenfont.Forimages,thisisthesizeoftheimageitself.Aview’sintrinsiccontentsizeactsasimplicitwidthandheightconstraints.Ifyoudonotspecifyconstraintsthatexplicitlydeterminethewidth,theviewwillbeitsintrinsicwidth.Thesamegoesfortheheight.Withthisknowledge,letthetoplabelhaveaflexiblesizebyremovingtheexplicitwidthandheightconstraints.InMain.storyboard,selectthewidthconstraintonthelabel.Youcandothisbyclickingontheconstraintonthecanvas.Alternatively,inthedocumentoutline,youcanclickonthe
disclosuretrianglenexttothe212label,thendisclosethelistofconstraintsforthelabel(Figure3.21).Figure3.21Selectingthewidthconstraint
Onceyouhaveselectedthewidthconstraint,presstheDeletekey.Dothesamefortheheightconstraint.Noticethattheconstraintsforthelabelarestillblue.Becausethewidthandheightarebeinginferredfromthelabel’sintrinsiccontentsize,therearestillenoughconstraintstodeterminethelabel’salignmentrectangle.
Misplacedviews
Asyouhaveseen,blueconstraintsindicatethatthealignmentrectangleforaviewisfully
specified.Orangeconstraintsoftenindicateamisplacedview.ThismeansthattheframefortheviewinInterfaceBuilderisdifferentthantheframethatAutoLayouthascomputed.Amisplacedviewisveryeasytofix.Thatisgood,becauseitisalsoaverycommonissuethatyouwillencounterwhenworkingwithAutoLayout.Giveyourtoplabelamisplacedviewsothatyoucanseehowtoresolvethisissue.Resizethetoplabelonthecanvasusingtheresizecontrolsandlookfortheyellowwarninginthetop-rightcornerofthecanvas.Clickonthiswarningicontorevealtheproblem:Framefor"212"willbedifferentatruntime(Figure3.22).Figure3.22Misplacedviewwarning
Asthewarningsays,theframeatruntimewillnotbethesameastheframespecifiedonthecanvas.Ifyoulookclosely,youwillseeanorangedottedlinethatindicateswhattheruntimeframewillbe.Buildandruntheapplication.NoticethatthelabelisstillcentereddespitethenewframethatyougaveitinInterfaceBuilder.Thismightseemgreat–yougettheresultthatyouwant,afterall.ButthedisconnectbetweenwhatyouhavespecifiedinInterfaceBuilderandtheconstraintscomputedbyAutoLayoutwillcauseproblemsdownthelineasyoucontinuetobuildyourviews.Let’sfixthemisplacedview.Backinthestoryboard,selectthetoplabelonthecanvas.Clickthe icon(theleft-mosticon)toupdatetheframeofthelabeltomatchtheframethattheconstraintswillcompute.YouwillgetveryusedtoupdatingtheframesofviewsasyouworkwithAutoLayout.Onewordofcaution:Ifyoutrytoupdatetheframesforaviewthatdoesnothaveenoughconstraints,youwillalmostcertainlygetunexpectedresults.Ifthathappens,undothechangeandinspecttheconstraintstoseewhatismissing.Atthispoint,thetoplabelisingoodshape.Ithasenoughconstraintstodetermineitsalignmentrectangle,andtheviewislayingoutthewayyouwant.BecomingproficientwithAutoLayouttakesalotofexperience,sointhenextsectionyouare
goingtoremovetheconstraintsfromthetoplabelandthenaddconstraintstoallofthelabels.
Addingmoreconstraints
Let’sfleshouttheconstraintsfortherestoftheviews.Beforeyoudothat,youwillremovetheexistingconstraintsfromthetoplabel.Selectthetoplabelonthecanvas.OpentheResolveAutoLayoutIssuesmenuandselectClearConstraintsfromtheSelectedViewssection(Figure3.23).Figure3.23Clearingconstraints
Youaregoingtoaddtheconstraintstoalloftheviewsintwosteps.Firstyouwillcenterthetoplabelhorizontallywithinthesuperview.Thenyouwilladdconstraintsthatpinthetopofeachlabeltoitsnearestneighborwhilealigningthecentersofallofthelabels.Selectthetoplabel.OpentheAlignmenuandchooseHorizontallyinContainerwithaconstantof0.MakesurethatUpdateFrameshasNoneselected;rememberthatyoudonotwanttoupdatetheframeofaviewthatdoesnothaveenoughconstraints,andthisoneconstraintwillcertainlynotprovideenoughinformationtocomputethealignmentrectangle.GoaheadandAdd1Constraint.Nowselectallfivelabelsonthecanvas.Itcanbeveryconvenienttoaddconstraintstomultipleviewssimultaneously.OpentheAddNewConstraintsmenuandmakethefollowingchoices:1. Selectthetopstrutandmakesureithasaconstantof8.2. FromtheAlignmenu,chooseHorizontalCenters.3. FromtheUpdateFramesmenu,chooseItemsofNewConstraints.
YourmenushouldmatchFigure3.24.Onceitdoes,clickAdd9Constraints.ThiswilladdtheconstraintstotheviewsandupdatetheirframestoreflecttheAutoLayoutchanges.
Figure3.24AddingmoreconstraintswiththeAddNewConstraintsmenu
BuildandruntheapplicationontheiPhone7simulator.Theviewswillbecenteredwithintheinterface.NowbuildandruntheapplicationontheiPhone7Plussimulator.Unlikeearlierinthechapter,allofthelabelsremaincenteredonthelargerinterface.AutoLayoutisacrucialtechnologyforeveryiOSdeveloper.Ithelpsyoucreateflexiblelayoutsthatworkacrossarangeofdevicesandinterfacesizes.Italsotakesalotofpracticetomaster.YouwillgetalotofexperienceusingAutoLayoutasyouworkthroughthisbook.
BronzeChallenge:MoreAutoLayoutPractice
RemovealloftheconstraintsfromtheViewControllerinterfaceandthenaddthembackin.Trytodothiswithoutconsultingthebook.
4TextInputandDelegation
WorldTrotterlooksgood,butsofaritdoesnotdoanything.Inthischapter,youaregoingtoaddaninstanceofUITextFieldtoWorldTrotter.ThetextfieldwillallowtheusertotypeinatemperatureinFahrenheitthatwillthenbeconvertedtoCelsiusanddisplayedontheinterface(Figure4.1).Figure4.1WorldTrotterwithaUITextField
TextEditing
ThefirstthingyouaregoingtodoisaddaUITextFieldtotheinterfaceandsetuptheconstraintsforthattextfield.Thistextfieldwillreplacethetoplabelintheinterfacethatcurrentlyhasthetext“212.”OpenMain.storyboard.SelectthetoplabelandpresstheDeletekeytoremovethissubview.Theconstraintsforalloftheotherlabelswillturnredbecausetheywerealldirectlyorindirectlyanchoredtothattoplabel(Figure4.2).ThatisOK;youwillfixthemshortly.Figure4.2Ambiguousframesforthelabels
OpentheobjectlibraryanddragaTextFieldtothetopofthecanvaswherethelabelyoudeletedwaspreviouslyplaced.Nowsetuptheconstraintsforthistextfield.Withthetextfieldselected,opentheAlignmenuandaligntheviewHorizontallyinContainerwithaconstantof0.MakesurethatUpdateFramesissettoNoneandthenAdd1Constraint.NowopentheAddNewConstraintsmenu.Givethetextfieldatopedgeconstraintof8points,abottomedgeconstraintof8points,andawidthof250(Figure4.3).Addthesethreeconstraints.
Figure4.3TextfieldAddNewConstraintsmenu
Finally,selectthetextfieldandthelabelrightbelowit.OpentheAlignmenu,selectHorizontalCenterswithaconstantof0,UpdateFramesforAllFramesinContainer,andfinallyAdd1Constraint(Figure4.4).
Figure4.4Aligningthetextfield
Next,customizesomeofthetextfieldproperties.Opentheattributesinspectorforthetextfieldandmakethefollowingchanges:
Setthetextcolor(fromtheColormenu)toburntorange.SetthefontsizetoSystem70.SettheAlignmenttocentered.Settheplaceholdertexttobevalue.Thisisthetextthatwillbedisplayedwhentheuserhasnotenteredanytext.SettheBorderStyletobenone,whichisthefirstelementofthesegmentedcontrolwiththedottedlines.
TheattributesinspectorforyourtextfieldshouldlooklikeFigure4.5.
Figure4.5Textfieldattributesinspector
Becausethetextfield’sfontchanged,theviewsonthecanvasnowaremisplaced.Selectthegraybackgroundview,opentheResolveAutoLayoutIssuesmenu,andselectUpdateFramesfromtheAllViewsinViewControllersection.Thetextfieldandlabelswillberepositionedtomatchtheirconstraints(Figure4.6).
Figure4.6Updatedframes
Buildandruntheapplication.Taponthetextfieldandentersometext.Ifyoudonotseethekeyboard,clickthesimulator ’sHardwaremenuandselectKeyboard→ToggleSoftwareKeyboardorusethekeyboardshortcutCommand-K.Bydefault,thesimulatortreatsyourcomputer ’skeyboardasaBluetoothkeyboardconnectedtothesimulator.Thisisnotusuallywhatyouwant.Instead,youwantthesimulatortomimicaniOSdevicerunningwithoutanyaccessoriesattachedbyusingtheonscreenkeyboard.
Keyboardattributes
Whenatextfieldistapped,thekeyboardautomaticallyslidesupontothescreen.(Youwillseewhythishappenslaterinthischapter.)Thekeyboard’sappearanceisdeterminedbyasetoftheUITextField’spropertiescalledtheUITextInputTraits.Oneofthesepropertiesisthetypeofkeyboardthatisdisplayed.Forthisapplication,youwanttousethedecimalpad.Intheattributesinspectorforthetextfield,findtheattributenamedKeyboardTypeandchooseDecimalPad.Inthesamesection,youcanseesomeoftheothertextinputtraitsthatyoucancustomizeforthekeyboard.ChangebothCorrectionandSpellCheckingtoNo(Figure4.7).
Figure4.7Keyboardtextinputtraits
Buildandruntheapplication.Tappingonthetextfieldwillnowrevealthedecimalpad.
Respondingtotextfieldchanges
ThenextstepoftheprojectwillbetoupdatetheCelsiuslabelwhentextistypedintothetextfield.Youaregoingtoneedtowritesomecodetodothis.Specifically,thiscodewillgointotheviewcontrollersubclassassociatedwiththisinterface.Currently,thatcorrespondswiththeViewControllerclassdefinedinViewController.swift.However,ViewControllerisnotaverydescriptivenameforaviewcontrollerthatmanagestheconversionbetweenFahrenheitandCelsius.Havingdescriptivetypenamesallowsyoutomoreeasilymaintainyourprojectsastheygrowlarger.Youaregoingtodeletethisfileandreplaceitwithamoredescriptiveclass.Intheprojectnavigator,findViewController.swiftanddeleteit.ThencreateanewfilebyselectingFile→New→File...(orpressCommand-N).WithiOSselectedatthetop,chooseSwiftFileundertheSourcelabelandclickNext.Onthenextpane,namethisfileConversionViewController.SavethefileintheWorldTrottergroupwithintheWorldTrotterprojectandmakesurethattheWorldTrottertargetischecked,asshowninFigure4.8.ClickCreate,andXcodewillopenConversionViewController.swiftintheeditor.
Figure4.8SavingaSwiftfile
InConversionViewController.swift,importUIKitanddefineanewviewcontrollernamedConversionViewController.importFoundation
importUIKit
classConversionViewController:UIViewController{
}
NowyouneedtoassociatetheinterfaceyoucreatedinMain.storyboardwiththisnewviewcontroller.OpenMain.storyboardandselecttheViewController,eitherinthedocumentoutlineorbyclickingtheyellowcircleabovetheinterface.Opentheidentityinspector,whichisthethirdtabintheutilitiesview(Command-Option-3).Atthetop,findtheCustomClasssectionandchangetheClasstoConversionViewController(Figure4.9).(YouwilllearnwhatallofthisisdoinginChapter5.)Figure4.9Changingthecustomclass
YousawinChapter1thatabuttoncansendeventstoacontrollerwhenthebuttonistapped.Textfieldsareanothercontrol(bothUIButtonandUITextFieldaresubclassesofUIControl)andcansendaneventwhenthetextchanges.
Togetthisallworking,youwillneedtocreateanoutlettotheCelsiustextlabelandcreateanactionforthetextfieldtocallwhenthetextchanges.OpenConversionViewController.swiftanddefinethisoutletandaction.Fornow,thelabelwillbeupdatedwithwhatevertexttheusertypesintothetextfield.classConversionViewController:UIViewController{
@IBOutletvarcelsiusLabel:UILabel!
@IBActionfuncfahrenheitFieldEditingChanged(_textField:UITextField){
celsiusLabel.text=textField.text
}
}
OpenMain.storyboardtomaketheseconnections.TheoutletwillbeconnectedjustasyoudidinChapter1.Control-dragfromtheConversionViewControllertotheCelsiuslabel(theonethatcurrentlysays“100”)andconnectittothecelsiusLabel.Connectingtheactionwillbealittledifferentbecauseyouwanttheactiontobetriggeredwhentheeditingchanges.Selectthetextfieldonthecanvasandopenitsconnectionsinspectorfromtheutilitypane(theright-mosttab,orCommand-Option-6).Theconnectionsinspectorallowsyoutomakeconnectionsandseewhatconnectionshavealreadybeenmade.YouaregoingtohavechangestothetextfieldtriggertheactionyoudefinedinConversionViewController.Intheconnectionsinspector,locatetheSentEventssectionandtheEditingChangedevent.ClickanddragfromthecircletotherightofEditingChangedtotheConversionViewControllerandclickthefahrenheitFieldEditingChanged:actioninthepop-upmenu(Figure4.10).Figure4.10Connectingtheeditingchangedevent
Buildandruntheapplication.Tapthetextfieldandtypesomenumbers.TheCelsiuslabelwillmimicthetextthatistypedin.Nowdeletethetextinthetextfieldandnoticehowthelabelseemstogoaway.Alabelwithnotexthasanintrinsiccontentwidthandheightof0,sothelabelsbelowitmoveup.Let’sfixthisissue.InConversionViewController.swift,updatefahrenheitFieldEditingChanged(_:)todisplay“???”ifthetextfieldisempty.@IBActionfuncfahrenheitFieldEditingChanged(_textField:UITextField){
celsiusLabel.text=textField.text
iflettext=textField.text,!text.isEmpty{
celsiusLabel.text=text
}else{
celsiusLabel.text="???"
}
}
Ifthetextfieldhastextandthattextisnotempty,itwillbesetonthecelsiusLabel.Ifeitherofthoseconditionsarenottrue,thenthecelsiusLabelwillbegiventhestring“???”.Buildandruntheapplication.Addsometext,deleteit,andconfirmthatthecelsiusLabelispopulatedwith“???”whenthetextfieldisempty.
Dismissingthekeyboard
Currently,thereisnowaytodismissthekeyboard.Let’saddthatfunctionality.OnecommonwayofdoingthisisbydetectingwhentheusertapstheReturnkeyandusingthatactiontodismissthekeyboard;youwillusethisapproachinChapter14.BecausethedecimalpaddoesnothaveaReturnkey,youwillallowtheusertotaponthebackgroundviewtotriggerthedismissal.Whenthetextfieldistapped,themethodbecomeFirstResponder()iscalledonit.Thisisthemethodthat,amongotherthings,causesthekeyboardtoappear.Todismissthekeyboard,youcallthemethodresignFirstResponder()onthetextfield.YouwilllearnmoreaboutthesemethodsinChapter14.ForWorldTrotter,youwillneedanoutlettothetextfieldandamethodthatistriggeredwhenthebackgroundviewistapped.ThismethodwillcallresignFirstResponder()onthetextfieldoutlet.Let’stakecareofthecodefirst.OpenConversionViewController.swiftanddeclareanoutletnearthetoptoreferencethetextfield.@IBOutletvarcelsiusLabel:UILabel!
@IBOutletvartextField:UITextField!
Nowimplementanactionmethodthatwilldismissthekeyboardwhencalled.(Inthecodeabove,weincludedexistingcodesothatyoucouldpositionthenewcodecorrectly.Inthecodebelow,wedonotprovidethatcontextbecausethepositionofthenew
codeisnotimportantsolongasitiswithinthecurlybracesforthetypebeingimplemented–inthiscase,theConversionViewControllerclass.Whenacodeblockincludesallnewcode,wesuggestthatyouputitattheendofthetype’simplementation,justinsidethefinalclosingbrace.InChapter15,youwillseehowtoeasilynavigatewithinanimplementationfilewhenyourfilesgetlongerandmorecomplex.)@IBActionfuncdismissKeyboard(_sender:UITapGestureRecognizer){
textField.resignFirstResponder()
}
Twothingsarestillneeded:ThetextFieldoutletneedstobeconnectedinthestoryboardfile,andyouneedawayoftriggeringthedismissKeyboard(_:)methodyouadded.Totakecareofthefirstitem,openMain.storyboardandselecttheConversionViewController.Control-dragfromtheConversionViewControllertothetextfieldonthecanvasandconnectittothetextFieldoutlet.Nowyouneedawayoftriggeringthemethodyouimplemented.Youwilluseagesturerecognizertoaccomplishthis.AgesturerecognizerisasubclassofUIGestureRecognizerthatdetectsaspecifictouchsequenceandcallsanactiononitstargetwhenthatsequenceisdetected.Therearegesturerecognizersthatdetecttaps,swipes,longpresses,andmore.Inthischapter,youwilluseaUITapGestureRecognizertodetectwhentheusertapsthebackgroundview.YouwilllearnmoreaboutgesturerecognizersinChapter19.InMain.storyboard,findTapGestureRecognizerintheobjectlibrary.DragthisobjectontothebackgroundviewfortheConversionViewController.Youwillseeareferencetothisgesturerecognizerinthescenedock,therowoficonsabovethecanvas.Control-dragfromthegesturerecognizerinthescenedocktotheConversionViewControllerandconnectittothedismissKeyboard:method(Figure4.11).
Figure4.11Connectingthegesturerecognizeraction
ImplementingtheTemperatureConversion
Withthebasicsoftheinterfacewiredup,let’simplementtheconversionfromFahrenheittoCelsius.YouaregoingtostorethecurrentFahrenheitvalueandcomputetheCelsiusvaluewheneverthetextfieldchanges.InConversionViewController.swift,addapropertyfortheFahrenheitvalue.Thiswillbeanoptionalmeasurementfortemperature(aMeasurement<UnitTemperature>?).@IBOutletvarcelsiusLabel:UILabel!
varfahrenheitValue:Measurement<UnitTemperature>?
Thereasonthispropertyisoptionalisbecausetheusermightnothavetypedinanumber,similartotheemptystringissueyoufixedearlier.NowaddacomputedpropertyfortheCelsiusvalue.ThisvaluewillbecomputedbasedontheFahrenheitvalue.varfahrenheitValue:Measurement<UnitTemperature>?
varcelsiusValue:Measurement<UnitTemperature>?{
ifletfahrenheitValue=fahrenheitValue{
returnfahrenheitValue.converted(to:.celsius)
}else{
returnnil
}
}
FirstyouchecktoseewhetherthereisaFahrenheitvalue.Ifthereis,youconvertthisvaluetotheequivalentvalueinCelsius.IfthereisnoFahrenheitvalue,thenyoucannotcomputeaCelsiusvalueandsoyoureturnnil.AnytimetheFahrenheitvaluechanges,theCelsiuslabelneedstobeupdated.Takecareofthatnext.AddamethodtoConversionViewControllerthatupdatesthecelsiusLabel.funcupdateCelsiusLabel(){
ifletcelsiusValue=celsiusValue{
celsiusLabel.text="\(celsiusValue.value)"
}else{
celsiusLabel.text="???"
}
}
YouwantthismethodtobecalledwhenevertheFahrenheitvaluechanges.Todothis,youwilluseapropertyobserver,whichisachunkofcodethatgetscalledwheneveraproperty’svaluechanges.Apropertyobserverisdeclaredusingcurlybracesimmediatelyafterthepropertydeclaration.Insidethebraces,youdeclareyourobserverusingeitherwillSetordidSet,dependingonwhetheryouwanttobenotifiedimmediatelybeforeorimmediatelyafterthepropertyvaluechanges,respectively.AddapropertyobservertofahrenheitValuethatgetscalledafterthepropertyvalue
changes.varfahrenheitValue:Measurement<UnitTemperature>?{
didSet{
updateCelsiusLabel()
}
}
(Onesmallnote:Propertyobserversarenottriggeredwhenthepropertyvalueischangedfromwithinaninitializer.)Withthatlogicinplace,youcannowupdatetheFahrenheitvaluewhenthetextfieldchanges(which,inturn,willtriggeranupdateoftheCelsiuslabel).InfahrenheitFieldEditingChanged(_:),deleteyourearliernonconvertingimplementationandinsteadupdatetheFahrenheitvalue.@IBActionfuncfahrenheitFieldEditingChanged(_textField:UITextField){
iflettext=textField.text,!text.isEmpty{
celsiusLabel.text=text
}else{
celsiusLabel.text="???"
}
iflettext=textField.text,letvalue=Double(text){
fahrenheitValue=Measurement(value:value,unit:.fahrenheit)
}else{
fahrenheitValue=nil
}
}
Firstyoucheckwhetherthetextfieldhassometext.Ifso,youchecktoseewhetherthattextcanberepresentedbyaDouble.Forexample,“3.14”canberepresentedbyaDouble,butboth“three”and“1.2.3”cannot.Ifbothofthosecheckspass,thentheFahrenheitvalueissettoaMeasurementinitializedwiththatDoublevalue.Ifeitherofthosechecksfails,thentheFahrenheitvalueissettonil.Buildandruntheapplication.TheconversionbetweenFahrenheitandCelsiusworksgreat–solongasyouenteravalidnumber.(Italsoshowsmoredigitsthanyouprobablywantitto,whichyouwilladdressinamoment.)ItwouldbeniceifthecelsiusLabelwasupdatedwhentheapplicationfirstlaunchedinsteadofstillshowingthevalue“100”.OverrideviewDidLoad()tosettheinitialvalue,similartowhatyoudidinChapter1.overridefuncviewDidLoad(){
super.viewDidLoad()
updateCelsiusLabel()
}
Intheremainderofthischapter,youwillupdateWorldTrottertoaddresstwoissues:YouwillformattheCelsiusvaluetoshowaprecisionuptoonefractionaldigit,andyouwillnotallowtheusertotypeinmorethanonedecimalseparator.Thereareacoupleofotherissueswithyourapp,butyouwillfocusonthesetwofornow.
Oneoftheotherissueswillbepresentedasachallengeattheendofthischapter.Let’sstartwithupdatingtheprecisionoftheCelsiusvalue.
Numberformatters
Youuseanumberformattertocustomizethedisplayofanumber.Thereareotherformattersforformattingdates,energy,mass,length,measurements,andmore.CreateaconstantnumberformatterinConversionViewController.swift.letnumberFormatter:NumberFormatter={
letnf=NumberFormatter()
nf.numberStyle=.decimal
nf.minimumFractionDigits=0
nf.maximumFractionDigits=1
returnnf
}()
Hereyouareusingaclosuretoinstantiatethenumberformatter.YouarecreatingaNumberFormatterwiththe.decimalstyleandconfiguringittodisplaynomorethanonefractionaldigit.YouwilllearnmoreaboutthisnewsyntaxfordeclaringpropertiesinChapter16.NowmodifyupdateCelsiusLabel()tousethisformatter.funcupdateCelsiusLabel(){
ifletcelsiusValue=celsiusValue{
celsiusLabel.text="\(celsiusValue.value)"
celsiusLabel.text=
numberFormatter.string(from:NSNumber(value:celsiusValue.value))
}else{
celsiusLabel.text="???"
}
}
Buildandruntheapplication.PlayaroundwithFahrenheitvaluestoseetheformatteratwork.YoushouldneverseemorethanonefractionaldigitontheCelsiuslabel.Inthenextsection,youwillupdatetheapplicationtoacceptamaximumofonedecimalseparatorinthetextfield.Todothis,youwilluseacommoniOSdesignpatterncalleddelegation.
Delegation
Delegationisanobject-orientedapproachtocallbacks.Acallbackisafunctionthatissuppliedinadvanceofaneventandiscalledeverytimetheeventoccurs.Someobjectsneedtomakeacallbackformorethanoneevent.Forinstance,thetextfieldneedsto“callback”whentheuserenterstextaswellaswhentheuserpressestheReturnkey.However,thereisnobuilt-inwayfortwo(ormore)callbackfunctionstocoordinateandshareinformation.Thisistheproblemaddressedbydelegation–yousupplyasingledelegatetoreceivealloftheevent-relatedcallbacksforaparticularobject.Thisdelegateobjectcanthenstore,manipulate,acton,andrelaytheinformationfromthecallbacksasitseesfit.Whentheusertypesintoatextfield,thattextfieldwillaskitsdelegateifitwantstoacceptthechangesthattheuserhasmade.ForWorldTrotter,youwanttodenythatchangeiftheuserattemptstoenteraseconddecimalseparator.ThedelegateforthetextfieldwillbetheinstanceofConversionViewController.
Conformingtoaprotocol
ThefirststepisenablinginstancesoftheConversionViewControllerclasstoperformtheroleofUITextFielddelegatebydeclaringthatConversionViewControllerconformstotheUITextFieldDelegateprotocol.Foreverydelegaterole,thereisacorrespondingprotocolthatdeclaresthemethodsthatanobjectcancallonitsdelegate.TheUITextFieldDelegateprotocollookslikethis:protocolUITextFieldDelegate:NSObjectProtocol{
optionalfunctextFieldShouldBeginEditing(_textField:UITextField)->Bool
optionalfunctextFieldDidBeginEditing(_textField:UITextField)
optionalfunctextFieldShouldEndEditing(_textField:UITextField)->Bool
optionalfunctextFieldDidEndEditing(_textField:UITextField)
optionalfunctextField(_textField:UITextField,
shouldChangeCharactersInrange:NSRange,
replacementStringstring:String)->Bool
optionalfunctextFieldShouldClear(_textField:UITextField)->Bool
optionalfunctextFieldShouldReturn(_textField:UITextField)->Bool
}
Thisprotocol,likeallprotocols,isdeclaredwithprotocolfollowedbyitsname,UITextFieldDelegate.TheNSObjectProtocolafterthecolonreferstotheNSObjectprotocolandtellsyouthatUITextFieldDelegateinheritsallofthemethodsintheNSObjectprotocol.ThemethodsspecifictoUITextFieldDelegatearedeclarednext.Youcannotcreateinstancesofaprotocol;itissimplyalistofmethodsandproperties.Instead,implementationislefttoeachtypethatconformstotheprotocol.Inaclass’sdeclaration,theprotocolsthattheclassconformstoareinacomma-delimitedlistfollowingthesuperclass(ifthereisone).InConversionViewController.swift,
declarethatConversionViewControllerconformstotheUITextFieldDelegateprotocol.classConversionViewController:UIViewController,UITextFieldDelegate{
Protocolsusedfordelegationarecalleddelegateprotocols,andthenamingconventionforadelegateprotocolisthenameofthedelegatingclassplusthewordDelegate.Notallprotocolsaredelegateprotocols,however,andyouwillseeanexampleofadifferentkindofprotocolinChapter16.TheprotocolswehavementionedsofararepartoftheiOSSDK,butyoucanalsowriteyourownprotocols.
Usingadelegate
NowthatyouhavedeclaredConversionViewControllerasconformingtotheUITextFieldDelegateprotocol,youcansetthedelegatepropertyofthetextfield.OpenMain.storyboardandControl-dragfromthetextfieldtotheConversionViewController.ChoosedelegatefromthepopovertoconnectthedelegatepropertyofthetextfieldtotheConversionViewController.Next,youaregoingtoimplementtheUITextFieldDelegatemethodthatyouareinterestedin–textField(_:shouldChangeCharactersIn:replacementString:).Becausethetextfieldcallsthismethodonitsdelegate,youmustimplementitinConversionViewController.swift.InConversionViewController.swift,implementtextField(_:shouldChangeCharactersIn:replacementString:)toprintthetextfield’scurrenttextaswellasthereplacementstring.Fornow,justreturntruefromthismethod.functextField(_textField:UITextField,
shouldChangeCharactersInrange:NSRange,
replacementStringstring:String)->Bool{
print("Currenttext:\(textField.text)")
print("Replacementtext:\(string)")
returntrue
}
NoticethatXcodewasabletoautocompletethismethodbecauseConversionViewControllerconformstoUITextFieldDelegate.ItisagoodideatodeclareaprotocolbeforeimplementingmethodsfromtheprotocolsothatXcodecanofferthissupport.Buildandruntheapplication.EnterseveraldigitsinthetextfieldandwatchXcode’sconsole(Figure4.12).Itprintsoutthecurrenttextofthetextfieldaswellasthereplacementstring.
Figure4.12Printingtotheconsole
Considerthis“currenttext”and“replacementtext”informationinlightofyourgoalofpreventingmultipledecimalseparators.Logically,iftheexistingstringhasadecimalseparatorandthereplacementstringhasadecimalseparator,thechangeshouldberejected.InConversionViewController.swift,updatetextField(_:shouldChangeCharactersIn:replacementString:)tousethislogic.functextField(_textField:UITextField,
shouldChangeCharactersInrange:NSRange,
replacementStringstring:String)->Bool{
print("Currenttext:\(textField.text)")
print("Replacementtext:\(string)")
returntrue
letexistingTextHasDecimalSeparator=textField.text?.range(of:".")
letreplacementTextHasDecimalSeparator=string.range(of:".")
ifexistingTextHasDecimalSeparator!=nil,
replacementTextHasDecimalSeparator!=nil{
returnfalse
}else{
returntrue
}
}
Buildandruntheapplication.Attempttoentermultipledecimalseparators;theapplicationwillrejecttheseconddecimalseparatorthatyouenter.
Moreonprotocols
IntheUITextFieldDelegateprotocol,therearetwokindsofmethods:methodsthathandleinformationupdatesandmethodsthathandlerequestsforinput.Forexample,thetextfield’sdelegateimplementsthetextFieldDidBeginEditing(_:)methodifitwantstoknowwhentheusertapsonthetextfield.Ontheotherhand,textField(_:shouldChangeCharactersIn:replacementString:)isarequestforinput.Atextfieldcallsthismethodonitsdelegatetoaskwhetherthereplacementstringshouldbeacceptedorrejected.ThemethodreturnsaBool,whichisthedelegate’sanswer.Methodsdeclaredinaprotocolcanberequiredoroptional.Bydefault,protocolmethodsarerequired,meaningthataclassconformingtotheprotocolmusthaveanimplementationofthosemethods.Ifaprotocolhasoptionalmethods,theseareprecededbythedirectiveoptional.LookingbackattheUITextFieldDelegateprotocol,youcanseethatallofitsmethodsareoptional.Thisistypicallytrueofdelegateprotocols.
BronzeChallenge:DisallowAlphabeticCharacters
Currently,theusercanenteralphabeticcharacterseitherbyusingaBluetoothkeyboardorbypastingcopiedtextintothetextfield.Fixthisissue.Hint:YouwillwanttousetheNSCharacterSetclass.
BIGNERDRANCHCODINGBOOTCAMPS
BigNerdRanchbootcampscoveralotofgroundinjustdays.Withourretreat-styletraining,we’llsubjectyoutothemostintensiveappdevelopmentcourseyoucanimagine,andwhenyoufinish,you’llbepartofanelitecorps:thefew,theproud,thenerds.Ourdistraction-freetraininggivesyoutheopportunitytomasternewskillsinanintensiveenvironment—nomeetings,nophonecalls,justlearning.
BigNerdRanch’strainingwasunlikeanyotherclassI’vehad.Ilearnedskillsthatmakemeexceptionallymorevaluable,givingmealeguponthecompetition.SincemyfirstBigNerdRanchclass,I’vewrittensoftwareusedinTheWhiteHouse,heldpositionsatAT&TandDisney—andultimatelylandedatApple.—JoshPaul,Alumnus
WeofferclassesiniOS,Android,Front-EndWeb,Back-EndWeb,macOSandDesign.UsecodeBNRGUIDE100for$100offabootcampofyourchoice.
www.bignerdranch.com
5ViewControllers
AviewcontrollerisaninstanceofasubclassofUIViewController.Aviewcontrollermanagesaviewhierarchy.Itisresponsibleforcreatingviewobjectsthatmakeupthehierarchyandforhandlingeventsassociatedwiththeviewobjectsinitshierarchy.Sofar,WorldTrotterhasasingleviewcontroller,ConversionViewController.Inthischapter,youwillupdateittousemultipleviewcontrollers.Theuserwillbeabletoswitchbetweentwoviewhierarchies–oneforviewingtheConversionViewControllerandanotherfordisplayingamap(Figure5.1).Figure5.1ThetwofacesofWorldTrotter
TheViewofaViewController
AssubclassesofUIViewController,allviewcontrollersinheritanimportantproperty:varview:UIView!
ThispropertypointstoaUIViewinstancethatistherootoftheviewcontroller ’sviewhierarchy.Whentheviewofaviewcontrollerisaddedasasubviewofthewindow,theviewcontroller ’sentireviewhierarchyisadded,asshowninFigure5.2.Figure5.2ObjectdiagramforWorldTrotter
Aviewcontroller ’sviewisnotcreateduntilitneedstoappearonthescreen.Thisoptimizationiscalledlazyloading,anditcanconservememoryandimproveperformance.Therearetwowaysthataviewcontrollercancreateitsviewhierarchy:
inInterfaceBuilder,byusinganinterfacefilesuchasastoryboardprogrammatically,byoverridingtheUIViewControllermethodloadView()
YousawthefirstapproachinChapter3.First,youcreatedasampleviewhierarchyprogrammatically,thenyouswitchedtoInterfaceBuildertocreatetheinterfaceforConversionViewControllerusingastoryboardfile.YouwillcontinuetouseInterfaceBuilderinthischapterasyoufurtherexploreviewcontrollers.InChapter6,youwillgetexperiencecreatingprogrammaticviewsusingloadView().
SettingtheInitialViewController
Althoughastoryboardcanhavemanyviewcontrollers,eachstoryboardfilehasexactlyoneinitialviewcontroller.Theinitialviewcontrolleractsasanentrypointintothestoryboard.Youaregoingtoaddandconfigureanotherviewcontrollertothecanvasandsetittobetheinitialviewcontrollerforthestoryboard.OpenMain.storyboard.Fromtheobjectlibrary,dragaViewControllerontothecanvas(Figure5.3).(Tomakespaceonthecanvas,youcanzoomoutbyControl-clickingonthebackground,usingthezoomcontrolsatthebottomofthecanvas,orusingpinchgesturesonyourtrackpad.)Figure5.3Addingaviewcontrollertothecanvas
YouwantthisviewcontrollertodisplayanMKMapView–aclassdesignedtodisplayamap–insteadoftheexistingwhiteUIView.SelecttheviewoftheViewController–nottheViewControlleritself!–andpressDeletetoremovethisviewfromthecanvas.ThendragaMapKitViewfromtheobjectlibraryontotheviewcontrollertosetitastheviewforthisviewcontroller(Figure5.4).
Figure5.4Addingamapviewtothecanvas
NowselecttheViewControllerandopenitsattributesinspector.UndertheViewControllersection,checktheboxnexttoIsInitialViewController(Figure5.5).DidyounoticethatthegrayarrowonthecanvasthatwaspointingattheConversionViewControllerisnowpointingtotheViewController?Thearrow,asyouhaveprobablysurmised,indicatestheinitialviewcontroller.Anotherwaytoassigntheinitialviewcontrolleristodragthatarrowfromoneviewcontrollertoanotheronthecanvas.
Figure5.5Settingtheinitialviewcontroller
Thereisaquirkthatwouldcauseproblemsifyouweretobuildandruntheapprightnow.(Tryit,ifyoulike.)MKMapViewisinaframeworkthatisnotcurrentlybeingloadedintotheapplication.Aframeworkisasharedlibraryofcodethatincludesassociatedresourcessuchasinterfacefilesandimages.YoubrieflylearnedaboutframeworksinChapter3,andyouhavebeenusingacoupleofframeworksalready:UIKitandFoundationarebothframeworks.Sofar,youhavebeenincludingframeworksinyourappbyusingtheimportkeyword,likeso:importUIKit
NowyouneedtoimporttheMapKitframeworksothattheMKMapViewwillload.However,ifyouimporttheMapKitframeworkusingtheimportkeywordwithoutincludinganycodethatusesthatframework,thecompilerwilloptimizeitout–eventhoughyouareusingamapviewinyourstoryboard.Instead,youneedtomanuallylinktheMapKitframeworktotheapp.Withtheprojectnavigatoropen,clickontheWorldTrotterprojectatthetopofthelisttoopentheprojectsettings.FindandopentheGeneraltabinthesettings.ScrolldowntothebottomandfindthesectionlabeledLinkedFrameworksandLibraries.Clickonthe+atthebottomandsearchforMapKit.framework.SelectthisframeworkandclickAdd(Figure5.6).
Figure5.6AddingtheMapKitframework
Nowyoucanbuildandruntheapplication.Becauseyouhavechangedtheinitialviewcontroller,themapshowsupinsteadoftheviewoftheConversionViewController.Asmentionedabove,therecanonlybeoneinitialviewcontrollerassociatedwithagivenstoryboard.YousawthisearlierwhenyousettheViewControllertobetheinitialviewcontroller.Atthatpoint,theConversionViewControllerwasnolongertheinitialviewcontrollerforthisstoryboard.Let’stakealookathowthisrequirementworkswiththerootlevelUIWindowtoaddtheinitialviewcontroller ’sviewtothewindowhierarchy.UIWindowhasarootViewControllerproperty.Whenaviewcontrollerissetasthewindow’srootViewController,thatviewcontroller ’sviewgetsaddedtothewindow’sviewhierarchy.Whenthispropertyisset,anyexistingsubviewsonthewindowareremovedandviewcontroller ’sviewgetsaddedtothewindowwiththeappropriateAutoLayoutconstraints.Eachapplicationhasonemaininterface,areferencetoastoryboard.Whentheapplicationlaunches,theinitialviewcontrollerforthemaininterfacegetssetastherootViewControllerofthewindow.Themaininterfaceforanapplicationissetintheprojectsettings.StillintheGeneraltaboftheprojectsettings,findtheDeploymentInfosection.HereyouwillseetheMainInterfacesetting(Figure5.7).ThisissettoMain,whichcorrespondstoMain.storyboard.
Figure5.7Anapplication’smaininterface
UITabBarController
Viewcontrollersbecomemoreinterestingwhentheuserhasawaytoswitchbetweenthem.Throughoutthisbook,youwilllearnanumberofwaystopresentviewcontrollers.Inthischapter,youwillcreateaUITabBarControllerthatwillallowtheusertoswapbetweentheConversionViewControllerandtheUIViewControllerdisplayingthemap.UITabBarControllerkeepsanarrayofviewcontrollers.Italsomaintainsatabbaratthebottomofthescreenwithatabforeachviewcontrollerinitsarray.Tappingonatabresultsinthepresentationoftheviewoftheviewcontrollerassociatedwiththattab.OpenMain.storyboardandselecttheViewController.FromtheEditormenu,chooseEmbedIn→TabBarController.ThiswilladdtheViewControllertotheviewcontrollersarrayoftheTabBarController.YoucanseethisrepresentedbytheRelationshiparrowpointingfromtheTabBarControllertotheViewController(Figure5.8).Additionally,InterfaceBuilderknowstomaketheTabBarControllertheinitialviewcontrollerforthestoryboard.Figure5.8Tabbarcontrollerwithoneviewcontroller
Atabbarcontrollerisnotveryusefulwithjustoneviewcontroller.AddtheConversionViewControllertotheTabBarController’sviewcontrollersarray.Control-dragfromtheTabBarControllertotheConversionViewController.FromtheRelationshipSeguesection,chooseviewcontrollers(Figure5.9).
Figure5.9Addingaviewcontrollertothetabbarcontroller
Buildandruntheapplication.Taponthetwotabsatthebottomtoswitchbetweenthetwoviewcontrollers.Atthemoment,thetabsjustsayItem,whichisnotveryhelpful.Inthenextsection,youwillupdatethetabbaritemstomakethetabsmoredescriptiveandobvious.UITabBarControllerisitselfasubclassofUIViewController.AUITabBarController’sviewisaUIViewwithtwosubviews:thetabbarandtheviewoftheselectedviewcontroller(Figure5.10).Figure5.10UITabBarControllerdiagram
Tabbaritems
Eachtabonthetabbarcandisplayatitleandanimage,andeachviewcontrollermaintainsatabBarItempropertyforthispurpose.WhenaviewcontrolleriscontainedbyaUITabBarController,itstabbaritemappearsinthetabbar.Figure5.11showsanexampleofthisrelationshipiniPhone’sPhoneapplication.
Figure5.11UITabBarItemexample
First,youneedtoaddafewfilestoyourprojectthatwillbetheimagesforthetabbaritems.Intheprojectnavigator,opentheAssetCatalogbyopeningAssets.xcassets.Anassetisasetoffilesfromwhichasinglefilewillbeselectedatruntimebasedontheuser ’sdeviceconfiguration(moreonthatattheendofthischapter).YouaregoingtoaddaConvertIconassetandaMapIconasset,eachwithimagesatthreedifferentresolutions.Inthe0-Resourcesdirectoryofthefilethatyoudownloadedearlier(www.bignerdranch.com/solutions/iOSProgramming6ed.zip),findConvertIcon.png,[email protected],[email protected],MapIcon.png,[email protected],andMapIcon@3x.png.DragthesefilesintotheimagessetlistontheleftsideoftheAssetCatalog(Figure5.12).
Figure5.12AddingimagestotheAssetCatalog
Thetabbaritempropertiescanbeseteitherprogrammaticallyorinastoryboard.Becauseyourdataisstatic,thestoryboardwillbethebestplacetosetthetabbaritemproperties.InMain.storyboard,locatetheViewController(itisnowlabeledItem).Noticethatatabbarwiththetabbariteminitwasaddedtotheinterfacebecausetheviewcontrollerwillbepresentedwithinatabbarcontroller.Thisisveryusefulwhenlayingoutyourinterface.Selectthistabbaritemandopenitsattributesinspector.UndertheBarItemsection,changetheTitleto“Map”andchooseMapIconfromtheImagemenu.Youcanalsochangethetextofthetabbaritembydouble-clickingonthetextonthecanvas.Thetabbarwillbeupdatedtoreflectthesevalues(Figure5.13).Figure5.13ViewController’stabbaritem
NowfindtheConversionViewControllerandselectitstabbaritem.SettheTitletobe“Convert”andtheImagetobeConvertIcon.Let’salsochangethefirsttabtobetheConvertViewController.Theorderofthetabsisdeterminedbytheorderoftheviewcontrollerswithinthetabbarcontroller ’sviewControllersarray.YoucanchangetheorderinastoryboardbydraggingthetabsatthebottomoftheTabBarController.FindtheTabBarControlleronthecanvas.DragtheConverttabtobeinthefirstposition.Buildandruntheapplication.Notonlyarethetabbaritemsatthebottommoredescriptive,
buttheConvertViewControllerisnowthefirstviewcontrollerthatisdisplayed(Figure5.14).Figure5.14Tabbaritemswithlabelsandicons
LoadedandAppearingViews
Nowthatyouhavetwoviewcontrollers,thelazyloadingofviewsmentionedearlierbecomesmoreimportant.Whentheapplicationlaunches,thetabbarcontrollerdefaultstoloadingtheviewofthefirstviewcontrollerinitsarray,whichistheConvertViewController.TheMapViewController’sviewisnotneededandwillonlybeneededwhen(orif)theusertapsthetabtoseeit.Youcantestthisbehaviorforyourself.Whenaviewcontrollerfinishesloadingitsview,viewDidLoad()iscalled,andyoucanoverridethismethodtomakeitprintamessagetotheconsole,allowingyoutoseethatitwascalled.Youaregoingtoaddcodetobothviewcontrollers.However,thereisnocodecurrentlyassociatedwiththeviewcontrollerdisplayingthemapbecauseeverythinghasbeenconfiguredusingthestoryboard.Nowthatyouwanttoaddcodetothatviewcontroller,youaregoingtocreateaviewcontrollersubclassandassociateitwiththatinterface.CreateanewSwiftfile(Command-N)andnameitMapViewController.OpenMapViewController.swiftanddefineaUIViewControllersubclassnamedMapViewController.importFoundation
importUIKit
classMapViewController:UIViewController{
}
NowopenMain.storyboardandselectthemap’sviewcontroller.OpenitsidentityinspectorandchangetheClasstoMapViewController.NowthatyouhaveassociatedtheMapViewControllerclasswiththeviewcontrolleronthecanvas,youcanaddcodetobothConversionViewControllerandMapViewControllertoprinttotheconsolewhentheirviewDidLoad()methodiscalled.InConversionViewController.swift,updateviewDidLoad()toprintastatementtotheconsole.overridefuncviewDidLoad(){
super.viewDidLoad()
print("ConversionViewControllerloadeditsview.")
updateCelsiusLabel()
}
InMapViewController.swift,overridethesamemethod.overridefuncviewDidLoad(){
super.viewDidLoad()
print("MapViewControllerloadeditsview.")
}
Buildandruntheapplication.TheconsolereportsthatConversionViewControllerloadeditsviewrightaway.TapMapViewController’stab,andtheconsolewillreportthatitsviewisnowloaded.Atthispoint,bothviewshavebeenloaded,soswitchingbetweenthetabsnowwillnolongertriggerviewDidLoad().(Tryitandsee.)
Accessingsubviews
Often,youwillwanttodosomeextrainitializationorconfigurationofsubviewsdefinedinInterfaceBuilderbeforetheyappeartotheuser.Sowherecanyouaccessasubview?Therearetwomainoptions,dependingonwhatyouneedtodo.ThefirstoptionistheviewDidLoad()methodthatyouoverrodetospotlazyloading.Thismethodiscalledaftertheviewcontroller ’sinterfacefileisloaded,atwhichpointalloftheviewcontroller ’soutletswillreferencetheappropriateobjects.ThesecondoptionisanotherUIViewControllermethod,viewWillAppear(_:).Thismethodiscalledjustbeforeaviewcontroller ’sviewisaddedtothewindow.Whichshouldyouchoose?OverrideviewDidLoad()iftheconfigurationonlyneedstobedoneonceduringtherunoftheapp.OverrideviewWillAppear(_:)ifyouneedtheconfigurationtobedoneeachtimetheviewcontroller ’sviewappearsonscreen.
InteractingwithViewControllersandTheirViews
Let’slookatsomemethodsthatarecalledduringthelifecycleofaviewcontrolleranditsview.Someofthesemethodsyouhavealreadyseen,andsomearenew.
init(coder:)istheinitializerforUIViewControllerinstancescreatedfromastoryboard.Whenaviewcontrollerinstanceiscreatedfromastoryboard,itsinit(coder:)getscalledonce.YouwilllearnmoreaboutthismethodinChapter16.init(nibName:bundle:)isthedesignatedinitializerforUIViewController.Whenaviewcontrollerinstanceiscreatedwithouttheuseofastoryboard,itsinit(nibName:bundle:)getscalledonce.Notethatinsomeapps,youmayendupcreatingseveralinstancesofthesameviewcontrollerclass.Thismethodwillgetcalledonceoneachviewcontrollerasitiscreated.loadView()isoverriddentocreateaviewcontroller ’sviewprogrammatically.viewDidLoad()isoverriddentoconfigureviewscreatedbyloadinganinterfacefile.Thismethodgetscalledaftertheviewofaviewcontrolleriscreated.viewWillAppear(_:)isoverriddentoconfigureviewscreatedbyloadinganinterfacefile.ThismethodandviewDidAppear(_:)getcalledeverytimeyourviewcontrollerismovedonscreen.viewWillDisappear(_:)andviewDidDisappear(_:)getcalledeverytimeyourviewcontrollerismovedoffscreen.
SilverChallenge:DarkMode
WhenevertheConversionViewControllerisviewed,updateitsbackgroundcolorbasedonthetimeofday.Intheevening,thebackgroundshouldbeadarkcolor.Otherwise,thebackgroundshouldbealightcolor.YouwillneedtooverrideviewWillAppear(_:)toaccomplishthis.(Ifthatisnotenoughexcitementinyourlife,youcanchangethebackgroundcoloreachtimetheviewcontrollerisviewed.)
FortheMoreCurious:RetinaDisplay
WiththereleaseofiPhone4,AppleintroducedtheRetinadisplayforiPhoneandiPodtouch.TheRetinadisplayhasmuchhigherresolutioncomparedtoearlierdevices.Let’slookatwhatyoushoulddotomakegraphicslooktheirbestonbothdisplays.Forvectorgraphics,youdonotneedtodoanything;yourcodewillrenderascrisplyasthedeviceallows.However,ifyoudrawusingCoreGraphicsfunctions,thesegraphicswillappeardifferentlyondifferentdevices.InCoreGraphics(alsocalledQuartz),lines,curves,text,etc.aredescribedintermsofpoints.Onanon-Retinadisplay,apointis1x1pixel.OnmostRetinadisplays,apointis2x2pixels(Figure5.15).Theexceptionsarethe5.5-inchiPhones,whichhaveahigher-resolutionRetinadisplaywhereapointis3x3pixels.Figure5.15Renderingtodifferentresolutions
Giventhesedifferences,bitmapimages(likeJPEGorPNGfiles)willbeunattractiveiftheimageisnottailoredtothedevice’sscreentype.Sayyourapplicationincludesasmallimageof25x25pixels.Ifthisimageisdisplayedona2xRetinadisplay,thentheimagemustbestretchedtocoveranareaof50x50pixels.Atthispoint,thesystemdoesatypeofaveragingcalledanti-aliasingtokeeptheimagefromlookingjagged.Theresultisanimagethatisnotjagged–butitisfuzzy(Figure5.16).Figure5.16Fuzzinessfromstretchinganimage
Youcouldusealargerfileinstead,buttheaveragingwouldthencauseproblemsintheotherdirectionwhentheimageisshrunkforanon-Retinadisplay.Theonlysolutionistobundletwoimagefileswithyourapplication:oneatapixelresolutionequaltothenumberofpointsonthescreenfornon-RetinadisplaysandonetwicethatsizeinpixelsforRetinadisplays.Fortunately,youdonothavetowriteanyextracodetohandlewhichimagegetsloadedonwhichdevice.AllyouhavetodoisassociatethedifferentresolutionimagesintheAssetCatalogwithasingleasset.Then,whenyouuseUIImage’sinit(named:)initializertoloadtheimage,thismethodlooksinthebundleandgetstheappropriatefilefortheparticulardevice.
6ProgrammaticViews
Inthischapter,youwillupdateWorldTrottertocreatetheviewforMapViewControllerprogrammatically(Figure6.1).Indoingso,youwilllearnmoreaboutviewcontrollersandhowtosetupconstraintsandcontrols(suchasUIButtons)programmatically.Figure6.1WorldTrotterwithprogrammaticviews
Currently,theviewforMapViewControllerisdefinedinthestoryboard.Thefirststep,then,istoremovethisviewfromthestoryboardsoyoucaninsteadcreateitprogrammatically.InMain.storyboard,selectthemapviewassociatedwithMapViewControllerandpressDelete(Figure6.2).
Figure6.2Deletingtheview
CreatingaViewProgrammatically
YoulearnedinChapter5thatyoucreateaviewcontroller ’sviewprogrammaticallybyoverridingtheUIViewControllermethodloadView().OpenMapViewController.swiftandoverrideloadView()tocreateaninstanceofMKMapViewandsetitastheviewoftheviewcontroller.Youwillneedareferencetothemapviewlateron,socreateapropertyforitaswell.importUIKit
importMapKit
classMapViewController:UIViewController{
varmapView:MKMapView!
overridefuncloadView(){
//Createamapview
mapView=MKMapView()
//Setitas*the*viewofthisviewcontroller
view=mapView
}
overridefuncviewDidLoad(){
super.viewDidLoad()
print("MapViewControllerloadeditsview.")
}
}
Whenaviewcontrolleriscreated,itsviewpropertyisnil.Ifaviewcontrollerisaskedforitsviewanditsviewisnil,thentheloadView()methodiscalled.Buildandruntheapplication.Althoughtheapplicationlooksthesame,themapviewisbeingcreatedprogrammaticallyinsteadofthroughInterfaceBuilder.
ProgrammaticConstraints
InChapter3,youlearnedaboutAutoLayoutconstraintsandhowtoaddthemusingInterfaceBuilder.Inthissection,youwilllearnhowtoaddconstraintstoaninterfaceprogrammatically.ApplerecommendsthatyoucreateandconstrainyourviewsinInterfaceBuilderwheneverpossible.However,ifyourviewsarecreatedincode,thenyouwillneedtoconstrainthemprogrammatically.TheinterfaceforMapViewControlleriscreatedprogrammatically,soitisagreatcandidateforprogrammaticconstraints.Tolearnaboutprogrammaticconstraints,youaregoingtoaddaUISegmentedControltoMapViewController’sinterface.Asegmentedcontrolallowstheusertochoosebetweenadiscretesetofoptions,andyouwilluseonetoallowtheusertoswitchbetweenmaptypes:standard,hybrid,andsatellite.InMapViewController.swift,updateloadView()toaddasegmentedcontroltotheinterface.overridefuncloadView(){
//Createamapview
mapView=MKMapView()
//Setitas*the*viewofthisviewcontroller
view=mapView
letsegmentedControl
=UISegmentedControl(items:["Standard","Hybrid","Satellite"])
segmentedControl.backgroundColor
=UIColor.white.withAlphaComponent(0.5)
segmentedControl.selectedSegmentIndex=0
segmentedControl.translatesAutoresizingMaskIntoConstraints=false
view.addSubview(segmentedControl)
}
(Notethatduetopagesizerestrictionsweareshowingsomeofthesedeclarationssplitacrosstwolines.Youshouldentereachdeclarationonasingleline.)Thelineofcoderegardingtranslatingconstraintshastodowithanoldersystemforscalinginterfaces–autoresizingmasks.BeforeAutoLayoutwasintroduced,iOSapplicationsusedautoresizingmaskstoallowviewstoscalefordifferent-sizedscreensatruntime.Everyviewhasanautoresizingmask.Bydefault,iOScreatesconstraintsthatmatchtheautoresizingmaskandaddsthemtotheview.Thesetranslatedconstraintswilloftenconflictwithexplicitconstraintsinthelayoutandcauseanunsatisfiableconstraintsproblem.ThefixistoturnoffthisdefaulttranslationbysettingthepropertytranslatesAutoresizingMaskIntoConstraintstofalse.(ThereismoreaboutAutoLayoutandautoresizingmasksattheendofthischapter.)
Anchors
WhenyouworkwithAutoLayoutprogrammatically,youwilluseanchorstocreateyourconstraints.Anchorsarepropertiesontheviewthatcorrespondtoattributesthatyoumightwanttoconstraintoananchoronanotherview.Forexample,youmightconstraintheleadinganchorofoneviewtotheleadinganchorofanotherview.Thiswouldhavetheeffectofthetwoviews’leadingedgesbeingaligned.Let’screatesomeconstraintstodothefollowing.
Thetopanchorofthesegmentedcontrolshouldbeequaltothetopanchorofitssuperview.Theleadinganchorofthesegmentedcontrolshouldbeequaltotheleadinganchorofitssuperview.Thetrailinganchorofthesegmentedcontrolshouldbeequaltothetrailinganchorofitssuperview.
InMapViewController.swift,createtheseconstraintsinloadView().letsegmentedControl
=UISegmentedControl(items:["Standard","Hybrid","Satellite"])
segmentedControl.backgroundColor
=UIColor.white.withAlphaComponent(0.5)
segmentedControl.selectedSegmentIndex=0
segmentedControl.translatesAutoresizingMaskIntoConstraints=false
view.addSubview(segmentedControl)
lettopConstraint
=segmentedControl.topAnchor.constraint(equalTo:view.topAnchor)
letleadingConstraint
=segmentedControl.leadingAnchor.constraint(equalTo:view.leadingAnchor)
lettrailingConstraint
=segmentedControl.trailingAnchor.constraint(equalTo:view.trailingAnchor)
Xcodewillalertyoutoaproblemwitheachlineyouhaveentered.Youwillfixtheminamoment.Anchorshaveamethodconstraint(equalTo:)thatwillcreateaconstraintbetweenthetwoanchors.ThereareafewotherconstraintcreationmethodsonNSLayoutAnchor,includingonethatacceptsaconstantasanargument:funcconstraint(equalToanchor:NSLayoutAnchor<AnchorType>,
constantc:CGFloat)->NSLayoutConstraint
Activatingconstraints
YounowhavethreeNSLayoutConstraintinstances.However,theseconstraintswillhavenoeffectonthelayoutuntilyouexplicitlyactivatethembysettingtheirisActivepropertiestotrue.ThiswillresolveXcode’scomplaint.InMapViewController.swift,activatetheconstraintsattheendofloadView().lettopConstraint=
segmentedControl.topAnchor.constraint(equalTo:view.topAnchor)
letleadingConstraint=
segmentedControl.leadingAnchor.constraint(equalTo:view.leadingAnchor)
lettrailingConstraint=
segmentedControl.trailingAnchor.constraint(equalTo:view.trailingAnchor)
topConstraint.isActive=true
leadingConstraint.isActive=true
trailingConstraint.isActive=true
Constraintsneedtobeaddedtothemostrecentcommonancestorfortheviewsassociatedwiththeconstraint.Figure6.3showsaviewhierarchyalongwiththecommonancestorfortwoviews.Figure6.3Commonancestor
Ifaconstraintisrelatedtojustoneview(suchaswhenaddingawidthorheightconstrainttoaview),thenthatviewisconsideredthecommonancestor.Bysettingtheactivepropertyonaconstrainttotrue,theconstraintwillworkitswayupthehierarchyfortheitemstofindthecommonancestortoaddtheconstraintto.ItwillthencallthemethodaddConstraint(_:)ontheappropriateview.SettingtheactivepropertyispreferabletocallingaddConstraint(_:)orremoveConstraint(_:)yourself.BuildandruntheapplicationandswitchtotheMapViewController.Thesegmentedcontrolisnowpinnedtothetop,leading,andtrailingedgesofitssuperview(Figure6.4).
Figure6.4Segmentedcontroladdedtothescreen
Althoughtheconstraintsaredoingtherightthing,theinterfacedoesnotlookgood.Thesegmentedcontrolisunderlappingthestatusbar,anditwouldlookbetterifthesegmentedcontrolwasinsetfromtheleadingandtrailingedgesofthescreen.Let’stacklethestatusbarissuefirst.
Layoutguides
Viewcontrollersexposetwolayoutguidestoassistwithlayoutcontent:thetopLayoutGuideandthebottomLayoutGuide.Thelayoutguidesindicatetheextenttowhichtheviewcontroller ’sviewcontentswillbevisible.UsingtopLayoutGuidewillallowyourcontenttonotunderlapthestatusbarornavigationbaratthetopofthescreen.(YouwilllearnaboutnavigationbarsinChapter14.)UsingthebottomLayoutGuidewillallowyourcontenttonotunderlapthetabbaratthebottomofthescreen.Thelayoutguidesexposethreeanchorsthatyoucanusetoaddconstraints:topAnchor,bottomAnchor,andheightAnchor.Becauseyouwantthesegmentedcontroltobeunderthestatusbar,youwillconstrainthebottomanchorofthetoplayoutguidetothetopanchorofthesegmentedcontrol.InMapViewController.swift,updatethesegmentedcontrol’sconstraintsinloadView().Makethesegmentedcontrolbe8pointsbelowthetoplayoutguide.lettopConstraint=
segmentedControl.topAnchor.constraint(equalTo:view.topAnchor)
lettopConstraint=
segmentedControl.topAnchor.constraint(equalTo:topLayoutGuide.bottomAnchor,
constant:8)
letleadingConstraint=
segmentedControl.leadingAnchor.constraint(equalTo:view.leadingAnchor)
lettrailingConstraint=
segmentedControl.trailingAnchor.constraint(equalTo:view.trailingAnchor)
topConstraint.isActive=true
leadingConstraint.isActive=true
trailingConstraint.isActive=true
Buildandruntheapplication.Thesegmentedcontrolnowappearsbelowthestatusbar.Byusingthelayoutguidesinsteadofahardcodedconstant,theviewswilladaptbasedonthecontexttheyappearin.
Nowlet’supdatethesegmentedcontrolsothatitisinsetfromtheleadingandtrailingedgesofitssuperview.
Margins
Althoughyoucouldinsetthesegmentedcontrolusingaconstantontheconstraint,itismuchbettertousethemarginsoftheviewcontroller ’sview.EveryviewhasalayoutMarginspropertythatdenotesthedefaultspacingtousewhenlayingoutcontent.ThispropertyisaninstanceofUIEdgeInsets,whichyoucanthinkofasatypeofframe.Whenaddingconstraints,youwillusethelayoutMarginsGuide,whichexposesanchorsthataretiedtotheedgesofthelayoutMargins.Theprimaryadvantageofusingthemarginsisthatthemarginscanchangedependingonthedevicetype(iPadoriPhone)aswellasthesizeofthedevice.Usingthemarginswillgiveyoucontentthatlooksgoodonanydevice.Updatethesegmentedcontrol’sleadingandtrailingconstraintsinloadView()tousethemargins.lettopConstraint=
segmentedControl.topAnchor.constraint(equalTo:topLayoutGuide.bottomAnchor,
constant:8)
letleadingConstraint=
segmentedControl.leadingAnchor.constraint(equalTo:view.leadingAnchor)
lettrailingConstraint=
segmentedControl.trailingAnchor.constraint(equalTo:view.trailingAnchor)
letmargins=view.layoutMarginsGuide
letleadingConstraint=
segmentedControl.leadingAnchor.constraint(equalTo:margins.leadingAnchor)
lettrailingConstraint=
segmentedControl.trailingAnchor.constraint(equalTo:margins.trailingAnchor)
topConstraint.isActive=true
leadingConstraint.isActive=true
trailingConstraint.isActive=true
Buildandruntheapplicationagain.Thesegmentedcontrolisnowinsetfromtheview’smargins(Figure6.5).Figure6.5Segmentedcontrolwithupdatedconstraints
Explicitconstraints
Itishelpfultounderstandhowthesemethodsthatyouhaveusedcreateconstraints.NSLayoutConstrainthasthefollowinginitializer:convenienceinit(itemview1:Any,
attributeattr1:NSLayoutAttribute,
relatedByrelation:NSLayoutRelation,
toItemview2:Any?,
attributeattr2:NSLayoutAttribute,
multiplier:CGFloat,
constantc:CGFloat)
Thisinitializercreatesasingleconstraintusingtwolayoutattributesoftwoviewobjects.Themultiplieristhekeytocreatingaconstraintbasedonaratio.Theconstantisafixednumberofpoints,similartowhatyouusedinyourspacingconstraints.ThelayoutattributesaredefinedasconstantsintheNSLayoutConstraintclass:
NSLayoutAttribute.left
NSLayoutAttribute.right
NSLayoutAttribute.leading
NSLayoutAttribute.trailing
NSLayoutAttribute.top
NSLayoutAttribute.bottom
NSLayoutAttribute.width
NSLayoutAttribute.height
NSLayoutAttribute.centerX
NSLayoutAttribute.centerY
NSLayoutAttribute.firstBaseline
NSLayoutAttribute.lastBaseline
Thereareadditionalattributesthathandlethemarginsassociatedwithaview,suchasNSLayoutAttribute.leadingMargin.Let’sconsiderahypotheticalconstraint.Sayyouwantedthewidthoftheimageviewtobe1.5timesitsheight.Youcouldmakethathappenwiththefollowingcode.(Donottypethishypotheticalconstraintinyourcode!Itwillconflictwithothersyoualreadyhave.)letaspectConstraint=NSLayoutConstraint(item:imageView,
attribute:.width,
relatedBy:.equal,
toItem:imageView,
attribute:.height,
multiplier:1.5,
constant:0.0)
Tounderstandhowthisinitializerworks,thinkofthisconstraintastheequationshowninFigure6.6.Figure6.6NSLayoutConstraintequation
Yourelatealayoutattributeofoneviewtothelayoutattributeofanotherviewusingamultiplierandaconstanttodefineasingleconstraint.
ProgrammaticControls
Nowlet’supdatethesegmentedcontroltochangethemaptypewhentheusertapsonasegment.AUISegmentedControlisasubclassofUIControl.YouworkedwithanotherUIControlsubclassinChapter1,theUIButtonclass.Controlsareresponsibleforcallingmethodsontheirtargetinresponsetosomeevent.ControleventsareoftypeUIControlEvents.Hereareafewofthecommoncontroleventsthatyouwilluse:UIControlEvents.touchDown
Atouchdownonthecontrol.UIControlEvents.touchUpInside
Atouchdownfollowedbyatouchupwhilestillwithintheboundsofthecontrol.UIControlEvents.valueChanged
Atouchthatcausesthevalueofthecontroltochange.UIControlEvents.editingChanged
AtouchthatcausesaneditingchangeforaUITextField.Youused.touchUpInsidefortheUIButtoninChapter1(itisthedefaulteventwhenyouControl-dragtoconnectactionsinInterfaceBuilder),andyousawthe.editingChangedeventinChapter4.Forthesegmentedcontrol,youwillusethe.valueChangedevent.InMapViewController.swift,updateloadView()toaddatarget-actionpairtothesegmentedcontrolandassociateitwiththe.valueChangedevent.overridefuncloadView(){
//Createamapview
mapView=MKMapView()
//Setitas*the*viewofthisviewcontroller
view=mapView
letsegmentedControl
=UISegmentedControl(items:["Standard","Satellite","Hybrid"])
segmentedControl.backgroundColor
=UIColor.white.withAlphaComponent(0.5)
segmentedControl.selectedSegmentIndex=0
segmentedControl.addTarget(self,
action:#selector(MapViewController.mapTypeChanged(_:)),
for:.valueChanged)
...
Next,implementtheactionmethodinMapViewControllerthattheeventwilltrigger.Thismethodwillcheckwhichsegmentwasselectedandupdatethemapaccordingly.funcmapTypeChanged(_segControl:UISegmentedControl){
switchsegControl.selectedSegmentIndex{
case0:
mapView.mapType=.standard
case1:
mapView.mapType=.hybrid
case2:
mapView.mapType=.satellite
default:
break
}
}
Buildandruntheapplication.Changetheselectedsegmentandthemapwillupdate.
BronzeChallenge:AnotherTab
Createanewviewcontrollerandaddittothetabbarcontroller.ThisviewcontrollershoulddisplayaWKWebView,whichisaclassusedtodisplaywebcontent.Thewebviewshoulddisplaywww.bignerdranch.comforyoutobookyournextvacation.
SilverChallenge:User’sLocation
AddabuttontotheMapViewControllerthatdisplaysandzoomsinontheuser ’scurrentlocation.Youwillneedtousedelegationtoaccomplishthis.RefertothedocumentationforMKMapViewDelegate.
GoldChallenge:DroppingPins
Mapviewscandisplaypins,whichareinstancesofMKPinAnnotationView.Addthreepinstothemapview:onewhereyouwereborn,onewhereyouarenow,andoneataninterestinglocationyouhavevisitedinthepast.Addabuttontotheinterfacethatallowsthemaptodisplaythelocationofapin.Subsequenttapsshouldsimplycyclethroughthelistofpins.
FortheMoreCurious:NSAutoresizingMaskLayoutConstraint
Aswementionedearlier,beforeAutoLayoutiOSapplicationsusedanothersystemformanaginglayout:autoresizingmasks.Eachviewhadanautoresizingmaskthatconstraineditsrelationshipwithitssuperview,butthismaskcouldnotaffectrelationshipsbetweensiblingviews.Bydefault,viewscreateandaddconstraintsbasedontheirautoresizingmasks.However,thesetranslatedconstraintsoftenconflictwiththeexplicitconstraintsinyourlayout,whichresultsinanunsatisfiableconstraintsproblem.Toseethishappen,commentoutthelineinloadView()thatturnsoffthetranslationofautoresizingmasks.//segmentedControl.translatesAutoresizingMaskIntoConstraints=false
view.addSubview(segmentedControl)
Nowthesegmentedcontrolhasaresizingmaskthatwillbetranslatedintoaconstraint.Buildandruntheapplicationandnavigatetothemapinterface.Youwillnotlikewhatyousee.Theconsolewillreporttheproblemanditssolution.Unabletosimultaneouslysatisfyconstraints.
Probablyatleastoneoftheconstraintsinthefollowinglistisoneyoudon't
want.Trythis:(1)lookateachconstraintandtrytofigureoutwhichyoudon't
expect;(2)findthecodethataddedtheunwantedconstraintorconstraintsand
fixit.(Note:Ifyou'reseeingNSAutoresizingMaskLayoutConstraintsthatyoudon't
understand,refertothedocumentationfortheUIViewproperty
translatesAutoresizingMaskIntoConstraints)
(
"<NSAutoresizingMaskLayoutConstraint:0x7fb6b8e0ad00
h=--&v=--&H:[UISegmentedControl:0x7fb6b9897390(212)]>",
"<NSLayoutConstraint:0x7fb6b9975350UISegmentedControl:0x7fb6b9897390.leading
==UILayoutGuide:0x7fb6b9972640'UIViewLayoutMarginsGuide'.leading>",
"<NSLayoutConstraint:0x7fb6b9975460UISegmentedControl:0x7fb6b9897390.trailing
==UILayoutGuide:0x7fb6b9972640'UIViewLayoutMarginsGuide'.trailing>",
"<NSLayoutConstraint:0x7fb6b8e0b370'UIView-Encapsulated-Layout-Width'
H:[MKMapView:0x7fb6b8d237c0(0)]>",
"<NSLayoutConstraint:0x7fb6b9972020'UIView-leftMargin-guide-constraint'
H:|-(0)-[UILayoutGuide:0x7fb6b9972640'UIViewLayoutMarginsGuide'](LTR)
(Names:'|':MKMapView:0x7fb6b8d237c0)>",
"<NSLayoutConstraint:0x7fb6b9974f50'UIView-rightMargin-guide-constraint'
H:[UILayoutGuide:0x7fb6b9972640'UIViewLayoutMarginsGuide']-(0)-|(LTR)
(Names:'|':MKMapView:0x7fb6b8d237c0)>"
)
Willattempttorecoverbybreakingconstraint
<NSLayoutConstraint:0x7fb6b9975460UISegmentedControl:0x7fb6b9897390.trailing
==UILayoutGuide:0x7fb6b9972640'UIViewLayoutMarginsGuide'.trailing>
MakeasymbolicbreakpointatUIViewAlertForUnsatisfiableConstraintstocatch
thisinthedebugger.
ThemethodsintheUIConstraintBasedLayoutDebuggingcategoryonUIViewlisted
in<UIKit/UIView.h>mayalsobehelpful.
Let’sgooverthisoutput.AutoLayoutisreportingthatitisUnabletosimultaneously
satisfyconstraints.Thishappenswhenaviewhierarchyhasconstraintsthatconflict.Then,theconsolespitsoutsomehandytipsandalistofallconstraintsthatareinvolved,withtheirdescriptions.Let’slookattheformatofoneoftheseconstraintsmoreclosely.<NSLayoutConstraint:0x7fb6b9975350UISegmentedControl:0x7fb6b9897390.leading
==UILayoutGuide:0x7fb6b9972640'UIViewLayoutMarginsGuide'.leading>
Thisdescriptionindicatesthattheconstraintlocatedatmemoryaddress0x7fb6b9975350issettingtheleadingedgeoftheUISegmentedControl(at0x7fb6b9897390)equaltotheleadingedgeofthemarginoftheUILayoutGuide(at0x7fb6b9972640).FiveoftheseconstraintsareinstancesofNSLayoutConstraint.One,however,isaninstanceofNSAutoresizingMaskLayoutConstraint.Thisconstraintistheproductofthetranslationoftheimageview’sautoresizingmask.Finally,AutoLayouttellsyouhowitisgoingtosolvetheproblembylistingtheconflictingconstraintthatitwillignore.Unfortunately,itchoosespoorlyandignoresoneofyourexplicitinstancesofNSLayoutConstraintinsteadoftheNSAutoresizingMaskLayoutConstraint.Thisiswhyyourinterfacelookslikeitdoes.Thenotebeforetheconstraintsarelistedisveryhelpful:TheNSAutoresizingMaskLayoutConstraintneedstoberemoved.Betteryet,youcanpreventthisconstraintfrombeingaddedinthefirstplacebyexplicitlydisablingtranslationinloadView()://segmentedControl.translatesAutoresizingMaskIntoConstraints=false
view.addSubview(segmentedControl)
7Localization
TheappealofiOSisglobal–iOSusersliveinmanycountriesandspeakmanylanguages.Youcanensurethatyourapplicationisreadyforaglobalaudiencethroughtheprocessesofinternationalizationandlocalization.Internationalizationismakingsureyournativeculturalinformation(likelanguage,currency,dateformat,numberformat,etc.)isnothardcodedintoyourapplication.Localizationistheprocessofprovidingtheappropriatedatainyourapplicationbasedontheuser ’sLanguageandRegionFormatsettings.YoucanfindthesesettingsintheiOSSettingsapplication(Figure7.1).SelecttheGeneralrowandthentheLanguage&Regionrow.Figure7.1Languageandregionsettings
Here,userscansettheirregion,likeUnitedStatesorUnitedKingdom.(WhydoesAppleuse“region”insteadof“country”?Somecountrieshavemorethanoneregionwithdifferentsettings.ScrollthroughtheoptionsinRegiontoseeforyourself.)Applemakesinternationalizationandlocalizationrelativelysimple.Anapplicationthattakes
advantageofthelocalizationAPIsdoesnotevenneedtoberecompiledtobedistributedinotherlanguagesorregions.(Bytheway,because“internationalization”and“localization”arelongwords,youwillsometimesseethemabbreviatedasi18nandL10n,respectively.)
Inthischapter,youwillfirstinternationalizetheWorldTrotterapplicationandthenlocalizeitintoSpanish(Figure7.2).Figure7.2LocalizedWorldTrotter
Internationalization
Inthisfirstsection,youwillusetheNumberFormatterandNSNumberclassestointernationalizetheConversionViewController.
Formatters
InChapter4,youusedaninstanceofNumberFormattertosetthetextoftheCelsiuslabelinConversionViewController.NumberFormatterhasalocaleproperty,whichissettothedevice’scurrentlocale.WheneveryouuseaNumberFormattertocreateanumber,itchecksitslocalepropertyandsetstheformataccordingly.SothetextoftheCelsiuslabelhasbeeninternationalizedfromthestart.Localeknowshowdifferentregionsdisplaysymbols,dates,anddecimalsandwhethertheyusethemetricsystem.AninstanceofLocalerepresentsoneregion’ssettingsforthesevariables.WhenyouaccessthecurrentpropertyonLocale,theinstanceofLocalethatrepresentstheuser ’sregionsettingisreturned.OnceyouhavethatinstanceofLocale,youcanaskitquestions,like,“Doesthisregionusethemetricsystem?”or,“Whatisthecurrencysymbolforthisregion?”letcurrentLocale=Locale.current
letisMetric=currentLocale.usesMetricSystem
letcurrencySymbol=currentLocale.currencySymbol
EventhoughtheCelsiuslabelisalreadyinternationalized,thereisstillaproblemwithit.ChangethesystemregiontoSpaintosee.Selecttheactiveschemepop-upandselectEditScheme...(Figure7.3).Figure7.3Editscheme
MakesurethatRunisselectedonthelefthandsideandthenselecttheOptionstabatthetop.IntheApplicationRegionpop-up,selectEuropeandthenSpain(Figure7.4).Finally,Closetheactiveschemewindow.
Figure7.4Selectingadifferentregion
Buildandruntheapplication.OntheConversionViewController,tapthetextfieldandmakesurethesoftwarekeyboardisvisible.Youmayalreadynoticeonedifference:InSpain,thedecimalseparatorisacommainsteadofaperiod(andthethousandsseparatorisaperiodinsteadofacomma),sothenumberwritten123,456.789intheUnitedStateswouldbewritten123.456,789inSpain.Attempttotypeinmultipledecimalseparators(thecomma)andnoticethattheapplicationhappilyallowsit.Whoops!Yourcodefordisallowingmultipledecimalseparatorschecksforaperiodinsteadofusingalocale-specificdecimalseparator.Let’sfixthat.OpenConversionViewController.swiftandupdatetextfield(_:shouldChangeCharactersIn:replacementString:)tousethelocale-specificdecimalseparator.functextField(_textField:UITextField,
shouldChangeCharactersInrange:NSRange,
replacementStringstring:String)->Bool{
letexistingTextHasDecimalSeparator=textField.text?.range(of:".")
letreplacementTextHasDecimalSeparator=string.range(of:".")
letcurrentLocale=Locale.current
letdecimalSeparator=currentLocale.decimalSeparator??"."
letexistingTextHasDecimalSeparator
=textField.text?.range(of:decimalSeparator)
letreplacementTextHasDecimalSeparator=string.range(of:decimalSeparator)
ifexistingTextHasDecimalSeparator!=nil,
replacementTextHasDecimalSeparator!=nil{
returnfalse
}else{
returntrue
}
}
Buildandruntheapplication.Theapplicationnolongerallowsyoutotypeinmultipledecimalseparators,anditdoesthisinawaythatisindependentoftheuser ’sregionchoice.Butthereisstillaproblem.Ifyoutypeinanumberwithadecimalseparatorthatisnotaperiod,theconversiontoCelsiusisnothappening–theCelsiuslabeldisplays“???”.Whatisgoingonhere?InfahrenheitFieldEditingChanged(_:),youareusinganinitializerfortheDoubletypethattakesinastringasitsargument.Thisinitializerdoesnotknowhowtohandleastringthatusessomethingotherthanaperiodforitsdecimalseparator.Let’sfixthiscodeusingtheNumberFormatterclass.InConversionViewController.swift,updatefahrenheitFieldEditingChanged(_:)toconvertthetextfield’sstringintoanumberinalocale-independentway.@IBActionfuncfahrenheitFieldEditingChanged(_textField:UITextField){
iflettext=textField.text,letvalue=Double(text){
fahrenheitValue=Measurement(value:value,unit:.fahrenheit)
iflettext=textField.text,letnumber=numberFormatter.number(from:text){
fahrenheitValue=Measurement(value:number.doubleValue,unit:.fahrenheit)
}else{
fahrenheitValue=nil
}
}
Hereyouareusingthenumberformatter ’sinstancemethodnumber(from:)toconvertthestringintoanumber.Becausethenumberformatterisawareofthelocale,itisabletoconvertthestringintoanumber.Ifthestringcontainsavalidnumber,themethodreturnsaninstanceofNSNumber.NSNumberisaclassthatcanrepresentavarietyofnumbertypes,includingInt,Float,Double,andmore.YoucanaskaninstanceofNSNumberforitsvaluerepresentedasoneofthosevalues.YouaredoingthatheretogetthedoubleValueofthenumber.Buildandruntheapplication.Nowthatyouareconvertingthestringinalocale-independentway,thetextfield’svalueisproperlyconvertedtoitsCelsiusvalue(Figure7.5).
Figure7.5Conversionwithacommaseparator
Baseinternationalization
Wheninternationalizing,youasktheinstanceofLocalequestions.ButtheLocaleonlyhasafewregion-specificvariables.Thisiswherelocalization–creatingapplication-specificsubstitutionsfordifferentregionandlanguagesettings–comesintoplay.Localizationusuallyinvolveseithergeneratingmultiplecopiesofresources(likeimages,sounds,andinterfacefiles)fordifferentregionsandlanguagesorcreatingandaccessingstringstables(whichyouwillseelaterinthechapter)totranslatetextintodifferentlanguages.Beforeyougothroughtheprocessoflocalizingresources,youmustunderstandhowaniOSapplicationhandleslocalizedresources.WhenyoubuildatargetinXcode,anapplicationbundleiscreated.AlloftheresourcesthatyouaddedtothetargetinXcodearecopiedintothisbundlealongwiththeexecutableitself.
ThisbundleisrepresentedatruntimebyaninstanceofBundleknownasthemainbundle.ManyclassesworkwiththeBundletoloadresources.
Localizingaresourceputsanothercopyoftheresourceintheapplicationbundle.Theseresourcesareorganizedintolanguage-specificdirectories,knownaslprojdirectories.Eachoneofthesedirectoriesisthenameofthelocalizationsuffixedwithlproj.Forexample,theAmericanEnglishlocalizationisen_US,whereenistheEnglishlanguagecodeandUSistheUnitedStatesofAmericaregioncode,sothedirectoryforAmericanEnglishresourcesisen_US.lproj.(Theregioncanbeomittedifyoudonotneedtomakeregionaldistinctionsinyourresourcefiles.)Theselanguageandregioncodesarestandardonallplatforms,notjustiOS.Whenabundleisaskedforthepathofaresourcefile,itfirstlooksattherootlevelofthebundleforafileofthatname.Ifitdoesnotfindone,itlooksatthelocaleandlanguagesettingsofthedevice,findstheappropriatelprojdirectory,andlooksforthefilethere.Thus,justbylocalizingresourcefiles,yourapplicationwillautomaticallyloadthecorrectfile.Oneoptionforlocalizingresourcefilesistocreateseparatestoryboardfilesandmanuallyediteachstringineachfile.However,thisapproachdoesnotscalewellifyouareplanningmultiplelocalizations.Whathappenswhenyouaddanewlabelorbuttontoyourlocalizedstoryboard?Youhavetoaddthisviewtothestoryboardforeverylanguage.Notfun.Tosimplifytheprocessoflocalizinginterfacefiles,Xcodehasafeaturecalledbaseinternationalization.BaseinternationalizationcreatestheBase.lprojdirectory,whichcontainsthemaininterfacefiles.LocalizingindividualinterfacefilescanthenbedonebycreatingjusttheLocalizable.stringsfiles.Itisstillpossibletocreatethefullinterfacefiles,incaselocalizationcannotbedonebychangingstringsalone.However,withthehelpofAutoLayout,stringreplacementissufficientformostlocalizationneeds.Inthenextsection,youwilluseAutoLayouttoprepareyourlayoutforlocalization.
Preparingforlocalization
OpenMain.storyboardandshowtheassistanteditoreitherbyclickingView→AssistantEditor→ShowAssistantEditororwiththekeyboardshortcutOption-Command-Return.Fromthejumpbardropdown,selectPreview(Figure7.6).Thepreviewassistantallowsyoutoeasilyseehowyourinterfacewilllookacrossscreensizesandorientationsaswellasbetweendifferentlocalizedlanguages.
Figure7.6Openingthepreviewassistant
Inthestoryboard,selecttheConversionViewControllertoseeitspreview(Figure7.7).Figure7.7Previewassistant
Noticethecontrolsinthelowercornersofthepreviewassistant.The+buttonontheleftsideallowsyoutoaddadditionalscreensizestothepreviewcanvas.Thisallowsyoutoeasilyseehowchangestoyourinterfacepropagateacrossscreensizesandorientationssimultaneously.Thebuttonontherightsideallowsyoutoselectalanguagetopreviewthisinterfacein.(IfyourpreviewisforaconfigurationotherthaniPhone7,usethe+buttontoaddthisconfiguration.ThenclickonwhateverpreviewopenedbydefaultandpresstheDeletekeytoremoveit.)Youhavenotlocalizedtheapplicationintoanotherlanguageyet,butXcodesuppliesapseudolanguageforyoutouse.Pseudolanguageshelpyouinternationalizeyourapplicationsbeforereceivingtranslationsforallofyourstringsandassets.Thebuilt-inpseudolanguage,Double-LengthPseudolanguage,mimicslanguagesthataremoreverbosebyrepeatingwhatevertextstringisinthetextelement.So,forexample,“isreally”becomes“isreallyisreally.”SelecttheLanguagepop-upthatsaysEnglishandchooseDouble-LengthPseudolanguage.Thelabelsall
havetheirtextdoubled(Figure7.8).Figure7.8Doubledtextstrings
Thedouble-lengthpseudolanguagerevealsaproblemimmediately:Thelabelsgooffboththeleftandrightedgesofthescreen,andyouareunabletoreadtheentirestrings.Thefixistoconstrainallofthelabelssothattheirleadingandtrailingedgesstaywithinthemarginsoftheirsuperview.Thenyouwillneedtochangethelinecountforthelabelsto0,whichtellsthelabelsthattheirtextshouldwraptomultiplelinesifneeded.Youaregoingtostartbyfixingonelabel,thenrepeatthestepsfortherestofthelabels.Inthecanvas,selectthedegreesFahrenheitlabel.Youaregoingtoaddconstraintstothislabelinanewway.Control-dragfromthelabeltotheleftsideofthesuperview.Whenyoudo,acontext-sensitivepop-upwillappeargivingyoutheconstraintsthatmakesenseforthisdirection(Figure7.9).SelectLeadingSpacetoContainerMarginfromthelist.
Figure7.9CreatingconstraintsbyControl-dragging
Thedirectionthatyoudraginfluenceswhichpossibleconstraintsaredisplayed.Ahorizontaldragwillshowhorizontalconstraints,andaverticaldragwillshowverticalconstraints.Adiagonaldragwillshowbothhorizontalandverticalconstraints,whichisusefulforsettingupmanyconstraintssimultaneously.NowControl-dragfromthedegreesFahrenheitlabeltotherightsideofthesuperviewandselectTrailingSpacetoContainerMargin.Ontheirown,theseconstraintsarenotverygood.Theymaintaintheexistingfixeddistancebetweentheleadingandtrailingedgesofthelabel,asyoucanseeinthepreviewassistant(Figure7.10).
Figure7.10Previewassistantwithnewconstraints
Whatyoureallywantisforthedistancebetweenthelabelandthemarginstobegreaterthanorequalto0.Youcandothiswithinequalityconstraints.SelecttheleadingconstraintbyclickingontheI-bartotheleftofthelabel.OpenitsattributesinspectorandchangetheRelationtoGreaterThanorEqualandtheConstantto0(Figure7.11).
Figure7.11Inequalityconstraint
Dothesameforthetrailingconstraint.Takealookatthepreviewassistant;theinterfaceislookingbetter,butthelabelisstillbeingtruncated.Selectthelabelandopenitsattributesinspector.ChangetheLinescountto0.Nowtakealookatthepreviewassistant;thelabelisnolongerbeingtruncatedandinsteadthetextflowstoasecondline.Becausetheotherlabelsareeachrelatedtothelabelabovethem,theyhaveautomaticallybeenmoveddown.Repeatthestepsabovefortheotherlabels.Youwillneedto:
Addaleadingandtrailingconstrainttoeachlabel.Settheconstraints’relationtoGreaterThanorEqualandtheconstantto0.(Ashortcutforeditingaconstraintistodouble-clickonit.)Changethelabel’slinecountto0.
Whenyouaredone,thepreviewassistantwiththedouble-lengthpseudolanguagewilllooklikeFigure7.12.
Figure7.12Previewassistantwithfinalconstraints
Atthispoint,youaredonewiththepreviewassistant.Youcanclosetheassistanteditorwiththexinthetop-rightcorner.
Localization
WorldTrotterisnowinternationalized–itsinterfaceisabletoadapttovariouslanguagesandregions.Nowitistimetolocalizetheapp–thatis,toupdatethestringsandresourcesintheapplicationforanewlanguage.Inthissection,youaregoingtolocalizetheinterfaceofWorldTrotter:theMain.storyboardfile.YouwillcreateEnglishandSpanishlocalizations,whichwillcreatetwolprojdirectoriesinadditiontothebaseone.Startbylocalizingastoryboardfile.SelectMain.storyboardintheprojectnavigator.Openthefileinspectorbyclickingthe tabintheinspectorselectororbyusingthekeyboardshortcutOption-Command-1.FindthesectioninthisinspectornamedLocalization.ChecktheEnglishboxandmakesurethatthedropdownsaysLocalizableStrings(Figure7.13).Thiswillcreateastringstablethatyouwilluselatertolocalizetheapplication.Figure7.13LocalizingintoEnglish
Next,intheprojectnavigator,selecttheWorldTrotterprojectatthetop.ThenselectWorldTrotterundertheProjectsectioninthesidelist,andmakesuretheInfotabisopen.(Ifyoucannotseethesidelist,youcanopenitusingtheShowprojectsandtargetslistbuttonintheupper-leftcorner(Figure7.14).)Figure7.14Showingtheprojectsettings
Clickthe+buttonunderLocalizationsandselectSpanish(es).Inthedialog,youcanunchecktheLaunchScreen.storyboardfile;keeptheMain.storyboardfilechecked.MakesurethatthereferencelanguageisBaseandthefiletypeisLocalizableStrings.ClickFinish.Thiscreatesanes.lprojfolderandgeneratestheMain.stringsfileinitthatcontainsallthe
stringsfromthebaseinterfacefile.TheLocalizationsconfigurationshouldlooklikeFigure7.15.Figure7.15Localizations
Lookintheprojectnavigator.ClickthedisclosurebuttonnexttoMain.storyboard(Figure7.16).XcodemovedtheMain.storyboardfiletotheBase.lprojdirectoryandcreatedtheMain.stringsfileinthees.lprojdirectory.Figure7.16Localizedstoryboardintheprojectnavigator
ClickontheSpanishversionofMain.strings.Whenthisfileopens,thetextisnotinSpanish.Youhavetotranslatelocalizedfilesyourself;Xcodeisnotthatsmart.Editthisfileaccordingtothefollowingtext.Thenumbersandordermaybedifferentinyour
file,butyoucanusethetextandtitlefieldsinthecommentstomatchupthetranslations./*Class="UITabBarItem";title="Map";ObjectID="6xh-o5-yRt";*/
"6xh-o5-yRt.title"="Map""Mapa";
/*Class="UILabel";text="degreesCelsius";ObjectID="7la-u7-mx6";*/
"7la-u7-mx6.text"="degreesCelsius""gradosCelsius";
/*Class="UILabel";text="degreesFahrenheit";ObjectID="Dic-rs-P0S";*/
"Dic-rs-P0S.text"="degreesFahrenheit""gradosFahrenheit";
/*Class="UILabel";text="100";ObjectID="Eso-Wf-EyH";*/
"Eso-Wf-EyH.text"="100";
/*Class="UITextField";placeholder="value";ObjectID="On4-jV-YlY";*/
"On4-jV-YlY.placeholder"="value""valor";
/*Class="UILabel";text="isreally";ObjectID="wtF-xR-gbZ";*/
"wtF-xR-gbZ.text"="isreally""esrealmente";
/*Class="UITabBarItem";title="Convert";ObjectID="zLY-50-CeX";*/
"zLY-50-CeX.title"="Convert""Convertir";
Nowthatyouhavefinishedlocalizingthisstoryboardfile,let’stestitout.First,thereisalittleXcodeglitchtobeawareof:SometimesXcodeignoresaresourcefile’schangeswhenyoubuildanapplication.Toensurethatyourapplicationisbeingbuiltfromscratch,firstdeleteitfromyourdeviceorsimulator.(Pressandholditsiconinthelauncher.Whenitstartstowiggle,tapthedeletebadge.)RelaunchXcode.(Yes,exitandstartitagain.)Then,chooseCleanfromtheProductmenu.Finally,tobeabsolutelysure,pressandholdtheOptionkeywhileopeningtheProductmenuandchooseCleanBuildFolder....Thiswillforcetheapplicationtobeentirelyrecompiled,rebundled,andreinstalled.Opentheactiveschemepop-upandselectEditScheme.MakesureRunisselectedonthelefthandsideandopentheOptionstab.OpentheApplicationLanguagepop-upandselectSpanish.Finally,confirmthatSpainisstillselectedfromtheApplicationRegionpop-up.Closethewindow.Buildandruntheapplication.MakesureyouareviewingtheConversionViewController,andyouwillseetheinterfaceinSpanish.Becauseyousettheconstraintsonthelabelstoaccommodatedifferentlengthsoftext,theyresizethemselvesappropriately(Figure7.17).
Figure7.17SpanishConversionViewController
NSLocalizedStringandstringstables
Inmanyplacesinyourapplications,youcreateStringinstancesdynamicallyordisplaystringliteralstotheuser.Todisplaytranslatedversionsofthesestrings,youmustcreateastringstable.Astringstableisafilecontainingalistofkey-valuepairsforallofthestringsthatyourapplicationusesandtheirassociatedtranslations.Itisaresourcefilethatyouaddtoyourapplication,butyoudonotneedtodoalotofworktogetdatafromit.Youmightuseastringinyourcodelikethis:letgreeting="Hello!"
Tointernationalizethestringinyourcode,youreplaceliteralstringswiththefunctionNSLocalizedString(_:comment:).letgreeting=NSLocalizedString("Hello!",comment:"Thegreetingfortheuser")
Thisfunctiontakestwoarguments:akeyandacommentthatdescribesthestring’suse.Thekeyisthelookupvalueinastringstable.Atruntime,NSLocalizedString(_:comment:)willlookthroughthestringstablesbundledwithyourapplicationforatablethatmatchestheuser ’slanguagesettings.Then,inthattable,thefunctiongetsthetranslatedstringthatmatchesthekey.NowyouaregoingtointernationalizethestringsthattheMapViewControllerdisplaysinitssegmentedcontrol.InMapViewController.swift,locatetheloadView()methodandupdatetheinitializerforthesegmentedcontroltouselocalizedstrings.
overridefuncloadView(){
//Createamapview
mapView=MKMapView()
//Setitas*the*viewofthisviewcontroller
view=mapView
letsegmentedControl
=UISegmentedControl(items:["Standard","Satellite","Hybrid"])
letstandardString=NSLocalizedString("Standard",comment:"Standardmapview")
letsatelliteString
=NSLocalizedString("Satellite",comment:"Satellitemapview")
lethybridString=NSLocalizedString("Hybrid",comment:"Hybridmapview")
letsegmentedControl
=UISegmentedControl(items:[standardString,satelliteString,hybridString])
OnceyouhavefilesthathavebeeninternationalizedwiththeNSLocalizedString(_:comment:)function,youcangeneratestringstableswithacommand-lineapplication.OpentheTerminalapp.ThisisaUnixterminal;itisusedtoruncommand-linetools.YouwanttonavigatetothelocationofMapViewController.swift.IfyouhaveneverusedtheTerminalappbefore,hereisahandytrick.InTerminal,typethefollowing:cd
followedbyaspace.(DonotpressReturnyet.)Next,openFinderandlocateMapViewController.swiftandthefolderthatcontainsit.DragtheiconofthatfolderontotheTerminalwindow.Terminalwillfilloutthepathforyou.Itwilllooksomethinglikethis:cd/Users/cbkeur/iOSDevelopment/WorldTrotter/WorldTrotter/
PressReturn.ThecurrentworkingdirectoryofTerminalisnowthisdirectory.UsetheterminalcommandlstoprintoutthecontentsoftheworkingdirectoryandconfirmthatMapViewController.swiftisinthatlist.Togeneratethestringstable,enterthefollowingintoTerminalandpressReturn:genstringsMapViewController.swift
Theresultingfile,Localizable.strings,containsthestringsfromMapViewController.DragthisnewfilefromFinderintotheprojectnavigator(orusetheFile→AddFilesto"WorldTrotter"...menuitem).Whentheapplicationiscompiled,thisresourcewillbecopiedintothemainbundle.OpenLocalizable.strings.Thefileshouldlooksomethinglikethis:/*Hybridmapview*/
"Hybrid"="Hybrid";
/*Satellitemapview*/
"Satellite"="Satellite";
/*Standardmapview*/
"Standard"="Standard";
NoticethatthecommentaboveyourstringisthesecondargumentyousuppliedtotheNSLocalizedStringfunction.Eventhoughthefunctiondoesnotrequirethecommentargument,includingitwillmakeyourlocalizinglifeeasier.NowthatyouhavecreatedLocalizable.strings,youneedtolocalizeitinXcode.OpenitsfileinspectorandclicktheLocalize...buttonintheutilityarea.MakesureBaseisselectedfromthepop-upandclickLocalize.AddtheSpanishandEnglishlocalizationbycheckingtheboxnexttoeachlanguage.Intheprojectnavigator,clickonthedisclosuretrianglethatnowappearsnexttoLocalizable.strings.OpentheSpanishversion.ThestringonthelefthandsideisthekeythatispassedtotheNSLocalizedString(_:comment:)function,andthestringontherighthandsideiswhatisreturned.ChangethetextontherighthandsidetotheSpanishtranslationsshownbelow.(Totypeanaccentedcharacter,suchas“é,”pressandholdtheappropriatecharacteronyourkeyboardandthenpresstheappropriatenumberfromthepop-up.)/*Hybridmapview*/
"Hybrid"="Hybrid""Híbrido";
/*Satellitemapview*/
"Satellite"="Satellite""Satélite";
/*Standardmapview*/
"Standard"="Standard""Estándar";
Buildandruntheapplicationagain.Nowallthesestrings,includingthetitlesinthesegmentedcontrol,willappearinSpanish(Figure7.18).Iftheydonot,youmightneedtodeletetheapplication,cleanyourproject,andrebuild.(Orcheckyourschemelanguagesetting.)Figure7.18SpanishMapViewController
Internationalizationandlocalizationareveryimportantforyourapptoreachthelargestaudience.Additionally,asyousawearlyinthischapter,yourappmightnotworkforsomeusersifyouhavenotproperlyinternationalizedit.Youwillinternationalize(butnotlocalize)
yourprojectsintherestofthisbook.Overthepastfivechapters,youhavebuiltaratherimpressiveapplicationthatallowstheusertoconvertbetweenCelsiusandFahrenheitaswellasdisplayamapinafewdifferentways.NotonlydoesthisapplicationscalewellonalliPhonescreensizes,butitisalsolocalizedintoanotherlanguage.Congratulations!
BronzeChallenge:AnotherLocalization
Practicemakesperfect.LocalizeWorldTrotterforanotherlanguage.Useatranslationwebsiteifyouneedhelpwiththelanguage.
FortheMoreCurious:Bundle’sRoleinInternationalization
TherealworkoftakingadvantageoflocalizationsisdoneforyoubytheclassBundle.Abundlerepresentsalocationonthefilesystemthatgroupsthecompiledcodeandresourcestogether.The“mainbundle”isanothernamefortheapplicationbundle,whichcontainsalloftheresourcesandtheexecutablefortheapplication.YouwilllearnmoreabouttheapplicationbundleinChapter16.Whenanapplicationisbuilt,allofthelprojdirectoriesarecopiedintothemainbundle.Figure7.19showsthemainbundleforWorldTrotter(withsomeadditionalimagesaddedtotheproject).Figure7.19Applicationbundle
Bundleknowshowtosearchthroughlocalizationdirectoriesforeverytypeofresourceusingtheinstancemethodurl(forResource:withExtension:).Whenyouwantapathtoaresourcebundledwithyourapplication,youcallthismethodonthemainbundle.HereisanexampleusingtheresourcefileBoo.png:letpath=Bundle.main.url(forResource:"Boo",withExtension:"png")
Whenattemptingtolocatetheresource,thebundlefirstcheckstoseewhethertheresourceexistsatthetopleveloftheapplicationbundle.Ifso,itreturnsthefullURLtothatfile.Ifnot,thebundlegetsthedevice’slanguageandregionsettingsandlooksintheappropriatelprojdirectoriestoconstructtheURL.Ifitstilldoesnotfindit,itlookswithintheBase.lproj
directory.Finally,ifnofileisfound,itreturnsnil.
IntheapplicationbundleshowninFigure7.19,iftheuser ’slanguageissettoSpanish,BundlewillfindBoo.pngatthetoplevel,Tom.pngines.lproj,andHat.pnginBase.lproj.Whenyouaddanewlocalizationtoyourproject,Xcodedoesnotautomaticallyremovetheresourcesfromthetop-leveldirectory.Thisiswhyyoumustdeleteandcleananapplicationwhenyoulocalizeafile–otherwise,thepreviousunlocalizedfilewillstillbeintherootleveloftheapplicationbundle.Eventhoughtherearelprojfoldersintheapplicationbundle,thebundlefindsthetop-levelfilefirstandreturnsitsURL.
FortheMoreCurious:ImportingandExportingasXLIFF
Theindustry-standardformatforlocalizationdataistheXLIFFdatatype,whichstandsforXMLLocalisationInterchangeFileFormat(andXMLstandsforExtensibleMarkupLanguage).Whenworkingwithtranslators,youwilloftensendthemanXLIFFfilecontainingthedataintheapplicationtolocalize,andtheywillgiveyoubackalocalizedXLIFFfileforyoutoimport.XcodenativelysupportsimportingandexportinglocalizationdatainXLIFF.Theexportingprocesswilltakecareoffindingandexportingthelocalizedstringswithintheproject,whichyoupreviouslydidmanuallyusingthegenstringstool.ToexportthelocalizablestringsinXLIFF,selecttheproject(WorldTrotter)intheprojectnavigator.ThenselecttheEditormenu,andthenExportForLocalization....Onthenextscreen,youcanchoosewhethertoexportexistingtranslations(whichisprobablyagoodideasothetranslatordoesnotdoredundantwork)andwhichlanguagesyouwouldlikeexported(Figure7.20).Figure7.20ExportinglocalizationdataasXLIFF
Toimportlocalizations,selecttheproject(WorldTrotter)intheprojectnavigator.ThenselectEditor→ImportLocalizations....Afterchoosingafile,youwillbeabletoconfirmtheupdatesbeforeyouimport.
8ControllingAnimations
Theword“animation”isderivedfromaLatinwordthatmeans“theactofbringingtolife.”Inyourapplications,animationscansmoothlybringinterfaceelementsonscreenorintofocus,theycandrawtheuser ’sattentiontoanactionableitem,andtheycangiveclearindicationsofhowyourappisrespondingtotheuser ’sactions.Inthischapter,youwillreturntoyourQuizappanduseavarietyofanimationtechniquestobringittolife.BeforeupdatingQuiz,though,let’stakealookatwhatcanbeanimatedbylookingatthedocumentation.Toopenthedocumentation,openXcode’sHelpmenuandselectDocumentationandAPIReference.Thiswillopenthedocumentationinanewwindow.Withthedocumentationopen,usethesearchbaratthetoptosearchfor“UIView.”UnderAPIReferenceinthesearchresults,clickUIViewtoopentheclassreference,thenscrolldowntothesectiontitledAnimations.Thedocumentationgivessomeanimationrecommendations(whichwewillfollowinthisbook)andliststhepropertiesonUIViewthatcanbeanimated(Figure8.1).Figure8.1UIViewanimationdocumentation
BasicAnimations
ThedocumentationisalwaysagoodstartingpointforlearningaboutanyiOStechnology.Withthatlittlebitofinformationunderyourbelt,let’sgoaheadandaddsomeanimationstoQuiz.Thefirsttypeofanimationyouaregoingtouseisthebasicanimation.Abasicanimationanimatesbetweenastartvalueandanendvalue(Figure8.2).Figure8.2Basicanimation
Thefirstanimationyouaregoingtoaddwillanimatethealphavalue(thedegreeoftransparency)ofthequestionlabelassociatedwithViewController.Whentheuseradvancestothenextquestion,youwilluseananimationtofadeinthelabel.ThereareclassmethodsonUIViewthatwillallowyoutoaccomplishthis.ThesimplestUIViewanimationmethodis:classfuncanimate(withDurationduration:TimeInterval,animations:()->Void)
Thisclassmethodtakestwoarguments:adurationoftypeTimeInterval(whichisanaliasforaDouble)andananimationsvariablethatisaclosure.
Closures
Aclosureisadiscretebundleoffunctionalitythatcanbepassedaroundyourcode.Closuresarealotlikefunctionsandmethods.Infact,functionsandmethodsarejustspecialcasesofclosures.Closureshavealightweightsyntaxthatallowsthemtobeeasilypassedinasargumentstofunctionsandmethods.Aclosurecanevenbethereturntypeofafunctionormethod.Inthissection,youwilluseaclosuretospecifytheanimationsthatyouwanttooccur.Thesignatureofaclosureisacomma-separatedlistofargumentswithinparenthesesfollowedbyareturnarrowandthereturntype:(arguments)->returntype
Noticethatthissyntaxissimilartothesyntaxforfunctions:funcfunctionName(arguments)->returntype
Nowtakealookagainattheclosuresignaturethattheanimationsargumentexpects:classfuncanimate(withDurationduration:TimeInterval,animations:()->Void)
Thisclosuretakesinnoargumentsanddoesnotreturnanything.(Youwillalsoseethisreturntypeexpressedas(),whichmeansthesamethingasVoid.)Theclosuresignatureisprettystraightforwardandfamiliar,buthowdoyoudeclareaclosure
incode?Closuresyntaxtakesthefollowingform:{(arguments)->returntypein
//code
}
Youwriteaclosureexpressioninsidebraces({}).Theclosure’sargumentsarelistedinsideparenthesesimmediatelyaftertheopeningbrace.Aclosure’sreturntypecomesaftertheparametersandusestheregularsyntax.Thekeywordinisusedtoseparatetheclosure’sargumentsandreturntypefromthestatementsinsideofitsbody.OpenQuiz.xcodeproj.InViewController.swift,addanewmethodtohandletheanimationsanddeclareaclosureconstantthattakesinnoargumentsanddoesnotreturnanything.funcanimateLabelTransitions(){
letanimationClosure={()->Voidin
}
}
Nowyouhaveaconstantthatreferencesachunkoffunctionality.Currently,however,thisclosuredoesnotactuallydoanything.AddfunctionalitytotheclosurethatsetsthealphaofthequestionLabelto1.Then,passthisclosureasanargumenttoanimate(withDuration:animations:).funcanimateLabelTransitions(){
letanimationClosure={()->Voidin
self.questionLabel.alpha=1
}
//Animatethealpha
UIView.animate(withDuration:0.5,animations:animationClosure)
}
ThequestionLabelalreadyhasanalphaof1whenitcomesonscreen,soyouwillnotseeanythinganimateifyoubuildandrun.Toaddressthis,overrideviewWillAppear(_:)toresetthequestionLabel’salphato0eachtimetheViewController’sviewcomesonscreen.overridefuncviewWillAppear(_animated:Bool){
super.viewWillAppear(animated)
//Setthelabel'sinitialalpha
questionLabel.alpha=0
}
Thecodeaboveworksgreat,butyoucanmakeitmoreconcise.Updatethecode.funcanimateLabelTransitions(){
letanimationClosure={()->Voidin
self.questionLabel.alpha=1
}
//Animatethealpha
UIView.animate(withDuration:0.5,animations:animationClosure)
UIView.animate(withDuration:0.5,animations:{
self.questionLabel.alpha=1
})
}
Youhavemadetwochanges:First,youarepassingintheclosureanonymously(i.e.,passingitdirectlyintothemethodinsteadofassigningittoavariableorconstant).Second,youhaveremovedthetypeinformationbecausetheclosurecaninferthisfromthecontext.NowcalltheanimateLabelTransitions()methodwhenevertheusertapstheNextQuestionbutton.@IBActionfuncshowNextQuestion(_sender:UIButton){
currentQuestionIndex+=1
ifcurrentQuestionIndex==questions.count{
currentQuestionIndex=0
}
letquestion:String=questions[currentQuestionIndex]
questionLabel.text=question
answerLabel.text="???"
animateLabelTransitions()
}
Buildandruntheapplication.WhenyoutapontheNextQuestionbutton,thelabelwillfadeintoview.Animationsprovidealessjarringuserexperiencethanhavingviewsjustpopintoexistence.
AnotherLabel
TheanimationworksgreatthefirsttimetheNextQuestionbuttonispressed,butthereisnovisibleanimationonsubsequentbuttonpressesbecausethelabelalreadyhasanalphavalueof1.Inthissection,youaregoingtoaddanotherlabeltotheinterface.WhentheNextQuestionbuttonispressed,theexistinglabelwillfadeoutwhilethenewlabel(withthetextofthenextquestion)willfadein.AtthetopofViewController.swift,replaceyourdeclarationofasinglelabelwithtwolabels.@IBOutletvarquestionLabel:UILabel!
@IBOutletvarcurrentQuestionLabel:UILabel!
@IBOutletvarnextQuestionLabel:UILabel!
@IBOutletvaranswerLabel:UILabel!
XcodeflagsfourplaceswhereyouneedtoreplacequestionLabelwithoneofyournewlabels.UpdateviewDidLoad()tousecurrentQuestionLabel.UpdateviewWillAppear(_:)andshowNextQuestion(_:)tousenextQuestionLabel.funcviewDidLoad(){
super.viewDidLoad()
questionLabel.text=questions[currentQuestionIndex]
currentQuestionLabel.text=questions[currentQuestionIndex]
}
overridefuncviewWillAppear(_animated:Bool){
super.viewWillAppear(animated)
//Setthelabel'sinitialalpha
questionLabel.alpha=0
nextQuestionLabel.alpha=0
}
@IBActionfuncshowNextQuestion(_sender:UIButton){
currentQuestionIndex+=1
ifcurrentQuestionIndex==questions.count{
currentQuestionIndex=0
}
letquestion:String=questions[currentQuestionIndex]
questionLabel.text=question
nextQuestionLabel.text=question
answerLabel.text="???"
animateLabelTransitions()
}
NowupdateanimateLabelTransitions()toanimatethealphaofthetwolabels.YouwillfadeoutthecurrentQuestionLabelandfadeinthenextQuestionLabelsimultaneously.funcanimateLabelTransitions(){
//Animatethealpha
UIView.animate(withDuration:0.5,animations:{
self.questionLabel.alpha=1
self.currentQuestionLabel.alpha=0
self.nextQuestionLabel.alpha=1
})
}
OpenMain.storyboard.Nowthatthecodehasbeenupdatedforthesetwolabels,youneedtomaketheconnections.Control-clickontheViewControllertoseealistofconnections.NoticethattheexistingquestionLabelisstillpresentwithayellowwarningsignnexttoit(Figure8.3).Clickonthextoremovethisconnection.Figure8.3Missingconnection
ConnectthecurrentQuestionLabeloutlettotheexistingquestionlabelbydraggingfromthecirclenexttocurrentQuestionLabeltothelabelonthecanvas.NowdraganewLabelontotheinterfaceandpositionitnexttotheexistingquestionlabel.ConnectthenextQuestionLabeltothisnewlabel.Youwantthislabeltobeinthesamepositionastheexistingquestionlabel.Asyouhavelikelyguessed,thebestwaytoachievethisisthroughconstraints.Control-dragfromthenextQuestionLabeltothecurrentQuestionLabelandselectTop.ThenControl-dragupwardfromthenextQuestionLabeltoitssuperviewandselectCenterHorizontallyinContainer.Atthispoint,nextQuestionLabelismisplaced.Selectthelabel,opentheResolveAutoLayoutIssuesmenu,andselectUpdateFrames.Thelabelswillnowbeontopofoneanother.Buildandruntheapplication.TaptheNextQuestionbuttonandyouwillseeagracefulfadeforbothofthelabels.Ifyoutapitagain,however,nofadeoccursbecausethenextQuestionLabelalreadyhasanalphaof1.Tofixthis,youwillswapthereferencestothetwolabels.Whentheanimationcompletes,thecurrentQuestionLabelneedstobesettotheonscreenlabel,andthenextQuestionLabelneedstobesettotheoffscreenlabel.Youwilluseacompletion
handlerontheanimationtoaccomplishthis.
AnimationCompletion
Themethodanimate(withDuration:animations:)returnsimmediately.Thatis,itstartstheanimation,butitdoesnotwaitfortheanimationtocomplete.Whatifyouwanttoknowwhenananimationcompletes?Forinstance,youmightwanttochainanimationstogetherorupdateanotherobjectwhentheanimationcompletes.Toknowwhentheanimationfinishes,passaclosureforthecompletionargument.Youwillusethisopportunitytoswapthetwolabelreferences.InViewController.swift,updateanimateLabelTransitions()tousetheUIViewanimationmethodthathasthemostparameters,includingonethattakesinacompletionclosure.funcanimateLabelTransitions(){
//Animatethealpha
UIView.animate(withDuration:0.5,animations:{
self.currentQuestionLabel.alpha=0
self.nextQuestionLabel.alpha=1
})
UIView.animate(withDuration:0.5,
delay:0,
options:[],
animations:{
self.currentQuestionLabel.alpha=0
self.nextQuestionLabel.alpha=1
},
completion:{_in
swap(&self.currentQuestionLabel,
&self.nextQuestionLabel)
})
}
Thedelayindicateshowlongthesystemshouldwaitbeforetriggeringtheanimation.Wewilltalkabouttheoptionslaterinthischapter.Fornow,youarepassinginanemptyarray.Inthecompletionclosure,youneedtotellthesystemthatwhatusedtobethecurrentQuestionLabelisnowthenextQuestionLabelandthatwhatusedtobethenextQuestionLabelisnowthecurrentQuestionLabel.Toaccomplishthis,youusetheswap(_:_:)function,whichacceptstworeferencesandswapsthem.Buildandruntheapplication.Nowyouareabletotransitionbetweenallofthequestions.
AnimatingConstraints
Inthissection,youaregoingtoextendyouranimationtohavethenextQuestionLabelpropertyflyinfromtheleftsideofthescreenandthecurrentQuestionLabelflyouttotherightsideofthescreenwhentheuserpressestheNextQuestionbutton.Indoingso,youwilllearnhowtoanimateconstraints.First,youneedareferencetotheconstraintsthatneedtobemodified.Sofar,allofyour@IBOutletshavebeentoviewobjects.Butoutletsarenotlimitedtoviews–infact,anyobjectinyourinterfacefilecanhaveanoutlet,includingconstraints.AtthetopofViewController.swift,declaretwooutletsforthetwolabels’centeringconstraints.@IBOutletvarcurrentQuestionLabel:UILabel!
@IBOutletvarcurrentQuestionLabelCenterXConstraint:NSLayoutConstraint!
@IBOutletvarnextQuestionLabel:UILabel!
@IBOutletvarnextQuestionLabelCenterXConstraint:NSLayoutConstraint!
@IBOutletvaranswerLabel:UILabel!
NowopenMain.storyboard.Youwanttoconnectthesetwooutletstotheirrespectiveconstraints.Theeasiestwaytoaccomplishthisisusingthedocumentoutline.ClickthedisclosuretrianglenexttoConstraintsinthedocumentoutlineandlocateCurrentQuestionLabelCenterXConstraint.Control-dragfromtheViewControllertothatconstraint(Figure8.4)andselectthecorrectoutlet.DothesameforNextQuestionLabelCenterXConstraint.Figure8.4Connectingaconstraintoutlet
Currently,theNextQuestionbuttonandtheanswersubviewshavetheircenterXconstrainedtothecenterXofthecurrentQuestionLabel.Whenyouimplementtheanimationforthislabeltoslideoffscreen,theothersubviewswillgowithit.Thisisnotwhatyouwant.SelecttheconstraintthatcenterstheXvalueoftheNextQuestionbuttontothecurrentQuestionLabelanddeleteit.ThenControl-dragupwardfromtheNextQuestionbuttontoitssuperviewandselectCenterHorizontallyinContainer.Next,youwantthetwoquestionlabelstobeonescreenwidthapart.ThecenterofnextQuestionLabelwillbehalfofthescreenwidthtotheleftoftheview.ThecenterofthecurrentQuestionLabelwillbeatitscurrentposition,centeredinthescreen.Whentheanimationistriggered,bothlabelswillmoveafullscreenwidthtotheright,placingthenextQuestionLabelatthecenterofthescreenandthecurrentQuestionLabelhalfascreenwidthtotherightofthescreen(Figure8.5).
Figure8.5Slidingthelabels
Toaccomplishthis,whentheviewofViewControllerisloaded,youneedtomovethenextQuestionLabeltoitsoffscreenposition.InViewController.swift,addanewmethodandcallitfromviewDidLoad().funcviewDidLoad(){
super.viewDidLoad()
currentQuestionLabel.text=questions[currentQuestionIndex]
updateOffScreenLabel()
}
funcupdateOffScreenLabel(){
letscreenWidth=view.frame.width
nextQuestionLabelCenterXConstraint.constant=-screenWidth
}
Nowyouwanttoanimatethelabelstogofromlefttoright.Animatingconstraintsisabitdifferentthananimatingotherproperties.Ifyoumodifytheconstantofaconstraintwithinananimationblock,noanimationwilloccur.Why?Afteraconstraintismodified,thesystemneedstorecalculatetheframesforalloftherelatedviewsinthehierarchytoaccommodatethischange.Itwouldbeexpensiveforanyconstraintchangetotriggerthisautomatically.(Imagineifyouupdatedquiteafewconstraints–youwouldnotwantittorecalculatetheframesaftereachchange.)Soyoumustaskthesystemtorecalculatetheframeswhenyouaredone.Todothis,youcallthemethodlayoutIfNeeded()onaview.Thiswillforcetheviewtolayoutitssubviewsbasedonthelatestconstraints.InViewController.swift,updateanimateLabelTransitions()tochangetheconstraintconstantsandthenforcethelayoutoftheviews.funcanimateLabelTransitions(){
//Animatethealpha
//andthecenterXconstraints
letscreenWidth=view.frame.width
self.nextQuestionLabelCenterXConstraint.constant=0
self.currentQuestionLabelCenterXConstraint.constant+=screenWidth
UIView.animate(withDuration:0.5,
delay:0,
options:[],
animations:{
self.currentQuestionLabel.alpha=0
self.nextQuestionLabel.alpha=1
self.view.layoutIfNeeded()
},
completion:{_in
swap(&self.currentQuestionLabel,
&self.nextQuestionLabel)
})
}
Finally,inthecompletionhandler,youneedtoswapthetwoconstraintoutletsandresetthenextQuestionLabeltobeontheleftsideofthescreen.funcanimateLabelTransitions(){
//Animatethealpha
//andthecenterXconstraints
letscreenWidth=view.frame.width
self.nextQuestionLabelCenterXConstraint.constant=0
self.currentQuestionLabelCenterXConstraint.constant+=screenWidth
UIView.animate(withDuration:0.5,
delay:0,
options:[],
animations:{
self.currentQuestionLabel.alpha=0
self.nextQuestionLabel.alpha=1
self.view.layoutIfNeeded()
},
completion:{_in
swap(&self.currentQuestionLabel,
&self.nextQuestionLabel)
swap(&self.currentQuestionLabelCenterXConstraint,
&self.nextQuestionLabelCenterXConstraint)
self.updateOffScreenLabel()
})
}
Buildandruntheapplication.Theanimationworksalmostperfectly.Thelabelsslideonandoffthescreen,andthealphavalueanimatesappropriatelyaswell.Thereisonesmallproblemtofix,butitcanbeabitdifficulttosee.Toseeitmoreeasily,turnonSlowAnimationsfromtheDebugmenuinthesimulator(Command-T).Thewidthofallofthelabelsgetsanimated(toseethisontheanswerLabel,youneedtoclicktheShowAnswerbutton).Thisisbecausetheintrinsiccontentsizechangeswhenthetextchanges.Thefixistoforcetheviewtolayoutitssubviewsbeforetheanimationbegins.Thiswillupdatetheframesofallthreelabelstoaccommodatethenexttextbeforethealphaandslidinganimationsstart.UpdateanimateLabelTransitions()toforcetheviewtolayoutitssubviewsbeforetheanimationbegins.funcanimateLabelTransitions(){
//Forceanyoutstandinglayoutchangestooccur
view.layoutIfNeeded()
//Animatethealpha
//andthecenterXconstraints
letscreenWidth=view.frame.width
self.nextQuestionLabelCenterXConstraint.constant=0
self.currentQuestionLabelCenterXConstraint.constant+=screenWidth
UIView.animate(withDuration:0.5,
delay:0,
options:[],
animations:{
self.currentQuestionLabel.alpha=0
self.nextQuestionLabel.alpha=1
self.view.layoutIfNeeded()
},
completion:{_in
swap(&self.currentQuestionLabel,
&self.nextQuestionLabel)
swap(&self.currentQuestionLabelCenterXConstraint,
&self.nextQuestionLabelCenterXConstraint)
self.updateOffScreenLabel()
})
}
Buildandruntheapplicationandcyclethroughsomequestionsandanswers.Theminoranimationissueisnowresolved.
TimingFunctions
Theaccelerationoftheanimationiscontrolledbyitstimingfunction.Bydefault,animationsuseanease-in/ease-outtimingfunction.Touseadrivinganalogy,thiswouldmeanthedriveracceleratessmoothlyfromresttoaconstantspeedandthengraduallyslowsdownattheend,comingtorest.Othertimingfunctionsincludelinear(aconstantspeedfrombeginningtoend),ease-in(acceleratingtoaconstantspeedandthenendingabruptly),andease-out(beginningatfullspeedandthenslowingdownattheend).InViewController.swift,updatetheanimationinanimateLabelTransitions()tousealineartimingfunction.UIView.animate(withDuration:0.5,
delay:0,
options:[.curveLinear],
animations:{
self.currentQuestionLabel.alpha=0
self.nextQuestionLabel.alpha=1
self.view.layoutIfNeeded()
},
completion:{_in
swap(&self.currentQuestionLabel,
&self.nextQuestionLabel)
swap(&self.currentQuestionLabelCenterXConstraint,
&self.nextQuestionLabelCenterXConstraint)
self.updateOffScreenLabel()
})
Now,asopposedtousingthedefaultease-in/ease-outanimationcurve,theanimationwillhavealinearanimationcurve.Buildandruntheapplication.Thedifferenceissubtle,butitisnoticeableifyouwatchforit.TheoptionsparametertakesinaUIViewAnimationOptionsargument.Whyisthisargumentinsquarebrackets?Therearemanyoptionsforananimationinadditiontothetimingfunction.Becauseofthis,youneedawayofspecifyingmorethanoneoption–anarray.UIViewAnimationOptionsconformstotheOptionSetprotocol,whichallowsyoutogroupmultiplevaluesusinganarray.Herearesomeofthepossibleanimationoptionsthatyoucanpassintotheoptionsparameter.AnimationcurveoptionsControltheaccelerationoftheanimation.Possiblevaluesare:
UIViewAnimationOptions.curveEaseInOut
UIViewAnimationOptions.curveEaseIn
UIViewAnimationOptions.curveEaseOut
UIViewAnimationOptions.curveLinear
UIViewAnimationOptions.allowUserInteraction
Bydefault,viewscannotbeinteractedwithwhenanimating.Specifyingthisoptionoverridesthedefault.Thiscanbeusefulforrepeatinganimations,suchasapulsingview.UIViewAnimationOptions.repeat
Repeatstheanimationindefinitely;oftenpairedwiththeUIViewAnimationOptions.autoreverseoption.UIViewAnimationOptions.autoreverse
Runstheanimationforwardandthenbackward,returningtheviewtoitsinitialstate.BesuretocheckouttheConstantssectionoftheUIViewClassReferencetoseeallofthepossibleoptions.
BronzeChallenge:SpringAnimations
iOShasapowerfulphysicsenginebuiltin.Aneasywaytoharnessthispowerisbyusingaspringanimation.//UIView
classfuncanimate(withDurationduration:TimeInterval,
delay:TimeInterval,
usingSpringWithDampingdampingRatio:CGFloat,
initialSpringVelocityvelocity:CGFloat,
options:UIViewAnimationOptions,
animations:()->Void,
completion:((Bool)->Void)?)
Usethismethodtohavethetwolabelsanimateonandoffthescreeninaspring-likefashion.RefertotheUIViewdocumentationtounderstandeachofthearguments.
SilverChallenge:LayoutGuides
Ifyourotateintolandscape,thenextQuestionLabelbecomesvisible.Insteadofhardcodingthespacingconstraint’sconstant,useaninstanceofUILayoutGuidetospacethetwolabelsapart.ThislayoutguideshouldhaveawidthconstraintequaltotheViewController’sviewtoensurethatthenextQuestionLabelremainsoffscreenwhennotanimating.
9Debugging
Asyouwriteanapplication,youwillinevitablymakemistakes.Evenworse,fromtimetotimeyouwillhaveerrorsinyourapplication’sdesign.Xcode’sdebugger(calledLLDB)isthefundamentaltoolthatwillhelpyoufindthesebugsandfixthem.ThischaptergivesyouanoverviewofXcode’sdebuggeranditsbasicfunctions.
ABuggyProject
YouwilluseasimpleprojecttoguideyouthroughyourexplorationoftheXcodedebugger.OpenXcodeandcreateanewprojectforaniOSsingleviewapplication.NametheprojectBuggyandmakesureLanguageissettoSwift,DevicesissettoiPhone,andUseCoreData,IncludeUnitTests,andIncludeUITestsareallunchecked(Figure9.1).ClickNext.Figure9.1ConfiguringBuggy
Asyouwritethisapplication’scode,keepinmindthatitisabuggyproject.Youmaybeaskedtotypecodeyouknowisincorrect.Donotfixitasyoutypeitin;thoseerrorswillhelpyoulearnaboutdebuggingtechniques.Togetstarted,openMain.storyboardanddragaUIButtonontotheViewControllerScene.Double-clickonthenewbuttonandchangeitstitleto“Tapme!”Withthebuttonstillselected,opentheAutoLayoutAlignmenu.CheckHorizontallyinContainerandclickAdd1Constraint.Next,opentheAddNewConstraintsmenu.Pinthedistancetothetopofthecontainer,checktheWidthandHeightboxes,andclickAdd3Constraints.YourresultsshouldlooksomethinglikeFigure9.2,butdonotworryifyouractualdimensionsandspacingareabitdifferent.
Figure9.2AutoLayoutconstraintsfortheTapme!button
Nowyouneedtoimplementamethodforthisbuttontotriggerandthenconnectittothebuttoninthestoryboard.OpenViewController.swiftandimplementanactionmethodforthebuttontotrigger.@IBActionfuncbuttonTapped(_sender:UIButton){
}
NowopenMain.storyboardandControl-dragfromthebuttontotheViewControllerandconnectittothebuttonTapped:option.BackinViewController.swift,addaprint()statementtothebuttonTapped(_:)methodtoconfirmthatthemethodiscalledinresponsetoabuttontap.@IBActionfuncbuttonTapped(_sender:UIButton){
print("CalledbuttonTapped(_:)")
}
Buildandruntheapplication.Makesurethebuttoniscorrectlydisplayedonthescreenandthatyoucantapit.AlsoconfirmthattheCalledbuttonTapped(_:)messageprintstotheconsolewhenyoutapthebutton.
DebuggingBasics
Thesimplestdebuggingusestheconsole.Interpretingtheinformationprovidedintheconsolewhenanapplicationcrashesorintentionallylogginginformationtotheconsoleallowsyoutoobserveandzeroinonyourcode’sfailures.Let’slookatsomeexamplesofhowtheconsolecansupportyourquestforbug-freecode.
Interpretingconsolemessages
TimetoaddsomemayhemtotheBuggyproject.SupposethatafterconsideringtheUIforawhile,youdecidethataswitchwouldbeabettercontrolthanabutton.OpenViewController.swiftandmakethefollowingchangestothebuttonTapped(_:)method.@IBActionfuncbuttonTapped(_sender:UIButton){
@IBActionfuncswitchToggled(_sender:UISwitch){
print("CalledbuttonTapped(_:)")
}
YourenamedtheactiontoreflectthechangeofcontrolandyouchangedthetypeofsendertoUISwitch.Unfortunately,youforgottoupdatetheinterfaceinMain.storyboard.Buildandruntheapplication,thentaponthebutton.Theapplicationwillcrashandyouwillseeamessageloggedtotheconsolesimilartotheoneonthenextpage.(Wehavetruncatedsomeoftheinformationtofitonthepage.)
2016-08-2412:52:38.463Buggy[1961:47078]-[Buggy.ViewControllerbuttonTapped:]:
unrecognizedselectorsenttoinstance0x7ff6db708870
2016-08-2412:52:38.470Buggy[1961:47078]***Terminatingappduetouncaught
exception'NSInvalidArgumentException',
reason:'-[Buggy.ViewControllerbuttonTapped:]:unrecognizedselectorsentto
instance0x7ff6db708870'
***Firstthrowcallstack:
(
0CoreFoundation[...]__exceptionPreprocess+171
1libobjc.A.dylib[...]objc_exception_throw+48
2CoreFoundation[...]-[NSObject(NSObject)doesNotRecognizeSelector:]+132
3CoreFoundation[...]___forwarding___+1013
4CoreFoundation[...]_CF_forwarding_prep_0+120
5UIKit[...]-[UIApplicationsendAction:to:from:forEvent:]+83
6UIKit[...]-[UIControlsendAction:to:forEvent:]+67
7UIKit[...]-[UIControl_sendActionsForEvents:withEvent:]+444
8UIKit[...]-[UIControltouchesEnded:withEvent:]+668
9UIKit[...]-[UIWindow_sendTouchesForEvent:]+2747
10UIKit[...]-[UIWindowsendEvent:]+4011
11UIKit[...]-[UIApplicationsendEvent:]+371
12UIKit[...]__dispatchPreprocessedEventFromEventQueue+3248
13UIKit[...]__handleEventQueue+4879
14CoreFoundation[...]__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION
15CoreFoundation[...]__CFRunLoopDoSources0+556
16CoreFoundation[...]__CFRunLoopRun+918
17CoreFoundation[...]CFRunLoopRunSpecific+420
18GraphicsServices[...]GSEventRunModal+161
19UIKit[...]UIApplicationMain+159
20Buggy[...]main+111
21libdyld.dylib[...]start+1
)
libc++abi.dylib:terminatingwithuncaughtexceptionoftypeNSException
Themessageintheconsolelooksprettyscaryandhardtounderstand,butitisnotasbadasitfirstseems.Thereallyusefulinformationisattheverytop.Let’sstartwiththeveryfirstline.2016-08-2412:52:38.463Buggy[1961:47078]-[Buggy.ViewControllerbuttonTapped:]:
unrecognizedselectorsenttoinstance0x7ff6db708870
Thereisatimestamp,thenameoftheapplication,andthestatementunrecognizedselectorsenttoinstance0x7ff6db708870.Tomakesenseofthisinformation,rememberthataniOSapplicationmaybewritteninSwift,butitisstillbuiltontopofCocoaTouch,whichisacollectionofframeworkswritteninObjective-C.Objective-Cisadynamiclanguage,andwhenamessageissenttoaninstance,theObjective-Cruntimefindstheactualmethodtobecalledatthatprecisetimebasedonitsselector,akindofID.Thus,thestatementthatanunrecognizedselector[was]senttoinstance0x7ff6db708870meansthattheapplicationtriedtocallamethodonaninstancethatdidnothaveit.Whichinstancewasit?Youhavetwopiecesofinformationaboutit.First,itisaBuggy.ViewController.(WhynotjustViewController?Swiftnamespacesincludethenameofthemodule,whichinthiscaseistheapplication’sname.)Second,itislocatedatmemoryaddress0x7ff6db708870(youractualaddresswilllikelybedifferent).Theexpression-[Buggy.ViewControllerbuttonTapped:]isarepresentationofObjective-Ccode.AmessageinObjective-Cisalwaysenclosedinsquarebracketsintheform[receiver
selector].Thereceiveristheclassorinstancetowhichthemessageissent.Thedash(-)beforetheopeningsquarebracketindicatesthatthereceiverisaninstanceofViewController.(Aplussign(+)wouldindicatethatthereceiverwastheclassitself.)
Inshort,thislinefromtheconsoletellsyouthattheselectorbuttonTapped:wassenttoaninstanceofBuggy.ViewControllerbutitwasnotrecognized.Thenextlineofthemessageaddstheinformationthattheappwasterminatedduetoan“uncaughtexception”andspecifiesthetypeoftheexceptionasNSInvalidArgumentException.Thebulkoftheconsolemessageisthestacktrace,alistofallthefunctionsormethodsthatwerecalleduptothepointoftheapplicationcrash.Knowingwhichlogicalpaththeapplicationtookbeforecrashingcanhelpyoureproduceandfixabug.Noneofthecallsinthestacktracehadachancetoreturn,andtheyarelistedwiththemostrecentcallontop.Hereisthestacktraceagain:***Firstthrowcallstack:
(
0CoreFoundation[...]__exceptionPreprocess+171
1libobjc.A.dylib[...]objc_exception_throw+48
2CoreFoundation[...]-[NSObject(NSObject)doesNotRecognizeSelector:]+132
3CoreFoundation[...]___forwarding___+1013
4CoreFoundation[...]_CF_forwarding_prep_0+120
5UIKit[...]-[UIApplicationsendAction:to:from:forEvent:]+83
6UIKit[...]-[UIControlsendAction:to:forEvent:]+67
7UIKit[...]-[UIControl_sendActionsForEvents:withEvent:]+444
8UIKit[...]-[UIControltouchesEnded:withEvent:]+668
9UIKit[...]-[UIWindow_sendTouchesForEvent:]+2747
10UIKit[...]-[UIWindowsendEvent:]+4011
11UIKit[...]-[UIApplicationsendEvent:]+371
12UIKit[...]__dispatchPreprocessedEventFromEventQueue+3248
13UIKit[...]__handleEventQueue+4879
14CoreFoundation[...]__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION
15CoreFoundation[...]__CFRunLoopDoSources0+556
16CoreFoundation[...]__CFRunLoopRun+918
17CoreFoundation[...]CFRunLoopRunSpecific+420
18GraphicsServices[...]GSEventRunModal+161
19UIKit[...]UIApplicationMain+159
20Buggy[...]main+111
21libdyld.dylib[...]start+1
)
Eachrowinthelistincludesacallnumber,themodulename,amemoryaddress(whichwehaveremovedtofittherestonthepage),andasymbolrepresentingthefunctionormethod.Ifyouscanthestacktracefromthebottomup,youcangetasensethattheapplicationstartsinthemainfunctionofBuggyatthelineidentifiedwithcallnumber20,receivesaneventrecognizedasatouchatcallnumber9,andthentriestosendthecorrespondingactiontothebutton’stargetatcallnumber7.Theselectorfortheactionisnotfound(callnumber2:-[NSObject(NSObject)doesNotRecognizeSelector:]),resultinginanexceptionbeingraised(callnumber1:objc_exception_throw).Althoughthisbreakdownoftheconsolemessageisspecifictooneerrortypeoutofmanypossibilities,understandingthebasicstructureofthesemessageswillhelpyoumakesenseof
theerrormessagesyouwillencounterinthefuture.Asyougainmoreexperience,youwillstartassociatingerrormessageswithtypesofproblemsandyouwillbecomebetteratdebuggingcode.
Fixingthefirstbug
ReviewingViewController.swift,youdiscoverthatyouchangedyouractionmethodfrombuttonTapped(_:)toswitchToggled(_:),whichiswhytheselectorbuttonTapped:isnotbeingrecognized.Tofixthebug,youhavetwochoices.YoucouldupdatetheactionconnectedtothebuttononMain.storyboardtomatchyournewactionmethod.OryoucouldrevertthenamechangeontheswitchToggled(_:)method.Youdecidethatyoudonotwantaswitchafterall,soopenViewController.swiftandchangeyourmethodbacktoitsearlierimplementation.(Rememberwhatwetoldyou:Makethechangesexactlyasshown,evenifyouseeaproblem.)@IBActionfuncswitchToggled(_sender:UISwitch){
@IBActionfuncbuttonTapped(_sender:UISwitch){
print("CalledbuttonTapped(_:)")
}
Buildandruntheapplication.Itworksfine…ordoesit?Actually,thereisaproblem,whichyouwillresolveinthenextsection.
Cavemandebugging
ThecurrentimplementationofViewController’sbuttonTapped(_:)methodjustlogsastatementtotheconsole.Thisisanexampleofatechniquethatisfondlycalledcavemandebugging:strategicallyplacingprint()callsinyourcodetoverifythatfunctionsandmethodsarebeingcalled(andcalledinthepropersequence)andtologvariablevaluestotheconsoletokeepaneyeonimportantdata.Likethecavemenintheinsurancecommercials,cavemandebuggingisnotasoutmodedasthenamemightsuggest,andmoderndeveloperscontinuetorelyonmessagesloggedtotheconsole.Toexplorewhatcavemandebuggingcandoforyou,logthestateofthesendercontrolwhenbuttonTapped(_:)iscalledinViewController.swift.@IBActionfuncbuttonTapped(_sender:UISwitch){
print("CalledbuttonTapped(_:)")
//Logthecontrolstate:
print("Iscontrolon?\(sender.isOn)")
}
Inthe@IBActionmethodsyouhavewrittenthroughoutthisbook,youhavebeenpassinginanargumentcalledsender.Thisargumentisareferencetothecontrolsendingthemessage.A
controlisasubclassofUIControl;youhaveworkedwithafewUIControlsubclassessofar,includingUIButton,UITextField,andUISegmentedControl.AsyoucanseeinbuttonTapped(_:)’ssignature,thesenderinthiscaseisaninstanceofaUISwitch.TheisOnpropertyisaBooleanindicatingwhethertheswitchinstanceisintheonstateornot.Buildandruntheapplication.Trytappingthebutton.Oops!Youhaveanunrecognizedselectorerroragain.CalledbuttonTapped(_:)
2016-08-3009:30:57.730Buggy[9738:1177400]-[UIButtonisOn]:
unrecognizedselectorsenttoinstance0x7fcc5d104cd0
2016-08-3009:30:57.734Buggy[9738:1177400]***Terminatingappduetouncaught
exception'NSInvalidArgumentException',reason:'-[UIButtonisOn]:unrecognized
selectorsenttoinstance0x7fcc5d104cd0'
TheconsolemessagebeginswiththeCalledbuttonTapped(_:)line,indicatingthattheactionwasindeedcalled.ButthentheapplicationcrashesbecausetheisOnselectorissenttoaninstanceofaUIButton.Youcanprobablyseetheproblem:senderistypedasaUISwitchinbuttonTapped(_:),buttheactionisactuallyattachedtoaUIButtoninstanceinMain.storyboard.Toconfirmthishypothesis,logtheaddressofsenderinViewController.swift,justbeforeyoucalltheisOnproperty.@IBActionfuncbuttonTaped(_sender:UISwitch){
print("CalledbuttonTapped(_:)")
//Logsender:
print("sender:\(sender)")
//Logthecontrolstate:
print("Iscontrolon?\(sender.isOn)")
}
Buildandruntheapplicationonemoretime.Aftertappingthebuttonandcrashingtheapplication,checkthefirstfewlinesoftheconsolelog,whichwilllooksomethinglikethis:CalledbuttonTapped(_:)
sender:<UIButton:0x7fcf8c508bb0;frame=(16084;5530);opaque=NO;
autoresize=RM+BM;layer=<CALayer:0x618000220ea0>>
2016-08-3009:45:00.562Buggy[9946:1187061]-[UIButtonisOn]:unrecognizedselector
senttoinstance0x7fcf8c508bb0
2016-08-3009:45:00.567Buggy[9946:1187061]***Terminatingappduetouncaught
exception'NSInvalidArgumentException',reason:'-[UIButtonisOn]:unrecognized
selectorsenttoinstance0x7fcf8c508bb0'
InthelineafterCalledbuttonTapped(_:),yougetinformationaboutthesender.Asexpected,itisaninstanceofaUIButtonanditexistsinmemoryataddress0x7fcf8c508bb0.FurtherdownthelogyoucanconfirmthatthisisthesameinstancetowhichyouaresendingtheisOnmessage.AbuttoncannotrespondtoaUISwitchproperty,sotheappcrashes.Tofixthisproblem,correctthebuttonTapped(_:)definitioninViewController.swift.Whileyouarethere,deletetheextracallstoprint(),whichyouwillnotneedagain.@IBActionfuncbuttonTaped(_sender:UISwitch){
@IBActionfuncbuttonTaped(_sender:UIButton){
print("CalledbuttonTapped(_:)")
//Logsender:
print("sender:\(sender)")
//Logthecontrolstate:
print("Iscontrolon?\(sender.isOn)")
}
Swifthasfourliteralexpressionsthatcanassistyouinlogginginformationtotheconsole(Table9.1):Table9.1Literalexpressionsusefulfordebugging
Literal Type Value
#file String Thenameofthefilewheretheexpressionappears.
#line Int Thelinenumbertheexpressionappearson.
#column Int Thecolumnnumbertheexpressionbeginsin.
#function String Thenameofthedeclarationtheexpressionappearsin.
Toillustratetheuseoftheseliteralexpressions,updateyourcalltoprint()inthebuttonTapped(_:)methodinViewController.swift.@IBActionfuncbuttonTapped(_sender:UIButton){
print("CalledbuttonTapped(_:)")
print("Method:\(#function)infile:\(#file)line:\(#line)called.")
}
Buildandruntheapplication.Asyoutapthebutton,youwillseeamessageloggedtotheconsolethatisequivalenttotheonebelow.Method:buttonTappedinfile:/Users/juampa/Desktop/Buggy/Buggy/ViewController.swift
atline:13wascalled.
Whilecavemandebuggingisuseful,beawarethatprint()statementsarenotstrippedfromyourcodeasyoubuildyourprojectforrelease.
TheXcodeDebugger:LLDB
Tocontinueyourdebuggingexperiments,youaregoingtoaddanotherbugtoyourapplication.AddthecodebelowtoViewController.swift.NoticethatyouwillbeusinganNSMutableArray,theObjective-CcounterpartofSwift’sArray,tomakethebugalittlehardertofind.@IBActionfuncbuttonTapped(_sender:UIButton){
print("Method:\(#function)infile:\(#file)line:\(#line)called.")
badMethod()
}
funcbadMethod(){
letarray=NSMutableArray()
foriin0..<10{
array.insert(i,at:i)
}
//Goonesteptoofaremptyingthearray(noticetherangechange):
for_in0...10{
array.remove(at:0)
}
}
BuildandruntheapplicationtoconfirmthatataponthebuttonresultsintheapplicationcrashingwithanuncaughtNSRangeExceptionexception.Useyourfreshlyacquiredknowledgetostudyandinterprettheerrormessageasmuchaspossible.IfyouusedaSwiftArraytypetocreatethisbug,Xcodewouldhavebeenabletohighlightthelineofcodethatcausedtheexception.BecauseyouusedanNSMutableArray,thecodethatraisedtheexceptionisdeepwithintheCocoaTouchframework.Frequentlythisisthecasewhendebugging;problemsarenotsoobviousandyouneedtodosomeinvestigativework.
Settingbreakpoints
Assumethatyoudonotknowthedirectcauseofthecrash.Youjustknowithappensafteryoutaptheapplication’sbutton.Areasonablewaytoproceedwouldbetostoptheapplicationafterthebuttonistappedandstepthroughthecodeuntilyougetaclueastotheproblem.OpenViewController.swift.Tostopanapplicationataspecifiedlocationinthecode,yousetabreakpoint.Thesimplestwaytosetabreakpointistoclickontheguttertotheleftoftheeditorpanenexttothelinewhereyouwantexecutiontostop.Tryit:Clicktotheleftoftheline@IBActionfuncbuttonTapped(_sender:UIButton){.Abluemarkerindicatingthenewbreakpointwillappear(Figure9.3).
Figure9.3Settingabreakpoint
Afterabreakpointisset,youcantoggleitbyclickingonthebluemarkerdirectly.Ifyouclickonthemarkeronce,itwillbecomedisabled,indicatedbyapalershadeofblue(Figure9.4).Figure9.4Disablingabreakpoint
Anotherclickre-enablesthebreakpoint.Youcanalsoenable,disable,delete,oreditabreakpointbyControl-clickingonthemarker.Acontextualmenuwillappear,asshowninFigure9.5.Figure9.5Modifyingabreakpoint
SelectingRevealinBreakpointNavigatoropensthebreakpointnavigatorinXcode’sleftpanewithalistofallthebreakpointsinyourapplication(Figure9.6).Youcanalsoopenthebreakpointnavigatorbyclickingitsiconinthenavigatorselector.
Figure9.6Thebreakpointnavigator
Steppingthroughcode
MakesureyourbreakpointonthebuttonTapped(_:)methodissetandactiveafteralltheclickingyoudidintheprevioussection.Runtheapplicationandtaponthebutton.Yourapplicationhitsthebreakpointandstopsexecuting,andXcodetakesyoutothelineofcodethatwouldbeexecutednext,whichishighlightedingreen.Italsoopenssomenewinformationareas(Figure9.7).Figure9.7Xcodestoppedatabreakpoint
Youarefamiliarwiththeconsoleandhavealreadyseenthedebugnavigator.Thenewareasherearethevariablesviewandthedebugbar,whichtogetherwiththeconsolemakeupthedebugarea.(Ifyoucannotseethevariablesview,clickonthe icononthebottom-rightcornerofthedebugarea.)Thevariablesviewcanhelpyoudiscoverthevaluesofvariablesandconstantswithinthescopeofthebreakpoint.However,tryingtofindaparticularvaluecanrequireafairamountofdigging.Initially,allyouwillseelistedinthevariablesviewarethesenderandselfargumentspassedtothebuttonTapped(_:)method.Clickonthedisclosuretriangleforsender,andyouwillseethatitcontainsaUIKit.UIControlproperty.Withinitthereisa_targetActionsarraythatcontainsthebutton’sattachedtarget-actionpairs.Openthe_targetActionsarray,openthefirstitem([0]),andthenselectthe_targetproperty.Tapthespacebarwhile_targetisselected,andaQuickLookwindowwillopen,showingapreviewofthevariable(whichisaninstanceofViewController).TheQuickLookisshowninFigure9.8.Figure9.8Inspectingvariablesinthevariablesview
Inthesamesectionasthe_target,youwillseethe_selector.Nexttoit,youwillsee(SEL)"buttonTapped:".The(SEL)indicatesthatthisisaselector,and"buttonTapped:"isthenameoftheselector.
Inthiscontrivedexample,itdoesnothelpyoumuchtodigtofindthe_targetandthe_action;however,onceyoustartworkingwithlarger,morecomplexapplications,itcanbeespeciallyusefultousethevariablesview.Youdoneedtoknowwhatyouarelookingfor,suchasthe_targetandthe_action–butfindingthevaluethatyouareinterestedincanbeveryhelpfulintrackingdownbugs.Nowitistimetostartadvancingthroughthecode.Youcandothisusingthebuttonsonthedebugbar,showninFigure9.9.Figure9.9Thedebugbar
Theimportantbuttonsinthedebugbarare:Continueprogramexecution( )–resumesnormalexecutionoftheprogramStepover( )–executesasinglelineofcodewithoutenteringanyfunctionormethodcallStepinto( )–executesthenextlineofcode,includingenteringafunctionormethodcallStepout( )–continuesexecutionuntilthecurrentfunctionormethodisexited
Clickthe buttonuntilyouhighlightthebadMethod()line(donotexecutethisline).Notethatyoudonotstepintotheprint()method–becauseitisanApple-writtenmethod,youknowtherewillbenoproblemsthere.WithbadMethod()highlighted,clickthe buttontostepintothebadMethod()method,andcontinuesteppingthroughthecodewith untiltheapplicationcrashes.Itwilltakeyouquiteafewclicks,anditwilllooklikeyouaregoingthroughthesamelinesofcodeoverandover–infact,youare,asthecodeloopsovertheranges.Asyoustepthroughthecode,youcanpausetomouseoveriandarray.removetoseetheirvaluesupdate(Figure9.10).
Figure9.10Examiningthevalueofavariable
Oncetheapplicationcrashes,youhaveconfirmationthatthecrashoccurswithinthebadMethod()method.WiththisknowledgeyoucannowdeleteordisablethebreakpointatthefuncbuttonTapped(_sender:UIButton)line.Todeleteabreakpoint,Control-clickitandselectDeleteBreakpoint.Youcanalsodeleteabreakpointbydraggingthebluemarkeroutofthegutter,asshowninFigure9.11.Figure9.11Draggingamarkertodeletethebreakpoint
Occasionally,youwanttobenotifiedwhenalineofcodeistriggered,butyoudonotneedanyadditionalinformationorfortheapplicationtopausewhenithitsthatline.Toaccomplishthis,youcanaddasoundtoabreakpointandhaveitautomaticallycontinueexecutionafterbeingtriggered.Addanewbreakpointatthearray.insert(i,at:i)lineofthebadMethod()method.ThenControl-clickonthemarkerandselectEditBreakpoint....ClickontheAddActionbuttonandselectSoundfromthepop-upmenu.Finally,checktheboxtoAutomaticallycontinueafterevaluatingactions(Figure9.12).
Figure9.12Enablingspecialactions
Youhaveconfiguredthebreakpointtomakeanalertsoundinsteadofstoppingexecutioneverytimeitisencountered.Runtheapplicationagainandtapthebutton.Youshouldhearasequenceofsounds,andthentheapplicationwillcrash.Itseemstheapplicationissafelycompletingtheforloop,butyouneedtobesure.FindandControl-clickyourbreakpointmarkeragain,selectingEditBreakpoint...asbefore.Intheeditorpop-up,clickthe+totherightofthesoundactiontoaddanewaction.Fromthepop-up,selectLogMessage.IntheTextfield,enterPassnumber%H(%Histhebreakpointhitcount,areferencetothenumberoftimesthebreakpointhasbeenencountered).Finally,makesuretheLogmessagetoconsoleradiobuttonisselected(Figure9.13).
Figure9.13Assigningmultipleactionstoabreakpoint
Runtheapplicationagainandtapthebutton.Youwillhearthesequenceofsoundsagain,andtheapplicationwillcrashasbefore.Butthistime,ifyouwatchtheconsole(orscrollupaftertheapplicationcrashes),youwillseethatthebreakpointwasencountered10times.Thisconfirmsthatyourcodeiscompletingtheloopsafely.Deleteyourcurrentbreakpointandaddanewoneonthelinearray.remove(at:0).Editthebreakpointtologthepassnumberandcontinueautomatically,asbefore(Figure9.14).
Figure9.14Addingaloggingbreakpoint
Runtheapplicationandtapthebutton.Whenitcrashes,scrollupintheconsoleandyouwillseethatthesecondbreakpointwasencountered11times.Thatisonetimetoomany,andyouhaveyoursmokinggun.ItalsoexplainstheNSRangeExceptionloggedontheconsoleastheapplicationcrashes.Carefullyreadthecrashlogontheconsoleagainandmakeasmuchsenseofitaspossible.Beforefixingtheproblem,takethetimetoexploreacouplemoredebuggingstrategies.First,disableordeleteanyremainingbreakpointsintheapplication.Inthesesimpleexamples,youhaveknownjustwheretolooktofindthebuginyourcode,butinreal-worlddevelopmentyouwilloftenhavenoideawhereinyourapplicationabugishiding.Itwouldbeniceifyoucouldtellwhichlineofcodeiscausinganuncaughtexceptionresultinginacrash.Itwouldbenice–andwithanexceptionbreakpoint,youcandojustthat.Openthebreakpointnavigatorandclickonthe+inthelower-leftcornerofthewindow.Fromthecontextualmenu,selectExceptionBreakpoint....Anewexceptionbreakpointiscreatedandapop-upappears.Makesureitcatchesallexceptionsonthrow,asshowninFigure9.15.
Figure9.15Addinganexceptionbreakpoint
Runtheapplicationandtapthebuttononceagain.TheapplicationautomaticallystopsandXcodetakesyoutothelinethatdirectlycausestheexceptiontoberaised.Note,however,thatthereisnoconsolelog.Thatisbecausetheapplicationhasnotcrashedyet.Toseethecrashandreadthecause,clickonthe buttononthedebugbaruntilyouseethecrash.Thisstrategyistheonetobeginwithasyoutackleanewbug.Infact,manyprogrammersalwayskeepanexceptionbreakpointactivewhiledeveloping.Whydidwemakeyouwaitsolongtouseit?Becauseifyouhadstartedwithanexceptionbreakpoint,youwouldnothaveneededtolearnabouttheotherdebuggingstrategies,andtheyhavetheiruses,too.Feelfreetoremovethisbreakpointifyouwouldlike;youwillnotneeditagain.Youaregoingtotryonefinaltechnique:thesymbolicbreakpoint.Thesearebreakpointsspecifiednotbylinenumber,butbythenameofafunctionormethod,referredtoasasymbol.Symbolicbreakpointsaretriggeredwhenthesymboliscalled–whetherthesymbolisinyourcodeorinaframeworkforwhichyouhavenocode.Addanewsymbolicbreakpointinthebreakpointnavigatorbyclickingthe+buttononthelower-leftcornerand,fromthecontextualmenu,selectingSymbolicBreakpoint....Inthepop-up,specify“badMethod”asthesymbol,asshowninFigure9.16.ThismeansthateverytimebadMethod()iscalled,theapplicationwillstop.
Figure9.16Addingasymbolicbreakpoint
Runtheapplicationtotestthebreakpoint.TheapplicationshouldstopatbadMethod()afteryoutaptheTapme!button.Inareal-worldapp,itisrarethatyouwoulduseasymbolicbreakpointonamethodthatyoucreated;youwouldlikelyaddanormalbreakpointliketheonesyousawearlierinthischapter.Symbolicbreakpointsaremostusefultostoponamethodthatyoudidnotwrite,suchasamethodinoneofApple’sframeworks.Forexample,youmightwanttoknowwheneverthemethodloadView()istriggeredforanyviewcontrollerwithintheapplication.Finally,fixthebug.funcbadMethod(){
letarray=NSMutableArray()
foriin0..<10{
array.insert(i,at:i)
}
//Goonesteptoofaremptyingthearray(noticetherangechange):
for_in0...10{
for_in0..<10{
array.remove(at:0)
}
}
TheLLDBconsole
AgreatfeatureofXcode’sLLDBdebuggeristhatithasacommand-lineinterface.Theconsoleareaisnotonlyusedtoreadmessages,butalsocanbeusedtotypeLLDBcommands.Thedebuggercommand-lineinterfaceisactivewheneveryouseetheblue(lldb)promptontheconsole.MakesureyoursymbolicbreakpointonbadMethod()isstillactive,runtheapplication,andtapthebuttontobreakatthatpoint.Lookattheconsoleandyouwillseethe(lldb)prompt(Figure9.17).Clickbesidetheprompt,andyoucantypecommands.
Figure9.17The(lldb)promptontheconsole
OneofthemostusefulLLDBcommandsisprint-object,abbreviatedpo.Thiscommandprintsanicedescriptionofanyinstance.Tryitoutbytypingontheconsole.(lldb)poself
<Buggy.ViewController:0x7fae9852bf20>
TheresponsetothecommandisthatselfisaninstanceofViewController.Nowadvanceonelineofcodewiththecommandstep;thiswillinitializethearrayconstantreference.Printthereference’svaluewithpo.(lldb)step
(lldb)poarray
0elements
Theresponse0elementsisnotveryuseful,asitdoesnotgiveyoualotofinformation.Theprintcommand,abbreviatedp,canbemoreverbose.Tryit.(lldb)parray
(NSMutableArray)$R3=0x00007fae98517c00"0values"{}
Frequently,usingtheconsolewithprintorprint-objecttoexaminevariablesismuchmoreconvenientthanXcode’svariablesviewpane.AnotherusefulLLDBcommandisexpression,abbreviatedexpr.ThiscommandallowsyoutoenterSwiftcodetomodifyvariables.Forexample,addsomedatatothearray,lookatthecontents,andcontinueexecution.(lldb)exprarray.insert(1,at:0)
(lldb)parray
(NSMutableArray)$R5=0x00007fae98517c00"1value"{
[0]=0xb000000000000013Int64(1)
}
(lldb)poarray
▿1element-[0]:1
(lldb)continue
Perhapsmoresurprisingly,youcanalsochangetheUIwithLLDBexpressions.Trychangingthebutton’stintColortored.(lldb)exprself.view.tintColor=UIColor.red
(lldb)continue
TherearemanyLLDBcommands.Tolearnmore,enterthehelpcommandatthe(lldb)prompt.
10UITableViewandUITableViewController
ManyiOSapplicationsshowtheuseralistofitemsandallowtheusertoselect,delete,orreorderitemsonthelist.Whetheranapplicationdisplaysalistofpeopleintheuser ’saddressbookoralistofbest-sellingitemsontheAppStore,itisaUITableViewdoingthework.AUITableViewdisplaysasinglecolumnofdatawithavariablenumberofrows.Figure10.1showssomeexamplesofUITableView.Figure10.1ExamplesofUITableView
BeginningtheHomepwnerApplication
Inthischapter,youaregoingtostartanapplicationcalledHomepwnerthatkeepsaninventoryofallyourpossessions.Inthecaseofafireorothercatastrophe,youwillhavearecordforyourinsurancecompany.(“Homepwner,”bytheway,isnotatypo.Ifyouneedadefinitionfortheword“pwn,”visitwww.wiktionary.org.)Sofar,youriOSprojectshavebeensmall,butHomepwnerwillgrowintoarealisticallycomplexapplicationoverthecourseofeightchapters.Bytheendofthischapter,HomepwnerwillpresentalistofIteminstancesinaUITableView,asshowninFigure10.2.Figure10.2Homepwner:phase1
Togetstarted,openXcodeandcreateanewiOSSingleViewApplicationproject.ConfigureitasshowninFigure10.3.
Figure10.3ConfiguringHomepwner
UITableViewController
AUITableViewisaviewobject.RecallthatintheMVCdesignpattern,whichiOSdevelopersdotheirbesttofollow,eachclassfallsintoexactlyoneofthefollowingcategories:
model:holdsdataandknowsnothingabouttheUIview:isvisibletotheuserandknowsnothingaboutthemodelobjectscontroller:keepstheUIandthemodelobjectsinsyncandcontrolstheflowoftheapplication
Asaviewobject,aUITableViewdoesnothandleapplicationlogicordata.WhenusingaUITableView,youmustconsiderwhatelseisnecessarytogetthetableworkinginyourapplication:
AUITableViewtypicallyneedsaviewcontrollertohandleitsappearanceonthescreen.AUITableViewneedsadatasource.AUITableViewasksitsdatasourceforthenumberofrowstodisplay,thedatatobeshowninthoserows,andothertidbitsthatmakeaUITableViewausefulUI.Withoutadatasource,atableviewisjustanemptycontainer.ThedataSourceforaUITableViewcanbeanytypeofobjectaslongasitconformstotheUITableViewDataSourceprotocol.AUITableViewtypicallyneedsadelegatethatcaninformotherobjectsofeventsinvolvingtheUITableView.ThedelegatecanbeanyobjectaslongasitconformstotheUITableViewDelegateprotocol.
AninstanceoftheclassUITableViewControllercanfillallthreeroles:viewcontroller,datasource,anddelegate.UITableViewControllerisasubclassofUIViewControllerandthereforehasaview.AUITableViewController’sviewisalwaysaninstanceofUITableView,andtheUITableViewControllerhandlesthepreparationandpresentationoftheUITableView.WhenaUITableViewControllercreatesitsview,thedataSourceanddelegatepropertiesoftheUITableViewareautomaticallysettopointattheUITableViewController(Figure10.4).
Figure10.4UITableViewController-UITableViewrelationship
SubclassingUITableViewController
YouaregoingtoimplementasubclassofUITableViewControllerforHomepwner.CreateanewSwiftfilenamedItemsViewController.InItemsViewController.swift,defineaUITableViewControllersubclassnamedItemsViewController.importFoundation
importUIKit
classItemsViewController:UITableViewController{
}
NowopenMain.storyboard.Youwanttheinitialviewcontrollertobeatableviewcontroller.SelecttheexistingViewControlleronthecanvasandpressDelete.ThendragaTableViewControllerfromtheobjectlibraryontothecanvas.WiththeTableViewControllerselected,openitsidentityinspectorandchangetheclasstoItemsViewController.Finally,opentheattributesinspectorforItemsViewControllerandchecktheboxforIsInitialViewController.Buildandrunyourapplication.Youshouldseeanemptytableview,asshowninFigure10.5.AsasubclassofUIViewController,aUITableViewControllerinheritstheviewproperty.Whenthispropertyisaccessedforthefirsttime,theloadView()methodiscalled,whichcreatesandloadsaviewobject.AUITableViewController’sviewisalwaysaninstanceofUITableView,soaskingfortheviewofaUITableViewControllergetsyouabright,shiny,andemptytableview.
Figure10.5EmptyUITableView
YounolongerneedtheViewController.swiftfilethatthetemplatecreatedforyou.SelectthisfileintheprojectnavigatorandpressDelete.
CreatingtheItemClass
Yourtableviewneedssomerowstodisplay.Eachrowinthetableviewwilldisplayanitemwithinformationsuchasaname,serialnumber,andvalueindollars.CreateanewSwiftfilenamedItem.InItem.swift,definetheItemclassandgiveitfourproperties.importFoundation
importUIKit
classItem:NSObject{
varname:String
varvalueInDollars:Int
varserialNumber:String?
letdateCreated:Date
}
IteminheritsfromNSObject.NSObjectisthebaseclassthatmostObjective-Cclassesinheritfrom.AlloftheUIKitclassesthatyouhaveworkedwith–UIView,UITextField,andUIViewController,tonameafew–inheriteitherdirectlyorindirectlyfromNSObject.YourownclasseswilloftenneedtoinheritfromNSObjectwhentheyneedtointerfacewiththeruntimesystem.NoticethatserialNumberisanoptionalString,necessarybecauseanitemmaynothaveaserialnumber.Also,noticethatnoneofthepropertieshaveadefaultvalue.Youwillneedtogivethemvaluesinadesignatedinitializer.
Custominitializers
YoulearnedaboutstructinitializersinChapter2.Initializersonstructsarefairlystraightforwardbecausestructsdonotsupportinheritance.Classes,ontheotherhand,havesomerulesforinitializerstosupportinheritance.Classescanhavetwokindsofinitializers:designatedinitializersandconvenienceinitializers.Adesignatedinitializerisaprimaryinitializerfortheclass.Everyclasshasatleastonedesignatedinitializer.Adesignatedinitializerensuresthatallpropertiesintheclasshaveavalue.Onceitensuresthat,adesignatedinitializercallsadesignatedinitializeronitssuperclass(ifithasone).ImplementanewdesignatedinitializerontheItemclassthatsetstheinitialvaluesforalloftheproperties.importUIKit
classItem:NSObject{
varname:String
varvalueInDollars:Int
varserialNumber:String?
letdateCreated:Date
init(name:String,serialNumber:String?,valueInDollars:Int){
self.name=name
self.valueInDollars=valueInDollars
self.serialNumber=serialNumber
self.dateCreated=Date()
super.init()
}
}
Thisinitializertakesinargumentsforthename,serialNumber,andvalueInDollars.Becausetheargumentnamesandthepropertynamesarethesame,youmustuseselftodistinguishthepropertyfromtheargument.Nowthatyouhaveimplementedyourowncustominitializer,youlosethefreeinitializer–init()–thatclasseshave.Thefreeinitializerisusefulwhenallofyourclass’spropertieshavedefaultvaluesandyoudonotneedtodoadditionalworktocreatethenewinstance.TheItemclassdoesnotsatisfythiscriteria,soyouhavedeclaredacustominitializerfortheclass.Everyclassmusthaveatleastonedesignatedinitializer,butconvenienceinitializersareoptional.Youcanthinkofconvenienceinitializersashelpers.Aconvenienceinitializeralwayscallsanotherinitializeronthesameclass.Convenienceinitializersareindicatedbytheconveniencekeywordbeforetheinitializername.AddaconvenienceinitializertoItemthatcreatesarandomlygenerateditem.convenienceinit(random:Bool=false){
ifrandom{
letadjectives=["Fluffy","Rusty","Shiny"]
letnouns=["Bear","Spork","Mac"]
varidx=arc4random_uniform(UInt32(adjectives.count))
letrandomAdjective=adjectives[Int(idx)]
idx=arc4random_uniform(UInt32(nouns.count))
letrandomNoun=nouns[Int(idx)]
letrandomName="\(randomAdjective)\(randomNoun)"
letrandomValue=Int(arc4random_uniform(100))
letrandomSerialNumber=
UUID().uuidString.components(separatedBy:"-").first!
self.init(name:randomName,
serialNumber:randomSerialNumber,
valueInDollars:randomValue)
}else{
self.init(name:"",serialNumber:nil,valueInDollars:0)
}
}
Ifrandomistrue,theinstanceisconfiguredwitharandomname,serialnumber,andvalue.(Thearc4random_uniformfunctionreturnsarandomvaluebetween0,inclusive,andthevaluepassedinastheargument,exclusive.)Noticethatattheendofbothbranchesoftheconditional,youarecallingthroughtothedesignatedinitializerforItem.Convenienceinitializersmustcallanotherinitializeronthesametype,whereasdesignatedinitializersmust
calladesignatedinitializeronitssuperclass.TheItemclassisreadyforwork.InthenextsectionyouwilldisplayanarrayofIteminstancesinatableview.
UITableView’sDataSource
TheprocessofprovidingrowstoaUITableViewinCocoaTouch(thecollectionofframeworksusedtobuildiOSapps)isdifferentfromthetypicalproceduralprogrammingtask.Inaproceduraldesign,youtellthetableviewwhatitshoulddisplay.InCocoaTouch,thetableviewasksanotherobject–itsdataSource–whatitshoulddisplay.Inthiscase,theItemsViewControlleristhedatasource,soitneedsawaytostoreitemdata.YouaregoingtouseanarraytostoretheIteminstances,butwithatwist.ThearraythatholdstheIteminstanceswillbeabstractedintoanotherobject–anItemStore(Figure10.6).Figure10.6Homepwnerobjectdiagram
Ifanobjectwantstoseealloftheitems,itwillasktheItemStoreforthearraythatcontainsthem.Infuturechapters,thestorewillberesponsibleforperformingoperationsonthearray,likereordering,adding,andremovingitems.Itwillalsoberesponsibleforsavingandloadingtheitemsfromdisk.CreateanewSwiftfilenamedItemStore.InItemStore.swift,definetheItemStoreclassanddeclareapropertytostorethelistofItems.importFoundation
importUIKit
classItemStore{
varallItems=[Item]()
}
ItemStoreisaSwiftbaseclass–itdoesnotinheritfromanyotherclass.UnliketheItemclassthatyoudefinedearlier,ItemStoredoesnotrequireanyofthebehaviorthatNSObjectaffords.TheItemsViewControllerwillcallamethodonItemStorewhenitwantsanewItemtobecreated.TheItemStorewilloblige,creatingtheobjectandaddingittoanarrayofinstancesofItem.InItemStore.swift,implementcreateItem()tocreateandreturnanewItem.@discardableResultfunccreateItem()->Item{
letnewItem=Item(random:true)
allItems.append(newItem)
returnnewItem
}
The@discardableResultannotationmeansthatacallerofthisfunctionisfreetoignoretheresultofcallingthisfunction.Takealookatthefollowingcodelistingthatillustratesthiseffect.//ThisisOK
letnewItem=itemStore.createItem()
//ThisisalsoOK;theresultisnotassignedtoavariable
itemStore.createItem()
Givingthecontrolleraccesstothestore
InItemsViewController.swift,addapropertyforanItemStore.classItemsViewController:UITableViewController{
varitemStore:ItemStore!
}
Now,whereshouldyousetthispropertyontheItemsViewControllerinstance?Whentheapplicationfirstlaunches,theAppDelegate’sapplication(_:didFinishLaunchingWithOptions:)methodiscalled.TheAppDelegateisdeclaredinAppDelegate.swiftand,asthenameimplies,servesasthedelegatefortheapplicationitself.Itisresponsibleforhandlingthechangesinstatethattheapplicationgoesthrough.YouwilllearnmoreabouttheAppDelegateandthestatesthattheapplicationgoesthroughinChapter16.OpenAppDelegate.swift.AccesstheItemsViewController(whichwillbetherootViewControllerofthewindow)andsetitsitemStorepropertytobeanewinstanceofItemStore.
funcapplication(_application:UIApplication,didFinishLaunchingWithOptions
launchOptions:[UIApplicationLaunchOptionsKey:Any]?)->Bool{
//Overridepointforcustomizationafterapplicationlaunch.
//CreateanItemStore
letitemStore=ItemStore()
//AccesstheItemsViewControllerandsetitsitemstore
letitemsController=window!.rootViewControlleras!ItemsViewController
itemsController.itemStore=itemStore
returntrue
}
Finally,inItemStore.swift,implementthedesignatedinitializertoaddfiverandomitems.init(){
for_in0..<5{
createItem()
}
}
Asaquickaside,ifcreateItem()wasnotannotatedwith@discardableResult,thenthecalltothatfunctionwouldhaveneededtolooklike://Callthefunction,butignoretheresult
let_=createItem()
AtthispointyoumaybewonderingwhyitemStorewassetexternallyontheItemsViewController.Whydidn’ttheItemsViewControllerinstanceitselfjustcreateaninstanceofthestore?Thereasonforthisapproachisbasedonafairlycomplextopiccalledthedependencyinversionprinciple.Theessentialgoalofthisprincipleistodecoupleobjectsinanapplicationbyinvertingcertaindependenciesbetweenthem.Thisresultsinmorerobustandmaintainablecode.Thedependencyinversionprinciplestatesthat:1. High-levelobjectsshouldnotdependonlow-levelobjects.Bothshoulddependon
abstractions.2. Abstractionsshouldnotdependondetails.Detailsshoulddependonabstractions.
TheabstractionrequiredbythedependencyinversionprincipleinHomepwneristheconceptofa“store.”Astoreisalower-levelobjectthatretrievesandsavesIteminstancesthroughdetailsthatareonlyknowntothatclass.ItemsViewControllerisahigher-levelobjectthatonlyknowsthatitwillbeprovidedwithautilityobject(thestore)fromwhichitcanobtainalistofIteminstancesandtowhichitcanpassneworupdatedIteminstancestobestoredpersistently.ThisresultsinadecouplingbecauseItemsViewControllerisnotdependentonItemStore.Infact,aslongasthestoreabstractionisrespected,ItemStorecouldbereplacedbyanotherobjectthatfetchesIteminstancesdifferently(suchasbyusingawebservice)withoutanychangestoItemsViewController.Acommonpatternusedwhenimplementingthedependencyinversionprincipleisdependencyinjection.Initssimplestform,higher-levelobjectsdonotassumewhichlower-levelobjectstheyneedtouse.Instead,thosearepassedtothemthroughaninitializerorproperty.Inyour
implementationofItemsViewController,youusedinjectionthroughapropertytogiveitastore.
Implementingdatasourcemethods
Nowthattherearesomeitemsinthestore,youneedtoteachItemsViewControllerhowtoturnthoseitemsintorowsthatitsUITableViewcandisplay.WhenaUITableViewwantstoknowwhattodisplay,itcallsmethodsfromthesetofmethodsdeclaredintheUITableViewDataSourceprotocol.OpenthedocumentationandsearchfortheUITableViewDataSourceprotocolreference.ScrolldowntothesectiontitledConfiguringaTableView(Figure10.7).Figure10.7UITableViewDataSourceprotocoldocumentation
IntheConfiguringaTableViewsection,noticethattwoofthemethodsaremarkedRequired.ForItemsViewControllertoconformtoUITableViewDataSource,itmustimplement
tableView(_:numberOfRowsInSection:)andtableView(_:cellForRowAt:).Thesemethodstellthetableviewhowmanyrowsitshoulddisplayandwhatcontenttodisplayineachrow.WheneveraUITableViewneedstodisplayitself,itcallsaseriesofmethods(therequiredmethodsplusanyoptionalonesthathavebeenimplemented)onitsdataSource.TherequiredmethodtableView(_:numberOfRowsInSection:)returnsanintegervalueforthenumberofrowsthattheUITableViewshoulddisplay.InthetableviewforHomepwner,thereshouldbearowforeachentryinthestore.InItemsViewController.swift,implementtableView(_:numberOfRowsInSection:).overridefunctableView(_tableView:UITableView,
numberOfRowsInSectionsection:Int)->Int{
returnitemStore.allItems.count
}
Wonderingaboutthesectionthatthismethodrefersto?Tableviewscanbebrokenupintosections,witheachsectionhavingitsownsetofrows.Forexample,intheaddressbook,allnamesbeginningwith“C”aregroupedtogetherinasection.Bydefault,atableviewhasonesection,andinthischapteryouwillworkwithonlyone.Onceyouunderstandhowatableviewworks,itisnothardtousemultiplesections.Infact,usingsectionsisthefirstchallengeattheendofthischapter.ThesecondrequiredmethodintheUITableViewDataSourceprotocolistableView(_:cellForRowAt:).Toimplementthismethod,youneedtolearnaboutanotherclass–UITableViewCell.
UITableViewCells
Eachrowofatableviewisaview.TheseviewsareinstancesofUITableViewCell.Inthissection,youwillcreatetheinstancesofUITableViewCelltofillthetableview.Acellitselfhasonesubview–itscontentView(Figure10.8).ThecontentViewisthesuperviewforthecontentofthecell.Thecellmayalsohaveanaccessoryview.Figure10.8UITableViewCelllayout
Theaccessoryviewshowsanaction-orientedicon,suchasacheckmark,adisclosureicon,oraninformationbutton.Theseiconsareaccessedthroughpredefinedconstantsfortheappearanceoftheaccessoryview.ThedefaultisUITableViewCellAccessoryType.none,andthatiswhatyouaregoingtouseinthischapter.YouwillseetheaccessoryviewagaininChapter23.(Curiousnow?SeethedocumentationforUITableViewCellformoredetails.)TherealmeatofaUITableViewCellisthecontentView,whichhasthreesubviewsofitsown(Figure10.9).TwoofthosesubviewsareUILabelinstancesthatarepropertiesofUITableViewCellnamedtextLabelanddetailTextLabel.ThethirdsubviewisaUIImageViewcalledimageView.Inthischapter,youwillusetextLabelanddetailTextLabel.
Figure10.9UITableViewCellhierarchy
EachcellalsohasaUITableViewCellStylethatdetermineswhichsubviewsareusedandtheirpositionwithinthecontentView.ExamplesofthesestylesandtheirconstantsareshowninFigure10.10.Figure10.10UITableViewCellStyle:stylesandconstants
CreatingandretrievingUITableViewCells
Fornow,eachcellwilldisplaythenameofanItemasitstextLabelandthevalueInDollarsoftheItemasitsdetailTextLabel.Tomakethishappen,youneed
toimplementthesecondrequiredmethodfromtheUITableViewDataSourceprotocol,tableView(_:cellForRowAt:).Thismethodwillcreateacell,setitstextLabeltothenameoftheItem,setitsdetailTextLabeltothevalueInDollarsoftheItem,andreturnittotheUITableView(Figure10.11).Figure10.11UITableViewCellretrieval
HowdoyoudecidewhichcellanItemcorrespondsto?OneoftheparameterssenttotableView(_:cellForRowAt:)isanIndexPath,whichhastwoproperties:sectionandrow.Whenthismethodiscalledonadatasource,thetableviewisasking,“CanIhaveacelltodisplayinsectionX,rowY?”Becausethereisonlyonesectioninthisexercise,yourimplementationwillonlybeconcernedwiththeindexpath’srow.InItemsViewController.swift,implementtableView(_:cellForRowAt:)sothatthenthrowdisplaysthenthentryintheallItemsarray.overridefunctableView(_tableView:UITableView,
cellForRowAtindexPath:IndexPath)->UITableViewCell{
//CreateaninstanceofUITableViewCell,withdefaultappearance
letcell=UITableViewCell(style:.value1,reuseIdentifier:"UITableViewCell")
//Setthetextonthecellwiththedescriptionoftheitem
//thatisatthenthindexofitems,wheren=rowthiscell
//willappearinonthetableview
letitem=itemStore.allItems[indexPath.row]
cell.textLabel?.text=item.name
cell.detailTextLabel?.text="$\(item.valueInDollars)"
returncell
}
BuildandruntheapplicationnowandyouwillseeaUITableViewpopulatedwithalistof
randomitems.
ReusingUITableViewCells
iOSdeviceshavealimitedamountofmemory.IfyouweredisplayingalistwiththousandsofentriesinaUITableView,youwouldhavethousandsofinstancesofUITableViewCell.Mostofthesecellswouldtakeupmemoryneedlessly.Afterall,iftheusercannotseeacellonscreen,thenthereisnoreasonforthatcelltohaveaclaimonmemory.Toconservememoryandimproveperformance,youcanreusetableviewcells.Whentheuserscrollsthetable,somecellsmoveoffscreen.Offscreencellsareputintoapoolofcellsavailableforreuse.Then,insteadofcreatingabrandnewcellforeveryrequest,thedatasourcefirstchecksthepool.Ifthereisanunusedcell,thedatasourceconfiguresitwithnewdataandreturnsittothetableview(Figure10.12).Figure10.12ReusableinstancesofUITableViewCell
Thereisoneproblemtobeawareof:SometimesaUITableViewhasdifferenttypesofcells.Occasionally,yousubclassUITableViewCelltocreateaspeciallookorbehavior.However,differentsubclassesfloatingaroundthepoolofreusablecellscreatethepossibilityofgettingbackacellofthewrongtype.Youmustbesureofthetypeofthecellreturnedsothatyoucanbesureofwhatpropertiesandmethodsithas.Notethatyoudonotcareaboutgettinganyspecificcelloutofthepoolbecauseyouaregoingtochangethecellcontentanyway.Whatyouneedisacellofaspecifictype.Thegoodnewsis
thateverycellhasareuseIdentifierpropertyoftypeString.Whenadatasourceasksthetableviewforareusablecell,itpassesastringandsays,“Ineedacellwiththisreuseidentifier.”Byconvention,thereuseidentifieristypicallythenameofthecellclass.Toreusecells,youneedtoregistereitheraprototypecelloraclasswiththetableviewforaspecificreuseidentifier.YouaregoingtoregisterthedefaultUITableViewCellclass.Youtellthetableview,“Hey,anytimeIaskforacellwiththisreuseidentifier,givemebackacellthatisthisspecificclass.”Thetableviewwilleithergiveyouacellfromthereusepoolorinstantiateanewcelliftherearenocellsofthattypeinthereusepool.OpenMain.storyboard.NoticeinthetableviewthatthereisasectionforPrototypeCells(Figure10.13).Figure10.13Prototypecells
Inthisarea,youcanconfigurethedifferentkindsofcellsthatyouneedfortheassociatedtableview.Ifyouarecreatingcustomcells,thisiswhereyouwillsetuptheinterfaceforthecells.ItemsViewControlleronlyneedsonekindofcell,andusingoneofthebuilt-instyleswillworkgreatfornow,soyouwillonlyneedtoconfiguresomeattributesonthecellthatisalreadyonthecanvas.Selecttheprototypecellandopenitsattributesinspector.ChangetheStyletoRightDetail(whichcorrespondstoUITableViewCellStyle.value1)andgiveitanIdentifierofUITableViewCell(Figure10.14).
Figure10.14Tableviewcellattributes
Next,inItemsViewController.swift,updatetableView(_:cellForRowAt:)toreusecells.overridefunctableView(_tableView:UITableView,
cellForRowAtindexPath:IndexPath)->UITableViewCell{
//CreateaninstanceofUITableViewCell,withdefaultappearance
letcell=UITableViewCell(style:.value1,reuseIdentifier:"UITableViewCell")
//Getaneworrecycledcell
letcell=tableView.dequeueReusableCell(withIdentifier:"UITableViewCell",
for:indexPath)
...
}
ThemethoddequeueReusableCell(withIdentifier:for:)willcheckthepool,orqueue,ofcellstoseewhetheracellwiththecorrectreuseidentifieralreadyexists.Ifso,itwill“dequeue”thatcell.Ifthereisnotanexistingcell,anewcellwillbecreatedandreturned.Buildandruntheapplication.Thebehavioroftheapplicationshouldremainthesame.Reusingcellsmeansthatyouonlyhavetocreateahandfulofcells,whichputsfewerdemandsonmemory.Yourapplication’susers(andtheirdevices)willthankyou.
ContentInsets
Asyouhavebeenrunningtheapplicationthroughoutthischapter,youmighthavenoticedthatthefirsttableviewcellunderlapsthestatusbar(Figure10.15).Theinterfacesfortheapplicationsyoucreatefilluptheentirewindowofthedevice.Thestatusbar,ifvisible,isplacedontopoftheinterface,soyourinterfacesmustaccountfortheplacementofthestatusbar.Figure10.15Tableviewcellunderlappingstatusbar
Tohavethetableviewcellsnotunderlapthestatusbar,youwilladdsomepaddingtothetopofthetableview.AUITableViewisasubclassofUIScrollView,fromwhichitinheritsthecontentInsetproperty.Youcanthinkofthecontentinsetaspaddingforallfoursidesofthescrollview.InItemsViewController.swift,overrideviewDidLoad()toupdatethetableviewcontentinset.overridefuncviewDidLoad(){
super.viewDidLoad()
//Gettheheightofthestatusbar
letstatusBarHeight=UIApplication.shared.statusBarFrame.height
letinsets=UIEdgeInsets(top:statusBarHeight,left:0,bottom:0,right:0)
tableView.contentInset=insets
tableView.scrollIndicatorInsets=insets
}
Thetopofthetableviewisgivenacontentinsetequaltotheheightofthestatusbar.Thiswillmakethecontentappearbelowthestatusbarwhenthetableviewisscrolledtothetop.Thescrollindicatorswillalsounderlapthestatusbar,soyougivethemthesameinsetstohavethemappearjustbelowthestatusbar.NoticethatyouaccessthetableViewpropertyontheItemsViewControllertogetatthetableview.ThispropertyisinheritedfromUITableViewControllerandreturnsthecontroller ’stableview.WhileyoucangetthesameobjectbyaccessingtheviewofaUITableViewController,usingtableViewtellsthecompilerthatthereturnedobject
willbeaninstanceofUITableView.Thus,callingamethodoraccessingapropertythatisspecifictoUITableViewwillnotgenerateanerror.
Buildandruntheapplication.Thetableviewcellcontentnolongerunderlapsthestatusbarwhenthetableviewisscrolledtothetop(Figure10.16).Figure10.16Tableviewwithadjustedcontentinset
BronzeChallenge:Sections
HavetheUITableViewdisplaytwosections–oneforitemsworthmorethan$50andonefortherest.Beforeyoustartthischallenge,copythefoldercontainingtheprojectandallofitssourcefilesinFinder.Thentacklethechallengeinthecopiedproject;youwillneedtheoriginaltobuildoninthefollowingchapters.
SilverChallenge:ConstantRows
MakeitsothelastrowoftheUITableViewalwayshasthetext“Nomoreitems!”Makesurethisrowappearsregardlessofthenumberofitemsinthestore(including0items).
GoldChallenge:CustomizingtheTable
Makeeachrow’sheight60points,exceptforthelastrowfromthesilverchallenge,whichshouldremain44points.Then,changethefontsizeofeveryrowexceptthelastto20points.Finally,makethebackgroundoftheUITableViewdisplayanimage.(Tomakethispixel-perfect,youwillneedanimageofthecorrectsizedependingonyourdevice.RefertothechartinChapter1.)
11EditingUITableView
Inthelastchapter,youcreatedanapplicationthatdisplaysalistofIteminstancesinaUITableView.Thenextstepisallowingtheusertointeractwiththetable–toadd,delete,andmoverows.Figure11.1showswhatHomepwnerwilllooklikebytheendofthischapter.Figure11.1Homepwnerineditingmode
EditingMode
UITableViewhasaneditingproperty,andwhenthispropertyissettotrue,theUITableViewenterseditingmode.Oncethetableviewisineditingmode,therowsofthetablecanbemanipulatedbytheuser.Dependingonhowthetableviewisconfigured,theusercanchangetheorderoftherows,addrows,orremoverows.(Editingmodedoesnotallowtheusertoeditthecontentofarow.)Butfirst,theuserneedsawaytoputtheUITableViewineditingmode.Fornow,youaregoingtoincludeabuttonintheheaderviewofthetable.Aheaderviewappearsatthetopofatableandisusefulforaddingsection-wideortable-widetitlesandcontrols.ItcanbeanyUIViewinstance.Notethatthetableviewusestheword“header”intwodifferentways:Therecanbeatableheaderandsectionheaders.Likewise,therecanbeatablefooterandsectionfooters(Figure11.2).Figure11.2Headersandfooters
Youarecreatingatableheaderview.ItwillhavetwosubviewsthatareinstancesofUIButton:onetotoggleeditingmodeandtheothertoaddanewItemtothetable.Youcouldcreatethisviewprogrammatically,butinthiscaseyouwillcreatetheviewanditssubviewsinthestoryboardfile.First,let’ssetupthenecessarycode.ReopenHomepwner.xcodeproj.InItemsViewController.swift,stubouttwomethodsintheimplementation.classItemsViewController:UITableViewController{
varitemStore:ItemStore!
@IBActionfuncaddNewItem(_sender:UIButton){
}
@IBActionfunctoggleEditingMode(_sender:UIButton){
}
NowopenMain.storyboard.Fromtheobjectlibrary,dragaViewtotheverytopofthetableview,abovetheprototypecell.Thiswilladdtheviewasaheaderviewforthetableview.Resizetheheightofthisviewtobeabout60points.(Youcanusethesizeinspectorifyouwanttomakeitexact.)NowdragtwoButtonsfromtheobjectlibrarytotheheaderview.ChangetheirtextandpositionthemasshowninFigure11.3.Youdonotneedtobeexact–youwilladdconstraintssoontopositionthebuttons.Figure11.3Addingbuttonstotheheaderview
SelectbothofthebuttonsandopentheAutoLayoutAlignmenu.SelectVerticallyinContainerwithaconstantof0.MakesureUpdateFramesissettoNone,andthenclickAdd2Constraints(Figure11.4).Figure11.4Alignmenuconstraints
OpentheAddNewConstraintsmenuandconfigureitasshowninFigure11.5.Makesurethevaluesfortheleadingandtrailingconstraintssaveafteryouhavetypedthem;sometimesthe
valuesdonotsave,soitcanbeabittricky.Whenyouhavedonethat,clickAdd4Constraints.Figure11.5Addingnewconstraints
Finally,connecttheactionsforthetwobuttonsasshowninFigure11.6.
Figure11.6Connectingthetwoactions
Buildandruntheapplicationtoseetheinterface.Nowlet’simplementthetoggleEditingMode(_:)method.YoucouldtoggletheeditingpropertyofUITableViewdirectly.However,UIViewControlleralsohasaneditingproperty.AUITableViewControllerinstanceautomaticallysetstheeditingpropertyofitstableviewtomatchitsowneditingproperty.Bysettingtheeditingpropertyontheviewcontrolleritself,itcanensurethatotheraspectsoftheinterfacealsoenterandleaveeditingmode.YouwillseeanexampleofthisinChapter14withUIViewController’seditButtonItem.TosettheisEditingpropertyforaviewcontroller,youcallthemethodsetEditing(_:animated:).InItemsViewController.swift,implementtoggleEditingMode(_:).@IBActionfunctoggleEditingMode(_sender:UIButton){
//Ifyouarecurrentlyineditingmode...
ifisEditing{
//Changetextofbuttontoinformuserofstate
sender.setTitle("Edit",for:.normal)
//Turnoffeditingmode
setEditing(false,animated:true)
}else{
//Changetextofbuttontoinformuserofstate
sender.setTitle("Done",for:.normal)
//Entereditingmode
setEditing(true,animated:true)
}
}
Buildandrunyourapplication.TaptheEditbuttonandtheUITableViewwillentereditingmode(Figure11.7).
Figure11.7UITableViewineditingmode
AddingRows
Therearetwocommoninterfacesforaddingrowstoatableviewatruntime.Abuttonabovethecellsofthetableview:usuallyforaddingarecordforwhichthereisadetailview.Forexample,intheContactsapp,youtapabuttonwhenyoumeetanewpersonandwanttotakedownhisorherinformation.Acellwithagreenplussign:usuallyforaddinganewfieldtoarecord,suchaswhenyouwanttoaddabirthdaytoaperson’srecordintheContactsapp.Ineditingmode,youtapthegreenplussignnextto“addbirthday.”
Inthisexercise,youwillusethefirstoptionandcreateaNewbuttonintheheaderview.Whenthisbuttonistapped,anewrowwillbeaddedtotheUITableView.InItemsViewController.swift,implementaddNewItem(_:).@IBActionfuncaddNewItem(_sender:UIButton){
//Makeanewindexpathforthe0thsection,lastrow
letlastRow=tableView.numberOfRows(inSection:0)
letindexPath=IndexPath(row:lastRow,section:0)
//Insertthisnewrowintothetable
tableView.insertRows(at:[indexPath],with:.automatic)
}
Buildandruntheapplication.TaptheAddbuttonand…theapplicationcrashes.Theconsoletellsyouthatthetableviewhasaninternalinconsistencyexception.Rememberthat,ultimately,itisthedataSourceoftheUITableViewthatdeterminesthenumberofrowsthetableviewshoulddisplay.Afterinsertinganewrow,thetableviewhassixrows(theoriginalfiveplusthenewone).WhentheUITableViewasksitsdataSourceforthenumberofrows,theItemsViewControllerconsultsthestoreandreturnsthatthereshouldbefiverows.TheUITableViewcannotresolvethisinconsistencyandthrowsanexception.YoumustmakesurethattheUITableViewanditsdataSourceagreeonthenumberofrowsbyaddinganewItemtotheItemStorebeforeinsertingthenewrow.InItemsViewController.swift,updateaddNewItem(_:).@IBActionfuncaddNewItem(_sender:UIButton){
//Makeanewindexpathforthe0thsection,lastrow
letlastRow=tableView.numberOfRows(inSection:0)
letindexPath=IndexPath(row:lastRow,section:0)
//Insertthisnewrowintothetable
tableView.insertRows(at:[indexPath],with:.automatic)
//Createanewitemandaddittothestore
letnewItem=itemStore.createItem()
//Figureoutwherethatitemisinthearray
ifletindex=itemStore.allItems.index(of:newItem){
letindexPath=IndexPath(row:index,section:0)
//Insertthisnewrowintothetable
tableView.insertRows(at:[indexPath],with:.automatic)
}
}
Buildandruntheapplication.TaptheAddbutton,andthenewrowwillslideintothebottompositionofthetable.Rememberthattheroleofaviewobjectistopresentmodelobjectstotheuser;updatingviewswithoutupdatingthemodelobjectsisnotveryuseful.Nowthatyouhavetheabilitytoaddrowsanditems,younolongerneedthecodethatputsfiverandomitemsintothestore.OpenItemStore.swiftandremovetheinitializercode.init(){
for_in0..<5{
createItem()
}
}
Buildandruntheapplication.Therewillnolongerbeanyrowswhenyoufirstlaunchtheapplication,butyoucanaddsomebytappingtheAddbutton.
DeletingRows
Ineditingmode,theredcircleswiththeminussign(showninFigure11.7)aredeletioncontrols,andtappingoneshoulddeletethatrow.However,atthispoint,youcannotactuallydeletetherow.(Tryitandsee.)Beforethetableviewwilldeletearow,itcallsamethodonitsdatasourceabouttheproposeddeletionandwaitsforconfirmation.Whendeletingacell,youmustdotwothings:removetherowfromtheUITableViewandremovetheItemassociatedwithitfromtheItemStore.Topullthisoff,theItemStoremustknowhowtoremoveobjectsfromitself.InItemStore.swift,implementanewmethodtoremoveaspecificitem.funcremoveItem(_item:Item){
ifletindex=allItems.index(of:item){
allItems.remove(at:index)
}
}
NowyouwillimplementtableView(_:commit:forRow:),amethodfromtheUITableViewDataSourceprotocol.(ThismethodiscalledontheItemsViewController.KeepinmindthatwhiletheItemStoreiswherethedataiskept,theItemsViewControlleristhetableview’sdataSource.)WhentableView(_:commit:forRowAt:)iscalledonthedatasource,twoextraargumentsarepassedalongwithit.ThefirstistheUITableViewCellEditingStyle,which,inthiscase,is.delete.TheotherargumentistheIndexPathoftherowinthetable.InItemsViewController.swift,implementthismethodtohavetheItemStoreremovetherightobjectandconfirmtherowdeletionbycallingthemethoddeleteRows(at:with:)onthetableview.overridefunctableView(_tableView:UITableView,
commiteditingStyle:UITableViewCellEditingStyle,
forRowAtindexPath:IndexPath){
//Ifthetableviewisaskingtocommitadeletecommand...
ifeditingStyle==.delete{
letitem=itemStore.allItems[indexPath.row]
//Removetheitemfromthestore
itemStore.removeItem(item)
//Alsoremovethatrowfromthetableviewwithananimation
tableView.deleteRows(at:[indexPath],with:.automatic)
}
}
Buildandrunyourapplication,createsomerows,andthendeletearow.Itwilldisappear.Noticethatswipe-to-deleteworksalso.
MovingRows
TochangetheorderofrowsinaUITableView,youwilluseanothermethodfromtheUITableViewDataSourceprotocol–tableView(_:moveRowAt:to:).Todeletearow,youhadtocallthemethoddeleteRows(at:with:)ontheUITableViewtoconfirmthedeletion.Movingarow,however,doesnotrequireconfirmation:ThetableviewmovestherowonitsownauthorityandreportsthemovetoitsdatasourcebycallingthemethodtableView(_:moveRowAt:to:).Youimplementthismethodtoupdateyourdatasourcetomatchtheneworder.Butbeforeyoucanimplementthismethod,youneedtogivetheItemStoreamethodtochangetheorderofitemsinitsallItemsarray.InItemStore.swift,implementthisnewmethod.funcmoveItem(fromfromIndex:Int,totoIndex:Int){
iffromIndex==toIndex{
return
}
//Getreferencetoobjectbeingmovedsoyoucanreinsertit
letmovedItem=allItems[fromIndex]
//Removeitemfromarray
allItems.remove(at:fromIndex)
//Insertiteminarrayatnewlocation
allItems.insert(movedItem,at:toIndex)
}
InItemsViewController.swift,implementtableView(_:moveRowAt:to:)toupdatethestore.overridefunctableView(_tableView:UITableView,
moveRowAtsourceIndexPath:IndexPath,
todestinationIndexPath:IndexPath){
//Updatethemodel
itemStore.moveItem(from:sourceIndexPath.row,to:destinationIndexPath.row)
}
Buildandrunyourapplication.Addafewitems,thentapEditandcheckoutthenewreorderingcontrols(thethreehorizontallines)onthesideofeachrow.Touchandholdareorderingcontrolandmovetherowtoanewposition(Figure11.8).
Figure11.8Movingarow
NotethatsimplyimplementingtableView(_:moveRowAt:to:)causedthereorderingcontrolstoappear.TheUITableViewcanaskitsdatasourceatruntimewhetheritimplementstableView(_:moveRowAt:to:).Ifitdoes,thenthetableviewaddsthereorderingcontrolswheneverthetableviewenterseditingmode.
DisplayingUserAlerts
Inthissection,youaregoingtolearnaboutuseralertsandthedifferentwaysofconfiguringanddisplayingthem.Useralertscanprovideyourapplicationwithabetteruserexperience,soyouwillusethemfairlyoften.Alertsareoftenusedtowarnusersthatanimportantactionisabouttohappenandperhapsgivethemtheopportunitytocancelthataction.Whenyouwanttodisplayanalert,youcreateaninstanceofUIAlertControllerwithapreferredstyle.ThetwoavailablestylesareUIAlertControllerStyle.actionSheetandUIAlertControllerStyle.alert(Figure11.9).Figure11.9UIAlertControllerstyles
The.actionSheetstyleisusedtopresenttheuserwithalistofactionsfromwhichtochoose.The.alerttypeisusedtodisplaycriticalinformationtorequiretheusertodecidehowtoproceed.Thedistinctionmayseemsubtle,butiftheusercanbackoutofadecisionoriftheactionisnotcritical,thenan.actionSheetisprobablythebestchoice.
YouaregoingtouseaUIAlertControllertoconfirmthedeletionofitems.Youwillusethe.actionSheetstylebecausethepurposeofthealertistoconfirmorcancelapossiblydestructiveaction.OpenItemsViewController.swiftandmodifytableView(_:commit:forRowAt:)toasktheusertoconfirmorcancelthedeletionofanitem.overridefunctableView(_tableView:UITableView,
commiteditingStyle:UITableViewCellEditingStyle,
forRowAtindexPath:IndexPath){
//Ifthetableviewisaskingtocommitadeletecommand...
ifeditingStyle==.delete{
letitem=itemStore.allItems[indexPath.row]
lettitle="Delete\(item.name)?"
letmessage="Areyousureyouwanttodeletethisitem?"
letac=UIAlertController(title:title,
message:message,
preferredStyle:.actionSheet)
//Removetheitemfromthestore
itemStore.removeItem(item)
//Alsoremovethatrowfromthetableviewwithananimation
tableView.deleteRows(at:[indexPath],with:.automatic)
}
}
Afterdeterminingthattheuserwantstodeleteanitem,youcreateaninstanceofUIAlertControllerwithanappropriatetitleandmessagedescribingwhatactionisabouttotakeplace.Also,youspecifythe.actionSheetstyleforthealert.TheactionsthattheusercanchoosefromwhenshownanalertareinstancesofUIAlertAction,andyoucanaddmultipleonesregardlessofthealert’sstyle.ActionsareaddedtotheUIAlertControllerusingtheaddAction(_:)method.AddthenecessaryactionstotheactionsheetintableView(_:commit:forRowAt:)....
letac=UIAlertController(title:title,
message:message,
preferredStyle:.actionSheet)
letcancelAction=UIAlertAction(title:"Cancel",style:.cancel,handler:nil)
ac.addAction(cancelAction)
letdeleteAction=UIAlertAction(title:"Delete",style:.destructive,
handler:{(action)->Voidin
//Removetheitemfromthestore
self.itemStore.removeItem(item)
//Alsoremovethatrowfromthetableviewwithananimation
self.tableView.deleteRows(at:[indexPath],with:.automatic)
})
ac.addAction(deleteAction)
...
Thefirstactionhasatitleof“Cancel”andiscreatedusingthe.cancelstyle.The.cancelstyleresultsintextinastandardbluefont.ThisactionwillallowtheusertobackoutofdeletinganItem.Thehandlerparameterallowsaclosuretobeexecutedwhenthatactionoccurs.Becausenootheractionisneeded,nilispassedastheargument.Thesecondactionhasatitleof“Delete”andiscreatedusingthe.destructivestyle.Becausedestructiveactionsshouldbeclearlymarkedandnoticed,the.destructivestyleresultsinbrightredtext.Iftheuserselectsthisaction,thentheitemandthetableviewcellneedtoberemoved.Thisisalldonewithinthehandlerclosurethatispassedtotheaction’sinitializer.Nowthattheactionshavebeenadded,thealertcontrollercanbedisplayedtotheuser.BecauseUIAlertControllerisasubclassofUIViewController,youcanpresentittotheusermodally.Amodalviewcontrollertakesovertheentirescreenuntilithasfinisheditswork.Topresentaviewcontrollermodally,youcallpresent(_:animated:completion:)ontheviewcontrollerwhoseviewisonthescreen.Theviewcontrollertobepresentedispassedtoit,andthisviewcontroller ’sviewtakesoverthescreen....
letdeleteAction=UIAlertAction(title:"Delete",style:.destructive,
handler:{(action)->Voidin
//Removetheitemfromthestore
self.itemStore.removeItem(item)
//Alsoremovethatrowfromthetableviewwithananimation
self.tableView.deleteRows(at:[indexPath],with:.automatic)
})
ac.addAction(deleteAction)
//Presentthealertcontroller
present(ac,animated:true,completion:nil)
...
Buildandruntheapplicationanddeleteanitem.Anactionsheetwillbepresentedforyoutoconfirmthedeletion(Figure11.10).
Figure11.10Deletinganitem
DesignPatterns
Adesignpatternsolvesacommonsoftwareengineeringproblem.Designpatternsarenotactualsnippetsofcode,butinsteadareabstractideasorapproachesthatyoucanuseinyourapplications.Gooddesignpatternsarevaluableandpowerfultoolsforanydeveloper.Theconsistentuseofdesignpatternsthroughoutthedevelopmentprocessreducesthementaloverheadinsolvingaproblemsoyoucancreatecomplexapplicationsmoreeasilyandrapidly.Herearesomeofthedesignpatternsthatyouhavealreadyused:
Delegation:Oneobjectdelegatescertainresponsibilitiestoanotherobject.YouuseddelegationwiththeUITextFieldtobeinformedwhenthecontentsofthetextfieldchange.Datasource:Adatasourceissimilartoadelegate,butinsteadofreactingtoanotherobject,adatasourceisresponsibleforprovidingdatatoanotherobjectwhenrequested.Youusedthedatasourcepatternwithtableviews:Eachtableviewhasadatasourcethatisresponsiblefor,ataminimum,tellingthetableviewhowmanyrowstodisplayandwhichcellitshoulddisplayateachindexpath.Model-View-Controller:Eachobjectinyourapplicationsfulfillsoneofthreeroles.Modelobjectsarethedata.ViewsdisplaytheUI.Controllersprovidethegluethattiesthemodelsandviewstogether.Target-actionpairs:Oneobjectcallsamethodonanotherobjectwhenaspecificeventoccurs.Thetargetistheobjectthathasamethodcalledonit,andtheactionisthemethodbeingcalled.Forexample,youusedtarget-actionpairswithbuttons:Whenatoucheventoccurs,amethodwillbecalledonanotherobject(oftenaviewcontroller).
Appleisveryconsistentinitsuseofthesedesignpatterns,andsoitisimportanttounderstandandrecognizethem.Keepaneyeoutforthesepatternsasyoucontinuethroughthisbook!Recognizingthemwillhelpyoulearnnewclassesandframeworksmuchmoreeasily.
BronzeChallenge:RenamingtheDeleteButton
Whendeletingarow,aconfirmationbuttonappearslabeledDelete.ChangethelabelofthisbuttontoRemove.
SilverChallenge:PreventingReordering
Makeitsothetableviewalwaysshowsafinalrowthatsays“Nomoreitems!”(Thispartofthechallengeisthesameasachallengefromthelastchapter.Ifyouhavealreadydoneit,youcancopyyourcodefrombefore.)Now,makeitsothatthefinalrowcannotbemoved.
GoldChallenge:ReallyPreventingReordering
Aftercompletingthesilverchallenge,youmaynoticethateventhoughyoucannotmovetheNomoreitems!rowitself,youcanstilldragotherrowsunderneathit.Makeitsothat–nomatterwhat–theNomoreitems!rowcanneverbeknockedoutofthelastposition.Finally,makeitundeletable.
12SubclassingUITableViewCell
AUITableViewdisplaysalistofUITableViewCellobjects.Formanyapplications,thebasiccellwithitstextLabel,detailTextLabel,andimageViewissufficient.However,whenyouneedacellwithmoredetailoradifferentlayout,yousubclassUITableViewCell.Inthischapter,youwillcreateasubclassofUITableViewCellnamedItemCellthatwilldisplayIteminstancesmoreeffectively.EachofthesecellswillshowanItem’sname,itsvalueindollars,anditsserialnumber(Figure12.1).Figure12.1Homepwnerwithsubclassedtableviewcells
YoucustomizetheappearanceofUITableViewCellsubclassesbyaddingsubviewstoitscontentView.AddingsubviewstothecontentViewinsteadofdirectlytothecellitselfisimportantbecausethecellwillresizeitscontentViewatcertaintimes.Forexample,whenatableviewenterseditingmode,thecontentViewresizesitselftomakeroomfortheeditingcontrols(Figure12.2).IfyouaddedsubviewsdirectlytotheUITableViewCell,theeditingcontrolswouldobscurethesubviews.Thecellcannotadjustitssizewhenenteringeditmode(itmustremainthewidthofthetableview),butthecontentViewcananddoes.
Figure12.2Tableviewcelllayoutinstandardandeditingmode
CreatingItemCell
CreateanewSwiftfilenamedItemCell.InItemCell.swift,defineItemCellasaUITableViewCellsubclass.importFoundation
importUIKit
classItemCell:UITableViewCell{
}
TheeasiestwaytoconfigureaUITableViewCellsubclassisthroughastoryboard.InChapter10,yousawthatstoryboardsfortableviewcontrollershaveaPrototypeCellssection.ThisiswhereyouwilllayoutthecontentfortheItemCell.OpenMain.storyboardandselecttheUITableViewCellinthedocumentoutline.Openitsattributesinspector,changetheStyletoCustom,andchangetheIdentifiertoItemCell.Nowopenitsidentityinspector(the tab).IntheClassfield,enterItemCell(Figure12.3).Figure12.3Changingthecellclass
Changetheheightoftheprototypecelltobeabout65pointstall.YoucanchangeiteitheronthecanvasorbyselectingthetableviewcellandchangingtheRowHeightfromitssizeinspector.AnItemCellwilldisplaythreetextelements,sodragthreeUILabelobjectsontothecell.ConfigurethemasshowninFigure12.4.Makethetextofthebottomlabelaslightlysmallerfontinalightshadeofgray.Makesurethatthelabelsdonotoverlapatall.Figure12.4ItemCell’slayout
Addconstraintstothesethreelabelsasfollows.1. Selectthetop-leftlabelandopentheAutoLayoutAddNewConstraintsmenu.Selectthetop
andleftstrutandthenclickAdd2Constraints.2. Youwantthebottom-leftlabeltoalwaysbealignedwiththetop-leftlabel.Control-drag
fromthebottom-leftlabeltothetop-leftlabelandselectLeading.3. Withthebottom-leftlabelstillselected,opentheAddNewConstraintsmenu,selectthe
bottomstrut,andthenclickAdd1Constraint.
4. SelecttherightlabelandControl-dragfromthislabeltoitssuperviewonitsrightside.SelectbothTrailingSpacetoContainerMarginandCenterVerticallyinContainer.
5. Selectthebottom-leftlabelandopenitssizeinspector.FindtheVerticalContentHuggingPriorityandloweritto250.LowertheVerticalContentCompressionResistancePriorityto749.YouwilllearnwhattheseAutoLayoutpropertiesdoinChapter13.
6. Yourframesmightbemisplaced,soselectthethreelabelsandclicktheUpdateFramesbutton.
ExposingthePropertiesofItemCell
ForItemsViewControllertoconfigurethecontentofanItemCellintableView(_:cellForRowAt:),thecellmusthavepropertiesthatexposethethreelabels.ThesepropertieswillbesetthroughoutletconnectionsinMain.storyboard.Thenextstep,then,istocreateandconnectoutletsonItemCellforeachofitssubviews.OpenItemCell.swiftandaddthreepropertiesfortheoutlets.importUIKit
classItemCell:UITableViewCell{
@IBOutletvarnameLabel:UILabel!
@IBOutletvarserialNumberLabel:UILabel!
@IBOutletvarvalueLabel:UILabel!
}
YouaregoingtoconnecttheoutletsforthethreeviewstotheItemCell.Whenconnectingoutletsearlierinthebook,youControl-draggedfromviewcontrollerinthestoryboardtotheappropriateview.ButtheoutletsforItemCellarenotoutletsonacontroller.Theyareoutletsonaview:thecustomUITableViewCellsubclass.Therefore,toconnecttheoutletsforItemCell,youwillconnectthemtotheItemCell.OpenMain.storyboard.Control-clickontheItemCellinthedocumentoutlineandmakethethreeoutletconnectionsshowninFigure12.5.
Figure12.5Connectingtheoutlets
UsingItemCell
Let’sgetyourcustomcellsonscreen.InItemsViewController’stableView(_:cellForRowAt:)method,youwilldequeueaninstanceofItemCellforeveryrowinthetable.NowthatyouareusingacustomUITableViewCellsubclass,thetableviewneedstoknowhowtalleachrowis.Thereareafewwaystoaccomplishthis,butthesimplestwayistosettherowHeightpropertyofthetableviewtoaconstantvalue.Youwillseeanotherwaylaterinthischapter.OpenItemsViewController.swiftandupdateviewDidLoad()tosettheheightofthetableviewcells.overridefuncviewDidLoad(){
super.viewDidLoad()
//Gettheheightofthestatusbar
letstatusBarHeight=UIApplication.shared.statusBarFrame.height
letinsets=UIEdgeInsets(top:statusBarHeight,left:0,bottom:0,right:0)
tableView.contentInset=insets
tableView.scrollIndicatorInsets=insets
tableView.rowHeight=65
}
NowthatyouhaveregisteredtheItemCellwiththetableview(usingtheprototypecellsinthestoryboard),youcanaskthetableviewtodequeueacellwiththeidentifier“ItemCell.”InItemsViewController.swift,modifytableView(_:cellForRowAt:).overridefunctableView(_tableView:UITableView,
cellForRowAtindexPath:NSIndexPath)->UITableViewCell{
//Getaneworrecycledcell
letcell=tableView.dequeueReusableCell(withIdentifier:"UITableViewCell",
for:indexPath)
letcell=tableView.dequeueReusableCell(withIdentifier:"ItemCell",
for:indexPath)as!ItemCell
//Setthetextonthecellwiththedescriptionoftheitem
//thatisatthenthindexofitems,wheren=rowthiscell
//willappearinonthetableview
letitem=itemStore.allItems[indexPath.row]
cell.textLabel?.text=item.name
cell.detailTextLabel?.text="$\(item.valueInDollars)"
//ConfigurethecellwiththeItem
cell.nameLabel.text=item.name
cell.serialNumberLabel.text=item.serialNumber
cell.valueLabel.text="$\(item.valueInDollars)"
returncell
}
First,thereuseidentifierisupdatedtoreflectyournewsubclass.Thecodeattheendofthismethodisfairlyobvious–foreachlabelonthecell,setitstexttosomepropertyfromtheappropriateItem.Buildandruntheapplication.ThenewcellsnowloadwiththeirlabelspopulatedwiththevaluesfromeachItem.
DynamicCellHeights
Currently,thecellshaveafixedheightof65points.Itismuchbettertoallowthecontentofthecelltodriveitsheight.Thatway,ifthecontenteverchanges,thetableviewcell’sheightcanchangeautomatically.Youcanachievethisgoal,asyouhaveprobablyguessed,withAutoLayout.TheUITableViewCellneedstohaveverticalconstraintsthatwillexactlydeterminetheheightofthecell.Currently,ItemCelldoesnothavesufficientconstraintsforthis.Youneedtoaddaconstraintbetweenthetwoleftlabelsthatfixestheverticalspacingbetweenthem.OpenMain.storyboard.Control-dragfromthenameLabeltotheserialNumberLabelandselectVerticalSpacing.NowopenItemsViewController.swiftandupdateviewDidLoad()totellthetableviewthatitshouldcomputethecellheightsbasedontheconstraints.overridefuncviewDidLoad(){
super.viewDidLoad()
//Gettheheightofthestatusbar
letstatusBarHeight=UIApplication.shared.statusBarFrame.height
letinsets=UIEdgeInsets(top:statusBarHeight,left:0,bottom:0,right:0)
tableView.contentInset=insets
tableView.scrollIndicatorInsets=insets
tableView.rowHeight=65
tableView.rowHeight=UITableViewAutomaticDimension
tableView.estimatedRowHeight=65
}
UITableViewAutomaticDimensionisthedefaultvalueforrowHeight,sowhileitisnotnecessarytoadd,itisusefulforunderstandingwhatisgoingon.SettingtheestimatedRowHeightpropertyonthetableviewcanimproveperformance.Insteadofaskingeachcellforitsheightwhenthetableviewloads,settingthispropertyallowssomeofthatperformancecosttobedeferreduntiltheuserstartsscrolling.Buildandruntheapplication.Theapplicationwilllookthesameasitdidbefore.Inthenextsection,youwilllearnaboutatechnologycalledDynamicTypethatwilltakeadvantageoftheautomaticallyresizingtableviewcells.
DynamicType
Creatinganinterfacethatappealstoeveryonecanbedaunting.Somepeopleprefermorecompactinterfacessotheycanseemoreinformationatonce.Othersmightwanttobeabletoeasilyseeinformationataglance,orperhapstheyhavepooreyesight.Inshort:Peoplehavedifferentneeds.Gooddevelopersstrivetomakeappsthatmeetthoseneeds.DynamicTypeisatechnologythathelpsrealizethisgoalbyprovidingspecificallydesignedtextstylesthatareoptimizedforlegibility.UserscanselectoneofsevenpreferredtextsizesfromwithinApple’sSettingsapplication(plusafewadditionallargersizesfromwithintheAccessibilitysection),andappsthatsupportDynamicTypewillhavetheirfontsscaledappropriately.Inthissection,youwillupdateItemCelltosupportDynamicType.Figure12.6showstheapplicationrenderedatthesmallestandlargestuser-selectableDynamicTypesizes.Figure12.6ItemCellwithDynamicTypesupported
TheDynamicTypesystemiscenteredaroundtextstyles.Whenafontisrequestedforagiventextstyle,thesystemwillconsidertheuser ’spreferredtextsizeinassociationwiththetextstyletoreturnanappropriatelyconfiguredfont.Figure12.7showsthedifferenttextstyles.
Figure12.7Textstyles
OpenMain.storyboard.Let’supdatethelabelstousethetextstylesinsteadoffixedfonts.SelectthenameLabelandvalueLabelandopentheattributesinspector.ClickonthetexticontotherightofFont.ForFont,chooseTextStyles-Body(Figure12.8).RepeatthesamestepsfortheserialNumberLabel,choosingtheCaption1textstyle.
Figure12.8Changingthetextstyle
Nowlet’schangethepreferredfontsize.YoudothisthroughtheSettingsapplication.Buildandruntheapplication.Fromthesimulator ’sHardwaremenu,selectHome.Next,onthesimulator ’sHomescreen,opentheSettingsapplication.ChooseGeneral,thenAccessibility,andthenLargerText.(Onanactualdevice,thismenucanalsobeaccessedinSettingsviaDisplay&Brightness→TextSize.)Dragthesliderallthewaytothelefttosetthefontsizetothesmallestvalue(Figure12.9).
Figure12.9Textsizesettings
Buildandruntheapplication.(Ifyouswitchbacktotheapplication,eitherusingthetaskswitcherorthroughtheHomescreen,youwillnotseethechanges.Youwillfixthatinthenextsection.)Addsomeitemstothetableviewandyouwillseethenewsmallerfontsizesinaction.
Respondingtouserchanges
Whentheuserchangesthepreferredtextsizeandreturnstotheapplication,thetableviewwillgetreloaded.Unfortunately,thelabelswillnotknowaboutthenewpreferredtextsize.Tofixthis,youneedtohavethelabelsautomaticallyadjusttocontentsizechanges.OpenItemCell.swiftandoverrideawakeFromNib()tohavethelabelsautomaticallyadjust.
overridefuncawakeFromNib(){
super.awakeFromNib()
nameLabel.adjustsFontForContentSizeCategory=true
serialNumberLabel.adjustsFontForContentSizeCategory=true
valueLabel.adjustsFontForContentSizeCategory=true
}
ThemethodawakeFromNib()getscalledonanobjectafteritisloadedfromanarchive,whichinthiscaseisthestoryboardfile.Bythetimethismethodiscalled,alloftheoutletshavevaluesandcanbeused.Buildandruntheapplicationandaddafewrows.GointoSettingsandchangethepreferredreadingsizetothelargestsize.Unlikebefore,youcannowswitchbacktoHomepwner,eitherbyopeningthetaskswitcherorthroughtheHomescreen,andthetableviewwillupdatetoreflectthenewpreferredtextsize.
BronzeChallenge:CellColors
UpdatetheItemCelltodisplaythevalueInDollarsingreenifthevalueislessthan50andredifthevalueisgreaterthanorequalto50.
13StackViews
YouhavebeenusingAutoLayoutthroughoutthisbooktocreateflexibleinterfacesthatscaleacrossdevicetypesandsizes.AutoLayoutisaverypowerfultechnology,butwiththatpowercomescomplexity.Layingoutaninterfacewelloftenneedsalotofconstraints,anditcanbedifficulttocreatedynamicinterfacesduetotheneedtoconstantlyaddandremoveconstraints.Often,aninterface(orasubsectionoftheinterface)canbelaidoutinalinearfashion.Thinkabouttheotherapplicationsyouwrote:TheQuizapplicationfromChapter1consistedoffoursubviewsthatwerelaidoutvertically.ThesameistruefortheWorldTrotterapplication;theConversionViewControllerhadaverticalinterfaceconsistingofatextfieldandafewlabels.Interfacesthathavealinearlayoutaregreatcandidatesforusingastackview.AstackviewisaninstanceofUIStackViewthatallowsyoutocreateaverticalorhorizontallayoutthatiseasytolayoutandmanagesmostoftheconstraintsthatyouwouldtypicallyhavetomanageyourself.Perhapsbestofall,youareabletoneststackviewswithinotherstackviews,whichallowsyoutocreatetrulyamazinginterfacesinafractionofthetime.Inthischapter,youaregoingtocontinueworkingonHomepwnertocreateaninterfacefordisplayingthedetailsofaspecificItem.Theinterfacethatyoucreatewillconsistofmultiplenestedstackviews,bothverticalandhorizontal(Figure13.1).Figure13.1Homepwnerwithstackviews
UsingUIStackView
YouaregoingtocreateaninterfaceforeditingthedetailsofanItem.Youwillgetthebasicinterfaceworkinginthischapter,andthenyouwillfinishimplementingthedetailsinChapter14.Atthetoplevel,youwillhaveaverticalstackviewwithfourelementsdisplayingtheitem’sname,serialnumber,value,anddatecreated(Figure13.2).Figure13.2Verticalstackviewlayout
OpenyourHomepwnerprojectandthenopenMain.storyboard.DraganewViewControllerfromtheobjectlibraryontothecanvas.DragaVerticalStackViewfromtheobjectlibraryontotheviewfortheViewController.Addconstraintstothestackviewtopinittotheleadingandtrailingmargins,andpinthetopandbottomedgestobe8pointsfromthetopandbottomlayoutguides.NowdragfourinstancesofUILabelfromtheobjectlibraryontothestackview.Fromtoptobottom,givetheselabelsthetext“Name,”“Serial,”“Value,”and“DateCreated”(Figure13.3).
Figure13.3Labelsaddedtothestackview
Youcanseeaproblemrightaway:Thelabelsallhavearedborder(indicatinganAutoLayoutproblem)andthereisawarningthatsomeviewsareverticallyambiguous.Therearetwowaysyoucanfixthisissue:byusingAutoLayout,orbyusingapropertyonthestackview.Let’sworkthroughtheAutoLayoutsolutionfirstbecauseithighlightsanimportantaspectofAutoLayout.
Implicitconstraints
YoulearnedinChapter3thateveryviewhasanintrinsiccontentsize.Youalsolearnedthatifyoudonotspecifyconstraintsthatexplicitlydeterminethewidthorheight,theviewwillderiveitswidthorheightfromitsintrinsiccontentsize.Howdoesthiswork?Itdoesthisusingimplicitconstraintsderivedfromaview’scontenthuggingprioritiesanditscontentcompressionresistancepriorities.Aviewhasoneoftheseprioritiesforeachaxis:
horizontalcontenthuggingpriority
verticalcontenthuggingpriorityhorizontalcontentcompressionresistancepriorityverticalcontentcompressionresistancepriority
Contenthuggingpriorities
Thecontenthuggingpriorityislikearubberbandthatisplacedaroundaview.Therubberbandmakestheviewnotwanttobebiggerthanitsintrinsiccontentsizeinthatdimension.Eachpriorityisassociatedwithavaluefrom0to1000.Avalueof1000meansthataviewcannotgetbiggerthanitsintrinsiccontentsizeonthatdimension.Let’slookatanexamplewithjustthehorizontaldimension.Sayyouhavetwolabelsnexttooneanotherwithconstraintsbothbetweenthetwoviewsandbetweeneachviewanditssuperview,asshowninFigure13.4.Figure13.4Twolabelssidebyside
Thisworksgreatuntilthesuperviewbecomeswider.Atthatpoint,whichlabelshouldbecomewider?Thefirstlabel,thesecondlabel,orboth?AsFigure13.5shows,theinterfaceiscurrentlyambiguous.Figure13.5Ambiguouslayout
Thisiswherethecontenthuggingprioritybecomesrelevant.Theviewwiththehighercontenthuggingpriorityistheonethatdoesnotstretch.Youcanthinkaboutthepriorityvalueasthe“strength”oftherubberband.Thehigherthepriorityvalue,thestrongertherubberband,andthemoreitwantstohugtoitsintrinsiccontentsize.
Contentcompressionresistancepriorities
Thecontentcompressionresistanceprioritiesdeterminehowmuchaviewresistsgettingsmallerthanitsintrinsiccontentsize.ConsiderthesametwolabelsfromFigure13.4.Whatwouldhappenifthesuperview’swidthdecreased?Oneofthelabelswouldneedtotruncateitstext(Figure13.6).Butwhichone?Figure13.6Compressedambiguouslayout
Theviewwiththegreatercontentcompressionresistancepriorityistheonethatwillresistcompressionand,therefore,nottruncateitstext.Withthisknowledge,youcannowfixtheproblemwiththestackview.SelecttheDateCreatedlabelandopenitssizeinspector.FindtheVerticalContentHuggingPriorityandloweritto249.Nowtheotherthreelabelshaveahighercontenthuggingpriority,sotheywillallhugtotheirintrinsiccontentheight.TheDateCreatedlabelwillstretchtofillintheremainingspace.
Stackviewdistribution
Let’stakealookatanotherwayofsolvingtheproblem.Stackviewshaveanumberofpropertiesthatdeterminehowtheircontentislaidout.Selectthestackview,eitheronthecanvasorusingthedocumentoutline.OpenitsattributesinspectorandfindthesectionatthetoplabeledStackView.OneofthepropertiesthatdetermineshowthecontentislaidoutistheDistributionproperty.CurrentlyitissettoFill,whichletstheviewslayouttheircontentbasedontheirintrinsiccontentsize.ChangethevaluetoFillEqually.Thiswillresizethelabelssothattheyallhavethesameheight,ignoringtheintrinsiccontentsize(Figure13.7).Besuretoreadthedocumentationfortheotherdistributionvaluesthatastackviewcanhave.
Figure13.7Stackviewsettofillequally
ChangetheDistributionofthestackviewbacktoFill;thisisthevalueyouwillwantgoingforwardinthischapter.
Nestedstackviews
Oneofthemostpowerfulfeaturesofstackviewsisthattheycanbenestedwithinoneanother.Youwillusethistonesthorizontalstackviewswithinthelargerverticalstackview.ThetopthreelabelswillhaveatextfieldnexttothemthatdisplaysthecorrespondingvaluefortheItemandwillalsoallowtheusertoeditthatvalue.SelecttheNamelabelonthecanvas.ClickthesecondiconfromtheleftintheAutoLayoutconstraintsmenu: .Thiswillembedtheselectedviewinastackview.
Selectthenewstackviewandopenitsattributesinspector.Thestackviewiscurrentlyaverticalstackview,butyouwantittobeahorizontalstackview.ChangetheAxistoHorizontal.NowdragaTextFieldfromtheobjectlibrarytotherightoftheNamelabel.Becauselabels,bydefault,haveagreatercontenthuggingprioritythantextfields,thelabelhugstoitsintrinsiccontentwidthandthetextfieldstretches.Thelabelandthetextfieldcurrentlyhavethesamecontentcompressionresistancepriorities,whichwouldresultinanambiguouslayoutifthetextfield’stextwastoolong.OpenthesizeinspectorforthetextfieldandsetitsHorizontalContentCompressionResistancePriorityto749.Thiswillensurethatthetextfield’stextwillbetruncatedifnecessary,ratherthanthelabel.
Stackviewspacing
Thelabelandtextfieldlookalittlesquishedbecausethereisnospacingbetweenthem.Stackviewsallowyoutocustomizethespacingbetweenitems.Selectthehorizontalstackviewandopenitsattributesinspector.ChangetheSpacingtobe8points.Noticethatthetextfieldshrinkstoaccommodatethespacing,becauseitislessresistanttocompressionthanthelabel.RepeatthesestepsfortheSerialandValuelabels:1. Selectthelabelandclickthe icon.2. Changethestackviewtobeahorizontalstackview.3. Dragatextfieldontothehorizontalstackviewandchangeitshorizontalcontent
compressionresistanceprioritytobe749.4. Updatethestackviewtohaveaspacingof8points.
Thereareacoupleofothertweaksyouwillwanttomaketotheinterface:Theverticalstackviewneedssomespacing.TheDateCreatedlabelshouldhaveacentertextalignment.AndtheName,Serial,andValuelabelsshouldbethesamewidth.Selecttheverticalstackview,openitsattributesinspector,andupdatetheSpacingtobe8points.ThenselecttheDateCreatedlabel,openitsattributesinspector,andchangetheAlignmenttobecentered.Thatsolvesthefirsttwoissues.Althoughstackviewssubstantiallyreducethenumberofconstraintsthatyouneedtoaddtoyourinterface,someconstraintsarestillimportant.Withtheinterfaceasis,thetextfieldsdonotalignontheirleadingedgeduetothedifferenceinthewidthsofthelabels.(ThedifferenceisnotverynoticeableinEnglish,butitbecomesmorepronouncedwhenlocalizedintootherlanguages.)Tosolvethis,youwilladdleadingedgeconstraintsbetweenthethreetextfields.Control-dragfromtheNametextfieldtotheSerialtextfieldandselectLeading.ThendothesamefortheSerialtextfieldandtheValuetextfield.ThecompletedinterfacewilllooklikeFigure13.8.
Figure13.8Finalstackviewinterface
Stackviewsallowyoutocreateveryrichinterfacesinafractionofthetimeitwouldtaketoconfigurethemmanuallyusingconstraints.Constraintsarestilladded,buttheyarebeingmanagedbythestackviewitselfinsteadofbyyou.Stackviewsallowyoutohaveverydynamicinterfacesatruntime.YoucanaddandremoveviewsfromstackviewsbyusingaddArrangedSubview(_:),insertArrangedSubview(_:at:),andremoveArrangedSubview(_:).Youcanalsotogglethehiddenpropertyonaviewinastackview.Thestackviewwillautomaticallylayoutitscontenttoreflectthatvalue.
Segues
MostiOSapplicationshaveanumberofviewcontrollersthatusersnavigatebetween.Storyboardsallowyoutosetuptheseinteractionsassegueswithouthavingtowritecode.Aseguemovesanotherviewcontroller ’sviewontothescreenandisrepresentedbyaninstanceofUIStoryboardSegue.Eachseguehasastyle,anactionitem,andanidentifier.Thestyleofaseguedetermineshowtheviewcontrollerwillbepresented.Theactionitemistheviewobjectinthestoryboardfilethattriggersthesegue,likeabutton,atableviewcell,orsomeotherUIControl.Theidentifierisusedtoprogrammaticallyaccessthesegue.Thisisusefulwhenyouwanttotriggeraseguethatdoesnotcomefromanactionitem,likeashakeorsomeotherinterfaceelementthatcannotbesetupinthestoryboardfile.Let’sstartwithashowsegue.Ashowseguedisplaysaviewcontrollerdependingonthecontextinwhichitisdisplayed.Theseguewillbebetweenthetableviewcontrollerandthenewviewcontroller.Theactionitemswillbethetableview’scells;tappingacellwillshowtheviewcontrollermodally.InMain.storyboard,selecttheItemCellprototypecellontheItemsViewController.Control-dragfromthecelltothenewviewcontrollerthatyousetupintheprevioussection.(MakesureyouareControl-draggingfromthecellandnotthetableview!)Ablackpanelwillappearthatliststhepossiblestylesforthissegue.SelectShowfromtheSelectionSeguesection(Figure13.9).Figure13.9Settingupashowsegue
Noticethearrowthatgoesfromthetableviewcontrollertothenewviewcontroller.Thisisasegue.Theiconinthecircletellsyouthatthissegueisashowsegue–eachseguehasauniqueicon.Buildandruntheapplication.Tapacellandthenewviewcontrollerwillslideupfromthe
bottomofthescreen.(Slidingupfromthebottomisthedefaultbehaviorwhenpresentingaviewcontrollermodally.)Sofar,sogood!Buttherearetwoproblemsatthemoment:TheviewcontrollerisnotdisplayingtheinformationfortheItemthatwasselected,andthereisnowaytodismisstheviewcontrollertoreturntotheItemsViewController.Youwillfixthefirstissueinthenextsection,andyouwillfixthesecondissueinChapter14.
HookingUptheContent
TodisplaytheinformationfortheselectedItem,youwillneedtocreateanewUIViewControllersubclass.CreateanewSwiftfileandnameitDetailViewController.OpenDetailViewController.swiftanddeclareanewUIViewControllersubclassnamedDetailViewController.importFoundation
importUIKit
classDetailViewController:UIViewController{
}
Becauseyouneedtobeabletoaccessthesubviewsyoucreatedduringruntime,DetailViewControllerneedsoutletsforthem.TheplanistoaddfournewoutletstoDetailViewControllerandthenmaketheconnections.Inpreviousexercises,youdidthisintwodistinctsteps:First,youaddedtheoutletsintheSwiftfile,thenyoumadeconnectionsinthestoryboardfile.Youcandobothatonceusingtheassistanteditor.WithDetailViewController.swiftopen,Option-clickonMain.storyboardintheprojectnavigator.ThiswillopenthefileintheassistanteditorrightnexttoDetailViewController.swift.(YoucantoggletheassistanteditorbyclickingthemiddlebuttonfromtheEditorcontrolatthetopoftheworkspace.TheshortcuttodisplaytheassistanteditorisCommand-Option-Return,andtheshortcuttoreturntothestandardeditorisCommand-Return.)Yourwindowhasbecomealittlecluttered.Let’smakesometemporaryspace.HidethenavigatorareabyclickingtheleftbuttonintheViewcontrolatthetopoftheworkspace(theshortcutforthisisCommand-0).Then,hidethedocumentoutlineinInterfaceBuilderbyclickingthetogglebuttoninthelower-leftcorneroftheeditor.YourworkspaceshouldnowlooklikeFigure13.10.
Figure13.10Layingouttheworkspace
Beforeyouconnecttheoutlets,youneedtotellthedetailinterfacethatitshouldbeassociatedwiththeDetailViewController.SelecttheViewControlleronthecanvasandopenitsidentityinspector.ChangetheClasstobeDetailViewController(Figure13.11).Figure13.11Settingtheviewcontrollerclass
ThethreeinstancesofUITextFieldandbottominstanceofUILabelwillbeoutletsinDetailViewController.Control-dragfromtheUITextFieldnexttotheNamelabeltothetopofDetailViewController.swift,asshowninFigure13.12.
Figure13.12Draggingfromstoryboardtosourcefile
Letgoandapop-upwindowwillappear.EnternameFieldintotheNamefield,makesuretheStorageissettoStrong,andclickConnect(Figure13.13).Figure13.13Autogeneratinganoutletandmakingaconnection
Thiswillcreatean@IBOutletpropertyoftypeUITextFieldnamednameFieldinDetailViewController.Inaddition,thisUITextFieldisalreadyconnectedtothenameFieldoutletoftheDetailViewController.YoucanverifythisbyControl-clickingontheDetailViewControllertoseetheconnections.AlsonoticethathoveringyourmouseabovethenameFieldconnectioninthepanelthatappearswillrevealtheUITextFieldthatyouconnected.Twobirds,onestone.
CreatetheotherthreeoutletsthesamewayandnamethemasshowninFigure13.14.Figure13.14Connectiondiagram
Afteryoumaketheconnections,DetailViewController.swiftshouldlooklikethis:importUIKit
classDetailViewController:UIViewController{
@IBOutletvarnameField:UITextField!
@IBOutletvarserialNumberField:UITextField!
@IBOutletvarvalueField:UITextField!
@IBOutletvardateLabel:UILabel!
}
Ifyourfilelooksdifferent,thenyouroutletsarenotconnectedcorrectly.Fixanydisparitiesbetweenyourfileandthecodeshownaboveinthreesteps:First,gothroughtheControl-drag
processandmakeconnectionsagainuntilyouhavethefourlinesshownaboveinyourDetailViewController.swift.Second,removeanywrongcode(likenon-propertymethoddeclarationsorproperties)thatgotcreated.Finally,checkforanybadconnectionsinthestoryboardfile–inMain.storyboard,Control-clickontheDetailViewController.Ifthereareyellowwarningsignsnexttoanyconnection,clickthexiconnexttothoseconnectionstodisconnectthem.Itisimportanttoensurethattherearenobadconnectionsinaninterfacefile.Abadconnectiontypicallyhappenswhenyouchangethenameofapropertybutdonotupdatetheconnectionintheinterfacefileorwhenyoucompletelyremoveapropertybutdonotremoveitfromtheinterfacefile.Eitherway,abadconnectionwillcauseyourapplicationtocrashwhentheinterfacefileisloaded.Withtheconnectionsmade,youcanclosetheassistanteditorandreturntoviewingjustDetailViewController.swift.DetailViewControllerwillholdontoareferencetotheItemthatisbeingdisplayed.Whenitsviewisloaded,youwillsetthetextoneachtextfieldtotheappropriatevaluefromtheIteminstance.InDetailViewController.swift,addapropertyforanIteminstanceandoverrideviewWillAppear(_:)tosetuptheinterface.classDetailViewController:UIViewController{
@IBOutletvarnameField:UITextField!
@IBOutletvarserialNumberField:UITextField!
@IBOutletvarvalueField:UITextField!
@IBOutletvardateLabel:UILabel!
varitem:Item!
overridefuncviewWillAppear(_animated:Bool){
super.viewWillAppear(animated)
nameField.text=item.name
serialNumberField.text=item.serialNumber
valueField.text="\(item.valueInDollars)"
dateLabel.text="\(item.dateCreated)"
}
}
Thisworks,butinsteadofusingstringinterpolationtoprintoutthevalueInDollarsanddateCreated,itwouldbebettertouseaformatter.YouusedaninstanceofNumberFormatterinChapter4.Youwilluseanotheronehere,aswellasaninstanceofDateFormattertoformatthedateCreated.AddaninstanceofNumberFormatterandDateFormattertotheDetailViewController.UsetheseformattersinviewWillAppear(_:)toformatthevalueInDollarsanddateCreated.varitem:Item!
letnumberFormatter:NumberFormatter={
letformatter=NumberFormatter()
formatter.numberStyle=.decimal
formatter.minimumFractionDigits=2
formatter.maximumFractionDigits=2
returnformatter
}()
letdateFormatter:DateFormatter={
letformatter=DateFormatter()
formatter.dateStyle=.medium
formatter.timeStyle=.none
returnformatter
}()
overridefuncviewWillAppear(_animated:Bool){
super.viewWillAppear(animated)
nameField.text=item.name
serialNumberField.text=item.serialNumber
valueField.text="\(item.valueInDollars)"
dateLabel.text="\(item.dateCreated)"
valueField.text=
numberFormatter.string(from:NSNumber(value:item.valueInDollars))
dateLabel.text=dateFormatter.string(from:item.dateCreated)
}
PassingDataAround
Whenarowinthetableviewistapped,youneedawayoftellingtheDetailViewControllerwhichitemwasselected.Wheneverasegueistriggered,theprepare(for:sender:)methodiscalledontheviewcontrollerinitiatingthesegue.Thismethodhastwoarguments:theUIStoryboardSegue,whichgivesyouinformationaboutwhichsegueishappening,andthesender,whichistheobjectthattriggeredthesegue(aUITableViewCelloraUIButton,forexample).TheUIStoryboardSeguegivesyouthreepiecesofinformation:thesourceviewcontroller(wherethesegueoriginates),thedestinationviewcontroller(wherethesegueends),andtheidentifierofthesegue.Theidentifierletsyoudifferentiatesegues.Let’sgivethesegueausefulidentifier.OpenMain.storyboardagain.Selecttheshowseguebyclickingonthearrowbetweenthetwoviewcontrollersandopentheattributesinspector.Fortheidentifier,entershowItem(Figure13.15).Figure13.15Segueidentifier
Withyoursegueidentified,youcannowpassyourIteminstancesaround.OpenItemsViewController.swiftandimplementprepare(for:sender:).overridefuncprepare(forsegue:UIStoryboardSegue,sender:Any?){
//Ifthetriggeredsegueisthe"showItem"segue
switchsegue.identifier{
case"showItem"?:
//Figureoutwhichrowwasjusttapped
ifletrow=tableView.indexPathForSelectedRow?.row{
//Gettheitemassociatedwiththisrowandpassitalong
letitem=itemStore.allItems[row]
letdetailViewController
=segue.destinationas!DetailViewController
detailViewController.item=item
}
default:
preconditionFailure("Unexpectedsegueidentifier.")
}
}
YoulearnedaboutswitchstatementsinChapter2.Here,youareusingonetoswitchoverthepossiblesegueidentifiers.Becausethesegue’sidentifierisanoptionalString,youincludea?afterthecasepattern(i.e.,after"showItem").ThedefaultblockusesthepreconditionFailure(_:)functiontocatchanyunexpectedsegueidentifiersandcrashtheapplication.Thiswouldbethecaseiftheprogrammereitherforgottogiveasegueanidentifieroriftherewasatyposomewherewiththesegueidentifiers.Ineithercase,itistheprogrammer’smistake,andusingpreconditionFailure(_:)canhelpyouidentifytheseproblemssooner.Buildandruntheapplication.TaponarowandtheDetailViewControllerwillslideonscreen,displayingthedetailsforthatitem.(YouwillfixtheinabilitytogobacktotheItemsViewControllerinChapter14.)ManyprogrammersnewtoiOSstrugglewithhowdataispassedbetweenviewcontrollers.HavingallofthedataintherootviewcontrollerandpassingsubsetsofthatdatatothenextUIViewController(asyoudidinthischapter)isacleanandefficientwayofperformingthistask.
BronzeChallenge:MoreStackViews
QuizandWorldTrotteraregoodcandidatesforusingstackviews.UpdatebothoftheseapplicationstouseUIStackView.
14UINavigationController
InChapter5,youlearnedaboutUITabBarControllerandhowitallowsausertoaccessdifferentscreens.Atabbarcontrollerisgreatforscreensthatareindependentofeachother,butwhatifyouhavescreensthatproviderelatedinformation?Forexample,theSettingsapplicationhasmultiplerelatedscreensofinformation:alistofsettings(likeSounds),adetailedpageforeachsetting,andaselectionpageforeachdetail(Figure14.1).Thistypeofinterfaceiscalledadrill-downinterface.Figure14.1Drill-downinterfaceinSettings
Inthischapter,youwilluseaUINavigationControllertoaddadrill-downinterfacetoHomepwnerthatletstheuserseeandeditthedetailsofanItem.ThesedetailswillbepresentedbytheDetailViewControllerthatyoucreatedinChapter13(Figure14.2).
Figure14.2HomepwnerwithUINavigationController
UINavigationController
AUINavigationControllermaintainsanarrayofviewcontrollerspresentingrelatedinformationinastack.WhenaUIViewControllerisontopofthestack,itsviewisvisible.WhenyouinitializeaninstanceofUINavigationController,yougiveitaUIViewController.ThisUIViewControllerisaddedtothenavigationcontroller ’sviewControllersarrayandbecomesthenavigationcontroller ’srootviewcontroller.Therootviewcontrollerisalwaysonthebottomofthestack.(Notethatwhilethisviewcontrollerisreferredtoasthenavigationcontroller ’s“rootviewcontroller,”UINavigationControllerdoesnothavearootViewControllerproperty.)MoreviewcontrollerscanbepushedontopoftheUINavigationController’sstackwhiletheapplicationisrunning.TheseviewcontrollersareaddedtotheendoftheviewControllersarraythatcorrespondstothetopofthestack.UINavigationController’stopViewControllerpropertykeepsareferencetotheviewcontrolleratthetopofthestack.Whenaviewcontrollerispushedontothestack,itsviewslidesonscreenfromtheright.Whenthestackispopped(i.e.,thelastitemisremoved),thetopviewcontrollerisremovedfromthestackanditsviewslidesofftotheright,exposingtheviewofthenextviewcontrolleronthestack,whichbecomesthetopviewcontroller.Figure14.3showsanavigationcontrollerwithtwoviewcontrollers.TheviewofthetopViewControlleriswhattheusersees.
Figure14.3UINavigationController’sstack
UINavigationControllerisasubclassofUIViewController,soithasaviewofitsown.Itsviewalwayshastwosubviews:aUINavigationBarandtheviewoftopViewController(Figure14.4).
Figure14.4AUINavigationController’sview
Inthischapter,youwilladdaUINavigationControllertotheHomepwnerapplicationandmaketheItemsViewControllertheUINavigationController’srootviewcontroller.TheDetailViewControllerwillbepushedontotheUINavigationController’sstackwhenanItemisselected.ThisviewcontrollerwillallowtheusertoviewandeditthepropertiesofanItemselectedfromthetableviewofItemsViewController.TheobjectdiagramfortheupdatedHomepwnerapplicationisshowninFigure14.5.
Figure14.5Homepwnerobjectdiagram
Thisapplicationisgettingfairlylarge,asyoucansee.Fortunately,viewcontrollersandUINavigationControllerknowhowtodealwiththistypeofcomplicatedobjectdiagram.WhenwritingiOSapplications,itisimportanttotreateachUIViewControllerasitsownlittleworld.ThestuffthathasalreadybeenimplementedinCocoaTouchwilldotheheavylifting.BeginbygivingHomepwneranavigationcontroller.ReopentheHomepwnerproject.TheonlyrequirementsforusingaUINavigationControllerarethatyougiveitarootviewcontrollerandadditsviewtothewindow.OpenMain.storyboardandselecttheItemsViewController.Then,fromtheEditormenu,chooseEmbedIn→NavigationController.ThiswillsettheItemsViewControllertobetherootviewcontrollerofaUINavigationController.ItwillalsoupdatethestoryboardtosettheNavigationControllerastheinitialviewcontroller.YourDetailViewControllerinterfacemayhavemisplacedviewsnowthatitiscontainedwithinanavigationcontroller.Ifitdoes,selectthestackviewandclicktheUpdateFramesbuttonintheAutoLayoutconstraintmenu.Buildandruntheapplicationand…theapplicationcrashes.Whatishappening?You
previouslycreatedacontractwiththeAppDelegatethataninstanceofItemsViewControllerwouldbetherootViewControllerofthewindow:letitemsController=window!.rootViewControlleras!ItemsViewController
YouhavenowbrokenthiscontractbyembeddingtheItemsViewControllerinaUINavigationController.Youneedtoupdatethecontract.OpenAppDelegate.swiftandupdateapplication(_:didFinishLaunchingWithOptions:)toreflectthenewviewcontrollerhierarchy.funcapplication(_application:UIApplication,didFinishLaunchingWithOptions
launchOptions:[UIApplicationLaunchOptionsKey:Any]?)->Bool{
//Overridepointforcustomizationafterapplicationlaunch.
//CreateanItemStore
letitemStore=ItemStore()
//AccesstheItemsViewControllerandsetitsitemstore
letitemsController=window!.rootViewControlleras!ItemsViewController
letnavController=window!.rootViewControlleras!UINavigationController
letitemsController=navController.topViewControlleras!ItemsViewController
itemsController.itemStore=itemStore
returntrue
}
Buildandruntheapplicationagain.Homepwnerworksagainandhasaverynice,iftotallyempty,UINavigationBaratthetopofthescreen(Figure14.6).Figure14.6Homepwnerwithanemptynavigationbar
NoticehowthescreenadjustedtofitItemsViewController’sviewaswellasthenewnavigationbar.UINavigationControllerdidthisforyou:WhiletheviewoftheItemsViewControlleractuallyunderlapsthenavigationbar,UINavigationControlleraddedpaddingtothetopsothateverythingfitsnicely.Thisisbecausethetoplayoutguidefortheviewcontrollerisadjusted,alongwithanyviewsconstrainedtothetoplayoutguide–likethestackview.
NavigatingwithUINavigationController
Withtheapplicationstillrunning,createanewitemandselectthatrowfromtheUITableView.NotonlyareyoutakentoDetailViewController’sview,butyoualsogetafreeanimationandaBackbuttonintheUINavigationBar.TapthisbuttontogetbacktoItemsViewController.NoticethatyoudidnothavetochangetheshowseguethatyoucreatedinChapter13togetthisbehavior.Asmentionedinthatchapter,theshowseguepresentsthedestinationviewcontrollerinawaythatmakessensegiventhesurroundingcontext.Whenashowsegueistriggeredfromaviewcontrollerembeddedwithinanavigationcontroller,thedestinationviewcontrollerispushedontothenavigationcontroller ’sviewcontrollerstack.BecausetheUINavigationController’sstackisanarray,itwilltakeownershipofanyviewcontrolleraddedtoit.Thus,theDetailViewControllerisownedonlybytheUINavigationControlleraftertheseguefinishes.Whenthestackispopped,theDetailViewControllerisdestroyed.Thenexttimearowistapped,anewinstanceofDetailViewControlleriscreated.Havingaviewcontrollerpushthenextviewcontrollerisacommonpattern.Therootviewcontrollertypicallycreatesthenextviewcontroller,andthenextviewcontrollercreatestheoneafterthat,andsoon.Someapplicationsmayhaveviewcontrollersthatcanpushdifferentviewcontrollersdependingonuserinput.Forexample,thePhotosapppushesavideoviewcontrolleroranimageviewcontrollerontothenavigationstackdependingonwhattypeofmediaisselected.NoticethatthedetailviewforanitemcontainstheinformationfortheselectedItem.However,whileyoucaneditthisdata,theUITableViewwillnotreflectthosechangeswhenyoureturntoit.Tofixthisproblem,youneedtoimplementcodetoupdatethepropertiesoftheItembeingedited.Inthenextsection,youwillseewhentodothis.
AppearingandDisappearingViews
WheneveraUINavigationControllerisabouttoswapviews,itcallstwomethods:viewWillDisappear(_:)andviewWillAppear(_:).TheUIViewControllerthatisabouttobepoppedoffthestackhasviewWillDisappear(_:)called.TheUIViewControllerthatwillthenbeontopofthestackhasviewWillAppear(_:)calledonit.Toholdontochangesinthedata,whenaDetailViewControllerispoppedoffthestackyouwillsetthepropertiesofitsitemtothecontentsofthetextfields.Whenimplementingthesemethodsforviewsappearinganddisappearing,itisimportanttocallthesuperclass’simplementation–itmighthavesomeworktodoandneedstobegiventhechancetodoit.InDetailViewController.swift,implementviewWillDisappear(_:).overridefuncviewWillDisappear(_animated:Bool){
super.viewWillDisappear(animated)
//"Save"changestoitem
item.name=nameField.text??""
item.serialNumber=serialNumberField.text
ifletvalueText=valueField.text,
letvalue=numberFormatter.number(from:valueText){
item.valueInDollars=value.intValue
}else{
item.valueInDollars=0
}
}
NowthevaluesoftheItemwillbeupdatedwhentheusertapstheBackbuttonontheUINavigationBar.WhenItemsViewControllerappearsbackonthescreen,themethodviewWillAppear(_:)iscalled.TakethisopportunitytoreloadtheUITableViewsotheusercanimmediatelyseethechanges.InItemsViewController.swift,overrideviewWillAppear(_:)toreloadthetableview.overridefuncviewWillAppear(_animated:Bool){
super.viewWillAppear(animated)
tableView.reloadData()
}
Buildandrunyourapplicationonceagain.Nowyoucanmovebackandforthbetweentheviewcontrollersthatyoucreatedandchangethedatawithease.
DismissingtheKeyboard
Runtheapplication,addandselectanitem,andtouchthetextfieldwiththeitem’sname.Whenyoutouchthetextfield,akeyboardappearsonscreen(Figure14.7),asyousawinyourWorldTrotterappinChapter4.(Ifyouareusingthesimulatorandthekeyboarddoesnotappear,rememberthatyoucanpressCommand-Ktotogglethedevicekeyboard.)Figure14.7Keyboardappearswhenatextfieldistouched
TheappearanceofthekeyboardinresponsetoatouchisbuiltintotheUITextFieldclassaswellasUITextView,soyoudonothavetodoanythingextraforthekeyboardtoappear.However,attimesyouwillwanttomakesurethekeyboardbehavesasyouwantitto.Forexample,noticethatthekeyboardcoversmorethanathirdofthescreen.Rightnow,itdoesnotobscureanything,butsoonyouwilladdmoredetailsthatextendtothebottomofthescreen,anduserswillwantawaytohidethekeyboardwhenitisnotneeded.Inthissection,youaregoingtogivetheusertwowaystodismissthekeyboard:pressingthekeyboard’s
Returnkey,andtappinganywhereelseonthedetailviewcontroller ’sview.Butfirst,let’slookatthecombinationofeventsthatmaketexteditingpossible.
Eventhandlingbasics
Whenyoutouchaview,aneventiscreated.Thisevent(knownasa“touchevent”)istiedtoaspecificlocationintheviewcontroller ’sview.Thatlocationdetermineswhichviewinthehierarchythetoucheventisdeliveredto.Forexample,whenyoutapaUIButtonwithinitsbounds,itwillreceivethetoucheventandrespondinbutton-likefashion–bycallingtheactionmethodonitstarget.Itisperfectlyreasonabletoexpectthatwhenaviewinyourapplicationistouched,thatviewreceivesatouchevent,anditmaychoosetoreacttothateventorignoreit.However,viewsinyourapplicationcanalsorespondtoeventswithoutbeingtouched.Agoodexampleofthisisashake.Ifyoushakethedevicewithyourapplicationrunning,oneofyourviewsonthescreencanrespond.Butwhichone?Anotherinterestingcaseisrespondingtothekeyboard.DetailViewController’sviewcontainsthreeUITextFields.Whichonewillreceivethetextwhentheusertypes?Forboththeshakeandkeyboardevents,thereisnoeventlocationwithinyourviewhierarchytodeterminewhichviewwillreceivetheevent,soanothermechanismmustbeused.Thismechanismisthefirstresponderstatus.Manyviewsandcontrolscanbeafirstresponderwithinyourviewhierarchy–butonlyoneatatime.Thinkofitasaflagthatcanbepassedamongviews.Whicheverviewholdstheflagwillreceivetheshakeorkeyboardevent.InstancesofUITextFieldandUITextViewhaveanuncommonresponsetotouchevents.Whentouched,atextfieldoratextviewbecomesthefirstresponder,whichinturntriggersthesystemtoputthekeyboardonscreenandsendthekeyboardeventstothetextfieldorview.Thekeyboardandthetextfieldorviewhavenodirectconnection,buttheyworktogetherthroughthefirstresponderstatus.Thisisaneatwaytoensurethatthekeyboardinputisdeliveredtothecorrecttextfield.TheconceptofafirstresponderispartofthebroadertopicofeventhandlinginCocoaTouchprogrammingthatincludestheUIResponderclassandtheresponderchain.YouwilllearnmoreaboutthemwhenyouhandletoucheventsinChapter18,andyoucanalsovisitApple’sEventHandlingGuideforiOSformoreinformation.
DismissingbypressingtheReturnkey
Nowlet’sgetbacktoallowinguserstodismissthekeyboard.Ifyoutouchanothertextfieldintheapplication,thattextfieldwillbecomethefirstresponder,andthekeyboardwillstayonscreen.Thekeyboardwillonlygiveupandgoawaywhennotextfield(ortextview)isthefirstresponder.Todismissthekeyboard,then,youcallresignFirstResponder()onthetextfieldthatisthefirstresponder.
TohavethetextfieldresigninresponsetotheReturnkeybeingpressed,youaregoingtoimplementtheUITextFieldDelegatemethodtextFieldShouldReturn(_:).ThismethodiscalledwhenevertheReturnkeyispressed.First,inDetailViewController.swift,haveDetailViewControllerconformtotheUITextFieldDelegateprotocol.classDetailViewController:UIViewController,UITextFieldDelegate{
Next,implementtextFieldShouldReturn(_:)tocallresignFirstResponder()onthetextfieldthatispassedin.functextFieldShouldReturn(_textField:UITextField)->Bool{
textField.resignFirstResponder()
returntrue
}
Finally,openMain.storyboardandconnectthedelegatepropertyofeachtextfieldtotheDetailViewController(Figure14.8).(Control-dragfromeachUITextFieldtotheDetailViewControllerandchoosedelegate.)Figure14.8Connectingthedelegatepropertyofatextfield
Buildandruntheapplication.TapatextfieldandthenpresstheReturnkeyonthekeyboard.Thekeyboardwilldisappear.Togetthekeyboardback,touchanytextfield.
Dismissingbytappingelsewhere
ItwouldbestylishtoalsodismissthekeyboardiftheusertapsanywhereelseonDetailViewController’sview.Todothis,youaregoingtouseagesturerecognizerwhentheviewistapped,justasyoudidintheWorldTrotterapp.Intheactionmethod,youwillcallresignFirstResponder()onthetextfield.OpenMain.storyboardandfindTapGestureRecognizerintheobjectlibrary.DragthisobjectontothebackgroundviewfortheDetailViewController.Youwillseeareferencetothisgesture
recognizerinthescenedock.Intheprojectnavigator,Option-clickDetailViewController.swifttoopenitintheassistanteditor.Control-dragfromthetapgesturerecognizerinthestoryboardtotheimplementationofDetailViewController.Inthepop-upthatappears,selectActionfromtheConnectionmenu.NametheactionbackgroundTapped.FortheType,chooseUITapGestureRecognizer(Figure14.9).Figure14.9ConfiguringaUITapGestureRecognizeraction
ClickConnectandthestubfortheactionmethodwillappearinDetailViewController.swift.UpdatethemethodtocallendEditing(_:)ontheviewofDetailViewController.@IBActionfuncbackgroundTapped(_sender:UITapGestureRecognizer){
view.endEditing(true)
}
CallingendEditing(_:)isaconvenientwaytodismissthekeyboardwithouthavingtoknow(orcare)whichtextfieldisthefirstresponder.Whentheviewgetsthiscall,itcheckstoseeifanytextfieldinitshierarchyisthefirstresponder.Ifso,thenresignFirstResponder()iscalledonthatparticularview.Buildandrunyourapplication.Taponatextfieldtoshowthekeyboard.Tapontheviewoutsideofatextfieldandthekeyboardwilldisappear.Thereisonefinalcasewhereyouneedtodismissthekeyboard.WhentheusertapstheBackbutton,viewWillDisappear(_:)iscalledontheDetailViewControllerbeforeitispoppedoffthestack,andthekeyboarddisappearsinstantly,withnoanimation.Todismissthekeyboardmoresmoothly,updatetheimplementationofviewWillDisappear(_:)inDetailViewController.swifttocallendEditing(_:).overridefuncviewWillDisappear(_animated:Bool){
super.viewWillDisappear(animated)
//Clearfirstresponder
view.endEditing(true)
//"Save"changestoitem
item.name=nameField.text??""
item.serialNumber=serialNumberField.text
ifletvalueText=valueField.text,
letvalue=numberFormatter.number(from:valueText){
item.valueInDollars=value.integerValue
}else{
item.valueInDollars=0
}
}
UINavigationBar
Inthissection,youaregoingtogivetheUINavigationBaradescriptivetitlefortheUIViewControllerthatiscurrentlyontopoftheUINavigationController’sstack.EveryUIViewControllerhasanavigationItempropertyoftypeUINavigationItem.However,unlikeUINavigationBar,UINavigationItemisnotasubclassofUIView,soitcannotappearonthescreen.Instead,thenavigationitemsuppliesthenavigationbarwiththecontentitneedstodraw.WhenaUIViewControllercomestothetopofaUINavigationController’sstack,theUINavigationBarusestheUIViewController’snavigationItemtoconfigureitself,asshowninFigure14.10.Figure14.10UINavigationItem
Bydefault,aUINavigationItemisempty.Atthemostbasiclevel,aUINavigationItemhasasimpletitlestring.WhenaUIViewControllerismovedtothetopofthenavigationstackanditsnavigationItemhasavalidstringforitstitleproperty,thenavigationbarwilldisplaythatstring(Figure14.11).Figure14.11UINavigationItemwithtitle
ThetitlefortheItemsViewControllerwillalwaysremainthesame,soyoucansetthetitleofitsnavigationitemwithinthestoryboarditself.OpenMain.storyboard.Double-clickonthecenterofthenavigationbarabovetheItemsViewControllertoedititstitle.Giveitatitleof“Homepwner”(Figure14.12).
Figure14.12Settingthetitleinastoryboard
Buildandruntheapplication.NoticethestringHomepwneronthenavigationbar.Createandtaponarowandnoticethatthenavigationbarnolongerhasatitle.ItwouldbenicetohavetheDetailViewController’snavigationitemtitlebethenameoftheItemitisdisplaying.BecausethetitlewilldependontheItemthatisbeingdisplayed,youneedtosetthetitleofthenavigationItemdynamicallyincode.InDetailViewController.swift,addapropertyobservertotheitempropertythatupdatesthetitleofthenavigationItem.varitem:Item!{
didSet{
navigationItem.title=item.name
}
}
Buildandruntheapplication.CreateandtaparowandyouwillseethatthetitleofthenavigationbaristhenameoftheItemyouselected.Anavigationitemcanholdmorethanjustatitlestring,asshowninFigure14.13.TherearethreecustomizableareasforeachUINavigationItem:aleftBarButtonItem,arightBarButtonItem,andatitleView.TheleftandrightbarbuttonitemsarereferencestoinstancesofUIBarButtonItem,whichcontaintheinformationforabuttonthatcanonlybedisplayedonaUINavigationBaroraUIToolbar.Figure14.13UINavigationItemwitheverything
RecallthatUINavigationItemisnotasubclassofUIView.Instead,UINavigationItemencapsulatesinformationthatUINavigationBarusestoconfigureitself.Similarly,UIBarButtonItemisnotaview,butholdstheinformationabouthowasinglebuttonontheUINavigationBarshouldbedisplayed.(AUIToolbaralsouses
instancesofUIBarButtonItemtoconfigureitself.)ThethirdcustomizableareaofaUINavigationItemisitstitleView.YoucaneitheruseabasicstringasthetitleorhaveasubclassofUIViewsitinthecenterofthenavigationitem.Youcannothaveboth.Ifitsuitsthecontextofaspecificviewcontrollertohaveacustomview(likeasegmentedcontroloratextfield,forexample),youwouldsetthetitleViewofthenavigationitemtothatcustomview.Figure14.13showsanexamplefromthebuilt-inMapsapplicationofaUINavigationItemwithacustomviewasitstitleView.Typically,however,atitlestringissufficient.
Addingbuttonstothenavigationbar
Inthissection,youaregoingtoreplacethetwobuttonsthatareinthetable’sheaderviewwithtwobarbuttonitemsthatwillappearintheUINavigationBarwhentheItemsViewControllerisontopofthestack.Abarbuttonitemhasatarget-actionpairthatworkslikeUIControl’starget-actionmechanism:Whentapped,itsendstheactionmessagetothetarget.First,let’sworkonabarbuttonitemforaddingnewitems.ThisbuttonwillsitontherightsideofthenavigationbarwhentheItemsViewControllerisontopofthestack.Whentapped,itwilladdanewItem.Beforeyouupdatethestoryboard,youneedtochangethemethodsignatureforaddNewItem(_:).CurrentlythismethodistriggeredbyaUIButton.NowthatyouarechangingthesendertoaUIBarButtonItem,youneedtoupdatethesignature.InItemsViewController.swift,updatethemethodsignatureforaddNewItem(_:).@IBActionfuncaddNewItem(_sender:UIButton){
@IBActionfuncaddNewItem(_sender:UIBarButtonItem){
...
}
NowopenMain.storyboardandthenopentheobjectlibrary.DragaBarButtonItemtotherightsideofItemsViewController’snavigationbar.Selectthisbarbuttonitemandopenitsattributesinspector.ChangetheSystemItemtoAdd(Figure14.14).
Figure14.14Systembarbuttonitem
Control-dragfromthisbarbuttonitemtotheItemsViewControllerandselectaddNewItem:(Figure14.15).Figure14.15ConnectingtheaddNewItem:action
Buildandruntheapplication.Tapthe+buttonandanewrowwillappearinthetable.Nowlet’sreplacetheEditbutton.Viewcontrollersexposeabarbuttonitemthatwillautomaticallytoggletheireditingmode.ThereisnowaytoaccessthisthroughInterfaceBuilder,soyouwillneedtoaddthisbarbuttonitemprogrammatically.InItemsViewController.swift,overridetheinit(coder:)methodtosettheleftbarbuttonitem.requiredinit?(coderaDecoder:NSCoder){
super.init(coder:aDecoder)
navigationItem.leftBarButtonItem=editButtonItem
}
Buildandruntheapplication,addsomeitems,andtaptheEditbutton.TheUITableViewenterseditingmode!TheeditButtonItempropertycreatesaUIBarButtonItemwiththetitleEdit.Evenbetter,thisbuttoncomeswithatarget-actionpair:ItcallsthemethodsetEditing(_:animated:)onitsUIViewControllerwhentapped.OpenMain.storyboard.NowthatHomepwnerhasafullyfunctionalnavigationbar,youcangetridoftheheaderviewandtheassociatedcode.SelecttheheaderviewonthetableviewandpressDelete.Also,theUINavigationControllerwillhandleupdatingtheinsetsforthetableview.InItemsViewController.swift,deletethefollowingcode.overridefuncviewDidLoad(){
super.viewDidLoad()
//Gettheheightofthestatusbar
letstatusBarHeight=UIApplication.shared.statusBarFrame.height
letinsets=UIEdgeInsets(top:statusBarHeight,left:0,bottom:0,right:0)
tableView.contentInset=insets
tableView.scrollIndicatorInsets=insets
tableView.rowHeight=UITableViewAutomaticDimension
tableView.estimatedRowHeight=65
}
Finally,removethetoggleEditingMode(_:)method.@IBActionfunctoggleEditingMode(_sender:UIButton){
//Ifyouarecurrentlyineditingmode...
ifisEditing{
//Changetextofbuttontoinformuserofstate
sender.setTitle("Edit",for:.normal)
//Turnoffeditingmode
setEditing(false,animated:true)
}else{
//Changetextofbuttontoinformuserofstate
sender.setTitle("Done",for:.normal)
//Entereditingmode
setEditing(true,animated:true)
}
}
Buildandrunagain.TheoldEditandAddbuttonsaregone,leavingyouwithalovelyUINavigationBar(Figure14.16).
Figure14.16Homepwnerwithnavigationbar
BronzeChallenge:DisplayingaNumberPad
ThekeyboardfortheUITextFieldthatdisplaysanItem’svalueInDollarsisaQWERTYkeyboard.Itwouldbebetterifitwereanumberpad.ChangetheKeyboardTypeofthatUITextFieldtotheNumberPad.(Hint:Youcandothisinthestoryboardfileusingtheattributesinspector.)
SilverChallenge:ACustomUITextField
MakeasubclassofUITextFieldandoverridethebecomeFirstResponder()andresignFirstResponder()methods(inheritedfromUIResponder)sothatitsborderstylechangeswhenitisthefirstresponder.YoucanusetheborderStylepropertyofUITextFieldtoaccomplishthis.UseyoursubclassforthetextfieldsinDetailViewController.
GoldChallenge:PushingMoreViewControllers
Currently,instancesofItemcannothavetheirdateCreatedpropertychanged.ChangeItemsothattheycan,andthenaddabuttonunderneaththedateLabelinDetailViewControllerwiththetitle“ChangeDate.”Whenthisbuttonistapped,pushanotherviewcontrollerinstanceontothenavigationstack.ThisviewcontrollershouldhaveaUIDatePickerinstancethatmodifiesthedateCreatedpropertyoftheselectedItem.
15Camera
Inthischapter,youaregoingtoaddphotostotheHomepwnerapplication.YouwillpresentaUIImagePickerControllersothattheusercantakeandsaveapictureofeachitem.TheimagewillthenbeassociatedwithanIteminstanceandviewableintheitem’sdetailview(Figure15.1).Figure15.1Homepwnerwithcameraaddition
Imagestendtobeverylarge,soitisagoodideatostoreimagesseparatelyfromotherdata.Thus,youaregoingtocreateasecondstoreforimages.ImageStorewillfetchandcacheimagesastheyareneeded.
DisplayingImagesandUIImageView
YourfirststepistohavetheDetailViewControllergetanddisplayanimage.AneasywaytodisplayanimageistoputaninstanceofUIImageViewonthescreen.OpenHomepwner.xcodeprojandMain.storyboard.ThendraganinstanceofUIImageViewontotheviewatthebottomofthestackview.Selecttheimageviewandopenitssizeinspector.Youwanttheverticalcontenthuggingandcontentcompressionresistanceprioritiesfortheimageviewtobelowerthanthoseoftheotherviews.ChangetheVerticalContentHuggingPrioritytobe248andtheVerticalContentCompressionResistancePrioritytobe749.YourlayoutwilllooklikeFigure15.2.Figure15.2UIImageViewonDetailViewController’sview
AUIImageViewdisplaysanimageaccordingtotheimageview’scontentModeproperty.Thispropertydetermineswheretopositionandhowtoresizethecontentwithinthe
imageview’sframe.UIImageView’sdefaultvalueforcontentModeisUIViewContentMode.scaleToFill,whichadjuststheimagetoexactlymatchtheboundsoftheimageview.Ifyoukeepthedefault,animagetakenbythecamerawillbescaledtofitintothesquareUIImageView.Tomaintaintheimage’saspectratio,youhavetoupdatecontentMode.WiththeUIImageViewselected,opentheattributesinspector.FindtheContentModeattributeandchangeittoAspectFit(Figure15.3).Youwillnotseeachangeonthestoryboard,butnowimageswillberesizedtofitwithintheboundsoftheUIImageView.Figure15.3ChangingUIImageView’smodetoAspectFit
Next,Option-clickDetailViewController.swiftintheprojectnavigatortoopenitintheassistanteditor.Control-dragfromtheUIImageViewtothetopofDetailViewController.swift.NametheoutletimageViewandmakesurethestoragetypeisStrong.ClickConnect(Figure15.4).Figure15.4CreatingtheimageViewoutlet
ThetopofDetailViewController.swiftshouldnowlooklikethis:classDetailViewController:UIViewController,UITextFieldDelegate{
@IBOutletvarnameField:UITextField!
@IBOutletvarserialNumberField:UITextField!
@IBOutletvarvalueField:UITextField!
@IBOutletvardateLabel:UILabel!
@IBOutletvarimageView:UIImageView!
Addingacamerabutton
Nowyouneedabuttontoinitiatethephoto-takingprocess.YouwillcreateaninstanceofUIToolbarandplaceitatthebottomofDetailViewController’sview.InMain.storyboard,pressCommand-Returntoclosetheassistanteditorandgiveyourselfmoreroomtoworkinthestoryboard.Youaregoingtoneedtotemporarilybreakyourinterfacetoaddthetoolbartotheinterface.SelectthebottomconstraintforthestackviewandpressDeletetoremoveit.Youneedtomakeroomforthetoolbaronthebottom.AsofXcode8.1,itisdifficulttoresizethestackview.Soinstead,dragthestackviewupabit(Figure15.5).Theviewwillbemisplacedfornow,butyouwillfixthisshortly.Figure15.5Movingthestackviewoutoftheway
Nowdragatoolbarfromtheobjectlibraryontothebottomoftheview.SelectthetoolbarandopentheAutoLayoutAddNewConstraintsmenu.ConfiguretheconstraintsexactlyasshowninFigure15.6andthenclickAdd5Constraints.Becauseyouchosetheoptiontoupdateframes,thestackviewisrepositionedtoitscorrectlocation.
Figure15.6Toolbarconstraints
AUIToolbarworksalotlikeaUINavigationBar–youcanaddinstancesofUIBarButtonItemtoit.However,whereanavigationbarhastwoslotsforbarbuttonitems,atoolbarhasanarrayofbarbuttonitems.Youcanplaceasmanybarbuttonitemsinatoolbarascanfitonthescreen.Bydefault,anewinstanceofUIToolbarthatiscreatedinaninterfacefilecomeswithoneUIBarButtonItem.Selectthisbarbuttonitemandopentheattributesinspector.ChangetheSystemItemtoCamera,andtheitemwillshowacameraicon(Figure15.7).
Figure15.7UIToolbarwithcamerabarbuttonitem
Buildandruntheapplicationandnavigatetoanitem’sdetailstoseethetoolbarwithitscamerabarbuttonitem.Youhavenotconnectedthecamerabuttontoanactionyet,sotappingonitwillnotdoanything.Thecamerabuttonneedsatargetandanaction.WithMain.storyboardstillopen,Option-clickDetailViewController.swiftintheprojectnavigatortoreopenitintheassistanteditor.InMain.storyboard,selectthecamerabuttonbyfirstclickingonthetoolbarandthenthebuttonitself.Control-dragfromtheselectedbuttontoDetailViewController.swift.IntheConnectionpop-upmenu,selectActionastheconnectiontype,nameittakePicture,selectUIBarButtonItemasthetype,andclickConnect(Figure15.8).
Figure15.8Creatinganaction
Ifyoumadeanymistakeswhilemakingthisconnection,youwillneedtoopenMain.storyboardanddisconnectanybadconnections.(Lookforyellowwarningsignsintheconnectionsinspector.)
TakingPicturesandUIImagePickerController
InthetakePicture(_:)method,youwillinstantiateaUIImagePickerControllerandpresentitonthescreen.WhencreatinganinstanceofUIImagePickerController,youmustsetitssourceTypepropertyandassignitadelegate.Becausethereisset-upworkneededfortheimagepickercontroller,youneedtocreateandpresentitprogrammaticallyinsteadofthroughthestoryboard.
Settingtheimagepicker’ssourceType
ThesourceTypeconstanttellstheimagepickerwheretogetimages.Ithasthreepossiblevalues:UIImagePickerControllerSourceType.camera
Allowstheusertotakeanewphoto.UIImagePickerControllerSourceType.photoLibrary
Promptstheusertoselectanalbumandthenaphotofromthatalbum.UIImagePickerControllerSourceType.savedPhotosAlbum
Promptstheusertochoosefromthemostrecentlytakenphotos.Figure15.9ExamplesofthethreesourceTypes
Thefirstsourcetype,.camera,willnotworkonadevicethatdoesnothaveacamera.So
beforeusingthistype,youhavetocheckforacamerabycallingthemethodisSourceTypeAvailable(_:)ontheUIImagePickerControllerclass:classfuncisSourceTypeAvailable(_type:UIImagePickerControllerSourceType)->Bool
CallingthismethodreturnsaBooleanvalueforwhetherthedevicesupportsthepassed-insourcetype.InDetailViewController.swift,findthestubfortakePicture(_:).AddthefollowingcodetocreatetheimagepickerandsetitssourceType.@IBActionfunctakePicture(_sender:UIBarButtonItem){
letimagePicker=UIImagePickerController()
//Ifthedevicehasacamera,takeapicture;otherwise,
//justpickfromphotolibrary
ifUIImagePickerController.isSourceTypeAvailable(.camera){
imagePicker.sourceType=.camera
}else{
imagePicker.sourceType=.photoLibrary
}
}
Settingtheimagepicker’sdelegate
Inadditiontoasourcetype,theUIImagePickerControllerinstanceneedsadelegate.WhentheuserselectsanimagefromtheUIImagePickerController’sinterface,thedelegateissentthemessageimagePickerController(_:didFinishPickingMediaWithInfo:).(Iftheusertapsthecancelbutton,thenthedelegatereceivesthemessageimagePickerControllerDidCancel(_:).)Theimagepicker ’sdelegatewillbetheinstanceofDetailViewController.AtthetopofDetailViewController.swift,declarethatDetailViewControllerconformstotheUINavigationControllerDelegateandtheUIImagePickerControllerDelegateprotocols.classDetailViewController:UIViewController,UITextFieldDelegate,
UINavigationControllerDelegate,UIImagePickerControllerDelegate{
WhyUINavigationControllerDelegate?UIImagePickerController’sdelegatepropertyisactuallyinheritedfromitssuperclass,UINavigationController,andwhileUIImagePickerControllerhasitsowndelegateprotocol,itsinheriteddelegatepropertyisdeclaredtoreferenceanobjectthatconformstoUINavigationControllerDelegate.InDetailViewController.swift,settheinstanceofDetailViewControllertobetheimagepicker ’sdelegateintakePicture(_:).@IBActionfunctakePicture(_sender:UIBarButtonItem){
letimagePicker=UIImagePickerController()
//Ifthedevicehasacamera,takeapicture;otherwise,
//justpickfromphotolibrary
ifUIImagePickerController.isSourceTypeAvailable(.camera){
imagePicker.sourceType=.camera
}else{
imagePicker.sourceType=.photoLibrary
}
imagePicker.delegate=self
}
Presentingtheimagepickermodally
OncetheUIImagePickerControllerhasasourcetypeandadelegate,youcandisplayitbypresentingtheviewcontrollermodally.InDetailViewController.swift,addcodetotheendoftakePicture(_:)topresenttheUIImagePickerController.imagePicker.delegate=self
//Placeimagepickeronthescreen
present(imagePicker,animated:true,completion:nil)
}
Buildandruntheapplication.SelectanItemtoseeitsdetailsandthentapthecamerabuttonontheUIToolbarand…theapplicationcrashes.Takealookatthedescriptionofthecrashintheconsole.Homepwner[3575:64615][access]Thisapphascrashedbecauseitattemptedto
accessprivacy-sensitivedatawithoutausagedescription.Theapp'sInfo.plist
mustcontainanNSPhotoLibraryUsageDescriptionkeywithastringvalueexplaining
totheuserhowtheappusesthisdata.
Whenattemptingtoaccessprivateinformation,suchasauser ’sphotos,iOSpresentsaprompttotheuseraskingthemwhethertheywanttoallowaccesstotheapplication.Containedwithinthispromptisadescriptionforwhytheapplicationwantstoaccessthisinformation.Homepwnerismissingthisdescription,andthereforetheapplicationiscrashing.
Permissions
ThereareanumberofcapabilitiesoniOSthatrequireuserapprovalbeforeuse.Hereareasubsetofthosecapabilities:
CameraandphotosLocationMicrophoneHealthKitdata
CalendarReminders
Foreachofthese,yourapplicationmustsupplyausagedescriptionthatspecifiesthereasonthatyourapplicationwantstoaccessthisinformation.Thisdescriptionwillbepresentedtotheuserwhenevertheapplicationaccessesthatcapability.Intheprojectnavigator,selecttheprojectatthetop.MakesuretheHomepwnertargetisselectedandopentheInfotabalongthetop(Figure15.10).Figure15.10Openingtheprojectinfo
HoveroverthelastentryinthislistofCustomiOSTargetPropertiesandclickthe+button.SettheKeyofthisnewentrytobeNSCameraUsageDescriptionandtheTypetobeaString.Double-clickontheValueforthisnewrowandenterthestring“Thisappusesthecameratoassociatephotoswithitems.”Thisisthestringthatwillbepresentedtotheuser.Nowrepeatthesamestepsabovetoaddausagedescriptionforthephotolibrary.TheKeywillbeNSPhotoLibraryUsageDescriptionoftypeStringandtheValuewillbe“ThisappusesthePhotoslibrarytoassociatephotoswithitems.”TheCustomiOSTargetPropertiessectionwillnowlooklikeFigure15.11.(Theentriesinyourlistmaybeinadifferentorder.)
Figure15.11Addinginthenewkeys
Buildandruntheapplicationandnavigatetoanitem.Tapthecamerabuttonandyouwillseethepermissiondialogpresentedwiththeusagedescriptionthatyouprovided(Figure15.12showsthedescriptionforthelibrary).Afteraccepting,theUIImagePickerController’sinterfacewillappearonthescreen(Figure15.13showsthecamerainterface),andyoucantakeapictureorchooseanexistingimageifyourdevicedoesnothaveacamera.Figure15.12Photoslibraryusagedescription
(Ifyouareworkingonthesimulator,therearesomedefaultimagesalreadyinthephotolibrary.Ifyouwouldliketoaddyourown,youcandraganimagefromyourcomputerontothesimulator,anditwillbeaddedtothesimulator ’sphotolibrary.Alternatively,youcanopenSafariinthesimulatorandnavigatetoapagewithanimage.ClickandholdtheimageandchooseSaveImagetosaveitinthesimulator ’sphotolibrary.)
Figure15.13UIImagePickerController’spreviewinterface
Savingtheimage
SelectinganimagedismissestheUIImagePickerControllerandreturnsyoutothedetailview.However,youdonothaveareferencetothephotooncetheimagepickerisdismissed.Tofixthis,youaregoingtoimplementthedelegatemethodimagePickerController(_:didFinishPickingMediaWithInfo:).Thismethodiscalledontheimagepicker ’sdelegatewhenaphotohasbeenselected.InDetailViewController.swift,implementthismethodtoputtheimageintotheUIImageViewandthencallthemethodtodismisstheimagepicker.funcimagePickerController(_picker:UIImagePickerController,
didFinishPickingMediaWithInfoinfo:[String:Any]){
//Getpickedimagefrominfodictionary
letimage=info[UIImagePickerControllerOriginalImage]as!UIImage
//Putthatimageonthescreenintheimageview
imageView.image=image
//Takeimagepickeroffthescreen-
//youmustcallthisdismissmethod
dismiss(animated:true,completion:nil)
}
Buildandruntheapplicationagain.Take(orselect)aphoto.Theimagepickerisdismissed,andyouarereturnedtotheDetailViewController’sview,whereyouwillseetheselectedphoto.Homepwner’suserscouldhavehundredsofitemstocatalog,andeachonecouldhavealargeimageassociatedwithit.KeepinghundredsofinstancesofIteminmemoryisnotabigdeal.Keepinghundredsofimagesinmemorywouldbebad:First,youwillgetalowmemorywarning.Then,ifyourapp’smemoryfootprintcontinuestogrow,theOSwillterminateit.Thesolution,whichyouaregoingtoimplementinthenextsection,istostoreimagestodiskandonlyfetchthemintoRAMwhentheyareneeded.Thisfetchingwillbedonebyanewclass,ImageStore.Whentheapplicationreceivesalow-memorynotification,theImageStore’scachewillbeflushedtofreethememorythatthefetchedimageswereoccupying.
CreatingImageStore
InChapter16,youwillhaveinstancesofItemwriteouttheirpropertiestoafile,whichwillthenbereadinwhentheapplicationstarts.However,becauseimagestendtobeverylarge,itisagoodideatokeepthemseparatefromotherdata.YouaregoingtostorethepicturestheusertakesinaninstanceofaclassnamedImageStore.Theimagestorewillfetchandcachetheimagesastheyareneeded.Itwillalsobeabletoflushthecacheifthedevicerunslowonmemory.CreateanewSwiftfilenamedImageStore.InImageStore.swift,definetheImageStoreclassandaddapropertythatisaninstanceofNSCache.importFoundation
importUIKit
classImageStore{
letcache=NSCache<NSString,UIImage>()
}
Thecacheworksverymuchlikeadictionary(whichyousawinChapter2).Youareabletoadd,remove,andupdatevaluesassociatedwithagivenkey.Unlikeadictionary,thecachewillautomaticallyremoveobjectsifthesystemgetslowonmemory.Whilethiscouldbeaprobleminthischapter(becauseimageswillonlyexistwithinthecache),youwillfixtheprobleminChapter16whenyouwillalsowritetheimagestothefilesystem.NotethatthecacheisassociatinganinstanceofNSStringwithUIImage.NSStringisObjective-C’sversionofString.DuetothewayNSCacheisimplemented(itisanObjective-Cclass,likemostofApple’sclassesthatyouhavebeenworkingwith),itrequiresyoutouseNSStringinsteadofString.Nowimplementthreemethodsforadding,retrieving,anddeletinganimagefromthedictionary.classImageStore{
letcache=NSCache<NSString,UIImage>()
funcsetImage(_image:UIImage,forKeykey:String){
cache.setObject(image,forKey:keyasNSString)
}
funcimage(forKeykey:String)->UIImage?{
returncache.object(forKey:keyasNSString)
}
funcdeleteImage(forKeykey:String){
cache.removeObject(forKey:keyasNSString)
}
}
ThesethreemethodsalltakeinakeyoftypeStringsothattherestofyourcodebasedoes
nothavetothinkabouttheunderlyingimplementationofNSCache.YouthencasteachStringtoanNSStringwhenpassingittothecache.
GivingViewControllersAccesstotheImageStore
TheDetailViewControllerneedsaninstanceofImageStoretofetchandstoreimages.YouwillinjectthisdependencyintotheDetailViewController’sdesignatedinitializer,justasyoudidforItemsViewControllerandItemStoreinChapter10.InDetailViewController.swift,addapropertyforanImageStore.varitem:Item!{
didSet{
navigationItem.title=item.name
}
}
varimageStore:ImageStore!
NowdothesameinItemsViewController.swift.varitemStore:ItemStore!
varimageStore:ImageStore!
Next,stillinItemsViewController.swift,updateprepare(for:sender:)tosettheimageStorepropertyonDetailViewController.overridefuncprepare(forsegue:UIStoryboardSegue,sender:Any?){
//Ifthetriggeredsegueisthe"showItem"segue"
switchsegue.identifier{
case"showItem"?:
//Figureoutwhichrowwasjusttapped
ifletrow=tableView.indexPathForSelectedRow?.row{
//Gettheitemassociatedwiththisrowandpassitalong
letitem=itemStore.allItems[row]
letdetailViewController
=segue.destinationas!DetailViewController
detailViewController.item=item
detailViewController.imageStore=imageStore
}
default:
preconditionFailure("Unexpectedsegueidentifier.")
}
}
Finally,updateAppDelegate.swifttocreateandinjecttheImageStore.funcapplication(_application:UIApplication,didFinishLaunchingWithOptions
launchOptions:[UIApplicationLaunchOptionsKey:Any]?)->Bool{
//Overridepointforcustomizationafterapplicationlaunch.
//CreateanItemStore
letitemStore=ItemStore()
//CreateanImageStore
letimageStore=ImageStore()
//AccesstheItemsViewControllerandsetitsitemstoreandimagestore
letnavController=window!.rootViewControlleras!UINavigationController
letitemsController=navController.topViewControlleras!ItemsViewController
itemsController.itemStore=itemStore
itemsController.imageStore=imageStore
CreatingandUsingKeys
Whenanimageisaddedtothestore,itwillbeputintothecacheunderauniquekey,andtheassociatedItemobjectwillbegiventhatkey.WhentheDetailViewControllerwantsanimagefromthestore,itwillaskitsitemforthekeyandsearchthecachefortheimage.AddapropertytoItem.swifttostorethekey.letdateCreated:Date
letitemKey:String
Theimagekeysneedtobeuniqueforyourcachetowork.Whiletherearemanywaystohacktogetherauniquestring,youaregoingtousetheCocoaTouchmechanismforcreatinguniversallyuniqueidentifiers(UUIDs),alsoknownasgloballyuniqueidentifiers(GUIDs).ObjectsoftypeNSUUIDrepresentaUUIDandaregeneratedusingthetime,acounter,andahardwareidentifier,whichisusuallytheMACaddressoftheWi-Ficard.Whenrepresentedasastring,UUIDslooksomethinglikethis:4A73B5D2-A6F4-4B40-9F82-EA1E34C1DC04
InItem.swift,generateaUUIDandsetitastheitemKey.init(name:String,serialNumber:String?,valueInDollars:Int){
self.name=name
self.valueInDollars=valueInDollars
self.serialNumber=serialNumber
self.dateCreated=Date()
self.itemKey=UUID().uuidString
super.init()
}
Then,inDetailViewController.swift,updateimagePickerController(_:didFinishPickingMediaWithInfo:)tostoretheimageintheImageStore.funcimagePickerController(_picker:UIImagePickerController,
didFinishPickingMediaWithInfoinfo:[String:Any]){
//Getpickedimagefrominfodictionary
letimage=info[UIImagePickerControllerOriginalImage]as!UIImage
//StoretheimageintheImageStorefortheitem'skey
imageStore.setImage(image,forKey:item.itemKey)
//Putthatimageonthescreenintheimageview
imageView.image=image
//Takeimagepickeroffthescreen-
//youmustcallthisdismissmethod
dismiss(animated:true,completion:nil)
}
Eachtimeanimageiscaptured,itwillbeaddedtothestore.BoththeImageStoreandtheItemwillknowthekeyfortheimage,sobothwillbeabletoaccessitasneeded(Figure15.14).
Figure15.14Accessingimagesfromthecache
Similarly,whenanitemisdeleted,youneedtodeleteitsimagefromtheimagestore.InItemsViewController.swift,updatetableView(_:commit:forRowAt:)toremovetheitem’simagefromtheimagestore.overridefunctableView(_tableView:UITableView,
commiteditingStyle:UITableViewCellEditingStyle,
forRowAtindexPath:IndexPath){
//Ifthetableviewisaskingtocommitadeletecommand...
ifeditingStyle==.delete{
letitem=itemStore.allItems[indexPath.row]
lettitle="Delete\(item.name)?"
letmessage="Areyousureyouwanttodeletethisitem?"
letac=UIAlertController(title:title,
message:message,
preferredStyle:.actionSheet)
letcancelAction=UIAlertAction(title:"Cancel",
style:.cancel,
handler:nil)
ac.addAction(cancelAction)
letdeleteAction=UIAlertAction(title:"Delete",style:.destructive,
handler:{(action)->Voidin
//Removetheitemfromthestore
self.itemStore.removeItem(item)
//Removetheitem'simagefromtheimagestore
self.imageStore.deleteImage(forKey:item.itemKey)
//Alsoremovethatrowfromthetableviewwithananimation
self.tableView.deleteRows(at:[indexPath],with:.automatic)
})
ac.addAction(deleteAction)
//Presentthealertcontroller
present(ac,animated:true,completion:nil)
}
}
WrappingUpImageStore
NowthattheImageStorecanstoreimagesandinstancesofItemhaveakeytogetanimage(Figure15.14),youneedtoteachDetailViewControllerhowtograbtheimagefortheselectedItemandplaceitinitsimageView.TheDetailViewController’sviewwillappearwhentheusertapsarowinItemsViewControllerandwhentheUIImagePickerControllerisdismissed.Inbothofthesesituations,theimageViewshouldbepopulatedwiththeimageoftheItembeingdisplayed.Currently,itisonlyhappeningwhentheUIImagePickerControllerisdismissed.InDetailViewController.swift,makethishappeninviewWillAppear(_:).overridefuncviewWillAppear(_animated:Bool){
super.viewWillAppear(animated)
nameField.text=item.name
serialNumberField.text=item.serialNumber
valueField.text=
numberFormatter.string(from:NSNumber(value:item.valueInDollars))
dateLabel.text=dateFormatter.string(from:item.dateCreated)
//Gettheitemkey
letkey=item.itemKey
//Ifthereisanassociatedimagewiththeitem
//displayitontheimageview
letimageToDisplay=imageStore.image(forKey:key)
imageView.image=imageToDisplay
}
Buildandruntheapplication.Createanitemandselectitfromthetableview.Then,tapthecamerabuttonandtakeapicture.Theimagewillappearasitshould.Popoutfromtheitem’sdetailstothelistofitems.Unlikebefore,ifyoutapanddrilldowntoseethedetailsoftheitemyouaddedapictureto,youwillseetheimage.
BronzeChallenge:EditinganImage
UIImagePickerControllerhasabuilt-ininterfaceforeditinganimageonceithasbeenselected.AllowtheusertoedittheimageandusetheeditedimageinsteadoftheoriginalimageinDetailViewController.
SilverChallenge:RemovinganImage
Addabuttonthatclearstheimageforanitem.
GoldChallenge:CameraOverlay
UIImagePickerControllerhasacameraOverlayViewproperty.MakeitsothatpresentingtheUIImagePickerControllershowsacrosshairinthemiddleoftheimagecapturearea.
FortheMoreCurious:NavigatingImplementationFiles
Bothofyourviewcontrollershavequiteafewmethodsintheirimplementationfiles.TobeaneffectiveiOSdeveloper,youmustbeabletogotothecodeyouarelookingforquicklyandeasily.ThesourceeditorjumpbarinXcodeisonetoolatyourdisposal(Figure15.15).Figure15.15Sourceeditorjumpbar
Thejumpbarshowsyouwhereexactlyyouarewithintheproject(andalsowherethecursoriswithinagivenfile).Figure15.16breaksdownthejumpbardetails.Figure15.16Jumpbardetails
Thebreadcrumbtrailnavigationofthejumpbarmirrorstheprojectnavigationhierarchy.Ifyouclickonanyofthesections,youwillbepresentedwithapopoverofthatsectionintheprojecthierarchy.Fromthere,youcaneasilynavigatetootherpartsoftheproject.Figure15.17showsthefilepopoverfortheHomepwnerfolder.Figure15.17Filepopover
Perhapsmostusefulistheabilitytonavigateeasilywithinanimplementationfile.Ifyouclickonthelastelementinthebreadcrumbtrail,youwillgetapopoverwiththecontentsofthefile,includingallofthemethodsimplementedwithinthatfile.Whilethepopoverisvisible,youcantypetofiltertheitemsinthelist.Atanypoint,youcan
usetheupanddownarrowkeysandthenpresstheReturnkeytojumptothatmethodinthecode.Figure15.18showswhatyougetwhenyousearchfor“tableview”inItemsViewController.swift.Figure15.18Filepopoverwith“tableview”search
//MARK:
Asyourclassesgetlonger,itcangetmoredifficulttofindamethodburiedinalonglistofmethods.Agoodwaytoorganizeyourmethodsistouse//MARK:comments.Twouseful//MARK:commentsarethedividerandthelabel://Thisisadivider
//MARK:-
//Thisisalabel
//MARK:MyAwesomeMethods
Thedividerandlabelcanbecombined://MARK:-Viewlifecycle
overridefuncviewDidLoad(){...}
overridefuncviewWillAppear(_animated:Bool){...}
//MARK:-Actions
funcaddNewItem(_sender:UIBarButtonItem){...}
Adding//MARK:commentstoyourcodedoesnotchangethecodeitself;itjusttellsXcodehowtovisuallyorganizeyourmethods.Youcanseetheresultsbyopeningthecurrentfileiteminthejumpbar.Figure15.19presentsawell-organizedItemsViewController.swift.
Figure15.19Filepopoverwith//MARK:s
Ifyoumakeahabitofusing//MARK:comments,youwillforceyourselftoorganizeyourcode.Ifdonethoughtfully,thiswillmakeyourcodemorereadableandeasiertoworkwith.
16Saving,Loading,andApplicationStates
TherearemanywaystosaveandloaddatainaniOSapplication.ThischapterwilltakeyouthroughsomeofthemostcommonmechanismsaswellastheconceptsyouneedforwritingtoorreadingfromthefilesysteminiOS.Alongtheway,youwillbeupdatingHomepwnersothatitsdatapersistsbetweenruns(Figure16.1).Figure16.1Homepwnerinthetaskswitcher
Archiving
MostiOSapplicationsare,atbase,doingthesamething:providinganinterfacefortheusertomanipulatedata.Everyobjectinanapplicationhasaroleinthisprocess.Modelobjectsareresponsibleforholdingontothedatathattheusermanipulates.Viewobjectsreflectthatdata,andcontrollersareresponsibleforkeepingtheviewsandthemodelobjectsinsync.Therefore,savingandloading“data”almostalwaysmeanssavingandloadingmodelobjects.InHomepwner,themodelobjectsthatausermanipulatesareinstancesofItem.ForHomepwnertobeausefulapplication,instancesofItemmustpersistbetweenrunsoftheapplication.YouwillbeusingarchivingtosaveandloadItemobjects.ArchivingisoneofthemostcommonwaysofpersistingmodelobjectsiniOS.Archivinganobjectinvolvesrecordingallofitspropertiesandsavingthemtothefilesystem.Unarchivingre-createstheobjectfromthatdata.ClasseswhoseinstancesneedtobearchivedandunarchivedmustconformtotheNSCodingprotocolandimplementitstworequiredmethods,encode(with:)andinit(coder:).protocolNSCoding{
funcencode(withaCoder:NSCoder)
init?(coderaDecoder:NSCoder)
}
Whenobjectsareaddedtoaninterfacefile,suchasastoryboardfile,theyarearchived.Atruntime,theobjectsareloadedintomemorybybeingunarchivedfromtheinterfacefile.UIViewandUIViewControllerbothconformtotheNSCodingprotocol,sobothcanbearchivedandunarchivedwithoutanyextraeffortfromyou.YourItemclass,ontheotherhand,doesnotcurrentlyconformtoNSCoding.OpenHomepwner.xcodeprojandaddthisprotocoldeclarationinItem.swift.classItem:NSObject,NSCoding{
Thenextstepistoimplementtherequiredmethods.Let’sstartwithencode(with:).WhenanItemissentthemessageencode(with:),itwillencodeallofitspropertiesintotheNSCoderobjectthatispassedasanargument.Whilesaving,youwilluseNSCodertowriteoutastreamofdata.Thatstreamwillbeorganizedaskey-valuepairsandstoredonthefilesystem.InItem.swift,implementencode(with:)toaddthenamesandvaluesoftheitem’spropertiestothestream.funcencode(withaCoder:NSCoder){
aCoder.encode(name,forKey:"name")
aCoder.encode(dateCreated,forKey:"dateCreated")
aCoder.encode(itemKey,forKey:"itemKey")
aCoder.encode(serialNumber,forKey:"serialNumber")
aCoder.encode(valueInDollars,forKey:"valueInDollars")
}
TofindoutwhichencodingmethodstouseforotherSwifttypes,youcancheckthedocumentationforNSCoder.Regardlessofthetypeoftheencodedvalue,thereisalwaysa
key,whichisastringthatidentifieswhichpropertyisbeingencoded.Byconvention,thiskeyisthenameofthepropertybeingencoded.Encodingisarecursiveprocess.Whenaninstanceisencoded(thatis,whenitisthefirstargumentinencode(_:forKey:)),thatinstanceissentencode(with:).Duringtheexecutionofitsencode(with:)method,itencodesitspropertiesusingencode(_:forKey:)(Figure16.2).Thus,eachinstanceencodesanypropertiesthatitreferences,whichencodeanypropertiesthattheyreference,andsoon.Figure16.2Encodinganobject
ThepurposeofthekeyistoretrievetheencodedvaluewhenthisItemisloadedfromthefilesystemlater.Objectsbeingloadedfromanarchivearesentthemessageinit(coder:).Thismethodshouldgraballoftheobjectsthatwereencodedinencode(with:)andassignthemtotheappropriateproperty.InItem.swift,implementinit(coder:).requiredinit(coderaDecoder:NSCoder){
name=aDecoder.decodeObject(forKey:"name")as!String
dateCreated=aDecoder.decodeObject(forKey:"dateCreated")as!Date
itemKey=aDecoder.decodeObject(forKey:"itemKey")as!String
serialNumber=aDecoder.decodeObject(forKey:"serialNumber")as!String?
valueInDollars=aDecoder.decodeInteger(forKey:"valueInDollars")
super.init()
}
NoticethatthismethodhasanNSCoderargument,too.Ininit(coder:),theNSCoderisfullofdatatobeconsumedbytheItembeinginitialized.AlsonoticethatyoucalldecodeObject(forKey:)onthecontainertogetobjectsand
decodeInteger(forKey:)togetthevalueInDollars.InChapter10,wetalkedabouttheinitializerchainanddesignatedinitializers.Theinit(coder:)methodisnotpartofthisdesignpattern.YouwillkeepItem’sdesignatedinitializerthesame,andinit(coder:)willnotcallit.InstancesofItemarenowNSCodingcompliantandcanbesavedtoandloadedfromthefilesystemusingarchiving.Youcanbuildtheapplicationnowtomakesuretherearenosyntaxerrors,butyoudonotyethaveawaytokickoffthesavingandloading.Youalsoneedaplaceonthefilesystemtostorethesaveditems.
ApplicationSandbox
EveryiOSapplicationhasitsownapplicationsandbox.Anapplicationsandboxisadirectoryonthefilesystemthatisbarricadedfromtherestofthefilesystem.Yourapplicationmuststayinitssandbox,andnootherapplicationcanaccessitssandbox.Figure16.3showstheapplicationsandboxforiTunes/iCloud.Figure16.3Applicationsandbox
Theapplicationsandboxcontainsanumberofdirectories:Documents/
Thisdirectoryiswhereyouwritedatathattheapplicationgeneratesduringruntimeandthatyouwanttopersistbetweenrunsoftheapplication.ItisbackedupwhenthedeviceissynchronizedwithiTunesoriCloud.Ifsomethinggoeswrongwiththedevice,filesinthisdirectorycanberestoredfromiTunesoriCloud.InHomepwner,thefilethatholdsthedataforallyouritemswillbestoredhere.Library/Caches/
Thisdirectoryiswhereyouwritedatathattheapplicationgeneratesduringruntimeandthatyouwanttopersistbetweenrunsoftheapplication.However,unliketheDocumentsdirectory,itdoesnotgetbackedupwhenthedeviceissynchronizedwithiTunesoriCloud.Amajorreasonfornotbackingupcacheddataisthatthedatacanbeverylargeandextendthetimeittakestosynchronizeyourdevice.Datastoredsomewhereelse–likeawebserver–canbeplacedinthisdirectory.Iftheuserneedstorestorethedevice,thisdatacanbedownloadedfromthewebserveragain.Ifthedeviceisverylowondiskspace,thesystemmaydeletethecontentsofthisdirectory.Library/Preferences/
ThisdirectoryiswhereanypreferencesarestoredandwheretheSettingsapplicationlooksforapplicationpreferences.Library/PreferencesishandledautomaticallybytheclassNSUserDefaultsandisbackedupwhenthedeviceissynchronizedwithiTunesoriCloud.tmp/
Thisdirectoryiswhereyouwritedatathatyouwillusetemporarilyduringanapplication’sruntime.TheOSmaypurgefilesinthisdirectorywhenyourapplicationisnotrunning.
However,tobetidyyoushouldexplicitlyremovefilesfromthisdirectorywhenyounolongerneedthem.ThisdirectorydoesnotgetbackedupwhenthedeviceissynchronizedwithiTunesoriCloud.
ConstructingafileURL
TheinstancesofItemfromHomepwnerwillbesavedtoasinglefileintheDocumentsdirectory.TheItemStorewillhandlewritingtoandreadingfromthatfile.Todothis,theItemStoreneedstoconstructaURLtothisfile.ImplementanewpropertyinItemStore.swifttostorethisURL.varallItems=[Item]()
letitemArchiveURL:URL={
letdocumentsDirectories=
FileManager.default.urls(for:.documentDirectory,in:.userDomainMask)
letdocumentDirectory=documentsDirectories.first!
returndocumentDirectory.appendingPathComponent("items.archive")
}()
Insteadofassigningavaluetothepropertydirectly,thevalueisbeingsetusingaclosure.YoumayrecallthatyoudidthiswiththenumberFormatterpropertyinChapter4.Noticethattheclosureherehasasignatureof()->URL,meaningitdoesnottakeinanyargumentsanditreturnsaninstanceofURL.WhentheItemStoreclassisinstantiated,thisclosurewillberunandthereturnvaluewillbeassignedtotheitemArchiveURLproperty.Usingaclosurelikethisallowsyoutosetthevalueforavariableorconstantthatrequiresmultiplelinesofcode,whichcanbeveryusefulwhenconfiguringobjects.Thismakesyourcodemoremaintainablebecauseitkeepsthepropertyandthecodeneededtogeneratethepropertytogether.Themethodurls(for:in:)searchesthefilesystemforaURLthatmeetsthecriteriagivenbythearguments.(Double-checkthatyourfirstargumentis.documentDirectoryandnot.documentationDirectory.Autocomplete’sfirstsuggestionis.documentationDirectory,soitiseasytointroducethiserrorandendupwiththewrongURL.)IniOS,thelastargumentisalwaysthesame.(ThismethodisborrowedfrommacOS,wheretherearesignificantlymoreoptions.)ThefirstargumentisaSearchPathDirectoryenumerationthatspecifiesthedirectoryinthesandboxyouwanttheURLfor.Forexample,searchingfor.cachesDirectorywillreturntheCachesdirectoryintheapplication’ssandbox.YoucansearchthedocumentationforSearchPathDirectorytolocatetheotheroptions.RememberthattheseenumerationvaluesaresharedbyiOSandmacOS,sonotallofthemwillworkoniOS.Thereturnvalueofurls(for:in:)isanarrayofURLs.ItisanarraybecauseinmacOStheremaybemultipleURLsthatmeetthesearchcriteria.IniOS,however,therewillonlybeone(ifthedirectoryyousearchedforisanappropriatesandboxdirectory).Therefore,the
nameofthearchivefileisappendedtothefirstandonlyURLinthearray.ThiswillbewherethearchiveofIteminstanceswilllive.
NSKeyedArchiverandNSKeyedUnarchiver
Younowhaveaplacetosavedataonthefilesystemandamodelobjectthatcanbesavedtothefilesystem.Thefinaltwoquestionsare:Howdoyoukickoffthesavingandloadingprocesses,andwhendoyoudoit?TosaveinstancesofItem,youwillusetheclassNSKeyedArchiverwhentheapplication“exits.”InItemStore.swift,implementanewmethodthatcallsarchiveRootObject(_:toFile:)ontheNSKeyedArchiverclass.funcsaveChanges()->Bool{
print("Savingitemsto:\(itemArchiveURL.path)")
returnNSKeyedArchiver.archiveRootObject(allItems,toFile:itemArchiveURL.path)
}
ThearchiveRootObject(_:toFile:)methodtakescareofsavingeverysingleIteminallItemstotheitemArchiveURL.Yes,itisthatsimple.HereishowarchiveRootObject(_:toFile:)works:
ThemethodbeginsbycreatinganinstanceofNSKeyedArchiver.(NSKeyedArchiverisaconcretesubclassoftheabstractclassNSCoder.)Themethodencode(with:)iscalledonallItemsandispassedtheinstanceofNSKeyedArchiverasanargument.TheallItemsarraythencallsencode(with:)toalloftheobjectsitcontains,passingthesameNSKeyedArchiver.Thus,allyourinstancesofItemencodetheirinstancevariablesintotheverysameNSKeyedArchiver(Figure16.4).TheNSKeyedArchiverwritesthedataitcollectedtothepath.
Figure16.4ArchivingtheallItemsarray
WhentheuserpressestheHomebuttononthedevice,themessage
applicationDidEnterBackground(_:)issenttotheAppDelegate.ThatiswhenyouwanttosendsaveChangestotheItemStore.
OpenAppDelegate.swiftandaddapropertytotheclasstostoretheItemStoreinstance.YouwillneedapropertytoreferencetheinstanceinapplicationDidEnterBackground(_:).classAppDelegate:UIResponder,UIApplicationDelegate{
varwindow:UIWindow?
letitemStore=ItemStore()
Thenupdateapplication(_:didFinishLaunchingWithOptions:)tousethispropertyinsteadofthelocalconstant.funcapplication(_application:UIApplication,didFinishLaunchingWithOptions
launchOptions:[UIApplicationLaunchOptionsKey:Any]?)->Bool{
//Overridepointforcustomizationafterapplicationlaunch.
//CreateanItemStore
letitemStore=ItemStore()
//CreateanImageStore
letimageStore=ImageStore()
//AccesstheItemsViewControllerandsetitsitemstoreandimagestore
letnavController=window!.rootViewControlleras!UINavigationController
letitemsController=navController.topViewControlleras!ItemsViewController
itemsController.itemStore=itemStore
itemsController.imageStore=imageStore
returntrue
}
Becausethepropertyandthelocalconstantwerenamedthesame,youonlyneededtoremovethecodethatcreatedthelocalconstant.Now,stillinAppDelegate.swift,implementapplicationDidEnterBackground(_:)tokickoffsavingtheIteminstances.(Thismethodmayhavealreadybeenimplementedbythetemplate.Ifso,makesuretoaddthiscodetotheexistingmethodinsteadofwritinganewone.)funcapplicationDidEnterBackground(_application:UIApplication){
letsuccess=itemStore.saveChanges()
if(success){
print("SavedalloftheItems")
}else{
print("CouldnotsaveanyoftheItems")
}
}
Buildandruntheapplicationonthesimulator.CreateafewinstancesofItem,thenpresstheHomebuttontoleavetheapplication.Checktheconsoleandyoushouldseealogstatementindicatingthattheitemsweresaved.WhileyoucannotyetloadtheseinstancesofItembackintotheapplication,youcanstillverifythatsomethingwassaved.
Intheconsole’slogstatements,findonethatlogsouttheitemArchiveURLlocationandanotherthatindicateswhethersavingwassuccessful.Ifsavingwasnotsuccessful,confirmthatyouritemArchiveURLisbeingcreatedcorrectly.Iftheitemsweresavedsuccessfully,copythepaththatisprintedtotheconsole.OpenFinderandpressCommand-Shift-G.PastethefilepaththatyoucopiedfromtheconsoleandpressReturn.Youwillbetakentothedirectorythatcontainstheitems.archivefile.PressCommand-Uptonavigatetotheparentdirectoryofitems.archive.Thisistheapplication’ssandboxdirectory.Here,youcanseetheDocuments,Library,andtmpdirectoriesalongsidetheapplicationitself(Figure16.5).Figure16.5Homepwner’ssandbox
Thelocationofthesandboxdirectorycanchangebetweenrunsoftheapplication;however,thecontentsofthesandboxwillremainunchanged.Duetothis,youmayneedtocopyandpastethedirectoryintoFinderfrequentlywhileworkingonanapplication.
Loadingfiles
Nowlet’sturntoloadingthesefiles.ToloadinstancesofItemwhentheapplicationlaunches,youwillusetheclassNSKeyedUnarchiverwhentheItemStoreiscreated.InItemStore.swift,overrideinit()toaddthefollowingcode.init(){
ifletarchivedItems=
NSKeyedUnarchiver.unarchiveObject(withFile:itemArchiveURL.path)as?[Item]{
allItems=archivedItems
}
}
TheunarchiveObject(withFile:)methodwillcreateaninstanceofNSKeyedUnarchiverandloadthearchivelocatedattheitemArchiveURLintothatinstance.TheNSKeyedUnarchiverwilltheninspectthetypeoftherootobjectinthearchiveandcreateaninstanceofthattype.Inthiscase,thetypewillbeanarrayofItemsbecauseyoucreatedthisarchivewitharootobjectoftype[Item].(IftherootobjectwereaninstanceofIteminstead,thenunarchiveObject(withFile:)wouldreturnanItem.)Thenewlycreatedarrayisthensentinit(coder:)and,asyoumayhaveguessed,theNSKeyedUnarchiverispassedastheargument.Thearraystartsdecodingitscontents(instancesofItem)fromtheNSKeyedUnarchiverandsendseachoftheseobjectsthemessageinit(coder:),passingthesameNSKeyedUnarchiver.
Buildandruntheapplication.Youritemswillbeavailableuntilyouexplicitlydeletethem.Onethingtonoteabouttestingyoursavingandloadingcode:IfyoukillHomepwnerfromXcode,themethodapplicationDidEnterBackground(_:)willnotgetachancetobecalledandtheitemarraywillnotbesaved.YoumustpresstheHomebuttonfirstandthenkillitfromXcodebyclickingtheStopbutton.
ApplicationStatesandTransitions
InHomepwner,theitemsarearchivedwhentheapplicationentersthebackgroundstate.Itisusefultounderstandthestatesanapplicationcanbein,whatcausesapplicationstotransitionbetweenstates,andhowyourcodecanbenotifiedofthesetransitions.ThisinformationissummarizedinFigure16.6.Figure16.6Statesofatypicalapplication
Whenanapplicationisnotrunning,itisinthenotrunningstateanditdoesnotexecuteanycodeorhaveanymemoryreservedinRAM.Aftertheuserlaunchesanapplication,itenterstheactivestate.Whenintheactivestate,anapplication’sinterfaceisonthescreen,itisacceptingevents,anditscodeishandlingthoseevents.Whileintheactivestate,anapplicationcanbetemporarilyinterruptedbyasystemeventlikeanSMSmessage,pushnotification,phonecall,oralarm.Anoverlaywillappearontopofyourapplicationtohandlethisevent,andtheapplicationenterstheinactivestate.Intheinactivestate,anapplicationisvisiblebehindtheoverlayandisexecutingcode,butitisnotreceivingevents.Applicationstypicallyspendverylittletimeintheinactivestate.YoucanforceanactiveapplicationintotheinactivestatebypressingtheLockbuttonatthetopofthedevice.Theapplicationwillstayinactiveuntilthedeviceisunlocked.WhentheuserpressestheHomebuttonorswitchestoanotherapplicationinsomeotherway,
theapplicationentersthebackgroundstate.(Actually,itspendsabriefmomentintheinactivestatebeforetransitioningtothebackgroundstate.)Inthebackgroundstate,anapplication’sinterfaceisnotvisibleorreceivingevents,butitcanstillexecutecode.Bydefault,anapplicationthatentersthebackgroundstatehasabout10secondsbeforeitentersthesuspendedstate.Yourapplicationshouldnotrelyonthisnumber;instead,itshouldsaveuserdataandreleaseanysharedresourcesasquicklyaspossible.Anapplicationinthesuspendedstatecannotexecutecode.Youcannotseeitsinterface,andanyresourcesitdoesnotneedwhilesuspendedaredestroyed.Asuspendedapplicationisessentiallyflash-frozenandcanbequicklythawedwhentheuserrelaunchesit.Table16.1summarizesthecharacteristicsofthedifferentapplicationstates.Table16.1Applicationstates
State Visible ReceivesEvents ExecutesCodeNotRunning no no noActive yes yes yesInactive mostly no yesBackground no no yesSuspended no no no
Youcanseewhatapplicationsareinthebackgroundorsuspendedbydouble-tappingtheHomebuttontogettothetaskswitcher(Figure16.7).YoucandothisinthesimulatorbypressingCommand-Shift-Htwice.(Recentlyrunapplicationsthathavebeenterminatedmayalsoappearinthisdisplay.)Figure16.7Backgroundandsuspendedapplicationsinthetaskswitcher
Anapplicationinthesuspendedstatewillremaininthatstateaslongasthereisadequatesystemmemory.WhentheOSdecidesmemoryisgettinglow,itwillterminatesuspendedapplicationsasneeded.Asuspendedapplicationgetsnoindicationthatitisabouttobeterminated.Itissimplyremovedfrommemory.(Anapplicationmayremaininthetaskswitcherafterithasbeenterminated,butitwillhavetorelaunchwhentapped.)Whenanapplicationchangesitsstate,amethodiscalledontheapplicationdelegate.HerearesomeofthemethodsfromtheUIApplicationDelegateprotocolthatannounceapplicationstatetransitions.(ThesearealsoshowninFigure16.6.)optionalfuncapplication(_application:UIApplication,didFinishLaunchingWithOptions
launchOptions:[UIApplicationLaunchOptionsKey:Any]?)->Bool
optionalfuncapplicationDidBecomeActive(_application:UIApplication)
optionalfuncapplicationWillResignActive(_application:UIApplication)
optionalfuncapplicationDidEnterBackground(_application:UIApplication)
optionalfuncapplicationWillEnterForeground(_application:UIApplication)
Youcanimplementthesemethodstotaketheappropriateactionsforyourapplication.Transitioningtothebackgroundstateisagoodplacetosaveanyoutstandingchangesbecause
itisthelasttimeyourapplicationcanexecutecodebeforeitentersthesuspendedstate.Onceinthesuspendedstate,anapplicationcanbeterminatedatthewhimoftheOS.
WritingtotheFilesystemwithData
YourarchivinginHomepwnersavesandloadstheitemKeyforeachItem,butwhatabouttheimages?Atthemoment,theyarelostwhentheappentersthebackgroundstate.Inthissection,youwillextendtheimagestoretosaveimagesastheyareaddedandfetchthemastheyareneeded.TheimagesforIteminstancesshouldalsobestoredintheDocumentsdirectory.Youcanusetheimagekeygeneratedwhentheusertakesapicturetonametheimageinthefilesystem.ImplementanewmethodinImageStore.swiftnamedimageURL(forKey:)tocreateaURLinthedocumentsdirectoryusingagivenkey.funcimageURL(forKeykey:String)->URL{
letdocumentsDirectories=
FileManager.default.urls(for:.documentDirectory,in:.userDomainMask)
letdocumentDirectory=documentsDirectories.first!
returndocumentDirectory.appendingPathComponent(key)
}
Tosaveandloadanimage,youaregoingtocopytheJPEGrepresentationoftheimageintoabufferinmemory.Insteadofjustcreatingabuffer,Swiftprogrammershaveahandyclasstocreate,maintain,anddestroythesesortsofbuffers–Data.ADatainstanceholdssomenumberofbytesofbinarydata,andyouwilluseDatatostoreimagedata.InImageStore.swift,modifysetImage(_:forKey:)togetaURLandsavetheimage.funcsetImage(_image:UIImage,forKeykey:String){
cache.setObject(image,forKey:keyasNSString)
//CreatefullURLforimage
leturl=imageURL(forKey:key)
//TurnimageintoJPEGdata
ifletdata=UIImageJPEGRepresentation(image,0.5){
//WriteittofullURL
let_=try?data.write(to:url,options:[.atomic])
}
}
Let’sexaminethiscodemoreclosely.ThefunctionUIImageJPEGRepresentationtakestwoparameters:aUIImageandacompressionquality.ThecompressionqualityisaFloatfrom0to1,where1isthehighestquality(leastcompression).ThefunctionreturnsaninstanceofDataifthecompressionsucceedsandnilifitdoesnot.ThisDatainstancecanbewrittentothefilesystembycallingwrite(to:options:).ThebytesheldintheDataarethenwrittentotheURLspecifiedbythefirstparameter.Thesecondparameterallowsforsomeoptionstobepassedintothemethod.Ifthe.atomicoptionispresent,thefileiswrittentoatemporaryplaceonthefilesystem,and,oncethewritingoperationiscomplete,thatfileisrenamedtotheURLofthefirstparameter,replacinganypreviouslyexistingfile.Writingatomicallypreventsdatacorruptionshouldyourapplication
crashduringthewriteprocedure.Itisworthnotingthatthiswayofwritingdatatothefilesystemisnotarchiving.WhileDatainstancescanbearchived,usingthemethodwrite(to:options:)copiesthebytesintheDatadirectlytothefilesystem.Nowthattheimageisstoredinthefilesystem,theImageStorewillneedtoloadthatimagewhenitisrequested.Theinitializerinit(contentsOfFile:)ofUIImagewillreadinanimagefromafile,givenaURL.InImageStore.swift,updatethemethodimage(forKey:)sothattheImageStorewillloadtheimagefromthefilesystemifitdoesnotalreadyhaveit.funcimage(forKeykey:String)->UIImage?{
returncache.object(forKey:keyasNSString)
ifletexistingImage=cache.object(forKey:keyasNSString){
returnexistingImage
}
leturl=imageURL(forKey:key)
guardletimageFromDisk=UIImage(contentsOfFile:url.path)else{
returnnil
}
cache.setObject(imageFromDisk,forKey:keyasNSString)
returnimageFromDisk
}
Whatisthatguardstatement?guardisaconditionalstatement,likeanifstatement.Thecompilerwillonlycontinuepasttheguardstatementiftheconditionwithintheguardistrue.Here,theconditioniswhethertheUIImageinitializationissuccessful.Iftheinitializationfails,theelseblockisexecuted,whichallowsyoutohaveanearlyreturn.Iftheinitializationsucceeds,anyvariablesorconstantsboundintheguardstatement(here,imageFromDisk)areusableaftertheguardstatement.Thecodeaboveisfunctionallyequivalenttothefollowingcode:ifletimageFromDisk=UIImage(contentsOfFile:url.path){
cache.setObject(imageFromDisk,forKey:key)
returnimageFromDisk
}
returnnil
Whileyoucoulddothis,guardprovidesbothacleaner–and,moreimportantly,asafer–waytoensurethatyouexitifyoudonothavewhatyouneed.Usingguardalsoforcesthefailurecasetobedirectlytiedtotheconditionbeingchecked.Thismakesthecodemorereadableandeasiertoreasonabout.Youareabletosaveanimagetodiskandretrieveanimagefromdisk,sothelastthingyouneedtodoisaddfunctionalitytoremoveanimagefromdisk.InImageStore.swift,makesurethatwhenanimageisdeletedfromthestore,itisalsodeletedfromthefilesystem.(Youwillseeanerrorwhenyoutypeinthiscode,whichwewilldiscussnext.)
funcdeleteImage(forKeykey:String){
cache.removeObject(forKey:keyasNSString)
leturl=imageURL(forKey:key)
FileManager.default.removeItem(at:url)
}
Let’stakealookattheerrormessagethatthiscodegenerated,showninFigure16.8.Figure16.8Errorwhenremovingtheimagefromdisk
ThiserrormessageislettingyouknowthatthemethodremoveItem(at:)canfail,butyouarenothandlingtheerror.Let’sfixthis.
ErrorHandling
Itisoftenusefultohaveawayofrepresentingthepossibilityoffailurewhencreatingmethods.Youhaveseenonewayofrepresentingfailurethroughoutthisbookwiththeuseofoptionals.Optionalsprovideasimplewaytorepresentfailurewhenyoudonotcareaboutthereasonforfailure.ConsiderthecreationofanIntfromaString.lettheMeaningOfLife="42"
letnumberFromString=Int(theMeaningOfLife)
ThisinitializeronInttakesaStringparameterandreturnsanoptionalInt(anInt?).ThisisbecausethestringmaynotbeabletoberepresentedasanInt.ThecodeabovewillsuccessfullycreateanInt,butthefollowingcodewillnot:letpi="ApplePie"
letnumberFromString=Int(pi)
Thestring“ApplePie”cannotberepresentedasanInt,sonumberFromStringwillcontainnil.Anoptionalworkswellforrepresentingfailureherebecauseyoudonotcarewhyitfailed.Youjustwanttoknowwhetheritwassuccessful.Whenyouneedtoknowwhysomethingfailed,anoptionalwillnotprovideenoughinformation.Thinkaboutremovingtheimagefromthefilesystem–whymightthatfail?PerhapsthereisnofileatthespecifiedURL,ortheURLismalformed,oryoudonothavepermissiontoremovethatfile.Thereareanumberofreasonsthismethodcouldfail,andyoumightwanttohandleeachcasedifferently.Swiftprovidesaricherrorhandlingsystemwithcompilersupporttoensurethatyourecognizewhensomethingbadcouldhappen.YoualreadysawthiswhentheSwiftcompilertoldyouthatyouwerenothandlingapossibleerrorwhenremovingthefilefromdisk.Ifamethodcouldgenerateanerror,itsmethodsignatureneedstoindicatethisusingthethrowskeyword.HereisthemethoddefinitionforremoveItem(at:):funcremoveItem(atURL:URL)throws
Thethrowskeywordindicatesthatthismethodcouldthrowanerror.(Ifyouarefamiliarwiththrowingexceptioninotherlanguages,Swift’serrorhandlingisnotthesameasthrowingexception.)Byusingthiskeyword,thecompilerensuresthatanyonewhousesthismethodknowsthatthismethodcanthrowanerror–and,moreimportantly,thatthecalleralsohandlesanypotentialerrors.Thisishowthecompilerwasabletoletyouknowthatyouarenothandlingerrorswhenattemptingtoremoveafilefromdisk.Tocallamethodthatcanthrow,youuseado-catchstatement.Withinthedoblock,youannotateanymethodsthatmightthrowanerrorusingthetrykeywordtoreinforcetheideathatthecallmightfail.InImageStore.swift,updatedeleteImage(forKey:)tocallremoveItem(at:)usingado-catchstatement.funcdeleteImage(forKeykey:String){
cache.removeObject(forKey:keyasNSString)
leturl=imageURL(forKey:key)
FileManager.default.removeItem(at:url)
do{
tryFileManager.default.removeItem(at:url)
}catch{
}
}
Ifamethoddoesthrowanerror,thentheprogramimmediatelyexitsthedoblock;nofurthercodeinthedoblockisexecuted.Atthatpoint,theerrorispassedtothecatchblockforittobehandledinsomeway.Now,updatedeleteImage(forKey:)toprintouttheerrortotheconsole.funcdeleteImage(forKeykey:String){
cache.removeObject(forKey:keyasNSString)
leturl=imageURL(forKey:key)
do{
tryFileManager.default.removeItem(at:url)
}catch{
print("Errorremovingtheimagefromdisk:\(error)")
}
}
Withinthecatchblock,thereisanimpliciterrorconstantthatcontainsinformationdescribingtheerror.Youcanoptionallygivethisconstantanexplicitname.UpdatedeleteImage(forKey:)againtouseanexplicitnamefortheerrorbeingcaught.funcdeleteImage(forKeykey:String){
cache.removeObject(forKey:keyasNSString)
leturl=imageURL(forKey:key)
do{
tryFileManager.default.removeItem(at:url)
}catchletdeleteError{
print("Errorremovingtheimagefromdisk:\(errordeleteError)")
}
}
Thereisalotmorethatyoucandowitherrorhandling,butthisisthebasicknowledgethatyouneedfornow.Wewillcovermoredetailsasyouprogressthroughthisbook.BuildandruntheapplicationnowthattheImageStoreiscomplete.TakeaphotoforanitemandexittheapplicationtotheHomescreen(onthesimulator,selectHardware→HomeorpressShift-Command-H;onahardwaredevicesimplypresstheHomebutton).Launchtheapplicationagain.Selectingthatsameitemwillshowallitssaveddetails–includingthephotoyoujusttook.Noticethattheimagesaresavedimmediatelyafterbeingtaken,whiletheinstancesofItemaresavedonlywhentheapplicationentersthebackground.Yousavetheimagesrightawaybecausetheyarejusttoobigtokeepinmemoryforlong.
BronzeChallenge:PNG
InsteadofsavingeachimageasaJPEG,saveitasaPNG.
FortheMoreCurious:ApplicationStateTransitions
Let’swritesomequickcodetogetabetterunderstandingofthedifferentapplicationstatetransitions.InAppDelegate.swift,implementtheapplicationstatetransitiondelegatemethodssothattheyprintoutthenameofthemethod.Youwillneedtoaddfourmoremethods.(Checktomakesurethetemplatehasnotalreadycreatedthesemethodsbeforewritingbrandnewones.)Ratherthanhardcodingthenameofthemethodinthecalltoprint(),usethe#functionexpression.Atcompiletime,the#functionexpressionwillevaluatetoaStringrepresentingthenameofthemethod.funcapplicationWillResignActive(_application:UIApplication){
print(#function)
}
funcapplicationDidEnterBackground(_application:UIApplication){
print(#function)
letsuccess=itemStore.saveChanges()
ifsuccess{
print("SavedalloftheItems")
}else{
print("CouldnotsaveanyoftheItems")
}
}
funcapplicationWillEnterForeground(_application:UIApplication){
print(#function)
}
funcapplicationDidBecomeActive(_application:UIApplication){
print(#function)
}
funcapplicationWillTerminate(_application:UIApplication){
print(#function)
}
Finally,addthesameprint()statementtothetopofapplication(_:didFinishLaunchingWithOptions:).funcapplication(_application:UIApplication,didFinishLaunchingWithOptions
launchOptions:[UIApplicationLaunchOptionsKey:Any]?)->Bool{
print(#function)
...
}
Buildandruntheapplication.Youwillseethattheapplicationgetssentapplication(_:didFinishLaunchingWithOptions:)andthenapplicationDidBecomeActive(_:).Playaroundtoseewhatactionscausewhattransitions.PresstheHomebuttonandtheconsolewillreportthattheapplicationbrieflyinactivatedandthenwentintothebackgroundstate.RelaunchtheapplicationbytappingitsiconontheHomescreenorinthetaskswitcher.Theconsolewillreportthattheapplicationenteredthe
foregroundandthenbecameactive.PresstheHomebuttontoexittheapplicationagain.Then,double-presstheHomebuttontoopenthetaskswitcher.SwipetheHomepwnerapplicationupandoffthisdisplaytoquittheapplication.Notethatnomethodiscalledonyourapplicationdelegateatthispoint–itissimplyterminated.
FortheMoreCurious:ReadingandWritingtotheFilesystem
InadditiontoarchivingandData’sbinaryreadandwritemethods,thereareafewmoremethodsfortransferringdatatoandfromthefilesystem.Oneofthem,CoreData,iscomingupinChapter22.Acoupleothersareworthmentioninghere.UsingDataworkswellforbinarydata.Fortextdata,Stringhastwoinstancemethods:write(to:atomically:encoding:)andinit(contentsOf:encoding:).Theyareusedasfollows://SavesomeStringtothefilesystem
do{
trysomeString.write(to:someURL,
atomically:true,
encoding:.utf8)
}catch{
print("ErrorwritingtoURL:\(error)")
}
//LoadsomeStringfromthefilesystem
do{
letmyEssay=tryString(contentsOf:someURL,encoding:.utf8)
print(myEssay)
}catch{
print("ErrorreadingfromURL:\(error)")
}
Notethatinmanylanguages,anythingunexpectedresultsinanexceptionbeingthrown.InSwift,exceptionsarenearlyalwaysusedtoindicateprogrammererror.Whenanexceptionisthrown,theinformationaboutwhatwentwrongisinanNSExceptionobject.Thatinformationisusuallyjustahinttotheprogrammer,like,“Youtriedtoaccesstheseventhobjectinthisarray,butthereareonlytwo.”Thesymbolsforthecallstack(asitappearedwhentheexceptionwasthrown)arealsointheNSException.Whendoyouuseexceptions,andwhendoyouuseerrorhandling?Ifyouarewritingamethodthatshouldonlybecalledwithanoddnumberasanargument,throwanexceptionifitiscalledwithanevennumber–thecallerismakinganerrorandyouwanttohelpthatprogrammerfindtheerror.Ifyouarewritingamethodthatwantstoreadthecontentsofaparticulardirectorybutdoesnothavethenecessaryprivileges,useSwift’serrorhandlingandthrowanerrortothecallertoindicatewhyyouwereunabletofulfillthisveryreasonablerequest.Propertylistserializabletypescanalsobewrittentothefilesystem.TheonlytypesthatarepropertylistserializableareString,NSNumber(includingprimitiveslikeInt,Double,andBool),Date,Data,Array<Element>,andDictionary<Key:Hashable,Value>.WhenanArray<Element>orDictionary<Key,Value>iswrittentothefilesystemwiththesemethods,anXMLpropertylistiscreated.AnXMLpropertylistisacollectionoftaggedvalues,like:<?xmlversion="1.0"encoding="UTF-8"?>
<!DOCTYPEplistPUBLIC"-//Apple//DTDPLIST1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plistversion="1.0">
<array>
<dict>
<key>firstName</key>
<string>Christian</string>
<key>lastName</key>
<string>Keur</string>
</dict>
<dict>
<key>firstName</key>
<string>Aaron</string>
<key>lastName</key>
<string>Hillegass</string>
</dict>
</array>
</plist>
XMLpropertylistsareaconvenientwaytostoredatabecausetheycanbereadonnearlyanysystem.Manywebserviceapplicationsusepropertylistsasinputandoutput.Thecodeforwritingandreadingapropertylistlookslikethis:letauthors=[
["firstName":"Christian","lastName":"Keur"],
["firstName":"Aaron","lastName":"Hillegass"]
]
//Writearraytodisk
ifPropertyListSerialization.propertyList(authors,
isValidFor:.xml){
do{
letdata=tryPropertyListSerialization.data(with:authors,
format:.xml,
options:[])
data.write(to:url,options:[.atomic])
}catch{
print("Errorwritingplist:\(error)")
}
}
//Readarrayfromdisk
do{
letdata=tryData(contentsOf:url,options:[])
letauthors=tryNSPropertyListSerialization.propertyList(from:data,
options:[],
format:nil)
print("Readinauthors:\(authors)")
}catch{
print("Errorreadingplist:\(error)")
}
FortheMoreCurious:TheApplicationBundle
WhenyoubuildaniOSapplicationprojectinXcode,youcreateanapplicationbundle.Theapplicationbundlecontainstheapplicationexecutableandanyresourcesyouhavebundledwithyourapplication.Resourcesarethingslikestoryboardfiles,images,andaudiofiles–anyfilesthatwillbeusedatruntime.Whenyouaddaresourcefiletoaproject,Xcodeissmartenoughtorealizethatitshouldbebundledwithyourapplication.Howcanyoutellwhichfilesarebeingbundledwithyourapplication?SelecttheHomepwnerprojectfromtheprojectnavigator.CheckouttheBuildPhasespaneintheHomepwnertarget.EverythingunderCopyBundleResourceswillbeaddedtotheapplicationbundlewhenitisbuilt.EachitemintheHomepwnertargetgroupisoneofthephasesthatoccurswhenyoubuildaproject.TheCopyBundleResourcesphaseiswherealloftheresourcesinyourprojectgetcopiedintotheapplicationbundle.Youcancheckoutwhatanapplicationbundlelookslikeonthefilesystemafteryouinstallanapplicationonthesimulator.Printtheapplicationbundlepathtotheconsoleandthennavigatetothatdirectory.print(Bundle.main.bundlePath)
Control-clicktheapplicationbundleandchooseShowPackageContentsfromthecontextualmenu(Figure16.9).Figure16.9Viewinganapplicationbundle
AFinderwindowwillappearshowingyouthecontentsoftheapplicationbundle(Figure16.10).WhenusersdownloadyourapplicationfromtheAppStore,thesefilesarecopiedtotheirdevices.
Figure16.10Theapplicationbundle
Youcanloadfilesfromtheapplication’sbundleatruntime.TogetthefullURLforfilesintheapplicationbundle,youneedtogetareferencetotheapplicationbundleandthenaskitfortheURLofaresource.//Getareferencetotheapplicationbundle
letapplicationBundle=Bundle.main
//AskfortheURLtoaresourcenamedmyImage.pnginthebundle
ifleturl=applicationBundle.url(forResource:"myImage",ofType:"png"){
//DosomethingwithURL
}
IfyouaskfortheURLtoafilethatisnotintheapplication’sbundle,thismethodwillreturnnil.Ifthefiledoesexist,thenthefullURLisreturned,andyoucanusethisURLtoloadthefilewiththeappropriateclass.Bearinmindthatfileswithintheapplicationbundleareread-only.Youcannotmodifythem,norcanyoudynamicallyaddfilestotheapplicationbundleatruntime.Filesintheapplicationbundlearetypicallythingslikebuttonimages,interfacesoundeffects,ortheinitialstateofadatabaseyoushipwithyourapplication.Youwillusethismethodinlaterchapterstoloadthesetypesofresourcesatruntime.
17SizeClasses
Often,youwantanapplication’sinterfacetohaveadifferentlayoutdependingonthedimensionsandorientationofthescreen.Inthischapter,youwillmodifytheinterfaceforDetailViewControllerinHomepwnersothatwhenitappearsonascreenthathasarelativelysmallheight,thesetoftextfieldsandtheimageviewaresidebysideinsteadofstackedontopofoneanother(Figure17.1).Figure17.1TwolayoutsforHomepwner’sDetailViewController
Therelativesizesofscreensaredefinedinsizeclasses.Asizeclassrepresentsarelativeamountofscreenspaceinagivendimension.Eachdimension(widthandheight)caneitherbecompactorregular,sotherearefourpossiblecombinationsofsizeclasses:CompactWidth|CompactHeight
iPhoneswith3.5,4,or4.7-inchscreensinlandscapeorientationCompactWidth|RegularHeight
iPhonesofallsizesinportraitorientationRegularWidth|CompactHeight
iPhoneswith5.5-inchscreensinlandscapeorientationRegularWidth|RegularHeight
iPadsofallsizesinallorientationsNoticethatthesizeclassescoverbothscreensizesandorientations.Insteadofthinkingabout
interfacesintermsoforientationordevice,itisbettertothinkintermsofsizeclasses.
ModifyingTraitsforaSpecificSizeClass
Wheneditingtheinterfaceforaspecificsizeclasscombination,youareabletochange:propertiesformanyviewswhetheraspecificsubviewisinstalledwhetheraspecificconstraintisinstalledtheconstantofaconstraintthefontforsubviewsthatdisplaytext
InHomepwner,youaregoingtofocusonthefirstiteminthatlist–adjustingviewpropertiesdependingonthesizeclassconfiguration.Thegoalistohavetheimageviewbeontherightsideofthelabelsandtextfieldsinacompactheightenvironment.Inaregularheightenvironment,theimageviewwillbebelowthelabelsandtextfields(asitcurrentlyis).Stackviewswillmakethisremarkablyeasy.Tobegin,youaregoingtoembedtheexistingverticalstackviewwithinanotherstackview.Thiswillmakeiteasytoaddanimageviewtotherightsideofthelabelsandtextfields.OpenHomepwner.xcodeprojandMain.storyboard.Selecttheverticalstackviewandclickthe icontoembedthisstackviewwithinanotherstackview.Withthisnewstackviewselected,opentheAutoLayoutAddNewConstraintsmenu,configureitasshowninFigure17.2,andaddtheconstraints.
Figure17.2Stackviewconstraints
Next,openthenewstackview’sattributesinspector.IncreasetheSpacingtobe8.Nowyouaregoingtomovetheimageviewfromtheinnerstackviewtotheouterstackviewthatyoujustcreated.Thisishowyouwillbeabletohavetheimageviewontherightsideoftherestoftheinterface:Inacompactheightenvironment,thestackviewwillbesettobehorizontalandtheimageviewwilltakeuptherightsideoftheinterface.Movingtheimageviewfromonestackviewtotheothercanbealittletricky,soyouaregoingtodoitinafewsteps.OpenthedocumentoutlineandexpandthesectionfortheDetailViewControllerScene.ExpandtheoutertwostackviewsasshowninFigure17.3.
Figure17.3Expandingthedocumentoutline
DragtheImageViewrightabovethestackviewthatitiscurrentlycontainedwithin(Figure17.4).Thiswillmoveitfromtheinnerstackviewtotheouterstackview.Figure17.4Movingtheimageviewtotheouterstackview
Finally,collapsetheinnerstackviewanddragtheImageViewtobebelowitinthestack(Figure17.5).MakesuretheImageViewisindentedatthesamelevelastheinnerstackview.
Youmayneedtoupdateframesatthispointtogetridofanywarnings.Figure17.5Movingtheimageviewbelowtheinnerstackview
Buildandruntheapplication.Confirmthatthebehaviorofthestackviewisunchanged.Atthispoint,youhaveupdatedeverythingthatiscommontoallsizeclasses.Nextyouwillmodifyspecificsizeclassestochangethelayoutofthecontent.AtthebottomofInterfaceBuilder,clickonthetextViewas:iPhone7(wChR)toexpandtheviewoptions.ThenselectthelandscapeOrientation(Figure17.6).LeavetheDeviceasiPhone7.
Figure17.6DetailViewControllerviewedasiPhone7landscape
Next,youwillupdatethepropertiesfortheouterstackviewsothattheimageviewisontherightside.Selecttheouterstackviewandopenitsattributesinspector.UndertheStackViewheading,findtheAxispropertyandclickthe+buttononitsleftside.Fromthepop-upmenu,chooseAnyfortheWidthvariationandCompactfortheHeightvariation(Figure17.7).ClickAddVariation.ThiswillallowyoutocustomizetheaxispropertyforalliPhonesinlandscape.Figure17.7Addingasize-class-specificoption
Forthenewoption(hC),chooseHorizontal(Figure17.8).Now,whenevertheinterfacehasa
compactheight,theouterstackviewwillhaveahorizontalconfiguration.Whentheinterfacehasaregularheight,theouterstackviewwillhaveaverticalconfiguration.Figure17.8Customizingtheaxis
Thelastchangeyouwanttomakeisfortheinnerstackviewandtheimageviewtofilltheouterstackviewequally.Todothis,youwillcustomizetheouterstackview’sdistribution.Withtheattributesinspectorstillopenfortheouterstackview,clickonthe+nexttoDistributionandonceagainselectAnyfortheWidthvariationandCompactfortheHeightvariationfromthepop-upmenu.ChangethedistributionforthissizeclasstobeFillEqually(Figure17.9).Figure17.9Customizingthedistribution
Buildandruntheapplication.Selectanitemanddrilldowntoitsdetailstoaddaphoto,ifitdoesnotalreadyhaveone.Rotatebetweenportraitandlandscape(onthesimulator,youcanuseCommandplustheleftorrightarrowkeytorotate)andnoticehowtheinterfaceislaidoutasyouspecifiedforbothregularandcompactheight.Withthat,yourHomepwnerapplicationiscomplete.Youhavebuiltanappwithaflexibleinterfacethatcantakephotosandstoredata,andwehopeyouareproudofyouraccomplishment!Takesometimetocelebrate.
BronzeChallenge:StackedTextFieldandLabels
Inacompactheightenvironment,makeitsothetextfieldsandlabelsarestackedverticallyinsteadofhorizontally(Figure17.10).Figure17.10Textfieldsandlabelsstacked
18TouchEventsandUIResponder
Inthenexttwochapters,youwillcreateTouchTracker,anappthatletstheuserdrawbytouchingthescreen.Inthischapter,youwillcreateaviewthatdrawslinesinresponsetotheuserdraggingacrossit(Figure18.1).Usingmultitouch,theuserwillbeabletodrawmorethanonelineatatime.Figure18.1TouchTracker
TouchEvents
AsasubclassofUIResponder,aUIViewcanoverridefourmethodstohandlethefourdistincttouchevents:
oneormorefingerstouchthescreenfunctouchesBegan(_touches:Set<UITouch>,withevent:UIEvent?)
oneormorefingersmoveacrossthescreen(thismessageissentrepeatedlyasafingermoves)functouchesMoved(_touches:Set<UITouch>,withevent:UIEvent?)
oneormorefingersareremovedfromthescreenfunctouchesEnded(_touches:Set<UITouch>,withevent:UIEvent?)
asystemevent,likeanincomingphonecall,interruptsatouchbeforeitendsfunctouchesCancelled(_touches:Set<UITouch>,withevent:UIEvent?)
Let’swalkthroughthetypicallifecycleofatouch.Whentheuser ’sfingertouchesthescreen,aninstanceofUITouchiscreated.ThetouchesBegan(_:with:)methodiscalledontheUIViewthatthefingertouched,andtheUITouchispassedinthroughtheSetoftouches.Asthefingermovesaroundthescreen,thetouchobjectisupdatedtocontainthecurrentlocationofthefingeronthescreen.Then,thesameUIViewthatthetouchbeganonissentthemessagetouchesMoved(_:with:).TheSetthatispassedasanargumenttothismethodcontainsthesameUITouchthatoriginallywascreatedwhenthefingeritrepresentstouchedthescreen.Whenthefingerisremovedfromthescreen,thetouchobjectisupdatedonelasttimetocontainthefinallocationofthefinger,andtheviewthatthetouchbeganonissentthemessagetouchesEnded(_:with:).Afterthatmethodfinishesexecuting,theUITouchobjectisdestroyed.Fromthisinformation,youcandrawafewconclusionsabouthowtouchobjectswork:
OneUITouchcorrespondstoonefingeronthescreen.Thistouchobjectlivesaslongasthefingerisonthescreenandalwayscontainsthecurrentpositionofthefingeronthescreen.Theviewthatthefingerstartedonwillreceiveeverytoucheventmessageforthatfinger.EvenifthefingermovesbeyondtheframeoftheUIViewthatthetouchbeganon,thetouchesMoved(_:with:)andtouchesEnded(_:with:)methodswillstillbecalledonthatview.Thus,ifatouchbeginsonaview,thenthatviewownsthetouchforthelifeofthetouch.Youdonothaveto–norshouldyouever–keepareferencetoaUITouchobject.TheapplicationwillgiveyouaccesstoatouchobjectviatheUIRespondermethodscalledatthedistinctpointsinthetouch’slifecycle.
Everytimeatouchdoessomething–likebegins,moves,orends–atoucheventisaddedtoa
queueofeventsthattheUIApplicationobjectmanages.Inpractice,thequeuerarelyfillsup,andeventsaredeliveredimmediately.ThedeliveryofthesetoucheventsinvolvessendingoneoftheUIRespondermessagestotheviewthatownsthetouch.Whataboutmultipletouches?Ifmultiplefingersdothesamethingattheexactsametimetothesameview,allofthesetoucheventsaredeliveredatonce.Eachtouchobject–oneforeachfinger–isincludedintheSetpassedasanargumentintheUIRespondermessages.However,thewindowofopportunityforthe“exactsametime”isfairlyshort.So,insteadofonerespondermessagewithallofthetouches,thereareusuallymultiplerespondermessageswithoneormoreofthetouches.Youwillseehowtohandlemultipletoucheslaterinthischapter.
CreatingtheTouchTrackerApplication
Nowlet’sgetstartedwithyourapplication.InXcode,createanewsingleviewuniversalprojectandnameitTouchTracker(Figure18.2).Figure18.2CreatingTouchTracker
InbuildingTouchTracker,youaregoingtousethedefaultviewcontrollerandthestoryboardthatthetemplatecreated.Foritsviewandmodellayers,youaregoingtocreateacustomviewclassandacustomstructure.Figure18.3showsthemajorpiecesofTouchTracker.
Figure18.3ObjectdiagramforTouchTracker
Let’sbeginwithyourcustomstruct.
CreatingtheLineStruct
YouaregoingtocreatethecustomLinetype.Sofar,allofthetypesthatyouhavecreatedhavebeenclasses.Infact,theyhavebeenCocoaTouchsubclasses;forexample,youhavecreatedsubclassesofNSObject,UIViewController,andUIView.Linewillbeastruct.Youhaveusedstructsthroughoutthisbook–CGRect,CGSize,andCGPointareallstructs.SotooareString,Int,Array,andDictionary.Nowyouaregoingtocreateoneofyourown.CreateanewSwiftfilenamedLine.InLine.swift,importCoreGraphicsanddeclaretheLinestruct.DeclaretwoCGPointpropertiesthatwilldeterminethebeginningandendingpointfortheline.importFoundation
importCoreGraphics
structLine{
varbegin=CGPoint.zero
varend=CGPoint.zero
}
Structs
Structsdifferfromclassesinanumberofways:Structsdonotsupportinheritance.Structsgetamember-wiseinitializerifnootherinitializersaredeclared.Themember-wiseinitializertakesinanargumentforeachpropertywithinthetype.TheLinestruct,forexample,hasthemember-wiseinitializerinit(begin:CGPoint,end:CGPoint).Ifallpropertieshavedefaultvaluesandnootherinitializersaredeclared,structsalsogainanemptyinitializer(init())thatcreatesaninstanceandsetsallofthepropertiestotheirdefaultvalue.Perhapsmostimportantly,structs(andenums)arevaluetypes–asopposedtoclasses,whicharereferencetypes.
Valuetypesvsreferencetypes
Valuetypesaretypeswhosevaluesarecopiedwhentheyareassignedtoanotherinstanceorpassedintheargumentofafunction.Thismeansthatassigninganinstanceofavaluetypetoanotheractuallyassignsacopyofthefirstinstancetothesecondinstance.ValuetypesplayanimportantroleinSwift.Forexample,arraysanddictionariesarebothvaluetypes.Allenumsandstructsyouwritearevaluetypesaswell.
Referencetypesarenotcopiedwhentheyareassignedtoaninstanceorpassedintoanargumentofafunction.Instead,areferencetothesameinstanceispassed.Classesandclosuresarereferencetypes.Sowhichdoyouchoose?Ingeneral,wesuggeststartingoutwithavaluetype(suchasastruct)unlessyouabsolutelyknowyouneedthebenefitsofareferencetype.Valuetypesareeasiertoreasonaboutbecauseyoudonotneedtoworryaboutwhathappenstoaninstancewhenyouchangevaluesonacopy.Ifyouwouldlikeadeeperdiscussiononthistopic,checkoutSwiftProgramming:TheBigNerdRanchGuide.
CreatingDrawView
Inadditiontoacustomstruct,TouchTrackerneedsacustomview.CreateanewSwiftfilenamedDrawView.InDrawView.swift,definetheDrawViewclass.Addtwoproperties:anoptionalLinetokeeptrackofalinethatispossiblybeingdrawnandanarrayofLinestokeeptrackoflinesthathavebeendrawn.importFoundation
importUIKit
classDrawView:UIView{
varcurrentLine:Line?
varfinishedLines=[Line]()
}
AninstanceofDrawViewwillbetheviewoftheapplication’srootViewController,thedefaultViewControllerincludedintheproject.TheviewcontrollerneedstoknowthatitsviewwillbeaninstanceofDrawView.OpenMain.storyboard.SelecttheViewandopentheidentityinspector(Command-Option-3).UnderCustomClass,changetheClasstoDrawView(Figure18.4).Figure18.4Changingtheviewclass
DrawingwithDrawView
AninstanceofDrawViewneedstobeabledrawlines.YouaregoingtowriteamethodthatusesUIBezierPathtocreateandstrokeapathbasedonthepropertiesofagivenLine.Thenyouwilloverridedraw(_:)todrawthelinesinthearrayoffinishedlinesaswellasthecurrentline,ifany.InDrawView.swift,implementthemethodforstrokinglinesandoverridedraw(_:).varcurrentLine:Line?
varfinishedLines=[Line]()
funcstroke(_line:Line){
letpath=UIBezierPath()
path.lineWidth=10
path.lineCapStyle=.round
path.move(to:line.begin)
path.addLine(to:line.end)
path.stroke()
}
overridefuncdraw(_rect:CGRect){
//Drawfinishedlinesinblack
UIColor.black.setStroke()
forlineinfinishedLines{
stroke(line)
}
ifletline=currentLine{
//Ifthereisalinecurrentlybeingdrawn,doitinred
UIColor.red.setStroke()
stroke(line)
}
}
TurningTouchesintoLines
Alineisdefinedbytwopoints.YourLinestoresthesepointsaspropertiesnamedbeginandend.Whenatouchbegins,youwillcreateaLineandsetbothofitspropertiestothepointwherethetouchbegan.Whenthetouchmoves,youwillupdatetheLine’send.Whenthetouchends,youwillhaveyourcompleteLine.InDrawView.swift,implementtouchesBegan(_:with:)tocreateanewline.overridefunctouchesBegan(_touches:Set<UITouch>,withevent:UIEvent?){
lettouch=touches.first!
//Getlocationofthetouchinview'scoordinatesystem
letlocation=touch.location(in:self)
currentLine=Line(begin:location,end:location)
setNeedsDisplay()
}
Thiscodefirstfiguresoutthelocationofthetouchwithintheview’scoordinatesystem.ThenitcallssetNeedsDisplay(),whichflagstheviewtoberedrawnattheendoftherunloop.Next,alsoinDrawView.swift,implementtouchesMoved(_:with:)sothatitupdatestheendofthecurrentLine.overridefunctouchesMoved(_touches:Set<UITouch>,withevent:UIEvent?){
lettouch=touches.first!
letlocation=touch.location(in:self)
currentLine?.end=location
setNeedsDisplay()
}
Finally,stillinDrawView.swift,updatetheendlocationofthecurrentLineandaddittothefinishedLinesarraywhenthetouchends.overridefunctouchesEnded(_touches:Set<UITouch>,withevent:UIEvent?){
ifvarline=currentLine{
lettouch=touches.first!
letlocation=touch.location(in:self)
line.end=location
finishedLines.append(line)
}
currentLine=nil
setNeedsDisplay()
}
Buildandruntheapplicationanddrawsomelinesonthescreen.Whileyouaredrawing,thelineswillappearinred.Oncefinished,theywillappearinblack.
Handlingmultipletouches
Whendrawinglines,youmayhavenoticedthathavingmorethanonefingeronthescreendoesnotdoanything–thatis,youcanonlydrawonelineatatime.Let’supdateDrawViewsothatyoucandrawasmanylinesasyoucanfitfingersonthescreen.Bydefault,aviewwillonlyacceptonetouchatatime.IfonefingerhasalreadytriggeredtouchesBegan(_:with:)buthasnotfinished–andthereforehasnottriggeredtouchesEnded(_:with:)–subsequenttouchesareignored.Inthiscontext,“ignored”meansthatneithertouchesBegan(_:with:)noranyotherUIRespondermethodrelatedtotheextratoucheswillbecalledontheDrawView.InMain.storyboard,selecttheDrawViewandopentheattributesinspector.ChecktheboxlabeledMultipleTouch(Figure18.5),whichwillsettheDrawViewinstance’smultipleTouchesEnabledpropertytotrue.Figure18.5MultipleTouchenabled
NowthatDrawViewwillacceptmultipletouches,eachtimeafingertouchesthescreen,moves,orisremovedfromthescreen,theappropriateUIResponderwillbecalledontheview.However,younowhaveaproblem:YourUIRespondercodeassumestherewillonlybeonetouchactiveandonelinebeingdrawnatatime.Forexample,eachtouch-handlingmethodasksforthefirstelementinthesetoftouchesitreceives.Inasingle-touchview,therewillonlyeverbeoneobjectintheset,soaskingforanyobjectalwaysreturnsthetouchthattriggeredtheevent.Inamultiple-touchview,thesetcancontainmorethanonetouch.Also,DrawViewhasonlyoneproperty(currentLine)thathangsontoalineinprogress.Obviously,youwillneedtoholdasmanylinesastherearetouchescurrentlyonthescreen.Whileyoucouldcreateafewmoreproperties,likecurrentLine1andcurrentLine2,itwouldbeahassletomanagewhichpropertycorrespondstowhichtouch.Insteadofaddingmoreproperties,youaregoingtoreplacethesingleLinewithadictionary
containinginstancesofLine.InDrawView.swift,addanewpropertytoreplacecurrentLine.classDrawView:UIView{
varcurrentLine:Line?
varcurrentLines=[NSValue:Line]()
ThekeytostorethelineinthedictionarywillbederivedfromtheUITouchobjectthatthelinecorrespondsto.Asmoretoucheventsoccur,youcanusethesamealgorithmtoderivethekeyfromtheUITouchthattriggeredtheeventanduseittolookuptheappropriateLineinthedictionary.NowyouneedtoupdatetheUIRespondermethodstoaddlinesthatarecurrentlybeingdrawntothisdictionary.InDrawView.swift,updatethecodeintouchesBegan(_:with:).overridefunctouchesBegan(_touches:Set<UITouch>,withevent:UIEvent?){
lettouch=touches.first!
//Getlocationofthetouchinview'scoordinatesystem
letlocation=touch.location(in:self)
currentLine=Line(begin:location,end:location)
//Logstatementtoseetheorderofevents
print(#function)
fortouchintouches{
letlocation=touch.location(in:self)
letnewLine=Line(begin:location,end:location)
letkey=NSValue(nonretainedObject:touch)
currentLines[key]=newLine
}
setNeedsDisplay()
}
Inthiscode,youfirstprintoutthenameofthemethodusingthe#functionexpression.Second,youenumerateoverallofthetouchesthatbegan,becauseitispossibleformorethanonetouchtobeginatthesametime.(Typically,touchesbeginatdifferenttimesandtouchesBegan(_:with:)getscalledmultipletimesontheDrawViewforeachtouch.Butyouhavetopreparefortheimprobable,ifnottheimpossible.)Next,noticetheuseofNSValue(nonretainedObject:)toderivethekeytostoretheLine.ThismethodcreatesanNSValueinstancethatholdsontotheaddressoftheUITouchobjectthatwillbeassociatedwiththisline.BecauseaUITouchiscreatedwhenatouchbegins,updatedthroughoutitslifetime,anddestroyedwhenthetouchends,theaddressofthatobjectwillbeconstantthrougheachtouch-event-handlingmethod.Figure18.6showsthenewstateofaffairs.
Figure18.6ObjectdiagramformultitouchTouchTracker
Youmaybewondering:WhynotusetheUITouchitselfasthekey?WhygothroughthehoopofcreatinganNSValue?ThedocumentationforUITouchsaysthatyoushouldneverkeepastrongreferencetoaUITouchobject.Thedetailsofmemorymanagementareoutsidethescopeofthisbook,buttoavoidcreatingastrongreferencetoatouchobject,youwrapthememoryaddressoftheUITouchinaninstanceofNSValueusingitsinit(nonretainedObject:)initializer.Thedocumentationforthismethodstates:“Thismethodisusefulifyouwanttoaddanobjecttoacollectionbutdon’twantthecollectiontocreateastrongreferencetoit,”whichisexactlywhatyouwant.BecausethesameUITouchobjectisreusedfortheentiretyofthattouch’slifecycle(andthereforehasthesamememoryaddress),youcanre-createthesameNSValueusingthesameUITouch.Next,updatetouchesMoved(_:with:)inDrawView.swiftsothatitcanlookuptherightLine.overridefunctouchesMoved(_touches:Set<UITouch>,withevent:UIEvent?){
lettouch=touches.first!
letlocation=touch.location(in:self)
currentLine?.end=location
//Logstatementtoseetheorderofevents
print(#function)
fortouchintouches{
letkey=NSValue(nonretainedObject:touch)
currentLines[key]?.end=touch.location(in:self)
}
setNeedsDisplay()
}
Now,updatetouchesEnded(_:with:)tomoveanyfinishedlinesintothefinishedLinesarray.
overridefunctouchesEnded(_touches:Set<UITouch>,withevent:UIEvent?){
ifvarline=currentLine{
lettouch=touches.first!
letlocation=touch.location(in:self)
line.end=location
finishedLines.append(line)
}
currentLine=nil
//Logstatementtoseetheorderofevents
print(#function)
fortouchintouches{
letkey=NSValue(nonretainedObject:touch)
ifvarline=currentLines[key]{
line.end=touch.location(in:self)
finishedLines.append(line)
currentLines.removeValue(forKey:key)
}
}
setNeedsDisplay()
}
Finally,updatedraw(_:)todraweachlineincurrentLines.overridefuncdraw(_rect:CGRect){
//Drawfinishedlinesinblack
UIColor.black.setStroke()
forlineinfinishedLines{
stroke(line)
}
ifletline=currentLine{
//Ifthereisalinecurrentlybeingdrawn,doitinred
UIColor.red.setStroke()
stroke(line)
}
//Drawcurrentlinesinred
UIColor.red.setStroke()
for(_,line)incurrentLines{
stroke(line)
}
}
Buildandruntheapplicationandstartdrawinglineswithmultiplefingers.(Onthesimulator,holddowntheOptionkeywhileyoudragtosimulatemultiplefingers.)Noticetheorderingofthelogmessagesintheconsole.YoushouldknowthatwhenaUIRespondermethodliketouchesMoved(_:with:)iscalledonaview,onlythetouchesthathavemovedwillbeinthesetoftouches.Thus,itispossibleforthreetouchestobeonaview,butonlyonetouchtobeinthesetoftouchespassedintooneofthesemethods.Additionally,onceaUITouchbeginsonaview,alltoucheventmethodsarecalledonthatsameviewoverthetouch’slifetime,evenifthattouchmovesoffoftheviewitbeganon.
ThelastthingleftforthebasicsofTouchTrackeristohandlewhathappenswhenatouchiscanceled.AtouchcanbecanceledwhentheapplicationisinterruptedbytheOS(forexample,whenaphonecallcomesin)whileatouchiscurrentlyonthescreen.Whenatouchiscanceled,anystateitsetupshouldbereverted.Inthiscase,youshouldremoveanylinesinprogress.InDrawView.swift,implementtouchesCancelled(_:with:).overridefunctouchesCancelled(_touches:Set<UITouch>,withevent:UIEvent?){
//Logstatementtoseetheorderofevents
print(#function)
currentLines.removeAll()
setNeedsDisplay()
}
@IBInspectable
WhenworkinginInterfaceBuilder,youareabletomodifyattributesfortheviewsthatyouaddtothecanvas.Forexample,youcansetthebackgroundcoloronaview,thetextonalabel,andthecurrentprogressonaslider.YoucanaddthissamebehaviortoyourowncustomUIViewsubclassesforcertaintypes.Let’saddintheabilityfortheDrawView’scurrentlinecolor,finishedlinecolor,andlinethicknesstobecustomizedthroughInterfaceBuilder.InDrawView.swift,declarethreepropertiestoreferencethesevalues.Givethemdefaultvaluesandhavetheviewflagitselfforredrawingwheneverthesepropertieschange.varcurrentLines=[NSValue:Line]()
varfinishedLines=[Line]()
@IBInspectablevarfinishedLineColor:UIColor=UIColor.black{
didSet{
setNeedsDisplay()
}
}
@IBInspectablevarcurrentLineColor:UIColor=UIColor.red{
didSet{
setNeedsDisplay()
}
}
@IBInspectablevarlineThickness:CGFloat=10{
didSet{
setNeedsDisplay()
}
}
The@IBInspectablekeywordletsInterfaceBuilderknowthatthisisapropertythatyouwanttocustomizethroughtheattributesinspector.Manyofthecommontypesaresupportedby@IBInspectable:Booleans,strings,numbers,CGPoint,CGSize,CGRect,UIColor,UIImage,andafewmoreareallcandidates.Nowupdatestroke(_:)anddrawView(_:)tousethesenewproperties.funcstroke(_line:Line){
letpath=UIBezierPath()
path.lineWidth=10
path.lineWidth=lineThickness
path.lineCapStyle=.round
path.move(to:line.begin)
path.addLine(to:line.end)
path.stroke()
}
overridefuncdraw(_rect:CGRect){
//Drawfinishedlinesinblack
UIColor.black.setStroke()
finishedLineColor.setStroke()
forlineinfinishedLines{
stroke(line)
}
//Drawcurrentlinesinred
UIColor.red.setStroke()
currentLineColor.setStroke()
for(_,line)incurrentLines{
stroke(line)
}
}
Now,whenyouaddaDrawViewtothecanvasinInterfaceBuilder,youcancustomizethesethreepropertiesintheattributesinspectortobedifferentfordifferentinstances(Figure18.7).Figure18.7CustomizingDrawView
SilverChallenge:Colors
MakeitsothattheangleatwhichalineisdrawndictatesitscoloronceithasbeenaddedtocurrentLines.
GoldChallenge:Circles
Usetwofingerstodrawcircles.Tryhavingeachfingerrepresentonecorneroftheboundingboxaroundthecircle.RecallthatyoucansimulatetwofingersonthesimulatorbyholdingdowntheOptionkey.(Hint:Thisismucheasierifyoutracktouchesthatareworkingonacircleinaseparatedictionary.)
FortheMoreCurious:TheResponderChain
InChapter14,wetalkedbrieflyaboutthefirstresponder.AUIRespondercanbeafirstresponderandreceivetouchevents.UIViewisoneexampleofaUIRespondersubclass,buttherearemanyothers,includingUIViewController,UIApplication,andUIWindow.Youareprobablythinking,“Butyoucan’ttouchaUIViewController.It’snotanonscreenobject.”Youareright–youcannotsendatoucheventdirectlytoaUIViewController,butviewcontrollerscanreceiveeventsthroughtheresponderchain.EveryUIRespondercanreferenceanotherUIResponderthroughitsnextproperty,andtogethertheseobjectsmakeuptheresponderchain(Figure18.8).Atoucheventstartsattheviewthatwastouched.ThenextresponderofaviewistypicallyitsUIViewController(ifithasone)oritssuperview(ifitdoesnot).Thenextresponderofaviewcontrolleristypicallyitsview’ssuperview.Thetop-mostsuperviewisthewindow.Thewindow’snextresponderisthesingletoninstanceofUIApplication.Figure18.8Responderchain
HowdoesaUIRespondernothandleanevent?Itcallsthesamemethodonitsnextresponder.ThatiswhatthedefaultimplementationofmethodsliketouchesBegan(_:with:)does.Soifamethodisnotoverridden,itsnextresponderwillattempttohandlethetouchevent.Iftheapplication(thelastobjectintheresponderchain)doesnothandletheevent,thenitisdiscarded.Youcanexplicitlycallamethodonanextresponder,too.Let’ssaythereisaviewthattrackstouches,butifadouble-tapoccurs,itsnextrespondershouldhandleit.Thecodewouldlooklikethis:overridefunctouchesBegan(_touches:Set<UITouch>,withevent:UIEvent?){
lettouch=touches.first!
iftouch.tapCount==2{
next?.touchesBegan(touches,with:event)
}else{
//Goontohandletouchesthatarenotdouble-taps
}
}
FortheMoreCurious:UIControl
TheclassUIControlisthesuperclassforseveralclassesinCocoaTouch,includingUIButtonandUISlider.Youhaveseenhowtosetthetargetsandactionsforthesecontrols.NowwecantakeacloserlookathowUIControloverridesthesameUIRespondermethodsyouimplementedinthischapter.InUIControl,eachpossiblecontroleventisassociatedwithaconstant.Buttons,forexample,typicallysendactionmessagesontheUIControlEvents.touchUpInsidecontrolevent.Atargetregisteredforthiscontroleventwillonlyreceiveitsactionmessageiftheusertouchesthecontrolandthenliftsthefingeroffthescreeninsidetheframeofthecontrol.Forabutton,however,youcanhaveactionsonothereventtypes.Forexample,youmighttriggeramethodiftheuserremovesthefingerinsideoroutsidetheframe.Assigningthetargetandactionprogrammaticallywouldlooklikethis:button.addTarget(self,
action:#selector(Thermostat.resetTemperature(_:)),
for:[.touchUpInside,.touchUpOutside])
NowconsiderhowUIControlhandlesUIControlEvents.touchUpInside.//Nottheexactcode.Thereisabitmoregoingon!
overridefunctouchesEnded(_touches:Set<UITouch>,withevent:UIEvent?){
//Referencetothetouchthatisending
lettouch=touches.first!
//Locationofthatpointinthiscontrol'scoordinatesystem
lettouchLocation=touch.location(in:self)
//Isthatpointstillinmyviewingbounds?
ifbounds.contains(touchLocation){
//Sendoutactionmessagestoalltargetsregisteredforthisevent!
sendActions(for:.touchUpInside)
}
else{
//Thetouchendedoutsidethebounds:differentcontrolevent
sendActions(for:.touchUpOutside)
}
}
Sohowdotheseactionsgetsenttotherighttarget?AttheendoftheUIRespondermethodimplementations,thecontrolcallsthemethodsendActions(for:)onitself.Thismethodlooksatallofthetarget-actionpairsthecontrolhas.Ifanyofthemareregisteredforthecontroleventpassedastheargument,thecorrespondingactionmethodiscalledonthosetargets.However,acontrolnevercallsamethoddirectlyonitstargets.Instead,itroutesthesemethodcallsthroughtheUIApplicationobject.Whynothavecontrolscalltheactionmethodsdirectlyonthetargets?Controlscanalsohavenil-targetedactions.IfaUIControl’stargetisnil,theUIApplicationfindsthefirstresponderofitsUIWindowandcallstheactionmethodonit.
19UIGestureRecognizerandUIMenuController
InChapter18,youhandledrawtouchesbyimplementingmethodsfromUIResponder.Sometimesyouwanttodetectaspecificpatternoftouchesthatmakeagesture,likeapinchoraswipe.Insteadofwritingcodetodetectcommongesturesyourself,youcanuseinstancesofUIGestureRecognizer.AUIGestureRecognizerinterceptstouchesthatareontheirwaytobeinghandledbyaview.Whenitrecognizesaparticulargesture,itcallsamethodontheobjectofyourchoice.ThereareseveraltypesofgesturerecognizersbuiltintotheSDK.Inthischapter,youwillusethreeofthemtoallowTouchTrackeruserstoselect,move,anddeletelines(Figure19.1).YouwillalsoseehowtouseanotherinterestingiOSclass,UIMenuController.Figure19.1TouchTrackerbytheendofthechapter
UIGestureRecognizerSubclasses
YoudonotinstantiateUIGestureRecognizeritself.Instead,thereareanumberofsubclassesofUIGestureRecognizer,andeachoneisresponsibleforrecognizingaparticulargesture.TouseaninstanceofaUIGestureRecognizersubclass,yougiveitatarget-actionpairandattachittoaview.Wheneverthegesturerecognizerrecognizesitsgestureontheview,itwillsendtheactionmessagetoitstarget.AllUIGestureRecognizeractionmessageshavethesameform:funcaction(_gestureRecognizer:UIGestureRecognizer){}
Whenrecognizingagesture,thegesturerecognizerinterceptsthetouchesdestinedfortheview(Figure19.2).Thus,thetypicalUIRespondermethodsliketouchesBegan(_:with:)maynotbecalledonaviewwithgesturerecognizers.Figure19.2Gesturerecognizersintercepttouches
DetectingTapswithUITapGestureRecognizer
ThefirstUIGestureRecognizersubclassyouwilluseisUITapGestureRecognizer.Whentheusertapsthescreentwice,allofthelinesonthescreenwillbecleared.OpenTouchTracker.xcodeprojandDrawView.swift.Addaninit?(coder:)methodandinstantiateaUITapGestureRecognizerthatrequirestwotapstofireandcallstheactionmethodonitstarget.requiredinit?(coderaDecoder:NSCoder){
super.init(coder:aDecoder)
letdoubleTapRecognizer=UITapGestureRecognizer(target:self,
action:#selector(DrawView.doubleTap(_:)))
doubleTapRecognizer.numberOfTapsRequired=2
addGestureRecognizer(doubleTapRecognizer)
}
Nowwhenadouble-tapoccursonaninstanceofDrawView,themethoddoubleTap(_:)willbecalledonthatinstance.ImplementthismethodinDrawView.swift.funcdoubleTap(_gestureRecognizer:UIGestureRecognizer){
print("Recognizedadoubletap")
currentLines.removeAll()
finishedLines.removeAll()
setNeedsDisplay()
}
NoticethattheargumenttotheactionmethodforagesturerecognizeristheinstanceofUIGestureRecognizerthatcalledthemethod.Inthecaseofadouble-tap,youdonotneedanyinformationfromtherecognizer,butyouwillneedinformationfromtheotherrecognizersyouinstalllaterinthechapter.Buildandruntheapplication,drawafewlines,anddouble-tapthescreentoclearthem.Youmayhavenoticed(especiallyonthesimulator)thatthefirsttapofadouble-tapresultsinasmallreddotbeingdrawn.ThisdotappearsbecausetouchesBegan(_:with:)iscalledontheDrawViewonthefirsttap,creatingaveryshortline.Checktheconsoleandyouwillseethefollowingsequenceofevents:touchesBegan(_:with:)
Recognizedadoubletap
touchesCancelled(_:with:)
Gesturerecognizersworkbyinspectingtoucheventstodeterminewhethertheirparticulargesturehasoccurred.Beforeagestureisrecognized,thegesturerecognizerinterceptsalltheUIRespondermethodcalls.Ifithasnotrecognizeditsgesture,eachcallisforwardedontotheview.Recognizingataprequiresthatatouchbeginandend.ThismeansthattheUITapGestureRecognizercannotknowwhetherthetouchisatapwhentouchesBegan(_:with:)isoriginallycalled,sothemethodiscalledontheviewaswell.Whenthetouchends,thetapisrecognizedandthegesturerecognizerclaimsthetouchforitself.ItdoessobycallingtouchesCancelled(_:with:)ontheview.Afterthat,no
moreUIRespondermethodswillbecalledontheviewforthatparticulartouch.Topreventthisreddotfromappearingtemporarily,youmustpreventtouchesBegan(_:with:)frombeingcalledontheview.YoucantellaUIGestureRecognizertodelaycallingtouchesBegan(_:with:)onitsviewifitisstillpossiblethatitsgesturemightberecognizedforthattouch.InDrawView.swift,modifyinit?(coder:)todojustthis.requiredinit?(coderaDecoder:NSCoder){
super.init(coder:aDecoder)
letdoubleTapRecognizer=UITapGestureRecognizer(target:self,
action:#selector(DrawView.doubleTap(_:)))
doubleTapRecognizer.numberOfTapsRequired=2
doubleTapRecognizer.delaysTouchesBegan=true
addGestureRecognizer(doubleTapRecognizer)
}
Buildandruntheapplication,drawsomelines,andthendouble-taptoclearthem.Youwillnolongerseethereddotwhiledouble-tapping.
MultipleGestureRecognizers
Thenextstepistoaddanothergesturerecognizerthatallowstheusertoselectaline.(Later,auserwillbeabletodeletetheselectedline.)YouwillinstallanotherUITapGestureRecognizerontheDrawViewthatonlyrequiresonetap.InDrawView.swift,modifyinit?(coder:)toaddthisgesturerecognizer.requiredinit?(coderaDecoder:NSCoder){
super.init(coder:aDecoder)
letdoubleTapRecognizer=UITapGestureRecognizer(target:self,
action:#selector(DrawView.doubleTap(_:)))
doubleTapRecognizer.numberOfTapsRequired=2
doubleTapRecognizer.delaysTouchesBegan=true
addGestureRecognizer(doubleTapRecognizer)
lettapRecognizer=
UITapGestureRecognizer(target:self,action:#selector(DrawView.tap(_:)))
tapRecognizer.delaysTouchesBegan=true
addGestureRecognizer(tapRecognizer)
}
Now,implementtap(_:)inDrawView.swifttologthetaptotheconsole.functap(_gestureRecognizer:UIGestureRecognizer){
print("Recognizedatap")
}
Buildandruntheapplication.Trytappinganddouble-tapping.Tappingoncelogstheappropriatemessagetotheconsole.Double-tapping,however,triggersbothtap(_:)anddoubleTap(_:).Insituationswhereyouhavemultiplegesturerecognizers,itisnotuncommontohaveonegesturerecognizerfireandclaimatouchwhenyoureallywantedanothergesturerecognizertohandleit.Inthesecases,yousetupdependenciesbetweenrecognizersthatsay,“Waitamomentbeforeyoufire,becausethistouchmightbemine!”Ininit?(coder:),makeitsothetapRecognizerwaitsuntilthedoubleTapRecognizerfailstorecognizeadouble-tapbeforeclaimingthesingletapforitself.requiredinit?(coderaDecoder:NSCoder){
super.init(coder:aDecoder)
letdoubleTapRecognizer=UITapGestureRecognizer(target:self,
action:#selector(DrawView.doubleTap(_:)))
doubleTapRecognizer.numberOfTapsRequired=2
doubleTapRecognizer.delaysTouchesBegan=true
addGestureRecognizer(doubleTapRecognizer)
lettapRecognizer=
UITapGestureRecognizer(target:self,action:#selector(DrawView.tap(_:)))
tapRecognizer.delaysTouchesBegan=true
tapRecognizer.require(toFail:doubleTapRecognizer)
addGestureRecognizer(tapRecognizer)
}
Buildandruntheapplicationagainandtryoutsometaps.Asingletapnowtakesasmallamountoftimetofireafterthetapoccurs,butdouble-tappingnolongertriggersthetap(_:)message.Next,let’sbuildontheDrawViewsothattheusercanselectalinewhenitistapped.First,addapropertyatthetopofDrawView.swifttoholdontotheindexofaselectedline.classDrawView:UIView{
varcurrentLines=[NSValue:Line]()
varfinishedLines=[Line]()
varselectedLineIndex:Int?
Nowmodifydraw(_:)todrawtheselectedlineingreen.overridefuncdraw(_rect:CGRect){
finishedLineColor.setStroke()
forlineinfinishedLines{
stroke(line)
}
currentLineColor.setStroke()
for(_,line)incurrentLines{
stroke(line)
}
ifletindex=selectedLineIndex{
UIColor.green.setStroke()
letselectedLine=finishedLines[index]
stroke(selectedLine)
}
}
StillinDrawView.swift,addanindexOfLine(at:)methodthatreturnstheindexoftheLineclosesttoagivenpoint.funcindexOfLine(atpoint:CGPoint)->Int?{
//Findalineclosetopoint
for(index,line)infinishedLines.enumerated(){
letbegin=line.begin
letend=line.end
//Checkafewpointsontheline
fortinstride(from:CGFloat(0),to:1.0,by:0.05){
letx=begin.x+((end.x-begin.x)*t)
lety=begin.y+((end.y-begin.y)*t)
//Ifthetappedpointiswithin20points,let'sreturnthisline
ifhypot(x-point.x,y-point.y)<20.0{
returnindex
}
}
}
//Ifnothingiscloseenoughtothetappedpoint,thenwedidnotselectaline
returnnil
}
Thestride(from:to:by:)methodwillallowttostartatthefromvalueandgoupto(butnotreach)thetovalue,incrementingthevalueoftbythebyvalue.
Thereareother,betterwaystodeterminetheclosestlinetoapoint,butthissimpleimplementationwillworkforyourpurposes.Thepointtobepassedinisthepointwherethetapoccurred.Youcaneasilygetthisinformation.EveryUIGestureRecognizerhasalocation(in:)method.Callingthismethodonthegesturerecognizerwillgiveyouthecoordinatewherethegestureoccurredinthecoordinatesystemoftheviewthatispassedastheargument.InDrawView.swift,updatetap(_:)tocalllocation(in:)onthegesturerecognizer,passtheresulttoindexOfLine(at:),andmakethereturnedindextheselectedLineIndex.functap(_gestureRecognizer:UIGestureRecognizer){
print("Recognizedatap")
letpoint=gestureRecognizer.location(in:self)
selectedLineIndex=indexOfLine(at:point)
setNeedsDisplay()
}
Iftheuserdouble-tapstoclearalllineswhilealineisselected,theapplicationwilltrap.Toaddressthis,updatedoubleTap(_:)tosettheselectedLineIndextonil.funcdoubleTap(_gestureRecognizer:UIGestureRecognizer){
print("Recognizedadoubletap")
selectedLineIndex=nil
currentLines.removeAll()
finishedLines.removeAll()
setNeedsDisplay()
}
Buildandruntheapplication.Drawafewlinesandthentapone.Thetappedlineshouldappearingreen(rememberthatittakesamomentbeforethetapisknownnottobeadouble-tap).
UIMenuController
Nextyouaregoingtomakeitsothatwhentheuserselectsaline,amenuwiththeoptiontodeletethatlineappearswheretheusertapped.Thereisabuilt-inclassforprovidingthissortofmenucalledUIMenuController(Figure19.3).AmenucontrollerhasalistofUIMenuItemobjectsandispresentedinanexistingview.Eachitemhasatitle(whatshowsupinthemenu)andanaction(themessageitsendsthefirstresponderofthewindow).Figure19.3AUIMenuController
ThereisonlyoneUIMenuControllerperapplication.Whenyouwishtopresentthisinstance,youfillitwithmenuitems,giveitarectangletopresentfrom,andsetittobevisible.DothisinDrawView.swift’stap(_:)methodiftheuserhastappedonaline.Iftheusertappedsomewherethatisnotnearaline,thecurrentlyselectedlinewillbedeselectedandthemenucontrollerwillhide.functap(_gestureRecognizer:UIGestureRecognizer){
print("Recognizedatap")
letpoint=gestureRecognizer.location(in:self)
selectedLineIndex=indexOfLine(at:point)
//Grabthemenucontroller
letmenu=UIMenuController.shared
ifselectedLineIndex!=nil{
//MakeDrawViewthetargetofmenuitemactionmessages
becomeFirstResponder()
//Createanew"Delete"UIMenuItem
letdeleteItem=UIMenuItem(title:"Delete",
action:#selector(DrawView.deleteLine(_:)))
menu.menuItems=[deleteItem]
//Tellthemenuwhereitshouldcomefromandshowit
lettargetRect=CGRect(x:point.x,y:point.y,width:2,height:2)
menu.setTargetRect(targetRect,in:self)
menu.setMenuVisible(true,animated:true)
}else{
//Hidethemenuifnolineisselected
menu.setMenuVisible(false,animated:true)
}
setNeedsDisplay()
}
Foramenucontrollertoappear,aviewthatrespondstoatleastoneactionmessageintheUIMenuController’smenuitemsmustbethefirstresponderofthewindow–thisiswhyyoucalledthemethodbecomeFirstResponder()ontheDrawViewbeforesettingup
themenucontroller.Ifyouhaveacustomviewclassthatneedstobecomethefirstresponder,youmustalsooverridecanBecomeFirstResponder.InDrawView.swift,overridethispropertytoreturntrue.overridevarcanBecomeFirstResponder:Bool{
returntrue
}
Finally,implementdeleteLine(_:)inDrawView.swift.funcdeleteLine(_sender:UIMenuController){
//RemovetheselectedlinefromthelistoffinishedLines
ifletindex=selectedLineIndex{
finishedLines.remove(at:index)
selectedLineIndex=nil
//Redraweverything
setNeedsDisplay()
}
}
Whenbeingpresented,themenucontrollergoesthrougheachmenuitemandasksthefirstresponderifitimplementstheactionmethodforthatitem.Ifthefirstresponderdoesnotimplementthatmethod,thenthemenucontrollerwillnotshowtheassociatedmenuitem.Ifnomenuitemshavetheiractionmethodsimplementedbythefirstresponder,themenuisnotshownatall.Buildandruntheapplication.Drawaline,taponit,andthenselectDeletefromthemenuitem.Ifyouselectalineandthendouble-taptoclearalllines,themenucontrollerwillstillbevisible.IftheselectedLineIndexeverbecomesnil,themenucontrollershouldnotbevisible.AddapropertyobservertoselectedLineIndexinDrawView.swiftthatsetsthemenucontrollertobenotvisibleiftheindexissettonil.varselectedLineIndex:Int?{
didSet{
ifselectedLineIndex==nil{
letmenu=UIMenuController.shared
menu.setMenuVisible(false,animated:true)
}
}
}
Buildandruntheapplication.Drawaline,selectit,andthendouble-tapthebackground.Thelineandthemenucontrollerwillnolongerbevisible.
MoreGestureRecognizers
Inthissection,youaregoingtoaddtheabilityforausertoselectalinebypressingandholding(alongpress)andthenmovetheselectedlinebydraggingthefinger(apan).ThiswillrequiretwomoresubclassesofUIGestureRecognizer:UILongPressGestureRecognizerandUIPanGestureRecognizer.
UILongPressGestureRecognizer
InDrawView.swift,instantiateaUILongPressGestureRecognizerininit?(coder:)andaddittotheDrawView....
addGestureRecognizer(tapRecognizer)
letlongPressRecognizer=UILongPressGestureRecognizer(target:self,
action:#selector(DrawView.longPress(_:)))
addGestureRecognizer(longPressRecognizer)
}
NowwhentheuserholdsdownontheDrawView,themethodlongPress(_:)willbecalledonit.Bydefault,atouchmustbeheld0.5secondstobecomealongpress,butyoucanchangetheminimumPressDurationofthegesturerecognizerifyoulike.Sofar,youhaveworkedwithtapgestures.Atapisadiscretegesture.Bythetimeitisrecognized,thegestureisover,andtheactionmessagehasbeendelivered.Alongpress,ontheotherhand,isacontinuousgesture.Continuousgesturesoccurovertime.Tokeeptrackofwhatisgoingonwithacontinuousgesture,youcancheckarecognizer ’sstateproperty.Forexample,consideratypicallongpress:
Whentheusertouchesaview,thelong-pressrecognizernoticesapossiblelongpress,butitmustwaittoseewhetherthetouchisheldlongenoughtobecomealong-pressgesture.Therecognizer ’sstateisUIGestureRecognizerState.possible.Oncetheuserholdsthetouchlongenough,thelongpressisrecognizedandthegesturehasbegun.Therecognizer ’sstateisUIGestureRecognizerState.began.Whentheuserremovesthefinger,thegesturehasended.Therecognizer ’sstateisUIGestureRecognizerState.ended.
Whenthelong-pressgesturerecognizertransitionsfrompossibletobeganandfrombegantoended,itsendsitsactionmessagetoitstarget.Todeterminewhichtransitiontriggeredtheaction,youcheckthegesturerecognizer ’sstate.Rememberthatthelongpressispartofalargerfeature.Inthenextsection,youwillenabletheusertomovetheselectedlinebydraggingitwiththesamefingerthatbeganthelongpress.SohereistheplanforimplementingthelongPress(_:)actionmethod:Whentherecognizerisinthebeganstate,youwillselecttheclosestlinetowherethegestureoccurred.Whentherecognizerisintheendedstate,youwilldeselecttheline.
InDrawView.swift,implementlongPress(_:).funclongPress(_gestureRecognizer:UIGestureRecognizer){
print("Recognizedalongpress")
ifgestureRecognizer.state==.began{
letpoint=gestureRecognizer.location(in:self)
selectedLineIndex=indexOfLine(at:point)
ifselectedLineIndex!=nil{
currentLines.removeAll()
}
}elseifgestureRecognizer.state==.ended{
selectedLineIndex=nil
}
setNeedsDisplay()
}
Buildandruntheapplication.Drawalineandthenpressandholdit;thelinewillturngreenandbecometheselectedline.Whenyouletgo,thelinewillreverttoitsformercolorandwillnolongerbetheselectedline.
UIPanGestureRecognizerandsimultaneousrecognizers
InDrawView.swift,declareaUIPanGestureRecognizerasapropertysothatyouhaveaccesstoitinallofyourmethods.classDrawView:UIView{
varcurrentLines=[NSValue:Line]()
varfinishedLines=[Line]()
varselectedLineIndex:Int?{
...
}
varmoveRecognizer:UIPanGestureRecognizer!
Next,inDrawView.swift,addcodetoinit?(coder:)toinstantiateaUIPanGestureRecognizer,setoneofitsproperties,andaddittotheDrawView.letlongPressRecognizer=UILongPressGestureRecognizer(target:self,
action:#selector(DrawView.longPress(_:)))
addGestureRecognizer(longPressRecognizer)
moveRecognizer=UIPanGestureRecognizer(target:self,
action:#selector(DrawView.moveLine(_:)))
moveRecognizer.cancelsTouchesInView=false
addGestureRecognizer(moveRecognizer)
}
WhatiscancelsTouchesInView?EveryUIGestureRecognizerhasthisproperty,whichdefaultstotrue.WhencancelsTouchesInViewistrue,thegesturerecognizerwill“eat”anytouchitrecognizes,andtheviewwillnotgetachancetohandlethetouchviathetraditionalUIRespondermethods,liketouchesBegan(_:with:).Usually,thisiswhatyouwant,butnotalways.Inthiscase,ifthepangesturerecognizerwere
toeatitstouches,thenuserswouldnotbeabletodrawlines.WhenyousetcancelsTouchesInViewtofalse,youensurethatanytouchrecognizedbythegesturerecognizerwillalsobedeliveredtotheviewviatheUIRespondermethods.InDrawView.swift,addasimpleimplementationfortheactionmethod:funcmoveLine(_gestureRecognizer:UIPanGestureRecognizer){
print("Recognizedapan")
}
Buildandruntheappanddrawsomelines.BecausecancelsTouchesInViewisfalse,thepangestureisrecognized,butlinescanstillbedrawn.YoucancommentoutthelinethatsetscancelsTouchesInViewandrunagaintoseethedifference.Soon,youwillupdatemoveLine(_:)toredrawtheselectedlineastheuser ’sfingermovesacrossthescreen.Butfirstyouneedtwogesturerecognizerstobeabletohandlethesametouch.Normally,whenagesturerecognizerrecognizesitsgesture,iteatsitandnootherrecognizergetsachancetohandlethattouch.Tryit:Runtheapp,drawaline,pressandholdtoselecttheline,andthenmoveyourfingeraround.Theconsolereportsthelongpressbutnotthepan.Inthiscase,thedefaultbehaviorisproblematic:Youruserswillpressandholdtoselectalineandthenpantomovetheline–withoutliftingthefingerinbetween.Thus,thetwogestureswilloccursimultaneously,andthepangesturerecognizermustbeallowedtorecognizeapaneventhoughthelong-pressgesturehasalreadyrecognizedalongpress.Toallowagesturerecognizertorecognizeitsgesturesimultaneouslywithanothergesturerecognizer,youimplementamethodfromtheUIGestureRecognizerDelegateprotocol:optionalfuncgestureRecognizer(_gestureRecognizer:UIGestureRecognizer,
shouldRecognizeSimultaneouslyWith
otherGestureRecognizer:UIGestureRecognizer)->Bool
Thefirstparameteristhegesturerecognizerthatisaskingforguidance.Itsaystoitsdelegate,“Sothere’smeandthisotherrecognizer,andoneofusjustrecognizedagesture.Shouldtheonewhodidnotrecognizeitstayinthepossiblestateandcontinuetotrackthistouch?”Notethatthecallitselfdoesnottellyouwhichofthetworecognizershasrecognizeditsgesture–and,thus,whichofthemwillpotentiallybedeprivedofthechancetorecognizeitsgesture.Bydefault,themethodreturnsfalse,andthegesturerecognizerstillinthepossiblestateleavesthetouchinthehandsofthegesturealreadyintherecognizedstate.Youcanimplementthemethodtoreturntruetoallowbothrecognizerstorecognizetheirgesturesinthesametouch.(Ifyouneedtodeterminewhichofthetworecognizershasrecognizeditsgesture,youcanchecktherecognizers’stateproperties.)Toenablepanningwhilelongpressing,youaregoingtogivethepangesturerecognizeradelegate(theDrawView).Then,whenthelong-pressrecognizerrecognizesitsgesture,thepangesturerecognizerwillcallthesimultaneousrecognitionmethodonitsdelegate.YouwillimplementthismethodinDrawViewtoreturntrue.Thiswillallowthepangesturerecognizertorecognizeanypanningthatoccurswhilealongpressisinprogress.
First,inDrawView.swift,declarethatDrawViewconformstotheUIGestureRecognizerDelegateprotocol.classDrawView:UIView,UIGestureRecognizerDelegate{
varcurrentLines=[NSValue:Line]()
varfinishedLines=[Line]()
varselectedLineIndex:Int?{
...
}
varmoveRecognizer:UIPanGestureRecognizer!
Next,ininit?(coder:),settheDrawViewtobethedelegateoftheUIPanGestureRecognizer.letlongPressRecognizer=UILongPressGestureRecognizer(target:self,
action:#selector(DrawView.longPress(_:)))
addGestureRecognizer(longPressRecognizer)
moveRecognizer=UIPanGestureRecognizer(target:self,
action:#selector(DrawView.moveLine(_:)))
moveRecognizer.delegate=self
moveRecognizer.cancelsTouchesInView=false
addGestureRecognizer(moveRecognizer)
}
Finally,inDrawView.swift,implementthedelegatemethodtoreturntrue.funcgestureRecognizer(_gestureRecognizer:UIGestureRecognizer,
shouldRecognizeSimultaneouslyWith
otherGestureRecognizer:UIGestureRecognizer)->Bool{
returntrue
}
Forthissituation,whereonlyyourpangesturerecognizerhasadelegate,thereisnoneedtodomorethanreturntrue.Inmorecomplicatedscenarios,youwouldusethepassed-ingesturerecognizerstomorecarefullycontrolsimultaneousrecognition.Now,whenalongpressbegins,theUIPanGestureRecognizerwillcontinuetokeeptrackofthetouch,andiftheuser ’sfingerbeginstomove,thepanrecognizerwillrecognizethepan.Toseethedifference,runtheapp,drawaline,selectit,andthenpan.Theconsolewillreportbothgestures.(TheUIGestureRecognizerDelegateprotocolincludesothermethodstohelpyoutweakthebehaviorofyourgesturerecognizers.Visittheprotocolreferencepageformoreinformation.)Inadditiontothestatesyouhavealreadyseen,apangesturerecognizersupportsthechangedstate.Whenafingerstartstomove,thepanrecognizerentersthebeganstateandcallsamethodonitstarget.Whilethefingermovesaroundthescreen,therecognizertransitionstothechangedstateandcallstheactionmethodonitstargetrepeatedly.Whenthefingerleavesthescreen,therecognizer ’sstateissettoended,andthemethodiscalledonthetargetforthefinaltime.ThenextstepistoimplementthemoveLine(_:)methodthatthepanrecognizercallsonitstarget.Inthisimplementation,youwillcallthemethodtranslationInView(_:)onthe
panrecognizer.ThisUIPanGestureRecognizermethodreturnshowfarthepanhasmovedasaCGPointinthecoordinatesystemoftheviewpassedastheargument.Whenthepangesturebegins,thispropertyissettothezeropoint(wherexandyare0).Asthepanmoves,thisvalueisupdated–ifthepangoesfartotheright,ithasahighxvalue;ifthepanreturnstowhereitbegan,itstranslationgoesbacktothezeropoint.InDrawView.swift,implementmoveLine(_:).NoticethatbecauseyouwillsendthegesturerecognizeramethodfromtheUIPanGestureRecognizerclass,theparameterofthismethodmustbeareferencetoaninstanceofUIPanGestureRecognizerratherthanUIGestureRecognizer.funcmoveLine(_gestureRecognizer:UIPanGestureRecognizer){
print("Recognizedapan")
//Ifalineisselected...
ifletindex=selectedLineIndex{
//Whenthepanrecognizerchangesitsposition...
ifgestureRecognizer.state==.changed{
//Howfarhasthepanmoved?
lettranslation=gestureRecognizer.translation(in:self)
//Addthetranslationtothecurrentbeginningandendpointsoftheline
//Makesuretherearenocopyandpastetypos!
finishedLines[index].begin.x+=translation.x
finishedLines[index].begin.y+=translation.y
finishedLines[index].end.x+=translation.x
finishedLines[index].end.y+=translation.y
//Redrawthescreen
setNeedsDisplay()
}
}else{
//Ifnolineisselected,donotdoanything
return
}
}
Buildandruntheapplication.Touchandholdonalineandbegindragging–andyouwillimmediatelynoticethatthelineandyourfingerarewayoutofsync.Whatisgoingon?Youareaddingthecurrenttranslationoverandoveragaintotheline’soriginalendpoints.Youreallyneedthegesturerecognizertoreportthechangeintranslationsincethelasttimethismethodwascalledinstead.Fortunately,youcandothis.Youcansetthetranslationofapangesturerecognizerbacktothezeropointeverytimeitreportsachange.Then,thenexttimeitreportsachange,itwillhavethetranslationsincethelastevent.NearthebottomofmoveLine(_:)inDrawView.swift,addthefollowinglineofcode.finishedLines[index].end.x+=translation.x
finishedLines[index].end.y+=translation.y
gestureRecognizer.setTranslation(CGPoint.zero,in:self)
//Redrawthescreen
setNeedsDisplay()
Buildandruntheapplicationandmovealinearound.Worksgreat!
MoreonUIGestureRecognizer
YouhaveonlyscratchedthesurfaceofUIGestureRecognizer.Therearemoresubclasses,moreproperties,andmoredelegatemethods–andyoucanevencreaterecognizersofyourown.ThissectionwillgiveyouanideaofwhatUIGestureRecognizeriscapableof.Youcanstudythedocumentationtolearnevenmore.Whenagesturerecognizerisonaview,itisreallyhandlingalloftheUIRespondermethods,liketouchesBegan(_:with:),foryou.Gesturerecognizersareprettygreedy,sotheytypicallydonotletaviewreceivetouchevents,ortheyatleastdelaythedeliveryofthoseevents.Youcansetpropertiesontherecognizer,likedelaysTouchesBegan,delaysTouchesEnded,andcancelsTouchesInView,tochangethisbehavior.Ifyouneedfinercontrolthanthisall-or-nothingapproach,youcanimplementdelegatemethodsfortherecognizer.Attimes,youmayhavetwogesturerecognizerslookingforverysimilargestures.Youcanchainrecognizerstogethersothatoneisrequiredtofailforthenextonetostartusingthemethodrequire(toFail:).Youusedthismethodininit?(coder:)tomakethetaprecognizerwaitforthedouble-taprecognizertofail.Onethingyoumustunderstandtomastergesturerecognizersishowtheyinterprettheirstate.Overall,therearesevenstatesarecognizercanenter:
UIGestureRecognizerState.possible
UIGestureRecognizerState.failed
UIGestureRecognizerState.began
UIGestureRecognizerState.cancelled
UIGestureRecognizerState.changed
UIGestureRecognizerState.recognized
UIGestureRecognizerState.ended
Thepossiblestateiswhererecognizersspendmostoftheirtime.Whenagesturetransitionstoanystateotherthanthepossiblestateorthefailedstate,theactionmessageoftherecognizerissentanditsstatepropertycanbecheckedtoseewhy.Thefailedstateisusedbyrecognizerswatchingforamultitouchgesture.Atsomepoint,theuser ’sfingersmayachieveapositionfromwhichtheycannolongermakethatrecognizer ’sgesture.Atthatpoint,thegesturerecognizerfails.Arecognizerentersthecanceledstatewhenitisinterrupted,suchasbyanincomingphonecall.Ifagestureiscontinuous,likeapan,thegesturerecognizerwillenterthebeganstateandthengointothechangedstateuntilthegestureends.Whenthegestureends(oriscanceled),therecognizerenterstheended(orcanceled)stateandsendsitsactionmessageafinaltimebeforereturningtothepossiblestate.
Forgesturerecognizersthatpickuponadiscretegesturelikeatap,youwillonlyseetherecognizedstate(whichhasthesamevalueastheendedstate).Thefourbuilt-inrecognizersthatyoudidnotimplementinthischapterareUIPinchGestureRecognizer,UISwipeGestureRecognizer,UIScreenEdgePanGestureRecognizer,andUIRotationGestureRecognizer.Eachhaspropertiesthatallowyoutofine-tuneitsbehavior.Thedocumentationwillshowyouhow.Finally,ifthereisagesturethatyouwanttorecognizethatisnotimplementedbythebuilt-insubclassesofUIGestureRecognizer,youcansubclassUIGestureRecognizeryourself.Thisisanintenseundertakingandoutsidethescopeofthisbook.YoucanreadtheMethodsforSubclassingsectionoftheUIGestureRecognizerdocumentationtolearnwhatisrequired.
SilverChallenge:MysteriousLines
Thereisabugintheapplication.Ifyoutaponalineandthenstartdrawinganewonewhilethemenuisvisible,youwilldragtheselectedlineanddrawanewlineatthesametime.Fixthisbug.
GoldChallenge:SpeedandSize
Piggy-backoffofthepangesturerecognizertorecordthevelocityofthepanwhenyouaredrawingaline.Adjustthethicknessofthelinebeingdrawnbasedonthisspeed.Makenoassumptionsabouthowsmallorlargethevelocityvalueofthepanrecognizercanbe.(Inotherwords,logavarietyofvelocitiestotheconsolefirst.)
PlatinumChallenge:Colors
Haveathree-fingerswipeupwardbringupapanelthatshowscolors.Selectingoneofthosecolorsshouldmakeanylinesyoudrawafterwardappearinthatcolor.Noextralinesshouldbedrawnbyputtingupthatpanel–oranylinesdrawnshouldbeimmediatelydeletedwhentheapplicationrealizesthatitisdealingwithathree-fingerswipe.
FortheMoreCurious:UIMenuControllerandUIResponderStandardEditActions
TheUIMenuControlleristypicallyresponsibleforshowingtheuseran“edit”menuwhenitisdisplayed.(Thinkofatextfieldortextviewwhenyoupressandhold.)Therefore,anunmodifiedmenucontroller(onethatyoudonotsetthemenuitemsfor)alreadyhasdefaultmenuitemsthatitpresents,likeCut,Copy,andotherfamiliaroptions.Eachitemhasanactionmessagewiredup.Forexample,cut:issenttotheviewpresentingthemenucontrollerwhentheCutmenuitemistapped.AllinstancesofUIResponderimplementthesemethods,but,bydefault,thesemethodsdonotdoanything.SubclasseslikeUITextFieldoverridethesemethodstodosomethingappropriatefortheircontext,likecutthecurrentlyselectedtext.ThemethodsarealldeclaredintheUIResponderStandardEditActionsprotocol.IfyouoverrideamethodfromUIResponderStandardEditActionsinaview,itsmenuitemwillautomaticallyappearinanymenuyoushowforthatview.ThisworksbecausethemenucontrollercallsthemethodcanPerformAction(_:withSender:)onitsview,whichreturnstrueorfalsedependingonwhethertheviewimplementsthismethod.Ifyouwanttoimplementoneofthesemethodsbutdonotwantittoappearinthemenu,youcanoverridecanPerformAction(_:withSender:)toreturnfalse:overridefunccanPerformAction(_action:Selector,
withSendersender:Any?)->Bool{
ifaction==#selector(copy(_:)){
returnfalse
}else{
//Elsereturnthedefaultbehavior
returnsuper.canPerformAction(action,withSender:sender)
}
}
20WebServices
Inthenextfourchapters,youwillcreateanapplicationnamedPhotoramathatreadsinalistofinterestingphotosfromFlickr.Thischapterwilllaythefoundationandfocusonimplementingthewebservicerequestsresponsibleforfetchingthemetadataforinterestingphotosaswellasdownloadingtheimagedataforaspecificphoto.InChapter21,youwilldisplayalloftheinterestingphotosinagridlayout.Figure20.1showsPhotoramaattheendofthischapter.Figure20.1Photorama
YourwebbrowserusesHTTPtocommunicatewithawebserver.Inthesimplestinteraction,thebrowsersendsarequesttotheserverspecifyingaURL.Theserverrespondsbysendingbacktherequestedpage(typicallyHTMLandimages),whichthebrowserformatsanddisplays.Inmorecomplexinteractions,browserrequestsincludeotherparameters,suchasformdata.Theserverprocessestheseparametersandreturnsacustomized,ordynamic,webpage.Webbrowsersarewidelyusedandhavebeenaroundforalongtime,sothetechnologiessurroundingHTTParestableandwelldeveloped:HTTPtrafficpassesneatlythroughmostfirewalls,webserversareverysecureandhavegreatperformance,andwebapplicationdevelopmenttoolshavebecomeeasytouse.YoucanwriteaclientapplicationforiOSthatleveragestheHTTPinfrastructuretotalktoaweb-enabledserver.Theserversideofthisapplicationisawebservice.Yourclient
applicationandthewebservicecanexchangerequestsandresponsesviaHTTP.BecauseHTTPdoesnotcarewhatdataittransports,theseexchangescancontaincomplexdata.ThisdataistypicallyinJSON(JavaScriptObjectNotation)orXMLformat.Ifyoucontrolthewebserveraswellastheclient,youcanuseanyformatyoulike.Ifnot,youhavetobuildyourapplicationtousewhatevertheserversupports.PhotoramawillmakeawebservicerequesttogetinterestingphotosfromFlickr.Thewebserviceishostedathttps://api.flickr.com/services/rest.ThedatathatisreturnedwillbeJSONthatdescribesthephotos.
StartingthePhotoramaApplication
CreateanewSingleViewApplicationfortheUniversaldevicefamily.NamethisapplicationPhotorama,asshowninFigure20.2.Figure20.2Creatingasingleviewapplication
Let’sknockoutthebasicUIbeforefocusingonwebservices.CreateanewSwiftfilenamedPhotosViewController.InPhotosViewController.swift,definethePhotosViewControllerclassandgiveitanimageViewproperty.importFoundation
importUIKit
classPhotosViewController:UIViewController{
@IBOutletvarimageView:UIImageView!
}
Intheprojectnavigator,deletetheexistingViewController.swift.OpenMain.storyboardandselecttheViewController.OpenitsidentityinspectorandchangetheClasstoPhotosViewController.WiththePhotosViewControllerstillselected,selecttheEditormenuandchooseEmbedIn→NavigationController.SelecttheNavigationControllerandopenitsattributesinspector.UndertheViewControllerheading,makesuretheboxforIsInitialViewControllerischecked.DraganImageViewontothecanvasforPhotosViewControllerandaddconstraintstopinittoalledgesofthesuperview.ConnecttheimageviewtotheimageViewoutleton
PhotosViewController.OpentheattributesinspectorfortheimageviewandchangetheContentModetoAspectFill.Finally,double-clickonthecenterofthenavigationbarforthePhotosViewControllerandgiveitatitleof“Photorama.”YourinterfacewilllooklikeFigure20.3.Figure20.3InitialPhotoramainterface
Buildandruntheapplicationtomakesuretherearenoerrors.
BuildingtheURL
Communicationwithserversisdoneviarequests.Arequestencapsulatesinformationabouttheinteractionbetweentheapplicationandtheserver,anditsmostimportantpieceofinformationisthedestinationURL.Inthissection,youwillbuilduptheURLforretrievinginterestingphotosfromtheFlickrwebservice.Thearchitectureoftheapplicationwillreflectbestpractices.Forexample,eachtypethatyoucreatewillencapsulateasingleresponsibility.Thiswillmakeyourtypesrobustandflexibleandyourapplicationeasiertoreasonabout.TobeagoodiOSdeveloper,younotonlyneedtogetthejobdone,butyoualsoneedtogetitdonethoughtfullyandwithforesight.
FormattingURLsandrequests
Theformatofawebservicerequestvariesdependingontheserverthattherequestisreachingoutto.Therearenoset-in-stoneruleswhenitcomestowebservices.Youwillneedtofindthedocumentationforthewebservicetoknowhowtoformatarequest.Aslongasaclientapplicationsendstheserverwhatitwants,youhaveaworkingexchange.Flickr ’sinterestingphotoswebservicewantsaURLthatlookslikethis:https://api.flickr.com/services/rest/?method=flickr.interestingness.getList
&api_key=a6d819499131071f158fd740860a5a88&extras=url_h,date_taken
&format=json&nojsoncallback=1
Webservicerequestscomeinallsortsofformats,dependingonwhatthecreatorofthatwebserviceistryingtoaccomplish.Theinterestingphotoswebservice,wherepiecesofinformationarebrokenupintokey-valuepairs,isprettycommon.Thekey-valuepairsthataresuppliedaspartoftheURLarecalledqueryitems.EachofthequeryitemsfortheinterestingphotosrequestisdefinedbyandisuniquetotheFlickrAPI.
ThemethoddetermineswhichendpointyouwanttohitontheFlickrAPI.Fortheinterestingphotos,thisisthestring"flickr.interestingness.getList".Theapi_keyisakeythatFlickrgeneratestoauthorizeanapplicationtousetheFlickrAPI.Theextrasareattributespassedintocustomizetheresponse.Here,theurl_h,date_takenvaluetellstheFlickrserverthatyouwantthephotoURLstoalsocomebackintheresponsealongwiththedatethephotowastaken.TheformatitemspecifiesthatyouwantthepayloadcomingbacktobeJSON.ThenojsoncallbackitemspecifiesthatyouwantJSONbackinitsrawformat.
URLComponents
Youwillcreatetwotypestodealwithallofthewebserviceinformation.TheFlickrAPIstructwillberesponsibleforknowingandhandlingallFlickr-relatedinformation.ThisincludesknowinghowtogeneratetheURLsthattheFlickrAPIexpectsaswellasknowingtheformatoftheincomingJSONandhowtoparsethatJSONintotherelevantmodelobjects.ThePhotoStoreclasswillhandletheactualwebservicecalls.Let’sstartbycreatingtheFlickrAPIstruct.CreateanewSwiftfilenamedFlickrAPIanddeclaretheFlickrAPIstruct,whichwillcontainalloftheknowledgethatisspecifictotheFlickrAPI.importFoundation
structFlickrAPI{
}
YouaregoingtouseanenumerationtospecifywhichendpointontheFlickrservertohit.Forthisapplication,youwillonlybeworkingwiththeendpointtogetinterestingphotos.However,FlickrsupportsmanyadditionalAPIs,suchassearchingforimagesbasedonastring.Usinganenumnowwillmakeiteasiertoaddendpointsinthefuture.InFlickrAPI.swift,createtheMethodenumeration.EachcaseofMethodhasarawvaluethatmatchesthecorrespondingFlickrendpoint.importFoundation
enumMethod:String{
caseinterestingPhotos="flickr.interestingness.getList"
}
structFlickrAPI{
}
InChapter2,youlearnedthatenumerationscanhaverawvaluesassociatedwiththem.AlthoughtherawvaluesareoftenInts,youcanseehereagreatuseofStringastherawvaluefortheMethodenumeration.Nowdeclareatype-levelpropertytoreferencethebaseURLstringforthewebservicerequests.enumMethod:String{
caseinterestingPhotos="flickr.interestingness.getList"
}
structFlickrAPI{
staticletbaseURLString="https://api.flickr.com/services/rest"
}
Atype-levelproperty(ormethod)isonethatisaccessedonthetypeitself–inthiscase,theFlickrAPItype.Forstructs,typepropertiesandmethodsaredeclaredwiththestatickeyword;classesusetheclasskeyword.YouusedatypemethodontheUIViewclassinChapter8whenyoucalledtheanimate(withDuration:animations:)method.YoualsousedatypemethodonUIImagePickerControllerinChapter15whenyoucalled
theisSourceTypeAvailable(_:)method.Here,youaredeclaringatype-levelpropertyonFlickrAPI.ThebaseURLStringisanimplementationdetailoftheFlickrAPItype,andnoothertypeneedstoknowaboutit.Instead,theywillaskforacompletedURLfromFlickrAPI.TokeepotherfilesfrombeingabletoaccessbaseURLString,markthepropertyasprivate.structFlickrAPI{
privatestaticletbaseURLString="https://api.flickr.com/services/rest"
}
Thisiscalledaccesscontrol.Youcancontrolwhatcanaccessthepropertiesandmethodsonyourowntypes.Therearefivelevelsofaccesscontrolthatcanbeappliedtotypes,properties,andmethods:
open–Thisisusedonlyforclasses,andmostlybyframeworkorthird-partylibraryauthors.Anythingcanaccessthisclass,property,ormethod.Additionally,classesmarkedasopencanbesubclassedandmethodscanbeoverriddenoutsideofthemodule.public–Thisisverysimilartoopen;however,classescanonlybesubclassedandmethodscanonlybeoverriddeninside(notoutsideof)themodule.internal–Thisisthedefault.Anythinginthecurrentmodulecanaccessthistype,property,ormethod.Foranapp,onlyfileswithinyourprojectcanaccessthese.Ifyouwriteathird-partylibrary,thenonlyfileswithinthatthird-partylibrarycanaccessthem–appsthatuseyourthird-partylibrarycannot.fileprivate–Anythinginthesamesourcefilecanseethistype,property,ormethod.private–Anythingwithintheenclosingscopecanaccessthistype,property,ormethod.
NowyouaregoingtocreateatypemethodthatbuildsuptheFlickrURLforaspecificendpoint.Thismethodwillaccepttwoarguments:ThefirstwillspecifywhichendpointtohitusingtheMethodenumeration,andthesecondwillbeanoptionaldictionaryofqueryitemparametersassociatedwiththerequest.ImplementthismethodinyourFlickrAPIstructinFlickrAPI.swift.Fornow,thismethodwillreturnanemptyURL.privatestaticfuncflickrURL(method:Method,
parameters:[String:String]?)->URL{
returnURL(string:"")!
}
NoticethattheflickrURL(method:parameters:)methodisprivate.ItisanimplementationdetailoftheFlickrAPIstruct.AninternaltypemethodwillbeexposedtotherestoftheprojectforeachofthespecificendpointURLs(currently,justtheinterestingphotosendpoint).TheseinternaltypemethodswillcallthroughtotheflickrURL(method:parameters:)method.InFlickrAPI.swift,defineandimplementtheinterestingPhotosURLcomputedproperty.staticvarinterestingPhotosURL:URL{
returnflickrURL(method:.interestingPhotos,
parameters:["extras":"url_h,date_taken"])
}
TimetoconstructthefullURL.YouhavethebaseURLdefinedasaconstant,andthequeryitemsarebeingpassedintotheflickrURL(method:parameters:)methodviatheparametersargument.YouwillbuilduptheURLusingtheURLComponentsclass,whichisdesignedtotakeinthesevariouscomponentsandconstructaURLfromthem.UpdatetheflickrURL(method:parameters:)methodtoconstructaninstanceofURLComponentsfromthebaseURL.Then,loopovertheincomingparametersandcreatetheassociatedURLQueryIteminstances.privatestaticfuncflickrURL(method:Method,
parameters:[String:String]?)->URL{
returnURL(string:"")!
varcomponents=URLComponents(string:baseURLString)!
varqueryItems=[URLQueryItem]()
ifletadditionalParams=parameters{
for(key,value)inadditionalParams{
letitem=URLQueryItem(name:key,value:value)
queryItems.append(item)
}
}
components.queryItems=queryItems
returncomponents.url!
}
ThelaststepinsettinguptheURListopassintheparametersthatarecommontoallrequests:method,api_key,format,andnojsoncallback.TheAPIkeyisatokengeneratedbyFlickrtoidentifyyourapplicationandauthenticateitwiththewebservice.WehavegeneratedanAPIkeyforthisapplicationbycreatingaFlickraccountandregisteringthisapplication.(IfyouwouldlikeyourownAPIkey,youwillneedtoregisteranapplicationatwww.flickr.com/services/apps/create.)InFlickrAPI.swift,createaconstantthatreferencesthistoken.structFlickrAPI{
privatestaticletbaseURLString="https://api.flickr.com/services/rest"
privatestaticletapiKey="a6d819499131071f158fd740860a5a88"
Double-checktomakesureyouhavetypedintheAPIkeyexactlyaspresentedhere.Ithastomatchortheserverwillrejectyourrequests.IfyourAPIkeyisnotworkingorifyouhaveanyproblemswiththerequests,checkouttheforumsatforums.bignerdranch.comforhelp.FinishimplementingflickrURL(method:parameters:)toaddthecommonqueryitemstotheURLComponents.privatestaticfuncflickrURL(method:Method,
parameters:[String:String]?)->URL{
varcomponents=URLComponents(string:baseURLString)!
varqueryItems=[URLQueryItem]()
letbaseParams=[
"method":method.rawValue,
"format":"json",
"nojsoncallback":"1",
"api_key":apiKey
]
for(key,value)inbaseParams{
letitem=URLQueryItem(name:key,value:value)
queryItems.append(item)
}
ifletadditionalParams=parameters{
for(key,value)inadditionalParams{
letitem=URLQueryItem(name:key,value:value)
queryItems.append(item)
}
}
components.queryItems=queryItems
returncomponents.url!
}
SendingtheRequest
AURLrequestencapsulatesinformationaboutthecommunicationfromtheapplicationtotheserver.Mostimportantly,itspecifiestheURLoftheserverfortherequest,butitalsohasatimeoutinterval,acachepolicy,andothermetadataabouttherequest.ArequestisrepresentedbytheURLRequestclass.CheckouttheFortheMoreCurioussectionattheendofthischapterformoreinformation.TheURLSessionAPIisacollectionofclassesthatusearequesttocommunicatewithaserverinanumberofways.TheURLSessionTaskclassisresponsibleforcommunicatingwithaserver.TheURLSessionclassisresponsibleforcreatingtasksthatmatchagivenconfiguration.InPhotorama,anewclass,PhotoStore,willberesponsibleforinitiatingthewebservicerequests.ItwillusetheURLSessionAPIandtheFlickrAPIstructtofetchalistofinterestingphotosanddownloadtheimagedataforeachphoto.CreateanewSwiftfilenamedPhotoStoreanddeclarethePhotoStoreclass.importFoundation
classPhotoStore{
}
URLSession
Let’slookatafewofthepropertiesonURLRequest:allHTTPHeaderFields–adictionaryofmetadataabouttheHTTPtransaction,includingcharacterencodingandhowtheservershouldhandlecachingallowsCellularAccess–aBooleanthatrepresentswhetherarequestisallowedtousecellulardatacachePolicy–thepropertythatdetermineswhetherandhowthelocalcacheshouldbeusedhttpMethod–therequestmethod;thedefaultisGET,andothervaluesarePOST,PUT,andDELETEtimeoutInterval–themaximumdurationaconnectiontotheserverwillbeattemptedfor
TheclassthatcommunicateswiththewebserviceisaninstanceofURLSessionTask.Therearethreekindsoftasks:datatasks,downloadtasks,anduploadtasks.URLSessionDataTaskretrievesdatafromtheserverandreturnsitasDatainmemory.URLSessionDownloadTaskretrievesdatafromtheserverandreturnsitasafilesavedtothefilesystem.URLSessionUploadTasksendsdatatotheserver.
Often,youwillhaveagroupofrequeststhathavemanypropertiesincommon.Forexample,maybesomedownloadsshouldneverhappenovercellulardata,ormaybecertainrequestsshouldbecacheddifferentlythanothers.Itcanbecometedioustoconfigurerelatedrequeststhesameway.ThisiswhereURLSessioncomesinhandy.URLSessionactsasafactoryforURLSessionTaskinstances.Thesessioniscreatedwithaconfigurationthatspecifiespropertiesthatarecommonacrossallofthetasksthatitcreates.AlthoughmanyapplicationsmightonlyneedtouseasingleinstanceofURLSession,havingthepowerandflexibilityofmultiplesessionsisagreattooltohaveatyourdisposal.InPhotoStore.swift,addapropertytoholdontoaninstanceofURLSession.classPhotoStore{
privateletsession:URLSession={
letconfig=URLSessionConfiguration.default
returnURLSession(configuration:config)
}()
}
InPhotoStore.swift,implementthefetchInterestingPhotos()methodtocreateaURLRequestthatconnectstoapi.flickr.comandasksforthelistofinterestingphotos.Then,usetheURLSessiontocreateaURLSessionDataTaskthattransfersthisrequesttotheserver.funcfetchInterestingPhotos(){
leturl=FlickrAPI.interestingPhotosURL
letrequest=URLRequest(url:url)
lettask=session.dataTask(with:request){
(data,response,error)->Voidin
ifletjsonData=data{
ifletjsonString=String(data:jsonData,
encoding:.utf8){
print(jsonString)
}
}elseifletrequestError=error{
print("Errorfetchinginterestingphotos:\(requestError)")
}else{
print("Unexpectederrorwiththerequest")
}
}
task.resume()
}
CreatingtheURLRequestisfairlystraightforward:YoucreateaURLinstanceusingtheFlickrAPIstructandinstantiatearequestobjectwithit.Bygivingthesessionarequestandacompletionclosuretocallwhentherequestfinishes,thesessionwillreturnaninstanceofURLSessionTask.BecausePhotoramaisrequestingdatafromawebservice,thetypeoftaskwillbeaninstanceofURLSessionDataTask.Tasksarealwayscreatedinthesuspendedstate,socallingresume()onthetaskwillstartthewebservicerequest.Fornow,thecompletionblockwilljustprintouttheJSONdatareturnedfrom
therequest.Tomakearequest,PhotosViewControllerwillcalltheappropriatemethodsonPhotoStore.Todothis,PhotosViewControllerneedsareferencetoaninstanceofPhotoStore.AtthetopofPhotosViewController.swift,addapropertytohangontoaninstanceofPhotoStore.classPhotosViewController:UIViewController{
@IBOutletvarimageView:UIImageView!
varstore:PhotoStore!
ThestoreisadependencyofthePhotosViewController.YouwillusepropertyinjectiontogivethePhotosViewControlleritsstoredependency,justasyoudidwiththeviewcontrollersinHomepwner.OpenAppDelegate.swiftandusepropertyinjectiontogivethePhotosViewControlleraninstanceofPhotoStore.funcapplication(_application:UIApplication,didFinishLaunchingWithOptions
launchOptions:[UIApplicationLaunchOptionsKey:Any]?)->Bool{
//Overridepointforcustomizationafterapplicationlaunch.
letrootViewController=window!.rootViewControlleras!UINavigationController
letphotosViewController=
rootViewController.topViewControlleras!PhotosViewController
photosViewController.store=PhotoStore()
returntrue
}
NowthatthePhotosViewControllercaninteractwiththePhotoStore,kickoffthewebserviceexchangewhentheviewcontrolleriscomingonscreenforthefirsttime.InPhotosViewController.swift,overrideviewDidLoad()andfetchtheinterestingphotos.overridefuncviewDidLoad(){
super.viewDidLoad()
store.fetchInterestingPhotos()
}
Buildandruntheapplication.AstringrepresentationoftheJSONdatacomingbackfromthewebservicewillprinttotheconsole.(Ifyoudonotseeanythingprinttotheconsole,makesureyoutypedtheURLandAPIkeycorrectly.)TheresponsewilllooksomethinglikeFigure20.4.
Figure20.4Webserviceconsoleoutput
ModelingthePhoto
Next,youwillcreateaPhotoclasstorepresenteachphotothatisreturnedfromthewebservicerequest.Therelevantpiecesofinformationthatyouwillneedforthisapplicationaretheid,thetitle,theurl_h,andthedatetaken.CreateanewSwiftfilecalledPhotoanddeclarethePhotoclasswithpropertiesforthephotoID,thetitle,andtheremoteURL.Finally,addadesignatedinitializerthatsetsuptheinstance.importFoundation
classPhoto{
lettitle:String
letremoteURL:URL
letphotoID:String
letdateTaken:Date
init(title:String,photoID:String,remoteURL:URL,dateTaken:Date){
self.title=title
self.photoID=photoID
self.remoteURL=remoteURL
self.dateTaken=dateTaken
}
}
YouwillusethisclassshortlyonceyouareparsingtheJSONdata.
JSONData
JSONdata,especiallywhenitiscondensedlikeitisinyourconsole,mayseemdaunting.However,itisactuallyaverysimplesyntax.JSONcancontainthemostbasictypesusedtorepresentmodelobjects:arrays,dictionaries,strings,andnumbers.AJSONdictionarycontainsoneormorekey-valuepairs,wherethekeyisastringandthevaluecanbeanotherdictionaryorastring,number,orarray.Anarraycanconsistofstrings,numbers,dictionaries,andotherarrays.Thus,aJSONdocumentisanestedsetofthesetypesofvalues.HereisanexampleofsomereallysimpleJSON:{
"name":"Christian",
"friends":["Stacy","Mikey"],
"job":{
"company":"BigNerdRanch",
"title":"SeniorNerd"
}
}
ThisJSONdocumentbeginsandendswithcurlybraces({and}),whichinJSONdelimitadictionary.Withinthecurlybracesarethekey-valuepairsthatbelongtothedictionary.Thisdictionarycontainsthreekey-valuepairs(name,friends,andjob).Astringisrepresentedbytextwithinquotationmarks.Stringsareusedasthekeyswithinadictionaryandcanbeusedasvalues,too.Thus,thevalueassociatedwiththenamekeyinthetop-leveldictionaryisthestringChristian.Arraysarerepresentedwithsquarebrackets([and]).AnarraycancontainanyotherJSONinformation.Inthiscase,thefriendskeyholdsanarrayofstrings(StacyandMikey).Adictionarycancontainotherdictionaries,andthefinalkeyinthetop-leveldictionary,job,isassociatedwithadictionarythathastwokey-valuepairs(companyandtitle).PhotoramawillparseouttheusefulinformationfromtheJSONdataandstoreitinaPhotoinstance.
JSONSerialization
Applehasabuilt-inclassforparsingJSONdata,JSONSerialization.YoucanhandthisclassabunchofJSONdata,anditwillcreateadictionaryforeveryJSONdictionary(theJSONspecificationcallsthese“objects”),anarrayforeveryJSONarray,aStringforeveryJSONstring,andanNSNumberforeveryJSONnumber.Let’sseehowthisclasshelpsyou.OpenPhotoStore.swiftandupdatefetchInterestingPhotos()toprinttheJSONobjecttotheconsole.funcfetchInterestingPhotos(){
leturl=FlickrAPI.interestingPhotosURL
letrequest=URLRequest(url:url)
lettask=session.dataTask(with:request){
(data,response,error)->Voidin
ifletjsonData=data{
ifletjsonString=String(data:jsonData,
encoding:.utf8){
print(jsonString)
}
do{
letjsonObject=tryJSONSerialization.jsonObject(with:jsonData,
options:[])
print(jsonObject)
}catchleterror{
print("ErrorcreatingJSONobject:\(error)")
}
}elseifletrequestError=error{
print("Errorfetchinginterestingphotos:\(requestError)")
}else{
print("Unexpectederrorwiththerequest")
}
}
task.resume()
}
Buildandruntheapplication,thenchecktheconsole.YouwillseetheJSONdataagain,butnowitwillbeformatteddifferentlybecauseprint()doesagoodjobformattingdictionariesandarrays.TheformatoftheJSONdataisdictatedbytheAPI,soyouwilladdthecodetoparsetheJSONtotheFlickrAPIstruct.Parsingthedatathatcomesbackfromtheservercouldgowronginanumberofways:ThedatamightnotcontainJSON.Thedatacouldbecorrupt.ThedatamightcontainJSONbutnotmatchtheformatthatyouexpect.Tomanagethepossibilityoffailure,youwilluseanenumerationwithassociatedvaluestorepresentthesuccessorfailureoftheparsing.
Enumerationsandassociatedvalues
YoulearnedaboutthebasicsofenumerationsinChapter2,andyouhavebeenusingthemthroughoutthisbook–includingtheMethodenumusedearlierinthischapter.Associatedvaluesareausefulfeatureofenumerations.Let’stakeamomenttolookatasimpleexamplebeforeyouusethisfeatureinPhotorama.Enumerationsareaconvenientwayofdefiningandrestrictingthepossiblevaluesforavariable.Forexample,let’ssayyouareworkingonahomeautomationapp.Youcoulddefineanenumerationtospecifytheovenstate,likethis:enumOvenState{
caseon
caseoff
}
Iftheovenison,youalsoneedtoknowwhattemperatureitissetto.Associatedvaluesarea
perfectsolutiontothissituation.enumOvenState{
caseon(Double)
caseoff
}
varovenState=OvenState.on(450)
Eachcaseofanenumerationcanhavedataofanytypeassociatedwithit.ForOvenState,its.oncasehasanassociatedDoublethatrepresentstheoven’stemperature.Noticethatnotallcasesneedtohaveassociatedvalues.Retrievingtheassociatedvaluefromanenumisoftendoneusingaswitchstatement.switchovenState{
caselet.on(temperature):
print("Theovenisonandsetto\(temperature)degrees.")
case.off:
print("Theovenisoff.")
}
Notethatthe.oncaseusesaletkeywordtostoretheassociatedvalueinthetemperatureconstant,whichcanbeusedwithinthecaseclause.(Youcanusethevarkeywordinsteadiftemperatureneedstobeavariable.)ConsideringthevaluegiventoovenState,theswitchstatementabovewouldresultinthelineTheovenisonandsetto450degrees.printedtotheconsole.Inthenextsection,youwilluseanenumerationwithassociatedvaluestotietheresultstatusofarequesttotheFlickrwebservicewithdata.Asuccessfulresultstatuswillbetiedtothedatacontaininginterestingphotos;afailureresultstatuswillbetiedwitherrorinformation.
ParsingJSONdata
InPhotoStore.swift,addanenumerationnamedPhotosResulttothetopofthefilethathasacaseforbothsuccessandfailure.importFoundation
enumPhotosResult{
casesuccess([Photo])
casefailure(Error)
}
classPhotoStore{
IfthedataisvalidJSONandcontainsanarrayofphotos,thosephotoswillbeassociatedwiththesuccesscase.Ifthereareanyerrorsduringtheparsingprocess,therelevantErrorwillbepassedalongwiththefailurecase.Errorisaprotocolthatallerrorsconformto.NSErroristheerrorthatmanyiOSframeworksthrow,anditconformstoError.YouwillcreateyourownErrorshortly.InFlickrAPI.swift,implementamethodthattakesinaninstanceofDataandusesthe
JSONSerializationclasstoconvertthedataintothebasicfoundationobjects.staticfuncphotos(fromJSONdata:Data)->PhotosResult{
do{
letjsonObject=tryJSONSerialization.jsonObject(with:data,
options:[])
varfinalPhotos=[Photo]()
return.success(finalPhotos)
}catchleterror{
return.failure(error)
}
}
(Thiscodewillgeneratesomewarnings.Youwillresolvethemshortly.)IftheincomingdataisvalidJSONdata,thenthejsonObjectinstancewillreferencetheappropriatemodelobject.Ifnot,thentherewasaproblemwiththedataandyoupassalongtheerror.YounowneedtogetthephotoinformationoutoftheJSONobjectandintoinstancesofPhoto.WhentheURLSessionDataTaskfinishes,youwilluseJSONSerializationtoconverttheJSONdataintoadictionary.Figure20.5showshowthedatawillbestructured.Figure20.5JSONobjects
AtthetopleveloftheincomingJSONdataisadictionary.Thevalueassociatedwiththe“photos”keycontainstheimportantinformation,andthemostimportantisthearrayofdictionaries.Asyoucansee,youhavetodigprettydeeptogettheinformationthatyouneed.IfthestructureoftheJSONdatadoesnotmatchyourexpectations,youwillreturnacustomerror.AtthetopofFlickrAPI.swift,declareacustomenumtorepresentpossibleerrorsfortheFlickrAPI.enumFlickrError:Error{
caseinvalidJSONData
}
enumMethod:String{
caseinterestingPhotos="flickr.interestingness.getList"
}
Now,inphotos(fromJSON:),digdownthroughtheJSONdatatogettothearrayofdictionariesrepresentingtheindividualphotos.staticfuncphotos(fromJSONdata:Data)->PhotosResult{
do{
letjsonObject=tryJSONSerialization.jsonObject(with:data,
options:[])
guard
letjsonDictionary=jsonObjectas?[AnyHashable:Any],
letphotos=jsonDictionary["photos"]as?[String:Any],
letphotosArray=photos["photo"]as?[[String:Any]]else{
//TheJSONstructuredoesn'tmatchourexpectations
return.failure(FlickrError.invalidJSONData)
}
varfinalPhotos=[Photo]()
return.success(finalPhotos)
}catchleterror{
return.failure(error)
}
}
ThenextstepistogetthephotoinformationoutofthedictionaryandintoPhotomodelobjects.YouwillneedaninstanceofDateFormattertoconvertthedatetakenstringintoaninstanceofDate.InFlickrAPI.swift,addaconstantinstanceofDateFormatter.privatestaticletbaseURLString="https://api.flickr.com/services/rest"
privatestaticletapiKey="a6d819499131071f158fd740860a5a88"
privatestaticletdateFormatter:DateFormatter={
letformatter=DateFormatter()
formatter.dateFormat="yyyy-MM-ddHH:mm:ss"
returnformatter
}()
StillinFlickrAPI.swift,writeanewmethodtoparseaJSONdictionaryintoaPhotoinstance.privatestaticfuncphoto(fromJSONjson:[String:Any])->Photo?{
guard
letphotoID=json["id"]as?String,
lettitle=json["title"]as?String,
letdateString=json["datetaken"]as?String,
letphotoURLString=json["url_h"]as?String,
leturl=URL(string:photoURLString),
letdateTaken=dateFormatter.date(from:dateString)else{
//Don'thaveenoughinformationtoconstructaPhoto
returnnil
}
returnPhoto(title:title,photoID:photoID,remoteURL:url,dateTaken:dateTaken)
}
Nowupdatephotos(fromJSON:)toparsethedictionariesintoPhotoinstancesandthenreturntheseaspartofthesuccessenumerator.AlsohandlethepossibilitythattheJSONformathaschanged,sonophotoswereabletobefound.staticfuncphotos(fromJSONdata:Data)->PhotosResult{
do{
letjsonObject=tryJSONSerialization.jsonObject(with:data,
options:[])
guard
letjsonDictionary=jsonObjectas?[AnyHashable:Any],
letphotos=jsonDictionary["photos"]as?[String:Any],
letphotosArray=photos["photo"]as?[[String:Any]]else{
//TheJSONstructuredoesn'tmatchourexpectations
return.failure(FlickrError.invalidJSONData)
}
varfinalPhotos=[Photo]()
forphotoJSONinphotosArray{
ifletphoto=photo(fromJSON:photoJSON){
finalPhotos.append(photo)
}
}
iffinalPhotos.isEmpty&&!photosArray.isEmpty{
//Weweren'tabletoparseanyofthephotos
//MaybetheJSONformatforphotoshaschanged
return.failure(FlickrError.invalidJSONData)
}
return.success(finalPhotos)
}catchleterror{
return.failure(error)
}
}
Next,inPhotoStore.swift,writeanewmethodthatwillprocesstheJSONdatathatisreturnedfromthewebservicerequest.privatefuncprocessPhotosRequest(data:Data?,error:Error?)->PhotosResult{
guardletjsonData=dataelse{
return.failure(error!)
}
returnFlickrAPI.photos(fromJSON:jsonData)
}
Now,updatefetchInterestingPhotos()tousethemethodyoujustcreated.funcfetchInterestingPhotos(){
leturl=FlickrAPI.interestingPhotosURL
letrequest=URLRequest(url:url)
lettask=session.dataTask(with:request){
(data,response,error)->Voidin
ifletjsonData=data{
do{
letjsonObject=tryJSONSerialization.jsonObject(with:jsonData,
options:[])
print(jsonObject)
}catchleterror{
print("ErrorcreatingJSONobject:\(error)")
}
}elseifletrequestError=error{
print("Errorfetchinginterestingphotos:\(requestError)")
}else{
print("Unexpectederrorwiththerequest")
}
letresult=self.processPhotosRequest(data:data,error:error)
}
task.resume()
}
Finally,updatethemethodsignatureforfetchInterestingPhotos()totakeinacompletionclosurethatwillbecalledoncethewebservicerequestiscompleted.funcfetchInterestingPhotos(completion:@escaping(PhotosResult)->Void){
leturl=FlickrAPI.interestingPhotosURL
letrequest=URLRequest(url:url)
lettask=session.dataTask(with:request){
(data,response,error)->Voidin
letresult=self.processPhotosRequest(data:data,error:error)
completion(result)
}
task.resume()
}
Fetchingdatafromawebserviceisanasynchronousprocess:Oncetherequeststarts,itmaytakeanontrivialamountoftimeforaresponsetocomebackfromtheserver.Becauseofthis,thefetchInterestingPhotos(completion:)methodcannotdirectlyreturnaninstanceofPhotosResult.Instead,thecallerofthismethodwillsupplyacompletionclosureforthePhotoStoretocalloncetherequestiscomplete.ThisfollowsthesamepatternthatURLSessionTaskuseswithitscompletionhandler:Thetaskiscreatedwithaclosureforittocalloncethewebservicerequestcompletes.Figure20.6describestheflowofdatawiththewebservicerequest.
Figure20.6Webservicerequestdataflow
Theclosureismarkedwiththe@escapingannotation.Thisannotationletsthecompilerknowthattheclosuremightnotgetcalledimmediatelywithinthemethod.Inthiscase,theclosureisgettingpassedtotheURLSessionDataTask,whichwillcallitwhenthewebservicerequestcompletes.InPhotosViewController.swift,updatetheimplementationoftheviewDidLoad()usingthetrailingclosuresyntaxtoprintouttheresultofthewebservicerequest.overridefuncviewDidLoad(){
super.viewDidLoad()
store.fetchInterestingPhotos(){
(photosResult)->Voidin
switchphotosResult{
caselet.success(photos):
print("Successfullyfound\(photos.count)photos.")
caselet.failure(error):
print("Errorfetchinginterestingphotos:\(error)")
}
}
}
Buildandruntheapplication.Oncethewebservicerequestcompletes,youshouldseethenumberofphotosfoundprintedtotheconsole.
DownloadingandDisplayingtheImageData
Youhavedonealotalreadyinthischapter:YouhavesuccessfullyinteractedwiththeFlickrAPIviaawebservicerequest,andyouhaveparsedtheincomingJSONdataintoPhotomodelobjects.Unfortunately,youhavenothingtoshowforitexceptsomelogmessagesintheconsole.Inthissection,youwillusetheURLreturnedfromthewebservicerequesttodownloadtheimagedata.ThenyouwillcreateaninstanceofUIImagefromthatdata,and,finally,youwilldisplaythefirstimagereturnedfromtherequestinaUIImageView.(Inthenextchapter,youwilldisplayalloftheimagesthatarereturnedinagridlayoutdrivenbyaUICollectionView.)Thefirststepisdownloadingtheimagedata.Thisprocesswillbeverysimilartothewebservicerequesttodownloadthephotos’JSONdata.OpenPhotoStore.swift,importUIKit,andaddanenumerationtothetopofthefilethatrepresentstheresultofdownloadingtheimage.ThisenumerationwillfollowthesamepatternasthePhotosResultenumeration,takingadvantageofassociatedvalues.YouwillalsocreateanErrortorepresentphotoerrors.importFoundation
importUIKit
enumImageResult{
casesuccess(UIImage)
casefailure(Error)
}
enumPhotoError:Error{
caseimageCreationError
}
enumPhotosResult{
casesuccess([Photo])
casefailure(Error)
}
Ifthedownloadissuccessful,thesuccesscasewillhavetheUIImageassociatedwithit.Ifthereisanerror,thefailurecasewillhavetheErrorassociatedwithit.Now,inthesamefile,implementamethodtodownloadtheimagedata.LikethefetchInterestingPhotos(completion:)method,thisnewmethodwilltakeinacompletionclosurethatwillreturnaninstanceofImageResult.funcfetchImage(forphoto:Photo,completion:@escaping(ImageResult)->Void){
letphotoURL=photo.remoteURL
letrequest=URLRequest(url:photoURL)
lettask=session.dataTask(with:request){
(data,response,error)->Voidin
}
task.resume()
}
Nowimplementamethodthatprocessesthedatafromthewebservicerequestintoanimage,ifpossible.privatefuncprocessImageRequest(data:Data?,error:Error?)->ImageResult{
guard
letimageData=data,
letimage=UIImage(data:imageData)else{
//Couldn'tcreateanimage
ifdata==nil{
return.failure(error!)
}else{
return.failure(PhotoError.imageCreationError)
}
}
return.success(image)
}
StillinPhotoStore.swift,updatefetchImage(for:completion:)tousethisnewmethod.funcfetchImage(forphoto:Photo,completion:@escaping(ImageResult)->Void){
letphotoURL=photo.remoteURL
letrequest=URLRequest(url:photoURL)
lettask=session.dataTask(with:request){
(data,response,error)->Voidin
letresult=self.processImageRequest(data:data,error:error)
completion(result)
}
task.resume()
}
Totestthiscode,youwilldownloadtheimagedataforthefirstphotothatisreturnedfromtheinterestingphotosrequestanddisplayitontheimageview.OpenPhotosViewController.swiftandaddanewmethodthatwillfetchtheimageanddisplayitontheimageview.funcupdateImageView(forphoto:Photo){
store.fetchImage(for:photo){
(imageResult)->Voidin
switchimageResult{
caselet.success(image):
self.imageView.image=image
caselet.failure(error):
print("Errordownloadingimage:\(error)")
}
}
}
NowupdateviewDidLoad()tousethisnewmethod.overridefuncviewDidLoad(){
super.viewDidLoad()
store.fetchInterestingPhotos{
(photosResult)->Voidin
switchphotosResult{
caselet.success(photos):
print("Successfullyfound\(photos.count)photos.")
ifletfirstPhoto=photos.first{
self.updateImageView(for:firstPhoto)
}
caselet.failure(error):
print("Errorfetchinginterestingphotos:\(error)")
}
}
}
Althoughyoucouldbuildandruntheapplicationatthispoint,theimagemayormaynotappearintheimageviewwhenthewebservicerequestfinishes.Why?Thecodethatupdatestheimageviewisnotbeingrunonthemainthread.
TheMainThread
ModerniOSdeviceshavemulticoreprocessorsthatenablethemtorunmultiplechunksofcodesimultaneously.Thesecomputationsproceedinparallel,sothisisreferredtoasparallelcomputing.Whendifferentcomputationsareinflightatthesametime,thisisknownasconcurrency,andthecomputationsaresaidtobehappeningconcurrently.Acommonwaytoexpressthisisbyrepresentingeachcomputationwithadifferentthreadofcontrol.Sofarinthisbook,allofyourcodehasbeenrunningonthemainthread.ThemainthreadissometimesreferredtoastheUIthread,becauseanycodethatmodifiestheUImustrunonthemainthread.Whenthewebservicecompletes,youwantittoupdatetheimageview.Butbydefault,URLSessionDataTaskrunsthecompletionhandleronabackgroundthread.Youneedawaytoforcecodetorunonthemainthreadtoupdatetheimageview.YoucandothateasilyusingtheOperationQueueclass.YouwillupdatetheasynchronousPhotoStoremethodstocalltheircompletionhandlersonthemainthread.InPhotoStore.swift,updatefetchInterestingPhotos(completion:)tocallthecompletionclosureonthemainthread.funcfetchInterestingPhotos(completion:@escaping(PhotosResult)->Void){
leturl=FlickrAPI.interestingPhotosURL
letrequest=URLRequest(url:url)
lettask=session.dataTask(with:request){
(data,response,error)->Voidin
letresult=self.processPhotosRequest(data:data,error:error)
OperationQueue.main.addOperation{
completion(result)
}
}
task.resume()
}
DothesameforfetchImage(for:completion:).funcfetchImage(forphoto:Photo,completion:@escaping(ImageResult)->Void){
letphotoURL=photo.remoteURL
letrequest=URLRequest(url:photoURL)
lettask=session.dataTask(with:request){
(data,response,error)->Voidin
letresult=self.processImageRequest(data:data,error:error)
OperationQueue.main.addOperation{
completion(result)
}
}
task.resume()
}
Buildandruntheapplication.Nowthattheimageviewisbeingupdatedonthemainthread,youwillhavesomethingtoshowforallyourhardwork:Animagewillappearwhenthewebservicerequestfinishes.(Itmighttakealittletimetoshowtheimageifthewebservicerequesttakesawhiletofinish.)
BronzeChallenge:PrintingtheResponseInformation
ThecompletionhandlerfordataTask(with:completionHandler:)providesaninstanceofURLResponse.WhenmakingHTTPrequests,thisresponseisoftypeHTTPURLResponse(asubclassofURLResponse).PrintthestatusCodeandheaderFieldstotheconsole.Thesepropertiesareveryusefulwhendebuggingwebservicecalls.
SilverChallenge:FetchRecentPhotosfromFlickr
Inthischapter,youfetchedtheinterestingphotosfromFlickrusingtheflickr.interestingness.getListendpoint.AddanewcasetoyourMethodenumerationforrecentphotos.Theendpointforthisisflickr.photos.getRecent.Extendtheapplicationsoyouareabletoswitchbetweeninterestingphotosandrecentphotos.(Hint:TheJSONformatforbothendpointsisthesame,soyourexistingparsingcodewillstillwork.)
FortheMoreCurious:HTTP
WhenURLSessionTaskinteractswithawebserver,itdoessoaccordingtotherulesoutlinedintheHTTPspecification.Thespecificationisveryclearabouttheexactformatoftherequest/responseexchangebetweentheclientandtheserver.AnexampleofasimpleHTTPrequestisshowninFigure20.7.Figure20.7HTTPrequestformat
AnHTTPrequesthasthreeparts:arequestline,requestheaders,andanoptionalrequestbody.Therequestlineisthefirstlineoftherequestandtellstheserverwhattheclientistryingtodo.Inthisrequest,theclientistryingtoGETtheresourceat/index.html.(ItalsospecifiestheHTTPversionthattherequestwillbeconformingto.)ThewordGETisanHTTPmethod.WhilethereareanumberofsupportedHTTPmethods,youwillseeGETandPOSTmostoften.ThedefaultofURLRequest,GET,indicatesthattheclientwantsaresourcefromtheserver.Theresourcerequestedmightbeanactualfileonthewebserver ’sfilesystem,oritcouldbegenerateddynamicallyatthemomenttherequestisreceived.Asaclient,youshouldnotcareaboutthisdetail,butmorethanlikelytheJSONresourcesyourequestedinthischapterwerecreateddynamically.Inadditiontogettingthingsfromaserver,youcansenditinformation.Forexample,manywebserversallowyoutouploadphotos.AclientapplicationwouldpasstheimagedatatotheserverthroughanHTTPrequest.Inthissituation,youwouldusetheHTTPmethodPOST,andyouwouldincludearequestbody.Thebodyofarequestisthepayloadyouaresendingtotheserver–typicallyJSON,XML,orbinarydata.Whentherequesthasabody,itmustalsohavetheContent-Lengthheader.Handily,URLRequestwillcomputethesizeofthebodyandaddthisheaderforyou.HereisanexampleofhowtoPOSTanimagetoanimaginarysiteusingaURLRequest.ifletsomeURL=URL(string:"http://www.photos.example.com/upload"){
letimage=profileImage()
letdata=UIImagePNGRepresentation(image)
varreq=URLRequest(url:someURL)
//ThisaddstheHTTPbodydataandautomaticallysetsthecontent-lengthheader
req.httpBody=data
//ThischangestheHTTPmethodintherequestline
req.httpMethod="POST"
//Ifyouwantedtosetarequestheader,suchastheAcceptheader
req.setValue("text/json",forHTTPHeaderField:"Accept")
}
Figure20.8showswhatasimpleHTTPresponsemightlooklike.WhileyouwillnotbemodifyingthecorrespondingHTTPURLResponseinstance,itisnicetounderstandwhatitismodeling.Figure20.8HTTPresponseformat
Asyoucansee,theformatoftheresponseisnottoodifferentfromtherequest.Itincludesastatusline,responseheaders,and,ofcourse,theresponsebody.Yes,thisiswherethatpesky404NotFoundcomesfrom!
21CollectionViews
Inthischapter,youwillcontinueworkingonthePhotoramaapplicationbydisplayingtheinterestingFlickrphotosinagridusingtheUICollectionViewclass.Thischapterwillalsoreinforcethedatasourcedesignpatternthatyouusedinpreviouschapters.Figure21.1showsyouwhattheapplicationwilllooklikeattheendofthischapter.Figure21.1Photoramawithacollectionview
InChapter10,youworkedwithUITableView.Tableviewsareagreatwaytodisplayandeditacolumnofinformationinahierarchicallist.Likeatableview,acollectionviewalsodisplaysanorderedcollectionofitems,butinsteadofdisplayingtheinformationinahierarchicallist,thecollectionviewhasalayoutobjectthatdrivesthedisplayofinformation.Youwilluseabuilt-inlayoutobject,theUICollectionViewFlowLayout,topresenttheinterestingphotosinascrollablegrid.
DisplayingtheGrid
Let’stackletheinterfacefirst.YouaregoingtochangetheUIforPhotosViewControllertodisplayacollectionviewinsteadofdisplayingtheimageview.OpenMain.storyboardandlocatethePhotoramaimageview.DeletetheimageviewfromthecanvasanddragaCollectionViewontothecanvas.Selectboththecollectionviewanditssuperview.(Theeasiestwaytodothisisusingthedocumentoutline.)OpentheAutoLayoutAlignmenu,configureitlikeFigure21.2,andclickAdd4Constraints.Figure21.2Collectionviewconstraints
BecauseyouusedtheAlignmenutopintheedges,thecollectionviewwillbepinnedtothetopoftheentireviewinsteadoftothetoplayoutguide.Thisisusefulforscrollviews(andtheirsubclasses,likeUITableViewandUICollectionView)sothatthecontentwillscrollunderneaththenavigationbar.Thescrollviewwillautomaticallyupdateitsinsetstomakethecontentvisible,asyousawinChapter10.ThecanvaswillnowlooklikeFigure21.3.
Figure21.3Storyboardcanvas
Currently,thecollectionviewcellshaveaclearbackgroundcolor.Selectthecollectionviewcell–thesmallrectangleintheupper-leftcornerofthecollectionview–andgiveitablackbackgroundcolor.Selecttheblackcollectionviewcellandopenitsattributesinspector.SettheIdentifiertoUICollectionViewCell(Figure21.4).Figure21.4Settingthereuseidentifier
Thecollectionviewisnowonthecanvas,butyouneedawaytopopulatethecellswithdata.Todothis,youwillcreateanewclasstoactasthedatasourceofthecollectionview.
CollectionViewDataSource
Applicationsareconstantlychanging,sopartofbeingagoodiOSdeveloperisbuildingapplicationsinawaythatallowsthemtoadapttochangingrequirements.ThePhotoramaapplicationwilldisplayasinglecollectionviewofphotos.YoucoulddosomethingsimilartowhatyoudidinHomepwnerandmakethePhotosViewControllerbethedatasourceofthecollectionview.Theviewcontrollerwouldimplementtherequireddatasourcemethods,andeverythingwouldworkjustfine.Atleast,itwouldworkfornow.Whatif,sometimeinthefuture,youdecidedtohaveadifferentscreenthatalsodisplayedacollectionviewofphotos?Maybeinsteadofdisplayingtheinterestingphotos,itwoulduseadifferentwebservicetodisplayallthephotosmatchingasearchterm.Inthiscase,youwouldneedtoreimplementthesamedatasourcemethodswithinthenewviewcontrollerwithessentiallythesamecode.Thatwouldnotbeideal.Instead,youwillabstractoutthecollectionviewdatasourcecodeintoanewclass.Thisclasswillberesponsibleforrespondingtodatasourcequestions–anditwillbereusableasnecessary.CreateanewSwiftfilenamedPhotoDataSourceanddeclarethePhotoDataSourceclass.importFoundation
importUIKit
classPhotoDataSource:NSObject,UICollectionViewDataSource{
varphotos=[Photo]()
}
ToconformtotheUICollectionViewDataSourceprotocol,atypealsoneedstoconformtotheNSObjectProtocol.TheeasiestandmostcommonwaytoconformtothisprotocolistosubclassfromNSObject,asyoudidabove.TheUICollectionViewDataSourceprotocoldeclarestworequiredmethodstoimplement:funccollectionView(_collectionView:UICollectionView,
numberOfItemsInSectionsection:Int)->Int
funccollectionView(_collectionView:UICollectionView,
cellForItemAtindexPath:IndexPath)->UICollectionViewCell
YoumightnoticethatthesetwomethodslookverysimilartothetworequiredmethodsofUITableViewDataSourcethatyousawinChapter10.Thefirstdatasourcecallbackaskshowmanycellstodisplay,andthesecondasksfortheUICollectionViewCelltodisplayforagivenindexpath.ImplementthesetwomethodsinPhotoDataSource.swift.classPhotoDataSource:NSObject,UICollectionViewDataSource{
varphotos=[Photo]()
funccollectionView(_collectionView:UICollectionView,
numberOfItemsInSectionsection:Int)->Int{
returnphotos.count
}
funccollectionView(_collectionView:UICollectionView,
cellForItemAtindexPath:IndexPath)->UICollectionViewCell{
letidentifier="UICollectionViewCell"
letcell=
collectionView.dequeueReusableCell(withReuseIdentifier:identifier,
for:indexPath)
returncell
}
}
Next,thecollectionviewneedstoknowthataninstanceofPhotoDataSourceisthedatasourceobject.InPhotosViewController.swift,addapropertytoreferenceaninstanceofPhotoDataSourceandanoutletforaUICollectionViewinstance.Also,youwillnotneedtheimageViewanymore,sodeleteit.classPhotosViewController:UIViewController{
@IBOutletvarimageView:UIImageView!
@IBOutletvarcollectionView:UICollectionView!
varstore:PhotoStore!
letphotoDataSource=PhotoDataSource()
WithouttheimageViewproperty,youwillnotneedthemethodupdateImageView(for:)anymore.Goaheadandremoveit.funcupdateImageView(forphoto:Photo){
store.fetchImage(for:photo){
(imageResult)->Voidin
switchimageResult{
caselet.success(image):
self.imageView.image=image
caselet.failure(error):
print("Errordownloadingimage:\(error)")
}
}
}
UpdateviewDidLoad()tosetthedatasourceonthecollectionview.overridefuncviewDidLoad(){
super.viewDidLoad()
collectionView.dataSource=photoDataSource
Finally,updatethephotoDataSourceobjectwiththeresultofthewebservicerequestandreloadthecollectionview.overridefuncviewDidLoad()
super.viewDidLoad()
collectionView.dataSource=photoDataSource
store.fetchInterestingPhotos{
(photosResult)->Voidin
switchphotosResult{
caselet.success(photos):
print("Successfullyfound\(photos.count)photos.")
ifletfirstPhoto=photos.first{
self.updateImageView(for:firstPhoto)
}
self.photoDataSource.photos=photos
caselet.failure(error):
print("Errorfetchinginterestingphotos:\(error)")
self.photoDataSource.photos.removeAll()
}
self.collectionView.reloadSections(IndexSet(integer:0))
}
}
ThelastthingyouneedtodoismakethecollectionViewoutletconnection.OpenMain.storyboardandnavigatetothecollectionview.Control-dragfromthePhotoramaviewcontrollertothecollectionviewandconnectittothecollectionViewoutlet.Buildandruntheapplication.Afterthewebservicerequestcompletes,checktheconsoletoconfirmthatphotoswerefound.OntheiOSdevice,therewillbeagridofblacksquarescorrespondingtothenumberofphotosfound(Figure21.5).Thesecellsarearrangedinaflowlayout.Aflowlayoutfitsasmanycellsonarowaspossiblebeforeflowingdowntothenextrow.IfyourotatetheiOSdevice,youwillseethecellsfillthegivenarea.
Figure21.5Initialflowlayout
CustomizingtheLayout
Thedisplayofcellsisnotdrivenbythecollectionviewitselfbutbythecollectionview’slayout.Thelayoutobjectisresponsiblefortheplacementofcellsonscreen.Layouts,inturn,aredrivenbyasubclassofUICollectionViewLayout.TheflowlayoutthatPhotoramaiscurrentlyusingisUICollectionViewFlowLayout,whichistheonlyconcreteUICollectionViewLayoutsubclassprovidedbytheUIKitframework.SomeofthepropertiesyoucancustomizeonUICollectionViewFlowLayoutare:
scrollDirection–Doyouwanttoscrollverticallyorhorizontally?minimumLineSpacing–Whatistheminimumspacingbetweenlines?minimumInteritemSpacing–Whatistheminimumspacingbetweenitemsinarow(orcolumn,ifscrollinghorizontally)?itemSize–Whatisthesizeofeachitem?sectionInset–Whatarethemarginsusedtolayoutcontentforeachsection?
Figure21.6showshowthesepropertiesaffectthepresentationofcellsusingUICollectionViewFlowLayout.Figure21.6UICollectionViewFlowLayoutproperties
OpenMain.storyboardandselectthecollectionview.OpenthesizeinspectorandconfiguretheCellSize,MinSpacing,andSectionInsetsasshowninFigure21.7.
Figure21.7Collectionviewsizeinspector
Buildandruntheapplicationtoseehowthelayouthaschanged.
CreatingaCustomUICollectionViewCell
NextyouaregoingtocreateacustomUICollectionViewCellsubclasstodisplaythephotos.Whiletheimagedataisdownloading,thecollectionviewcellwilldisplayaspinningactivityindicatorusingtheUIActivityIndicatorViewclass.CreateanewSwiftfilenamedPhotoCollectionViewCellanddefinePhotoCollectionViewCellasasubclassofUICollectionViewCell.Thenaddoutletstoreferencetheimageviewandtheactivityindicatorview.importFoundation
importUIKit
classPhotoCollectionViewCell:UICollectionViewCell{
@IBOutletvarimageView:UIImageView!
@IBOutletvarspinner:UIActivityIndicatorView!
}
Theactivityindicatorviewshouldonlyspinwhenthecellisnotdisplayinganimage.InsteadofalwaysupdatingthespinnerwhentheimageViewisupdated,orviceversa,youwillwriteahelpermethodtotakecareofitforyou.CreatethishelpermethodinPhotoCollectionViewCell.swift.funcupdate(withimage:UIImage?){
ifletimageToDisplay=image{
spinner.stopAnimating()
imageView.image=imageToDisplay
}else{
spinner.startAnimating()
imageView.image=nil
}
}
Itwouldbenicetoreseteachcelltothespinningstatebothwhenthecellisfirstcreatedandwhenthecellisgettingreused.ThemethodawakeFromNib()willbeusedfortheformer,andthemethodprepareForReuse()willbeusedforthelatter.RecallthatyouusedawakeFromNib()inChapter12.ThemethodprepareForReuse()iscalledwhenacellisabouttobereused.ImplementthesetwomethodsinPhotoCollectionViewCell.swifttoresetthecellbacktothespinningstate.overridefuncawakeFromNib(){
super.awakeFromNib()
update(with:nil)
}
overridefuncprepareForReuse(){
super.prepareForReuse()
update(with:nil)
}
Youwilluseaprototypecelltosetuptheinterfaceforthecollectionviewcellinthestoryboard,justasyoudidinChapter12forItemCell.Ifyourecall,eachprototypecellcorrespondstoavisuallyuniquecellwithauniquereuseidentifier.Mostofthetime,theprototypecellswillbeassociatedwithdifferentUICollectionViewCellsubclassestoprovidebehaviorspecifictothatkindofcell.Inthecollectionview’sattributesinspector,youcanadjustthenumberofItemsthatthecollectionviewdisplays,andeachitemcorrespondstoaprototypecellinthecanvas.ForPhotorama,youonlyneedonekindofcell:thePhotoCollectionViewCellthatdisplaysaphoto.OpenMain.storyboardandselectthecollectionviewcell.Intheidentityinspector,changetheClasstoPhotoCollectionViewCell(Figure21.8)and,intheattributesinspector,changetheIdentifiertoPhotoCollectionViewCell.Figure21.8Changingthecellclass
DraganimageviewontotheUICollectionViewCell.Addconstraintstopintheimageviewtotheedgesofthecell.OpentheattributesinspectorfortheimageviewandsettheContentModetoAspectFill.Thiswillcutoffpartsofthephotos,butitwillallowthephotostocompletelyfillinthecollectionviewcell.Next,draganactivityindicatorviewontopoftheimageview.Addconstraintstocentertheactivityindicatorviewbothhorizontallyandverticallywiththeimageview.OpenitsattributesinspectorandselectHidesWhenStopped(Figure21.9).Figure21.9Configuringtheactivityindicator
Selectthecollectionviewcellagain.Thiscanbeabittrickytodoonthecanvasbecausethenewlyaddedsubviewscompletelycoverthecellitself.AhelpfulInterfaceBuildertipisto
holdControlandShifttogetherandthenclickontopoftheviewyouwanttoselect.Youwillbepresentedwithalistofalloftheviewsandcontrollersunderthepointyouclicked(Figure21.10).Figure21.10Selectingthecellonthecanvas
Withthecellselected,opentheconnectionsinspectorandconnecttheimageViewandspinnerpropertiestotheimageviewandactivityindicatorviewonthecanvas(Figure21.11).
Figure21.11ConnectingPhotoCollectionViewCelloutlets
Next,openPhotoDataSource.swiftandupdatethedatasourcemethodtousethePhotoCollectionViewCell.funccollectionView(_collectionView:UICollectionView,
cellForItemAtindexPath:IndexPath)->UICollectionViewCell{
letidentifier="UICollectionViewCell""PhotoCollectionViewCell"
letcell=
collectionView.dequeueReusableCell(withReuseIdentifier:identifier,
for:indexPath)as!PhotoCollectionViewCell
returncell
}
Buildandruntheapplication.Whentheinterestingphotosrequestcompletes,youwillseetheactivityindicatorviewsallspinning(Figure21.12).
Figure21.12Customcollectionviewsubclass
DownloadingtheImageData
Nowallthatisleftisdownloadingtheimagedataforthephotosthatcomebackintherequest.Thistaskisnotverydifficult,butitrequiressomethought.Imagesarelargefiles,anddownloadingthemcouldeatupyourusers’cellulardataallowance.AsaconsiderateiOSdeveloper,youwanttomakesureyourapp’sdatausageisonlywhatitneedstobe.Consideryouroptions.YoucoulddownloadtheimagedatainviewDidLoad()whenthefetchInterestingPhotos(completion:)methodcallsitscompletionclosure.Atthatpoint,youalreadyassigntheincomingphotostothephotosproperty,soyoucoulditerateoverallofthosephotosanddownloadtheirimagedatathen.Althoughthiswouldwork,itwouldbeverycostly.Therecouldbealargenumberofphotoscomingbackintheinitialrequest,andtheusermayneverevenscrolldownintheapplicationfarenoughtoseesomeofthem.Ontopofthat,ifyouinitializetoomanyrequestssimultaneously,someoftherequestsmaytimeoutwhilewaitingforotherrequeststofinish.Sothisisprobablynotthebestsolution.Instead,itmakessensetodownloadtheimagedataforonlythecellsthattheuserisattemptingtoview.UICollectionViewhasamechanismtosupportthisthroughitsUICollectionViewDelegatemethodcollectionView(_:willDisplay:forItemAt:).Thisdelegatemethodwillbecalledeverytimeacellisgettingdisplayedonscreenandisagreatopportunitytodownloadtheimagedata.RecallthatthedataforthecollectionviewisdrivenbyaninstanceofPhotoDataSource,areusableclasswiththesingleresponsibilityofdisplayingphotosinacollectionview.Collectionviewsalsohaveadelegate,whichisresponsibleforhandlinguserinteractionwiththecollectionview.Thisincludestaskssuchasmanagingcellselectionandtrackingcellscomingintoandoutofview.Thisresponsibilityismoretightlycoupledwiththeviewcontrolleritself,sowhereasthedatasourceisaninstanceofPhotoDataSource,thecollectionview’sdelegatewillbethePhotosViewController.InPhotosViewController.swift,havetheclassconformtotheUICollectionViewDelegateprotocol.classPhotosViewController:UIViewController,UICollectionViewDelegate{
(BecausetheUICollectionViewDelegateprotocolonlydefinesoptionalmethods,Xcodedoesnotreportanyerrorswhenyouaddthisdeclaration.)UpdateviewDidLoad()tosetthePhotosViewControllerasthedelegateofthecollectionview.overridefuncviewDidLoad(){
super.viewDidLoad()
collectionView.dataSource=photoDataSource
collectionView.delegate=self
Finally,implementthedelegatemethodinPhotosViewController.swift.
funccollectionView(_collectionView:UICollectionView,
willDisplaycell:UICollectionViewCell,
forItemAtindexPath:IndexPath){
letphoto=photoDataSource.photos[indexPath.row]
//Downloadtheimagedata,whichcouldtakesometime
store.fetchImage(for:photo){(result)->Voidin
//Theindexpathforthephotomighthavechangedbetweenthe
//timetherequeststartedandfinished,sofindthemost
//recentindexpath
//(Note:Youwillhaveanerroronthenextline;youwillfixitsoon)
guardletphotoIndex=self.photoDataSource.photos.index(of:photo),
caselet.success(image)=resultelse{
return
}
letphotoIndexPath=IndexPath(item:photoIndex,section:0)
//Whentherequestfinishes,onlyupdatethecellifit'sstillvisible
ifletcell=self.collectionView.cellForItem(at:photoIndexPath)
as?PhotoCollectionViewCell{
cell.update(with:image)
}
}
}
Youareusinganewformofpatternmatchingintheabovecode.TheresultthatisreturnedfromfetchImage(for:completion:)isanenumerationwithtwocases:.successand.failure.Becauseyouonlyneedtohandlethe.successcase,youuseacasestatementtocheckwhetherresulthasavalueof.success.Comparethefollowingcodetoseehowyoucouldusepatternmatchinginanifstatementversusaswitchstatement.Thiscode:ifcaselet.success(image)=result{
photo.image=image
}
behavesjustlikethiscode:switchresult{
caselet.success(image):
photo.image=image
case.failure:
break
}
Let’sfixtheerroryousawwhenfindingtheindexofphotointhephotosarray.Theindex(of:)methodworksbycomparingtheitemthatyouarelookingfortoeachoftheitemsinthecollection.Itdoesthisusingthe==operator.TypesthatconformtotheEquatableprotocolmustimplementthisoperator,andPhotodoesnotyetconformtoEquatable.InPhoto.swift,declarethatPhotoconformstotheEquatableprotocolandimplementtherequiredoverloadingofthe==operator.classPhoto:Equatable{
...
staticfunc==(lhs:Photo,rhs:Photo)->Bool{
//TwoPhotosarethesameiftheyhavethesamephotoID
returnlhs.photoID==rhs.photoID
}
}
InSwift,itiscommontogrouprelatedchunksoffunctionalityintoanextension.Let’stakeashortdetourtolearnaboutextensionsandthenusethisknowledgetoseehowconformingtotheEquatableprotocolisoftendoneinpractice.
Extensions
Extensionsserveacoupleofpurposes:Theyallowyoutogroupchunksoffunctionalityintoalogicalunit,andtheyalsoallowyoutoaddfunctionalitytoyourowntypesaswellastypesprovidedbythesystemorotherframeworks.Beingabletoaddfunctionalitytoatypewhosesourcecodeyoudonothaveaccesstoisaverypowerfulandflexibletool.Extensionscanbeaddedtoclasses,structs,andenums.Let’stakealookatanexample.SayyouwantedtoaddfunctionalitytotheInttypetoprovideadoubledvalueofthatInt.Forexample:letfourteen=7.doubled//Thevalueoffourteenis'14'
YoucanaddthisfunctionalitybyextendingtheInttype:extensionInt{
vardoubled:Int{
returnself*2
}
}
Withextensions,youcanaddcomputedproperties,addmethods,andconformtoprotocols.However,youcannotaddstoredpropertiestoanextension.Extensionsprovideagreatmechanismforgroupingrelatedpiecesoffunctionality.Theycanmakethecodemorereadableandhelpwithlong-termmaintainabilityofyourcodebase.Onecommonchunkoffunctionalitythatisoftengroupedintoanextensionisconformancetoaprotocolalongwiththemethodsofthatprotocol.UpdatePhoto.swifttouseanextensiontoconformtotheEquatableprotocol.classPhoto:Equatable{
...
staticfunc==(lhs:Photo,rhs:Photo)->Bool{
//TwoPhotosarethesameiftheyhavethesamephotoID
returnlhs.photoID==rhs.photoID
}
}
extensionPhoto:Equatable{
staticfunc==(lhs:Photo,rhs:Photo)->Bool{
//TwoPhotosarethesameiftheyhavethesamephotoID
returnlhs.photoID==rhs.photoID
}
}
Thisisasimplifiedexample,butextensionsareverypowerfulforbothextendingexistingtypesandgroupingrelatedfunctionality.Infact,theSwiftstandardlibrarymakesextensiveuseofextensions–andyouwill,too.Buildandruntheapplication.Theimagedatawilldownloadforthecellsvisibleonscreen(Figure21.13).Scrolldowntomakemorecellsvisible.Atfirst,youwillseetheactivityindicatorviewsspinning,butsoontheimagedataforthosecellswillload.Figure21.13Imagedownloadsinprogress
Ifyouscrollbackup,youwillseeadelayinloadingtheimagedataforthepreviouslyvisiblecells.Thisisbecausewheneveracellcomesonscreen,theimagedataisredownloaded.Tofixthis,youwillimplementimagecaching,similartowhatyoudidintheHomepwner
application.
Imagecaching
Fortheimagedata,youwillusethesameapproachthatyouusedinyourHomepwnerapplication.Infact,youwillusethesameImageStoreclassthatyouwroteforthatproject.OpenHomepwner.xcodeprojanddragtheImageStore.swiftfilefromtheHomepwnerapplicationtothePhotoramaapplication.MakesuretochooseCopyitemsifneeded.OncetheImageStore.swiftfilehasbeenaddedtoPhotorama,youcanclosetheHomepwnerproject.BackinPhotorama,openPhotoStore.swiftandgiveitapropertyforanImageStore.classPhotoStore{
letimageStore=ImageStore()
ThenupdatefetchImage(for:completion:)tosavetheimagesusingtheimageStore.funcfetchImage(forphoto:Photo,completion:@escaping(ImageResult)->Void){
letphotoKey=photo.photoID
ifletimage=imageStore.image(forKey:photoKey){
OperationQueue.main.addOperation{
completion(.success(image))
}
return
}
letphotoURL=photo.remoteURL
letrequest=URLRequest(url:photoURL)
lettask=session.dataTask(with:request){
(data,response,error)->Voidin
letresult=self.processImageRequest(data:data,error:error)
ifcaselet.success(image)=result{
self.imageStore.setImage(image,forKey:photoKey)
}
OperationQueue.main.addOperation{
completion(result)
}
}
task.resume()
}
Buildandruntheapplication.Nowwhentheimagedataisdownloaded,itwillbesavedtothefilesystem.Thenexttimethatphotoisrequested,itwillbeloadedfromthefilesystemifitisnotcurrentlyinmemory.
NavigatingtoaPhoto
Inthissection,youaregoingtoaddfunctionalitytoallowausertonavigatetoanddisplayasinglephoto.CreateanewSwiftfilenamedPhotoInfoViewController,declarethePhotoInfoViewControllerclass,andaddanimageViewoutlet.importFoundation
importUIKit
classPhotoInfoViewController:UIViewController{
@IBOutletvarimageView:UIImageView!
}
Nowsetuptheinterfaceforthisviewcontroller.OpenMain.storyboardanddraganewViewControllerontothecanvasfromtheobjectlibrary.Withthisviewcontrollerselected,openitsidentityinspectorandchangetheClasstoPhotoInfoViewController.Whentheusertapsononeofthecollectionviewcells,theapplicationwillnavigatetothisnewviewcontroller.Control-dragfromthePhotoCollectionViewCelltothePhotoInfoViewControllerandselecttheShowsegue.Withthenewsegueselected,openitsattributesinspectorandgivethesegueanIdentifierofshowPhoto(Figure21.14).Figure21.14Navigationtoaphoto
AddanimageviewtothePhotoInfoViewController’sview.SetupitsAutoLayoutconstraintsto
pintheimageviewtoallfoursides.OpentheattributesinspectorfortheimageviewandsetitsContentModetoAspectFit.Finally,connecttheimageviewtotheimageViewoutlet.Whentheusertapsacell,theshowPhotoseguewillbetriggered.Atthispoint,thePhotosViewControllerwillneedtopassboththePhotoandthePhotoStoretothePhotoInfoViewController.OpenPhotoInfoViewController.swiftandaddtwoproperties.classPhotoInfoViewController:UIViewController{
@IBOutletvarimageView:UIImageView!
varphoto:Photo!{
didSet{
navigationItem.title=photo.title
}
}
varstore:PhotoStore!
}
Whenphotoissetonthisviewcontroller,thenavigationitemwillbeupdatedtodisplaythenameofthephoto.NowoverrideviewDidLoad()tosettheimageontheimageViewwhentheviewisloaded.overridefuncviewDidLoad(){
super.viewDidLoad()
store.fetchImage(for:photo){(result)->Voidin
switchresult{
caselet.success(image):
self.imageView.image=image
caselet.failure(error):
print("Errorfetchingimageforphoto:\(error)")
}
}
}
InPhotosViewController.swift,implementprepare(for:sender:)topassalongthephotoandthestore.overridefuncprepare(forsegue:UIStoryboardSegue,sender:Any?){
switchsegue.identifier{
case"showPhoto"?:
ifletselectedIndexPath=
collectionView.indexPathsForSelectedItems?.first{
letphoto=photoDataSource.photos[selectedIndexPath.row]
letdestinationVC=segue.destinationas!PhotoInfoViewController
destinationVC.photo=photo
destinationVC.store=store
}
default:
preconditionFailure("Unexpectedsegueidentifier.")
}
}
Buildandruntheapplication.Afterthewebservicerequesthasfinished,tapononeofthephotostoseeitinthenewviewcontroller(Figure21.15).Figure21.15Displayingaphoto
Collectionviewsareapowerfulwaytodisplaydatausingaflexiblelayout.Youhavejustbarelytappedintothepowerofcollectionviewsinthischapter.
SilverChallenge:UpdatedItemSizes
Havethecollectionviewalwaysdisplayfouritemsperrow,takingupasmuchasthescreenwidthaspossible.Thisshouldworkinbothportraitandlandscapeorientations.
GoldChallenge:CreatingaCustomLayout
Createacustomlayoutthatdisplaysthephotosinaflipbook.Youwillneedtousethetransformpropertyonthecelllayertogetanappropriate3-Deffect.YoucansubclassUICollectionViewLayoutforthischallenge,butalsoconsidersubclassingUICollectionViewFlowLayout.CheckouttheclassreferenceforUICollectionViewLayoutformoreinformation.
22CoreData
WhendecidingbetweenapproachestosavingandloadingforiOSapplications,thefirstquestionis“Localorremote?”Ifyouwanttosavedatatoaremoteserver,youwilllikelyuseawebservice.Ifyouwanttostoredatalocally,youhavetoaskanotherquestion:“ArchivingorCoreData?”YourHomepwnerapplicationusedkeyedarchivingtosaveitemdatatothefilesystem.Thebiggestdrawbacktoarchivingisitsall-or-nothingnature:Toaccessanythinginthearchive,youmustunarchivetheentirefile,andtosaveanychanges,youmustrewritetheentirefile.CoreData,ontheotherhand,canfetchasubsetofthestoredobjects.Andifyouchangeanyofthoseobjects,youcanupdatejustthatpartofthefile.Thisincrementalfetching,updating,deleting,andinsertingcanradicallyimprovetheperformanceofyourapplicationwhenyouhavealotofmodelobjectsbeingshuttledbetweenthefilesystemandRAM.
ObjectGraphs
CoreDataisaframeworkthatletsyouexpresswhatyourmodelobjectsareandhowtheyarerelatedtooneanother.Itthentakescontrolofthelifetimesoftheseobjects,makingsuretherelationshipsarekeptuptodate.Whenyousaveandloadtheobjects,CoreDatamakessureeverythingisconsistent.Thiscollectionofmodelobjectsisoftencalledanobjectgraph,astheobjectscanbethoughtofasnodesandtherelationshipsasverticesinamathematicalgraph.OftenyouwillhaveCoreDatasaveyourobjectgraphtoaSQLitedatabase.DeveloperswhoareusedtootherSQLtechnologiesmightexpecttotreatCoreDatalikeanobject-relationalmappingsystem,butthismindsetwillleadtoconfusion.UnlikeanORM,CoreDatatakescompletecontrolofthestorage,whichjusthappenstobearelationaldatabase.Youdonothavetodescribethingslikethedatabaseschemaandforeignkeys–CoreDatadoesthat.YoujusttellCoreDatawhatneedsstoringandletitworkouthowtostoreit.CoreDatagivesyoutheabilitytofetchandstoredatainarelationaldatabasewithouthavingtoknowthedetailsoftheunderlyingstoragemechanism.ThischapterwillgiveyouanunderstandingofCoreDataasyouaddpersistencetothePhotoramaapplication.
Entities
Arelationaldatabasehassomethingcalledatable.Atablerepresentsatype:Youcanhaveatableofpeople,atableofacreditcardpurchases,oratableofrealestatelistings.Eachtablehasanumberofcolumnstoholdpiecesofinformationaboutthetype.Atablethatrepresentspeoplemighthavecolumnsforlastname,dateofbirth,andheight.Everyrowinthetablerepresentsanexampleofthetype–e.g.,asingleperson.ThisorganizationtranslateswelltoSwift.EverytableislikeaSwifttype.Everycolumnisoneofthetype’sproperties.Everyrowisaninstanceofthattype.Thus,CoreData’sjobistomovedatatoandfromthesetworepresentations(Figure22.1).Figure22.1RoleofCoreData
CoreDatausesdifferentterminologytodescribetheseideas:Atable/typeiscalledanentity,andthecolumns/propertiesarecalledattributes.ACoreDatamodelfileisthedescriptionofeveryentityalongwithitsattributesinyourapplication.InPhotorama,youaregoingtodescribeaPhotoentityinamodelfileandgiveitattributesliketitle,remoteURL,anddateTaken.
Modelingentities
OpenPhotorama.xcodeproj.Createanewfile,butdonotmakeitaSwiftfileliketheonesyouhavecreatedbefore.Instead,selectiOSatthetopandscrolldowntotheCoreDatasection.CreateanewDataModel(Figure22.2).NameitPhotorama.
Figure22.2Creatingthemodelfile
ThiswillcreatethePhotorama.xcdatamodeldfileandaddittoyourproject.SelectthisfilefromtheprojectnavigatorandtheeditorareawillrevealtheUIformanipulatingaCoreDatamodelfile.FindtheAddEntitybuttonatthebottomleftofthewindowandclickit.Anewentitywillappearinthelistofentitiesinthelefthandtable.Double-clickthisentityandchangeitsnametoPhoto(Figure22.3).
Figure22.3CreatingthePhotoentity
NowyourPhotoentityneedsattributes.RememberthatthesewillbethepropertiesofthePhotoclass.Thenecessaryattributesarelistedbelow.Foreachattribute,clickthe+buttonintheAttributessectionandedittheAttributeandTypevalues.
photoIDisaString.titleisaString.dateTakenisaDate.remoteURLisaTransformable.(ItisaURL,butthatisnotoneofthepossibilities.Wewilldiscuss“transformable”next.)
Transformableattributes
CoreDataisonlyabletostorecertaindatatypesinitsstore.URLisnotoneofthesetypes,soyoudeclaredtheremoteURLattributeastransformable.Withatransformableattribute,CoreDatawillconverttheobjectintoatypethatitcanstorewhensavingandthenconvertitbackto
theoriginalobjectwhenloadingfromthefilesystem.CoreDataworkswithclassesunderthehoodbecauseitisanObjective-Cframework.SoinsteadofworkingwithaninstanceofURL(whichisastruct),youwillworkwithaninstanceofNSURL(whichisaclass)whendealingwithCoreData.SwiftprovidesamechanismforconvertingaURLtoanNSURLandviceversa,whichyouwillseelateroninthischapter.AtransformableattributerequiresaValueTransformersubclasstohandletheconversionsbetweentypes.Ifyoudonotspecifyacustomsubclass,thesystemwillusethetransformernamedNSKeyedUnarchiveFromDataTransformer.ThistransformerusesarchivingtoconverttheobjecttoandfromData.BecauseNSURLconformstoNSCoding,thedefaultNSKeyedUnarchiveFromDataTransformerwillbesufficient.IfthetypeyouwantedtotransformdidnotconformtoNSCoding,youwouldneedtowriteyourowncustomValueTransformersubclass.WithPhotorama.xcdatamodeldstillopen,selecttheremoteURLattributeandopenitsDataModelinspectorontherighthandside.UndertheAttributesection,enterNSURLastheCustomClass.ThiswillallowCoreDatatodothetransformationforyou.Atthispoint,yourmodelfileissufficienttosaveandloadphotos.Inthenextsection,youwillcreateacustomsubclassforthePhotoentity.
NSManagedObjectandsubclasses
WhenanobjectisfetchedwithCoreData,itsclass,bydefault,isNSManagedObject.NSManagedObjectisasubclassofNSObjectthatknowshowtocooperatewiththerestofCoreData.AnNSManagedObjectworksabitlikeadictionary:Itholdsakey-valuepairforeveryproperty(attributeorrelationship)intheentity.AnNSManagedObjectislittlemorethanadatacontainer.Ifyouneedyourmodelobjectstodosomethinginadditiontoholdingdata,youmustsubclassNSManagedObject.Then,inyourmodelfile,youspecifythatthisentityisrepresentedbyinstancesofyoursubclass,notthestandardNSManagedObject.XcodecangenerateNSManagedObjectsubclassesforyoubasedonwhatyouhavedefinedinyourCoreDatamodelfile.Intheprojectnavigator,selectthePhoto.swiftfileanddeleteit.Whenprompted,moveittothetrashtomakesureitdoesnotstillexistintheprojectdirectory.OpenPhotorama.xcdatamodeld.SelectthePhotoentityandopentheDataModelinspector.LocatetheCodegenoptionandselectManual/None.WiththePhotoentitystillselected,opentheEditormenuandselectCreateNSManagedObjectSubclass….Onthenextscreen,checktheboxforPhotoramaandclickNext.ChecktheboxforthePhotoentityandclickNextagain.Finally,clickCreate.Therewillbeafewerrorsintheproject.Youwillfixthoseshortly.
Thetemplatewillcreatetwofilesforyou:Photo+CoreDataClass.swiftandPhoto+CoreDataProperties.swift.ThetemplateplacesalloftheattributesthatyoudefinedinthemodelfileintoPhoto+CoreDataProperties.swift.Ifyoueverchangeyourentityinthemodelfile,youcansimplydeletePhoto+CoreDataProperties.swiftandregeneratetheNSManagedObjectsubclass.XcodewillrecognizethatyoualreadyhavePhoto+CoreDataClass.swiftandwillonlyre-createPhoto+CoreDataProperties.swift.OpenPhoto+CoreDataProperties.swiftandtakealookatwhatthetemplatecreatedforyou.Allofthepropertiesaremarkedwiththe@NSManagedkeyword.Thiskeyword,whichisspecifictoCoreData,letsthecompilerknowthatthestorageandimplementationofthesepropertieswillbeprovidedatruntime.BecauseCoreDatawillcreatetheNSManagedObjectinstances,youcannolongeruseacustominitializer,sothepropertiesaredeclaredasvariablesinsteadofconstants.AnycustompropertiesorcodethatyouwanttoaddshouldbeaddedtoPhoto+CoreDataClass.swift.Let’sfixsomeoftheerrorsthatareintheproject.OpenPhotoStore.swiftandfindfetchImage(for:completion:).ThismethodexpectsthephotoIDandtheremoteURLtobenon-optional;however,CoreDatamodelsitsattributesasoptionals.Additionally,theURLRequestinitializerexpectsaURLinstanceasitsargumentinsteadofanNSURLinstance.Updatethemethodtoaddresstheseissues.funcfetchImage(forphoto:Photo,completion:@escaping(ImageResult)->Void){
guardletphotoKey=photo.photoIDelse{
preconditionFailure("PhotoexpectedtohaveaphotoID.")
}
ifletimage=imageStore.image(forKey:photoKey){
OperationQueue.main.addOperation{
completion(.success(image))
}
return
}
guardletphotoURL=photo.remoteURLelse{
preconditionFailure("PhotoexpectedtohavearemoteURL.")
}
letrequest=URLRequest(url:photoURLasURL)
Toaddressthefirstissue,youareusingaguardstatementtounwraptheoptionalNSURL.Toaddressthesecondissue,youbridgetheNSURLinstancetoaURLinstanceusinganascast.ThecompilerknowsthatNSURLandURLarerelated,soithandlesthebridgingconversion.YouhavecreatedyourmodelgraphanddefinedyourPhotoentity.Thenextstepistosetupthepersistentcontainer,whichwillmanagetheinteractionsbetweentheapplicationandCoreData.Therearestillsomeerrorsintheproject;youwillfixthemafteryouhaveaddedaCoreDatapersistentcontainerinstance.
NSPersistentContainer
CoreDataisrepresentedbyacollectionofclassesoftenreferredtoastheCoreDatastack.ThiscollectionofclassesisabstractedawayfromyouviatheNSPersistentContainerclass.YouwilllearnmoreabouttheCoreDatastackclassesintheFortheMoreCurioussectionattheendofthischapter.TouseCoreData,youwillneedtoimporttheCoreDataframeworkinthefilesthatneedit.OpenPhotoStore.swiftandimportCoreDataatthetopofthefile.importUIKit
importCoreData
AlsoinPhotoStore.swift,addapropertytoholdontoaninstanceofNSPersistentContainer.classPhotoStore{
letimageStore=ImageStore()
letpersistentContainer:NSPersistentContainer={
letcontainer=NSPersistentContainer(name:"Photorama")
container.loadPersistentStores{(description,error)in
ifleterror=error{
print("ErrorsettingupCoreData(\(error)).")
}
}
returncontainer
}()
YouinstantiateanNSPersistentContainerwithaname.Thisnamemustmatchthenameofthedatamodelfilethatdescribesyourentities.Aftercreatingthecontainer,itneedstoloaditspersistentstores.Thestoreiswherethedataisactuallystoredondisk.Bydefault,thisisgoingtobeaSQLitedatabase.Duetothepossibilityofthisoperationtakingsometime,loadingthepersistentstoresisanasynchronousoperationthatcallsacompletionhandlerwhencomplete.
UpdatingItems
Withthepersistentcontainersetup,youcannowinteractwithCoreData.Primarily,youwilldothisthroughitsviewContext.Thisishowyouwillbothcreatenewentitiesandsavechanges.TheviewContextisaninstanceofNSManagedObjectContext.Thisistheportalthroughwhichyouinteractwithyourentities.Youcanthinkofthemanagedobjectcontextasanintelligentscratchpad.Whenyouaskthecontexttofetchsomeentities,thecontextwillworkwithitspersistentstorecoordinatortobringtemporarycopiesoftheentitiesandobjectgraphintomemory.Unlessyouaskthecontexttosaveitschanges,thepersisteddataremainsthesame.
Insertingintothecontext
Whenanentityiscreated,itshouldbeinsertedintoamanagedobjectcontext.OpenFlickrAPI.swiftandimportCoreData.importFoundation
importCoreData
Next,updatethephoto(fromJSON:)methodtotakeinanadditionalargumentoftypeNSManagedObjectContextandusethiscontexttoinsertnewPhotoinstances.privatestaticfuncphoto(fromJSONjson:[String:Any],
intocontext:NSManagedObjectContext)->Photo?{
guard
letphotoID=json["id"]as?String,
lettitle=json["title"]as?String,
letdateString=json["datetaken"]as?String,
letphotoURLString=json["url_h"]as?String,
leturl=URL(string:photoURLString),
letdateTaken=dateFormatter.date(from:dateString)else{
//Don'thaveenoughinformationtoconstructaPhoto
returnnil
}
returnPhoto(title:title,photoID:photoID,remoteURL:url,dateTaken:dateTaken)
varphoto:Photo!
context.performAndWait{
photo=Photo(context:context)
photo.title=title
photo.photoID=photoID
photo.remoteURL=urlasNSURL
photo.dateTaken=dateTakenasNSDate
}
returnphoto
}
EachNSManagedObjectContextisassociatedwithaspecificconcurrencyqueue,andthe
viewContextisassociatedwiththemain,orUI,queue.Youhavetointeractwithacontextonthequeuethatitisassociatedwith.NSManagedObjectContexthastwomethodsthatensurethishappens:perform(_:)andperformAndWait(_:).Thedifferencebetweenthemisthatperform(_:)isasynchronousandperformAndWait(_:)issynchronous.Becauseyouarereturningtheresultoftheinsertoperationfromthephoto(fromJSON:into:)method,youusethesynchronousmethod.
Thephoto(fromJSON:into:)methodiscalledfromthemethodphotos(fromJSON:).Updatethismethodtotakeinacontextandpassittothephoto(fromJSON:into:)method.staticfuncphotos(fromJSONdata:Data,
intocontext:NSManagedObjectContext)->PhotosResult{
do{
...
varfinalPhotos=[Photo]()
forphotoJSONinphotosArray{
ifletphoto=photo(fromJSON:photoJSON,into:context){
finalPhotos.append(photo)
}
}
Finally,youneedtopasstheviewContexttotheFlickrAPIstructoncethewebservicerequestsuccessfullycompletes.OpenPhotoStore.swiftandupdateprocessPhotosRequest(data:error:).privatefuncprocessPhotosRequest(data:Data?,error:Error?)->PhotosResult{
guardletjsonData=dataelse{
return.failure(error!)
}
returnFlickrAPI.photos(fromJSON:jsonData,
into:persistentContainer.viewContext)
}
Buildandruntheapplicationnowthatallerrorshavebeenaddressed.Althoughthebehaviorremainsunchanged,theapplicationisnowbackedbyCoreData.Inthenextsection,youwillimplementsavingforboththephotosandtheirassociatedimagedata.
Savingchanges
RecallthatNSManagedObjectchangesdonotpersistuntilyoutellthecontexttosavethesechanges.OpenPhotoStore.swiftandupdatefetchInterestingPhotos(completion:)tosavethechangestothecontextafterPhotoentitieshavebeeninsertedintothecontext.funcfetchInterestingPhotos(completion:@escaping(PhotosResult)->Void){
leturl=FlickrAPI.interestingPhotosURL
letrequest=URLRequest(url:url)
lettask=session.dataTask(with:request){
(data,response,error)->Voidin
letvarresult=self.processPhotosRequest(data:data,error:error)
ifcase.success=result{
do{
tryself.persistentContainer.viewContext.save()
}catchleterror{
result=.failure(error)
}
}
OperationQueue.main.addOperation{
completion(result)
}
}
task.resume()
}
UpdatingtheDataSource
OneproblemwiththeappatthemomentisthatfetchInterestingPhotos(completion:)onlyreturnsthenewlyinsertedphotos.Nowthattheapplicationsupportssaving,itshouldreturnallofthephotos–thepreviouslysavedphotosaswellasthenewlyinsertedones.YouneedtoaskCoreDataforallofthePhotoentities,andyouwillaccomplishthisusingafetchrequest.
Fetchrequestsandpredicates
TogetobjectsbackfromtheNSManagedObjectContext,youmustprepareandexecuteanNSFetchRequest.Afterafetchrequestisexecuted,youwillgetanarrayofalltheobjectsthatmatchtheparametersofthatrequest.Afetchrequestneedsanentitydescriptionthatdefineswhichentityyouwanttogetobjectsfrom.TofetchPhotoinstances,youspecifythePhotoentity.Youcanalsosettherequest’ssortdescriptorstospecifytheorderoftheobjectsinthearray.AsortdescriptorhasakeythatmapstoanattributeoftheentityandaBoolthatindicateswhethertheordershouldbeascendingordescending.ThesortDescriptorspropertyonNSFetchRequestisanarrayofNSSortDescriptorinstances.Whyanarray?Thearrayisusefulifyouthinktheremightbecollisionswhensorting.Forexample,sayyouaresortinganarrayofpeoplebytheirlastnames.Itisentirelypossiblethatmultiplepeoplehavethesamelastname,soyoucanspecifythatpeoplewiththesamelastnameshouldbesortedbytheirfirstnames.ThiswouldbeimplementedbyanarrayoftwoNSSortDescriptorinstances.Thefirstsortdescriptorwouldhaveakeythatmapstotheperson’slastname,andthesecondsortdescriptorwouldhaveakeythatmapstotheperson’sfirstname.ApredicateisrepresentedbytheNSPredicateclassandcontainsaconditionthatcanbetrueorfalse.Ifyouwantedtofindallphotoswithagivenidentifier,youwouldcreateapredicateandaddittothefetchrequestlikethis:letpredicate=NSPredicate(format:"#keyPath(Photo.photoID)==\(someIdentifier)")
request.predicate=predicate
Theformatstringforapredicatecanbeverylongandcomplex.Apple’sPredicateProgrammingGuideisacompletediscussionofwhatispossible.YouwanttosortthereturnedinstancesofPhotobydateTakenindescendingorder.Todothis,youwillinstantiateanNSFetchRequestforrequesting“Photo”entities.ThenyouwillgivethefetchrequestanarrayofNSSortDescriptorinstances.ForPhotorama,thisarraywillcontainasinglesortdescriptorthatsortsphotosbytheirdateTakenproperties.Finally,youwillaskthemanagedobjectcontexttoexecutethisfetchrequest.InPhotoStore.swift,implementamethodthatwillfetchthePhotoinstancesfromtheviewcontext.
funcfetchAllPhotos(completion:@escaping(PhotosResult)->Void){
letfetchRequest:NSFetchRequest<Photo>=Photo.fetchRequest()
letsortByDateTaken=NSSortDescriptor(key:#keyPath(Photo.dateTaken),
ascending:true)
fetchRequest.sortDescriptors=[sortByDateTaken]
letviewContext=persistentContainer.viewContext
viewContext.perform{
do{
letallPhotos=tryviewContext.fetch(fetchRequest)
completion(.success(allPhotos))
}catch{
completion(.failure(error))
}
}
}
Next,openPhotosViewController.swiftandaddanewmethodthatwillupdatethedatasourcewithallofthephotos.privatefuncupdateDataSource(){
store.fetchAllPhotos{
(photosResult)in
switchphotosResult{
caselet.success(photos):
self.photoDataSource.photos=photos
case.failure:
self.photoDataSource.photos.removeAll()
}
self.collectionView.reloadSections(IndexSet(integer:0))
}
}
NowupdateviewDidLoad()tocallthismethodtofetchanddisplayallofthephotossavedtoCoreData.overridefuncviewDidLoad()
super.viewDidLoad()
collectionView.dataSource=photoDataSource
collectionView.delegate=self
store.fetchInterestingPhotos{
(photosResult)->Voidin
switchphotosResult{
caselet.success(photos):
print("Successfullyfound\(photos.count)photos.")
self.photoDataSource.photos=photos
caselet.failure(error):
print("Errorfetchinginterestingphotos:\(error)")
self.photoDataSource.photos.removeAll()
}
self.collectionView.reloadSections(IndexSet(integer:0))
self.updateDataSource()
}
}
Previouslysavedphotoswillnowbereturnedwhenthewebservicerequestfinishes.Butthere
isstilloneproblem:Iftheapplicationisrunmultipletimesandthesamephotoisreturnedfromthewebservicerequest,itwillbeinsertedintothecontextmultipletimes.Thisisnotgood–youdonotwantduplicatephotos.Luckilythereisauniqueidentifierforeachphoto.Whentheinterestingphotoswebservicerequestfinishes,theidentifierforeachphotointheincomingJSONdatacanbecomparedtothephotosstoredinCoreData.Ifoneisfoundwiththesameidentifier,thatphotowillbereturned.Otherwise,anewphotowillbeinsertedintothecontext.Todothis,youneedawaytotellthefetchrequestthatitshouldnotreturnallphotosbutinsteadonlythephotosthatmatchsomespecificcriteria.Inthiscase,thespecificcriteriais“onlyphotosthathavethisspecificidentifier,”ofwhichthereshouldeitherbezerooronephoto.InCoreData,thisisdonewithapredicate.InFlickrAPI.swift,updatephoto(fromJSON:into:)tocheckwhetherthereisanexistingphotowithagivenIDbeforeinsertinganewone.privatestaticfuncphoto(fromJSONjson:[String:Any],
intocontext:NSManagedObjectContext)->Photo?{
guard
letphotoID=json["id"]as?String,
lettitle=json["title"]as?String,
letdateString=json["datetaken"]as?String,
letphotoURLString=json["url_h"]as?String,
leturl=URL(string:photoURLString),
letdateTaken=dateFormatter.date(from:dateString)else{
//Don'thaveenoughinformationtoconstructaPhoto
returnnil
}
letfetchRequest:NSFetchRequest<Photo>=Photo.fetchRequest()
letpredicate=NSPredicate(format:"\(#keyPath(Photo.photoID))==\(photoID)")
fetchRequest.predicate=predicate
varfetchedPhotos:[Photo]?
context.performAndWait{
fetchedPhotos=try?fetchRequest.execute()
}
ifletexistingPhoto=fetchedPhotos?.first{
returnexistingPhoto
}
varphoto:Photo!
context.performAndWait{
photo=Photo(context:context)
photo.title=title
photo.photoID=photoID
photo.remoteURL=urlasNSURL
photo.dateTaken=dateTakenasNSDate
}
returnphoto
}
DuplicatephotoswillnolongerbeinsertedintoCoreData.
Buildandruntheapplication.ThephotoswillappearjustastheydidbeforeintroducingCoreData.AsyoudidinChapter16,closetheapplicationusingtheHomebutton(orShift-Command-Hinthesimulator).LaunchtheapplicationagainandyouwillseethephotosthatCoreDatasavedinthecollectionview.Thereisonelastsmallproblemtoaddress:Theuserwillnotseeanyphotosappearinthecollectionviewunlessthewebservicerequestcompletes.Iftheuserhasslownetworkaccess,itmighttakeupto60seconds(whichisthedefaulttimeoutintervalfortherequest)toseeanyphotos.ItwouldbebesttoseethepreviouslysavedphotosimmediatelyonlaunchandthenrefreshthecollectionviewoncenewphotosarefetchedfromFlickr.Goaheadanddothis.InPhotosViewController.swift,updatethedatasourceassoonastheviewisloaded.overridefuncviewDidLoad()
super.viewDidLoad()
collectionView.dataSource=photoDataSource
collectionView.delegate=self
updateDataSource()
store.fetchInterestingPhotos{
(photosResult)->Voidin
self.updateDataSource()
}
}
ThePhotoramaapplicationisnowpersistingitsdatabetweenruns.ThephotometadataisbeingpersistedusingCoreData,andtheimagedataisbeingpersisteddirectlytothefilesystem.Asyouhaveseen,thereisnoone-size-fits-allapproachtodatapersistence.Instead,eachpersistencemechanismhasitsownsetofbenefitsanddrawbacks.Inthischapter,youhaveexploredoneofthose,CoreData,butyouhaveonlyseenthetipoftheiceberg.InChapter23,youwillexploretheCoreDataframeworkfurthertolearnaboutrelationshipsandperformance.
BronzeChallenge:PhotoViewCount
AddanattributetothePhotoentitythattrackshowmanytimesaphotoisviewed.DisplaythisnumbersomewhereonthePhotoInfoViewControllerinterface.
FortheMoreCurious:TheCoreDataStack
NSManagedObjectModel
Youworkedwiththemodelfileearlierinthechapter.Themodelfileiswhereyoudefinetheentitiesforyourapplicationalongwiththeirproperties.ThemodelfileisaninstanceofNSManagedObjectModel.
NSPersistentStoreCoordinator
CoreDatacanpersistdatainseveralformats:
SQLite DataissavedtodiskusingaSQLitedatabase.Thisisthemostcommonlyusedstoretype.
Atomic Dataissavedtodiskusingabinaryformat.
XML DataissavedtodiskusinganXMLformat.ThisstoretypeisnotavailableoniOS.
In-Memory Dataisnotsavedtodisk,butinsteadisstoredinmemory.
ThemappingbetweenanobjectgraphandthepersistentstoreisaccomplishedusinganinstanceofNSPersistentStoreCoordinator.Thepersistentstorecoordinatorneedstoknowtwothings:“Whataremyentities?”and,“WhereamIsavingtoandloadingdatafrom?”Toanswerthesequestions,youinstantiateanNSPersistentStoreCoordinatorwiththeNSManagedObjectModel.Thenyouaddapersistentstore,representingoneofthepersistenceformatsabove,tothecoordinator.Afterthecoordinatoriscreated,youattempttoaddaspecificstoretothecoordinator.Ataminimum,thisstoreneedstoknowitstypeandwhereitshouldpersistthedata.
NSManagedObjectContext
TheportalthroughwhichyouinteractwithyourentitiesistheNSManagedObjectContext.Themanagedobjectcontextisassociatedwithaspecificpersistentstorecoordinator.Youcanthinkofthemanagedobjectcontextasanintelligentscratchpad.Whenyouaskthecontexttofetchsomeentities,thecontextwillworkwithitspersistentstorecoordinatortobringtemporarycopiesoftheentitiesandobjectgraphintomemory.Unlessyouaskthecontexttosaveitschanges,thepersisteddataremainsthesame.
23CoreDataRelationships
CoreDataisnotthatexcitingwithjustoneentity.MuchofthepowerbehindCoreDatacomestolightwhentherearemultipleentitiesthatarerelatedtooneanother,becauseCoreDatamanagesrelationshipsbetweenentities.Inthischapter,youaregoingtoaddtagstothephotosinPhotoramawithlabelssuchas“Nature,”“Electronics,”or“Selfies.”Userswillbeabletoaddoneormoretagstophotosandalsocreatetheirowncustomtags(Figure23.1).Figure23.1FinalPhotoramaapplication
Relationships
OneofthebenefitsofusingCoreDataisthatentitiescanberelatedtooneanotherinawaythatallowsyoutodescribecomplexmodels.Relationshipsbetweenentitiesarerepresentedbyreferencesbetweenobjects.Therearetwokindsofrelationships:to-oneandto-many.Whenanentityhasato-onerelationship,eachinstanceofthatentitywillhaveareferencetoaninstanceintheentityithasarelationshipto.Whenanentityhasato-manyrelationship,eachinstanceofthatentityhasareferencetoaSet.Thissetcontainstheinstancesoftheentitythatithasarelationshipwith.Toseethisinaction,youaregoingtoaddanewentitytothemodelfile.ReopenthePhotoramaapplication.InPhotorama.xcdatamodeld,addanotherentitycalledTag.GiveitanattributecallednameoftypeString.Tagwillallowuserstotagphotos.UnlikewiththePhotoentityinChapter22,youwillnotgenerateanNSManagedObjectsubclassfortheTagentity.Instead,youwillletXcodeautogenerateasubclassforyouthroughafeaturecalledcodegeneration.IfyoudonotneedanycustombehaviorforyourCoreDataentity,lettingXcodegenerateyoursubclassforyouisquitehelpful.TheNSManagedObjectsubclassfortheTagentityisalreadybeinggeneratedforyou.Toseethesettingthatdeterminesthis,selecttheTagentityandopenitsdatamodelinspector.IntheClasssection,noticethesettingforCodegen:ItiscurrentlysettoClassDefinition.Thissettingmeansthatanentireclassdefinitionwillbegeneratedforyou.TheothercodegenerationsettingsareCategory/Extension(whichallowsyoutodefineanNSManagedObjectsubclasswithcustombehaviorwhilestillallowingXcodetogeneratetheextensionthatdefinestheattributesandrelationships)andManual/None(whichtellsXcodenottogenerateanycodefortheentity).Aphotomighthavemultipletagsthatdescribeit,andatagmightbeassociatedwithmultiplephotos.Forexample,apictureofaniPhonemightbetagged“Electronics”and“Apple,”andapictureofaBetamaxplayermightbetagged“Electronics”and“Rare.”TheTagentitywillhaveato-manyrelationshiptothePhotoentitybecausemanyinstancesofPhotocanhavethesameTag.AndthePhotoentitywillhaveato-manyrelationshiptotheTagentitybecauseaphotocanbeassociatedwithmanyTags.AsFigure23.2shows,aPhotowillhaveareferencetoasetofTags,andaTagwillhaveareferencetoasetofPhotos.
Figure23.2EntitiesinPhotorama
Whentheserelationshipsaresetup,youwillbeabletoaskaPhotoobjectforthesetofTagobjectsthatitisassociatedwithandaskaTagobjectforthesetofPhotoobjectsthatitisassociatedwith.Toaddthesetworelationshipstothemodelfile,firstselecttheTagentityandclickthe+buttonintheRelationshipssection.ClickintheRelationshipcolumnandenterphotos.IntheDestinationcolumn,selectPhoto.Inthedatamodelinspector,changetheTypedropdownfromToOnetoToMany(Figure23.3).Figure23.3Creatingthephotosrelationship
Next,selectthePhotoentity.AddarelationshipnamedtagsandpickTagasitsdestination.Inthedatamodelinspector,changetheTypedropdowntoToManyanduncheckitsOptionalcheckbox.Nowthatyouhavetwounidirectionalrelationships,youcanmakethemintoaninverse
relationship.Aninverserelationshipisabidirectionalrelationshipbetweentwoentities.WithaninverserelationshipsetupbetweenPhotoandTag,CoreDatacanensurethatyourobjectgraphremainsinaconsistentstatewhenanychangesaremade.Tocreatetheinverserelationship,clickthedropdownnexttoInverseinthedatamodelinspectorandchangeitfromNoInverseRelationshiptophotos(Figure23.4).(YoucanalsomakethischangeintheRelationshipssectionintheeditorareabyclickingNoInverseintheInversecolumnandselectingphotos.)IfyoureturntotheTagentity,youwillseethatthephotosrelationshipnowshowstagsasitsinverse.Figure23.4Creatingthetagsrelationship
NowthatthemodelhaschangedforthePhotoentity,youwillneedtoregeneratethePhoto+CoreDataProperties.swiftfile.Fromtheprojectnavigator,selectanddeletethePhoto+CoreDataProperties.swiftfile.MakesuretoselectMovetoTrashwhenprompted.OpenPhotorama.xcdatamodeldandselectthePhotoentity.FromtheEditormenu,selectCreateNSManagedObjectSubclass….Onthenextscreen,checktheboxforPhotoramaandclickNext.ChecktheboxforthePhotoentityandclickNext.MakesureyouarecreatingthefileinthesamedirectoryasthePhoto+CoreDataClass.swiftfile;thiswillensurethatXcodewillonlycreatethenecessaryPhoto+CoreDataProperties.swiftfile.Onceyouhaveconfirmedthis,clickCreate.
AddingTagstotheInterface
Whenusersnavigatetoaspecificphoto,theycurrentlyseeonlythetitleofthephotoandtheimageitself.Let’supdatetheinterfacetoincludeaphoto’sassociatedtags.OpenMain.storyboardandnavigatetotheinterfaceforPhotoInfoViewController.Addatoolbartothebottomoftheview.UpdatetheAutoLayoutconstraintssothatthetoolbarisanchoredtothebottom,justasitwasinHomepwner.ThebottomconstraintfortheimageViewshouldbeanchoredtothetopofthetoolbarinsteadofthebottomofthesuperview.AddaUIBarButtonItemtothetoolbar,ifoneisnotalreadypresent,andgiveitatitleofTags.YourinterfacewilllooklikeFigure23.5.Figure23.5PhotoInfoViewControllerinterface
CreateanewSwiftfilenamedTagsViewController.OpenthisfileanddeclaretheTagsViewControllerclassasasubclassofUITableViewController.Import
UIKitandCoreDatainthisfile.importFoundation
importUIKit
importCoreData
classTagsViewController:UITableViewController{
}
TheTagsViewControllerwilldisplayalistofallthetags.Theuserwillseeandbeabletoselectthetagsthatareassociatedwithaspecificphoto.Theuserwillalsobeabletoaddnewtagsfromthisscreen.ThecompletedinterfacewilllooklikeFigure23.6.Figure23.6TagsViewController
GivetheTagsViewControllerclassapropertytoreferencethePhotoStoreaswellasaspecificPhoto.Youwillalsoneedapropertytokeeptrackofthecurrentlyselectedtags,whichyouwilltrackusinganarrayofIndexPathinstances.classTagsViewController:UITableViewController{
varstore:PhotoStore!
varphoto:Photo!
varselectedIndexPaths=[IndexPath]()
}
Thedatasourceforthetableviewwillbeaseparateclass.AswediscussedwhenyoucreatedPhotoDataSourceinChapter21,anapplicationwhosetypeshaveasingleresponsibilityiseasiertoadapttofuturechanges.Thisclasswillberesponsiblefordisplayingthelistoftagsinthetableview.CreateanewSwiftfilenamedTagDataSource.swift.DeclaretheTagDataSourceclassandimplementthetableviewdatasourcemethods.YouwillneedtoimportUIKitandCoreData.
importFoundation
importUIKit
importCoreData
classTagDataSource:NSObject,UITableViewDataSource{
vartags:[Tag]=[]
functableView(_tableView:UITableView,
numberOfRowsInSectionsection:Int)->Int{
returntags.count
}
functableView(_tableView:UITableView,
cellForRowAtindexPath:IndexPath)->UITableViewCell{
letcell=tableView.dequeueReusableCell(withIdentifier:"UITableViewCell",
for:indexPath)
lettag=tags[indexPath.row]
cell.textLabel?.text=tag.name
returncell
}
}
OpenPhotoStore.swiftanddefineanewresulttypeatthetopforusewhenfetchingtags.enumPhotosResult{
casesuccess([Photo])
casefailure(Error)
}
enumTagsResult{
casesuccess([Tag])
casefailure(Error)
}
classPhotoStore{
Nowdefineanewmethodthatfetchesallthetagsfromtheviewcontext.funcfetchAllTags(completion:@escaping(TagsResult)->Void){
letfetchRequest:NSFetchRequest<Tag>=Tag.fetchRequest()
letsortByName=NSSortDescriptor(key:#keyPath(Tag.name),ascending:true)
fetchRequest.sortDescriptors=[sortByName]
letviewContext=persistentContainer.viewContext
viewContext.perform{
do{
letallTags=tryfetchRequest.execute()
completion(.success(allTags))
}catch{
completion(.failure(error))
}
}
}
OpenTagsViewController.swiftandsetthedataSourceforthetableviewtobean
instanceofTagDataSource.classTagsViewController:UITableViewController{
varstore:PhotoStore!
varphoto:Photo!
varselectedIndexPaths=[IndexPath]()
lettagDataSource=TagDataSource()
overridefuncviewDidLoad(){
super.viewDidLoad()
tableView.dataSource=tagDataSource
}
}
Nowfetchthetagsandassociatethemwiththetagspropertyonthedatasource.overridefuncviewDidLoad(){
super.viewDidLoad()
tableView.dataSource=tagDataSource
updateTags()
}
funcupdateTags(){
store.fetchAllTags{
(tagsResult)in
switchtagsResult{
caselet.success(tags):
self.tagDataSource.tags=tags
caselet.failure(error):
print("Errorfetchingtags:\(error).")
}
self.tableView.reloadSections(IndexSet(integer:0),
with:.automatic)
}
}
TheTagsViewControllerneedstomanagetheselectionoftagsandupdatethePhotoinstancewhentheuserselectsordeselectsatag.InTagsViewController.swift,addtheappropriateindexpathstotheselectedIndexPathsarray.overridefuncviewDidLoad(){
super.viewDidLoad()
tableView.dataSource=tagDataSource
tableView.delegate=self
updateTags()
}
funcupdateTags(){
store.fetchAllTags{
(tagsResult)in
switchtagsResult{
caselet.success(tags):
self.tagDataSource.tags=tags
guardletphotoTags=self.photo.tagsas?Set<Tag>else{
return
}
fortaginphotoTags{
ifletindex=self.tagDataSource.tags.index(of:tag){
letindexPath=IndexPath(row:index,section:0)
self.selectedIndexPaths.append(indexPath)
}
}
caselet.failure(error):
print("Errorfetchingtags:\(error).")
}
self.tableView.reloadSections(IndexSet(integer:0),
with:.automatic)
}
}
NowaddtheappropriateUITableViewDelegatemethodstohandleselectinganddisplayingthecheckmarks.overridefunctableView(_tableView:UITableView,
didSelectRowAtindexPath:IndexPath){
lettag=tagDataSource.tags[indexPath.row]
ifletindex=selectedIndexPaths.index(of:indexPath){
selectedIndexPaths.remove(at:index)
photo.removeFromTags(tag)
}else{
selectedIndexPaths.append(indexPath)
photo.addToTags(tag)
}
do{
trystore.persistentContainer.viewContext.save()
}catch{
print("CoreDatasavefailed:\(error).")
}
tableView.reloadRows(at:[indexPath],with:.automatic)
}
overridefunctableView(_tableView:UITableView,
willDisplaycell:UITableViewCell,
forRowAtindexPath:IndexPath){
ifselectedIndexPaths.index(of:indexPath)!=nil{
cell.accessoryType=.checkmark
}else{
cell.accessoryType=.none
}
}
Let’ssetupTagsViewControllertobepresentedmodallywhentheusertapstheTagsbarbuttonitemonthePhotoInfoViewController.OpenMain.storyboardanddragaNavigationControllerontothecanvas.ThisshouldgiveyouaUINavigationControllerwitharootviewcontrollerthatisaUITableViewController.IftherootviewcontrollerisnotaUITableViewController,deletetherootviewcontroller,dragaTableViewControllerontothecanvas,andmakeittherootviewcontrolleroftheNavigationController.Control-dragfromtheTagsitemonPhotoInfoViewControllertothenewNavigationControllerandselectthePresentModallyseguetype(Figure23.7).OpentheattributesinspectorforthesegueandgiveitanIdentifiernamedshowTags.Figure23.7Addingthetagsviewcontroller
SelecttheRootViewControllerthatyoujustaddedtothecanvasandopenitsidentityinspector.ChangeitsClasstoTagsViewController.Thisnewviewcontrollerdoesnothaveanavigationitemassociatedwithit,sofindNavigationItemintheobjectlibraryanddragitontotheviewcontroller.Double-clickthenewnavigationitem’sTitlelabelandchangeittoTags.Next,theUITableViewCellontheTagsViewControllerinterfaceneedstomatchwhattheTagDataSourceexpects.Itneedstousethecorrectstyleandhavethecorrectreuseidentifier.SelecttheUITableViewCell.(Itmightbeeasiertoselectinthedocumentoutline.)Openitsattributesinspector.ChangetheStyletoBasicandsettheIdentifiertoUITableViewCell(Figure23.8).
Figure23.8ConfiguringtheUITableViewCell
Now,theTagsViewControllerneedstwobarbuttonitemsonitsnavigationbar:aDonebuttonthatdismissestheviewcontrolleranda+buttonthatallowstheusertoaddanewtag.DragabarbuttonitemtotheleftandrightbarbuttonitemslotsfortheTagsViewController.SettheleftitemtousetheDonestyleandsystemitem.SettherightitemtousetheBorderedstyleandAddsystemitem(Figure23.9).Figure23.9Barbuttonitemattributes
CreateandconnectanactionforeachoftheseitemstotheTagsViewController.TheDoneitemshouldbeconnectedtoamethodnameddone(_:),andthe+itemshouldbeconnectedtoamethodnamedaddNewTag(_:).ThetwomethodsinTagsViewController.swiftwillbe:@IBActionfuncdone(_sender:UIBarButtonItem){
}
@IBActionfuncaddNewTag(_sender:UIBarButtonItem){
}
Theimplementationofdone(_:)issimple:Theviewcontrollerjustneedstobedismissed.Implementthisfunctionalityindone(_:).@IBActionfuncdone(_sender:UIBarButtonItem){
presentingViewController?.dismiss(animated:true,
completion:nil)
}
Whentheusertapsthe+item,analertwillbepresentedthatwillallowtheusertotypeinthenameforanewtag.Figure23.10Addinganewtag
SetupandpresentaninstanceofUIAlertControllerinaddNewTag(_:).@IBActionfuncaddNewTag(_sender:UIBarButtonItem){
letalertController=UIAlertController(title:"AddTag",
message:nil,
preferredStyle:.alert)
alertController.addTextField{
(textField)->Voidin
textField.placeholder="tagname"
textField.autocapitalizationType=.words
}
letokAction=UIAlertAction(title:"OK",style:.default){
(action)->Voidin
}
alertController.addAction(okAction)
letcancelAction=UIAlertAction(title:"Cancel",
style:.cancel,
handler:nil)
alertController.addAction(cancelAction)
present(alertController,
animated:true,
completion:nil)
}
UpdatethecompletionhandlerfortheokActiontoinsertanewTagintothecontext.Thensavethecontext,updatethelistoftags,andreloadthetableviewsection.letokAction=UIAlertAction(title:"OK",style:.default){
(action)->Voidin
iflettagName=alertController.textFields?.first?.text{
letcontext=self.store.persistentContainer.viewContext
letnewTag=NSEntityDescription.insertNewObject(forEntityName:"Tag",
into:context)
newTag.setValue(tagName,forKey:"name")
do{
tryself.store.persistentContainer.viewContext.save()
}catchleterror{
print("CoreDatasavefailed:\(error)")
}
self.updateTags()
}
}
alertController.addAction(okAction)
Finally,whentheTagsbarbuttonitemonPhotoInfoViewControlleristapped,thePhotoInfoViewControllerneedstopassalongitsstoreandphotototheTagsViewController.OpenPhotoInfoViewController.swiftandimplementprepare(for:).overridefuncprepare(forsegue:UIStoryboardSegue,sender:Any?){
switchsegue.identifier{
case"showTags"?:
letnavController=segue.destinationas!UINavigationController
lettagController=navController.topViewControlleras!TagsViewController
tagController.store=store
tagController.photo=photo
default:
preconditionFailure("Unexpectedsegueidentifier.")
}
}
Buildandruntheapplication.NavigatetoaphotoandtaptheTagsitemonthetoolbaratthebottom.TheTagsViewControllerwillbepresentedmodally.Tapthe+item,enteranewtag,andselectthenewtagtoassociateitwiththephoto.
BackgroundTasks
TheviewContextofNSPersistentContainerisassociatedwiththemainqueue.Becauseofthis,anyoperationsthattakealongtimewillblockthemainqueue,whichcanleadtoanunresponsiveapplication.Toaddressthis,itisoftenagoodideatodoexpensiveoperationsonabackgroundtask.Theinsertionofphotosfromthewebserviceisagoodcandidateforabackgroundtask.YouaregoingtoupdateprocessPhotosRequest(data:error:)touseabackgroundtask.Backgroundtasksareanasynchronousoperation,soyouwillneedtoupdatethemethodsignaturetouseacompletionhandler.OpenPhotoStore.swiftandupdateprocessPhotosRequest(data:error:)totakeinacompletionhandler.Youwillhavesomeerrorsinthecodeduetothesignaturechange;youwillfixtheseshortly.privatefuncprocessPhotosRequest(data:Data?,error:Error?)->PhotosResult{
privatefuncprocessPhotosRequest(data:Data?,
error:Error?,
completion:@escaping(PhotosResult)->Void){
guardletjsonData=dataelse{
return.failure(error!)
}
returnFlickrAPI.photos(fromJSON:jsonData,
into:persistentContainer.viewContext)
}
Ifthereisnodata,youwillneedtocallthecompletionhandler,passinginthefailureerrorinsteadofdirectlyreturning.Updatetheguardstatementtopassalongthefailure.privatefuncprocessPhotosRequest(data:Data?,
error:Error?,
completion:@escaping(PhotosResult)->Void){
guardletjsonData=dataelse{
return.failure(error!)
completion(.failure(error!))
return
}
returnFlickrAPI.photos(fromJSON:jsonData,
into:persistentContainer.viewContext)
}
Noticetheuseofreturnwithintheguardstatement.Recallthatwithaguardstatement,youmustexitscope.Thescopeoftheguardstatementisthefunctionitself,soyoumustexitthescopeofthefunctionsomehow.Thisisafantasticbenefittousingaguardstatement.Thecompilerwillenforcethisrequirement,soyoucanbecertainthatnocodebelowtheguardstatementwillbeexecuted.Nowyoucanaddinthecodeforthebackgroundtask.NSPersistentContainerhasamethodtoperformabackgroundtask.Thismethodtakesinaclosuretocall,andthisclosurevendsanewNSManagedObjectContexttouse.UpdateprocessPhotosRequest(data:error:completion:)tokickoffanew
backgroundtask.privatefuncprocessPhotosRequest(data:Data?,
error:Error?,
completion:@escaping(PhotosResult)->Void){
guardletjsonData=dataelse{
completion(.failure(error!))
return
}
returnFlickrAPI.photos(fromJSON:jsonData,
into:persistentContainer.viewContext)
persistentContainer.performBackgroundTask{
(context)in
}
}
Withinthebackgroundtask,youwilldoessentiallythesamethingyoudidbefore.TheFlickrAPIstructwillingesttheJSONdataandconvertittoPhotoinstances.Thenyouwillsavethecontextsothattheinsertionspersist.UpdatethebackgroundtaskinprocessPhotosRequest(data:error:completion:)todothis.persistentContainer.performBackgroundTask{
(context)in
letresult=FlickrAPI.photos(fromJSON:jsonData,into:context)
do{
trycontext.save()
}catch{
print("ErrorsavingtoCoreData:\(error).")
completion(.failure(error))
return
}
}
Hereiswherethingschangeabit.AnNSManagedObjectshouldonlybeaccessedfromthequeuethatitisassociatedwith.AftertheexpensiveoperationofinsertingthePhotoinstancesandsavingthecontext,youwillwanttofetchthesamephotos,butonlythosethatareassociatedwiththeviewContext(i.e.,thephotosassociatedwiththemainqueue).EachNSManagedObjecthasanobjectIDthatisthesameacrossdifferentcontexts.YouwillusethisobjectIDtofetchthecorrespondingPhotoinstancesassociatedwiththeviewContext.UpdateprocessPhotosRequest(data:error:completion:)togetthePhotoinstancesassociatedwiththeviewContextandpassthembacktothecallerviathecompletionhandler.persistentContainer.performBackgroundTask{
(context)in
letresult=FlickrAPI.photos(fromJSON:jsonData,into:context)
do{
trycontext.save()
}catch{
print("ErrorsavingtoCoreData:\(error).")
completion(.failure(error))
return
}
switchresult{
caselet.success(photos):
letphotoIDs=photos.map{return$0.objectID}
letviewContext=self.persistentContainer.viewContext
letviewContextPhotos=
photoIDs.map{returnviewContext.object(with:$0)}as![Photo]
completion(.success(viewContextPhotos))
case.failure:
completion(result)
}
}
HereyouareusingthemapmethodonArraytotransformonearrayintoanotherarray.Thiscode:letphotoIDs=photos.map{return$0.objectID}
hasthesameresultasthiscode:varphotoIDs=[String]()
forphotoinphotos{
photoIDs.append(photo.objectID)
}
The$0intheclosureisashorthandwayofaccessingtheargumentsoftheclosure.Iftherearetwoparameters,forexample,theirargumentscanbeaccessedby$0and$1.Sothiscode:letphotosIDs=photos.map{return$0.objectID}
alsohasthesameresultasthiscode:letphotoIDs=photos.map{
(photo:Photo)in
returnphoto.objectID
}
Let’stakealookatthecodebeingusinginthebackgroundtaskagain.letphotoIDs=photos.map{return$0.objectID}
letviewContext=self.persistentContainer.viewContext
letviewContextPhotos=
photoIDs.map{returnviewContext.object(with:$0)}as![Photo]
ThefirstthingthatyouaredoingisgettinganarrayofalloftheobjectIDsassociatedwiththePhotoinstances.ThiswillbeanarrayofStringinstances.Withintheclosure,$0isoftypePhoto.ThenyoucreatealocalvariabletoreferencetheviewContext.Finally,youmapoverthephotoIDs.Withintheclosure,$0isoftypeString.YouusethisstringtoasktheviewContextfortheobjectassociatedwithaspecificobjectidentifier.Themethodobject(with:)returnsanNSManagedObject,sotheresultoftheentiremapoperationwillbeanarrayofNSManagedObjectinstances.YouknowthattheinstancesbeingreturnedwillbeoftypePhoto,soyoudowncastthearrayofNSManagedObjectinstances
intoanarrayofPhotoinstances.Themapmethodisausefulabstractionforthecommonoperationofconvertingonearrayintoanotherarray.ThefinalchangeyouneedtomakeistoupdatefetchInterestingPhotos(completion:)tousetheupdatedprocessPhotosRequest(data:error:completion:)method.funcfetchInterestingPhotos(completion:@escaping(PhotosResult)->Void){
leturl=FlickrAPI.interestingPhotosURL
letrequest=URLRequest(url:url)
lettask=session.dataTask(with:request){
(data,response,error)->Voidin
varresult=self.processPhotosRequest(data:data,error:error)
ifcase.success=result{
do{
tryself.persistentContainer.viewContext.save()
}catchleterror{
result=.failure(error)
}
}
OperationQueue.main.addOperation{
completion(result)
}
self.processPhotosRequest(data:data,error:error){
(result)in
OperationQueue.main.addOperation{
completion(result)
}
}
}
task.resume()
}
Buildandruntheapplication.Althoughthebehaviorhasnotchanged,theapplicationisnolongerindangerofbecomingunresponsivewhilenewphotosarebeingadded.Asthescaleofyourapplicationsincreases,handlingCoreDataentitiessomewhereotherthanthemainqueueasyouhavedoneherecanresultinhugeperformancewins.Congratulations!Overthepastfourchapters,youhaveworkedonarathercomplexapp.Photoramaisabletomakemultiplewebservicecalls,displayphotosinagrid,cacheimagedatatothefilesystem,andpersistphotodatausingCoreData.Toaccomplishthis,youusedknowledgethatyouhavegainedthroughoutthisbook,andyouappliedthatknowledgetocreateanawesomeappthatisalsorobustandmaintainable.Itwashardwork,andyoushouldbeproudofyourself.
SilverChallenge:Favorites
Allowtheusertofavoritephotos.Becreativeinhowyoupresentthefavoritephotostotheuser.TwopossibilitiesincludeviewingthemusingaUITabBarControlleroraddingaUISegmentedControltothePhotosViewControllerthatswitchesbetweenallphotosandfavoritephotos.(Hint:YouwillneedtoaddanewattributetothePhotoentity.)
24Accessibility
iOSisthemostaccessiblemobileplatformintheworld.Whetherauserneedssupportforvision,hearing,motorskills,orlearningchallenges,iOSprovideswaystohelp.Mostaccessibilityfeaturesarebuiltintothesystem,soyou,thedeveloper,donotneedtodoanything.Someallowthedevelopertoprovideanevenricherexperiencefortheuser,oftenwithverylittleworkonthedeveloper ’spart.Let’stakealookatsomeoftheaccessibilityoptionsthatiOSprovides.
VoiceOver
VoiceOverisanaccessibilityfeaturethathelpsuserswithvisualimpairmentsnavigateyourapplication’sinterface.Appleprovideshooksintothesystemthatallowyoutodescribeaspectsofyourinterfacetotheuser.MostUIKitviewsandcontrolsautomaticallyprovideusefulinformationtotheuser,butitisoftenbeneficialtoprovideadditionalinformationthatcannotbeinferred.Andyouwillalwaysneedtoprovidetheinformationyourselfforcustomviewsorcontrolsyoucreate.ThesehintstotheuserarelargelyprovidedthroughtheUIAccessibilityprotocol.Let’stakealookatthisprotocolandtheinformationthatitprovides.TheUIAccessibilityprotocolisaninformalprotocolthatisimplementedonallofthestandardUIKitviewsandcontrols.Aninformalprotocolisalooser“contract”thantheformalprotocolsthatyouhavebeenintroducedtobefore.Aformalprotocolisdeclaredusingtheprotocolkeywordanddeclaresalistofmethodsandpropertiesthatmustbeimplementedbysomethingthatconformstothatprotocol.AninformalprotocolisimplementedasanextensiononNSObject;therefore,allsubclassesofNSObjectimplicitlyconformtotheprotocol.YoumightbewonderingwhyUIAccessibilityisnotaregular,formalprotocolliketheothersyouhaveseenthroughoutthisbook.InformalprotocolsarealegacyofthedayswhenObjective-Cdidnothaveoptionalmethodsinformalprotocols.Informalprotocolswereaworkaroundtosolvethisissue.Essentially,theyrequiredeveryNSObjecttodeclareamethodwithnocorrespondingimplementation.Then,subclassescouldimplementthemethodsthattheywereinterestedin.Atruntime,anobjectwouldbeaskedifithadanimplementationforthosemethods.SomeoftheusefulpropertiesprovidedbytheUIAccessibilityprotocolare:accessibilityLabel
Ashortdescriptionofanelement.Forviewswithtext,thisisoftenthetextthattheviewisdisplaying.accessibilityHint
Ashortdescriptionoftheresultofinteractingwiththeassociatedelement.Forexample,theaccessibilityhintforabuttonthatstopsvideorecordingmightbe“Stoprecording.”accessibilityFrame
Theframeoftheaccessibilityelement.ForUIViewobjects,thisisequaltotheframeoftheview.accessibilityTraits
Descriptionsofthecharacteristicsoftheelement.Therearealotoftraits,andmultipletraitscanbeusedtodescribetheelement.Toseealistofallofthepossibletraits,lookatthedocumentationforUIAccessibilityTraits.accessibilityValue
Adescriptionofthevalueofanelement,independentofitslabeldescription.Forexample,aUITextFieldwillhaveanaccessibilityvaluethatisthecontentsofthetextfield,andaUISliderwillhaveanaccessibilityvaluethatisthepercentagethatthesliderhasbeensetto.Let’stakealookathowtoimplementaccessibilityviathePhotoramaapplication.ReopenPhotorama.xcodeproj.YouaregoingtoimplementVoiceOveraccessibilityfeatures.Currently,Photoramaisnotveryaccessibleatall.Let’sstartbyconsideringhowtheapplicationwouldlooktosomeonewithvisualimpairments.
TestingVoiceOver
ThebestwaytotestVoiceOveriswithanactualdevice,sowestronglyrecommendusingadeviceifyouhaveoneavailable.Ifyoudonothaveadeviceavailable,youcanusethesimulator.BeginbyclickingontheXcodemenuandchoosingOpenDeveloperTool→AccessibilityInspector.Buildandruntheapplication;oncethesimulatorisrunningtheapp,switchtotheAccessibilityInspectorandselectthesimulatorfromthetargetdropdownlist(Figure24.1).Figure24.1ChangingtargetsintheAccessibilityInspector
Oncethetargethasbeensettothesimulator,clicktheStartinspectionfollowspointbuttonontheAccessibilityInspector’stoolbar.Asyoumouseoverandnavigateinthesimulator,theAccessibilityInspectorwillprovideinformationaboutwhateverelementhasfocusonthesimulator ’sscreen.VoiceOverisnotincludedonthesimulator,buttheinformationshownintheAccessibilityInspectorissimilar.Ifyouhaveadevice,openSettings,chooseGeneral→Accessibility→VoiceOver,andfinallyturnonVoiceOver(Figure24.2).
Figure24.2EnablingVoiceOver
ThereareacoupleofwaystonavigatewithVoiceOveron.Tostart,slideyourfingeraroundthescreen.Noticethatthesystemspeaksadescriptionofwhateverelementyourfingeriscurrentlyover.NowtaptheBackbuttoninthetopleftcornerofthescreenthatsaysAccessibility.Thesystemwilltellyouthatthiselementisthe“Accessibility-Backbutton.”ThesystemisreadingyouboththeaccessibilityLabelaswellaswhatisessentiallytheaccessibilityTraits.NoticethattappingtheAccessibilityBackbuttondoesnottakeyoubacktothepreviousscreen.Toactivatetheselecteditem,double-tapanywhereonthescreen.Thiscorrespondstoasingle-tapwithVoiceOverdisabled.ThiswilltakeyoutothepreviousscreenforAccessibility.Anotherwaytonavigateistoswipeleftandrightonthescreen.Thiswillselectthepreviousandnextaccessibleelementsonthescreen,respectively.TheVoiceOverrowshouldbeselected.Playaroundwithswipingleftandrighttomovethefocusaroundthescreen.Swipewiththreefingerstoscroll.Notethattoscroll,thescrollvieworoneofitssubviews
mustbethecurrentlyfocusedelement.Playaroundwithsingle-anddouble-tapstoselectandactivateitemsaswellasusingthreefingerstoscroll.ThisishowyouwillnavigatewithVoiceOverenabled.OnefinalusefulgesturetoknowishowtoenableScreenCurtain.Usingthreefingers,triple-tapanywhereonthescreen.Theentirescreenwillgoblack,allowingyoutotrulytestandexperiencehowyourappwillfeeltosomeonewithavisualimpairment.Three-fingertriple-tapanywhereagaintoturnScreenCurtainoff.
AccessibilityinPhotorama
WithVoiceOverstillenabled,buildandrunPhotoramaonyourdevicetotestitsaccessibility.Oncetheapplicationisrunning,dragyourfingeraroundthescreen.Noticethatthesystemisplayingadulledbeepingsoundasyoudragoverthephotos.Thisisthesystem’swayofinformingyouthatitisnotabletofindanaccessibilityelementunderyourfinger.Currently,thePhotoCollectionViewCellsarenotaccessibilityelements.Thisiseasytofix.OpenPhotoCollectionViewCell.swiftandoverridetheisAccessibilityElementpropertytoletthesystemknowthateachcellisaccessible.overridevarisAccessibilityElement:Bool{
get{
returntrue
}
set{
super.isAccessibilityElement=newValue
}
}
Nowbuildandruntheapplication.Asyoudragyourfingeracrossthephotos,youwillhearamoreaffirmingbeepandseeeachcelloutlinedwiththefocusrectangle(Figure24.3).Nodescriptionisspoken,butyouaremakingprogress.
Figure24.3Accessiblecells
GobacktoPhotoCollectionViewCell.swift.YouaregoingtoaddanaccessibilitylabelforVoiceOvertoreadwhenanitemisselected.Currently,acellknowsnothingaboutthePhotothatitisdisplaying,soaddanewpropertytoholdontothisinformation.classPhotoCollectionViewCell:UICollectionViewCell{
@IBOutletvarimageView:UIImageView!
@IBOutletvarspinner:UIActivityIndicatorView!
varphotoDescription:String?
Inthesamefile,overridetheaccessibilityLabeltoreturnthisstring.overridevaraccessibilityLabel:String?{
get{
returnphotoDescription
}
set{
//Ignoreattemptstoset
}
}
OpenPhotoDataSource.swiftandupdatecollectionView(_:cellForItemAt:)tosetthephotoDescriptiononthecell.funccollectionView(_collectionView:UICollectionView,
cellForItemAtindexPath:IndexPath)->UICollectionViewCell{
letidentifier="PhotoCollectionViewCell"
letcell=
collectionView.dequeueReusableCell(withReuseIdentifier:identifier,
for:indexPath)as!PhotoCollectionViewCell
letphoto=photos[indexPath.row]
cell.photoDescription=photo.title
returncell
}
Buildandruntheapplication.Dragyourfingeroverthescreenandyouwillhearthetitlesforeachphoto.Currentlythereisnoindicationtousersthattheycandouble-taptodrilldowntoaspecificphoto.Thisisbecausethecellsdonothaveanyaccessibilitytraitsset.InPhotoCollectionViewCell.swift,overridetheaccessibilityTraitspropertytoletthesystemknowthatacellholdsanimage.overridevaraccessibilityTraits:UIAccessibilityTraits{
get{
returnsuper.accessibilityTraits|UIAccessibilityTraitImage
}
set{
//Ignoreattemptstoset
}
}
YouarecombininganytraitsinheritedfromthesuperclasswiththeUIAccessibilityTraitImage.Thisisdoneusingthe|(or)operatortocombinethetwo.Likemanyotherthingsrelatedtoaccessibility,thisisalegacyofthepast.Incurrent,idiomaticSwift,thiswouldbedoneusinganOptionSetbypassingtheoptionsasanarray.ButUIAccessibilitydoesnotsupportthissyntax,soinsteadyouareusing|togrouptheoptions,whichishowthisisdoneinCandObjective-C.Buildandruntheapplication.Noticethatthenewtraityouaddedisspokenwhenyouselectacell.Theremainingpartsoftheapplicationaremostlyaccessiblebecausetheyusestandardviewsandcontrols.TheonlythingyouneedtoupdateistheimageviewwhendrillingdowntoaspecificPhoto.Youcancustomizemanyviews’accessibilityinformationfromwithinstoryboards,andyouwillbeabletodothatfortheimageview.OpenMain.storyboardandnavigatetothesceneassociatedwiththePhotoInfoViewController.Selecttheimageviewandopenitsidentityinspector.ScrolltothebottomtothesectionlabeledAccessibility.Checktheboxatthetopofthissectionto
enableaccessibilityforthisimageviewandunchecktheboxnexttoUserInteractionEnabled(Figure24.4).Figure24.4Updatingtheaccessibilityoptions
OpenPhotoInfoViewController.swiftandupdateviewDidLoad()togivetheimageviewamoremeaningfulaccessibilitylabel.overridefuncviewDidLoad(){
super.viewDidLoad()
imageView.accessibilityLabel=photo.title
Buildandruntheapplicationandnavigatetoaspecificphoto.Youwillnoticethatwiththissmalladdition,thisentirescreenisaccessible.Finally,let’sturnourattentiontotheTagsViewController.Whilestillrunningtheapplication,drilldowntotheTagsViewController.Addatagtothetableviewifoneisnotalreadypresent.SelectarowinthetableandnoticethatVoiceOverreadsthenameofthistag;however,thereisnoindicationtousersthattheycantogglethe
checkmarkforeachrow,noristhepresenceorabsenceofthatcheckmarkcommunicated.OpenTagDataSource.swiftandupdatethecell’saccessibilityhintintableView(_:cellForRowAt:).functableView(_tableView:UITableView,
cellForRowAtindexPath:IndexPath)->UITableViewCell{
letcell=tableView.dequeueReusableCell(withIdentifier:"UITableViewCell",
for:indexPath)
lettag=tags[indexPath.row]
cell.textLabel?.text=tag.name
cell.accessibilityHint="Doubletaptotoggleselected"
returncell
}
Buildandruntheapplicationandmarvelatitsaccessibility.
25Afterword
Welcometotheendofthebook!Youshouldbeveryproudofallyourworkandallthatyouhavelearned.Nowthereisgoodnewsandbadnews:
Thegoodnews:ThestuffthatleavesprogrammersbefuddledwhentheycometotheiOSplatformisbehindyounow.YouareaniOSdeveloper.Thebadnews:YouareprobablynotaverygoodiOSdeveloper.
WhattoDoNext
Itisnowtimetomakesomemistakes,readsomereallytediousdocumentation,andbehumbledbytheheartlessexpertswhowillridiculeyourquestions.Hereiswhatwerecommend:Writeappsnow.Ifyoudonotimmediatelyusewhatyouhavelearned,itwillfade.Exerciseandextendyourknowledge.Now.Godeep.Thisbookhasconsistentlyfavoredbreadthoverdepth;anychaptercouldhavebeenexpandedintoanentirebook.Findatopicthatyoufindinterestingandreallywallowinit–dosomeexperiments,readApple’sdocsonthetopic,andreadpostingsonblogsandStackOverflow.Connect.ThereareiOSDeveloperMeetupsinmostcities,andthetalksaresurprisinglygood.ThereareCocoaHeadschaptersaroundtheworld.Therearediscussiongroupsonline.Ifyouaredoingaproject,findpeopletohelpyou:designers,testers(AKAguineapigs),andotherdevelopers.Makemistakesandfixthem.Youwilllearnalotonthedayswhenyousay,“Thisapplicationhasbecomeaballofcrap!I’mgoingtothrowitawayandwriteitagainwithanarchitecturethatmakessense.”Politeprogrammerscallthisrefactoring.Giveback.Sharetheknowledge.Answeradumbquestionwithgrace.Giveawaysomecode.
ShamelessPlugs
YoucanfindusonTwitter,wherewekeepyouinformedaboutprogrammingandentertainedaboutlife:@cbkeurand@aaronhillegass.KeepaneyeoutforotherguidesfromBigNerdRanch.Wealsoofferweek-longcoursesfordevelopers.Andifyoujustneedsomecodewritten,wedocontractprogramming.Formoreinformation,visitourwebsiteatwww.bignerdranch.com.You,dearreader,makeourlivesofwriting,coding,andteachingpossible.Sothankyouforbuyingourbook.
AtBigNerdRanch,wecreateelegant,authenticallyusefulsolutionsthroughbest-in-classdevelopmentandtraining.CLIENTSOLUTIONSBigNerdRanchdesigns,developsanddeploysapplicationsforclientsofallsizes—fromsmallstart-upstolargecorporations.Ourin-houseengineeringanddesignteamspossessexpertiseiniOS,Androidandfull-stackwebapplicationdevelopment.
TEAMTRAININGForcompanieswithcapableengineeringteams,BigNerdRanchcanprovideon-sitecorporatetraininginiOS,Android,Front-EndWeb,Back-EndWeb,macOSandDesign.Ofthetop25appsintheU.S.,19arebuiltbycompaniesthatbroughtinBigNerdRanchtotraintheirdevelopers.
CODINGBOOTCAMPSBigNerdRanchoffersintensiveappdevelopmentanddesignretreatsforindividuals.Lodging,foodandcoursematerialsareincluded,andwe’llevenpickyouupattheairport!Thesecoursesarenotforthefaintofheart.YouwilllearnnewskillsiniOS,Android,Front-EndWeb,Back-EndWeb,macOSorDesignindays—notweeks.
www.bignerdranch.com
Index
ABCDEFGHIJKLMNOPQRSTUVWX
Symbols
#columnexpression,Cavemandebugging#fileexpression,Cavemandebugging#functionexpression,Cavemandebugging,FortheMoreCurious:ApplicationStateTransitions#lineexpression,Cavemandebugging$0,$1...(shorthandargumentnames),BackgroundTasks.xcassets(AssetCatalog),ApplicationIcons.xcdatamodeld(datamodelfile),Modelingentities//MARK:,//MARK:@discardableResult,UITableView’sDataSource@escapingannotation,ParsingJSONdata@IBAction,Definingactionmethods@IBInspectable,@IBInspectable@IBOutlet,Declaringoutlets,HookingUptheContent@NSManagedkeyword,NSManagedObjectandsubclasses
A
accesscontrol,URLComponentsaccessibilityaccessibilityelements,AccessibilityinPhotoramaaddingaccessibilityhints,AccessibilityinPhotoramaaddingaccessibilitylabels,AccessibilityinPhotoramasettingaccessibilityinformationinstoryboards,AccessibilityinPhotoramasettingaccessibilitytraits,AccessibilityinPhotoramaUIAccessibilityprotocol,VoiceOverVoiceOver,VoiceOver,TestingVoiceOver
AccessibilityInspector,TestingVoiceOveraccessoryview(UITableViewCell),UITableViewCellsactionmethodsabout,Definingactionmethodsimplementing,ImplementingactionmethodsUIControlclassand,FortheMoreCurious:UIControl
activestate,ApplicationStatesandTransitionsaddSubview(_:)method,ViewsandFramesalerts,displaying,DisplayingUserAlerts
alignmentrectangle,Thealignmentrectangleandlayoutattributesanchors,Anchorsanimate(withDuration:animations:)method,BasicAnimationsanimationsbasic,BasicAnimationsforconstraints,AnimatingConstraintsmarkingcompletionof,AnimationCompletionspring,BronzeChallenge:SpringAnimationstimingfunctions,TimingFunctions
anti-aliasing,FortheMoreCurious:RetinaDisplayappend(_:)method,Instancemethodsapplicationbundleabout,FortheMoreCurious:TheApplicationBundleinternationalizationand,Baseinternationalization,FortheMoreCurious:Bundle’sRoleinInternationalization
applicationsandbox,ApplicationSandbox,FortheMoreCurious:TheApplicationBundleapplicationstates,ApplicationStatesandTransitions,FortheMoreCurious:ApplicationStateTransitionsapplicationDidBecomeActive(_:)method,ApplicationStatesandTransitions,FortheMoreCurious:ApplicationStateTransitionsapplicationDidEnterBackground(_:)method,NSKeyedArchiverandNSKeyedUnarchiver,ApplicationStatesandTransitionsapplications(seealsoapplicationbundle,debugging,projects)building,Runningonthesimulator,Localizationcleaning,Localizationdatastorage,ApplicationSandboxdirectoriesin,ApplicationSandboxiconsfor,ApplicationIconslaunchimagesfor,LaunchScreenmultiplethreadsin,TheMainThreadrunningonsimulator,Runningonthesimulator
applicationWillEnterForeground(_:)method,ApplicationStatesandTransitions,FortheMoreCurious:ApplicationStateTransitionsapplicationWillResignActive(_:)method,ApplicationStatesandTransitions,FortheMoreCurious:ApplicationStateTransitionsarchiveRootObject(_:toFile:)method,NSKeyedArchiverandNSKeyedUnarchiverarchivingabout,ArchivingCoreDatavs,CoreDataimplementing,ArchivingwithNSKeyedArchiver,NSKeyedArchiverandNSKeyedUnarchiverunarchiving,Loadingfiles
arraysabout,Collectiontypescountproperty,Propertiesliteral,Literalsandsubscripting
subscripting,LiteralsandsubscriptingAssetCatalog(Xcode),ApplicationIconsassistanteditor,HookingUptheContentattributes(CoreData),EntitiesAutoLayout(seealsoconstraints(AutoLayout),views)about,AbriefintroductiontoAutoLayoutalignmentrectangle,Thealignmentrectangleandlayoutattributesautoresizingmasksand,FortheMoreCurious:NSAutoresizingMaskLayoutConstraintconstraints,AbriefintroductiontoAutoLayoutdynamiccellheights,DynamicCellHeightshorizontalambiguity,AddingconstraintsinInterfaceBuilderlayoutattributes,Thealignmentrectangleandlayoutattributesnearestneighborand,Constraintspurposeof,TheAutoLayoutSystem
autoresizingmasks,ProgrammaticConstraints,FortheMoreCurious:NSAutoresizingMaskLayoutConstraintawakeFromNib()method,Respondingtouserchanges
B
backgroundstate,ApplicationStatesandTransitions,FortheMoreCurious:ApplicationStateTransitionsbaseinternationalization,Baseinternationalizationbaselines,ThealignmentrectangleandlayoutattributesbecomeFirstResponder()method,DismissingthekeyboardBooltype,NumberandBooleantypesbreakpointsaddingactionsto,Steppingthroughcodeadvancingthroughcode,Steppingthroughcodedeleting,Steppingthroughcodeexception,Steppingthroughcodesetting,Settingbreakpointssymbolic,Steppingthroughcodeusingtologtotheconsole,Steppingthroughcode
Bundleclass,FortheMoreCurious:Bundle’sRoleinInternationalizationbundles(seealsoapplicationbundle)application,FortheMoreCurious:TheApplicationBundleBundleclass,FortheMoreCurious:Bundle’sRoleinInternationalization
buttonsaddingtonavigationbars,Addingbuttonstothenavigationbarcamera,Addingacamerabutton
C
callbacks,Delegation(seealsodelegation,target-actionpairs)
camera(seealsoimages)takingpictures,Addingacamerabutton
canBecomeFirstResponderproperty,UIMenuControllercancelsTouchesInViewproperty,UIPanGestureRecognizerandsimultaneousrecognizerscanPerformAction(_:withSender:)method,FortheMoreCurious:UIMenuControllerandUIResponderStandardEditActionscells(seealsoUITableViewCellclass)addingpaddingto,ContentInsetschangingcellclass,CreatingItemCellcustomizinglayout,CustomizingtheLayoutdynamiccellheights,DynamicCellHeightsprototype,ReusingUITableViewCells
CGPointtype,ViewsandFramesCGRecttype,ViewsandFramesCGSizetype,ViewsandFramesclassmethods,TypesinSwiftclassesBundle,FortheMoreCurious:Bundle’sRoleinInternationalizationData,WritingtotheFilesystemwithDataDateFormatter,HookingUptheContentIndexPath,CreatingandretrievingUITableViewCells,DeletingRowsJSONSerialization,JSONSerializationLocale,FormattersNSCoder,ArchivingNSFetchRequest,FetchrequestsandpredicatesNSKeyedArchiver,NSKeyedArchiverandNSKeyedUnarchiverNSKeyedUnarchiver,LoadingfilesNSManagedObject,NSManagedObjectandsubclassesNSUserDefaults,ApplicationSandboxNSUUID,CreatingandUsingKeysNSValueTransformer,TransformableattributesNumberFormatter,HookingUptheContentOperationQueue,TheMainThreadUIActivityIndicatorView,CreatingaCustomUICollectionViewCellUIAlertController,DisplayingUserAlertsUIApplication(seeUIApplicationclass)UIBarButtonItem(seeUIBarButtonItemclass)UICollectionViewCell,CreatingaCustomUICollectionViewCellUICollectionViewFlowLayout,CollectionViewsUICollectionViewLayout,CustomizingtheLayout
UIColor,ViewsandFramesUIControl,FortheMoreCurious:UIControlUIGestureRecognizer(seeUIGestureRecognizerclass)UIImagePickerController(seeUIImagePickerControllerclass)UIImageView,DisplayingImagesandUIImageViewUILongPressGestureRecognizer,UILongPressGestureRecognizerUIMenuController,UIMenuController,FortheMoreCurious:UIMenuControllerandUIResponderStandardEditActionsUINavigationBar,UINavigationController,NavigatingwithUINavigationControllerUINavigationController(seeUINavigationControllerclass)UINavigationItem,UINavigationBarUIPanGestureRecognizer,UIPanGestureRecognizerandsimultaneousrecognizersUIResponder(seeUIResponderclass)UIStackView,UsingUIStackViewUIStoryboardSegue,SeguesUITabBarController(seeUITabBarControllerclass)UITabBarItem,TabbaritemsUITableView(seeUITableViewclass)UITableViewCell,SubclassingUITableViewCell(seeUITableViewCellclass)UITableViewController(seeUITableViewControllerclass)UITapGestureRecognizer,Dismissingthekeyboard,DetectingTapswithUITapGestureRecognizerUITextField(seeUITextFieldclass)UIToolbar,UINavigationBar(seealsotoolbars)
UITouch,TouchEvents,TurningTouchesintoLines,HandlingmultipletouchesUIView(seeUIViewclass)UIViewController(seeUIViewControllerclass)UIWindow(seeUIWindowclass)URLComponents,URLComponentsURLRequest,SendingtheRequest,FortheMoreCurious:HTTPURLSession,SendingtheRequestURLSessionDataTask,URLSession,TheMainThreadURLSessionTask,SendingtheRequest,FortheMoreCurious:HTTP
closures,Closures,ConstructingafileURLcollectionviewscustomizinglayout,CustomizingtheLayoutdisplaying,DisplayingtheGriddownloadingimagedata,DownloadingtheImageDatalayoutobject,CollectionViewssettingdatasource,CollectionViewDataSource
colorsbackground,ViewsandFramescustomizing,Customizingthelabels
#columnexpression,Cavemandebugging
commonancestor,Activatingconstraintsconcurrency,TheMainThreadconditionalsif-let,Optionalsswitch,EnumerationsandtheSwitchStatement
connections(inInterfaceBuilder),Makingconnectionsconnectionsinspector(Xcode),Summaryofconnectionsconsoleinterpretingmessages,Interpretingconsolemessagesliteralexpressionsfordebugging,Cavemandebuggingprintingto,Usingadelegateviewinginplayground,Optionals
constants,UsingStandardTypesconstraint(equalTo:)method,Anchorsconstraints(AutoLayout)about,Constraintsactivatingprogrammatically,Activatingconstraintsactivepropertyforprogrammaticconstraints,Activatingconstraintsaddingnewconstraints,AddingconstraintsinInterfaceBuilderalign,AddingconstraintsinInterfaceBuilderanimating,AnimatingConstraintsclearing,AbriefintroductiontoAutoLayout,Addingmoreconstraintscollectionviews,DisplayingtheGridcreatingexplicitconstraints,ExplicitconstraintscreatinginInterfaceBuilder,AddingconstraintsinInterfaceBuilder,Preparingforlocalizationcreatingprogrammatically,ProgrammaticConstraintsimplicit,Implicitconstraintsresolvingunsatisfiable,FortheMoreCurious:NSAutoresizingMaskLayoutConstraintspecifying,AbriefintroductiontoAutoLayout
contentcompression,ContentcompressionresistanceprioritiescontentModeproperty(UIImageView),DisplayingImagesandUIImageViewcontentView(UITableViewCell),UITableViewCellscontrolevents,FortheMoreCurious:UIControlcontrollers,inModel-View-Controller,Model-View-Controllercontrols,Cavemandebuggingcontrols,programmatic,ProgrammaticControlsCoreData@NSManagedkeyword,NSManagedObjectandsubclassesarchivingvs,CoreDataattributes,Entitiescreatingapersistentcontainer,NSPersistentContainerentities(seeentities(CoreData))fetchrequests,Fetchrequestsandpredicatespersistentstoreformats,NSPersistentStoreCoordinator
relationshipmanagementwith,CoreDataRelationshipsroleof,EntitiessubclassingNSManagedObject,NSManagedObjectandsubclassestransformingvalues,Transformableattributes
CoreGraphics,FortheMoreCurious:RetinaDisplaycountproperty(arrays),PropertiescurrentLocalemethod,Formatters
D
Dataclass,WritingtotheFilesystemwithDatadatasourcemethods,Implementingdatasourcemethods,CollectionViewDataSource,UpdatingtheDataSourcedatasources,DesignPatternsdatastorage(seealsoarchiving,CoreData)forapplicationdata,ApplicationSandboxbinary,FortheMoreCurious:ReadingandWritingtotheFilesystemwithData,WritingtotheFilesystemwithData
dataSource(UITableView),UITableViewController,UITableView’sDataSource,ImplementingdatasourcemethodsDateFormatterclass,HookingUptheContentdebugging(seealsodebuggingtools,exceptions)breakpoints(seebreakpoints)cavemandebugging,Cavemandebuggingliteralexpressionsfor,CavemandebuggingLLDBconsolecommands,TheLLDBconsolestacktraces,Interpretingconsolemessagesusingtheconsole,Interpretingconsolemessages
debuggingtoolsissuenavigator,BuildingtheFinishedApplicationinplaygrounds,Optionals
default:(switchstatement),EnumerationsandtheSwitchStatementdelegationabout,Delegationasadesignpattern,DesignPatternsforUIImagePickerController,Settingtheimagepicker’sdelegateforUITableView,UITableViewControllerprotocolsfor,ConformingtoaprotocolforUICollectionView,DownloadingtheImageData
deleteRows(at:with:)method,DeletingRowsdependencyinjection,Givingthecontrolleraccesstothestoredependencyinversionprinciple,GivingthecontrolleraccesstothestoredequeueReusableCell(withIdentifier:for:)method,ReusingUITableViewCells
designpatterns,DesignPatternsdevicescheckingforcamera,Settingtheimagepicker’ssourceTypedisplayresolution,ViewsandFrames,FortheMoreCurious:RetinaDisplayenablingVoiceOver,TestingVoiceOverRetinadisplay,FortheMoreCurious:RetinaDisplayscreensizes,SizeClasses
dictionaries(seealsoJSONdata)about,Collectiontypesaccessing,Subscriptingdictionariesliteral,Literalsandsubscriptingsubscripting,Subscriptingdictionariesusing,CreatingandUsingKeys
directoriesapplication,ApplicationSandboxDocuments,ApplicationSandboxLibrary/Caches,ApplicationSandboxLibrary/Preferences,ApplicationSandboxlproj,Baseinternationalization,FortheMoreCurious:Bundle’sRoleinInternationalizationtmp,ApplicationSandbox
@discardableResult,UITableView’sDataSourcedisplayresolution,ViewsandFramesdo-catchstatements,ErrorHandlingdocumentoutline(InterfaceBuilder),InterfaceBuilderdocumentationopening,ControllingAnimationsforSwift,ExploringApple’sSwiftDocumentation
Documentsdirectory,ApplicationSandboxDoubletype,NumberandBooleantypesdrawing(seeviews)drill-downinterface,UINavigationControllerDynamicType,DynamicType
E
editButtonItem,Addingbuttonstothenavigationbareditingproperty(UITableView,UITableViewController),EditingModeemptyinitializers,Structsencode(with:)method,Archiving,NSKeyedArchiverandNSKeyedUnarchiverendEditing(_:)method,Dismissingbytappingelsewhereentities(CoreData)about,Entitiesmodeling,Modelingentities
relationshipsbetween,Relationshipssavingchangesto,Savingchanges
enumerated()function,LoopsandStringInterpolationenums(enumerations)about,EnumerationsandtheSwitchStatementassociatedvaluesand,Enumerationsandassociatedvaluesrawvaluesand,Enumerationsandrawvaluesswitchstatementsand,EnumerationsandtheSwitchStatement
errorhandling,ErrorHandlingErrorprotocol,ParsingJSONdataerrorsinplaygrounds,UsingStandardTypestraps,Literalsandsubscripting
@escapingannotation,ParsingJSONdataeventscontrol,ProgrammaticControls,FortheMoreCurious:UIControleventhandling,Eventhandlingbasicstouch,Eventhandlingbasics,TouchEvents(seealsotouchevents)
exceptionbreakpoints,Steppingthroughcodeexceptionserrorhandlingvs,FortheMoreCurious:ReadingandWritingtotheFilesysteminternalinconsistency,AddingRowsSwiftvsotherlanguages,FortheMoreCurious:ReadingandWritingtotheFilesystem
expressions,stringinterpolationand,LoopsandStringInterpolationextensions,Extensions
F
fallthrough(switchstatement),EnumerationsandtheSwitchStatementfetchrequests,Fetchrequestsandpredicates#fileexpression,Cavemandebuggingfileinspector,LocalizationfileURLs,constructing,ConstructingafileURLfilesystem,writingto,WritingtotheFilesystemwithData,FortheMoreCurious:ReadingandWritingtotheFilesystemfirstrespondersabout,Eventhandlingbasicsbecoming,DismissingthekeyboardcanBecomeFirstResponderproperty,UIMenuControllernil-targetedactionsand,FortheMoreCurious:UIControlresigning,Dismissingthekeyboard,DismissingbypressingtheReturnkey,Dismissingbytappingelsewhereresponderchainand,FortheMoreCurious:TheResponderChainUIMenuControllerand,UIMenuController
Floattype,NumberandBooleantypesFloat80type,NumberandBooleantypesfor-inloops,LoopsandStringInterpolationforcedunwrapping(ofoptionals),Optionalsframeproperty(UIView),ViewsandFramesframeworksabout,ViewsandFramesCoreData(seeCoreData)linkingmanually,SettingtheInitialViewControllerUIKit,ViewsandFrames
#functionexpression,Cavemandebugging,FortheMoreCurious:ApplicationStateTransitionsfunctionscallback,Delegationenumerated(),LoopsandStringInterpolationNSLocalizedString(_:comment:),NSLocalizedStringandstringstablesswap(_:_:),AnimationCompletionUIImageJPEGRepresentation,WritingtotheFilesystemwithData
G
genstrings,NSLocalizedStringandstringstablesgesturerecognizers(seeUIGestureRecognizerclass)gestures(seealsoUIGestureRecognizerclass)discretevscontinuous,UILongPressGestureRecognizerlongpress,UILongPressGestureRecognizerpan,UIPanGestureRecognizerandsimultaneousrecognizerstap,DetectingTapswithUITapGestureRecognizer
globallyuniqueidentifiers(GUIDs),CreatingandUsingKeys
H
headerview(UITableView),EditingModeHTTPmethods,FortheMoreCurious:HTTPrequestspecifications,FortheMoreCurious:HTTP
I
@IBAction,Definingactionmethods@IBInspectable,@IBInspectable@IBOutlet,Declaringoutlets,HookingUptheContenticons(seealsoimages)
application,ApplicationIconsinAssetCatalog,ApplicationIcons
if-letstatements,Optionalsimagepicker(seeUIImagePickerControllerclass)imagePickerController(_:didFinishPickingMediaWithInfo:)method,Settingtheimagepicker’sdelegate,SavingtheimageimagePickerControllerDidCancel(_:)method,Settingtheimagepicker’sdelegateimages(seealsocamera,icons)accessingfromthecache,CreatingandUsingKeyscaching,WritingtotheFilesystemwithDatadisplayinginUIImageView,DisplayingImagesandUIImageViewdownloadingimagedata,DownloadingandDisplayingtheImageData,DownloadingtheImageDatafetching,GivingViewControllersAccesstotheImageStoremodelobjectstorepresent,ModelingthePhotoforRetinadisplay,FortheMoreCurious:RetinaDisplaysaving,Savingtheimagestoring,CreatingImageStore
implementationfiles,navigating,FortheMoreCurious:NavigatingImplementationFilesimplicitconstraints,Implicitconstraintsinactivestate,ApplicationStatesandTransitionsIndexPathclass,CreatingandretrievingUITableViewCells,DeletingRowsinequalityconstraints,Preparingforlocalizationinit(coder:)method,InteractingwithViewControllersandTheirViews,Archivinginit(contentsOf:encoding:)method,FortheMoreCurious:ReadingandWritingtotheFilesysteminit(contentsOfFile:)method,WritingtotheFilesystemwithDatainit(frame:)initializer,ViewsandFramesinit(nibName:bundle:)method,InteractingwithViewControllersandTheirViewsinitialviewcontroller,SettingtheInitialViewControllerinitializersabout,Initializersforclassesvsstructs,Custominitializersconvenience,Custominitializerscustom,Custominitializersdesignated,Custominitializersempty,Structsfree,Custominitializersmember-wise,Structsreturningemptyliterals,Initializers
instancevariables(seeoutlets,properties)instances,InitializersInttype,NumberandBooleantypesInterfaceBuilder(seealsoXcode)
addingconstraints,AddingconstraintsinInterfaceBuilderAutoLayout(seeAutoLayout)canvas,InterfaceBuilderconnectingobjects,Makingconnectionsconnectingwithsourcefiles,ExposingthePropertiesofItemCelldocumentoutline,InterfaceBuildermodifyingviewattributes,@IBInspectablepropertiesand,ExposingthePropertiesofItemCellscene,InterfaceBuildersettingoutletsin,Settingoutlets,HookingUptheContentsettingtarget-actionin,Settingtargetsandactionssizeinspector,ViewsandFrames
interfacefilesbadconnectionsin,HookingUptheContentbaseinternationalizationand,Baseinternationalization
internalinconsistencyexception,AddingRowsinternationalization,Localization,FortheMoreCurious:Bundle’sRoleinInternationalization(seealsolocalization)
intrinsicvsexplicitcontentsize,Intrinsiccontentsizeinverserelationships,RelationshipsiOSsimulatorrunningapplicationson,Runningonthesimulatorsandboxlocation,NSKeyedArchiverandNSKeyedUnarchiversavingimagesto,Permissionsviewingapplicationbundlein,FortheMoreCurious:TheApplicationBundle
iPad(seealsodevices)applicationiconsfor,ApplicationIcons
isEmptyproperty(strings),PropertiesisSourceTypeAvailable(_:)method,Settingtheimagepicker’ssourceTypeissuenavigator(Xcode),BuildingtheFinishedApplication
J
JSONdata,JSONDataJSONSerializationclass,JSONSerialization
K
key-valuepairsindictionaries,CollectiontypesinJSONdata,JSONDatainwebservices,FormattingURLsandrequests
keyboards
attributes,Keyboardattributesdismissing,Dismissingthekeyboard,DismissingtheKeyboard
keys,creating/using,CreatingandUsingKeys
L
labelsadding,ViewsandFramesaddingtotabbar,Tabbaritemscustomizing,Customizingthelabelsupdatingpreferredtextsize,Respondingtouserchanges
languagesettings,Localization(seealsolocalization)
launchimages,LaunchScreenlayoutattributes,Thealignmentrectangleandlayoutattributeslayoutguides,Layoutguides,SilverChallenge:LayoutGuideslayoutIfNeeded()method,AnimatingConstraintslazyloading,TheViewofaViewController,LoadedandAppearingViewsletkeyword,UsingStandardTypeslibraries(seeframeworks)Library/Cachesdirectory,ApplicationSandboxLibrary/Preferencesdirectory,ApplicationSandbox#lineexpression,Cavemandebuggingliteralvalues,LiteralsandsubscriptingloadView()method,TheViewofaViewController,InteractingwithViewControllersandTheirViews,CreatingaViewProgrammaticallyLocaleclass,Formatterslocalizationbaseinternationalizationand,BaseinternationalizationBundleclass,FortheMoreCurious:Bundle’sRoleinInternationalizationinternationalization,Localization,FortheMoreCurious:Bundle’sRoleinInternationalizationlprojdirectories,Baseinternationalization,FortheMoreCurious:Bundle’sRoleinInternationalizationstringstables,NSLocalizedStringandstringstablesusersettingsfor,LocalizationXLIFFdatatype,FortheMoreCurious:ImportingandExportingasXLIFF
location(in:)method,MultipleGestureRecognizersloopsexamininginValueHistory,LoopsandStringInterpolationfor-in,LoopsandStringInterpolationinSwift,LoopsandStringInterpolation
low-memorywarnings,Savingtheimagelprojdirectories,Baseinternationalization,FortheMoreCurious:Bundle’sRoleinInternationalization
M
mainbundle,Baseinternationalization,FortheMoreCurious:Bundle’sRoleinInternationalization(seealsoapplicationbundle)
maininterface,SettingtheInitialViewControllermainthread,TheMainThreadmargins,Margins,Explicitconstraints//MARK:,//MARK:member-wiseinitializers,Structsmemorymanagementmemorywarnings,SavingtheimageUITableViewCellclass,ReusingUITableViewCells
menus(UIMenuController),UIMenuController,FortheMoreCurious:UIMenuControllerandUIResponderStandardEditActionsmessages(seealsomethods)action,UIGestureRecognizerSubclasses,UIMenuControllerlog,Handlingmultipletouches
methodsabout,Instancemethodsaction,Definingactionmethods,FortheMoreCurious:UIControladdSubview(_:),ViewsandFramesanimate(withDuration:animations:),BasicAnimationsappend(_:),InstancemethodsapplicationDidBecomeActive(_:),ApplicationStatesandTransitions,FortheMoreCurious:ApplicationStateTransitionsapplicationDidEnterBackground(_:),NSKeyedArchiverandNSKeyedUnarchiver,ApplicationStatesandTransitionsapplicationWillEnterForeground(_:),ApplicationStatesandTransitions,FortheMoreCurious:ApplicationStateTransitionsapplicationWillResignActive(_:),ApplicationStatesandTransitions,FortheMoreCurious:ApplicationStateTransitionsarchiveRootObject(_:toFile:),NSKeyedArchiverandNSKeyedUnarchiverawakeFromNib(),RespondingtouserchangesbecomeFirstResponder(),DismissingthekeyboardcanPerformAction(_:withSender:),FortheMoreCurious:UIMenuControllerandUIResponderStandardEditActionsclass,TypesinSwiftconstraint(equalTo:),AnchorscurrentLocale,Formattersdatasource,Implementingdatasourcemethods,CollectionViewDataSource,UpdatingtheDataSourcedeleteRows(at:with:),DeletingRowsdequeueReusableCell(withIdentifier:for:),ReusingUITableViewCellsencode(with:),Archiving,NSKeyedArchiverandNSKeyedUnarchiverendEditing(_:),DismissingbytappingelsewhereHTTP,FortheMoreCurious:HTTPimagePickerController(_:didFinishPickingMediaWithInfo:),Settingtheimagepicker’sdelegate,Savingtheimage
imagePickerControllerDidCancel(_:),Settingtheimagepicker’sdelegateinit(coder:),InteractingwithViewControllersandTheirViews,Archivinginit(contentsOf:encoding:),FortheMoreCurious:ReadingandWritingtotheFilesysteminit(contentsOfFile:),WritingtotheFilesystemwithDatainit(nibName:bundle:),InteractingwithViewControllersandTheirViewsinstance,TypesinSwift,InstancemethodsisSourceTypeAvailable(_:),Settingtheimagepicker’ssourceTypelayoutIfNeeded(),AnimatingConstraintsloadView(),TheViewofaViewController,InteractingwithViewControllersandTheirViews,CreatingaViewProgrammaticallylocation(in:),MultipleGestureRecognizersoverriding,Loadingthefirstquestionprepare(for:sender:),PassingDataAroundprepareForReuse(),CreatingaCustomUICollectionViewCellpresent(_:animated:completion:),DisplayingUserAlertsprotocol,Moreonprotocolsrequire(toFail:),MoreonUIGestureRecognizerresignFirstResponder(),Dismissingthekeyboard,DismissingbypressingtheReturnkeyreverse(),Instancemethodsselectors,InterpretingconsolemessagessendActions(for:),FortheMoreCurious:UIControlsetEditing(_:animated:),EditingModesetNeedsDisplay(),TurningTouchesintoLinesstatic,TypesinSwifttableView(_:cellForRowAt:),Implementingdatasourcemethods,CreatingandretrievingUITableViewCellstableView(_:commit:forRow:),DeletingRowstableView(_:moveRowAtIndexPath:toIndexPath:),MovingRowstableView(_:numberOfRowsInSection:),ImplementingdatasourcemethodstextFieldShouldReturn(_:),DismissingbypressingtheReturnkeytouchesBegan(_:with:),TouchEventstouchesCancelled(_:with:),TouchEventstouchesEnded(_:with:),TouchEventstouchesMoved(_:with:),TouchEventstranslationInView(_:),UIPanGestureRecognizerandsimultaneousrecognizersunarchiveObject(withFile:),Loadingfilesurl(forResource:withExtension:),FortheMoreCurious:Bundle’sRoleinInternationalizationurls(for:in:),ConstructingafileURLviewDidLoad(),ViewsandFrames,AccessingsubviewsviewWillAppear(_:),Accessingsubviews,AppearingandDisappearingViewsviewWillDisappear(_:),AppearingandDisappearingViewswrite(to:atomically:encoding:),FortheMoreCurious:ReadingandWritingtotheFilesystemwrite(to:options:),WritingtotheFilesystemwithData
minimumPressDurationproperty,UILongPressGestureRecognizermodalviewcontrollers,DisplayingUserAlerts,Presentingtheimagepickermodally
Model-View-Controller(MVC),Model-View-Controller,UITableViewController,DesignPatternsmodels,inModel-View-Controller,Model-View-ControllermultipleTouchEnabledproperty(UIView),Handlingmultipletouchesmultithreading,TheMainThreadmultitouch,enabling,HandlingmultipletouchesMVC(Model-View-Controller),Model-View-Controller,UITableViewController,DesignPatterns
N
namingconventionscellreuseidentifiers,ReusingUITableViewCellsdelegateprotocols,Conformingtoaprotocolencodingkeysforarchiving,Archiving
navigationcontrollers(seeUINavigationControllerclass)navigationItem(UIViewController),UINavigationBarnearestneighbor,Constraintsnextproperty,FortheMoreCurious:TheResponderChainnil-targetedactions,FortheMoreCurious:UIControlNSCoderclass,ArchivingNSCodingprotocol,ArchivingNSFetchRequestclass,FetchrequestsandpredicatesNSKeyedArchiverclass,NSKeyedArchiverandNSKeyedUnarchiverNSKeyedUnarchiverclass,LoadingfilesNSLocalizedString(_:comment:)function,NSLocalizedStringandstringstables@NSManagedkeyword,NSManagedObjectandsubclassesNSManagedObjectclass,NSManagedObjectandsubclassesNSUserDefaultsclass,ApplicationSandboxNSUUIDclass,CreatingandUsingKeysNSValueTransformerclass,Transformableattributesnumberformatters,Numberformatters,FormattersNumberFormatterclass,HookingUptheContent
O
objectgraphs,ObjectGraphsobjectlibrary(Xcode),Creatingviewobjectsobjects(seememorymanagement)OperationQueueclass,TheMainThreadoptionalkeyword,Moreonprotocolsoptionalmethods(protocols),Moreonprotocolsoptionalsabout,Optionalsdictionarysubscriptingand,Subscriptingdictionariesforcedunwrapping,Optionals
if-letstatements,Optionalsoptionalbinding,Optionalsunwrapping,Optionals
outletsabout,Makingconnectionsautogenerating/connecting,HookingUptheContentconnectingconstraintsto,AnimatingConstraintsconnectingwithsourcefiles,ExposingthePropertiesofItemCellsetting,MakingconnectionssettinginInterfaceBuilder,HookingUptheContent
overridekeyword,Loadingthefirstquestion
P
padding,ContentInsetsparallelcomputing,TheMainThreadphotos(seecamera,images)pixels(vspoints),ViewsandFramesplaygrounds(Xcode)about,UsingStandardTypeserrorsin,UsingStandardTypesValueHistory,LoopsandStringInterpolationviewingconsolein,Optionals
pointers,inInterfaceBuilder(seeoutlets)points(vspixels),ViewsandFramespredicates,Fetchrequestsandpredicatespreferences,ApplicationSandbox(seealsoDynamicType,localization)
prepare(for:sender:)method,PassingDataAroundprepareForReuse()method,CreatingaCustomUICollectionViewCellpresent(_:animated:completion:)method,DisplayingUserAlertspreviewassistant,Preparingforlocalizationprogrammaticviewsactivatingconstraints,Activatingconstraintsactivepropertyonconstraints,Activatingconstraintsanchors,Anchorsconstraint(equalTo:)method,Anchorscontrols,ProgrammaticControlscreatingconstraints,ProgrammaticConstraintscreatingexplicitconstraints,Explicitconstraintscreatingmargins,Marginslayoutguides,LayoutguidesloadViewmethod,CreatingaViewProgrammatically
projectnavigator(Xcode),CreatinganXcodeProject
projectscleaningandbuilding,Localizationcreating,CreatinganXcodeProjecttargetsettingsin,FortheMoreCurious:TheApplicationBundle
propertiesabout,PropertiescreatinginInterfaceBuilder,ExposingthePropertiesofItemCell
propertylistserializabletypes,FortheMoreCurious:ReadingandWritingtotheFilesystempropertyobservers,ImplementingtheTemperatureConversionprotocolkeyword,Conformingtoaprotocolprotocolsconformingto,Conformingtoaprotocoldeclaring,Conformingtoaprotocoldelegate,ConformingtoaprotocolError,ParsingJSONdataNSCoding,Archivingoptionalmethodsin,Moreonprotocolsrequiredmethodsin,Moreonprotocolsstructureof,ConformingtoaprotocolUIAccessibility,VoiceOverUIApplicationDelegate,ApplicationStatesandTransitionsUICollectionViewDataSource,CollectionViewDataSourceUICollectionViewDelegate,DownloadingtheImageDataUIGestureRecognizerDelegate,UIPanGestureRecognizerandsimultaneousrecognizersUIImagePickerControllerDelegate,Settingtheimagepicker’sdelegate,SavingtheimageUINavigationControllerDelegate,SavingtheimageUIResponderStandardEditActions,FortheMoreCurious:UIMenuControllerandUIResponderStandardEditActionsUITableViewDataSource,UITableViewController,Implementingdatasourcemethods,CreatingandretrievingUITableViewCells,DeletingRows,MovingRowsUITableViewDelegate,UITableViewControllerUITextFieldDelegate,Conformingtoaprotocol
pseudolanguage,Preparingforlocalization
Q
Quartz,FortheMoreCurious:RetinaDisplay(seeCoreGraphics)queryitems,FormattingURLsandrequestsQuickHelp(Xcode),Inferringtypes
R
Rangetype,LoopsandStringInterpolationrawValue(enums),Enumerationsandrawvalues
referencetypes,vsvaluetypes,Valuetypesvsreferencetypesregionsettings,Localizationreorderingcontrols,MovingRowsrequire(toFail:)method,MoreonUIGestureRecognizerrequiredmethods(protocols),MoreonprotocolsresignFirstResponder()method,Dismissingthekeyboard,DismissingbypressingtheReturnkeyresourcesabout,ApplicationIcons,FortheMoreCurious:TheApplicationBundleAssetCatalog,ApplicationIcons
responderchain,FortheMoreCurious:TheResponderChainresponders(seefirstresponders,UIResponderclass)Retinadisplay,FortheMoreCurious:RetinaDisplayreuseIdentifier(UITableViewCell),ReusingUITableViewCellsreverse()method,Instancemethodsrootviewcontroller(UINavigationController),UINavigationControllerrows(UITableView)adding,AddingRowsdeleting,DeletingRowsmoving,MovingRows
S
sandbox,application,ApplicationSandbox,FortheMoreCurious:TheApplicationBundlesections(UITableView),Implementingdatasourcemethodssegues,Seguesselectorsabout,Interpretingconsolemessagesunrecognized,Interpretingconsolemessages
sendActions(for:)method,FortheMoreCurious:UIControlsenderargument,CavemandebuggingSettype,CollectiontypessetEditing(_:animated:)method,EditingModesetNeedsDisplay()method,TurningTouchesintoLinessettings(seepreferences)simulatorAccessibilityInspector,TestingVoiceOverrunningapplicationson,Runningonthesimulatorsandboxlocation,NSKeyedArchiverandNSKeyedUnarchiversavingimagesto,Permissionsviewingapplicationbundlein,FortheMoreCurious:TheApplicationBundle
sizeclasses,SizeClassessortdescriptors(NSFetchRequest),FetchrequestsandpredicatessourceTypeproperty(UIImagePickerController),TakingPicturesandUIImagePickerControllerstacktraces,Interpretingconsolemessages
stackviewsabout,StackViewsdistributingcontents,Implicitconstraintsnested,Nestedstackviews
states,application,ApplicationStatesandTransitions,FortheMoreCurious:ApplicationStateTransitionsstaticmethods,TypesinSwiftstringsinternationalizing,NSLocalizedStringandstringstablesinterpolation,LoopsandStringInterpolationisEmptyproperty,Propertiesliteral,Literalsandsubscriptingstringstables,NSLocalizedStringandstringstables
structs,vsclasses,Structssubscriptingarrays,Literalsandsubscriptingdictionaries,Subscriptingdictionaries
subviews,TheViewHierarchy,Accessingsubviewssuperviewproperty,ViewsandFramessuspendedstate,ApplicationStatesandTransitionsswap(_:_:)function,AnimationCompletionSwiftabout,TheSwiftLanguagedocumentationfor,ExploringApple’sSwiftDocumentationenumerations,EnumerationsandtheSwitchStatementextensionsin,Extensionsloops,LoopsandStringInterpolationoptionaltypesin,Optionals,ErrorHandlingreferencetypesvsvaluetypes,Valuetypesvsreferencetypesstringinterpolation,LoopsandStringInterpolationswitchstatements,EnumerationsandtheSwitchStatementtypes,TypesinSwift,UsingStandardTypes
switchstatements,EnumerationsandtheSwitchStatementsymbolicbreakpoints,Steppingthroughcode
T
tabbarcontrollers(seeUITabBarControllerclass)tabbaritems,Tabbaritemstableviewcells(seeUITableViewCellclass)tableviewcontrollers(seeUITableViewControllerclass)tableviews(seeUITableViewclass)tables(database),EntitiestableView(_:cellForRowAt:)method,Implementingdatasourcemethods,CreatingandretrievingUITableViewCells
tableView(_:commit:forRow:)method,DeletingRowstableView(_:moveRowAtIndexPath:toIndexPath:)method,MovingRowstableView(_:numberOfRowsInSection:)method,Implementingdatasourcemethodstarget-actionpairsabout,Definingactionmethods,DesignPatternsUIControlclassand,FortheMoreCurious:UIControlUIGestureRecognizerand,UIGestureRecognizerSubclasses
targets,buildsettingsfor,FortheMoreCurious:TheApplicationBundletext(seealsoAutoLayout)changingpreferredsize,DynamicTypecustomizingappearance,Customizingthelabels,TextEditingcustomizingsize,Customizingthelabelsdynamicstylingof,DynamicTypeinput,TextInputandDelegation
textFieldShouldReturn(_:)method,DismissingbypressingtheReturnkeythreads,TheMainThreadtimingfunctions,TimingFunctionstmpdirectory,ApplicationSandboxto-manyrelationships,Relationshipsto-onerelationships,Relationshipstoolbarsadding,Addingacamerabuttonaddingbuttonsto,Addingacamerabutton
topViewControllerproperty(UINavigationController),UINavigationControllertoucheventsabout,Eventhandlingbasics,TouchEventsenablingmultitouch,Handlingmultipletouchesresponderchainand,FortheMoreCurious:TheResponderChaintarget-actionpairsand,FortheMoreCurious:UIControlUIControlclassand,FortheMoreCurious:UIControl
touchesBegan(_:with:)method,TouchEventstouchesCancelled(_:with:)method,TouchEventstouchesEnded(_:with:)method,TouchEventstouchesMoved(_:with:)method,TouchEventstransformableattributes(CoreData),TransformableattributestranslationInView(_:)method,UIPanGestureRecognizerandsimultaneousrecognizerstraps,Literalsandsubscriptingtuples,LoopsandStringInterpolationtypesBool,NumberandBooleantypesCGPoint,ViewsandFramesCGRect,ViewsandFramesCGSize,ViewsandFrames
Double,NumberandBooleantypesextending,ExtensionsFloat,NumberandBooleantypesFloat80,NumberandBooleantypeshashable,Collectiontypesinitializers,Initializersinstancesof,InitializersInt,NumberandBooleantypespropertylistserializable,FortheMoreCurious:ReadingandWritingtotheFilesystemRange,LoopsandStringInterpolationreferencetypesvsvaluetypes,ValuetypesvsreferencetypesSet,Collectiontypesspecifying,Specifyingtypestypeinference,InferringtypesUIControlEvents,ProgrammaticControlsUITableViewAutomaticDimension,DynamicCellHeightsUITableViewCellStyle,UITableViewCells
U
UIthread,TheMainThreadUIAccessibilityprotocol,VoiceOverUIActivityIndicatorViewclass,CreatingaCustomUICollectionViewCellUIAlertControllerclass,DisplayingUserAlertsUIApplicationclasseventsand,TouchEventsresponderchainand,FortheMoreCurious:TheResponderChain,FortheMoreCurious:UIControl
UIApplicationDelegateprotocol,ApplicationStatesandTransitionsUIBarButtonItemclass,UINavigationBar,AddingacamerabuttonUICollectionViewCellclass,CreatingaCustomUICollectionViewCellUICollectionViewDataSourceprotocol,CollectionViewDataSourceUICollectionViewDelegateprotocol,DownloadingtheImageDataUICollectionViewFlowLayoutclass,CollectionViewsUICollectionViewLayoutclass,CustomizingtheLayoutUIColorclass,ViewsandFramesUIControlclass,FortheMoreCurious:UIControlUIControlEvent.touchUpInside,FortheMoreCurious:UIControlUIControlEvent.touchUpOutside,FortheMoreCurious:UIControlUIControlEventstype,ProgrammaticControlsUIGestureRecognizerclassabout,UIGestureRecognizerandUIMenuControlleractionmessages,UIGestureRecognizerSubclasses,UILongPressGestureRecognizercancelsTouchesInViewproperty,UIPanGestureRecognizerandsimultaneousrecognizerschainingrecognizers,MoreonUIGestureRecognizer
delayingtouches,MoreonUIGestureRecognizerdetectingtaps,DetectingTapswithUITapGestureRecognizerenablingsimultaneousrecognizers,UIPanGestureRecognizerandsimultaneousrecognizersimplementingmultiple,MultipleGestureRecognizers,UIPanGestureRecognizerandsimultaneousrecognizersinterceptingtouchesfromview,UIGestureRecognizerSubclasses,UIPanGestureRecognizerandsimultaneousrecognizerslocation(in:),MultipleGestureRecognizerslongpress,UILongPressGestureRecognizerpan,UIPanGestureRecognizerandsimultaneousrecognizersstateproperty,UILongPressGestureRecognizer,UIPanGestureRecognizerandsimultaneousrecognizers,MoreonUIGestureRecognizersubclasses,UIGestureRecognizerSubclasses,MoreonUIGestureRecognizersubclassing,MoreonUIGestureRecognizertranslationInView(_:),UIPanGestureRecognizerandsimultaneousrecognizerstypesof,DismissingthekeyboardUIRespondermethodsand,UIPanGestureRecognizerandsimultaneousrecognizers
UIGestureRecognizerDelegateprotocol,UIPanGestureRecognizerandsimultaneousrecognizersUIImageclass(seeimages,UIImageViewclass)UIImageJPEGRepresentationfunction,WritingtotheFilesystemwithDataUIImagePickerControllerclassinstantiating,TakingPicturesandUIImagePickerControllerpresenting,Presentingtheimagepickermodally
UIImagePickerControllerDelegateprotocol,Settingtheimagepicker’sdelegate,SavingtheimageUIImageViewclass,DisplayingImagesandUIImageViewUIKitframework,ViewsandFramesUILongPressGestureRecognizerclass,UILongPressGestureRecognizerUIMenuControllerclass,UIMenuController,FortheMoreCurious:UIMenuControllerandUIResponderStandardEditActionsUINavigationBarclass,UINavigationController,NavigatingwithUINavigationControllerUINavigationControllerclass(seealsoviewcontrollers)about,UINavigationControlleraddingviewcontrollersto,AppearingandDisappearingViewsinstantiating,UINavigationControllermanagingviewcontrollerstack,UINavigationControllerrootviewcontroller,UINavigationControllerinstoryboards,SeguestopViewControllerproperty,UINavigationControllerUINavigationBarclassand,UINavigationBarUITabBarControllervs,UINavigationControllerview,UINavigationControllerviewControllersproperty,UINavigationControllerviewWillAppear(_:)method,AppearingandDisappearingViewsviewWillDisappear(_:)method,AppearingandDisappearingViews
UINavigationControllerDelegateprotocol,Savingtheimage
UINavigationItemclass,UINavigationBarUIPanGestureRecognizerclass,UIPanGestureRecognizerandsimultaneousrecognizersUIResponderclassmenuactions,FortheMoreCurious:UIMenuControllerandUIResponderStandardEditActionsresponderchainand,FortheMoreCurious:TheResponderChaintoucheventsand,TouchEvents
UIResponderStandardEditActionsprotocol,FortheMoreCurious:UIMenuControllerandUIResponderStandardEditActionsUIStackViewclass,UsingUIStackViewUIStoryboardSegueclass,SeguesUITabBarControllerclassimplementing,UITabBarControllerUINavigationControllervs,UINavigationControllerview,UITabBarController
UITabBarItemclass,TabbaritemsUITableViewclass(seealsoUITableViewCellclass,UITableViewControllerclass)about,UITableViewandUITableViewControlleraddingrows,AddingRowscontentInsetproperty,ContentInsetsdataSourceproperty,UITableViewControllerdelegation,UITableViewControllerdeletingrows,DeletingRowseditingmode,EditingMode,SubclassingUITableViewCell,Addingbuttonstothenavigationbareditingproperty,EditingModefooterview,EditingModeheaderview,EditingModemovingrows,MovingRowspopulating,UITableView’sDataSourcesections,Implementingdatasourcemethodsview,SubclassingUITableViewController
UITableViewAutomaticDimensiontype,DynamicCellHeightsUITableViewCellclass(seealsocells)about,UITableViewCells,SubclassingUITableViewCellaccessoryview,UITableViewCellscellstyles,UITableViewCellscontentView,UITableViewCellsdetailTextLabelproperty,UITableViewCellsimageView,UITableViewCellsretrievinginstancesof,CreatingandretrievingUITableViewCellsreuseIdentifierproperty,ReusingUITableViewCellsreusinginstancesof,ReusingUITableViewCellssubclassing,SubclassingUITableViewCell
textLabelproperty,UITableViewCellsUITableViewCellStyletype,UITableViewCells
UITableViewCellEditingStyle.delete,DeletingRowsUITableViewControllerclass(seealsoUITableViewclass)about,UITableViewControlleraddingrows,AddingRowsdatasourcemethods,ImplementingdatasourcemethodsdataSourceproperty,UITableView’sDataSourcedeletingrows,DeletingRowseditingproperty,EditingModemovingrows,MovingRowsreturningcells,CreatingandretrievingUITableViewCellssubclassing,SubclassingUITableViewControllertableViewproperty,ContentInsets
UITableViewDataSourceprotocol,UITableViewController,Implementingdatasourcemethods,CreatingandretrievingUITableViewCells,DeletingRows,MovingRowsUITableViewDelegateprotocol,UITableViewControllerUITapGestureRecognizerclass,Dismissingthekeyboard,DetectingTapswithUITapGestureRecognizerUITextFieldclassconfiguring,TextEditingasfirstresponder,Eventhandlingbasicskeyboardand,DismissingtheKeyboard
UITextFieldDelegateprotocol,ConformingtoaprotocolUIToolbarclass,UINavigationBar(seealsotoolbars)
UITouchclass,TouchEvents,TurningTouchesintoLines,HandlingmultipletouchesUIViewclass(seealsoUIViewControllerclass,views)about,ViewBasicsanimationdocumentation,ControllingAnimationsframeproperty,ViewsandFramessuperview,ViewsandFrames
UIViewControllerclass(seealsoUIViewclass,viewcontrollers)loadViewmethod,CreatingaViewProgrammaticallyloadView()method,TheViewofaViewControllernavigationItemproperty,UINavigationBartabBarItemproperty,Tabbaritemsview,TheViewofaViewController,FortheMoreCurious:TheResponderChainviewDidLoad()method,AccessingsubviewsviewWillAppear(_:)method,Accessingsubviews
UIWindowclassabout,TheViewHierarchy
responderchainand,FortheMoreCurious:TheResponderChainunarchiveObject(withFile:)method,Loadingfilesuniversallyuniqueidentifiers(UUIDs),CreatingandUsingKeysunrecognizedselectorerror,Interpretingconsolemessagesurl(forResource:withExtension:)method,FortheMoreCurious:Bundle’sRoleinInternationalizationURLComponentsclass,URLComponentsURLRequestclass,SendingtheRequest,FortheMoreCurious:HTTPURLs,FormattingURLsandrequestsurls(for:in:)method,ConstructingafileURLURLSessionclass,SendingtheRequestURLSessionDataTaskclass,URLSession,TheMainThreadURLSessionTaskclass,SendingtheRequest,FortheMoreCurious:HTTPuseralerts,displaying,DisplayingUserAlertsuserinterface(seealsoAutoLayout,views)drill-down,UINavigationControllerkeyboard,DismissingtheKeyboard
usersettings(seepreferences)
V
valuetypes,vsreferencetypes,Valuetypesvsreferencetypesvarkeyword,UsingStandardTypesvariables,UsingStandardTypes(seealsoinstancevariables(seeoutlets,properties),properties)
viewcontrollers(seealsoUIViewControllerclass,views)allowingaccesstoimagestore,GivingViewControllersAccesstotheImageStoreinitial,SettingtheInitialViewControllerinteractingwith,InteractingwithViewControllersandTheirViewslazyloadingofviews,TheViewofaViewController,LoadedandAppearingViewsmodal,DisplayingUserAlerts,Presentingtheimagepickermodallynavigatingbetween,Seguespresenting,UITabBarControllerroot,UINavigationControllerviewhierarchyand,TheViewofaViewController
viewhierarchy,TheViewHierarchyviewproperty(UIViewController),TheViewofaViewControllerviewControllers(UINavigationController),UINavigationControllerviewDidLoad()method,ViewsandFrames,Accessingsubviewsviews(seealsoAutoLayout,touchevents,UIViewclass,viewcontrollers)about,ViewBasicsanimating,ControllingAnimations
appearing/disappearing,AppearingandDisappearingViewscontentcompressionresistancepriorities,Contentcompressionresistanceprioritiescontenthuggingpriorities,Contenthuggingprioritiescreatingprogrammatically(seeprogrammaticviews)drawingtoscreen,TheViewHierarchyhierarchy,TheViewHierarchylayersand,TheViewHierarchylazyloading,TheViewofaViewController,LoadedandAppearingViewsmisplaced,MisplacedviewsinModel-View-Controller,Model-View-Controllerpresentingmodally,Presentingtheimagepickermodallyremovingfromstoryboard,ProgrammaticViewsrendering,TheViewHierarchyresizing,DisplayingImagesandUIImageViewscroll,DisplayingtheGridsizeandpositionof,ViewsandFramesstackviews(seestackviews)subviews,TheViewHierarchy
viewWillAppear(_:)method,Accessingsubviews,AppearingandDisappearingViewsviewWillDisappear(_:)method,AppearingandDisappearingViewsVoiceOver,VoiceOver,TestingVoiceOver
W
webservicesabout,WebServicesHTTPrequestspecificationsand,FortheMoreCurious:HTTPwithJSONdata,JSONDatarequestingdatafrom,BuildingtheURLURLSessionclassand,SendingtheRequest
write(to:atomically:encoding:)method,FortheMoreCurious:ReadingandWritingtotheFilesystemwrite(to:options:)method,WritingtotheFilesystemwithData
X
.xcassets(AssetCatalog),ApplicationIcons
.xcdatamodeld(datamodelfile),ModelingentitiesXcode(seealsodebuggingtools,InterfaceBuilder,projects,iOSsimulator)AssetCatalog,ApplicationIconsassistanteditor,HookingUptheContentAutoLayout(seeAutoLayout)breakpointnavigator,Settingbreakpointsconnectionsinspector,Summaryofconnections
creatingprojects,CreatinganXcodeProjectdebugarea,Steppingthroughcodedebugbar,Steppingthroughcodedocumentation,ControllingAnimationseditorarea,CreatinganXcodeProject,InterfaceBuilderfileinspector,Localizationissuenavigator,BuildingtheFinishedApplicationnavigatorarea,CreatinganXcodeProjectnavigators,CreatinganXcodeProjectobjectlibrary,Creatingviewobjectsorganizingfileswith//MARK:,//MARK:playgrounds,UsingStandardTypesprojectnavigator,CreatinganXcodeProjectQuickHelp,Inferringtypesschemes,Runningonthesimulatorsourceeditorjumpbar,FortheMoreCurious:NavigatingImplementationFilesutilityarea,Creatingviewobjectsversions,CreatinganXcodeProjectworkspace,CreatinganXcodeProject
XLIFFdatatype,FortheMoreCurious:ImportingandExportingasXLIFFXMLpropertylists,FortheMoreCurious:ReadingandWritingtotheFilesystem