Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,19 @@ concurrency:
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}

jobs:
lint:
name: SwiftLint
runs-on: ubuntu-latest
timeout-minutes: 10
container:
# Pin the SwiftLint release so lint results are reproducible.
image: ghcr.io/realm/swiftlint:0.64.1
steps:
- uses: actions/checkout@v7
- name: Lint
# --strict promotes warnings to errors, so any violation fails the job.
run: swiftlint lint --strict --reporter github-actions-logging

package:
name: Package tests (iOS Simulator)
runs-on: macos-26
Expand Down
40 changes: 40 additions & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# SwiftLint configuration for SwipeMenuViewController.
#
# CI runs `swiftlint lint --strict`, so every warning fails the build. The
# thresholds below are deliberate deviations from the defaults, chosen to match
# how this codebase is organized rather than to silence real problems.

included:
- Sources
- Tests
- Example/Example
- Example/ExampleTests

# SwipeMenuViewOptions groups its option structs by component
# (TabView.IndicatorView.Underline, ...); the nesting is the API design.
nesting:
type_level: 3

# Allow idiomatic two-letter names such as `vc` in tests, and the
# conventional color-component names in the UIColor tests.
identifier_name:
min_length: 2
excluded:
- r
- g
- b
- a

line_length:
warning: 160
ignores_urls: true

file_length:
warning: 600

type_body_length:
warning: 350

# Tests keep windows/data sources alive with withExtendedLifetime((a, b, c)).
large_tuple:
warning: 4
6 changes: 5 additions & 1 deletion Sources/SwipeMenuViewController/SwipeMenuView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,11 @@ open class SwipeMenuView: UIView {
tabView = TabView(frame: CGRect(x: 0, y: 0, width: frame.width, height: options.tabView.height), options: options.tabView)
tabView?.clipsToBounds = options.tabView.clipsToBounds

contentScrollView = ContentScrollView(frame: CGRect(x: 0, y: options.tabView.height, width: frame.width, height: frame.height - options.tabView.height), default: defaultIndex, options: options.contentScrollView)
let contentFrame = CGRect(x: 0,
y: options.tabView.height,
width: frame.width,
height: frame.height - options.tabView.height)
contentScrollView = ContentScrollView(frame: contentFrame, default: defaultIndex, options: options.contentScrollView)
contentScrollView?.clipsToBounds = options.contentScrollView.clipsToBounds

tabView?.update(defaultIndex)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,10 @@ open class SwipeMenuViewController: UIViewController, SwipeMenuViewDelegate, Swi
/// than crashing. Override to provide pages that are not backed by `children`.
open func swipeMenuView(_ swipeMenuView: SwipeMenuView, viewControllerForPageAt index: Int) -> UIViewController {
guard children.indices.contains(index) else {
assertionFailure("SwipeMenuViewController: requested a page at \(index) but only \(children.count) child view controllers exist. Override the data source to provide the missing pages.")
assertionFailure("""
SwipeMenuViewController: requested a page at \(index) but only \(children.count) \
child view controllers exist. Override the data source to provide the missing pages.
""")
// Return a detached placeholder rather than crashing. It is
// intentionally not added via `addChild(_:)`: the default
// `numberOfPages(in:)` counts `children`, so adding children on this
Expand Down
5 changes: 2 additions & 3 deletions Sources/SwipeMenuViewController/SwipeMenuViewOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ public nonisolated struct SwipeMenuViewOptions: Sendable {
public nonisolated enum Style: Sendable {
case flexible
case segmented
// TODO: case infinity
}

public nonisolated enum Indicator: Sendable {
Expand Down Expand Up @@ -65,11 +64,11 @@ public nonisolated struct SwipeMenuViewOptions: Sendable {
public nonisolated struct Circle: Sendable {
/// The corner radius of the highlight when the indicator is `.circle`.
/// Defaults to `nil`, which uses half the highlight's height (a capsule).
public var cornerRadius: CGFloat? = nil
public var cornerRadius: CGFloat?

/// The corners rounded by `cornerRadius` when the indicator is `.circle`.
/// Defaults to `nil`, which rounds all four corners.
public var maskedCorners: CACornerMask? = nil
public var maskedCorners: CACornerMask?
}

/// The padding around the indicator view. Defaults to `.zero`.
Expand Down
46 changes: 31 additions & 15 deletions Sources/SwipeMenuViewController/TabView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -299,11 +299,12 @@ open class TabView: UIScrollView {

containerView.frame.size.width = containerWidth
containerView.translatesAutoresizingMaskIntoConstraints = false

let heightConstraint: NSLayoutConstraint
switch options.indicator {
case .underline:
heightConstraint = containerView.heightAnchor.constraint(equalToConstant: options.height - options.indicatorView.underline.height - options.indicatorView.padding.bottom)
let height = options.height - options.indicatorView.underline.height - options.indicatorView.padding.bottom
heightConstraint = containerView.heightAnchor.constraint(equalToConstant: height)
case .circle, .none:
heightConstraint = containerView.heightAnchor.constraint(equalToConstant: options.height)
}
Expand Down Expand Up @@ -334,8 +335,8 @@ open class TabView: UIScrollView {
}

private func updateSelectedItem(by newIndex: Int) {
for (i, itemView) in itemViews.enumerated() {
itemView.isSelected = i == newIndex
for (index, itemView) in itemViews.enumerated() {
itemView.isSelected = index == newIndex
}
}
}
Expand All @@ -358,20 +359,27 @@ extension TabView {
switch options.indicator {
case .underline:
let itemView = itemViews[currentIndex]
indicatorView = UIView(frame: CGRect(x: itemView.frame.origin.x + options.indicatorView.padding.left, y: itemView.frame.height - options.indicatorView.padding.vertical, width: itemView.frame.width - options.indicatorView.padding.horizontal, height: options.indicatorView.underline.height))
let padding = options.indicatorView.padding
indicatorView = UIView(frame: CGRect(x: itemView.frame.origin.x + padding.left,
y: itemView.frame.height - padding.vertical,
width: itemView.frame.width - padding.horizontal,
height: options.indicatorView.underline.height))
indicatorView.layer.cornerRadius = options.indicatorView.underline.cornerRadius
indicatorView.backgroundColor = options.indicatorView.backgroundColor
containerView.addSubview(indicatorView)
case .circle:
let itemView = itemViews[currentIndex]
let height = itemView.bounds.height - options.indicatorView.padding.vertical
indicatorView = UIView(frame: CGRect(x: itemView.frame.origin.x + options.indicatorView.padding.left, y: 0, width: itemView.frame.width - options.indicatorView.padding.horizontal, height: height))
let padding = options.indicatorView.padding
indicatorView = UIView(frame: CGRect(x: itemView.frame.origin.x + padding.left,
y: 0,
width: itemView.frame.width - padding.horizontal,
height: itemView.bounds.height - padding.vertical))
indicatorView.layer.position.y = itemView.layer.position.y
indicatorView.layer.cornerRadius = options.indicatorView.circle.cornerRadius ?? indicatorView.frame.height / 2
indicatorView.backgroundColor = options.indicatorView.backgroundColor
if let m = options.indicatorView.circle.maskedCorners {
indicatorView.layer.maskedCorners = m

if let maskedCorners = options.indicatorView.circle.maskedCorners {
indicatorView.layer.maskedCorners = maskedCorners
}

containerView.addSubview(indicatorView)
Expand Down Expand Up @@ -432,20 +440,29 @@ extension TabView {
guard let currentItem else { return }

if options.indicatorView.isAnimationOnSwipeEnabled {
let padding = options.indicatorView.padding
switch direction {
case .forward:
if let nextItem {
indicatorView.frame.origin.x = currentItem.frame.origin.x + (nextItem.frame.origin.x - currentItem.frame.origin.x) * ratio + options.indicatorView.padding.left
indicatorView.frame.size.width = currentItem.frame.size.width + (nextItem.frame.size.width - currentItem.frame.size.width) * ratio - options.indicatorView.padding.horizontal
indicatorView.frame.origin.x = currentItem.frame.origin.x
+ (nextItem.frame.origin.x - currentItem.frame.origin.x) * ratio
+ padding.left
indicatorView.frame.size.width = currentItem.frame.size.width
+ (nextItem.frame.size.width - currentItem.frame.size.width) * ratio
- padding.horizontal
if options.interpolatesTextColorOnSwipe {
nextItem.titleLabel.textColor = options.itemView.textColor.convert(to: options.itemView.selectedTextColor, multiplier: ratio)
currentItem.titleLabel.textColor = options.itemView.selectedTextColor.convert(to: options.itemView.textColor, multiplier: ratio)
}
}
case .reverse:
if let previousItem {
indicatorView.frame.origin.x = previousItem.frame.origin.x + (currentItem.frame.origin.x - previousItem.frame.origin.x) * ratio + options.indicatorView.padding.left
indicatorView.frame.size.width = previousItem.frame.size.width + (currentItem.frame.size.width - previousItem.frame.size.width) * ratio - options.indicatorView.padding.horizontal
indicatorView.frame.origin.x = previousItem.frame.origin.x
+ (currentItem.frame.origin.x - previousItem.frame.origin.x) * ratio
+ padding.left
indicatorView.frame.size.width = previousItem.frame.size.width
+ (currentItem.frame.size.width - previousItem.frame.size.width) * ratio
- padding.horizontal
if options.interpolatesTextColorOnSwipe {
previousItem.titleLabel.textColor = options.itemView.selectedTextColor.convert(to: options.itemView.textColor, multiplier: ratio)
currentItem.titleLabel.textColor = options.itemView.textColor.convert(to: options.itemView.selectedTextColor, multiplier: ratio)
Expand Down Expand Up @@ -530,4 +547,3 @@ extension TabView {
}
}
}