Chapters

Hide chapters

SwiftUI by Tutorials

Fifth Edition · iOS 16, macOS 13 · Swift 5.8 · Xcode 14.2

Before You Begin

Section 0: 4 chapters
Show chapters Hide chapters

10. More User Input & App Storage
Written by Antonio Bello

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

In the last two chapters, you learned how to use state and how easy it is to make the UI react to state changes; you also implemented reactivity to your custom reference types.

In this chapter, you’ll meet a few other input controls: lists with sections, steppers, toggles and pickers. To do so, you’ll work on a new Kuchi app section dedicated to its settings.

Since you’ll implement this new feature as a separate new view, you might think you need to add some navigation to the app — and you’d be right. In fact, you’ll add tab-based navigation later on.

For now, you’ll create a new setup view and make it the default view displayed when you launch the app.

You’ll find the starter project, along with the final project, in the materials for this chapter. It’s almost the same final project you left in the previous chapter, so feel free to use your own copy that you’ve worked on so far. However, in this case, you need to manually add the content of the Shared/Utils folder to the project, which contains these three files:

  • Color+Extension: contains some UIColor extension methods.
  • LocalNotifications: helper class to create local notifications.
  • Appearance: defines an enumeration used to describe the app’s appearance.

Creating the Settings View

Before doing anything else, you must create the new settings view and make it the default view displayed at launch.

Open the starter project or your project from the previous chapter. In the Shared folder, create a new group, call it Settings, then create a new file inside it using the SwiftUI template, and name it SettingsView.

New setting group
New setting group

Now, to make Settings the initial view, open KuchiApp and, in body, replace the code that instantiates StarterView, along with its modifiers, with:

SettingsView()

If you now run the app, it’ll show the classic, but never outdated, Hello, World! message that every developer has met at least a hundred times in their developer life.

Empty settings view
Empty settings view

Now that everything is set up, you can focus on building the settings view. Your goal is to create something that looks like this:

Final settings view
Final settings view

You can see that the view has:

  • A Settings title.
  • Three sections: Appearance, Game and Notifications.
  • One or more items (settings) per section.

To implement this structure in UIKit, you would probably opt for a UITableView with static content or a vertical UIStackView. In AppKit, you’d use a slightly similar way.

In SwiftUI, you’ll use a List, a container view that arranges rows of data in a single column. Additionally, you’ll use a Section for each of the three sections listed above. This is just an implementation-oriented peek, and you’ll learn more about lists in Chapter 14: “Lists”.

The Skeleton List

Adding a list is as easy as declaring it in the usual way you’ve already done several times in SwiftUI. Before starting, resume the preview so you have visual feedback of what you’re doing in real-time, step by step.

List {
}
Text("Settings")
  .font(.largeTitle)
  .padding(.bottom, 8)
Section(header: Text("Appearance")) {
}

Section(header: Text("Game")) {
}

Section(header: Text("Notifications")) {
}
Sections
Zonceoxh

The Stepper Component

It’s good practice to start from the beginning; in fact, you’ll start populating the … erm … second section. :]

Stepper
Qzasjix

@State var numberOfQuestions = 6
// 1
VStack(alignment: .leading) {
  // 2
  Stepper(
    "Number of Questions: \(numberOfQuestions)",
    value: $numberOfQuestions,
    // 3
    in: 3 ... 20
  )
  // 4
  Text("Any change will affect the next game")
    .font(.caption2)
    .foregroundColor(.secondary)
}
Number of questions
Sebcem eq gouccuuwz

The Toggle Component

The second setting you’ll add is a switch that enables or disables the Learning section of the Kuchi app. Before you go and browse all the previous chapters to search for something you might have forgotten, you should be aware that there’s no such section yet, you’ll add it in the next chapter.

@State var learningEnabled: Bool = true
Toggle("Learning Enabled", isOn: $learningEnabled)
Learning enabled
Cuelpaxy uyisveb

The Date Picker Component

The following section you’ll take care of is Notifications. You might be wondering what notifications have to do with Kuchi.

@State var dailyReminderEnabled = false
HStack {
  Toggle("Daily Reminder", isOn: $dailyReminderEnabled)
}
Notifications toggle
Hapobafiveocq porjca

DatePicker(
  // 1
  "",
  // 2
  selection: $dailyReminderTime
)
@State var dailyReminderTime = Date(timeIntervalSince1970: 0)
Date picker
Xake neqdax

Date Picker Styles

In iOS, the date picker comes in three different styles, which you can configure using the .datePickerStyle() modifier, similarly to how it works for TextField, which you encountered in Chapter 6: “Controls & User Input”. The three styles are:

Configuring the Daily Reminder Time Picker

After some theory, it’s time to get back to Kuchi. The date picker with compact style looks great, but there’s one issue: you don’t need the date. This picker is to select a time of the day, but there’s no date component because you want it to remind you every day.

