LastPass clone - part 8

lastpass redesign May 04, 2020

Hey guys, in the previous part, we created the user interface for the home view. In this one, we will make the EditFormView. As its name implies, the EditFormView will be used to edit a password or a note as well as view their respective details.

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.

DetailHeader

For simplicity, we will have 2 types of information to keep safe, but the real LastPass service has more than 2 therefore a user will only be able to save passwords and notes. You could also extend the app by yourself to include credit cards information, Identification number and so on…

To track the type of info, we need some kind of model, so let’s use an enum for that. In the Utils folder, add a file named FormType.swift with the following code inside:

enum FormType: String{
    case Password
    case Note
}

With that in place, add another file named FormHeader.swift in the Views folder.

Now add the following to the top of struct:

@Binding var showDetails: Bool
@Binding var formType: FormType
    

Then replace the content of the body with the following:

let imageName = "https://www.amazon.com".getImageName()

return HStack {
                Button(action: {
                    HapticFeedback.generate()
                    withAnimation(.easeInOut) {
                        self.showDetails.toggle()
                    }
                }) {
                    Image(systemName: "chevron.left")
                        .imageScale(.large)
                        .foregroundColor(Color.text)
                }
                
                Picker("", selection:  self.$formType) {
                    Text(FormType.Password.rawValue).tag(FormType.Password)
                    Text(FormType.Note.rawValue).tag(FormType.Note)
                }.pickerStyle(SegmentedPickerStyle())
                    .background(Color.background)
                    .padding()
                
                Image(imageName)
                    .resizable()
                    .aspectRatio(contentMode: .fit)
                    .frame(width: 40, height: 40)
                    .foregroundColor(Color.gray)
                
            }.padding()
				.padding(.top)
                .frame(maxWidth: .infinity)
                .frame(height: 100)
                .background(Color.background)
                .shadow(color: Color.darkShadow, radius: 5, x: 0, y: 5)

Here is what we are doing above:

  1. In the Assets.xcassets I put are a bunch of icons for most of the major websites. I have created a helper extension method to retrieve the name of the asset from a website’s URL that I will show you shortly. For now, we are hard-coding the URL, but later on, this will be dynamic.
  2. We create a button that will be used to dismiss the details view. We also generate some haptic feedback when one clicks on the button. If you are not aware, we created the HapticFeedback helper struct in one of the previous parts.
  3. We then create a Picker that, in our case, has only 2 options I talked about earlier.
  4. We then create an image that will display which website we are viewing or editing.
  5. And last we add a bunch of modifiers to style HStack container itself.

Let’s now create the getImageName extension method. In the Extensions folder, add a file named StringExt.swift, and put the following code inside:

    func getImageName() -> String {
         let urlComponents = self.lowercased().split(separator: ".")
         let count = urlComponents.count

         if urlComponents.isEmpty{
             return "placeholder"
         }
         return count > 2 ? String(urlComponents[count - 2]) : String(urlComponents.first ?? "")
     }
}

Here is what we are doing above:

  1. We need to convert the string to lowercase since all asset names are in lowercase, and then split the string based on dots.
  2. The split method will return an array of strings, so we need to check whether the array is empty and return a placeholder if it’s the case, otherwise we return the name of the website. This method will only work for urls with the following form :
    https://hostname.domain.com or https://domain.com
    Any other case might fail, and return the placeholder icon name.

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

Edit Form View

Now is the time to create the edit form. Contrary to its name, this form will not only be used to edit, but also to view and add new information. So, create a new swiftUI file in the Views folder named EditFormView.swift.

Now, add the following methods below body:

   fileprivate func createBodyContent() -> some View {
        EmptyView()
    }
    
    fileprivate func createNoteForm() -> some View{
         EmptyView()
    }
      
      fileprivate func createPasswordForm()-> some View{
        EmptyView()
      }

We will work on those 3 methods shortly, but first replace the text in body with the following:

 SubscriptionView(content:  createBodyContent(), publisher: NotificationCenter.keyboardPublisher) { rect in
                   withAnimation {
                       self.formOffsetY = rect.height > 0 ? -150 : 0
                   }
      }

Here is a quick explanation of what we’ve just done above:

  1. We use the SubscriptionView to subscribe to the keyboardPublisher events relayed by the keyboard’s notification.
  2. We then set the offset accordingly. When the keyboard pops up, the rect.height will be greater than 0, hence the need to move the content a little bit higher.

We have an compiler error, add the following property to the top to get rid of it:

   @State private var formOffsetY: CGFloat = 0

Password & Note Form

Before creating the password, let's first create some properties to the top that will hold our states.

Add this above formOffsetY:

@Binding var showDetails: Bool
@State private var username = ""
    @State private var password = ""
    @State private var website = ""
    @State private var passwordNote = ""
    @State private var noteName = ""
    @State private var noteContent = ""
    @State private var favoriteImage = "heart"

    
    @State private var formType = FormType.Password

Next, replace the EmptyView in the createPasswordForm with the following:

 VStack(spacing: 40) {
                   SharedTextfield(value: self.$username)
                   VStack(alignment: .leading) {
                       PasswordField(value: self.$password, header: "Password",placeholder: "Make sure the password is secure")
                       Button(action: {
                       }) {
                           Text("Generate password")
                               .foregroundColor(.accent)
                       }
                   }
                   SharedTextfield(value: self.$website, header: "Website", placeholder: "https://")
                   SharedTextfield(value: self.$passwordNote, header: "Note",placeholder: "You can write anything here...", showUnderline: false)
               }.padding()
      }

This will need to be refactored later on in a separate view. The above code does anything special, it just stacks views vertically. We created SharedTextfield in one of the previous parts, in case you are wondering where it’s coming from.

Next, replace the EmptyView in the createNoteForm with the following:

VStack(spacing: 40) {
            SharedTextfield(value: self.$noteName, header: "Title",placeholder: "Your note title goes here")
            SharedTextfield(value: self.$noteContent, header: "Note",placeholder: "You can write anything here...", showUnderline: false)
            
        }.padding()

Here we only need 2 fields for the note, the note’s name or title and content. So we simply stack them vertically.

Let’s now put the 2 together in the createBodyContent method. Now replace the EmptyView in the aforementioned method with the following:

 VStack {
            FormHeader(showDetails: self.$showDetails, formType: self.$formType)
            ScrollView {
                VStack {
                    if formType == FormType.Password{
                        createPasswordForm()
                    }
                    else {
                        createNoteForm()
                    }
                    
                }
            }
        }

Here is what we are doing here:

  1. We stack the FormHeader and the password or note form vertically
  2. We defined a formType state above that we use here to check which type of form we are working with, and display it appropriately.

We are still missing a couple of details before we finish this part.

Now, add the following inside the inner VStack containing the forms, and below those forms:

 HStack(spacing: 20) {
                    createSaveButton()
                    createHeartButton()
                }.padding(.horizontal)

We still don’t have those 2 methods, hence the errors. Add them to silence those errors:

  fileprivate func createSaveButton() -> LCButton {
           return LCButton(text: "Save", backgroundColor: Color.accent) {
               switch self.formType{
               case .Password:
                   break
               case .Note:
                   break
               }
           }
       }
       
    fileprivate func createHeartButton() -> some View {
        
        Button(action: {
            
        }) {
            Image(systemName: self.favoriteImage)
                .resizable()
                .aspectRatio(contentMode: .fit)
                .frame(width: 30)
                .foregroundColor( favoriteImage == "heart" ? Color.gray : Color.orange)
        }
    }

Those two methods create nothing else but two buttons that don’t do anything yet, we will implement their actions in a later article.

Last, but not least we need to add a bunch of modifiers to style whole body to make it look presentable. So, in the createBodyContent function, add the following modifiers to the outer VStack container :

.background(Color.background)
            .cornerRadius(20)
            .padding()
			.padding(.top, 40)
            .neumorphic()

Here is what we have now:

It’s not pretty yet, but things start to come together. With this, we finish this part, stay tuned for the next one. If you have any question, send me an email. Don’t forget to subscribe if you haven’t done so already, and share this article. 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]der.com or https://twitter.com/liquidcoder