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
22 changes: 22 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,28 @@ concurrency:
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}

jobs:
format:
name: swift-format
runs-on: macos-26
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
- name: Select Xcode
# Use the same toolchain as the build jobs so the bundled swift-format
# version matches what contributors run locally.
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: '^26.0'
- name: Lint
run: swift format lint --strict --recursive --parallel Sources Tests Example/Example Example/ExampleTests Package.swift
- name: Verify formatting
run: |
set -euo pipefail
swift format --in-place --recursive --parallel Sources Tests Example/Example Example/ExampleTests Package.swift
# Fail if the formatter produced any change, and surface the diff so
# the required fix is obvious from the log.
git diff --exit-code

package:
name: Package tests (iOS Simulator)
runs-on: macos-26
Expand Down
12 changes: 12 additions & 0 deletions .swift-format
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"version": 1,
"lineLength": 160,
"indentation": {
"spaces": 4
},
"maximumBlankLines": 1,
"respectsExistingLineBreaks": true,
"lineBreakBeforeControlFlowKeywords": false,
"lineBreakBeforeEachArgument": false,
"prioritizeKeepingFunctionOutputTogether": true
}
18 changes: 11 additions & 7 deletions Example/Example/MenuViewController.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import UIKit
import SwipeMenuViewController
import UIKit

/// The example's root screen.
///
Expand All @@ -10,7 +10,7 @@ final class MenuViewController: SwipeMenuViewController {

private let pageTitles = [
"Bulbasaur", "Caterpie", "Golem", "Jynx",
"Marshtomp", "Salamence", "Riolu", "Araquanid"
"Marshtomp", "Salamence", "Riolu", "Araquanid",
]

private var settings = SwipeMenuSettings()
Expand All @@ -22,9 +22,11 @@ final class MenuViewController: SwipeMenuViewController {
configuration.image = UIImage(systemName: "gearshape")
configuration.cornerStyle = .capsule
configuration.preferredSymbolConfigurationForImage = UIImage.SymbolConfiguration(pointSize: 22, weight: .medium)
let button = UIButton(configuration: configuration, primaryAction: UIAction { [weak self] _ in
self?.presentOptions()
})
let button = UIButton(
configuration: configuration,
primaryAction: UIAction { [weak self] _ in
self?.presentOptions()
})
button.accessibilityLabel = "Options"
button.translatesAutoresizingMaskIntoConstraints = false
return button
Expand All @@ -33,7 +35,9 @@ final class MenuViewController: SwipeMenuViewController {
private static let settingsButtonDiameter: CGFloat = 56

override func viewDidLoad() {
pageTitles.forEach { addChild(PageViewController(title: $0)) }
for pageTitle in pageTitles {
addChild(PageViewController(title: pageTitle))
}

super.viewDidLoad()

Expand Down Expand Up @@ -62,7 +66,7 @@ final class MenuViewController: SwipeMenuViewController {
settingsButton.widthAnchor.constraint(equalToConstant: Self.settingsButtonDiameter),
settingsButton.heightAnchor.constraint(equalToConstant: Self.settingsButtonDiameter),
settingsButton.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -16),
settingsButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -16)
settingsButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -16),
])
}

Expand Down
95 changes: 51 additions & 44 deletions Example/Example/OptionsViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ final class OptionsViewController: UIViewController {
adjustWidthRow,
itemWidthRow,
makeSliderRow(title: "Tab margin", valueLabel: tabMarginValueLabel, slider: tabMarginSlider),
makeToggleRow(title: "Swipe between pages", toggle: contentScrollSwitch)
makeToggleRow(title: "Swipe between pages", toggle: contentScrollSwitch),
]

let contentStack = UIStackView(arrangedSubviews: rows)
Expand All @@ -104,59 +104,66 @@ final class OptionsViewController: UIViewController {
contentStack.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor),
contentStack.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor),
contentStack.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor),
contentStack.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor)
contentStack.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor),
])
}

// MARK: - Actions

