Lastpass clone 14 - Final

lastpass redesign Jun 29, 2020

Navigation drawer and clean up

Hey guys, in the previous post we finished implementing all the CoreData stuff, if you haven’t read it, I suggest you read it first before this. In this one, we will finish off this series by implementing the navigation drawer and other miscellaneous stuff.

Preparations

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

There are multiple ways to implement the navigation drawer, I will use one method, but you are totally free to modify it or implement your own.

We will have 4 screens and we need to create a navigation menu item for each of them. In the utils folder, add a file named Screens containing the following code:

enum Screens: String, CaseIterable {
    case Vault
    case Premium = "Go premium"
    case Security
    case Settings
}

Then in the Views folder, add a swiftUI file named MenuItem. Replace the content of the entire file with the following:

import SwiftUI

struct MenuItem: View {
    var icon: String = "briefcase.fill"
    var screen: Screens = .Vault
    @Binding var isSelected: Bool
    var onSelect: ((Screens)->()) = {_ in}
    
    var body: some View {
        
        Button(action: {
            HapticFeedback.generate()
            self.onSelect(self.screen)
        }) {
            HStack {
                       Image(systemName: icon).imageScale(.large).foregroundColor(isSelected ? .accent : .gray)
                       Spacer()
                       Text(screen.rawValue)
                           .frame(maxWidth: .infinity, alignment: .trailing)
                           .foregroundColor(isSelected ? .accent : .gray)
                   }.padding()
                       .background(Color.background)
                       .cornerRadius(20).modifier(NeumorphicEffect())
        }
    }
}

struct MenuItem_Previews: PreviewProvider {
    static var previews: some View {
        MenuItem(isSelected: .constant(false))
    }
}

The above code creates the following view:

https://res.cloudinary.com/liquidcoder/image/upload/v1591865787/lastpass%20clone/vet3bgxjyudafualf8wt.png
https://res.cloudinary.com/liquidcoder/image/upload/v1591865787/lastpass%20clone/vet3bgxjyudafualf8wt.png

In the Views folder, add another swiftUI file named MenuContent. Add the following properties to the top of the struct:

    private let icons = ["star.fill","shield.lefthalf.fill","briefcase.fill"]
    @Binding var showMenu: Bool
    @Binding var selectedScreen: Screens

Next, add the following function below body:

    fileprivate func createMenuItem(icon: String, screen: Screens) -> MenuItem {
        return MenuItem(icon: icon , screen: screen, isSelected: .constant(self.selectedScreen == screen),onSelect: { selectedScreen in
            self.selectedScreen = selectedScreen
            withAnimation {
                self.showMenu = false
            }
        })
    }

Next, replace the content of the body with the following:

        VStack(spacing: 30) {
            Image("singlePass-dynamic").resizable().aspectRatio(contentMode: .fit) .frame(height: 30)
                .padding()
            
            ForEach(0..<icons.count, id: \\.self){ i in
                self.createMenuItem(icon: self.icons[i], screen: Screens.allCases[i])
            }
            
            Spacer()
            createMenuItem(icon: "gear", screen: .Settings)
            
        }.padding(.top, 44)
            .padding(.bottom, 30)
            .padding(.horizontal)
            .padding(.leading, UIScreen.main.bounds.width - UIScreen.main.bounds.width / 1.5 )
            .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .trailing)
            .background(Color.background).edgesIgnoringSafeArea(.all) 

To get rid of the errors you are getting, replace the content of the previews static property with the following :

 MenuContent(showMenu: .constant(false), selectedScreen: .constant(Screens.Vault))

With all of that in place, resume the preview and you should see this:

https://res.cloudinary.com/liquidcoder/image/upload/v1591868568/lastpass%20clone/if9w3jtncvqh7gepx5mo.png
https://res.cloudinary.com/liquidcoder/image/upload/v1591868568/lastpass%20clone/if9w3jtncvqh7gepx5mo.png

It’s time to bring the navigation drawer content to the ContentView. So open the ContentView.swift file, and add the following properties at the top of the struct:

    @State var selectedScreen: Screens = .Vault
    @GestureState private var  translation: CGFloat = 0
    private let triggerLocation = UIScreen.main.bounds.width - 50
    var offset: CGFloat {
        return showMenu ? UIScreen.main.bounds.width / 1.5 : 0
    }