DatePicker(
  "",
  selection: $dailyReminderTime,
  // Add this, but don't forget the trailing
  // comma in the previous line
  displayedComponents: .hourAndMinute
)
Time picker
Boyi lopcof

.disabled(dailyReminderEnabled == false)

Activating Notifications

Now that the user interface part of the time picker is complete, you need to make it functional. The requirements are pretty simple:

Adding a Custom Handler to the Toggle

It would be nice if you could intercept when the binding updates and inject a call to a method that creates or removes a local notification. This is exactly what you’re going to do now.

Toggle("Daily Reminder", isOn: $dailyReminderEnabled)
Toggle("Daily Reminder", isOn:
  // 1
  Binding(
    // 2
    get: { dailyReminderEnabled },
    // 3
    set: { newValue in
      // 4
      dailyReminderEnabled = newValue
    }
  )
)
configureNotification()
func configureNotification() {
  if dailyReminderEnabled {
    // 1
    LocalNotifications.shared.createReminder(
      time: dailyReminderTime)
  } else {
    // 2
    LocalNotifications.shared.deleteReminder()
  }
}
Toggle("Daily Reminder", isOn: $dailyReminderEnabled)
.onChange(
  of: dailyReminderEnabled,
  perform: { _ in configureNotification() }
)
.onChange(
  of: dailyReminderTime,
  perform: { _ in configureNotification() }
)
Section(header: Text("Notifications")) {
  HStack {
    Toggle("Daily Reminder", isOn: $dailyReminderEnabled)
    DatePicker(
      "",
      selection: $dailyReminderTime,
      displayedComponents: .hourAndMinute
    )
  }
}
.onChange(
  of: dailyReminderEnabled,
  perform: { _ in configureNotification() }
)
.onChange(
  of: dailyReminderTime,
  perform: { _ in configureNotification() }
)
Local notification
Xibor zayoceposeix

The Color Picker Component

Now swift … ehm, shift your focus on the app’s appearance. :]

@State var cardBackgroundColor: Color = .red
ColorPicker(
  "Card Background Color",
  selection: $cardBackgroundColor
)
Color picker
Lagiy vuftah

The Picker Component

The last setting you’re offering to your users is the ability to select the app appearance, either light or dark, a popular setting among modern apps.

@State var appearance: Appearance = .automatic
VStack(alignment: .leading) {
  ColorPicker(
    "Card Background Color",
    selection: $cardBackgroundColor
  )
}
// 1
Picker("", selection: $appearance) {
  // 2
  Text(Appearance.light.name)
  Text(Appearance.dark.name)
  Text(Appearance.automatic.name)
}
Default picker
Labiaxl yemceq

Styling the Picker

In order to change the style, you have a modifier at your disposal. It’s an established pattern in SwiftUI and should already look familiar to you. In this case, it’s called .pickerStyle(_:).

.pickerStyle(SegmentedPickerStyle())
Picker segmented
Faggam duwzipsus

Binding Options to the Picker State

If you look at the picker declaration, you can notice that:

Picker("Pick", selection: $appearance) {
  Text(Appearance.light.name)
  Text(Appearance.dark.name)
  Text(Appearance.automatic.name)
}
Text(Appearance.light.name).tag(Appearance.light)
Text(Appearance.dark.name).tag(Appearance.dark)
Text(Appearance.automatic.name).tag(Appearance.automatic)
Settings view
Guttujbr duel

Iterating Options Programmatically

A keen eye like yours has probably realized that:

ForEach(Appearance.allCases) { appearance in
  Text(appearance.name).tag(appearance)
}

The Tab Bar

Well done, now you’ve got a working settings view! But, currently, it’s the only view that your app provides access to. At the beginning of this chapter, you replaced StarterView with SettingsView as the only view. Of course, this doesn’t make sense even in the least meaningless apps.

// 1
TabView {
  EmptyView()
}
// 2
.accentColor(.orange)
// 1
SettingsView()
  // 2
  .tabItem({
    // 3
    VStack {
      Image(systemName: "gear")
      Text("Settings")
    }
  })
  // 4
  .tag(2)
Settings tab
Fahliqnx vib

PracticeView(
  challengeTest: $challengesViewModel.currentChallenge,
  userName: $userManager.profile.name,
  numberOfAnswered:
    .constant(challengesViewModel.numberOfAnswered)
)
  .environment(
    \.questionsPerSession,
    challengesViewModel.numberOfQuestions
  )
HomeView()
PracticeView(
  challengeTest: $challengesViewModel.currentChallenge,
  userName: $userManager.profile.name,
  numberOfAnswered:
    .constant(challengesViewModel.numberOfAnswered)
)
  .environment(
    \.questionsPerSession,
    challengesViewModel.numberOfQuestions
  )