private func setUpActions() {
pageCountStepper.addAction(UIAction { [weak self] _ in
guard let self else { return }
settings.pageCount = Int(pageCountStepper.value)
pageCountValueLabel.text = "\(settings.pageCount)"
notifyChange()
}, for: .valueChanged)

styleControl.addAction(UIAction { [weak self] _ in
guard let self else { return }
settings.setStyle(styleControl.selectedSegmentIndex == 0 ? .flexible : .segmented)
updateControls()
notifyChange()
}, for: .valueChanged)

decorationControl.addAction(UIAction { [weak self] _ in
guard let self else { return }
settings.tabDecoration = SwipeMenuSettings.TabDecoration.allCases[decorationControl.selectedSegmentIndex]
notifyChange()
}, for: .valueChanged)

adjustWidthSwitch.addAction(UIAction { [weak self] _ in
guard let self else { return }
settings.adjustsItemWidthToFit = adjustWidthSwitch.isOn
updateControls()
notifyChange()
}, for: .valueChanged)

itemWidthSlider.addAction(UIAction { [weak self] _ in
guard let self else { return }
settings.itemWidth = CGFloat(itemWidthSlider.value)
itemWidthValueLabel.text = Self.format(settings.itemWidth)
}, for: .valueChanged)
pageCountStepper.addAction(
UIAction { [weak self] _ in
guard let self else { return }
settings.pageCount = Int(pageCountStepper.value)
pageCountValueLabel.text = "\(settings.pageCount)"
notifyChange()
}, for: .valueChanged)

styleControl.addAction(
UIAction { [weak self] _ in
guard let self else { return }
settings.setStyle(styleControl.selectedSegmentIndex == 0 ? .flexible : .segmented)
updateControls()
notifyChange()
}, for: .valueChanged)

decorationControl.addAction(
UIAction { [weak self] _ in
guard let self else { return }
settings.tabDecoration = SwipeMenuSettings.TabDecoration.allCases[decorationControl.selectedSegmentIndex]
notifyChange()
}, for: .valueChanged)

adjustWidthSwitch.addAction(
UIAction { [weak self] _ in
guard let self else { return }
settings.adjustsItemWidthToFit = adjustWidthSwitch.isOn
updateControls()
notifyChange()
}, for: .valueChanged)

itemWidthSlider.addAction(
UIAction { [weak self] _ in
guard let self else { return }
settings.itemWidth = CGFloat(itemWidthSlider.value)
itemWidthValueLabel.text = Self.format(settings.itemWidth)
}, for: .valueChanged)
itemWidthSlider.addAction(UIAction { [weak self] _ in self?.notifyChange() }, for: [.touchUpInside, .touchUpOutside])

tabMarginSlider.addAction(UIAction { [weak self] _ in
guard let self else { return }
settings.tabMargin = CGFloat(tabMarginSlider.value)
tabMarginValueLabel.text = Self.format(settings.tabMargin)
}, for: .valueChanged)
tabMarginSlider.addAction(
UIAction { [weak self] _ in
guard let self else { return }
settings.tabMargin = CGFloat(tabMarginSlider.value)
tabMarginValueLabel.text = Self.format(settings.tabMargin)
}, for: .valueChanged)
tabMarginSlider.addAction(UIAction { [weak self] _ in self?.notifyChange() }, for: [.touchUpInside, .touchUpOutside])

contentScrollSwitch.addAction(UIAction { [weak self] _ in
guard let self else { return }
settings.isContentScrollEnabled = contentScrollSwitch.isOn
notifyChange()
}, for: .valueChanged)
contentScrollSwitch.addAction(
UIAction { [weak self] _ in
guard let self else { return }
settings.isContentScrollEnabled = contentScrollSwitch.isOn
notifyChange()
}, for: .valueChanged)
}

private func reset() {
Expand Down
2 changes: 1 addition & 1 deletion Example/Example/PageViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ final class PageViewController: UIViewController {
label.centerXAnchor.constraint(equalTo: view.centerXAnchor),
label.centerYAnchor.constraint(equalTo: view.centerYAnchor),
label.leadingAnchor.constraint(greaterThanOrEqualTo: view.leadingAnchor, constant: 16),
label.trailingAnchor.constraint(lessThanOrEqualTo: view.trailingAnchor, constant: -16)
label.trailingAnchor.constraint(lessThanOrEqualTo: view.trailingAnchor, constant: -16),
])
}
}
2 changes: 1 addition & 1 deletion Example/Example/SwipeMenuSettings.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import UIKit
import SwipeMenuViewController
import UIKit

/// The user-adjustable configuration behind the example's swipe menu.
///
Expand Down
3 changes: 2 additions & 1 deletion Example/ExampleTests/SwipeMenuSettingsTests.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import SwipeMenuViewController
import Testing
import UIKit
import SwipeMenuViewController

@testable import Example

@Suite("SwipeMenuSettings")
Expand Down
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,6 @@ let package = Package(
swiftSettings: [
.defaultIsolation(MainActor.self)
]
)
),
]
)
9 changes: 6 additions & 3 deletions Sources/SwipeMenuViewController/ContentScrollView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,9 @@ open class ContentScrollView: UIScrollView {
guard superview != nil else { return }
// Discard the previous pages first so a reload replaces them instead of
// stacking a second set of page views into the hierarchy.
pageViews.forEach { $0.removeFromSuperview() }
for pageView in pageViews {
pageView.removeFromSuperview()
}
pageViews = []
setup()
}
Expand All @@ -98,7 +100,8 @@ open class ContentScrollView: UIScrollView {
private func setup() {

guard let dataSource,
dataSource.numberOfPages(in: self) > 0 else { return }
dataSource.numberOfPages(in: self) > 0
else { return }

setupScrollView()
setupPages()
Expand Down Expand Up @@ -141,7 +144,7 @@ open class ContentScrollView: UIScrollView {
pageView.topAnchor.constraint(equalTo: self.topAnchor),
pageView.widthAnchor.constraint(equalTo: self.widthAnchor),
pageView.heightAnchor.constraint(equalTo: self.heightAnchor),
pageView.leadingAnchor.constraint(equalTo: leadingAnchor)
pageView.leadingAnchor.constraint(equalTo: leadingAnchor),
])
}
}
Expand Down
23 changes: 14 additions & 9 deletions Sources/SwipeMenuViewController/SwipeMenuView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,10 @@ import UIKit
}