And this function below the body block:

    fileprivate func createMenuContent() -> some View {
        return MenuContent(showMenu: self.$showMenu, selectedScreen: self.$selectedScreen)
            .padding(.top, 40).padding(.bottom, 65)
            
            .rotation3DEffect(Angle(degrees: 30 - Double(30 * dragPercent())) , axis: (x: 0, y: 1, z: 0))
            .offset(x: 100 - (100 * dragPercent()), y: 0)
            .animation(.spring(), value: showMenu)
            .onTapGesture {
                if self.showMenu { HapticFeedback.generate() }
                self.showMenu = false
                
        }
    }
  

The above function creates and customises the menu content, we’ve also added a tapGesture recogniser that will be used to close the navigation drawer.

Next, add the following function below:

// 1    
fileprivate func dragPercent() -> CGFloat {
         min(1,(self.offset +  self.translation) / (UIScreen.main.bounds.width / 1.5))
    }
       
// 2    
fileprivate func handleGesture() -> _EndedGesture<GestureStateGesture<DragGesture, CGFloat>> {
        return DragGesture().updating(self.$translation, body: { (value, state, transaction) in
            if value.startLocation.x > self.triggerLocation {
                state = -value.translation.width
            }
        }
        ).onEnded({ value in
            guard value.startLocation.x > self.triggerLocation else { return }
            HapticFeedback.generate()
            self.showMenu = value.translation.width < 0
        })
    }

Here is what those 2 are doing:

  1. The first one just calculates the drag progress in percentage based on the drag translation.
  2. The second one, as its name implies, just handles the DragGesture’s methods. We don’t want the gesture to work on the entire width of the screen, we want it to work only on the right edge of the screen that’s why we don’t change the state unless the startLocation is greater than the triggerLocation that we have declared above. We’ve also added some haptic feedback for better user experience.

Next, call the createMenuContent in the body block, above the VStack, so that it is like this:

 if authManager.isLoggedIn {
                createMenuContent()
                
                VStack {
					...

Then add the following modifiers to the VStack inside body:

.background(Color.background)
                    .clipped()
                    .shadow(color:  showMenu ? .darkShadow : .clear, radius: showMenu ? 6 : 0, x: 3, y: 0)
                    .offset(x:  -max(self.offset + self.translation, 0), y: 0)
                    .animation(.spring(), value: showMenu)
                    .gesture( handleGesture() )

And replace the 2 modifiers on the ZStack with the following:

.background(Color.background)
        .edgesIgnoringSafeArea(.all)

Other screens

In the Utils, add a file named Screens containing the following code:

enum Screens: String, CaseIterable {
    case Vault
    case Premium = "Go premium"
    case Security
    case Settings
}

Next, replace the Navbar call with the following:

NavBar(showMenu: self.$showMenu, title: selectedScreen.rawValue, showSearchField: selectedScreen == .Vault)

And replace the HomeView() call with the following:

if selectedScreen == .Vault{
      HomeView().transition(.opacity)
  }
  
  if selectedScreen == .Premium{
      GoPremiumView()
  }
  
  if selectedScreen == .Security{
      SecurityView()
  }
  
  if selectedScreen == .Settings{
      SettingsView()
  }

We still don’t have those 3 Views. Let’s create them now… In the Screens folder, create 3 files named GoPremiumView, GoPremiumView and SettingsView. These screens do not matter as they will only contain static placeholder to better illustrate the navigation drawer's functionality. With that in mind, you can put whatever you like in these files as long as what you put in them are swiftUI Views.

GoPremiumView

Replace everything in that file with the following:

import SwiftUI

struct GoPremiumView: View {
    var body: some View {
        ScrollView{
            VStack(spacing: 20) {
                HStack {
                    Text("SinglePass").font(.title)
                    Text("Premium").font(.title).bold()
                }
                
                Text("Take your productivity to the next lavel")
                
                LCButton(text: "Go Premium", backgroundColor: .accent) { }
                
                Text("It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English.")
            }.padding()
                .padding(.vertical)
        }
    }
}

struct GoPremiumView_Previews: PreviewProvider {
    static var previews: some View {
        GoPremiumView()
    }
}

Like I've just said the content is just a placeholder, don't mind it.

SecurityView

Replace the content of this file with the following:

import SwiftUI

struct SecurityView: View {
    var body: some View {
        ScrollView{
            VStack(spacing: 0){
                           ForEach(0..<4, id: \.self) { i in
                               VStack {
                                   HStack {
                                       Text(SettingItems.allCases[i].rawValue).padding()
                                       Spacer()
                                       Image(systemName: "chevron.right")
                                   }
                                   if i < 3{
                                       Rectangle().frame(height: 0.5).foregroundColor(Color.gray)
                                   }
                               }.padding(.horizontal)
                               
                           }
                       }.frame(maxWidth: .infinity)
                       .background(Color.background).cornerRadius(20)
                           .padding(10)
                  .padding(.vertical)
                           .modifier(NeumorphicEffect())
        }
    }
}

struct SecurityView_Previews: PreviewProvider {
    static var previews: some View {
        SecurityView()
    }
}

SettingsView

Last, replace the content of that file with the following code :

import SwiftUI

struct SettingsView: View {
    
    var body: some View {
        ScrollView{
            VStack(spacing: 0){
                ForEach(0..<6, id: \.self) { i in
                    VStack {
                        HStack {
                            Text(SettingItems.allCases[i].rawValue).padding()
                            Spacer()
                            Image(systemName: "chevron.right")
                        }
                        if i < 5{
                            Rectangle().frame(height: 0.5).foregroundColor(Color.gray)
                        }
                    }.padding(.horizontal)
                    
                }
            }.frame(maxWidth: .infinity)
            .background(Color.background).cornerRadius(20)
                .padding(10)
                .modifier(NeumorphicEffect())
            
            VStack(spacing: 0){
                ForEach(6..<8, id: \.self) { i in
                    VStack {
                        HStack {
                            Text(SettingItems.allCases[i].rawValue).padding()
                            Spacer()
                            Image(systemName: "chevron.right")
                        }
                        if i < 7{
                            Rectangle().frame(height: 0.5).foregroundColor(Color.gray)
                        }
                    }.padding(.horizontal)
                    
                }
            }.frame(maxWidth: .infinity)
            .background(Color.background).cornerRadius(20)
                .padding(10)
                .modifier(NeumorphicEffect())
            
            VStack {
                HStack {
                    Text(SettingItems.PrivacyPolicy.rawValue).foregroundColor(Color.accent).padding()
                    Spacer()
                    Image(systemName: "doc.on.doc.fill").foregroundColor(Color.accent)
                }
            }.padding(.horizontal)
            .frame(maxWidth: .infinity)
            .background(Color.background).cornerRadius(20)
                .padding(10)
                .modifier(NeumorphicEffect())
            
            VStack {
                HStack {
                    Text(SettingItems.LogOut.rawValue).foregroundColor(Color.red).padding()
                    Spacer()
                    Image(systemName: "exclamationmark.triangle.fill").foregroundColor(Color.orange)
                }
            }.padding(.horizontal)
                 
            .frame(maxWidth: .infinity)
            .background(Color.background).cornerRadius(20)
                .padding(10)
                
                .modifier(NeumorphicEffect())
        }
        .onAppear {
            UITableView.appearance().backgroundColor = UIColor(named: "bg")
        }
    }
}

struct SettingsView_Previews: PreviewProvider {
    static var previews: some View {
        SettingsView()
    }
}

I totally agree that this is over-kill for some placeholder content, but I didn't want to ruin the design by making some part ugly and others pretty.

Few more things

Open the NavBar file, find the code that renders the navigation drawer icon and replace it with the following:

Button(action: {
      self.showMenu.toggle()
  }) {

  Image(systemName: "line.horizontal.3")
      .resizable()
      .frame(width: 20, height: 20)
      .imageScale(.large)
      .foregroundColor(Color.white)
      .padding(.trailing)
}

What we've done here was re-used the existing code to create that button. The button will be used to open or close the navigation drawer.

Next, in the EditFormView, find the createBodyContent method and add the following line to the VStack container:

.offset(x: 0, y: self.formOffsetY)

The above code will be used handle moving the form up when in edit mode.

The last thing we need to do is to go in the AuthenticationManager and change the isLoggedIn property to false like this:

@Published var isLoggedIn = false

Final Conclusion

This is it folks, the last post for this series...Hope you enjoyed reading this as much as I did preparing, coding and writing it. With the release of Swift UI 2.0 , i will go through this app, update what need to be updated and write an article about the process, so make sure to stay tuned, subscribe and please share this whole series to whoever needs to start creating apps with swift UI.

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