Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
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
42 changes: 33 additions & 9 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,38 @@ on:
branches: [ main ]

jobs:
# test:
# runs-on: macos-latest
# steps:
# - uses: actions/checkout@v3
# - run: swift test
ui-tests:
name: UI tests (iOS ${{ matrix.config.version }})
runs-on: macos-latest
strategy:
fail-fast: false
matrix:
config:
- version: '16.4'
device: 'iPhone 14'
- version: '17.5'
device: 'iPhone 15'
- version: '18.6'
device: 'iPhone 16'
- version: '26.0'
device: 'iPhone 17'

lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: norio-nomura/action-swiftlint@3.2.1
- name: Checkout
uses: actions/checkout@v4

- name: Prepare runtime
id: check-runtime
run: |
available_runtimes="$(xcrun simctl list runtimes available)"
echo "$available_runtimes"

if echo "$available_runtimes" | grep -q "^iOS ${{ matrix.config.version }}"; then
echo "iOS ${{ matrix.config.version }} runtime is available"
else
sudo xcodes runtimes install "iOS ${{ matrix.config.version }}"
fi

- name: Run tests
run: |
xcodebuild test -project Sample/Sample.xcodeproj -scheme Sample -destination "platform=iOS Simulator,OS=${{ matrix.config.version }},name=${{ matrix.config.device }}"
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import PackageDescription

let package = Package(
name: "LongPressButton",
platforms: [.iOS(.v13)],
platforms: [.iOS(.v15)],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,4 @@ Or add [https://github.com/Tunous/LongPressButton.git](https://github.com/Tunous

## Credits

[Supporting Both Tap and Long Press on a Button in SwiftUI](https://steipete.com/posts/supporting-both-tap-and-longpress-on-button-in-swiftui/) by Peter Steinberger - Great article with few potential solution on how to create button with long press action. Unfortunately none of them worked correctly for my use case.
[Supporting Both Tap and Long Press on a Button in SwiftUI](https://steipete.me/posts/2021/supporting-both-tap-and-longpress-on-button-in-swiftui) by Peter Steinberger - Great article with few potential solution on how to create button with long press action. Unfortunately none of them worked correctly for my use case.
2 changes: 1 addition & 1 deletion Sample/Sample.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@
};
};
buildConfigurationList = 00F7985C28C7B2510039458B /* Build configuration list for PBXProject "Sample" */;
compatibilityVersion = "Xcode 14.0";
compatibilityVersion = "Xcode 13.0";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
Expand Down
6 changes: 2 additions & 4 deletions Sample/Sample/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,6 @@ struct ContentView: View {
}
}

struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
#Preview {
ContentView()
}
69 changes: 56 additions & 13 deletions Sources/LongPressButton/LongPressButton.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,68 @@ public struct LongPressButton<Label: View>: View {
private let minimumDuration: TimeInterval
private let maximumDistance: CGFloat
private let longPressAction: () -> Void
private let action: (() -> Void)?
private let action: () -> Void
private let label: Label
private let longPressActionName: Text?

@available(iOS, obsoleted: 26.0)
@State private var didLongPress = false
@available(iOS, obsoleted: 26.0)
@State private var longPressTask: Task<Void, Never>?

public var body: some View {
Button(action: performActionIfNeeded) {
label
button
.accessibilityAction {
action()
}
.accessibilityAction(named: longPressActionName ?? Text("Alternative Action")) {
longPressAction()
}
}

@ViewBuilder
private var button: some View {
if #available(iOS 26.0, *) {
Button(action: {}) {
label
}
.simultaneousGesture(longPress.exclusively(before: tap))
} else {
Button(action: performActionIfNeeded) {
label
}
.onLongPressGesture(
maximumDistance: maximumDistance,
perform: {},
onPressingChanged: handleLongPress(isPressing:)
)
}
.onLongPressGesture(
maximumDistance: maximumDistance,
perform: {},
onPressingChanged: handleLongPress(isPressing:)
)
}

private var longPress: some Gesture {
LongPressGesture(minimumDuration: minimumDuration, maximumDistance: maximumDistance)
.onEnded { _ in
longPressAction()
}
}

private var tap: some Gesture {
TapGesture().onEnded {
action()
}
}

@available(iOS, obsoleted: 26.0)
private func performActionIfNeeded() {
longPressTask?.cancel()
if didLongPress {
didLongPress = false
} else {
action?()
action()
}
}

@available(iOS, obsoleted: 26.0)
private func handleLongPress(isPressing: Bool) {
longPressTask?.cancel()
guard isPressing else { return }
Expand All @@ -41,10 +77,8 @@ public struct LongPressButton<Label: View>: View {
} catch {
return
}
await MainActor.run {
didLongPress = true
longPressAction()
}
didLongPress = true
longPressAction()
}
}
}
Expand All @@ -61,10 +95,12 @@ extension LongPressButton {
/// the gesture fails.
/// - action: The action to perform when the user taps the button.
/// - longPressAction: The action to perform when the user long presses the button.
/// - longPressActionName: The name used by assistive technologies (such as VoiceOver) for the long-press accessibility action.
/// - label: A view that describes the purpose of the button’s action.
public init(
minimumDuration: TimeInterval = 0.5,
maximumDistance: CGFloat = 10,
longPressActionName: Text? = nil,
action: @escaping () -> Void,
longPressAction: @escaping () -> Void,
@ViewBuilder label: () -> Label
Expand All @@ -73,6 +109,7 @@ extension LongPressButton {
self.maximumDistance = maximumDistance
self.action = action
self.longPressAction = longPressAction
self.longPressActionName = longPressActionName
self.label = label()
}

Expand All @@ -83,18 +120,21 @@ extension LongPressButton {
/// - minimumDuration: The minimum duration of the long press that must elapse before the gesture succeeds.
/// - maximumDistance: The maximum distance that the fingers or cursor performing the long press can move before
/// the gesture fails.
/// - longPressActionName: The name used by assistive technologies (such as VoiceOver) for the long-press accessibility action.
/// - action: The action to perform when the user taps the button.
/// - longPressAction: The action to perform when the user long presses the button.
public init(
_ titleKey: LocalizedStringKey,
minimumDuration: TimeInterval = 0.5,
maximumDistance: CGFloat = 10,
longPressActionName: LocalizedStringKey? = nil,
action: @escaping () -> Void,
longPressAction: @escaping () -> Void
) where Label == Text {
self.init(
minimumDuration: minimumDuration,
maximumDistance: maximumDistance,
longPressActionName: longPressActionName.map { Text($0) },
action: action,
longPressAction: longPressAction
) {
Expand All @@ -109,18 +149,21 @@ extension LongPressButton {
/// - minimumDuration: The minimum duration of the long press that must elapse before the gesture succeeds.
/// - maximumDistance: The maximum distance that the fingers or cursor performing the long press can move before
/// the gesture fails.
/// - longPressActionName: The name used by assistive technologies (such as VoiceOver) for the long-press accessibility action.
/// - action: The action to perform when the user taps the button.
/// - longPressAction: The action to perform when the user long presses the button.
public init<S: StringProtocol>(
_ title: S,
minimumDuration: TimeInterval = 0.5,
maximumDistance: CGFloat = 10,
longPressActionName: S? = nil,
action: @escaping () -> Void,
longPressAction: @escaping () -> Void
) where Label == Text {
self.init(
minimumDuration: minimumDuration,
maximumDistance: maximumDistance,
longPressActionName: longPressActionName.map { Text($0) },
action: action,
longPressAction: longPressAction
) {
Expand Down