extension SwipeMenuViewDelegate {
public func swipeMenuView(_ swipeMenuView: SwipeMenuView, viewWillSetupAt currentIndex: Int) { }
public func swipeMenuView(_ swipeMenuView: SwipeMenuView, viewDidSetupAt currentIndex: Int) { }
public func swipeMenuView(_ swipeMenuView: SwipeMenuView, willChangeIndexFrom fromIndex: Int, to toIndex: Int) { }
public func swipeMenuView(_ swipeMenuView: SwipeMenuView, didChangeIndexFrom fromIndex: Int, to toIndex: Int) { }
public func swipeMenuView(_ swipeMenuView: SwipeMenuView, viewWillSetupAt currentIndex: Int) {}
public func swipeMenuView(_ swipeMenuView: SwipeMenuView, viewDidSetupAt currentIndex: Int) {}
public func swipeMenuView(_ swipeMenuView: SwipeMenuView, willChangeIndexFrom fromIndex: Int, to toIndex: Int) {}
public func swipeMenuView(_ swipeMenuView: SwipeMenuView, didChangeIndexFrom fromIndex: Int, to toIndex: Int) {}
}

// MARK: - SwipeMenuViewDataSource
Expand Down Expand Up @@ -299,7 +299,12 @@ 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 All @@ -317,8 +322,8 @@ open class SwipeMenuView: UIView {
tabView.topAnchor.constraint(equalTo: self.topAnchor),
tabView.leadingAnchor.constraint(equalTo: self.leadingAnchor),
tabView.trailingAnchor.constraint(equalTo: self.trailingAnchor),
tabView.heightAnchor.constraint(equalToConstant: options.tabView.height)
])
tabView.heightAnchor.constraint(equalToConstant: options.tabView.height),
])
}

private func layout(contentScrollView: ContentScrollView) {
Expand All @@ -329,8 +334,8 @@ open class SwipeMenuView: UIView {
contentScrollView.topAnchor.constraint(equalTo: self.topAnchor, constant: options.tabView.height),
contentScrollView.leadingAnchor.constraint(equalTo: self.leadingAnchor),
contentScrollView.trailingAnchor.constraint(equalTo: self.trailingAnchor),
contentScrollView.bottomAnchor.constraint(equalTo: self.bottomAnchor)
])
contentScrollView.bottomAnchor.constraint(equalTo: self.bottomAnchor),
])
}

private func reset() {
Expand Down
19 changes: 12 additions & 7 deletions Sources/SwipeMenuViewController/SwipeMenuViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,30 +39,31 @@ open class SwipeMenuViewController: UIViewController, SwipeMenuViewDelegate, Swi
// The top anchor is chosen once from the initial options. Toggling
// `options.tabView.isSafeAreaEnabled` at runtime does not re-pin the
// view, so configure the safe area behavior before the view loads.
let topAnchor = swipeMenuView.options.tabView.isSafeAreaEnabled
let topAnchor =
swipeMenuView.options.tabView.isSafeAreaEnabled
? view.safeAreaLayoutGuide.topAnchor
: view.topAnchor
NSLayoutConstraint.activate([
swipeMenuView.topAnchor.constraint(equalTo: topAnchor),
swipeMenuView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
swipeMenuView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
swipeMenuView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
swipeMenuView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
}

// MARK: - SwipeMenuViewDelegate

/// Called before the swipe menu view sets up its views. The default implementation does nothing; override to react.
open func swipeMenuView(_ swipeMenuView: SwipeMenuView, viewWillSetupAt currentIndex: Int) { }
open func swipeMenuView(_ swipeMenuView: SwipeMenuView, viewWillSetupAt currentIndex: Int) {}

/// Called after the swipe menu view has set up its views. The default implementation does nothing; override to react.
open func swipeMenuView(_ swipeMenuView: SwipeMenuView, viewDidSetupAt currentIndex: Int) { }
open func swipeMenuView(_ swipeMenuView: SwipeMenuView, viewDidSetupAt currentIndex: Int) {}

/// Called before the front page changes. The default implementation does nothing; override to react.
open func swipeMenuView(_ swipeMenuView: SwipeMenuView, willChangeIndexFrom fromIndex: Int, to toIndex: Int) { }
open func swipeMenuView(_ swipeMenuView: SwipeMenuView, willChangeIndexFrom fromIndex: Int, to toIndex: Int) {}

/// Called after the front page has changed. The default implementation does nothing; override to react.
open func swipeMenuView(_ swipeMenuView: SwipeMenuView, didChangeIndexFrom fromIndex: Int, to toIndex: Int) { }
open func swipeMenuView(_ swipeMenuView: SwipeMenuView, didChangeIndexFrom fromIndex: Int, to toIndex: Int) {}

// MARK: - SwipeMenuViewDataSource

Expand All @@ -87,7 +88,11 @@ 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
Loading