Skip to content
Merged
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
11 changes: 8 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,23 @@
## Unreleased

### Breaking
- Renamed the "addition" terminology to "indicator" throughout `SwipeMenuViewOptions`, matching UIKit's selection-indicator vocabulary. The old names have been removed:
- `TabView.Addition` → `TabView.Indicator` (the `.underline` / `.circle` / `.none` cases are unchanged)
- `TabView.addition` → `TabView.indicator`
- `TabView.AdditionView` → `TabView.IndicatorView`
- `TabView.additionView` → `TabView.indicatorView`
- Renamed three `SwipeMenuViewOptions` properties to follow Swift API naming conventions. The old spellings have been removed:
- `TabView.needsAdjustItemViewWidth` → `TabView.adjustsItemViewWidth`
- `TabView.needsConvertTextColorRatio` → `TabView.interpolatesTextColorOnSwipe`
- `TabView.AdditionView.isAnimationOnSwipeEnable` → `TabView.AdditionView.isAnimationOnSwipeEnabled`
- `TabView.AdditionView.isAnimationOnSwipeEnable` → `TabView.IndicatorView.isAnimationOnSwipeEnabled`

### Added
- `SwipeMenuViewOptions.TabView.ItemView.selectedFont` to use a different title font while a tab is selected. Defaults to the same 14 pt bold system font as `font`, so the title font does not change on selection unless you set it. It affects the selected title's appearance only; in the `.flexible` style item widths are still measured with `font`.
- `SwipeMenuViewOptions.TabView.ItemView.numberOfLines` to let tab titles wrap onto multiple lines (use `0` for as many lines as the title needs). Defaults to `1`, preserving the previous single-line behavior. Most useful with the `.segmented` style, where a long title would otherwise be truncated.
- `SwipeMenuViewOptions.TabView.AdditionView.Underline.cornerRadius` to round the corners of the underline indicator (set it to half the underline height for a pill shape). Defaults to `0`, preserving the previous square corners.
- `SwipeMenuViewOptions.TabView.IndicatorView.Underline.cornerRadius` to round the corners of the underline indicator (set it to half the underline height for a pill shape). Defaults to `0`, preserving the previous square corners.

### Fixed
- The `.segmented` tab style mispositioned the selection indicator when `additionView.padding` had non-zero horizontal insets: the first tab's indicator spilled off the leading edge, and each later tab drifted increasingly to the left. The indicator now aligns with every tab, inset by the padding, consistent with the other tab styles (issue #25).
- The `.segmented` tab style mispositioned the selection indicator when `indicatorView.padding` had non-zero horizontal insets: the first tab's indicator spilled off the leading edge, and each later tab drifted increasingly to the left. The indicator now aligns with every tab, inset by the padding, consistent with the other tab styles (issue #25).
- The `.flexible` tab style computed its scrollable width by subtracting the right safe-area inset instead of adding it, so on devices with a non-zero right inset (for example landscape with the notch on the left) the last tab could not be scrolled fully into view.
- `ContentScrollView.reload()` left the previous page views in the view hierarchy while building the new ones, stacking a duplicate set of pages on every call. It now replaces the pages and preserves the current page index.

