A Bucket List application where you can keep track of all of the things you want to do and places you want to go before you die.
Begin by creating a new iOS App in Xcode and name it Bucket List.
As ContentView
is going to be displaying our list of items on our bucket list, it is not a very clear description of the view's purpose so it is good practice to rename your views to reflect the content that it will display.
Rename the view by selecting the ContentView text and right-click and choose Refactor -> Rename... and give it the name BucketListView
Change the preview provider as well so that it is named BucketListView_Previews
To get started, create a constant called bucketList
and initialize it with an array of Strings representing 3 or 4 examples of items that might be on your bucket list
let bucketList = ["Climb Everest", "Visit Hawaii", "Get Married"]
Replace the VStack
and padding with a List
view that iterates over the bucketList and use the \.self
keyPath for the id
.
This will provide you with an iterator you can call item
and we will use it in
the body of the closure where we can create a Text
view that will display that item.
Set the listStyle
of the list to .plain
.
Embed the list in a NavigationStack
.
Add a .navigationTitle
to the List and the style and assign the string "Bucket List".
struct BucketListView: View {
let bucketList = ["Climb Everest", "Visit Hawaii", "Get Married"]
var body: some View {
NavigationStack{
List(bucketList, id: \.self) { item in
Text(item)
}
.listStyle(.plain)
.navigationTitle("Bucket List")
}
}
}
If we want to be able to add, update, delete or edit one of the items in the bucketList, it will need to be changed to an @State
property and make it private (this is best practice).
Create another private @State
property called newItem
and initialize it as an empty string.
Embed everything in the NavigationStack
except the navigationTitle
in a VStack
.
As the first item in the VStack
, create an HStack
.
Inside the HStack
, create a TextField
..
For the title, use the string "New Bucket Item" and for the text, bind it to $newItem
.
Apply the .roundedBorder
.textFieldStyle
.
Create a button and use the overload that will allow us to specify a view instead of just a string for the label.
Button {
} label: {
}
For the label, use Image(systemName: "plus.circle.fill")
For the action.
Append
the newItem
to the bucketList.
Set newItem
back to an empty String ("").
Set the .disabled
modifier on the button to be true with the newItem
is empty.
Apply padding
to the HStack
.
struct BucketListView: View {
@State private var bucketList = ["Climb Everest", "Visit Hawaii", "Get Married"]
@State private var newItem = ""
var body: some View {
NavigationStack{
VStack {
HStack {
TextField("New Bucket item", text: $newItem)
.textFieldStyle(.roundedBorder)
Button {
bucketList.append(newItem)
newItem = ""
} label: {
Image(systemName: "plus.circle.fill")
}
.disabled(newItem.isEmpty)
}
.padding()
List(bucketList, id: \.self) { item in
Text(item)
}
.listStyle(.plain)
}
.navigationTitle("Bucket List")
}
}
}
If we refactor the List
view to use a ForEach
loop, it provides us with the built in ability to easily add swipe deletion.
Remove the (bucketList, id: \.self) { item in
from the List constructor and leave it simply as List {
Embed the Text
view within a ForEach
loop that uses the same constructor that we had for the List previously.
List {
ForEach(bucketList, id: \.self) { item in
Text(item)
}
}
Apply an .onDelete
method on the ForEach loop that will remove at the offsets of the selected indexSet
.
List {
ForEach(bucketList, id: \.self) { item in
Text(item)
}
.onDelete { indexSet in
bucketList.remove(atOffsets: indexSet)
}
}
When we tap on an item in the list, we want to be taken to a different view that will display the item alone on that view. We will improve on this in the next section.
Change the Text(item)
to a NavigationLink
.
This requires a Hashable
value and since strings are Hashable, we can use our item as that value for the label.
The label requires a view, so we can create a TextView with our item.
Below the .navigationTitle
, but still within the NavigationStack
, add a .navigationDestination
method.
A navigation destination requires that we identify the Type
of object that will initiate the move to the destination. In our case, that is a String
so we enter String.self
The destination will be provided with the actual value that triggered the navigation and we know that will be the item that we tapped on, so we can specify that with a variable called item
that we will use in
the creation of our View
For now, the view that will be presented will just be a Text
view using that item
.
Apply a .font
size of .title
List {
ForEach(bucketList, id: \.self) { item in
NavigationLink(value: item) {
Text(item)
}
}
.onDelete { indexSet in
bucketList.remove(atOffsets: indexSet)
}
}
.listStyle(.plain)
.navigationTitle("Bucket List")
.navigationDestination(for: String.self) { item in
Text(item)
.font(.title)
}
For more information and tutorials on NavigationStack see these videos:
Introduction to NavigationStack in iOS 16 https://youtu.be/6-OeaFfDXXw
iOS 16 Navigation Stack Part 2 - Back to Root and Deep Links https://youtu.be/pwP3_OX2G9A
Enum Navigation in iOS 16 https://youtu.be/RPhBPhHw2gA
struct BucketListView: View {
@State private var bucketList = ["Climb Everest", "Visit Hawaii", "Get Married"]
@State private var newItem = ""
var body: some View {
NavigationStack{
VStack {
HStack {
TextField("New Bucket item", text: $newItem)
.textFieldStyle(.roundedBorder)
Button {
bucketList.append(newItem)
newItem = ""
} label: {
Image(systemName: "plus.circle.fill")
}
.disabled(newItem.isEmpty)
}
.padding()
List {
ForEach(bucketList, id: \.self) { item in
NavigationLink(value: item) {
Text(item)
}
}
.onDelete { indexSet in
bucketList.remove(atOffsets: indexSet)
}
}
.listStyle(.plain)
}
.navigationTitle("Bucket List")
.navigationDestination(for: String.self) { item in
Text(item)
.font(.title)
}
}
}
}
struct BucketListView_Previews: PreviewProvider {
static var previews: some View {
BucketListView()
}
}