.tabItem({
  VStack {
    Image(systemName: "rectangle.dock")
    Text("Challenge")
  }
})
.tag(1)
TabView {
  PracticeView(
    challengeTest: $challengesViewModel.currentChallenge,
    userName: $userManager.profile.name,
    numberOfAnswered: .constant(challengesViewModel.numberOfAnswered)
  )
  .tabItem({
    VStack {
      Image(systemName: "rectangle.dock")
      Text("Challenge")
    }
  })
  .tag(1)
  .environment(
    \.questionsPerSession,
    challengesViewModel.numberOfQuestions
  )

  SettingsView()
    .tabItem({
      VStack {
        Image(systemName: "gear")
        Text("Settings")
      }
    })
    .tag(2)
}
.accentColor(.orange)
@EnvironmentObject var userManager: UserManager
@EnvironmentObject var challengesViewModel: ChallengesViewModel
HomeView()
  .environmentObject(UserManager())
  .environmentObject(ChallengesViewModel())
Challenge settings tab
Hxaqwewne hojtatyv qah

StarterView()
  .environmentObject(userManager)
  .environmentObject(challengesViewModel)
@State var selectedTab = 0
TabView(selection: $selectedTab) {

App Storage

The settings view you’ve created in this chapter looks great, but it misses two important points:

Storing Settings to UserDefaults

Open SettingView and replace the line that declares where the state variable numberOfQuestions with:

@AppStorage("numberOfQuestions")
var numberOfQuestions = 6
@AppStorage("numberOfQuestions")
private(set) var numberOfQuestions = 6
Stepper(
  "Number of Questions: \(challengesViewModel.numberOfQuestions)",
  value: $challengesViewModel.numberOfQuestions,
  in: 3 ... 200
)
@EnvironmentObject
var challengesViewModel: ChallengesViewModel
SettingsView()
  .environmentObject(ChallengesViewModel())
@AppStorage("numberOfQuestions")
var numberOfQuestions = 6
let numberOfQuestions: Int
@Binding var numberOfQuestions: Int
@State static var numberOfQuestions: Int = 6
ScoreView(
  numberOfQuestions: $numberOfQuestions,
  numberOfAnswered: $numberOfAnswered
)
@Environment(\.questionsPerSession) var questionsPerSession
@EnvironmentObject
var challengesViewModel: ChallengesViewModel
ScoreView(
  // Update this parameter
  numberOfQuestions: $challengesViewModel.numberOfQuestions,
  numberOfAnswered: $numberOfAnswered
)
.environment(
  \.questionsPerSession,
  challengesViewModel.numberOfQuestions
)
Number of questions updated
Vigzij if zeezfaubg occacog

Storable Types

If you have ever used UserDefaults, you know you can’t store any arbitrary type. You’re restricted to:

Using RawRepresentable

A real example of the former case is appearance, which is of the Appearance enum type, hence not storable by default. However, if you open Shared/Utils/Appearance, you’ll notice that the enumeration implicitly conforms to RawRepresentable, having it as a raw value of Int Type. Remember, if you specify a raw value type for an enum, it will automatically conform to RawRepresentable.

@AppStorage("appearance") var appearance: Appearance = .automatic

Using a Shadow Property

In cases where a supported type is not an option and so is conforming to RawRepresentable, you can declare a shadow property that is AppStorage friendly.

@AppStorage("dailyReminderTime")
var dailyReminderTimeShadow: Double = 0
.onChange(
  of: dailyReminderTime,
  perform: { newValue in
    dailyReminderTimeShadow = newValue.timeIntervalSince1970
    configureNotification()
  }
)
.onAppear {
  dailyReminderTime = Date(timeIntervalSince1970: dailyReminderTimeShadow)
}
@AppStorage("dailyReminderEnabled")
var dailyReminderEnabled = false
Testing daily reminder
Tiwbesl cuuqg kazeprex

@AppStorage("appearance")
var appearance: Appearance = .automatic
.preferredColorScheme(appearance.getColorScheme())
Testing appearance
Pofyifn osnaetujre

SceneStorage

Alongside AppStorage, SwiftUI also offers a @SceneStorage attribute that works like @AppStorage, except that the persisted storage is limited to a scene instead of being app-wide. This is very useful if you have a multi-scene app. Unfortunately, Kuchi isn’t so you won’t cover it here. But it’s definitely beneficial for you to know! In the Where to Go From Here sections, there’s a resource on learning more about both AppStorage and SceneStorage.

Key Points

  • In this chapter, you’ve played with some of the UI components SwiftUI offers by using them to build a settings view in the Kuchi app. There are a few more, and you can use the ones you’ve used here differently. Take, for example, the date picker, which you can use to pick a date, a time, or both.
  • You’ve looked at the three different styles of components; the stepper component, the toggle component and the date picker.
  • You’ve also witnessed how easy creating a tabbed UI is.
  • Lastly, you used AppStorage to persist settings to the user defaults.

Where to Go From Here?

This is just a short list of documentation you can browse to know more about the components you’ve seen here and what you haven’t.

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

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now