5.
Organizing Your App's Data
Written by Audrey Tam
In this chapter, you’ll use structures and enumerations to organize your app’s data. The compiler can then help you avoid errors like using the wrong type value or misspelling a string.
Your app needs sample data during development. You’ll use a compiler directive to create this data only during development. And you’ll store your development-only code and data in Preview Content to exclude them from the release version of your app.
You’ll learn how to localize your app to expand its audience. You’ll replace user-facing text with NSLocalizedString
instances, generate the development language (English) Localizable.strings file, then use this as the reference language resource file for adding another language.
➤ Continue with your project from the previous chapter or open the project in this chapter’s starter folder.
Creating the Exercise structure
Skills you’ll learn in this section: how to use enumeration, computed property,
extension
,static
property
For the initial layout of HIITFit, you used two arrays of strings for the exercise names and video file names. This minimalist approach helped you see exactly what data each view needs, and this helped to keep the previews manageable.
But you had to manually ensure the strings matched up across the two arrays. It’s safer to encapsulate them as properties of a named type.
First, you’ll create an Exercise
structure with the properties you need. Then, you’ll create an array of Exercise
instances and loop over this array to create the ExerciseView
pages of the TabView
.
➤ Create a new Swift file and name it Exercise.swift. Add the following code below import Foundation
:
struct Exercise {
let exerciseName: String
let videoName: String
enum ExerciseEnum: String {
case squat = "Squat"
case stepUp = "Step Up"
case burpee = "Burpee"
case sunSalute = "Sun Salute"
}
}
In the previous chapters, you’ve created and used structures that conform to the View
protocol. This Exercise
structure models your app’s data, encapsulating the exerciseName
and videoName
properties.
Enumerating exercise names
enum
is short for enumeration. A Swift enumeration is a named type and can have methods and computed properties. It’s useful for grouping related values so the compiler can help you avoid mistakes like misspelling a string.
Swift Tip: A stored property is one you declare with a type and/or an initial value, like
let name: String
orlet name = "Audrey"
. You declare a computed property with a type and a closure where you compute its value, likevar body: some View { ... }
.
Here, you create an enumeration for the four exercise names. The case names are camelCase: If you start typing ExerciseEnum.sunSalute
, Xcode will suggest the auto-completion.
Because this enumeration has String
type, you can specify a String
as the raw value of each case. Here, you specify the title-case version of the exercise name: ”Sun Salute” for sunSalute
, for example. You access this String
with ExerciseEnum.sunSalute.rawValue
, for example.
Creating an array of Exercise instances
You get to use your enumeration right away to create your exercises
array.
➤ Below Exercise
, completely outside its braces, add this code:
extension Exercise {
static let exercises = [
Exercise(
exerciseName: ExerciseEnum.squat.rawValue,
videoName: "squat"),
Exercise(
exerciseName: ExerciseEnum.stepUp.rawValue,
videoName: "step-up"),
Exercise(
exerciseName: ExerciseEnum.burpee.rawValue,
videoName: "burpee"),
Exercise(
exerciseName: ExerciseEnum.sunSalute.rawValue,
videoName: "sun-salute")
]
}
In an extension to the Exercise
structure, you initialize the exercises
array as a type property.
exerciseName
and videoName
are instance properties: Each Exercise
instance has its own values for these properties. A type property belongs to the type, and you declare it with the static
keyword. The exercises
array doesn’t belong to an Exercise
instance. There’s just one exercises
array no matter how many Exercise
instances you create. You use the type name to access it: Exercise.exercises
.
You create the exercises
array with an array literal: a comma-separated list of values, enclosed in square brackets. Each value is an instance of Exercise
, supplying the raw value of an enumeration case and the corresponding video file name.
As the word suggests, an extension extends a named type. The starter project includes two extensions, in DateExtension.swift and ImageExtension.swift. Date
and Image
are built-in SwiftUI types but, using extension
, you can add methods and computed or type properties.
Here, Exercise
is your own custom type, so why do you have an extension? In this case, it’s just for housekeeping, to keep this particular task — initializing an array of Exercise
values — separate from the core definition of your structure — stored properties and any custom initializers.
Developers also use extensions to encapsulate the requirements for protocols, one for each protocol. When you organize your code like this, you can more easily see where to add features or look for bugs.
Refactoring ContentView and ExerciseView
Now, you’ll modify ContentView
and ExerciseView
to use your new Exercise.exercises
array.
➤ In ContentView.swift, replace the ForEach
loop range with this:
ForEach(0 ..< Exercise.exercises.count) { index in
Instead of a magic number, you use the number of exercises
elements as the upper bound of the ForEach
range.
Note: You could pass the whole
Exercise
item toExerciseView
but, in the next chapter, you’ll useindex
to decide when to showSuccessView
.
➤ In ExerciseView.swift, delete the videoNames
and exerciseNames
arrays. The Exercise.exercises
array contains the same data. The error flags tell you where you need to use this array.
Replace exerciseNames[index]
with this:
Exercise.exercises[index].exerciseName
And replace videoNames[index]
, in two places, with this:
Exercise.exercises[index].videoName
➤ Run live preview in ContentView.swift to check everything still works:
Refactoring ContentView
and ExerciseView
is almost everything you need to do. You don’t need to modify any of the other views, except HistoryView
.
Structuring HistoryView data
Skills you’ll learn in this section:
Identifiable
,mutating func
, initializer, compiler directive / conditional compilation, debug/release build config, Preview Content,ForEach
with an array ofIdentifiable
values
HistoryView
currently uses hard-coded dates and exercise lists to mock up its display. You need a data structure for storing your user’s activity. And, in the next chapter, you’ll implement the Done button to add completed exercise names to this data structure.
Creating HistoryStore
➤ Create a new Swift file and name it HistoryStore.swift. Group it with Exercise.swift and name the group folder Model:
➤ Add the following code below import Foundation
:
struct ExerciseDay: Identifiable {
let id = UUID()
let date: Date
var exercises: [String] = []
}
struct HistoryStore {
var exerciseDays: [ExerciseDay] = []
}
An ExerciseDay
has properties for the date and a list of exercise names completed by your user on that date.
ExerciseDay
conforms to Identifiable
. This protocol is useful for named types that you plan to use as elements of a collection, because you usually want to loop over these elements or display them in a list.
When you loop over a collection with ForEach
, it must have a way to uniquely identify each of the collection’s elements. The easiest way is to make the element’s type conform to Identifiable
and include id: UUID
as a property.
UUID
is a basic Foundation
type, and UUID()
is the easiest way to create a unique identifier whenever you create an ExerciseDay
instance.
The only property in HistoryStore
is an array of ExerciseDay
values you’ll loop over in HistoryView
.
In Chapter 9, “Saving History Data”, you’ll extend HistoryStore
with a method to save the user’s history to persistent storage and another method to load the history. Soon, you’ll add a HistoryStore
property to HistoryView
, which will initialize it.
In the meantime, you need some sample history data and an initializer to create it.
➤ Below HistoryStore
, completely outside its braces, add this code:
extension HistoryStore {
mutating func createDevData() {
// Development data
exerciseDays = [
ExerciseDay(
date: Date().addingTimeInterval(-86400),
exercises: [
Exercise.exercises[0].exerciseName,
Exercise.exercises[1].exerciseName,
Exercise.exercises[2].exerciseName
]),
ExerciseDay(
date: Date().addingTimeInterval(-86400 * 2),
exercises: [
Exercise.exercises[1].exerciseName,
Exercise.exercises[0].exerciseName
])
]
}
}
This is pretty much the same sample data you had before, now stored in your new Exercise
and ExerciseDay
structures. In the next chapter, you’ll add a new ExerciseDay
item, so I’ve moved the development data to yesterday and the day before yesterday.
You create this sample data in a method named createDevData
. This method changes, or mutates, the exerciseDays
property, so you must mark it with the mutating
keyword.
And you create this method in an extension because it’s not part of the core definition. But there’s another reason, too – coming up soon!
➤ Now, in the main HistoryStore
, create an initializer for HistoryStore
that calls createDevData()
:
init() {
#if DEBUG
createDevData()
#endif
}
You don’t want to call createDevData()
in the release version of your app, so you use a compiler directive to check whether the current Build Configuration is Debug:
Note: To see this window, click the toolbar button labeled HIITFit. It also opens the run destination menu alongside. Select Edit Scheme…, then select the Info tab.
Moving development code into Preview Content
In fact, you don’t want createDevData()
to ship in your release version at all. Xcode provides a place for development code and data: Preview Content. Anything you put into this group will not be included in your release version.
➤ In the Preview Content group, create a new Swift file named HistoryStoreDevData.swift and move the HistoryStore
extension into it:
And this is the other reason createDevData()
is in an extension: You can store extensions in separate files. This means you never have to scroll through very long files.
Refactoring HistoryView
➤ In HistoryView.swift, delete the Date
properties and the exercise arrays, then add this property:
let history = HistoryStore()
HistoryStore
now encapsulates all the information in the stored properties today
, yesterday
and the exercises arrays.
The Form
closure currently displays each day in a Section
. Now that you have an exerciseDays
array, you should loop over the array.
➤ Replace the Form
closure with the following:
Form {
ForEach(history.exerciseDays) { day in
Section(
header:
Text(day.date.formatted(as: "MMM d"))
.font(.headline)) {
ForEach(day.exercises, id: \.self) { exercise in
Text(exercise)
}
}
}
}
Instead of today
and yesterday
, you use day.date
. And instead of the named exercises arrays, you use day.exercises
.
The code you just replaced looped over exercises1
and exercises2
arrays of String
. The id: \.self
argument told ForEach
to use the instance itself as the unique identifier. The exercises
array also contains String
instances, so you still need to specify this id
value.
➤ Refresh the preview to make sure it still looks the same:
Congratulations, you’ve set up your data structures and refactored your views to use them. The final project up to this point is in the final-no-loc folder. The rest of this chapter shows you how to localize your app.
Localizing your app
Skills you’ll learn in this section: how to localize your app; how to use
CustomStringConvertible
andgenstrings
; how to change app language
You surely want to maximize the audience for your app. A good way to do this is to translate it into languages other than English. This is called localization.
You need to complete the following tasks. You can do these in a different order, but this workflow will save you some time:
- Set up localization for the project’s development language (English).
- Decide which user-facing strings you want to localize and replace these with
NSLocalizedString
instances. - Generate the contents of Localizable.strings from these
NSLocalizedString
instances. - Add another language, choosing the existing English Localizable.strings as the reference language resource file.
- In the Localizable.strings file for the new language, replace English strings with translated strings.
Getting started
➤ In the Project navigator, select the top-level HIITFit folder. This opens the project page in the editor:
If you don’t see the projects and targets list, click the button in the upper left corner.
➤ Select the HIITFit Project, then its Info tab:
The Localizations section lists Base and English — Development Language, both with 0 Files Localized.
UIKit projects have a Base.lproj folder containing .storyboard and/or .xib files. SwiftUI projects with a LaunchScreen.storyboard file also have this folder. These files are already marked as localized in the development language (English). When you add another language, they appear in a checklist of resources you want to localize in the new language. So projects like these have at least one base-localized file.
If you don’t do anything to localize your app, you won’t have any development-language-localized files. All the user-facing text in your app just appears the way you write it. As soon as you decide to add another language, you’ll replace this text with NSLocalizedString
instances. For this mechanism to work, you’ll also have to localize in the development language.
Note: To change Development Language to another language, edit project.pbxproj in a text editor and, for both developmentRegion and knownRegions, change en to the language ID for your preferred development language.
You could add another language now, but the workflow you’ll follow below saves you some time.
Creating en.lproj/Localizable.strings
This step sets up localization for the project’s development language (English). First, you’ll create a Strings file named Localizable.strings.
➤ To create this file in the HIITFit group, but not in the Views group, select Assets.xcassets or HIITFitApp.swift in the Project navigator.
➤ Press Command-N to open the new file window, search for string, then select Strings File:
➤ Name this file Localizable. This is the default name iOS uses. Don’t let your auto-correct change it to Localisable, or you’ll have to type the name of this file every time you reference a localized string.
Naming this file “Localizable” doesn’t make it so. You must explicitly localize it.
➤ Select Localizable.strings in the Project navigator and open the File inspector (Option-Command-1).
Notice the file’s pathname is:
HIITFit/HIITFit/Localizable.strings
➤ Click Localize…. Something very quick happens! If the file inspector goes blank, select Localizable.strings in the Project navigator again:
A Localization section has replaced the button. And now the file’s pathname has a new subdirectory:
HIITFit/HIITFit/en.lproj/Localizable.strings
This new en.lproj folder doesn’t appear in the Project navigator, but here it is in Finder:
Whenever you localize a resource, Xcode stores it in a folder named xx.lproj, where xx is the language ID (en for English).
That’s the project-level setup done. The next two steps will populate this Localizable.strings file with lines like "Start" = "Start";
, one for each string you want to translate into another language.
Which strings?
The next step starts with deciding which user-facing strings you want to localize.
➤ Now scan your app to find all the text the user sees:
-
WelcomeView
text: Welcome, Get fit …. - Exercise names used as
ExerciseView
titles and inHistoryView
lists. - Button labels Get Started, Start/Done and History.
-
SuccessView
text: High Five, Good job ….
Creating NSLocalizedString instances
Next, you’ll replace these strings with instances of NSLocalizedString(_:comment:)
, where the first argument is the English text and comment
provides information to clarify the string’s context. If you don’t know the other language well enough to translate your text, you’ll usually ask someone else to provide translations. The comment
should help a translator make a translation that’s accurate for your context.
➤ Start in WelcomeView.swift. Replace "Welcome"
with this:
NSLocalizedString("Welcome", comment: "greeting")
Here, “Welcome” is a greeting, not a verb.
➤ Replace most of the other strings in this view:
NSLocalizedString("History", comment: "view user activity")
NSLocalizedString("Get Fit", comment: "invitation to exercise")
NSLocalizedString("Get Started", comment: "invitation")
➤ Leave “with high intensity interval training” as it is, for now.
➤ In ExerciseView.swift, reuse the "History"
NSLocalizedString
and replace "Start/Done"
with this:
NSLocalizedString(
"Start/Done",
comment: "begin exercise / mark as finished")
➤ In HistoryView.swift, again reuse the "History"
NSLocalizedString
. DateFormatter
automatically localizes dates, so you don’t have to do anything to day.date
.
➤ Leave SuccessView.swift as it is.
That takes care of everything except the exercise names. You create these in Exercise.swift, so that’s where you’ll set up the localized strings.
Localizing the Exercise structure
In Exercise.swift, you’ve been using raw values of ExerciseEnum
for exerciseName
. Now you need to use NSLocalizedString("Squat", comment: "exercise")
instead. But an enumeration’s raw value must be a literal string, so you can’t just replace the raw values with NSLocalizedString
instances.
You need to refactor Exercise
to use localized strings instead of enumeration raw values.
➤ Delete the raw values, then make the enumeration conform to CustomStringConvertible
. This simply requires each case to have a description string. And a description string can be an NSLocalizedString
instance.
enum ExerciseEnum: CustomStringConvertible {
case squat
case stepUp
case burpee
case sunSalute
var description: String {
switch self {
case .squat:
return NSLocalizedString("Squat", comment: "exercise")
case .stepUp:
return NSLocalizedString("Step Up", comment: "exercise")
case .burpee:
return NSLocalizedString("Burpee", comment: "exercise")
case .sunSalute:
return NSLocalizedString(
"Sun Salute", comment: "yoga stretch")
}
}
}
➤ Now, in the exercises
array, use the description
string for exerciseName
instead of the literal string:
extension Exercise {
static let exercises = [
Exercise(
exerciseName: String(describing: ExerciseEnum.squat),
videoName: "squat"),
Exercise(
exerciseName: String(describing: ExerciseEnum.stepUp),
videoName: "step-up"),
Exercise(
exerciseName: String(describing: ExerciseEnum.burpee),
videoName: "burpee"),
Exercise(
exerciseName: String(describing: ExerciseEnum.sunSalute),
videoName: "sun-salute")
]
}
Now, when ContentView.swift initializes an ExerciseView
with an exerciseName
, ExerciseView
will be able to display that name in Spanish.
Note: Why not
ExerciseEnum.squat.description
instead ofString(describing:)
? Well, theCustomStringConvertible
documentation says “Accessing a type’sdescription
property directly … is discouraged.”
Generating Localizable.strings content
Here’s the first time-saving step.
Your Localizable.strings file needs to contain lines like "Start" = "Start";
, but it’s currently blank. You could type every line yourself, but fortunately Xcode provides a tool to generate these from your NSLocalizedString
instances.
➤ In Finder, locate the HIITFit folder that contains the Assets.xcassets and en.lproj subfolders:
➤ Open Terminal, type cd
followed by a space, then drag this HIITFit folder into Terminal:
cd <drag HIITFit folder here>
➤ Press Return. You changed directory to the folder that contains Assets.xcassets and en.lproj. Enter this command to check:
ls
You should see something like this:
Assets.xcassets Info.plist Views
DateExtension.swift Model en.lproj
HIITFitApp.swift Preview Content
ImageExtension.swift Videos
➤ Now enter this command:
genstrings -o en.lproj Views/*.swift Model/*.swift
You use the Xcode command line tool genstrings
to scan files in Views and Model for NSLocalizedString
. It generates the necessary strings for the key
values and stores these in your Localizable.strings file.
➤ Back in Xcode, select Localizable.strings in the Project navigator. It contains lines like these:
/* view user activity */
"History" = "History";
That’s your comment
in comments and the key
string assigned to itself. Aren’t you glad you didn’t have to type all that out yourself? ;]
Adding a language
And here’s the other time-saving step. You’ll add another language, choosing the existing English Localizable.strings as the reference language resource file. And automagic happens!
➤ In the Project navigator, select the top-level HIITFit folder, then the project in the projects and target list. In the Localizations section, click the + button and select another language:
This chapter uses Spanish.
Now you get to choose the file and reference language to create your localization:
➤ Click Finish.
This produces several changes. The Localizations section now has a Spanish item, which already has 1 File Localized.
Sure enough, the Project navigator shows Localizable.strings is now a group containing two Localizable.strings files.
And the Spanish file has the same contents as the English file!
Translating
Now for the final step: In the Localizable.strings file for the alternate language, you need to replace English strings with translated strings.
➤ Open Localizable.strings (Spanish) and replace the right-hand-side strings with translations:
/* exercise */
"Burpee" = "Burpee";
/* invitation to exercise */
"Get Fit" = "Ponte en forma";
/* invitation */
"Get Started" = "Empieza";
/* view user activity */
"History" = "Historia";
/* exercise */
"Squat" = "Sentadilla";
/* begin exercise / mark as finished */
"Start/Done" = "Empieza/Hecho";
/* exercise */
"Step Up" = "Step Up";
/* warm up stretch */
"Sun Salute" = "Saludo al Sol";
/* greeting */
"Welcome" = "Bienvenid@";
Note: Often, Spanish-speakers just use the English exercise names. And using ’@’ to mean ’a or o’ is a convenient way to be gender-inclusive.
Exporting for localization (Optional)
If you use a localization service to translate your strings, Xcode has commands to export Localizable.strings files to XLIFF (XML Localization Interchange File Format) and import XLIFF translations.
Before you export, localize any media resources or assets that provide useful context information to translators.
Resources like the .mp4 files have a Localize button in their file inspector.
Select the Base menu option:
This moves the .mp4 file into a new Base.lproj folder in Videos.
The Localize button for an Assets.xcassets item is in its Attributes inspector (Option-Command-4). Check the box for Spanish:
To export for localization, select the project in the Project navigator, then select Editor ▸ Export for Localization…:
Check the languages you want translations for and choose where to save the exported folder.
Note: The final-loc project exported to Preview Content to keep it with the project.
See what you got:
The exported folder has the same name as your project and contains .xcloc folders for the languages you checked. For each language, the .xliff file is in Localized Contents, and localized assets and resources are in Source Contents. You can supply additional context information in the Notes folder.
Note: I had mixed results exporting videos. If you don’t see these in the exported folder, just copy resources and assets directly to the exported Source Contents folder.
Testing your localization
To test your localization, you simply need to set the project’s App Language to Spanish.
➤ Edit the scheme and select Run ▸ Options ▸ App Language ▸ Spanish
➤ Now check your previews:
Note: The first three letters of November are the same in Spanish, so I changed the date format to
"MMMM d"
to display the full month name.
Key points
-
To use a collection in a
ForEach
loop, it needs to have a way to uniquely identify each of its elements. The easiest way is to make it conform toIdentifiable
and includeid: UUID
as a property. -
An enumeration is a named type, useful for grouping related values so the compiler can help you avoid mistakes like misspelling a string.
-
Use compiler directives to create development data only while you’re developing and not in the release version of your app.
-
Preview Content is a convenient place to store code and data you use only while developing. Its contents won’t be included in the release version of your app.
-
Localize your app to create a larger audience for your app. Replace user-facing text with
NSLocalizedString
instances, generate the English Localizable.strings file, then use this as the reference language resource file for adding other languages.