diff --git a/CHANGELOG.md b/CHANGELOG.md index bd61ea6..83afae7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/Example/Example/SwipeMenuSettings.swift b/Example/Example/SwipeMenuSettings.swift index c1db38b..0fdf564 100644 --- a/Example/Example/SwipeMenuSettings.swift +++ b/Example/Example/SwipeMenuSettings.swift @@ -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 @@ -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 } diff --git a/Example/ExampleTests/SwipeMenuSettingsTests.swift b/Example/ExampleTests/SwipeMenuSettingsTests.swift index 4b4f65b..c38c3a7 100644 --- a/Example/ExampleTests/SwipeMenuSettingsTests.swift +++ b/Example/ExampleTests/SwipeMenuSettingsTests.swift @@ -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) @@ -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) } diff --git a/Sources/SwipeMenuViewController/SwipeMenuView.swift b/Sources/SwipeMenuViewController/SwipeMenuView.swift index a6d3245..e0d0847 100644 --- a/Sources/SwipeMenuViewController/SwipeMenuView.swift +++ b/Sources/SwipeMenuViewController/SwipeMenuView.swift @@ -395,7 +395,7 @@ extension SwipeMenuView: UIScrollViewDelegate { update(from: currentIndex, to: currentIndex - 1) } - moveAdditionView(by: scrollView) + moveIndicatorView(by: scrollView) } public func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) { @@ -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) } } diff --git a/Sources/SwipeMenuViewController/SwipeMenuViewController.docc/CustomizingAppearance.md b/Sources/SwipeMenuViewController/SwipeMenuViewController.docc/CustomizingAppearance.md index 4cb68e3..adc308f 100644 --- a/Sources/SwipeMenuViewController/SwipeMenuViewController.docc/CustomizingAppearance.md +++ b/Sources/SwipeMenuViewController/SwipeMenuViewController.docc/CustomizingAppearance.md @@ -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) ``` @@ -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`. @@ -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 @@ -83,7 +83,7 @@ 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`. @@ -91,39 +91,39 @@ The `additionView` group configures the selection indicator drawn behind or bene - `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 diff --git a/Sources/SwipeMenuViewController/SwipeMenuViewOptions.swift b/Sources/SwipeMenuViewController/SwipeMenuViewOptions.swift index ed641cf..8d73a68 100644 --- a/Sources/SwipeMenuViewController/SwipeMenuViewOptions.swift +++ b/Sources/SwipeMenuViewController/SwipeMenuViewOptions.swift @@ -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 @@ -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 @@ -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`. @@ -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() { } } diff --git a/Sources/SwipeMenuViewController/TabView.swift b/Sources/SwipeMenuViewController/TabView.swift index 396ae3e..2b0295f 100644 --- a/Sources/SwipeMenuViewController/TabView.swift +++ b/Sources/SwipeMenuViewController/TabView.swift @@ -49,7 +49,7 @@ extension TabViewDelegate { /// The scrollable tab bar displayed at the top of a ``SwipeMenuView``. /// -/// A `TabView` lays out one tab item per page and draws the selection addition +/// A `TabView` lays out one tab item per page and draws the selection indicator /// (underline or circle) configured by ``SwipeMenuViewOptions/TabView``. It is created and /// managed by ``SwipeMenuView``; you normally configure it through the options rather than /// instantiating it directly. @@ -65,7 +65,7 @@ open class TabView: UIScrollView { private let containerView = UIStackView() - private var additionView = UIView() + private var indicatorView = UIView() private var currentIndex: Int = 0 @@ -101,7 +101,7 @@ open class TabView: UIScrollView { open override func layoutSubviews() { super.layoutSubviews() - resetAdditionViewPosition(index: currentIndex) + resetIndicatorViewPosition(index: currentIndex) } open override func safeAreaInsetsDidChange() { @@ -161,7 +161,7 @@ open class TabView: UIScrollView { setupScrollView() setupContainerView(dataSource: dataSource) setupTabItemViews(dataSource: dataSource) - setupAdditionView() + setupIndicatorView() if let defaultIndex { moveTabItem(index: defaultIndex, animated: animated) @@ -171,7 +171,7 @@ open class TabView: UIScrollView { func reset() { currentIndex = 0 itemViews.forEach { $0.removeFromSuperview() } - additionView.removeFromSuperview() + indicatorView.removeFromSuperview() containerView.removeFromSuperview() itemViews = [] } @@ -210,9 +210,9 @@ open class TabView: UIScrollView { let itemCount = dataSource.numberOfItems(in: self) var containerHeight: CGFloat = 0.0 - switch options.addition { + switch options.indicator { case .underline: - containerHeight = frame.height - options.additionView.underline.height - options.additionView.padding.bottom + containerHeight = frame.height - options.indicatorView.underline.height - options.indicatorView.padding.bottom case .none, .circle: containerHeight = frame.height } @@ -292,7 +292,7 @@ open class TabView: UIScrollView { layout(containerView: containerView, containerWidth: xPosition) addTabItemGestures() - animateAdditionView(index: currentIndex, animated: false) + animateIndicatorView(index: currentIndex, animated: false) } private func layout(containerView: UIView, containerWidth: CGFloat) { @@ -301,9 +301,9 @@ open class TabView: UIScrollView { containerView.translatesAutoresizingMaskIntoConstraints = false let heightConstraint: NSLayoutConstraint - switch options.addition { + switch options.indicator { case .underline: - heightConstraint = containerView.heightAnchor.constraint(equalToConstant: options.height - options.additionView.underline.height - options.additionView.padding.bottom) + heightConstraint = containerView.heightAnchor.constraint(equalToConstant: options.height - options.indicatorView.underline.height - options.indicatorView.padding.bottom) case .circle, .none: heightConstraint = containerView.heightAnchor.constraint(equalToConstant: options.height) } @@ -340,11 +340,11 @@ open class TabView: UIScrollView { } } -// MARK: - AdditionView +// MARK: - IndicatorView extension TabView { - /// The direction in which the selection addition moves as the content scrolls. + /// The direction in which the selection indicator moves as the content scrolls. public nonisolated enum Direction: Sendable { /// Moving toward a higher page index (scrolling forward). case forward @@ -352,51 +352,51 @@ extension TabView { case reverse } - private func setupAdditionView() { + private func setupIndicatorView() { if itemViews.isEmpty { return } - switch options.addition { + switch options.indicator { case .underline: let itemView = itemViews[currentIndex] - additionView = UIView(frame: CGRect(x: itemView.frame.origin.x + options.additionView.padding.left, y: itemView.frame.height - options.additionView.padding.vertical, width: itemView.frame.width - options.additionView.padding.horizontal, height: options.additionView.underline.height)) - additionView.layer.cornerRadius = options.additionView.underline.cornerRadius - additionView.backgroundColor = options.additionView.backgroundColor - containerView.addSubview(additionView) + 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)) + 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.additionView.padding.vertical - additionView = UIView(frame: CGRect(x: itemView.frame.origin.x + options.additionView.padding.left, y: 0, width: itemView.frame.width - options.additionView.padding.horizontal, height: height)) - additionView.layer.position.y = itemView.layer.position.y - additionView.layer.cornerRadius = options.additionView.circle.cornerRadius ?? additionView.frame.height / 2 - additionView.backgroundColor = options.additionView.backgroundColor + 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)) + 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.additionView.circle.maskedCorners { - additionView.layer.maskedCorners = m + if let m = options.indicatorView.circle.maskedCorners { + indicatorView.layer.maskedCorners = m } - containerView.addSubview(additionView) - containerView.sendSubviewToBack(additionView) + containerView.addSubview(indicatorView) + containerView.sendSubviewToBack(indicatorView) case .none: - additionView.backgroundColor = .clear + indicatorView.backgroundColor = .clear } jump(to: currentIndex) } - private func updateAdditionViewPosition(index: Int) { + private func updateIndicatorViewPosition(index: Int) { guard let target = currentItem else { return } - additionView.frame.origin.x = target.frame.origin.x + options.additionView.padding.left + indicatorView.frame.origin.x = target.frame.origin.x + options.indicatorView.padding.left if options.adjustsItemViewWidth { let cellWidth = itemViews[index].frame.width - additionView.frame.size.width = cellWidth - options.additionView.padding.horizontal + indicatorView.frame.size.width = cellWidth - options.indicatorView.padding.horizontal } focus(on: target) } - private func resetAdditionViewPosition(index: Int) { + private func resetIndicatorViewPosition(index: Int) { guard options.style == .segmented, let dataSource, dataSource.numberOfItems(in: self) > 0 else { return } @@ -408,35 +408,35 @@ extension TabView { let inset = layoutSafeAreaInsets let cellWidth = (frame.width - options.margin * 2 - inset.horizontal) / CGFloat(dataSource.numberOfItems(in: self)) - additionView.frame.origin.x = cellWidth * CGFloat(index) + options.additionView.padding.left - additionView.frame.size.width = cellWidth - options.additionView.padding.horizontal + indicatorView.frame.origin.x = cellWidth * CGFloat(index) + options.indicatorView.padding.left + indicatorView.frame.size.width = cellWidth - options.indicatorView.padding.horizontal } - private func animateAdditionView(index: Int, animated: Bool, completion: ((Bool) -> Void)? = nil) { + private func animateIndicatorView(index: Int, animated: Bool, completion: ((Bool) -> Void)? = nil) { update(index) if animated { - UIView.animate(withDuration: options.additionView.animationDuration, animations: { - self.updateAdditionViewPosition(index: index) + UIView.animate(withDuration: options.indicatorView.animationDuration, animations: { + self.updateIndicatorViewPosition(index: index) }, completion: completion) } else { - updateAdditionViewPosition(index: index) + updateIndicatorViewPosition(index: index) } } - func moveAdditionView(index: Int, ratio: CGFloat, direction: Direction) { + func moveIndicatorView(index: Int, ratio: CGFloat, direction: Direction) { update(index) guard let currentItem else { return } - if options.additionView.isAnimationOnSwipeEnabled { + if options.indicatorView.isAnimationOnSwipeEnabled { switch direction { case .forward: if let nextItem { - additionView.frame.origin.x = currentItem.frame.origin.x + (nextItem.frame.origin.x - currentItem.frame.origin.x) * ratio + options.additionView.padding.left - additionView.frame.size.width = currentItem.frame.size.width + (nextItem.frame.size.width - currentItem.frame.size.width) * ratio - options.additionView.padding.horizontal + 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 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) @@ -444,8 +444,8 @@ extension TabView { } case .reverse: if let previousItem { - additionView.frame.origin.x = previousItem.frame.origin.x + (currentItem.frame.origin.x - previousItem.frame.origin.x) * ratio + options.additionView.padding.left - additionView.frame.size.width = previousItem.frame.size.width + (currentItem.frame.size.width - previousItem.frame.size.width) * ratio - options.additionView.padding.horizontal + 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 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) @@ -460,7 +460,7 @@ extension TabView { updateSelectedItem(by: currentIndex) } - focus(on: additionView, animated: false) + focus(on: indicatorView, animated: false) } } @@ -484,9 +484,9 @@ extension TabView { guard let currentItem else { return } - if options.addition == .underline { - additionView.frame.origin.x = currentItem.frame.origin.x + options.additionView.padding.left - additionView.frame.size.width = currentItem.frame.size.width - options.additionView.padding.horizontal + if options.indicator == .underline { + indicatorView.frame.origin.x = currentItem.frame.origin.x + options.indicatorView.padding.left + indicatorView.frame.size.width = currentItem.frame.size.width - options.indicatorView.padding.horizontal } focus(on: currentItem, animated: false) @@ -522,9 +522,9 @@ extension TabView { private func moveTabItem(index: Int, animated: Bool) { - switch options.addition { + switch options.indicator { case .underline, .circle: - animateAdditionView(index: index, animated: animated, completion: nil) + animateIndicatorView(index: index, animated: animated, completion: nil) case .none: update(index) } diff --git a/Tests/SwipeMenuViewControllerTests/SwipeMenuViewOptionsTests.swift b/Tests/SwipeMenuViewControllerTests/SwipeMenuViewOptionsTests.swift index 754e0a5..ba774fc 100644 --- a/Tests/SwipeMenuViewControllerTests/SwipeMenuViewOptionsTests.swift +++ b/Tests/SwipeMenuViewControllerTests/SwipeMenuViewOptionsTests.swift @@ -19,7 +19,7 @@ struct SwipeMenuViewOptionsTests { #expect(options.tabView.height == 44.0) #expect(options.tabView.margin == 0.0) #expect(options.tabView.style == .flexible) - #expect(options.tabView.addition == .underline) + #expect(options.tabView.indicator == .underline) #expect(options.tabView.adjustsItemViewWidth == true) #expect(options.tabView.interpolatesTextColorOnSwipe == true) #expect(options.tabView.isSafeAreaEnabled == true) @@ -38,15 +38,15 @@ struct SwipeMenuViewOptionsTests { #expect(options.tabView.itemView.numberOfLines == 1) } - @Test("AdditionView documented defaults") - func additionViewDefaults() { + @Test("IndicatorView documented defaults") + func indicatorViewDefaults() { let options = SwipeMenuViewOptions() - #expect(options.tabView.additionView.underline.height == 2.0) - #expect(options.tabView.additionView.underline.cornerRadius == 0) - #expect(options.tabView.additionView.animationDuration == 0.3) - #expect(options.tabView.additionView.isAnimationOnSwipeEnabled == true) - #expect(options.tabView.additionView.padding == .zero) + #expect(options.tabView.indicatorView.underline.height == 2.0) + #expect(options.tabView.indicatorView.underline.cornerRadius == 0) + #expect(options.tabView.indicatorView.animationDuration == 0.3) + #expect(options.tabView.indicatorView.isAnimationOnSwipeEnabled == true) + #expect(options.tabView.indicatorView.padding == .zero) } @Test("ContentScrollView documented defaults") diff --git a/Tests/SwipeMenuViewControllerTests/TabViewTests.swift b/Tests/SwipeMenuViewControllerTests/TabViewTests.swift index 0fea6e6..21a8c07 100644 --- a/Tests/SwipeMenuViewControllerTests/TabViewTests.swift +++ b/Tests/SwipeMenuViewControllerTests/TabViewTests.swift @@ -250,10 +250,10 @@ struct TabViewTests { } /// Counts the plain (non-`TabItemView`) `UIView` subviews inside the tab's - /// container stack view. The underline/circle addition view is added there - /// as a plain `UIView`; the item views are `TabItemView`s. `additionView` + /// container stack view. The underline/circle indicator view is added there + /// as a plain `UIView`; the item views are `TabItemView`s. `indicatorView` /// itself is `private`, so we detect it through the view hierarchy. - private func additionViewCount(in tabView: TabView) -> Int { + private func indicatorViewCount(in tabView: TabView) -> Int { guard let container = tabView.subviews.first(where: { $0 is UIStackView }) else { return 0 } @@ -262,54 +262,54 @@ struct TabViewTests { /// Returns the plain (non-`TabItemView`) `UIView` acting as the selection /// indicator inside the container stack view, or `nil` if there is none. - /// `additionView` is `private`, so it is located through the hierarchy. - private func additionView(in tabView: TabView) -> UIView? { + /// `indicatorView` is `private`, so it is located through the hierarchy. + private func indicatorView(in tabView: TabView) -> UIView? { guard let container = tabView.subviews.first(where: { $0 is UIStackView }) else { return nil } return container.subviews.first { type(of: $0) == UIView.self } } - @Test("Underline addition produces an addition view in the hierarchy") - func underlineAdditionExists() { + @Test("Underline indicator produces an indicator view in the hierarchy") + func underlineIndicatorExists() { var options = SwipeMenuViewOptions.TabView() - options.addition = .underline + options.indicator = .underline let (tabView, dataSource) = makeTabView(titles: ["A", "B", "C"], options: options) let window = hostTabView(tabView) defer { withExtendedLifetime((window, dataSource)) {} } - // The underline addition view is added to the container view hierarchy. - #expect(additionViewCount(in: tabView) == 1) + // The underline indicator view is added to the container view hierarchy. + #expect(indicatorViewCount(in: tabView) == 1) } - @Test("Circle addition produces an addition view in the hierarchy") - func circleAdditionExists() { + @Test("Circle indicator produces an indicator view in the hierarchy") + func circleIndicatorExists() { var options = SwipeMenuViewOptions.TabView() - options.addition = .circle + options.indicator = .circle let (tabView, dataSource) = makeTabView(titles: ["A", "B", "C"], options: options) let window = hostTabView(tabView) defer { withExtendedLifetime((window, dataSource)) {} } - // The circle addition view is added to the container view hierarchy. - #expect(additionViewCount(in: tabView) == 1) + // The circle indicator view is added to the container view hierarchy. + #expect(indicatorViewCount(in: tabView) == 1) } - @Test("No addition leaves the addition view out of the hierarchy") - func noAdditionHasNoAdditionView() { + @Test("No indicator leaves the indicator view out of the hierarchy") + func noIndicatorHasNoIndicatorView() { var options = SwipeMenuViewOptions.TabView() - options.addition = .none + options.indicator = .none let (tabView, dataSource) = makeTabView(titles: ["A", "B", "C"], options: options) let window = hostTabView(tabView) defer { withExtendedLifetime((window, dataSource)) {} } - // With `.none`, the addition view is never added to the container. - #expect(additionViewCount(in: tabView) == 0) + // With `.none`, the indicator view is never added to the container. + #expect(indicatorViewCount(in: tabView) == 0) } // MARK: - Item fonts @@ -378,31 +378,31 @@ struct TabViewTests { @Test("The underline indicator has square corners by default") func underlineCornerRadiusDefaultsToSquare() throws { var options = SwipeMenuViewOptions.TabView() - options.addition = .underline + options.indicator = .underline let (tabView, dataSource) = makeTabView(titles: ["A", "B", "C"], options: options) let window = hostTabView(tabView) defer { withExtendedLifetime((window, dataSource)) {} } - let indicator = try #require(additionView(in: tabView)) + let indicator = try #require(indicatorView(in: tabView)) #expect(indicator.layer.cornerRadius == 0) } @Test("underline.cornerRadius is applied to the indicator layer") func underlineCornerRadiusIsApplied() throws { var options = SwipeMenuViewOptions.TabView() - options.addition = .underline - options.additionView.underline.height = 4 + options.indicator = .underline + options.indicatorView.underline.height = 4 // Half the height rounds the underline into a pill. - options.additionView.underline.cornerRadius = 2 + options.indicatorView.underline.cornerRadius = 2 let (tabView, dataSource) = makeTabView(titles: ["A", "B", "C"], options: options) let window = hostTabView(tabView) defer { withExtendedLifetime((window, dataSource)) {} } - let indicator = try #require(additionView(in: tabView)) + let indicator = try #require(indicatorView(in: tabView)) #expect(indicator.layer.cornerRadius == 2) } @@ -412,11 +412,11 @@ struct TabViewTests { func segmentedIndicatorAlignsWithEachTab() throws { var options = SwipeMenuViewOptions.TabView() options.style = .segmented - options.addition = .underline + options.indicator = .underline options.margin = 0 // Non-zero horizontal padding makes both the offset sign (first tab) and // the per-tab stride (later tabs) observable. - options.additionView.padding = UIEdgeInsets(top: 0, left: 20, bottom: 0, right: 20) + options.indicatorView.padding = UIEdgeInsets(top: 0, left: 20, bottom: 0, right: 20) let titles = ["A", "B", "C"] let (tabView, dataSource) = makeTabView(titles: titles, options: options) @@ -424,8 +424,8 @@ struct TabViewTests { let window = hostTabView(tabView, width: 375) defer { withExtendedLifetime((window, dataSource)) {} } - let indicator = try #require(additionView(in: tabView)) - let padding = options.additionView.padding + let indicator = try #require(indicatorView(in: tabView)) + let padding = options.indicatorView.padding // The indicator should align with the selected tab's frame, inset by the // padding — the same way the flexible style aligns it. Before the fix the @@ -477,7 +477,7 @@ struct TabViewTests { // `nextItem` returned the current item and its label was overwritten // with an interpolated (mid-fade) color, causing a flicker. Post-fix the // branch is skipped, so the label keeps the selected color. - tabView.moveAdditionView(index: 2, ratio: 0.3, direction: .forward) + tabView.moveIndicatorView(index: 2, ratio: 0.3, direction: .forward) let actual = lastItem.titleLabel.textColor ?? .clear #expect(colorsEqual(actual, expectedColor)) @@ -498,7 +498,7 @@ struct TabViewTests { tabView.jump(to: 0) // Halfway through a forward swipe from item 0 to item 1. - tabView.moveAdditionView(index: 0, ratio: 0.5, direction: .forward) + tabView.moveIndicatorView(index: 0, ratio: 0.5, direction: .forward) let midGray = UIColor(white: 0.5, alpha: 1) let current = tabView.itemViews[0].titleLabel.textColor ?? .clear