LastPass re-design clone part 1

lastpass redesign Feb 16, 2020

Part 1: Login & SignUp User interface

This is part one of many in which we are going to build a re-designed clone of the LastPass app. If you don’t know what LastPass is, allow me to tell you about it.

LastPass is a password management service that helps in safely storing passwords and other sensitive pieces of information. You can check it out here.

So in this series, we are going to clone some functionalities of their iOS app, not the entire app. I am thinking of creating a full-blown swiftUI app in a paid course, what do you think? If you agree, send me an email or share this tutorial.

Preparations

As always, I have sent  you the source code if you are subscribed, otherwise, click here to get  it. The folder will contain a starter project and the finished project for this part 1 tutorial.  Also, if I release a paid course, all subscribers will get an amazing early discount.

Starter project

The starter project contains the boilerplate code that comes with a new Xcode project with the addition of assets that we will use throughout this series. The ColorExt file contains the Color extension to make our lives a little bit easier.

Neumorphism: Soft Design

Neumorphism is a modern UI design trend that's a new take on Skeuomorphism. The app will be built using this emerging design language which in my opinion looks stunning.

In the root folder, create another folder named Modifiers containing a swift file named NeumorphicEffect.swift, and put the following code inside:

import SwiftUI

struct NeumorphicEffect: ViewModifier {
    func body(content: Content) -> some View {
        content
            .shadow(color: Color.darkShadow , radius: 10, x: 9, y: 9)
            .shadow(color: Color.lightShadow, radius: 10, x: -9, y: -9)
            
    }
}

@available(iOS 13, macCatalyst 13, tvOS 13, watchOS 6, *)
extension View {
    func neumorphic() -> some View {
        return self.modifier(NeumorphicEffect())
    }
}


That’s the code that will apply the amazing neumorphic design to all views that have the neumorphic modifier. Pretty simple!

Now, let’s start creating swiftUI views.

AuthenticationView

The AuthenticationView will always appear when the user opens the app or when the app switches from background to foreground. The user will only be able to use the app when he authenticates successfully using a combination of a username and a password or touch ID or FaceID depending on the device.

Now, create a folder named Screens containing a swiftUI file named AuthenticationView.swift with the following code inside:

import SwiftUI

struct AuthenticationView: View {
    
    @State private var showAccountCreationView = false
    
    var body: some View {
        VStack {
            if showAccountCreationView {
                AccountCreationView(showLogin: self.$showAccountCreationView)
                    .transition(.asymmetric(insertion: .move(edge: .leading), removal: .move(edge: .trailing)))
            } else {
                LoginView(showCreateAccount: self.$showAccountCreationView)
                    .transition(.asymmetric(insertion: .move(edge: .trailing), removal: .move(edge: .leading)))
            }
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(Color.background)
         .edgesIgnoringSafeArea(.all)
    }
}

struct AuthenticationView_Previews: PreviewProvider {
    static var previews: some View {
        AuthenticationView()
    }
}

We are missing a couple of views hence the compiler error, but we will fix that shortly. What the above code does is it will initially show the login view, and if the user does not have an account registered with the device, he will switch to the account creation view by changing the showAccountCreationView state. We also apply some transition for when each view appears.

Next up, before creating the 2 views, we first need to create some custom textfields that they will utilise.

SharedTextfield

Create a Views folder containing a swiftUI file named SharedTextfield.swift, inside it put the following code:


import SwiftUI

struct SharedTextfield: View {
    @Binding var value: String
     var header = "Username"
     var placeholder = "Your username or email"
     var trailingIconName = ""
     var errorMessage = ""
     var showUnderline = true
     var onEditingChanged: ((Bool)->()) = {_ in }
     var onCommit: (()->()) = {}
    
    var body: some View {
        VStack(alignment: .leading, spacing: 0) {
            Text(header.uppercased()).font(.footnote).foregroundColor(Color.gray)
            HStack {
                TextField(placeholder, text: self.$value, onEditingChanged: { flag in
                    self.onEditingChanged(flag)
                }, onCommit: {
                    self.onCommit()
                }).padding(.vertical, 15)
                
                if !self.trailingIconName.isEmpty{
                    Image(systemName: self.trailingIconName ).foregroundColor(Color.gray)
                }
            }
            .frame(height: 45)
            if showUnderline{
                Rectangle().frame(height: 1).foregroundColor(Color.gray)
            }
            
            if !errorMessage.isEmpty {
                Text(errorMessage)
                    .lineLimit(nil)
                    .font(.footnote)
                    .foregroundColor(Color.red)
                    .transition(AnyTransition.opacity.animation(.easeIn))
            }
        }.background(Color.background)
    }
}

struct SharedTextfield_Previews: PreviewProvider {
    static var previews: some View {
        SharedTextfield(value: .constant(""))
    }
}

The above code does nothing special, it just stacks 3 views vertically to decorate the textfield. We will also show the error message if there is one. We will create validations using combine in the second part.

PasswordField

In the same folder, create a new swiftUI file named PasswordField.swift, and put the following code inside:


import SwiftUI

struct PasswordField: View {
        @Binding var value: String
        var header = "Username"
        var placeholder = "Your password"
        var errorMessage = ""
        var trailingIconName = ""
        var showUnderline = true
        var onEditingChanged: ((Bool)->()) = {_ in }
        var onCommit: (()->()) = {}
        @State var isSecure: Bool = true
        let pasteboard = UIPasteboard.general
       
       var body: some View {
        
        VStack(alignment: .leading, spacing: 0) {
            Text(header.uppercased()).font(.footnote).foregroundColor(Color.gray)
            HStack {
                ZStack{
                    SecureField(placeholder, text: self.$value, onCommit: {
                        self.onEditingChanged(false)
                    }).padding(.vertical, 15).opacity(isSecure ? 1 : 0)
                    
                    TextField(placeholder, text: self.$value, onEditingChanged: { flag in
                        self.onEditingChanged(flag)
                    }).padding(.vertical, 15).opacity(isSecure ? 0 : 1)
                }
                
                
                HStack {
                    if isSecure{
                        Image(systemName: "eye.slash").foregroundColor(Color.gray).onTapGesture {
                            withAnimation {
                                self.isSecure.toggle()
                            }
                        }
                    } else {
                        Image(systemName: "eye").foregroundColor(Color.gray).onTapGesture {
                           withAnimation {
                                self.isSecure.toggle()
                            }
                        }
                    }
                    if !isSecure{
                        Image(systemName: self.trailingIconName )
                            .foregroundColor(Color.gray)
                            .transition(.opacity)
                            .onTapGesture {
                                self.pasteboard.string = self.value
                        }
                    }
                }
            }.frame(height: 45)
            Rectangle().frame(height: 1).foregroundColor(Color.gray)
            if !errorMessage.isEmpty{
                Text(errorMessage)
                    .lineLimit(nil)
                    .font(.footnote)
                    .foregroundColor(Color.red)
                    .transition(AnyTransition.opacity.animation(.easeIn))
            }
            
        }.background(Color.background)
    }
}

struct PasswordField_Previews: PreviewProvider {
    static var previews: some View {
        PasswordField(value: .constant(""))
    }
}


The above code does almost the same thing as the SharedTextfield with some minor additions:

  • We stack a SecureField and a normal TextField on top of each other so that when one presses the eye icon, the field will toggle between showing and hiding the content of the password.

LCButton

In the folder, add a swiftUI file named LCButton.swift containing the following code:


import SwiftUI

struct LCButton: View {
    var text = ""
    var backgroundColor = Color.black
    var action = {}
    
