-
Notifications
You must be signed in to change notification settings - Fork 0
Modernize NerdzPinView (3.2.0): tests, CI, DocC, docs, underline fix #4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
191d8aa
test: add Swift Testing target covering pin view logic layer
RomanKovalchukDev be43561
Merge test/logic-tests into release/3.2.0
RomanKovalchukDev 7823b22
ci: add GitHub Actions workflow for build and test
RomanKovalchukDev dfb2b2f
Merge ci/github-actions into release/3.2.0
RomanKovalchukDev d923b87
docs: fix Swift version badge and add CHANGELOG
RomanKovalchukDev 532ee20
Merge docs/changelog-readme into release/3.2.0
RomanKovalchukDev 38f74b2
chore: fix stale file header project name
RomanKovalchukDev 5a15804
Merge chore/header-cleanup into release/3.2.0
RomanKovalchukDev 27dcb0b
docs: add DocC catalog and public API doc comments
RomanKovalchukDev a8a3e3a
Merge docs/docc into release/3.2.0
RomanKovalchukDev e79ccb7
fix: remove unused underlineHeight parameter from UnderlineItemView.L…
RomanKovalchukDev 5be265c
Merge fix/underline-layout-dead-param into release/3.2.0
RomanKovalchukDev 754f1d8
docs: address PR review on DocC articles and aliases
RomanKovalchukDev File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| name: CI | ||
|
|
||
| on: | ||
| push: | ||
| branches: [main] | ||
| pull_request: | ||
|
|
||
| concurrency: | ||
| group: ci-${{ github.ref }} | ||
| cancel-in-progress: true | ||
|
|
||
| jobs: | ||
| build-and-test: | ||
| runs-on: macos-latest | ||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@v4 | ||
|
|
||
| - name: Select Xcode | ||
| # Pin to the latest stable Xcode provided by the runner image so the | ||
| # Swift 6 toolchain is deterministic across runs. | ||
| run: sudo xcode-select -switch /Applications/Xcode.app | ||
|
|
||
| - name: Show environment | ||
| run: | | ||
| xcodebuild -version | ||
| swift --version | ||
|
|
||
| - name: Build | ||
| # Build against a generic simulator destination. We do not pin a device | ||
| # name here (like "iPhone 16") because the exact simulators installed on | ||
| # GitHub macos-latest runners change over time, and a generic Simulator | ||
| # destination always resolves. A successful build in Swift 6 language | ||
| # mode (tools-version 6.0) is also the strict concurrency guard. | ||
| run: | | ||
| xcodebuild build \ | ||
| -scheme NerdzPinView \ | ||
| -destination 'generic/platform=iOS Simulator' | ||
|
|
||
| - name: Select an available iPhone simulator | ||
| # Tests need a concrete device, so we derive one at runtime instead of | ||
| # hardcoding a name. We list available devices as JSON and pick the first | ||
| # available iPhone, exposing its UDID for the test step. This survives | ||
| # runner image changes (any iPhone the image ships with will be used). | ||
| id: sim | ||
| run: | | ||
| UDID=$(xcrun simctl list devices available --json | python3 -c "import json,sys; d=json.load(sys.stdin)['devices']; ids=[x['udid'] for r in d for x in d[r] if x.get('isAvailable') and x.get('name','').startswith('iPhone')]; print(ids[0] if ids else '')") | ||
| if [ -z "$UDID" ]; then | ||
| echo "No available iPhone simulator found." >&2 | ||
| exit 1 | ||
| fi | ||
| echo "Using simulator UDID: $UDID" | ||
| echo "udid=$UDID" >> "$GITHUB_OUTPUT" | ||
|
|
||
| - name: Test | ||
| run: | | ||
| xcodebuild test \ | ||
| -scheme NerdzPinView \ | ||
| -destination "platform=iOS Simulator,id=${{ steps.sim.outputs.udid }}" \ | ||
| -enableCodeCoverage YES |
This file contains hidden or 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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| # Changelog | ||
|
|
||
| All notable changes to this project will be documented in this file. | ||
|
|
||
| The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), | ||
| and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). | ||
|
|
||
| This changelog starts its history at version 3.2.0. Earlier history is available through the git tags up to 3.1.0. | ||
|
|
||
| ## [Unreleased] | ||
|
|
||
| Nothing yet. | ||
|
|
||
| ## [3.2.0] 2026-09-16 | ||
|
|
||
| ### Added | ||
|
|
||
| * Swift Testing unit test target covering the pin view logic layer (text position, range, and selection math, plus per state appearance config resolution). | ||
| * GitHub Actions CI workflow that builds and tests on a macOS runner via xcodebuild against an iOS Simulator. | ||
| * DocC documentation catalog and doc comments for the public API. | ||
|
|
||
| ### Changed | ||
|
|
||
| * Corrected the README Swift version badge (it showed Swift 5.1 or 5.9, but the package requires Swift 6.0) and documented the Xcode 16 requirement. | ||
| * Raised the package to Swift tools 6.0, which sets the minimum Xcode to 16 for consumers. | ||
|
|
||
| ### Removed | ||
|
|
||
| * Removed the unused `underlineHeight` parameter from `UnderlineItemView.LayoutConfig.init`. The parameter was never stored and had no effect (underline height is controlled by `UnderlineItemView.AppearanceConfig` via `getUnderlineHeight(for:)`). Runtime behavior is unchanged. Call sites that passed `underlineHeight:` to the layout initializer must remove that argument and set the height on the appearance config instead. |
This file contains hidden or 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
This file contains hidden or 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
This file contains hidden or 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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| # Getting Started | ||
|
|
||
| Add NerdzPinView to your project. | ||
|
|
||
| ## Installation | ||
|
|
||
| NerdzPinView is distributed as a Swift package. Add it to your project through Xcode with File, Add Package Dependencies, then enter the repository URL and pick the ``NerdzPinView`` library product. | ||
|
|
||
| You can also declare the dependency directly in a `Package.swift` manifest. | ||
|
|
||
| ```swift | ||
| // swift-tools-version: 6.0 | ||
| import PackageDescription | ||
|
|
||
| let package = Package( | ||
| name: "MyApp", | ||
| platforms: [.iOS(.v16)], | ||
| dependencies: [ | ||
| .package(url: "https://github.com/RomanKovalchukDev/NerdzPinView.git", from: "1.0.0") | ||
| ], | ||
| targets: [ | ||
| .target( | ||
| name: "MyApp", | ||
| dependencies: ["NerdzPinView"] | ||
| ) | ||
| ] | ||
| ) | ||
| ``` | ||
|
|
||
| The package targets iOS 16 and later. | ||
|
|
||
| ## Next Steps | ||
|
|
||
| Pick the integration that matches your app. | ||
|
|
||
| - <doc:SwiftUIUsage> for the SwiftUI wrappers. | ||
| - <doc:UIKitUsage> for direct use of the UIKit input views. |
This file contains hidden or 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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| # ``NerdzPinView`` | ||
|
|
||
| Customizable pin code and one-time code input views for UIKit, with ready to use SwiftUI wrappers. | ||
|
|
||
| ## Overview | ||
|
|
||
| NerdzPinView provides styled, multi-cell code entry components for iOS. At its core are two generic UIKit containers. ``PinCodeInputView`` drives a row of tappable item cells through `UIKeyInput`, while ``OneTimeCodeInputView`` implements the full `UITextInput` protocol so it supports the system caret and one-time code autofill. Each container is parameterized by an item view type, and the module ships bordered, underlined, and grouped item views out of the box. | ||
|
|
||
| For most apps the pre-styled wrappers are enough. ``DesignableBorderedPinInputView``, ``DesignableUnderlinedPinInputView``, and ``DesignableOneTimeCodeInputView`` bundle sensible defaults, and ``NerdzBorderedPinView`` and ``NerdzUnderlinePinView`` expose that behavior to SwiftUI through bindings for the text, the state, and the keyboard focus. | ||
|
|
||
| To get started, read <doc:GettingStarted>, then <doc:SwiftUIUsage> for SwiftUI or <doc:UIKitUsage> for UIKit. | ||
|
|
||
| ## Topics | ||
|
|
||
| ### Essentials | ||
|
|
||
| - <doc:GettingStarted> | ||
| - <doc:SwiftUIUsage> | ||
| - <doc:UIKitUsage> | ||
|
|
||
| ### SwiftUI Views | ||
|
|
||
| - ``NerdzBorderedPinView`` | ||
| - ``NerdzUnderlinePinView`` | ||
|
|
||
| ### UIKit Input Views | ||
|
|
||
| - ``PinCodeInputView`` | ||
| - ``OneTimeCodeInputView`` | ||
| - ``DesignableBorderedPinInputView`` | ||
| - ``DesignableUnderlinedPinInputView`` | ||
| - ``DesignableOneTimeCodeInputView`` | ||
| - ``PinTapableView`` | ||
|
|
||
| ### Item Views | ||
|
|
||
| - ``BorderedItemView`` | ||
| - ``UnderlineItemView`` | ||
| - ``OneTimeItemView`` | ||
|
|
||
| ### Item View Protocols | ||
|
|
||
| - ``PinCodeItemViewType`` | ||
| - ``OneTimeCodeItemViewType`` | ||
| - ``PinCodeItemView`` | ||
| - ``OneTimeCodeItemView`` | ||
|
|
||
| ### Configuration | ||
|
|
||
| - ``DefaultableConfigType`` | ||
| - ``ItemViewAppearanceConfigurable`` | ||
| - ``ItemViewLayoutConfigurable`` | ||
| - ``PinCodeItemViewState`` | ||
|
|
||
| ### Text Input Primitives | ||
|
|
||
| - ``PinTextPosition`` | ||
| - ``PinTextRange`` | ||
| - ``PinTextSelectionRect`` | ||
|
|
||
| ### Callbacks | ||
|
|
||
| - ``PinCodeEmptyAction`` | ||
| - ``PinCodeTextAction`` |
This file contains hidden or 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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| # SwiftUI Usage | ||
|
|
||
| Present a pin input in SwiftUI with the ready to use wrappers. | ||
|
|
||
| ## Overview | ||
|
|
||
| Use ``NerdzBorderedPinView`` (or ``NerdzUnderlinePinView`` for the underlined style). It needs three bindings. One for the entered ``NerdzBorderedPinView/text``, one for the ``NerdzBorderedPinView/viewState``, and a `FocusState` binding for the keyboard focus. | ||
|
|
||
| ```swift | ||
| import SwiftUI | ||
| import NerdzPinView | ||
|
|
||
| struct VerificationView: View { | ||
| @State private var code: String = "" | ||
| @State private var pinState: NerdzBorderedPinView.ViewState = .normal | ||
| @FocusState private var isFocused: Bool | ||
|
|
||
| var body: some View { | ||
| NerdzBorderedPinView( | ||
| text: $code, | ||
| viewState: $pinState, | ||
| isFocused: $isFocused, | ||
| onPinViewEnteredFully: { enteredCode in | ||
| verify(enteredCode) | ||
| } | ||
| ) | ||
| .frame(height: 56) | ||
| .onAppear { | ||
| isFocused = true | ||
| } | ||
| } | ||
|
|
||
| private func verify(_ enteredCode: String) { | ||
| // Validate the code, then reflect the result in the state. | ||
| pinState = enteredCode == "123456" ? .normal : .error | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| The ``NerdzBorderedPinView/onPinViewEnteredFully`` closure fires once every cell is filled. Drive the ``NerdzBorderedPinView/viewState`` binding to `.error` to highlight an invalid code, or to `.disabled` to block further input. | ||
|
|
||
| ## Customizing the Appearance | ||
|
|
||
| Pass a ``BorderedItemView/AppearanceConfig`` and a ``BorderedItemView/LayoutConfig`` (or the underline equivalents) to the initializer to change colors, fonts, and metrics. Pass a ``NerdzBorderedPinView/ViewConfig`` to adjust behavior such as the number of characters. | ||
|
|
||
| ```swift | ||
| NerdzBorderedPinView( | ||
| text: $code, | ||
| viewState: $pinState, | ||
| isFocused: $isFocused, | ||
| config: .init(pinLength: 4), | ||
| itemsAppearanceConfig: BorderedItemView.AppearanceConfig( | ||
| defaultBorderColor: .systemGray, | ||
| activeBorderColor: .systemBlue, | ||
| font: .systemFont(ofSize: 20, weight: .semibold) | ||
| ) | ||
| ) | ||
| ``` | ||
|
|
||
| ## See Also | ||
|
|
||
| - <doc:GettingStarted> | ||
| - <doc:UIKitUsage> | ||
| - ``NerdzUnderlinePinView`` |
This file contains hidden or 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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| # Using the UIKit Input Views | ||
|
|
||
| Embed a pin or one-time code input directly in a view controller. | ||
|
|
||
| ## Overview | ||
|
|
||
| The SwiftUI wrappers are built on top of two generic UIKit containers, and you can use those containers directly. ``PinCodeInputView`` is a `UIKeyInput` based row of tappable cells, and ``OneTimeCodeInputView`` is a `UITextInput` based field that supports the system caret and one-time code autofill. Both are generic over an item view type, so you specialize them with one of the predefined item views such as ``BorderedItemView``, ``UnderlineItemView``, or ``OneTimeItemView``. | ||
|
|
||
| ## PinCodeInputView | ||
|
|
||
| Specialize ``PinCodeInputView`` with an item view, configure it, then add it to your hierarchy. Call `becomeFirstResponder()` to raise the keyboard. | ||
|
|
||
| ```swift | ||
| import UIKit | ||
| import NerdzPinView | ||
|
|
||
| final class PinViewController: UIViewController { | ||
|
|
||
| private let pinView = PinCodeInputView<BorderedItemView>() | ||
|
|
||
| override func viewDidLoad() { | ||
| super.viewDidLoad() | ||
|
|
||
| pinView.config = PinCodeInputView<BorderedItemView>.PinViewConfig(pinLength: 6) | ||
| pinView.appearanceConfig = BorderedItemView.AppearanceConfig( | ||
| font: .systemFont(ofSize: 18, weight: .medium) | ||
| ) | ||
|
|
||
| pinView.onPinValueChanged = { value in | ||
| print("Current value: \(value)") | ||
| } | ||
|
|
||
| pinView.onPinViewEnteredFully = { value in | ||
| print("Completed: \(value)") | ||
| } | ||
|
|
||
| pinView.translatesAutoresizingMaskIntoConstraints = false | ||
| view.addSubview(pinView) | ||
|
|
||
| NSLayoutConstraint.activate([ | ||
| pinView.centerYAnchor.constraint(equalTo: view.centerYAnchor), | ||
| pinView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 24), | ||
| pinView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -24), | ||
| pinView.heightAnchor.constraint(equalToConstant: 56) | ||
| ]) | ||
| } | ||
|
|
||
| override func viewDidAppear(_ animated: Bool) { | ||
| super.viewDidAppear(animated) | ||
|
|
||
| pinView.becomeFirstResponder() | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| Set ``PinCodeInputView/viewState`` to `.error` to highlight an invalid entry, and use ``PinCodeInputView/setText(_:)`` to prefill or clear the value without triggering the change callbacks. | ||
|
|
||
| ## OneTimeCodeInputView | ||
|
|
||
| ``OneTimeCodeInputView`` works the same way, specialized with ``OneTimeItemView``. Because it conforms to `UITextInput`, it participates in one-time code autofill and can group the cells into two halves. | ||
|
|
||
| ```swift | ||
| let codeView = OneTimeCodeInputView<OneTimeItemView>() | ||
|
|
||
| codeView.config = OneTimeCodeInputView<OneTimeItemView>.Config( | ||
| pinLength: 6, | ||
| shouldGroupNumbers: true | ||
| ) | ||
| codeView.appearanceConfig = OneTimeItemView.AppearanceConfig( | ||
| font: .systemFont(ofSize: 18, weight: .medium) | ||
| ) | ||
|
|
||
| codeView.onPinViewEnteredFully = { value in | ||
| print("Completed: \(value)") | ||
| } | ||
| ``` | ||
|
|
||
| Read the entered characters at any time through ``OneTimeCodeInputView/value``. | ||
|
|
||
| ## Using the Pre-Styled Wrappers | ||
|
|
||
| If you do not need to pick the item type yourself, the designable wrappers apply a default styling and expose the same callbacks and configuration. Use ``DesignableBorderedPinInputView``, ``DesignableUnderlinedPinInputView``, or ``DesignableOneTimeCodeInputView``. These are also the views the SwiftUI wrappers bridge to. | ||
|
|
||
| ## See Also | ||
|
|
||
| - <doc:GettingStarted> | ||
| - ``PinCodeInputView`` | ||
| - ``OneTimeCodeInputView`` | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.