Expand Down
8 changes: 4 additions & 4 deletions Example/Example/SwipeMenuSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ struct SwipeMenuSettings: Equatable {
options.tabView.adjustsItemViewWidth = adjustsItemWidthToFit
options.tabView.itemView.width = itemWidth
options.tabView.itemView.textColor = .secondaryLabel
options.tabView.additionView.backgroundColor = .label
options.tabView.indicatorView.backgroundColor = .label

switch style {
case .flexible: options.tabView.style = .flexible
Expand All @@ -101,14 +101,14 @@ struct SwipeMenuSettings: Equatable {

switch tabDecoration {
case .underline:
options.tabView.addition = .underline
options.tabView.indicator = .underline
options.tabView.itemView.selectedTextColor = .label
case .circle:
options.tabView.addition = .circle
options.tabView.indicator = .circle
// The pill is filled with `.label`, so the title inverts to stay legible.
options.tabView.itemView.selectedTextColor = .systemBackground
case .none:
options.tabView.addition = .none
options.tabView.indicator = .none
options.tabView.itemView.selectedTextColor = .label
}

Expand Down
12 changes: 6 additions & 6 deletions Example/ExampleTests/SwipeMenuSettingsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ struct SwipeMenuSettingsTests {
let options = SwipeMenuSettings().makeOptions()

#expect(options.tabView.style == .flexible)
#expect(options.tabView.addition == .underline)
#expect(options.tabView.indicator == .underline)
#expect(options.tabView.margin == 0)
#expect(options.tabView.adjustsItemViewWidth)
#expect(options.tabView.itemView.width == 100)
Expand Down Expand Up @@ -59,23 +59,23 @@ struct SwipeMenuSettingsTests {
#expect(settings.makeOptions().tabView.style == .segmented)
}

@Test("Each decoration maps to its addition and a legible selected color")
func decorationMapsToAdditionAndColor() {
@Test("Each decoration maps to its indicator and a legible selected color")
func decorationMapsToIndicatorAndColor() {
var settings = SwipeMenuSettings()

settings.tabDecoration = .underline
var options = settings.makeOptions()
#expect(options.tabView.addition == .underline)
#expect(options.tabView.indicator == .underline)
#expect(options.tabView.itemView.selectedTextColor == UIColor.label)

settings.tabDecoration = .circle
options = settings.makeOptions()
#expect(options.tabView.addition == .circle)
#expect(options.tabView.indicator == .circle)
#expect(options.tabView.itemView.selectedTextColor == UIColor.systemBackground)

settings.tabDecoration = .none
options = settings.makeOptions()
#expect(options.tabView.addition == .none)
#expect(options.tabView.indicator == .none)
#expect(options.tabView.itemView.selectedTextColor == UIColor.label)
}

Expand Down
10 changes: 5 additions & 5 deletions Sources/SwipeMenuViewController/SwipeMenuView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,7 @@ extension SwipeMenuView: UIScrollViewDelegate {
update(from: currentIndex, to: currentIndex - 1)
}

moveAdditionView(by: scrollView)
moveIndicatorView(by: scrollView)
}

public func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
Expand All @@ -411,18 +411,18 @@ extension SwipeMenuView: UIScrollViewDelegate {
return
}

moveAdditionView(by: scrollView)
moveIndicatorView(by: scrollView)
}

/// Moves the tab bar's addition view (underline/circle) to track the content scroll position.
private func moveAdditionView(by scrollView: UIScrollView) {
/// Moves the tab bar's indicator view (underline/circle) to track the content scroll position.
private func moveIndicatorView(by scrollView: UIScrollView) {

guard let tabView, let contentScrollView else { return }

let ratio = scrollView.contentOffset.x.truncatingRemainder(dividingBy: contentScrollView.frame.width) / contentScrollView.frame.width
let direction: TabView.Direction = scrollView.contentOffset.x >= frame.width * CGFloat(currentIndex) ? .forward : .reverse

tabView.moveAdditionView(index: currentIndex, ratio: ratio, direction: direction)
tabView.moveIndicatorView(index: currentIndex, ratio: ratio, direction: direction)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ defaults, so you only override what you need.
```swift
var options = SwipeMenuViewOptions()
options.tabView.style = .segmented
options.tabView.addition = .underline
options.tabView.indicator = .underline
swipeMenuView.reloadData(options: options)
```

Expand All @@ -33,7 +33,7 @@ top-level `isSafeAreaEnabled` toggles the safe-area behavior of both at once.
- `backgroundColor`: the bar's background color. Defaults to `.clear`.
- `clipsToBounds`: whether the bar clips its contents. Defaults to `true`.
- `style`: `.flexible` (items sized to their content) or `.segmented` (items share the width equally). Defaults to `.flexible`.
- `addition`: the selection indicator — `.underline`, `.circle`, or `.none`. Defaults to `.underline`.
- `indicator`: the selection indicator — `.underline`, `.circle`, or `.none`. Defaults to `.underline`.
- `adjustsItemViewWidth`: whether flexible item widths are adjusted to fit their titles. Defaults to `true`.
- `interpolatesTextColorOnSwipe`: whether the item text color interpolates toward the selected color as you swipe. Defaults to `true`.
- `isSafeAreaEnabled`: whether the bar respects the safe area. Defaults to `true`.
Expand All @@ -44,7 +44,7 @@ options.tabView.height = 52
options.tabView.margin = 8
options.tabView.backgroundColor = .systemBackground
options.tabView.style = .flexible
options.tabView.addition = .underline
options.tabView.indicator = .underline
options.tabView.adjustsItemViewWidth = true
options.tabView.interpolatesTextColorOnSwipe = true
options.tabView.isSafeAreaEnabled = true
Expand Down Expand Up @@ -83,47 +83,47 @@ options.tabView.itemView.numberOfLines = 0

## Selection indicator

The `additionView` group configures the selection indicator drawn behind or beneath the items:
The `indicatorView` group configures the selection indicator drawn behind or beneath the items:

- `padding`: insets applied to the indicator. Defaults to `.zero`.
- `backgroundColor`: the indicator color. Defaults to `.black`.
- `animationDuration`: the duration used when animating the indicator to a tapped tab. Defaults to `0.3`.
- `isAnimationOnSwipeEnabled`: whether the indicator follows your finger continuously while swiping. When `false`, it jumps to the destination tab instead. Defaults to `true`.

```swift
options.tabView.addition = .underline
options.tabView.additionView.padding = UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 8)
options.tabView.additionView.backgroundColor = .systemBlue
options.tabView.additionView.animationDuration = 0.25
options.tabView.additionView.isAnimationOnSwipeEnabled = true
options.tabView.indicator = .underline
options.tabView.indicatorView.padding = UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 8)
options.tabView.indicatorView.backgroundColor = .systemBlue
options.tabView.indicatorView.animationDuration = 0.25
options.tabView.indicatorView.isAnimationOnSwipeEnabled = true
```

### Underline

When `addition` is `.underline`, the `underline` group sets the underline thickness. There is no
top-level height on the addition view — the thickness lives on the underline options:
When `indicator` is `.underline`, the `underline` group sets the underline thickness. There is no
top-level height on the indicator view — the thickness lives on the underline options:

- `height`: the underline thickness. Defaults to `2.0`.
- `cornerRadius`: the underline's corner radius. Defaults to `0` (square corners). Set it to half the height for a pill shape.

```swift
options.tabView.addition = .underline
options.tabView.additionView.underline.height = 3
options.tabView.additionView.underline.cornerRadius = 1.5
options.tabView.indicator = .underline
options.tabView.indicatorView.underline.height = 3
options.tabView.indicatorView.underline.cornerRadius = 1.5
```

### Circle

When `addition` is `.circle`, the `circle` group shapes the highlight drawn behind the selected item:
When `indicator` is `.circle`, the `circle` group shapes the highlight drawn behind the selected item:

- `cornerRadius`: the corner radius. When `nil` (the default), it is half the indicator's height, producing a pill.
- `maskedCorners`: which corners are rounded. Defaults to `nil` (all corners).

```swift
options.tabView.addition = .circle
options.tabView.additionView.backgroundColor = .systemBlue
options.tabView.additionView.circle.cornerRadius = 8
options.tabView.additionView.circle.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
options.tabView.indicator = .circle
options.tabView.indicatorView.backgroundColor = .systemBlue
options.tabView.indicatorView.circle.cornerRadius = 8
options.tabView.indicatorView.circle.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
```

## Content area
Expand Down
33 changes: 17 additions & 16 deletions Sources/SwipeMenuViewController/SwipeMenuViewOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public nonisolated struct SwipeMenuViewOptions: Sendable {
// TODO: case infinity
}

public nonisolated enum Addition: Sendable {
public nonisolated enum Indicator: Sendable {
case underline
case circle
case none
Expand Down Expand Up @@ -51,37 +51,37 @@ public nonisolated struct SwipeMenuViewOptions: Sendable {
public var numberOfLines: Int = 1
}

public nonisolated struct AdditionView: Sendable {
public nonisolated struct IndicatorView: Sendable {

public nonisolated struct Underline: Sendable {
/// Underline height if addition style select `.underline`. Defaults to `2.0`.
/// The underline thickness when the indicator is `.underline`. Defaults to `2.0`.
public var height: CGFloat = 2.0

/// Corner radius of the underline if addition style select `.underline`.
/// The corner radius of the underline when the indicator is `.underline`.
/// Defaults to `0` (square corners). Set it to half of `height` for a pill shape.
public var cornerRadius: CGFloat = 0
}

public nonisolated struct Circle: Sendable {
/// Circle cornerRadius if addition style select `.circle`. Defaults to `nil`.
/// `AdditionView.height / 2` in the case of nil.
/// 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

/// Circle maskedCorners if addition style select `.circle`. Defaults to `nil`.
/// It helps to make specific corners rounded.
/// The corners rounded by `cornerRadius` when the indicator is `.circle`.
/// Defaults to `nil`, which rounds all four corners.
public var maskedCorners: CACornerMask? = nil
}

/// AdditionView paddings. Defaults to `.zero`.
/// The padding around the indicator view. Defaults to `.zero`.
public var padding: UIEdgeInsets = .zero

/// AdditionView backgroundColor. Defaults to `.black`.
/// The indicator view's background color. Defaults to `.black`.
public var backgroundColor: UIColor = .black

/// AdditionView animating duration. Defaults to `0.3`.
/// The duration of the indicator's move animation, in seconds. Defaults to `0.3`.
public var animationDuration: Double = 0.3

/// Whether the addition view continuously tracks the finger while the content is
/// Whether the indicator view continuously tracks the finger while the content is
/// swiped. When `false`, it animates to the destination tab once the page changes
/// instead. Defaults to `true`.
public var isAnimationOnSwipeEnabled: Bool = true
Expand All @@ -108,8 +108,9 @@ public nonisolated struct SwipeMenuViewOptions: Sendable {
/// TabView style. Defaults to `.flexible`. Style type has [`.flexible` , `.segmented`].
public var style: Style = .flexible

/// TabView addition. Defaults to `.underline`. Addition type has [`.underline`, `.circle`, `.none`].
public var addition: Addition = .underline
/// The selection indicator drawn on the selected tab: `.underline`, `.circle`,
/// or `.none`. Defaults to `.underline`.
public var indicator: Indicator = .underline

/// Whether each `.flexible` item is sized to fit its title (plus ``ItemView/margin``
/// on both sides) instead of using the fixed ``ItemView/width``. Defaults to `true`.
Expand All @@ -126,8 +127,8 @@ public nonisolated struct SwipeMenuViewOptions: Sendable {
/// ItemView options
public var itemView = ItemView()

/// AdditionView options
public var additionView = AdditionView()
/// IndicatorView options
public var indicatorView = IndicatorView()

public init() { }
}
Expand Down
Loading