Last active
May 4, 2023 20:28
-
-
Save st235/2620413193080866e90ae2c0c1f225d7 to your computer and use it in GitHub Desktop.
PinView for iOS
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
extension NSAttributedString { | |
func height(containerWidth: CGFloat) -> CGFloat { | |
let rect = self.boundingRect(with: CGSize.init(width: containerWidth, height: CGFloat.greatestFiniteMagnitude), | |
options: [.usesLineFragmentOrigin, .usesFontLeading], | |
context: nil) | |
return ceil(rect.size.height) | |
} | |
func width(containerHeight: CGFloat) -> CGFloat { | |
let rect = self.boundingRect(with: CGSize.init(width: CGFloat.greatestFiniteMagnitude, height: containerHeight), | |
options: [.usesLineFragmentOrigin, .usesFontLeading], | |
context: nil) | |
return ceil(rect.size.width) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
import UIKit | |
@IBDesignable class PinView: UIControl, UIKeyInput { | |
@IBInspectable var digitCellsSelectionColor: UIColor = UIColor.black { | |
didSet { | |
setNeedsDisplay() | |
} | |
} | |
@IBInspectable var digitCellsSelectionStroke: UIColor = UIColor.clear { | |
didSet { | |
setNeedsDisplay() | |
} | |
} | |
@IBInspectable var digitCellsFillColor: UIColor = UIColor.black { | |
didSet { | |
setNeedsDisplay() | |
} | |
} | |
@IBInspectable var digitsCellStrokeColor: UIColor = UIColor.clear { | |
didSet { | |
setNeedsDisplay() | |
} | |
} | |
@IBInspectable var textColor: UIColor = UIColor.white { | |
didSet { | |
setNeedsDisplay() | |
} | |
} | |
@IBInspectable var font: UIFont = UIFont.systemFont(ofSize: 32.0).bold() { | |
didSet { | |
setNeedsDisplay() | |
} | |
} | |
@IBInspectable var hideDigits: Bool = false { | |
didSet { | |
setNeedsDisplay() | |
} | |
} | |
@IBInspectable var digitsCount: Int = 5 { | |
didSet { | |
setNeedsDisplay() | |
} | |
} | |
@IBInspectable var digitsCellsInset: Float = 10 { | |
didSet { | |
setNeedsDisplay() | |
} | |
} | |
@IBInspectable var digitsCellsStrokeWidth: CGFloat = 0 { | |
didSet { | |
setNeedsDisplay() | |
} | |
} | |
private var index = 0 | |
private var digits: [String?] = [] | |
override var canBecomeFirstResponder: Bool { | |
true | |
} | |
override init(frame: CGRect) { | |
super.init(frame: frame) | |
self.digits = [String?](repeating: nil, count: digitsCount) | |
addTarget(self, action: #selector(onTap), for: .touchUpInside) | |
} | |
required init?(coder aDecoder: NSCoder) { | |
super.init(coder: aDecoder) | |
self.digits = [String?](repeating: nil, count: digitsCount) | |
addTarget(self, action: #selector(onTap), for: .touchUpInside) | |
} | |
@objc private func onTap(_: AnyObject) { | |
// When the view is tapped, it becomes the first responder. If the keyboard is not already on screen, it will appear. | |
becomeFirstResponder() | |
} | |
var hasText: Bool { | |
var empty = true | |
for digit in digits { | |
if digit != nil { | |
empty = false | |
break | |
} | |
} | |
return empty | |
} | |
func insertText(_ text: String) { | |
if index < digitsCount { | |
digits[index] = text | |
index = min(index + 1, digitsCount) | |
} | |
setNeedsDisplay() | |
} | |
func deleteBackward() { | |
index = max(index - 1, 0) | |
digits[index] = nil | |
setNeedsDisplay() | |
} | |
override func draw(_ rect: CGRect) { | |
guard let context = UIGraphicsGetCurrentContext() else { return } | |
let digitCellWidth = (bounds.width - CGFloat(digitsCellsInset) * CGFloat(digitsCount - 1)) / CGFloat(digitsCount) | |
let digitCellHeight = bounds.height | |
var currentX = CGFloat(0) | |
for i in 0..<digitsCount { | |
if i == index { | |
drawCell(withWidth: digitCellWidth, height: digitCellHeight, atX: currentX, y: 0, withRadius: 12, fillColor: digitCellsSelectionColor, strokeColor: digitCellsSelectionStroke, and: context) | |
} else { | |
drawCell(withWidth: digitCellWidth, height: digitCellHeight, atX: currentX, y: 0, withRadius: 12, fillColor: digitCellsFillColor, strokeColor: digitsCellStrokeColor, and: context) | |
} | |
if let digit = digits[i] { | |
if hideDigits { | |
drawHiddenCharacter(withWidth: digitCellWidth, height: digitCellHeight, atX: currentX, y: 0, and: context) | |
} else { | |
draw(text: digit, withWidth: digitCellWidth, height: digitCellHeight, atX: currentX, y: 0, and: context) | |
} | |
} | |
currentX += digitCellWidth + CGFloat(digitsCellsInset) | |
} | |
} | |
private func drawCell(withWidth width: CGFloat, height: CGFloat, atX x: CGFloat, y: CGFloat, withRadius radius: CGFloat, fillColor: UIColor, strokeColor: UIColor, and context: CGContext) { | |
context.saveGState() | |
let rect = CGRect(x: x, y: y, width: width, height: height) | |
let clipPath: CGPath = UIBezierPath(roundedRect: rect, cornerRadius: radius).cgPath | |
context.addPath(clipPath) | |
context.setFillColor(fillColor.cgColor) | |
context.setStrokeColor(strokeColor.cgColor) | |
context.setLineWidth(digitsCellsStrokeWidth) | |
context.drawPath(using: .fillStroke) | |
context.restoreGState() | |
} | |
private func draw(text text: String, withWidth width: CGFloat, height: CGFloat, atX x: CGFloat, y: CGFloat, and context: CGContext) { | |
let paragraphStyle = NSMutableParagraphStyle() | |
paragraphStyle.alignment = .center | |
let attributes: [NSAttributedString.Key : Any] = [ | |
.paragraphStyle: paragraphStyle, | |
.font: font, | |
.foregroundColor: textColor | |
] | |
let attributedString = NSAttributedString(string: text, attributes: attributes) | |
let stringRect = CGRect(x: x, y: y + height / 2 - attributedString.height(containerWidth: width) / 2, width: width, height: height) | |
attributedString.draw(in: stringRect) | |
} | |
private func drawHiddenCharacter(withWidth width: CGFloat, height: CGFloat, atX x: CGFloat, y: CGFloat, and context: CGContext) { | |
context.saveGState() | |
context.setFillColor(textColor.cgColor) | |
context.setStrokeColor(UIColor.clear.cgColor) | |
let radius = CGFloat(6) | |
context.addEllipse(in: CGRect(x: x + width / 2 - radius, y: y + height / 2 - radius, width: radius * 2, height: radius * 2)) | |
context.drawPath(using: .fillStroke) | |
context.restoreGState() | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import UIKit | |
extension UIFont { | |
func withTraits(_ traits: UIFontDescriptor.SymbolicTraits) -> UIFont { | |
// create a new font descriptor with the given traits | |
guard let fd = fontDescriptor.withSymbolicTraits(traits) else { | |
// the given traits couldn't be applied, return self | |
return self | |
} | |
// return a new font with the created font descriptor | |
return UIFont(descriptor: fd, size: pointSize) | |
} | |
func italics() -> UIFont { | |
return withTraits(.traitItalic) | |
} | |
func bold() -> UIFont { | |
return withTraits(.traitBold) | |
} | |
func boldItalics() -> UIFont { | |
return withTraits([ .traitBold, .traitItalic ]) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Screens