Skip to content

Instantly share code, notes, and snippets.

@stammy
Created January 4, 2022 02:54
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save stammy/c278c05c26a02257c195731bb420754a to your computer and use it in GitHub Desktop.
Save stammy/c278c05c26a02257c195731bb420754a to your computer and use it in GitHub Desktop.
Better Password Field
//
// ContentView.swift
// BetterPasswordField
//
// Created by Paul Stamatiou on 1/3/22.
// Idea from https://twitter.com/Stammy/status/1478108590066089987
//
import SwiftUI
enum FocusableField: Hashable {
case password
}
struct ContentView: View {
@State var countdown: Int = 5
@State var showPassword: Bool = false
@State var password: String = ""
@FocusState private var focus: FocusableField?
var timer = Timer.publish(every: 0.45, on: .main, in: .common).autoconnect()
var focusedBG: Color {
if focus == .password {
return Color.black.opacity(0.08)
} else {
return Color.clear
}
}
var pwdView: some View {
HStack(spacing: 0) {
ForEach(0..<matchedStr.count, id: \.self) { _ in
Text("")
.font(.system(size: 22, weight: .medium).monospaced())
}
}
}
var matchedStr: String {
let limit: Int = countdown > 5 ? 5 : countdown
let charCount: Int = password.count < (limit + 1) ? 0 : password.count - limit
return String(password.prefix(charCount))
}
var body: some View {
VStack(alignment: .center, spacing: 8) {
HStack(spacing: 5) {
ZStack {
pwdView
.frame(width: 240, height: 38, alignment: .leading)
.opacity(showPassword ? 0 : 1)
TextField("Password", text: $password)
.focused($focus, equals: .password)
.textInputAutocapitalization(.none)
.disableAutocorrection(true)
.font(.system(size: 22, weight: .medium).monospaced())
.frame(width: 240, height: 38, alignment: .leading)
.foregroundColor(showPassword ? Color.black : password.count > 0 ? Color.clear : Color.black)
Text(password) { string in
string.foregroundColor = Color.black//.opacity(0.1)
string.font = .system(size: 22, weight: .medium).monospaced()
// what you hide
if let range = string.range(of: matchedStr) {
string[range].foregroundColor = .clear
}
}
.frame(width: 240, height: 38, alignment: .leading)
.opacity(showPassword ? 0 : 1)
.onReceive(timer) { _ in
if (countdown > 0) {
countdown -= 1
}
}
.onChange(of: password) { _ in
// for every new char typed, reset timer logic
countdown = 6
}
}
Button(action: { showPassword.toggle() }) {
Image(systemName: showPassword ? "eye" : "eye.slash")
.resizable()
.font(.system(size: 16, weight: .medium))
.aspectRatio(contentMode: .fit)
.frame(width: 24, height: 16)
.foregroundColor(Color.black.opacity(0.5))
.padding(.horizontal, 6)
}
.buttonStyle(Scaler())
.contentShape(Rectangle())
.opacity(password.count > 0 ? 1 : 0)
}
.padding(.vertical, 12)
.padding(.horizontal, 16)
.background(focusedBG)
.clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
.padding(.horizontal, 24)
.onTapGesture {
focus = .password
}
.task {
// focus on load.. added delay required or it won't focus
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
focus = .password
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
.edgesIgnoringSafeArea(.all)
.statusBar(hidden: true)
}
}
struct Scaler: ButtonStyle {
func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.scaleEffect(configuration.isPressed ? 0.90 : 1.0)
.animation(Animation.spring(response: 0.45, dampingFraction: 0.6, blendDuration: 0), value: configuration.isPressed)
}
}
extension Text {
init(_ string: String, configure: ((inout AttributedString) -> Void)) {
var attributedString = AttributedString(string)
configure(&attributedString)
self.init(attributedString)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment