Coding Workshop 6 - Bucket List App Part 2

homepage

 

So far so good, but we want to be able to provide more information about our bucket list items and be able to assign a date when completed.

New BucketItem model file

  1. Create a new Swift file (not SwiftUI) and call it BucketItem.

  2. Inside there, create a new struct with the same name.

    All Structs and Classes should be Uppercased - CamelCased. (SomeStruct), properties and variables should be lowercased CamelCased (someProperty),

  3. If we want to be able to use this in a ForEach loop, we nee to be able to identify unique items in our list but how do we identify something that has more than one property. We can solve this by conforming the BucketItem struct to the Identifiable protocol.

  4. This will require the creation of an id property that will be unique.

    1. We can use a special object called a UUID() that generates a random, unique string.

  5. Create a property for name and assign it the type String but provide no initial value.

  6. Create a property called note and assign it the initial value of an empty string. Swift will infer then that name's Type is a String.

  7. Create a property called completedDate and though things are initially not completed, when working with dates, I prefer to assign dates that are not completed using a specific date that is a static Date property called distantPast this is a unique date so far in the past, there is virtually no chance that anyone would ever pick it from a date picker. So whenever we need to check to see if a completed date has been set, we can compare it to Date.distantPast.

Static Array for Sample Items

For design and testing purposes as you build your app, it is useful to create an array of sample items as we did for our simple array of String.

  1. We can do this by creating a static property called samples in our BucketItem struct that will return and array of BucketItem.

  2. For the return, you can begin by creating an array [].

    1. Create a new instance of BucketItem and since id, note and completedDate all have initial values, we only need to provide a name.

    2. If we want, however, we can add additional values to override the default ones.

      1. Create one with a note.

      2. Create one with a note and a completedDate and use today's date as the date - Date().

Update BucketListView

Now that we have our new model and sample items, we can update our BucketListView to use this new model and array of sample data for your project.

State property

Instead of our bucketList being an array of String, we change it to an array of BucketItem and initialize it with our array of samples using our static property.

Button Action

A number of errors occur because we are no longer working with a simple String, but first we should fix our button action.

  1. We will no longer be appending just a newItem string to our bucketList. We will need a new BucketItem initialized with newItem as the name property..

  2. Then the bucketList can append the newBucketItem.

List -> ForEach

Our ForEach loop within the List now must iterate over the new array of BucketItem

  1. We no longer need to provide the id because we have specified that the BucketItem conforms to the Identifiable protocol and it has an id property.

  2. The value for the navigationLink will continue to be the item but the Text view will display only the item.name.

  3. There is one other error that is displayed and this lets us know that BucketItem must also conform to the Hashable protocol.

    In computer science, "hashable" means that an object can be converted into a unique, fixed-size value (called a "hash") using a hash function. This hash can then be used to quickly look up the original object in a collection, such as a dictionary or set. In simple terms, it means that an object can be turned into a unique code so it can be easily found and identified later. In swift, String, Int, Date, Double etc are all Hashable.

    1. Return to the BucketItem struct and since all thee properties are Hashable types, all we need to do is to add the Hashable protocol in our declaration.

Update .navigationDestination

  1. Our .navigationDestination Type is no longer a String.self, so we need to change it to BucketItem.self.

  2. The Text view that we will be presenting then also needs to present the name property of the item.

Change ForEach's Text view to TextField

We way to be able to update my bucketList item names inline, in the list so we can't use a Text view, we need to use a TextField and this asks us for the title which can be a String, and the text argument must be a Binding<String> which means that it is going to update whatever it is bound to. The problem right now is that our item is not a Binding<String>, it is just a String. We can fix this in a list.

  1. Change the ForEach array to a binding of bucketList $ this will allow us to iterate over the item as a binding: $item.

  2. Now we can create our TextField displaying our label string as item.name and the text is bound to $item.

  3. To make the entry more distinguishable and to separate it from the navigation chevron > we can apply a .textFieldStyle of .roundedBorder.

  4. We can also increase the size of the text in a TextField to a .font(.title3).

  5. Then we no longer need to see the separator lines between rows so we apply a listRowSeparator modifier of .hidden to the NavigationLink.

