diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ca2d981..341ac46 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -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 }}" diff --git a/Package.swift b/Package.swift index 10754d8..59b0daa 100644 --- a/Package.swift +++ b/Package.swift @@ -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( diff --git a/README.md b/README.md index 91fea18..1ef19aa 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/Sample/Sample.xcodeproj/project.pbxproj b/Sample/Sample.xcodeproj/project.pbxproj index 791882e..602b063 100644 --- a/Sample/Sample.xcodeproj/project.pbxproj +++ b/Sample/Sample.xcodeproj/project.pbxproj @@ -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 = ( diff --git a/Sample/Sample/ContentView.swift b/Sample/Sample/ContentView.swift index ccbd7b4..7c44464 100644 --- a/Sample/Sample/ContentView.swift +++ b/Sample/Sample/ContentView.swift @@ -49,8 +49,6 @@ struct ContentView: View { } } -struct ContentView_Previews: PreviewProvider { - static var previews: some View { - ContentView() - } +#Preview { + ContentView() } diff --git a/Sources/LongPressButton/LongPressButton.swift b/Sources/LongPressButton/LongPressButton.swift index 9732751..fa839a5 100644 --- a/Sources/LongPressButton/LongPressButton.swift +++ b/Sources/LongPressButton/LongPressButton.swift @@ -5,32 +5,68 @@ public struct LongPressButton: 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? 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 } @@ -41,10 +77,8 @@ public struct LongPressButton: View { } catch { return } - await MainActor.run { - didLongPress = true - longPressAction() - } + didLongPress = true + longPressAction() } } } @@ -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 @@ -73,6 +109,7 @@ extension LongPressButton { self.maximumDistance = maximumDistance self.action = action self.longPressAction = longPressAction + self.longPressActionName = longPressActionName self.label = label() } @@ -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 ) { @@ -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( _ 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 ) {