12.
App Privacy
Written by Renan Benatti Dias
The iPhone completely changed the way people use phones. A phone is no longer just a device for making calls: It’s a whole computer in your pocket, full of features and capabilities.
With it, the App Store transformed the way developers deliver their experience to customers. Developers create apps and submit them to the App Store. Apple reviews them, and then they become available to users. Potential users can browse and find many different kinds of apps, and they trust Apple’s review to guarantee the minimum quality of that app.
However, with all the new capabilities developers can access, privacy has become a real concern to everyone. The conversation about who owns your data and how companies can use it is an issue that triggers a never-ending debate.
Apple has taken a strong position on this: People’s privacy is important and they should decide if they want to share any data with developers.
With that in mind, Apple built new features into iOS and the App Store to make developers more transparent and iOS more private.
In this chapter, you’ll learn about:
-
iOS’s many privacy features.
-
Requesting location data from users.
-
Sending the user’s location in the request to Petfinder’s API to find pets near that location.
-
How apps adapt UI to account for users’ privacy choices.
-
The App Store’s Privacy section.
Before you start, take a closer look at why privacy is important.
Why privacy is important
Apple takes people’s privacy very seriously. It understands that people don’t want to share every detail of their lives. People have the right to choose what and how much they share. That’s why Apple has worked hard to make privacy features: to give people a choice in what they want to share.
Apps may want to collect data to drive a specific feature, make a more targeted advertisement or even improve released features.
Apple understands that developers may need access to this data to build better features, but that doesn’t mean people don’t have a choice. Transparency, letting users understand what they are sharing and why, is a critical aspect of privacy.
That’s why Apple has built features that allow developers to ask for authorization to access those capabilities and make users aware of when and why.
iOS privacy features
Many of iOS’s privacy features focus on apps and developers being transparent.
iOS’s camera and microphone indicator are great examples. iOS displays a green circular dot at the top-right corner of the screen whenever an app uses either camera.
You’ll also find an indicator at the top of control center, showing the name of the app that recently used the camera.
Similarly, when an app accesses the microphone, an orange circular dot appears in the top right-hand corner.
Much like the green light near the MacBook’s camera, these indicators try to call the users’ attention and show them an app is using those capabilities.
iOS has many other privacy features like the clipboard prompt, which shows a banner displaying an app is pasting text from the clipboard, or the permission to access the user’s gallery, contacts or location data.
The privacy view modifier
SwiftUI also has a couple of view modifiers to keep sensitive data private. Lock screens can access a widget without unlocking the iPhone, which is a potential problem if a widget shows sensitive information.
Apple introduced the redacted(reason:) and privacySensitive(_:) view modifiers and the environment variable redactionReasons to help with this problem. With these view modifiers, you can protect sensitive content given the context.
Take a look at the following code:
struct CreditCardView: View {
var body: some View {
VStack(alignment: .leading, spacing: 82) {
VStack(alignment: .leading) {
HStack {
Text("Ray Bank")
.font(.title2)
Spacer()
Text("platinum")
.font(.caption2)
}
Text("Credit Card")
.font(.caption)
}
VStack(alignment: .leading) {
HStack(alignment: .bottom) {
Text("1234 5678 9123 456")
Spacer()
Image(systemName: "flag.square.fill")
.font(.title)
.imageScale(.large)
}
HStack {
Text("Exp: 02/20")
Text("Valid: 02/21")
Text("Security Code:")
Text("123")
}
.font(.caption2)
}
}
.padding()
.background(.green)
.foregroundColor(.white)
.mask(RoundedRectangle(cornerRadius: 8))
.padding()
}
}
This view shows a virtual credit card from a banking app.
Credit cards contain sensitive information like the card number, security code and even the user’s name. If you were to use this view in a widget, the cards number and security code could be accessible to anyone through the lock screen.
Even with the iPhone unlocked, people might want to redact those pieces of information so that those around them can’t see.
That’s where redacted(reason:) and privacySensitive(_:) come into play. By adding privacySensitive(_:) to the sensitive views and redacted(reason:) in a top hierarchy view, SwiftUI redacts that information.
Text("1234 5678 9123 456")
.privacySensitive()
Text("Security Code:")
Text("123")
.privacySensitive()
privacySensitive marks the credit card’s number and security code Text as private and sensitive data. Now, using this view in a private context will redact those fields.
struct WidgetView: View {
var body: some View {
CreditCardView()
.redacted(reason: .privacy)
}
}
WidgetView is a sensitive context, and the lock screen can display it. You can redact these fields by adding redacted(reason:) with the reason of .privacy to CreditCardView.
You can also use the environment variable redactionReasons to change how you hide the view.
if redactionReasons == .privacy {
Text("Hidden card number")
} else {
Text("1234 5678 9123 456")
}
That way, when the view is presented in a sensitive context, the hidden message replaces the card number.
These are a couple of APIs you can use to protect user privacy.
PetSave doesn’t display any sensitive data. Even though it uses the user’s location to find pets, it doesn’t display it in any view, so you won’t use these view modifiers.
Location and privacy
A user’s location is very sensitive data. It might reveal to developers where the user lives, walks or commutes. So, users should be aware and in control whenever apps have access to their location.
Right now, the Animals near you feature does not use the user’s location to find pets near them. It randomly lists pets. However, the whole point of this feature is to find pets that match the user and that are near them.
You’ll ask the user’s permission to access their location data to improve Animals near you.
Adding location to find pets near you
To access the user’s location, you first need to ask their permission with a valid reason. Remember, it’s the user’s choice to allow access to their private details.
You’ll build a new view to ask for permission. With that permission in place, users will get a location-specific list, rather than the default list the app shows when it doesn’t have permission to access the user’s location.
Building a view to ask permission
Inside AnimalsNearYou/views, create a new SwiftUI View and name it RequestLocationView.swift.
Add the following property to the new view:
@EnvironmentObject var locationManager: LocationManager
LocationManager is a class you use to manage the person’s location. You’ll use it to start monitoring the person’s location and ask them for permission to do so.
Since you’ll use a single instance of LocationManager throughout the project, you’ll use an @EnvironmentObject. @EnvironmentObject is a property wrapper that stores and shares an instance among views of the view hierarchy.
Next, add the following method:
func startUpdatingLocation() {
locationManager.startUpdatingLocation()
}
startUpdatingLocation is a method from LocationManager that starts tracking the user’s location. Once the user grants permission to their location data, LocationManager will keep track of their location and call the delegate method locationManager(_:didUpdateLocations:) with the updated location.
Next, add the following import at the top of the file:
import CoreLocationUI
Apple introduced a new CoreLocationUI framework in iOS 15. You’ll mainly use it with the CoreLocation, working in a standard, known way, to request access to the user’s location data.
Then, replace the code of body with:
VStack {
// 1
Image("creature_dog-and-bone")
.resizable()
.frame(width: 240, height: 240)
// 2
Text("""
To find pets near you, first, you need to
share your current location.
""")
.multilineTextAlignment(.center)
// 3
LocationButton {
locationManager.startUpdatingLocation()
}
.symbolVariant(.fill)
.foregroundColor(.white)
.cornerRadius(8)
}
.padding()
.onAppear {
// 4
locationManager.updateAuthorizationStatus()
}
Here’s a code breakdown:
-
Adds an image of a dog as a placeholder.
-
A
Textthat explains why the user has to share their current location. -
A button to ask the user to share their location. Here, you call
startUpdatingLocationto starts tracking the user’s location. -
An
onAppear(perform:)view modifier to update the authorization status when the view first appears.
Next, add the following under RequestLocationView, inside the preview code:
.environmentObject(LocationManager())
You need this code to make Xcode previews work because you use an @EnvironmentObject inside the view.
The new Location button
LocationButton is a new SwiftUI button that brings a couple of improvements to asking permission to access the user’s location. This new button doesn’t require you to add a reason text in the info.plist that explains why you want to access the user’s location data. It already has a default text and alert for asking permission when the user taps the button and doesn’t require you to manually call a method.
It also creates a consistent design language between iOS apps while still allowing you to customize their appearance.
Location status types
iOS has five location authorization statuses:
-
notDetermined: iOS returns this status when
CLLocationManagerdoesn’t yet know if the user has granted or denied access to their location data. -
restricted: This status doesn’t mean the user has denied authorization. Instead, it indicates the user has an active parental control restriction. The user can’t change the authorization status. However, the user’s parent can change it under Settings ▸ Screen Time ▸ Content & Privacy Restrictions ▸ Privacy ▸ Location Services.
-
denied: This status explicitly indicates that the user has denied authorization for this app to access their location data.
-
authorizedAlways: Apple introduced this status in iOS 8 with
authorizedWhenInUseto improve user privacy. It indicates the app has access to the location data at any time, even when the user is not using the app. -
authorizedWhenInUse: This status indicates the app has access to the user’s location data only when the user is using the app.
You have to keep track of these statuses when working with location data to handle them in the features using location data.
When you use LocationButton, it grants the app a temporary authorizedWhenInUse if the user grants permission. This is great for features that require a one-time authorization to work.
Updating Animals Near You to request authorization
Now that you have a view for asking for the user’s authorization, it’s time to update Animals Near You to use it.
Back inside AnimalsNearYouView.swift, add:
@EnvironmentObject var locationManager: LocationManager
This line helps access locationManager using @EnvironmentObject.
Now, replace the code inside NavigationView with:
// 1
if locationManager.locationIsDisabled {
RequestLocationView()
.navigationTitle("Animals near you")
} else {
// 2
AnimalListView(animals: animals) {
if !animals.isEmpty && viewModel.hasMoreAnimals {
HStack(alignment: .center) {
LoadingAnimation()
.frame(maxWidth: 125, minHeight: 125)
Text("Loading more animals...")
}
.task {
await viewModel.fetchMoreAnimals()
}
}
}
.task {
await viewModel.fetchAnimals()
}
.listStyle(.plain)
.navigationTitle("Animals near you")
.overlay {
if viewModel.isLoading && animals.isEmpty {
ProgressView("Finding Animals near you...")
}
}
}
Here’s what’s happening:
-
First, you use
locationIsDisabledto check if you have access to the user’s location. It’s a computed property ofLocationManagerthat checks the app’s location status. If the app doesn’t have permission to access the user’s location data, you show the newRequestLocationViewto ask for authorization. -
If the app has access to location services, you show the list of animals, just like before.
Finally, inside the preview at the bottom of the file, add the following line at the end of the view:
.environmentObject(LocationManager())
This code makes Xcode previews work since you’re using an @EnvironmentObject inside AnimalsNearYouView.
Before you build and run, go back to ContentView.swift and add the following property:
@StateObject var locationManager = LocationManager()
Here, you use @StateObject to store an instance of LocationManager. Then, at the end of tab view, add the environmentObject:
.environmentObject(locationManager)
You then pass the locationManager to the environment of the view.
@StateObject is a property wrapper that works like @State, except it creates a single instance only once, even if the view is invalidated and recreated.
Before you build and run, update the preview code with the following line under ContentView:
.environmentObject(LocationManager())
This code makes preview work.
Build and run.
Tap Current Location.
Tapping the button presents an alert requesting the user’s authorization to access their location. The user may or may not allow it. If the user taps Not Now, the same alert shows till the user taps OK. Thus, it becomes clear to the user that the location-based feature will only work when they allow access.
Tapping OK shows the list of animals once again.
Sending location data in the request
Now, PetSave asks for authorization to access the user’s location. However, it’s not doing anything with this data yet.
You’ll add this data in the body of the request to fetch animals.
Open AnimalsNearYouViewModel.swift and find the following code inside AnimalsFetcher:
func fetchAnimals(page: Int) async -> [Animal]
Update this line with:
func fetchAnimals(
page: Int,
latitude: Double?,
longitude: Double?
) async -> [Animal]
This code updates the protocol method to accept latitude and longitude as parameters.
Now, open services/FetchAnimalsService.swift and update fetchAnimals(page:) to conform to AnimalsFetcher:
func fetchAnimals(
page: Int,
latitude: Double?,
longitude: Double?
) async -> [Animal] {
Next, find the following two lines inside fetchAnimals(page:latitude:longitude:):
latitude: nil,
longitude: nil
And replace them with:
latitude: latitude,
longitude: longitude
This updates FetchAnimalsService to accept latitude and longitude to pass them as parameters to the request.
Sending a latitude and longitude to Petfinder’s API makes it search and return pets in a 100 miles radius of that location.
Back inside AnimalsNearYouViewModel.swift, import CoreLocation and replace:
func fetchAnimals() async {
With:
func fetchAnimals(location: CLLocation?) async {
This code updates fetchAnimals to take a CLLocation instance with the user’s current location.
Next, replace the contents of fetchAnimals(location:) with:
isLoading = true
do {
// 1
let animals = await animalFetcher.fetchAnimals(
page: page,
latitude: location?.coordinate.latitude,
longitude: location?.coordinate.longitude
)
// 2
try await animalStore.save(animals: animals)
// 3
hasMoreAnimals = !animals.isEmpty
} catch {
// 4
print("Error fetching animals... \(error.localizedDescription)")
}
isLoading = false
Here, you:
-
Pass the user’s latitude and longitude from
locationto the request to fetch animals. -
Store the animals from the response just like before.
-
Set
hasMoreAnimalsto false if the response returned no animals. -
Catch and print the error fetching animals may cause.
You also have to update fetchMoreAnimals to pass the location data.
Find fetchMoreAnimals and replace it with:
func fetchMoreAnimals(location: CLLocation?) async {
Next, replace:
await fetchAnimals()
With:
await fetchAnimals(location: location)
fetchAnimals(location:) will now fetch pets with the user’s location.
Before you move on, update AnimalsNearYouView.swift to get the user’s location and pass it to the view model call.
Find the following line:
await viewModel.fetchMoreAnimals()
And replace it with:
await viewModel.fetchMoreAnimals(location: locationManager.lastSeenLocation)
Also, find:
await viewModel.fetchAnimals()
And replace it with:
await viewModel.fetchAnimals(location: locationManager.lastSeenLocation)
The view model uses the location manager’s lastSeenLocation to fetch the pets.
Finally, you also have to update AnimalsFetcherMock.swift to conform to AnimalsFetcher.
Open AnimalsFetcherMock.swift and replace fetchAnimals(page:) with:
func fetchAnimals(
page: Int,
latitude: Double?,
longitude: Double?
) async -> [Animal] {
Again, the mock method uses latitude and longitude to fetch animals.
Build and run. Tap Current Location to list animals near you.
Great, AnimalsNearYouView now uses the user’s location to list pets near them.
However, every time you close the app and relaunch it, you’ll have to tap Current Location again.
That’s because LocationButton only grants the app a temporary authorizedWhenInUse. So, every time you open the app, you’ll have to tap Current Location again.
Note: When the user taps OK, the alert asking for their permission is not displayed again.
That’s not a good user experience, at least not for this type of feature.
You’ll change the implementation of LocationManager to request a permanent authorizedWhenInUse.
Requesting authorization when in use
Open LocationManager.swift and add the function below to LocationManager:
func requestWhenInUseAuthorization() {
cllLocationManager.requestWhenInUseAuthorization()
}
requestWhenInUseAuthorization is a method from CLLocationManager that asks the user’s permission for their location data. This method prompts an alert to the user asking permission to access their location.
Note: This method requires adding a reason text, inside info.plist, explaining why your app needs the user’s location while using the app. You use the key Privacy - Location When In Use Usage Description and a text explaining how you’ll use the user’s location. The sample project already comes with this text inside its info.plist.
Back in RequestLocationView.swift, inside the action of LocationButton, find:
locationManager.startUpdatingLocation()
Replace it with:
locationManager.requestWhenInUseAuthorization()
Now when the user taps Current Location, it’ll prompt the new alert to ask for a permanent authorizedWhenInUse.
Finally, delete PetSave from the device or simulator you’re using for more accurate results. Then, build and run. Tap Current Location.
The user can Allow Once, Allow While Using App or Don’t Allow. If the user taps Allow Once, the app will have the same one-time authorizedWhenInUse as LocationButton. If the user taps Allow While Using App, the app will have a permanent authorizedWhenInUse.
If the user taps Don’t Allow, the app won’t have access to their location, and tapping the button won’t do anything.
Go ahead and tap the different options to test the behavior. You can delete PetSave from the device or simulator to get a fresh start every time.
Location accuracy
iOS also has a neat feature for protecting people’s privacy. Most features that request the user’s location don’t need their accurate location. They only require an approximate location to recommend or find places nearby them.
When allowing an app access to their location, people can choose whether it’s their precise location or not.
So, instead of giving the app their full location, users can choose to disable precise location, giving the app an approximate range where they are. The app can still use this data to drive features, and the user’s precise location is still private.
Adapting the UI depending on accuracy level
Dealing with location accuracy means the app has to adapt to some situations. Take Apple Maps for example. When the user allows the app to use a precise location, a blue dot in the map represents their location.
However, if the user disables precise location, a shaded circular area represents their approximate location.
This example shows how an app’s behavior can change depending on the user’s choice.
Another way Apple Maps adapts is the Favorites section. If precise location is off, it doesn’t show an Estimated Time of Arrival for each place since calculating this value requires more precise data.
You can use requestTemporaryFullAccuracyAuthorization(withPurposeKey:) to request a precise location temporarily to increase the accuracy until the next app launch.
While designing your app, it’s important to pay attention to your users’ privacy and your app needs. If you don’t need to use precise location data, there’s no need to ask for it.
AnimalsNearYouView requires a location to work, but it doesn’t matter if it’s precise. The API still finds pets within a 100 miles radius.
Updating the tests
Before you finish this feature, you have to update your tests to take into account the new location manager property.
Inside AnimalsNearYouViewModelTestCase.swift, find the following line inside testFetchAnimalsLoadingState and testFetchAnimalsEmptyResponse:
await viewModel.fetchAnimals()
And replace it with:
await viewModel.fetchAnimals(location: nil)
Next, inside testUpdatePageOnFetchMoreAnimals, replace:
await viewModel.fetchMoreAnimals()
With:
await viewModel.fetchMoreAnimals(location: nil)
And finally, inside testFetchAnimalsEmptyResponse, replace:
await viewModel.fetchAnimals()
``
With:
```swift
await viewModel.fetchAnimals(location: nil)
In these tests, AnimalsNearYouViewModel initializes the location with nil.
Finally, inside EmptyResponseAnimalsFetcherMock at the bottom, update fetchAnimals(page:) with:
func fetchAnimals(
page: Int,
latitude: Double?,
longitude: Double?
) async -> [Animal] {
This test now uses latitude and longitude to fetch animals.
Build and run the tests.
App Store’s Privacy section
When launching an app in the App Store, Apple makes every developer provide a list of data their app collects. App Store’s Privacy Section ensures developers explain to their users what kind of data they’re collecting.
Whenever a developer wants to release an app in the App Store, they must provide a list of the data they and third-party partners collect. This information allows users to better choose and understand what kind of data they’ll be giving the developer before they download the app.
Understanding different types of data
Apple categorizes the many types of data an app may collect. It requires you to understand which kind of data you collect and disclose them under the following categories:
-
Contact Info: Data that may contain the user’s name, email, phone, physical address or any other information that could be used to contact them.
-
Health & Fitness: Data related to the user’s health and fitness, including data from HealthKit API or the Fitness API.
-
Financial Info: Any data related to payments and purchases inside the app or related to the user’s assets and financial information.
-
Location: The precise location data and course location information of any route the user may take, including approximate location.
-
Sensitive Info: Sensitive info may include personal information data such as racial or ethnic data, sexual orientation, political opinion and biometric data.
-
Contacts: The app may access users’ contacts with names, phone numbers and email.
-
User Content: Any data that users create like text messages, email, photos and videos and audio data, as well as gameplay data for games.
-
Browsing History: Any information regarding content the user has viewed online outside the app.
-
Search History: Search information like search queries inside the app.
-
Identifiers: Any data that may identify the user like a screen name, handle or ID, including Device IDs, such as the device’s advertising identifier.
-
Purchases: Tracking a user’s purchases or purchase tendencies.
-
Usage Data: Any device interactions such as taps, clicks and scrolling. Any other information related to how the user interacts with the device and app, including information about advertisements the user may have seen or interacted with.
-
Diagnostics: Any performance, crash and log data.
-
Other Data: Any other type of data related to the user.
These categories describe the type of data developers may want to collect. Users can see the whole list and what data the app collects.
When you open an app in the App Store and scroll down, you’ll find the App Privacy section with the data the developer has provided.
For example, the raywenderlich.com app collects the user’s Search History, Usage Data and Identifier. It also collects Diagnostics data not linked to users.
Tapping the section will open the details of the data that it collects.
Key points
-
Users’ privacy is very important. Always design your app with privacy in mind.
-
Location data is also private data, and developers must handle it with care.
-
You can use
LocationButtonfor features that require the person’s location on a one-time basis. -
Use
requestWhenInUseAuthorizationto request the person’s location whenever they use your app. -
Not all apps require precise location data. When designing your app, remember people may not want to share their precise location.
-
App Store’s App Privacy section is the place where you’ll disclose what kinds of data your app collects.
Where to go from here?
In this chapter, you went over what’s privacy and why you should care. Also, you added the functionality to get the current location so users of PetSave can enjoy a more personalized experience.
To learn more about privacy, iOS’s latest privacy features, and requesting location data, check out our article What’s New With Privacy?.
You can also learn more about app privacy on Apple’s App privacy details on the App Store page.
In the next chapter, you’ll learn some techniques to find bugs in your app. Debugging is part of the software development process, so it makes sense to go over it while creating a real-world app.