Add Wordwrap

Testing now, you see we can update our items directly in the list, however, if we type too much into our TextField, it just keeps moving to the right, and then if the field loses focus, the text will truncate to show ... at the end.

We can improve the display of our TextField by adding an axis argument of .vertical and this will allow the TextField to grow vertically.

Editable

DetailView

Simply displaying our name when we navigate is really not ideal. What I want to do is to navigate to a new view where I am able to update the other two properties; the note and the completedDate. Remember, we modify the name property within the list so I can use that property when I get to the new view as the navigation bar title.

  1. Create a new SwiftUI view called DetailView

  2. When this view appears, it will be receiving the bucketItem that was tapped on. Unfortunately, this is a "copy" of the actual item and not a reference to the original so we will need to figure out how we can determine which one that is and then update the original, but for now, let's pass it in as a constant called bucketItem that is a BucketItem type.

  3. Since this view will be pushed on top of the list view by a NavigationLink this view will inherit the NavigationStack so we can add a .navigationTitle to the Text view that we have. So we can specify that it is the bucketItem.name property.

PreviewProvider Parameters

The preview provider is complaining because it does not have a bucketItem parameter.

  1. For previews to display content that requires a parameter, we can create static property that will represent a bucketItem and se can use one of the 3 items in our BucketItem.samples array like BucketItem.samples[2]

  2. Then we can provide that for our DetailView

Fix the previewProvider by providing a sample bucketItem

Missing NavigationTitle

You may also notice that the preview canvas is not showing our title. This is because the preview has no idea that it is coming from a NavigationLink.

  1. We can fix this by specifically enclosing the DetailView in the preview in a NavigationStack

    With this done, you should be able to choose a different array index for you static bucketItem and see the title change.

New @State Properties

As mentioned above, the passed in (injected) bucketItem is a copy of the original item so updating it will not update the original.

In addition, this is not a @State or @Binding to the item in list view so we cannot use its properties to update. To solve this, we create new individual @State properties to represent the bucketList properties that we want to update, namely name and completedDate and we can give them both the same default initial values as we do in our model.

Create @State properties for the two properties besides name.

onAppear block

When our view appears we can execute an onAppear block of code and after it appears. The injected bucketItem is known, so we can assign to the two @State properties we just created the values corresponding to our bucketList item.

Form

We can use a Form to to display the properties for updating.

  1. Replace the TextView with a Form block .

  2. Create a TextField .

    1. Use the string "Bucket note" as the title and bind text to the $note @State property.

      1. Add a vertical axis to the TextField so that it will expand vertically as more text is entered.

  3. For the completedDate entry, we want to show an existing date only if the date has been completed so that means if it is not equal to that very old date of Date.distantPast.

    1. If it is not equal to that date, we can create a DatePicker.

      1. For the title we can use "Completed on:"

      2. The selection will be bound to our $completeDate @State property.

      3. We also want to only display the date and not the time as you see the default, so we can specify displayedComponents of .date.

If you change the sample bucketItem array index in the sample to see changes in the preview provider you will see the date show up only when you have specified a date other than Date.distantPast

  1. When there is not date, we want to set one, so create a button.

    1. Set the label to "Add Date".

    2. For the action, set the completedDate to the currentDate.

      This will allow the users to change it if they like, but the default will be the current day.

ternary conditional operator

We can make this button double purposed by using a ternary operator. This is a shortened case of an if...then...else clause and is often referred to as WTF What ? True : False.

