diff --git a/.DS_Store b/.DS_Store index e7c1f61..a381b3e 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/Demo/Assets.xcassets/Contents.json b/Demo/Assets.xcassets/Contents.json index da4a164..73c0059 100644 --- a/Demo/Assets.xcassets/Contents.json +++ b/Demo/Assets.xcassets/Contents.json @@ -1,6 +1,6 @@ { "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git "a/Demo/Assets.xcassets/\347\231\273\345\275\225-\345\272\225\345\233\276 2.imageset/Contents.json" "b/Demo/Assets.xcassets/\347\231\273\345\275\225-\345\272\225\345\233\276 2.imageset/Contents.json" new file mode 100644 index 0000000..cec24c6 --- /dev/null +++ "b/Demo/Assets.xcassets/\347\231\273\345\275\225-\345\272\225\345\233\276 2.imageset/Contents.json" @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "登录-底图 2@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "登录-底图 2@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git "a/Demo/Assets.xcassets/\347\231\273\345\275\225-\345\272\225\345\233\276 2.imageset/\347\231\273\345\275\225-\345\272\225\345\233\276 2@2x.png" "b/Demo/Assets.xcassets/\347\231\273\345\275\225-\345\272\225\345\233\276 2.imageset/\347\231\273\345\275\225-\345\272\225\345\233\276 2@2x.png" new file mode 100644 index 0000000..c1722d2 Binary files /dev/null and "b/Demo/Assets.xcassets/\347\231\273\345\275\225-\345\272\225\345\233\276 2.imageset/\347\231\273\345\275\225-\345\272\225\345\233\276 2@2x.png" differ diff --git "a/Demo/Assets.xcassets/\347\231\273\345\275\225-\345\272\225\345\233\276 2.imageset/\347\231\273\345\275\225-\345\272\225\345\233\276 2@3x.png" "b/Demo/Assets.xcassets/\347\231\273\345\275\225-\345\272\225\345\233\276 2.imageset/\347\231\273\345\275\225-\345\272\225\345\233\276 2@3x.png" new file mode 100644 index 0000000..67d6bc4 Binary files /dev/null and "b/Demo/Assets.xcassets/\347\231\273\345\275\225-\345\272\225\345\233\276 2.imageset/\347\231\273\345\275\225-\345\272\225\345\233\276 2@3x.png" differ diff --git a/Demo/Base.lproj/Main.storyboard b/Demo/Base.lproj/Main.storyboard index f1bcf38..2beaac3 100644 --- a/Demo/Base.lproj/Main.storyboard +++ b/Demo/Base.lproj/Main.storyboard @@ -1,7 +1,9 @@ - + + - + + @@ -9,16 +11,31 @@ - + - + - + + + + + + + + + + + + + + + + diff --git a/Demo/Info.plist b/Demo/Info.plist index 16be3b6..ef8475a 100644 --- a/Demo/Info.plist +++ b/Demo/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.0 + 2.0.0 CFBundleVersion 1 LSRequiresIPhoneOS diff --git a/Demo/ViewController.swift b/Demo/ViewController.swift index 1b68c73..f1383ac 100644 --- a/Demo/ViewController.swift +++ b/Demo/ViewController.swift @@ -14,7 +14,7 @@ class ViewController: UIViewController { // default item view let pinCodeInputView: PinCodeInputView = .init( digit: 6, - itemSpacing: 8, + itemSpacing: 10, itemFactory: { return ItemView() }, @@ -65,22 +65,24 @@ class ViewController: UIViewController { titleLabel.font = UIFont.systemFont(ofSize: 24, weight: .bold) titleLabel.textColor = UIColor.lightText titleLabel.frame = CGRect(x: 0, y: 0, width: view.bounds.width - 56, height: 60) - titleLabel.center = CGPoint(x: view.center.x, y: view.center.y - 94) + titleLabel.center = CGPoint(x: view.center.x, y: view.center.y - 124) pinCodeInputView.frame = CGRect(x: 0, y: 0, width: view.bounds.width - 56, height: 80) - pinCodeInputView.center = view.center + pinCodeInputView.center = CGPoint(x: view.center.x, y: view.center.y - 30) pinCodeInputView.set(changeTextHandler: { text in print(text) }) pinCodeInputView.set( appearance: .init( - itemSize: CGSize(width: 44, height: 68), - font: .systemFont(ofSize: 28, weight: .bold), - textColor: .white, - backgroundColor: UIColor.white.withAlphaComponent(0.3), + itemSize: CGSize(width: 48, height: 48), + font: .systemFont(ofSize: 20, weight: .bold), + textColor: .black, + backgroundColor: UIColor.white.withAlphaComponent(0.8), + highlightBackgroundColor: UIColor(red: 186/255.0, green: 212/255.0, blue: 255/255.0, alpha: 0.8), cursorColor: UIColor(red: 69/255, green: 108/255, blue: 1, alpha: 1), - cornerRadius: 8, - borderColor: UIColor.red + cornerRadius: 4, + highlightBorderColor: UIColor(red: 38/255.0, green: 86/255.0, blue: 235/255.0, alpha: 1), + borderColor:.white ) ) diff --git a/PinCodeInputView.podspec b/PinCodeInputView.podspec index e2c8e1a..6b17e7d 100644 --- a/PinCodeInputView.podspec +++ b/PinCodeInputView.podspec @@ -9,7 +9,7 @@ Pod::Spec.new do |s| s.name = "PinCodeInputView" - s.version = "1.0.0" + s.version = "2.0.0" s.summary = "TextView for entering pin code. " s.description = <<-DESC diff --git a/PinCodeInputView.xcodeproj/project.pbxproj b/PinCodeInputView.xcodeproj/project.pbxproj index 40b9c6b..217601d 100644 --- a/PinCodeInputView.xcodeproj/project.pbxproj +++ b/PinCodeInputView.xcodeproj/project.pbxproj @@ -502,7 +502,7 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = 64T4PJ8WCZ; + DEVELOPMENT_TEAM = 2286X5X7D9; INFOPLIST_FILE = Demo/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -521,7 +521,7 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = 64T4PJ8WCZ; + DEVELOPMENT_TEAM = 2286X5X7D9; INFOPLIST_FILE = Demo/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", diff --git a/PinCodeInputView/Info.plist b/PinCodeInputView/Info.plist index e1fe4cf..0aad175 100644 --- a/PinCodeInputView/Info.plist +++ b/PinCodeInputView/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 1.0 + 2.0.0 CFBundleVersion $(CURRENT_PROJECT_VERSION) diff --git a/PinCodeInputView/ItemAppearance.swift b/PinCodeInputView/ItemAppearance.swift index 28d9c41..5fd7d23 100644 --- a/PinCodeInputView/ItemAppearance.swift +++ b/PinCodeInputView/ItemAppearance.swift @@ -14,25 +14,32 @@ public struct ItemAppearance { public let font: UIFont public let textColor: UIColor public let backgroundColor: UIColor + public let highlightBackgroundColor: UIColor public let cursorColor: UIColor public let cornerRadius: CGFloat public let borderColor: UIColor + public let highlightBorderColor: UIColor + public init( itemSize: CGSize, font: UIFont, textColor: UIColor, backgroundColor: UIColor, + highlightBackgroundColor: UIColor, cursorColor: UIColor, cornerRadius: CGFloat, + highlightBorderColor: UIColor = UIColor.clear, borderColor: UIColor = UIColor.clear) { self.itemSize = itemSize self.font = font self.textColor = textColor self.backgroundColor = backgroundColor + self.highlightBackgroundColor = highlightBackgroundColor self.cursorColor = cursorColor self.cornerRadius = cornerRadius self.borderColor = borderColor + self.highlightBorderColor = highlightBorderColor } } diff --git a/PinCodeInputView/ItemView.swift b/PinCodeInputView/ItemView.swift index 21f5968..bfae4ba 100644 --- a/PinCodeInputView/ItemView.swift +++ b/PinCodeInputView/ItemView.swift @@ -27,8 +27,12 @@ public class ItemView: UIView, ItemType { didSet { cursor.isHidden = isHiddenCursor if (isHiddenCursor) { - self.layer.borderWidth = 0 + self.backgroundColor = _appearance?.backgroundColor + self.layer.borderColor = _appearance?.borderColor.cgColor + self.layer.borderWidth = 1 } else { + self.backgroundColor = _appearance?.highlightBackgroundColor + self.layer.borderColor = _appearance?.highlightBorderColor.cgColor self.layer.borderWidth = 1 } } @@ -50,27 +54,36 @@ public class ItemView: UIView, ItemType { label.isUserInteractionEnabled = false cursor.isHidden = true - - UIView.animateKeyframes( - withDuration: 1.6, - delay: 0.8, - options: [.repeat], - animations: { - UIView.addKeyframe( - withRelativeStartTime: 0, - relativeDuration: 0.2, - animations: { - self.cursor.alpha = 0 - }) - UIView.addKeyframe( - withRelativeStartTime: 0.8, - relativeDuration: 0.2, - animations: { - self.cursor.alpha = 1 - }) - }, - completion: nil - ) + NotificationCenter.default.addObserver(self, selector: #selector(becomeActive), name: UIApplication.didBecomeActiveNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(enterBack), name: UIApplication.didEnterBackgroundNotification, object: nil) + becomeActive() + + } + + /// 闪烁动画 + fileprivate var opacityAnimation: CABasicAnimation = { + let opacityAnimation = CABasicAnimation.init(keyPath: "opacity") + opacityAnimation.fromValue = 1.0 + opacityAnimation.toValue = 0.0 + opacityAnimation.duration = 0.9 + opacityAnimation.repeatCount = HUGE + opacityAnimation.isRemovedOnCompletion = true + opacityAnimation.fillMode = .forwards + opacityAnimation.timingFunction = CAMediaTimingFunction.init(name: .linear) + return opacityAnimation + }() + + + /// 去后台 + @objc fileprivate func enterBack() { + // 移除动画 + cursor.layer.removeAnimation(forKey: "kOpacityAnimation") + } + + /// 回前台 + @objc fileprivate func becomeActive() { + // 重新添加动画 + cursor.layer.add(opacityAnimation, forKey: "kOpacityAnimation") } required init?(coder aDecoder: NSCoder) { @@ -93,7 +106,10 @@ public class ItemView: UIView, ItemType { ) } + private var _appearance: ItemAppearance? + public func set(appearance: ItemAppearance) { + _appearance = appearance bounds.size = appearance.itemSize label.font = appearance.font label.textColor = appearance.textColor diff --git a/PinCodeInputView/PinCodeInputView.swift b/PinCodeInputView/PinCodeInputView.swift index 4f13cf3..3e365f9 100644 --- a/PinCodeInputView/PinCodeInputView.swift +++ b/PinCodeInputView/PinCodeInputView.swift @@ -9,10 +9,10 @@ import UIKit @IBDesignable -public class PinCodeInputView: UIControl, UITextInputTraits, UIKeyInput { - +public class PinCodeInputView: UIControl, UITextFieldDelegate { + // MARK: - Properties - + private(set) public var text: String = "" { didSet { if let handler = changeTextHandler { @@ -21,17 +21,28 @@ public class PinCodeInputView: UIControl, UITextInputTrait updateText() } } - + public var isEmpty: Bool { return text.isEmpty } - + + public var canPaste: Bool = true { + didSet { + inputTextField.isPasteEnabled = canPaste + } + } + public var isFilled: Bool { return text.count == digit } public var hasText: Bool { - return !(text.isEmpty) + return !text.isEmpty + } + + public var currentFocusIndex: Int { + if isFilled { return 0 } + return text.count } override public var intrinsicContentSize: CGSize { @@ -40,68 +51,184 @@ public class PinCodeInputView: UIControl, UITextInputTrait private let digit: Int private let itemSpacing: CGFloat - private var changeTextHandler: ((String) -> Void)? = nil + private var changeTextHandler: ((String) -> Void)? private let stackView: UIStackView = .init() + private let inputTextField: OTPTextField = .init(frame: .zero) private var items: [ContainerItemView] = [] private let itemFactory: () -> UIView private var appearance: ItemAppearance? - private let autoResizes: Bool + private let autoResizes: Bool + private var preferredTextContentType: UITextContentType? + + // MARK: - UITextInputTraits-compatible properties + + public var autocapitalizationType: UITextAutocapitalizationType = .none { + didSet { inputTextField.autocapitalizationType = autocapitalizationType } + } + + public var autocorrectionType: UITextAutocorrectionType = .no { + didSet { inputTextField.autocorrectionType = autocorrectionType } + } - // MARK: - UITextInputTraits + public var spellCheckingType: UITextSpellCheckingType = .no { + didSet { + if #available(iOS 10.0, *) { + inputTextField.spellCheckingType = spellCheckingType + } + } + } + + public var keyboardType: UIKeyboardType = .numberPad { + didSet { inputTextField.keyboardType = keyboardType } + } + + public var keyboardAppearance: UIKeyboardAppearance = .default { + didSet { inputTextField.keyboardAppearance = keyboardAppearance } + } - public var autocapitalizationType = UITextAutocapitalizationType.none - public var autocorrectionType = UITextAutocorrectionType.no - public var spellCheckingType = UITextSpellCheckingType.no - public var keyboardType = UIKeyboardType.numberPad - public var keyboardAppearance = UIKeyboardAppearance.default - public var returnKeyType = UIReturnKeyType.done - public var enablesReturnKeyAutomatically = true + public var returnKeyType: UIReturnKeyType = .done { + didSet { inputTextField.returnKeyType = returnKeyType } + } + + public var enablesReturnKeyAutomatically: Bool = true { + didSet { inputTextField.enablesReturnKeyAutomatically = enablesReturnKeyAutomatically } + } + + public var textContentType: UITextContentType! { + get { + if let preferredTextContentType = preferredTextContentType { + return preferredTextContentType + } + + if #available(iOS 12.0, *) { + return .oneTimeCode + } + + return nil + } + set { + preferredTextContentType = newValue + if #available(iOS 10.0, *) { + inputTextField.textContentType = newValue + } + } + } // MARK: - Initializers - + public init( digit: Int, itemSpacing: CGFloat, itemFactory: @escaping (() -> T), - autoresizes: Bool = false) { - + autoresizes: Bool = false) { + self.digit = digit self.itemSpacing = itemSpacing self.itemFactory = itemFactory self.autoResizes = autoresizes super.init(frame: .zero) - + self.items = (0.. Bool { + if action == #selector(pasteText) { + return canPaste && UIPasteboard.general.string != nil + } + return false + } + // MARK: - Functions override public func layoutSubviews() { super.layoutSubviews() - + + inputTextField.frame = bounds + guard let appearance = appearance else { stackView.frame = bounds return } - + stackView.bounds = CGRect( x: 0, y: 0, @@ -112,128 +239,184 @@ public class PinCodeInputView: UIControl, UITextInputTrait } public func set(text: String) { - if Validator.isPinCode(text: text, digit: digit) { - self.text = text - } + apply(text: text, notify: true) } public func set(changeTextHandler: @escaping (String) -> ()) { self.changeTextHandler = changeTextHandler } - + + public func clear() { + apply(text: "", notify: true) + } + public func set(appearance: ItemAppearance) { - self.appearance = appearance - if autoResizes { - self.appearance = ItemAppearance(itemSize: CGSize(width: (self.bounds.width - (self.itemSpacing * CGFloat(self.digit))) / CGFloat(self.digit), height: appearance.itemSize.height), font: appearance.font, textColor: appearance.textColor, backgroundColor: appearance.backgroundColor, cursorColor: appearance.cursorColor, cornerRadius: appearance.cornerRadius, borderColor: appearance.borderColor) - } - items.forEach { $0.itemView.set(appearance: appearance) } + let resolvedAppearance: ItemAppearance + if autoResizes { + resolvedAppearance = ItemAppearance( + itemSize: CGSize( + width: (bounds.width - (itemSpacing * CGFloat(digit - 1))) / CGFloat(digit), + height: appearance.itemSize.height + ), + font: appearance.font, + textColor: appearance.textColor, + backgroundColor: appearance.backgroundColor, + highlightBackgroundColor: appearance.highlightBackgroundColor, + cursorColor: appearance.cursorColor, + cornerRadius: appearance.cornerRadius, + highlightBorderColor: appearance.highlightBorderColor, + borderColor: appearance.borderColor + ) + } else { + resolvedAppearance = appearance + } + + self.appearance = resolvedAppearance + items.forEach { $0.itemView.set(appearance: resolvedAppearance) } + setNeedsLayout() } - + + private func apply(text rawText: String, notify: Bool) { + let normalizedText = normalize(text: rawText) + guard normalizedText.count == digit || normalizedText.isEmpty else { return } + + if inputTextField.text != normalizedText { + inputTextField.text = normalizedText + } + + if notify { + text = normalizedText + sendActions(for: .editingChanged) + } else { + self.text = normalizedText + } + } + + private func normalize(text rawText: String) -> String { + let numericText = rawText.filter { $0.isNumber } + return String(numericText.prefix(digit)) + } + private func updateText() { - - items.enumerated().forEach { (index, item) in + items.enumerated().forEach { index, item in if (0.. Bool { + return textField.resignFirstResponder() + } + // MARK: - UIResponder - + @discardableResult override public func becomeFirstResponder() -> Bool { showCursor() - return super.becomeFirstResponder() + return inputTextField.becomeFirstResponder() } - + override public var canBecomeFirstResponder: Bool { - return true + return inputTextField.canBecomeFirstResponder } @discardableResult override public func resignFirstResponder() -> Bool { hiddenCursor() - return super.resignFirstResponder() + return inputTextField.resignFirstResponder() } - - // MARK: - private class - private class ContainerItemView: UIView { - - var itemView: T + private final class OTPTextField: UITextField { + var isPasteEnabled: Bool = true + + override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { + if action == #selector(paste(_:)) { + return isPasteEnabled && super.canPerformAction(action, withSender: sender) + } + return super.canPerformAction(action, withSender: sender) + } + } + + private final class ContainerItemView: UIView { + + var itemView: Item private let surfaceView: UIView = .init() - private var didTapHandler: (() -> ())? - - init(itemView: T) { - + private var didTapHandler: (() -> Void)? + + init(itemView: Item) { self.itemView = itemView - super.init(frame: .zero) - + addSubview(itemView) addSubview(surfaceView) - + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTap)) surfaceView.addGestureRecognizer(tapGesture) } - + required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + override func layoutSubviews() { super.layoutSubviews() - itemView.frame = bounds surfaceView.frame = bounds } - - func setHandler(handler: @escaping () -> ()) { + + func setHandler(handler: @escaping () -> Void) { didTapHandler = handler } - + @objc private func didTap() { - if let handler = didTapHandler { - handler() - } + didTapHandler?() } } - }