    var body: some View {
        Button(action: {
            HapticFeedback.generate()
            self.action()
        }) {
            HStack {
                Text(text)
                    .font(.system(size: 20, weight: Font.Weight.semibold))
                    .frame(minWidth: 0, maxWidth: .infinity)
                    .padding(.vertical)
                    .accentColor(Color.white)
                    .background(backgroundColor.opacity(0.9))
                    .cornerRadius(20)
            }
        }
    }
}

struct LCButton_Previews: PreviewProvider {
    static var previews: some View {
        LCButton()
    }
}

This is just a shared view that we will reuse in several places in this project. We also generate a light haptic feedback when one clicks the button.

Create a folder named Shared, and add a swift file inside it named HapticFeedback.swift containing the following code:

import SwiftUI

struct HapticFeedback {
    public static func generate(){
        let generator = UIImpactFeedbackGenerator(style: .light)
        generator.prepare()
        generator.impactOccurred()
    }
}

Here we are just generating a simple light feedback, but you can change the style to your liking. To test this, you will need to run the app on a real device

Now that we have our view components in place, it’s time to create the login and account creation views.

Login View

In the views folder, add a swiftUI file named LoginView.swift. Put the following properties at the top of the file:

    @Binding var showCreateAccount: Bool
    @State private var email = ""
    @State private var password = ""
    @State private var formOffset: CGFloat = 0

The email and password will change when we implement form validation with the combine framework, but for now those will do the job.

Next, add the following method to create the content of the form.

       fileprivate func createContent() -> some View {
        VStack {
            Image("singlePass-dynamic").resizable().aspectRatio(contentMode: .fit) .frame(height: 30)
            .padding(.bottom)
           
            VStack(spacing: 30) {
                Text("Login").font(.title).bold()
                VStack(spacing: 30) {
                    SharedTextfield(value: self.$email, header: "Email" , placeholder: "Your email",errorMessage: "")
                    PasswordField(value: self.$password, header: "Master Password", placeholder: "Make sure the password is strong", errorMessage: "" , isSecure: true)
                    
                    LCButton(text: "Login", backgroundColor: Color.accent ) { }
                    
                    Button(action: {
                
                    }) {
                        VStack {
                            Image(systemName: "faceid")
                            .resizable()
                                .aspectRatio(contentMode: .fit)
                            .frame(width: 40, height: 40)
                                .foregroundColor(Color.accent)
                            Text("Use face ID").foregroundColor(.accent)
                        }
                    }
                }
                
                
            }.modifier(FormModifier()).offset(y: self.formOffset)
            createAccountButton()
        }
    }

You will get a bunch of errors caused by the missing FormModifier and the createAccountButton() method. To create the FormModifier, add a swift file named, you guessed it, FormModifier.swift containing the following code:

import SwiftUI

struct FormModifier: ViewModifier {
        
     func body(content: Content) -> some View {
        content.padding()
                 .background(Color.background)
                             .cornerRadius(10)
                             .padding()
                             .neumorphic()
               
       }
}


The above code just creates a typical swift UI layout, hopefully you can understand easily, if you don’t, feel free to send me an email.

Next, add the following method below the one you’ve just added:

 fileprivate func createAccountButton() -> some View {
        return Button(action: {
            withAnimation(.spring()) {
                self.showCreateAccount.toggle()
            }
        }) {
            HStack {
                Image(systemName: "arrow.left.square.fill")
                .resizable()
                    .aspectRatio(contentMode: .fit)
                .frame(height: 20)
                    .foregroundColor(Color.darkerAccent)
                Text("Create account")
                    .accentColor(Color.darkerAccent)
            }
        }
    }

Right now the body still contains the hello world text. Before we put the login form content we’ve just created inside, let’s first write the code that will handle the keyboard management.

In the Extensions folder, add a swift file named NotificationCenterExt.swift containing the following code:


import UIKit

extension NotificationCenter {
    private static let keyboardWillShow = NotificationCenter.Publisher(center: .default, name: UIResponder.keyboardWillShowNotification)
    private static let keyboardWillHide = NotificationCenter.Publisher(center: .default, name: UIResponder.keyboardWillHideNotification)
    
    static var keyboardPublisher = NotificationCenter.keyboardWillShow.merge(with: NotificationCenter.keyboardWillHide.map { Notification(name: $0.name, object: $0.object, userInfo: nil) })
        .map { ($0.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect ?? .zero)}
}

The above code create 2 notifications that will listen for when the keyboard shows or hides. We then merge them together using combine’s merge operator to retrieve the keyboard’s frame. So when we subscribe to this publisher, we will get back the frame that we will utilise to adjust the content hidden by the keyboard.

There are several ways one can subscribe to a publisher in swift UI using combine, but we will use a special view that comes with swiftUI called SubscriptionView. This view was made to handle publishers, its constructor takes in the content, a publisher and spills out the output of the publisher. Here is its signature:

@inlinable public init(content: Content, publisher: PublisherType, action: @escaping (PublisherType.Output) -> Void)

Let’s now implement the body, shall we?

Replace the content of the body with the following:

SubscriptionView(content: createContent(), publisher: NotificationCenter.keyboardPublisher) { frame in
            withAnimation {
                self.formOffset = frame.height > 0 ? -200 : 0
            }
        }

In the code above, we return a SubscriptionView passing in the content we created earlier and the keyboard publisher. We get the keyboard’s frame that we use to change the formOffset.

Account creation view

In the Views folder, create a file named AccountCreationView.swift containing the following code:


import SwiftUI

struct AccountCreationView: View {
    
    @Binding var showLogin: Bool
    @State private var email = ""
    @State private var password = ""
    @State private var confirmedPassword = ""
    @State private var formOffset: CGFloat = 0
    

    fileprivate func goToLoginButton() -> some View {
        return Button(action: {
            withAnimation(.spring() ) {
                self.showLogin.toggle()
            }
        }) {
            HStack {
                Text("Login")
                    .accentColor(Color.darkerAccent)
                Image(systemName: "arrow.right.square.fill")
                    .resizable()
                    .aspectRatio(contentMode: .fit)
                    .frame(height: 20)
                    .foregroundColor(Color.darkerAccent)
                    
            }
        }
    }
    
    fileprivate func createContent() -> some View{
        VStack {
                Image("singlePass-dynamic").resizable().aspectRatio(contentMode: .fit) .frame(height: 30)
                    .padding(.bottom)
                VStack(spacing: 10) {
                    Text("Create Account").font(.title).bold()
                    VStack(spacing: 30) {
                        SharedTextfield(value: self.$email,header: "Email", placeholder: "Your primary email",errorMessage: "")
                        PasswordField(value: self.$password,header: "Password",  placeholder: "Make sure it's string",errorMessage: "", isSecure: true)
                        PasswordField(value: self.$confirmedPassword,header: "Confirm Password",  placeholder: "Must match the password", errorMessage: "", isSecure: true)
                        
                    }
                    LCButton(text: "Sign up", backgroundColor: Color.accent ) {}
                    
                }.modifier(FormModifier()).offset(y: self.formOffset)
            
            goToLoginButton()
        }
    }

    var body: some View {
        
        SubscriptionView(content: createContent(), publisher: NotificationCenter.keyboardPublisher) { frame in
            withAnimation {
                self.formOffset = frame.height > 0 ? -200 : 0
            }
        }
    }
}

struct SignupView_Previews: PreviewProvider {
    static var previews: some View {
        AccountCreationView(showLogin: .constant(false))
    }
}

The above code is the same as what I’ve just explained. We’ve only added an extra textfield for the password confirmation and removed the Touch ID or Face ID button.

The last thing to do is to call the AuthenticationView() in the ContentView’s body. Your ContentView’s body should look like this:

 var body: some View {
    AuthenticationView()
 }

After that, run the app. Here is the final result:

As already, feel free to share this tutorial and happy coding!!! Get the source code here

Creating this type of project is tiring and time-consuming. I spent days planning, designing, struggling and coding, that’s why I need your support to keep making this type of project for free. So if you want to support me, you can do so by being my Patreon.

Become a Patron!

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