LastPass clone - part 6

SwiftUI Apr 20, 2020

In the previous article, we built a custom navigation bar. If you haven’t read it , check it out here. In this one, we will create the filter header which will be an horizontal scrollable list. Let’s. Get started.

Preparations

You should have the source code link in your email inbox if you are subscribed, otherwise click here to subscribe and get the source code.

Filter View

Like I’ve just said in the intro, the header will contain a Scrollview where the filterViews will be the items scrolled horizontally, so let’s first create the FilterView that will be used as a cell.

Create a swift UI file named FilterView.swift in the Views folder. Put the following properties to the top:

    var filter: Filter
    @Binding var isSelected: Bool
    var onSelect: ((Filter)->()) = {_ in }

As you may have noticed, the selectedFilter has a @Binding property wrapper because we want the view’s body to be re-computed when the isSelected state changes.

And the onSelect is a closure that will be called whenever we select one of the the filter item. You will see how we’ll call this later on. Before we implement the body, we still need to create the Filter enum to silence the error you are getting. Create a folder named Utils, add a swift file named Filter.swift inside containing the following code:

enum Filter: String, CaseIterable{
    case AllItems = "All Items"
    case Passwords
    case Notes
    case MostUsed = "Most Used"
    case Favorites
}

Command + B to build the project, and the error should leave you alone except the one produced by the missing binding parameter in the preview. So replace the content of the previews static property with the following:

        FilterView(filter: .AllItems, isSelected: .constant(false))

With that in place, you are totally clear.

Now, replace the content of body with the following:

 ZStack(alignment: .bottom) {
                   Text(filter.rawValue)
                   .layoutPriority(1)
                       .foregroundColor(isSelected ? Color.text : Color.gray)
                   .padding(5)
        }

The only modifier to note here is the layoutPriority. We pass in 1 to make the text value fit perfectly and not being truncated. The default value is 0. You will see why we had to add this modifier later on.

Next add the following code inside the ZStack below the Text:

 if isSelected{
                Rectangle()
                    .frame(width: 50, height: 3)
                    .foregroundColor(Color.accent)
                    .cornerRadius(2)
					.transition(.scale)
            }

We want to the horizontal line only when the filter is selected. Here is a quick explanation of what the above code does:

  1. We use a rectangle which we flattened to make it a line by giving it a height of 3 and a width of 50. One could also use a rotated Capsule by 90 degrees and the result would be the same with more lines of code.
  2. We give the rectangle a foreground color rather than background because it’s a shape.
  3. The corner radius is understandable. We then add a scale transition that will make the line scale from the middle when the filter is selected.

Here is a preview of what we’ve just created:

Last for the filter view, add the following directly on the ZStack container:

.padding().onTapGesture {
            self.onSelect(self.filter)
        }

Here, we’ve added some padding around the FilterView as well as a tapGesture. Inside the tapGesture’s block we call the onSelect closure to send the selected filter to the parent view container.

Header View

Create a swift UI file in the Views folder named HeaderView.swift. Add the following properties to the top:

    @State private var selectedFilter = Filter.AllItems
    var onSelect: ((Filter)->()) = {_ in }

We make selectedFilter mutable by preceding it with @State property wrapper that will be used to update the User interface when it’s set.

@State: SwiftUI manages the storage of any property you declare as a state. When the state value changes, the view invalidates its appearance and recomputes the body. Use the state as the single source of truth for a given view.

Now replace the content of the body with the following:

ScrollView(.horizontal, showsIndicators: false) {
            HStack{
                ForEach(Filter.allCases, id: \.self){ filter in
                    FilterView(filter: filter, isSelected: .constant(self.selectedFilter == filter), onSelect: { selected in
                            // Replace this
                    })
                }
            }
        }

As you can see the filters will be horizontally scrollable. Because the Filter enum conform to CaseIterable, we can get all the cases as an array which in turn allows us to iterate through it using swiftUI’s ForEach. We then return an instance of the FilterView for each filter.

Being a binding property wrapper, the isSelected parameter is set by giving it a constant value using the static function contant(value:) which returns a binding.

Now replace the comment inside the onSelect block with the following:

withAnimation(.spring()) {
                            self.selectedFilter = selected
                        }
                        
                        self.onSelect(selected)

Here, we set the selectedFilter property inside the withAnimation block to animate the selection. At the same time, we want to call the onSelect closure to pass the value to the parent view container.

Now is the time to explain the benefit of the ‘layoutPriority’ modifier. Go back to the FilterView and comment out the .layoutPriority(1), then inside the HeaderView, resume the preview and click play to make it interactive. Now, select on one of the filter item… You should see this:

As you can see the password is being truncated. This happens because the parent does not change its width to accommodate the widest child. So to make the parent view to stretch or shrink sooner to fit a child view, we need to change the layout priority of the child that we know will be wider. Now go back and un-comment the layoutPriority in the FilterView . Try again selecting the filters, and everything should work fine.

And with this, we conclude this part. In the next part we will put this component and the Navigation bar together after creating the list using neumorphic design. Feel free to share this article and subscribe if you haven’t done so already. Thank you for reading HAPPY CODING!!!

John K

I am a software developer and code enthusiast. Do you want to work with me, have a suggestion or a request? Feel free to contact me at [email protected] or https://twitter.com/liquidcoder