You state the comparison that is either true or false as the W, then follow that with a ? And present the True case then a : followed by the False case.

  1. Change the button label to display "Add Date" if comparing the completedDate to DateDistantPast is true, or "ClearDate" if not.

  2. Use an If, else clause in the action to compare the two dates but in the case that the completedDate is equal to the distantPast then we can set the completedDate to today's Date(), else, we will reset it back to the Date.distantPast.

  3. We can also apply a .buttonStyle of .borderedProminent.

GetMarried

Saving Updates

Making these changes does not update our original item though we still need to create an Update button.

.toolbar and ToolbarItem

The navigation bar has a toolbar that we can add ToolbarItems to.

  1. After the .onAppear block add a .toolBar.

  2. Inside there, create a ToolbarItem and accept the default override. SwiftUI is smart enough to know that you want create the item on the navigationBar and that you will want it on the trailing edge. (It is possible to change the toolbar location as well as the location of the item).

  3. Create a button using the label "Update" and leave the action empty for now.

  4. Add a buttonStyle of .borderedProminent.

dismiss @Environment variable

SwiftUI is smart enough to know a lot about what you are doing and it stores that information in what is known as the Environment. One of the things it knows is whether or not you have pushed a view on to the NavigationStack. This is stored in an environment variable with a KeyPath of \.dismiss .

  1. We can access that value using an @Environment property wrapper that we can assign to a variable of the same name.

  1. Once we update our bucketItem we want to go back to the main list so in our action, (after we have coded the updates (we still need to code the update part) we can have the dismiss environment property run as an action.

For more on @Environment variables and values, see these videos:

Environment Variables in SwiftUI https://youtu.be/DyaU04R9kBA

Custom Environment Values in SwiftUI https://youtu.be/rl7xj5usTzk

Updating

To update we need to know the complete array so we can update a single one.

This means we will need to pass it in (inject) from the BucketListView so we have access to it. We are already expecting the bucketItem as a constant, unchanging value.

  1. This will be received but we need to be able to modify it and since we are not creating this item we cannot use @State, instead we use @Binding.

  1. The preview will complain because it is missing that argument. We fix this the same way that we fixed our bucketItem. We create a static property to represent the binding of an array of BucketItem Binding<[BucketItem]>.

    1. To this, we need to assign our array of sample items, but since in the preview, this will not be changing, we can assign it as a constant BucketItem.sample.

    2. Then in the detail view, we can add the required argument for the parameter.

Note: you will often see developers take a shortcut here by creating the static items with the DetailView itself so the PreviewProvider could have been written like this.

 

  1. We can return to the action now and figure out which one of our items in the bound bucketList needs to be updated.

    1. We can determine the index of that by using a findFirstIndex method on a collection like an array that compares the iterator $0 's id with the bucketItem.id.

    2. Unfortunately, there may be no match so what gets returned might be nil so we need to make that comparison.

    3. If it is not nil, we can update the note and completedDate properties at that index.

      Note: using ! On the index will perform what is known as a force unwrap of an optional property (one that can possibly be nil) and guarantees that it exists. We know it does because we are within an if block that assures that for us so it is safe here to force unwrap

  2. The let index = ...and then checking to see if index != nil can be shortened into a single line using if let index =...1 When we do that, this is doing the check for us and is unwrapping the optional value so there is no need to use ! to unwrap our index and the above code can be shortened to this.

Update

BucketListView

We need to return to the BucketListView now and update destination.

  1. Replace the Text View and the font modifier with a DetailView.

    1. For the bucketItem, we can pass in the item that we received from our NavigationLink.

    2. The bucketList requires a binding to an array of BucketItem and we have that so we can specify it as a binding by prefacing it with a $.

     

Conditionally color if the item has been completed

I want to do one more thing, using a ternary operator again (W ? T : F) and apply a modifier to the TextField to change the .foregroundColor if the item's completed date is not distantPast to .red. If it is still distant past, we use the default .primary color.

image-20230117093312998

Part 2 completed Code

BucketListView

DetailView

BucketItem

Back to Project Index