From 560d158ecab8c582e696f6cad7ce336418b1a3ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Rutkowski?= Date: Sat, 11 Oct 2025 17:30:12 +0200 Subject: [PATCH 01/24] New implementation that works on iOS 26 The previous solution started triggering taps after long presses in my application on iOS 26. --- README.md | 2 +- Sample/Sample/ContentView.swift | 6 +-- Sources/LongPressButton/LongPressButton.swift | 42 +++++-------------- 3 files changed, 14 insertions(+), 36 deletions(-) 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/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..8ecd57c 100644 --- a/Sources/LongPressButton/LongPressButton.swift +++ b/Sources/LongPressButton/LongPressButton.swift @@ -5,46 +5,26 @@ 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 - @State private var didLongPress = false - @State private var longPressTask: Task? - public var body: some View { - Button(action: performActionIfNeeded) { + Button(action: {}) { label } - .onLongPressGesture( - maximumDistance: maximumDistance, - perform: {}, - onPressingChanged: handleLongPress(isPressing:) - ) - } - - private func performActionIfNeeded() { - longPressTask?.cancel() - if didLongPress { - didLongPress = false - } else { - action?() - } + .simultaneousGesture(longPress.exclusively(before: tap)) } - private func handleLongPress(isPressing: Bool) { - longPressTask?.cancel() - guard isPressing else { return } - didLongPress = false - longPressTask = Task { - do { - try await Task.sleep(nanoseconds: UInt64(minimumDuration * 1_000_000_000)) - } catch { - return - } - await MainActor.run { - didLongPress = true + private var longPress: some Gesture { + LongPressGesture(minimumDuration: minimumDuration, maximumDistance: maximumDistance) + .onEnded { _ in longPressAction() } + } + + private var tap: some Gesture { + TapGesture().onEnded { + action() } } } From 7e1b759ae03aac4595c28c7f725b75ad1cf93559 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Rutkowski?= Date: Sat, 11 Oct 2025 17:35:47 +0200 Subject: [PATCH 02/24] Accessibility actions --- Sources/LongPressButton/LongPressButton.swift | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/Sources/LongPressButton/LongPressButton.swift b/Sources/LongPressButton/LongPressButton.swift index 8ecd57c..b56a309 100644 --- a/Sources/LongPressButton/LongPressButton.swift +++ b/Sources/LongPressButton/LongPressButton.swift @@ -7,11 +7,18 @@ public struct LongPressButton: View { private let longPressAction: () -> Void private let action: () -> Void private let label: Label + private let longPressActionName: Text? public var body: some View { Button(action: {}) { label } + .accessibilityAction { + action() + } + .accessibilityAction(named: longPressActionName ?? Text("Alternative Action")) { + longPressAction() + } .simultaneousGesture(longPress.exclusively(before: tap)) } @@ -41,18 +48,21 @@ 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 + @ViewBuilder label: () -> Label, ) { self.minimumDuration = minimumDuration self.maximumDistance = maximumDistance self.action = action self.longPressAction = longPressAction + self.longPressActionName = longPressActionName self.label = label() } @@ -63,20 +73,23 @@ 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 + longPressAction: @escaping () -> Void, ) where Label == Text { self.init( minimumDuration: minimumDuration, maximumDistance: maximumDistance, + longPressActionName: longPressActionName.map { Text($0) }, action: action, - longPressAction: longPressAction + longPressAction: longPressAction, ) { Text(titleKey) } @@ -89,20 +102,23 @@ 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 + longPressAction: @escaping () -> Void, ) where Label == Text { self.init( minimumDuration: minimumDuration, maximumDistance: maximumDistance, + longPressActionName: longPressActionName.map { Text($0) }, action: action, - longPressAction: longPressAction + longPressAction: longPressAction, ) { Text(title) } From bff0bd55b012003f1f08f4f58fa31f02565d5ce7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Rutkowski?= Date: Sat, 11 Oct 2025 17:42:05 +0200 Subject: [PATCH 03/24] Convert groups to folders --- Sample/Sample.xcodeproj/project.pbxproj | 77 ++++++------------------- 1 file changed, 18 insertions(+), 59 deletions(-) diff --git a/Sample/Sample.xcodeproj/project.pbxproj b/Sample/Sample.xcodeproj/project.pbxproj index 791882e..6711851 100644 --- a/Sample/Sample.xcodeproj/project.pbxproj +++ b/Sample/Sample.xcodeproj/project.pbxproj @@ -3,19 +3,12 @@ archiveVersion = 1; classes = { }; - objectVersion = 56; + objectVersion = 70; objects = { /* Begin PBXBuildFile section */ - 00F7986528C7B2510039458B /* SampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00F7986428C7B2510039458B /* SampleApp.swift */; }; - 00F7986928C7B2520039458B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 00F7986828C7B2520039458B /* Assets.xcassets */; }; - 00F7986C28C7B2520039458B /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 00F7986B28C7B2520039458B /* Preview Assets.xcassets */; }; - 00F7988028C7B2520039458B /* LongPressButtonTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00F7987F28C7B2520039458B /* LongPressButtonTests.swift */; }; 00F7989428C7B2780039458B /* LongPressButton in Frameworks */ = {isa = PBXBuildFile; productRef = 00F7989328C7B2780039458B /* LongPressButton */; }; - 00F7989628C7B2AE0039458B /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00F7989528C7B2AD0039458B /* ContentView.swift */; }; 00F7989928C7BFA70039458B /* XCAppTest in Frameworks */ = {isa = PBXBuildFile; productRef = 00F7989828C7BFA70039458B /* XCAppTest */; }; - 00F7989C28C7C01D0039458B /* ElementIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00F7989B28C7C01D0039458B /* ElementIdentifier.swift */; }; - 00F7989D28C7C0560039458B /* ElementIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00F7989B28C7C01D0039458B /* ElementIdentifier.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -31,15 +24,15 @@ /* Begin PBXFileReference section */ 00C9094B28CC9485006341AF /* LongPressButton */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = LongPressButton; path = ..; sourceTree = ""; }; 00F7986128C7B2510039458B /* Sample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sample.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 00F7986428C7B2510039458B /* SampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleApp.swift; sourceTree = ""; }; - 00F7986828C7B2520039458B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 00F7986B28C7B2520039458B /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 00F7987B28C7B2520039458B /* SampleUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SampleUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 00F7987F28C7B2520039458B /* LongPressButtonTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LongPressButtonTests.swift; sourceTree = ""; }; - 00F7989528C7B2AD0039458B /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; - 00F7989B28C7C01D0039458B /* ElementIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementIdentifier.swift; sourceTree = ""; }; /* End PBXFileReference section */ +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 006210562E9AB21100CB48B8 /* SharedWithTests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = SharedWithTests; sourceTree = ""; }; + 0062105E2E9AB21300CB48B8 /* Sample */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Sample; sourceTree = ""; }; + 006210642E9AB21600CB48B8 /* SampleUITests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = SampleUITests; sourceTree = ""; }; +/* End PBXFileSystemSynchronizedRootGroup section */ + /* Begin PBXFrameworksBuildPhase section */ 00F7985E28C7B2510039458B /* Frameworks */ = { isa = PBXFrameworksBuildPhase; @@ -71,10 +64,10 @@ 00F7985828C7B2510039458B = { isa = PBXGroup; children = ( + 0062105E2E9AB21300CB48B8 /* Sample */, + 006210562E9AB21100CB48B8 /* SharedWithTests */, + 006210642E9AB21600CB48B8 /* SampleUITests */, 00C9094A28CC9485006341AF /* Packages */, - 00F7989A28C7C0100039458B /* SharedWithTests */, - 00F7986328C7B2510039458B /* Sample */, - 00F7987E28C7B2520039458B /* SampleUITests */, 00F7986228C7B2510039458B /* Products */, 00F7989228C7B2780039458B /* Frameworks */, ); @@ -89,33 +82,6 @@ name = Products; sourceTree = ""; }; - 00F7986328C7B2510039458B /* Sample */ = { - isa = PBXGroup; - children = ( - 00F7989528C7B2AD0039458B /* ContentView.swift */, - 00F7986428C7B2510039458B /* SampleApp.swift */, - 00F7986828C7B2520039458B /* Assets.xcassets */, - 00F7986A28C7B2520039458B /* Preview Content */, - ); - path = Sample; - sourceTree = ""; - }; - 00F7986A28C7B2520039458B /* Preview Content */ = { - isa = PBXGroup; - children = ( - 00F7986B28C7B2520039458B /* Preview Assets.xcassets */, - ); - path = "Preview Content"; - sourceTree = ""; - }; - 00F7987E28C7B2520039458B /* SampleUITests */ = { - isa = PBXGroup; - children = ( - 00F7987F28C7B2520039458B /* LongPressButtonTests.swift */, - ); - path = SampleUITests; - sourceTree = ""; - }; 00F7989228C7B2780039458B /* Frameworks */ = { isa = PBXGroup; children = ( @@ -123,14 +89,6 @@ name = Frameworks; sourceTree = ""; }; - 00F7989A28C7C0100039458B /* SharedWithTests */ = { - isa = PBXGroup; - children = ( - 00F7989B28C7C01D0039458B /* ElementIdentifier.swift */, - ); - path = SharedWithTests; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -146,6 +104,10 @@ ); dependencies = ( ); + fileSystemSynchronizedGroups = ( + 006210562E9AB21100CB48B8 /* SharedWithTests */, + 0062105E2E9AB21300CB48B8 /* Sample */, + ); name = Sample; packageProductDependencies = ( 00F7989328C7B2780039458B /* LongPressButton */, @@ -167,6 +129,10 @@ dependencies = ( 00F7987D28C7B2520039458B /* PBXTargetDependency */, ); + fileSystemSynchronizedGroups = ( + 006210562E9AB21100CB48B8 /* SharedWithTests */, + 006210642E9AB21600CB48B8 /* SampleUITests */, + ); name = SampleUITests; packageProductDependencies = ( 00F7989828C7BFA70039458B /* XCAppTest */, @@ -221,8 +187,6 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 00F7986C28C7B2520039458B /* Preview Assets.xcassets in Resources */, - 00F7986928C7B2520039458B /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -240,9 +204,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 00F7986528C7B2510039458B /* SampleApp.swift in Sources */, - 00F7989628C7B2AE0039458B /* ContentView.swift in Sources */, - 00F7989C28C7C01D0039458B /* ElementIdentifier.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -250,8 +211,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 00F7988028C7B2520039458B /* LongPressButtonTests.swift in Sources */, - 00F7989D28C7C0560039458B /* ElementIdentifier.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; From c7471398d7dc726da6a67fb54693df2e011913a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Rutkowski?= Date: Sat, 11 Oct 2025 18:00:47 +0200 Subject: [PATCH 04/24] Bump minimum supported version to iOS 15 --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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( From 718e71530904fe4a19ae47ebaf4c0bf577e78fe2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Rutkowski?= Date: Sat, 11 Oct 2025 18:01:04 +0200 Subject: [PATCH 05/24] Add UI Tests GitHub action --- .github/workflows/main.yml | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ca2d981..97ebbbe 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -7,14 +7,27 @@ on: branches: [ main ] jobs: - # test: - # runs-on: macos-latest - # steps: - # - uses: actions/checkout@v3 - # - run: swift test + ui-tests: + name: UI tests (iOS ${{ matrix.ios.version }}) + runs-on: macos-latest + strategy: + matrix: + ios: + - version: '15.5' + device: 'iPhone 13' + - 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: Run tests + run: | + xcodebuild test -project Sample/Sample.xcodeproj -scheme Sample -destination "platform=iOS Simulator,OS=${{ matrix.ios.version }},name=${{ matrix.ios.device }}" From 1e2892950c2a2810fd3fd7c9117fd92feae75698 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Rutkowski?= Date: Sat, 11 Oct 2025 18:17:23 +0200 Subject: [PATCH 06/24] Use different system version for each simulator --- .github/workflows/main.yml | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 97ebbbe..ca5ddb1 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -8,21 +8,26 @@ on: jobs: ui-tests: - name: UI tests (iOS ${{ matrix.ios.version }}) - runs-on: macos-latest + name: UI tests (iOS ${{ matrix.config.version }}) + runs-on: ${{ matrix.config.system }} strategy: matrix: - ios: - - version: '15.5' + config: + - version: '15.2' device: 'iPhone 13' - - version: '16.4' + system: macos-11 + - version: '16.2' device: 'iPhone 14' - - version: '17.5' + system: macos-12 + - version: '17.2' device: 'iPhone 15' - - version: '18.6' + system: macos-13 + - version: '18.2' device: 'iPhone 16' + system: macos-14 - version: '26.0' device: 'iPhone 17' + system: macos-latest steps: - name: Checkout @@ -30,4 +35,4 @@ jobs: - name: Run tests run: | - xcodebuild test -project Sample/Sample.xcodeproj -scheme Sample -destination "platform=iOS Simulator,OS=${{ matrix.ios.version }},name=${{ matrix.ios.device }}" + xcodebuild test -project Sample/Sample.xcodeproj -scheme Sample -destination "platform=iOS Simulator,OS=${{ matrix.config.version }},name=${{ matrix.config.device }}" From 3ab5598e32d1b3938adea4d772e86e08073821bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Rutkowski?= Date: Sat, 11 Oct 2025 18:22:20 +0200 Subject: [PATCH 07/24] Lower Xcode project version --- Sample/Sample.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sample/Sample.xcodeproj/project.pbxproj b/Sample/Sample.xcodeproj/project.pbxproj index 6711851..6e457f7 100644 --- a/Sample/Sample.xcodeproj/project.pbxproj +++ b/Sample/Sample.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 70; + objectVersion = 55; objects = { /* Begin PBXBuildFile section */ @@ -161,7 +161,7 @@ }; }; buildConfigurationList = 00F7985C28C7B2510039458B /* Build configuration list for PBXProject "Sample" */; - compatibilityVersion = "Xcode 14.0"; + compatibilityVersion = "Xcode 13.0"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( From e4ed0e64b5c780da6fee04e556992d5dec59d1f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Rutkowski?= Date: Sat, 11 Oct 2025 18:24:01 +0200 Subject: [PATCH 08/24] Revert "Convert groups to folders" This reverts commit bff0bd55b012003f1f08f4f58fa31f02565d5ce7. # Conflicts: # Sample/Sample.xcodeproj/project.pbxproj --- Sample/Sample.xcodeproj/project.pbxproj | 77 +++++++++++++++++++------ 1 file changed, 59 insertions(+), 18 deletions(-) diff --git a/Sample/Sample.xcodeproj/project.pbxproj b/Sample/Sample.xcodeproj/project.pbxproj index 6e457f7..602b063 100644 --- a/Sample/Sample.xcodeproj/project.pbxproj +++ b/Sample/Sample.xcodeproj/project.pbxproj @@ -3,12 +3,19 @@ archiveVersion = 1; classes = { }; - objectVersion = 55; + objectVersion = 56; objects = { /* Begin PBXBuildFile section */ + 00F7986528C7B2510039458B /* SampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00F7986428C7B2510039458B /* SampleApp.swift */; }; + 00F7986928C7B2520039458B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 00F7986828C7B2520039458B /* Assets.xcassets */; }; + 00F7986C28C7B2520039458B /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 00F7986B28C7B2520039458B /* Preview Assets.xcassets */; }; + 00F7988028C7B2520039458B /* LongPressButtonTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00F7987F28C7B2520039458B /* LongPressButtonTests.swift */; }; 00F7989428C7B2780039458B /* LongPressButton in Frameworks */ = {isa = PBXBuildFile; productRef = 00F7989328C7B2780039458B /* LongPressButton */; }; + 00F7989628C7B2AE0039458B /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00F7989528C7B2AD0039458B /* ContentView.swift */; }; 00F7989928C7BFA70039458B /* XCAppTest in Frameworks */ = {isa = PBXBuildFile; productRef = 00F7989828C7BFA70039458B /* XCAppTest */; }; + 00F7989C28C7C01D0039458B /* ElementIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00F7989B28C7C01D0039458B /* ElementIdentifier.swift */; }; + 00F7989D28C7C0560039458B /* ElementIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00F7989B28C7C01D0039458B /* ElementIdentifier.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -24,15 +31,15 @@ /* Begin PBXFileReference section */ 00C9094B28CC9485006341AF /* LongPressButton */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = LongPressButton; path = ..; sourceTree = ""; }; 00F7986128C7B2510039458B /* Sample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 00F7986428C7B2510039458B /* SampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleApp.swift; sourceTree = ""; }; + 00F7986828C7B2520039458B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 00F7986B28C7B2520039458B /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 00F7987B28C7B2520039458B /* SampleUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SampleUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 00F7987F28C7B2520039458B /* LongPressButtonTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LongPressButtonTests.swift; sourceTree = ""; }; + 00F7989528C7B2AD0039458B /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + 00F7989B28C7C01D0039458B /* ElementIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementIdentifier.swift; sourceTree = ""; }; /* End PBXFileReference section */ -/* Begin PBXFileSystemSynchronizedRootGroup section */ - 006210562E9AB21100CB48B8 /* SharedWithTests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = SharedWithTests; sourceTree = ""; }; - 0062105E2E9AB21300CB48B8 /* Sample */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Sample; sourceTree = ""; }; - 006210642E9AB21600CB48B8 /* SampleUITests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = SampleUITests; sourceTree = ""; }; -/* End PBXFileSystemSynchronizedRootGroup section */ - /* Begin PBXFrameworksBuildPhase section */ 00F7985E28C7B2510039458B /* Frameworks */ = { isa = PBXFrameworksBuildPhase; @@ -64,10 +71,10 @@ 00F7985828C7B2510039458B = { isa = PBXGroup; children = ( - 0062105E2E9AB21300CB48B8 /* Sample */, - 006210562E9AB21100CB48B8 /* SharedWithTests */, - 006210642E9AB21600CB48B8 /* SampleUITests */, 00C9094A28CC9485006341AF /* Packages */, + 00F7989A28C7C0100039458B /* SharedWithTests */, + 00F7986328C7B2510039458B /* Sample */, + 00F7987E28C7B2520039458B /* SampleUITests */, 00F7986228C7B2510039458B /* Products */, 00F7989228C7B2780039458B /* Frameworks */, ); @@ -82,6 +89,33 @@ name = Products; sourceTree = ""; }; + 00F7986328C7B2510039458B /* Sample */ = { + isa = PBXGroup; + children = ( + 00F7989528C7B2AD0039458B /* ContentView.swift */, + 00F7986428C7B2510039458B /* SampleApp.swift */, + 00F7986828C7B2520039458B /* Assets.xcassets */, + 00F7986A28C7B2520039458B /* Preview Content */, + ); + path = Sample; + sourceTree = ""; + }; + 00F7986A28C7B2520039458B /* Preview Content */ = { + isa = PBXGroup; + children = ( + 00F7986B28C7B2520039458B /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; + 00F7987E28C7B2520039458B /* SampleUITests */ = { + isa = PBXGroup; + children = ( + 00F7987F28C7B2520039458B /* LongPressButtonTests.swift */, + ); + path = SampleUITests; + sourceTree = ""; + }; 00F7989228C7B2780039458B /* Frameworks */ = { isa = PBXGroup; children = ( @@ -89,6 +123,14 @@ name = Frameworks; sourceTree = ""; }; + 00F7989A28C7C0100039458B /* SharedWithTests */ = { + isa = PBXGroup; + children = ( + 00F7989B28C7C01D0039458B /* ElementIdentifier.swift */, + ); + path = SharedWithTests; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -104,10 +146,6 @@ ); dependencies = ( ); - fileSystemSynchronizedGroups = ( - 006210562E9AB21100CB48B8 /* SharedWithTests */, - 0062105E2E9AB21300CB48B8 /* Sample */, - ); name = Sample; packageProductDependencies = ( 00F7989328C7B2780039458B /* LongPressButton */, @@ -129,10 +167,6 @@ dependencies = ( 00F7987D28C7B2520039458B /* PBXTargetDependency */, ); - fileSystemSynchronizedGroups = ( - 006210562E9AB21100CB48B8 /* SharedWithTests */, - 006210642E9AB21600CB48B8 /* SampleUITests */, - ); name = SampleUITests; packageProductDependencies = ( 00F7989828C7BFA70039458B /* XCAppTest */, @@ -187,6 +221,8 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 00F7986C28C7B2520039458B /* Preview Assets.xcassets in Resources */, + 00F7986928C7B2520039458B /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -204,6 +240,9 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 00F7986528C7B2510039458B /* SampleApp.swift in Sources */, + 00F7989628C7B2AE0039458B /* ContentView.swift in Sources */, + 00F7989C28C7C01D0039458B /* ElementIdentifier.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -211,6 +250,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 00F7988028C7B2520039458B /* LongPressButtonTests.swift in Sources */, + 00F7989D28C7C0560039458B /* ElementIdentifier.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; From 796b71ba3d3e68cdda384c680ea9cc1cc531e631 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Rutkowski?= Date: Sat, 11 Oct 2025 18:28:38 +0200 Subject: [PATCH 09/24] Remove trailing commas --- Sources/LongPressButton/LongPressButton.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/LongPressButton/LongPressButton.swift b/Sources/LongPressButton/LongPressButton.swift index b56a309..c478048 100644 --- a/Sources/LongPressButton/LongPressButton.swift +++ b/Sources/LongPressButton/LongPressButton.swift @@ -56,7 +56,7 @@ extension LongPressButton { longPressActionName: Text? = nil, action: @escaping () -> Void, longPressAction: @escaping () -> Void, - @ViewBuilder label: () -> Label, + @ViewBuilder label: () -> Label ) { self.minimumDuration = minimumDuration self.maximumDistance = maximumDistance @@ -82,14 +82,14 @@ extension LongPressButton { maximumDistance: CGFloat = 10, longPressActionName: LocalizedStringKey? = nil, action: @escaping () -> Void, - longPressAction: @escaping () -> Void, + longPressAction: @escaping () -> Void ) where Label == Text { self.init( minimumDuration: minimumDuration, maximumDistance: maximumDistance, longPressActionName: longPressActionName.map { Text($0) }, action: action, - longPressAction: longPressAction, + longPressAction: longPressAction ) { Text(titleKey) } @@ -111,14 +111,14 @@ extension LongPressButton { maximumDistance: CGFloat = 10, longPressActionName: S? = nil, action: @escaping () -> Void, - longPressAction: @escaping () -> Void, + longPressAction: @escaping () -> Void ) where Label == Text { self.init( minimumDuration: minimumDuration, maximumDistance: maximumDistance, longPressActionName: longPressActionName.map { Text($0) }, action: action, - longPressAction: longPressAction, + longPressAction: longPressAction ) { Text(title) } From c546fbe057966f57d8e01ac840bec0aae4a008cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Rutkowski?= Date: Sat, 11 Oct 2025 18:47:56 +0200 Subject: [PATCH 10/24] Bring back previous implementation on older versions --- Sources/LongPressButton/LongPressButton.swift | 67 ++++++++++++++++--- 1 file changed, 58 insertions(+), 9 deletions(-) diff --git a/Sources/LongPressButton/LongPressButton.swift b/Sources/LongPressButton/LongPressButton.swift index c478048..2bcc28a 100644 --- a/Sources/LongPressButton/LongPressButton.swift +++ b/Sources/LongPressButton/LongPressButton.swift @@ -9,17 +9,38 @@ public struct LongPressButton: View { 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: {}) { - label - } - .accessibilityAction { - action() - } - .accessibilityAction(named: longPressActionName ?? Text("Alternative Action")) { - longPressAction() + 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:) + ) } - .simultaneousGesture(longPress.exclusively(before: tap)) } private var longPress: some Gesture { @@ -34,6 +55,34 @@ public struct LongPressButton: View { action() } } + + @available(iOS, obsoleted: 26.0) + private func performActionIfNeeded() { + longPressTask?.cancel() + if didLongPress { + didLongPress = false + } else { + action() + } + } + + @available(iOS, obsoleted: 26.0) + private func handleLongPress(isPressing: Bool) { + longPressTask?.cancel() + guard isPressing else { return } + didLongPress = false + longPressTask = Task { + do { + try await Task.sleep(nanoseconds: UInt64(minimumDuration * 1_000_000_000)) + } catch { + return + } + await MainActor.run { + didLongPress = true + longPressAction() + } + } + } } // MARK: - Initialization From c68805c8e50c1dac105b6bf5b2559222288db5fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Rutkowski?= Date: Sat, 11 Oct 2025 19:15:03 +0200 Subject: [PATCH 11/24] Run tests on lates macOS only --- .github/workflows/main.yml | 42 ++++++++++++++++++++++++++++++++------ 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ca5ddb1..0b3cad0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -9,30 +9,60 @@ on: jobs: ui-tests: name: UI tests (iOS ${{ matrix.config.version }}) - runs-on: ${{ matrix.config.system }} + runs-on: macos-latest strategy: matrix: config: - version: '15.2' + versioncode: '15-2' device: 'iPhone 13' - system: macos-11 + devicecode: 'iPhone-13' - version: '16.2' + versioncode: '16-2' device: 'iPhone 14' - system: macos-12 + devicecode: 'iPhone-14' - version: '17.2' + versioncode: '17-2' device: 'iPhone 15' - system: macos-13 + devicecode: 'iPhone-15' - version: '18.2' + versioncode: '18-2' device: 'iPhone 16' - system: macos-14 + devicecode: 'iPhone-16' - version: '26.0' + versioncode: '26-0' device: 'iPhone 17' - system: macos-latest + devicecode: 'iPhone-17' steps: - name: Checkout uses: actions/checkout@v4 + - name: Setup xcodesorg/made/xcodes + run: | + brew install xcodesorg/made/xcodes + brew install aria2 + + - name: Check and Download iOS Runtime + id: check-runtime + run: | + echo "Runtimes before download" + available_runtimes="$(xcrun simctl list runtimes)" + echo "$available_runtimes" + + if echo "$available_runtimes" | grep -q "^iOS ${{ matrix.config.version }}"; then + echo "iOS ${{ matrix.config.version }} runtime is already installed" + else + echo "iOS ${{ matrix.config.version }} runtime is not installed" + sudo xcodes runtimes install "iOS ${{ matrix.config.version }}" + fi + + - name: Create iOS Simulators + run: | + sudo xcodebuild -license accept || echo "License already accepted" + + xcrun simctl create "${{ matrix.config.device }}" com.apple.CoreSimulator.SimDeviceType.${{ matrix.config.devicecode }} com.apple.CoreSimulator.SimRuntime.${{ matrix.config.versioncode }} || echo "Simulator already exists" + - name: Run tests run: | xcodebuild test -project Sample/Sample.xcodeproj -scheme Sample -destination "platform=iOS Simulator,OS=${{ matrix.config.version }},name=${{ matrix.config.device }}" From ffa82df50d21b15864ecec9af81cd4ed4ab48114 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Rutkowski?= Date: Sat, 11 Oct 2025 19:16:26 +0200 Subject: [PATCH 12/24] Remove xcodes setup step --- .github/workflows/main.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0b3cad0..b00842d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -38,11 +38,6 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Setup xcodesorg/made/xcodes - run: | - brew install xcodesorg/made/xcodes - brew install aria2 - - name: Check and Download iOS Runtime id: check-runtime run: | From 5df08852ec21dc3a78a0772cb155d70dc600729a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Rutkowski?= Date: Sat, 11 Oct 2025 19:20:13 +0200 Subject: [PATCH 13/24] Use latest version of each system --- .github/workflows/main.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b00842d..709e465 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -13,20 +13,20 @@ jobs: strategy: matrix: config: - - version: '15.2' - versioncode: '15-2' + - version: '15.5' + versioncode: '15-5' device: 'iPhone 13' devicecode: 'iPhone-13' - - version: '16.2' - versioncode: '16-2' + - version: '16.4' + versioncode: '16-4' device: 'iPhone 14' devicecode: 'iPhone-14' - - version: '17.2' - versioncode: '17-2' + - version: '17.5' + versioncode: '17-5' device: 'iPhone 15' devicecode: 'iPhone-15' - - version: '18.2' - versioncode: '18-2' + - version: '18.6' + versioncode: '18-6' device: 'iPhone 16' devicecode: 'iPhone-16' - version: '26.0' From 92686988e1f8038cfcb1b93c24e197be4e826d25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Rutkowski?= Date: Sat, 11 Oct 2025 19:34:24 +0200 Subject: [PATCH 14/24] Fix simulator creation --- .github/workflows/main.yml | 40 ++++++++++++++++---------------------- 1 file changed, 17 insertions(+), 23 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 709e465..d0175ea 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -14,50 +14,44 @@ jobs: matrix: config: - version: '15.5' - versioncode: '15-5' - device: 'iPhone 13' - devicecode: 'iPhone-13' + version-code: '15-5' + device-number: '13' - version: '16.4' - versioncode: '16-4' - device: 'iPhone 14' - devicecode: 'iPhone-14' + version-code: '16-4' + device-number: '14' - version: '17.5' - versioncode: '17-5' - device: 'iPhone 15' - devicecode: 'iPhone-15' + version-code: '17-5' + device-number: '15' - version: '18.6' - versioncode: '18-6' - device: 'iPhone 16' - devicecode: 'iPhone-16' + version-code: '18-6' + device-number: '16' - version: '26.0' - versioncode: '26-0' - device: 'iPhone 17' - devicecode: 'iPhone-17' + version-code: '26-0' + device-number: '17' steps: - name: Checkout uses: actions/checkout@v4 - - name: Check and Download iOS Runtime + - name: Prepare runtime id: check-runtime run: | - echo "Runtimes before download" available_runtimes="$(xcrun simctl list runtimes)" echo "$available_runtimes" if echo "$available_runtimes" | grep -q "^iOS ${{ matrix.config.version }}"; then - echo "iOS ${{ matrix.config.version }} runtime is already installed" + echo "iOS ${{ matrix.config.version }} runtime is available" else - echo "iOS ${{ matrix.config.version }} runtime is not installed" + echo "Installing iOS ${{ matrix.config.version }} runtime" sudo xcodes runtimes install "iOS ${{ matrix.config.version }}" + echo "Done" fi - - name: Create iOS Simulators + - name: Prepare simulator run: | sudo xcodebuild -license accept || echo "License already accepted" - - xcrun simctl create "${{ matrix.config.device }}" com.apple.CoreSimulator.SimDeviceType.${{ matrix.config.devicecode }} com.apple.CoreSimulator.SimRuntime.${{ matrix.config.versioncode }} || echo "Simulator already exists" + xcrun simctl create "${{ matrix.config.device }}" com.apple.CoreSimulator.SimDeviceType.iPhone-${{ matrix.config.device-number }} com.apple.CoreSimulator.SimRuntime.iOS-${{ matrix.config.version-code }} || echo "Simulator already exists" - name: Run tests run: | - xcodebuild test -project Sample/Sample.xcodeproj -scheme Sample -destination "platform=iOS Simulator,OS=${{ matrix.config.version }},name=${{ matrix.config.device }}" + xcodebuild test -project Sample/Sample.xcodeproj -scheme Sample -destination "platform=iOS Simulator,OS=${{ matrix.config.version }},name=iPhone ${{ matrix.config.device-number }}" From c6270b440259e2d6d1146a0dccab7609bf2a67d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Rutkowski?= Date: Sat, 11 Oct 2025 19:46:14 +0200 Subject: [PATCH 15/24] Fix iPhone creation --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d0175ea..3f54203 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -50,7 +50,7 @@ jobs: - name: Prepare simulator run: | sudo xcodebuild -license accept || echo "License already accepted" - xcrun simctl create "${{ matrix.config.device }}" com.apple.CoreSimulator.SimDeviceType.iPhone-${{ matrix.config.device-number }} com.apple.CoreSimulator.SimRuntime.iOS-${{ matrix.config.version-code }} || echo "Simulator already exists" + xcrun simctl create "iPhone ${{ matrix.config.device-number }}" com.apple.CoreSimulator.SimDeviceType.iPhone-${{ matrix.config.device-number }} com.apple.CoreSimulator.SimRuntime.iOS-${{ matrix.config.version-code }} || echo "Simulator already exists" - name: Run tests run: | From 4cb14799ace72e6a983a5f9dcea84ca9da08652a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Rutkowski?= Date: Sun, 12 Oct 2025 11:39:55 +0200 Subject: [PATCH 16/24] Create simulator only if not exists --- .github/workflows/main.yml | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3f54203..54f8982 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -42,15 +42,20 @@ jobs: if echo "$available_runtimes" | grep -q "^iOS ${{ matrix.config.version }}"; then echo "iOS ${{ matrix.config.version }} runtime is available" else - echo "Installing iOS ${{ matrix.config.version }} runtime" sudo xcodes runtimes install "iOS ${{ matrix.config.version }}" - echo "Done" fi - name: Prepare simulator run: | sudo xcodebuild -license accept || echo "License already accepted" - xcrun simctl create "iPhone ${{ matrix.config.device-number }}" com.apple.CoreSimulator.SimDeviceType.iPhone-${{ matrix.config.device-number }} com.apple.CoreSimulator.SimRuntime.iOS-${{ matrix.config.version-code }} || echo "Simulator already exists" + matching_devices="$(xcrun simctl list devices "iOS ${{ matrix.config.version }}")" + echo "$matching_devices" + if echo "$matching_devices" | grep -q "iPhone ${{ matrix.config.device-number }}"; then + echo "iPhone ${{ matrix.config.device-number }} simulator is available" + else + echo "Creating iPhone ${{ matrix.config.device-number }} simulator" + xcrun simctl create "iPhone ${{ matrix.config.device-number }}" com.apple.CoreSimulator.SimDeviceType.iPhone-${{ matrix.config.device-number }} com.apple.CoreSimulator.SimRuntime.iOS-${{ matrix.config.version-code }} + fi - name: Run tests run: | From 98c54159186de1b1be94b256f1205bd59d48ee3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Rutkowski?= Date: Sun, 12 Oct 2025 12:00:19 +0200 Subject: [PATCH 17/24] Always use created simulator --- .github/workflows/main.yml | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 54f8982..9d49671 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -45,18 +45,9 @@ jobs: sudo xcodes runtimes install "iOS ${{ matrix.config.version }}" fi - - name: Prepare simulator + - name: Run tests run: | sudo xcodebuild -license accept || echo "License already accepted" matching_devices="$(xcrun simctl list devices "iOS ${{ matrix.config.version }}")" - echo "$matching_devices" - if echo "$matching_devices" | grep -q "iPhone ${{ matrix.config.device-number }}"; then - echo "iPhone ${{ matrix.config.device-number }} simulator is available" - else - echo "Creating iPhone ${{ matrix.config.device-number }} simulator" - xcrun simctl create "iPhone ${{ matrix.config.device-number }}" com.apple.CoreSimulator.SimDeviceType.iPhone-${{ matrix.config.device-number }} com.apple.CoreSimulator.SimRuntime.iOS-${{ matrix.config.version-code }} - fi - - - name: Run tests - run: | - xcodebuild test -project Sample/Sample.xcodeproj -scheme Sample -destination "platform=iOS Simulator,OS=${{ matrix.config.version }},name=iPhone ${{ matrix.config.device-number }}" + device_id="$(xcrun simctl create "iPhone ${{ matrix.config.device-number }}" com.apple.CoreSimulator.SimDeviceType.iPhone-${{ matrix.config.device-number }} com.apple.CoreSimulator.SimRuntime.iOS-${{ matrix.config.version-code }})" + xcodebuild test -project Sample/Sample.xcodeproj -scheme Sample -destination "id=$device_id" From cdf7bf8761edc1a41064dca83c6d9f846e01ba91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Rutkowski?= Date: Sun, 12 Oct 2025 13:27:07 +0200 Subject: [PATCH 18/24] Remove unnecessary main actor switch --- Sources/LongPressButton/LongPressButton.swift | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Sources/LongPressButton/LongPressButton.swift b/Sources/LongPressButton/LongPressButton.swift index 2bcc28a..fa839a5 100644 --- a/Sources/LongPressButton/LongPressButton.swift +++ b/Sources/LongPressButton/LongPressButton.swift @@ -77,10 +77,8 @@ public struct LongPressButton: View { } catch { return } - await MainActor.run { - didLongPress = true - longPressAction() - } + didLongPress = true + longPressAction() } } } From a111d0ef27a18449b329bbac1d2a728509c43721 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Rutkowski?= Date: Sun, 12 Oct 2025 13:28:39 +0200 Subject: [PATCH 19/24] Do not fail all jobs if one fails --- .github/workflows/main.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9d49671..27af2b0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -11,6 +11,7 @@ jobs: name: UI tests (iOS ${{ matrix.config.version }}) runs-on: macos-latest strategy: + fail-fast: false matrix: config: - version: '15.5' From 3a7a7e6a1eeec652543e21b3ef2127c51856b315 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Rutkowski?= Date: Sun, 12 Oct 2025 13:53:12 +0200 Subject: [PATCH 20/24] Boot simulator before starting tests --- .github/workflows/main.yml | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 27af2b0..0a48337 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -46,9 +46,28 @@ jobs: sudo xcodes runtimes install "iOS ${{ matrix.config.version }}" fi - - name: Run tests + - name: Create simulator + id: create-simulator run: | sudo xcodebuild -license accept || echo "License already accepted" - matching_devices="$(xcrun simctl list devices "iOS ${{ matrix.config.version }}")" device_id="$(xcrun simctl create "iPhone ${{ matrix.config.device-number }}" com.apple.CoreSimulator.SimDeviceType.iPhone-${{ matrix.config.device-number }} com.apple.CoreSimulator.SimRuntime.iOS-${{ matrix.config.version-code }})" - xcodebuild test -project Sample/Sample.xcodeproj -scheme Sample -destination "id=$device_id" + echo "Created device with ID: $device_id" + echo "device_id=$device_id" >> $GITHUB_OUTPUT + + - name: Boot simulator + run: | + device_id="${{ steps.create-simulator.outputs.device_id }}" + xcrun simctl boot "$device_id" || true + for i in {1..30}; do + state="$(xcrun simctl list devices "$runtime_id" | grep "$device_id" | sed -E 's/.*\(([^)]+)\).*/\1/')" + echo "Device state: $state" + if echo "$state" | grep -q "Booted"; then + break + fi + sleep 1 + done + + + - name: Run tests + run: | + xcodebuild test -project Sample/Sample.xcodeproj -scheme Sample -destination "platform=iOS Simulator,id=${{ steps.create-simulator.outputs.device_id }}" From a85daff26fb44e1437ee123ac52b9b949fcc7b03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Rutkowski?= Date: Sun, 12 Oct 2025 13:58:54 +0200 Subject: [PATCH 21/24] Fix runtime --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0a48337..8a62494 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -59,7 +59,7 @@ jobs: device_id="${{ steps.create-simulator.outputs.device_id }}" xcrun simctl boot "$device_id" || true for i in {1..30}; do - state="$(xcrun simctl list devices "$runtime_id" | grep "$device_id" | sed -E 's/.*\(([^)]+)\).*/\1/')" + state="$(xcrun simctl list devices "com.apple.CoreSimulator.SimRuntime.iOS-${{ matrix.config.version-code }}" | grep "$device_id" | sed -E 's/.*\(([^)]+)\).*/\1/')" echo "Device state: $state" if echo "$state" | grep -q "Booted"; then break From 77883a5df817f3bc6bf84f27e55172bf2cedfa3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Rutkowski?= Date: Sun, 12 Oct 2025 14:01:37 +0200 Subject: [PATCH 22/24] Fix runtime --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8a62494..721e2ad 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -59,7 +59,7 @@ jobs: device_id="${{ steps.create-simulator.outputs.device_id }}" xcrun simctl boot "$device_id" || true for i in {1..30}; do - state="$(xcrun simctl list devices "com.apple.CoreSimulator.SimRuntime.iOS-${{ matrix.config.version-code }}" | grep "$device_id" | sed -E 's/.*\(([^)]+)\).*/\1/')" + state="$(xcrun simctl list devices "iOS ${{ matrix.config.version }}" | grep "$device_id" | sed -E 's/.*\(([^)]+)\).*/\1/')" echo "Device state: $state" if echo "$state" | grep -q "Booted"; then break From 07ae70e09b7e7edce014cd491ebb4feeb68aa823 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Rutkowski?= Date: Sun, 12 Oct 2025 14:04:34 +0200 Subject: [PATCH 23/24] List only available runtimes --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 721e2ad..ab24b7b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -37,7 +37,7 @@ jobs: - name: Prepare runtime id: check-runtime run: | - available_runtimes="$(xcrun simctl list runtimes)" + available_runtimes="$(xcrun simctl list runtimes available)" echo "$available_runtimes" if echo "$available_runtimes" | grep -q "^iOS ${{ matrix.config.version }}"; then From a7fefcfe6bf47bac595088b5ff8f315af30e5455 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Rutkowski?= Date: Sun, 12 Oct 2025 14:33:33 +0200 Subject: [PATCH 24/24] Do not run iOS 15 tests due to missing simulator --- .github/workflows/main.yml | 39 +++++--------------------------------- 1 file changed, 5 insertions(+), 34 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ab24b7b..341ac46 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -14,21 +14,14 @@ jobs: fail-fast: false matrix: config: - - version: '15.5' - version-code: '15-5' - device-number: '13' - version: '16.4' - version-code: '16-4' - device-number: '14' + device: 'iPhone 14' - version: '17.5' - version-code: '17-5' - device-number: '15' + device: 'iPhone 15' - version: '18.6' - version-code: '18-6' - device-number: '16' + device: 'iPhone 16' - version: '26.0' - version-code: '26-0' - device-number: '17' + device: 'iPhone 17' steps: - name: Checkout @@ -46,28 +39,6 @@ jobs: sudo xcodes runtimes install "iOS ${{ matrix.config.version }}" fi - - name: Create simulator - id: create-simulator - run: | - sudo xcodebuild -license accept || echo "License already accepted" - device_id="$(xcrun simctl create "iPhone ${{ matrix.config.device-number }}" com.apple.CoreSimulator.SimDeviceType.iPhone-${{ matrix.config.device-number }} com.apple.CoreSimulator.SimRuntime.iOS-${{ matrix.config.version-code }})" - echo "Created device with ID: $device_id" - echo "device_id=$device_id" >> $GITHUB_OUTPUT - - - name: Boot simulator - run: | - device_id="${{ steps.create-simulator.outputs.device_id }}" - xcrun simctl boot "$device_id" || true - for i in {1..30}; do - state="$(xcrun simctl list devices "iOS ${{ matrix.config.version }}" | grep "$device_id" | sed -E 's/.*\(([^)]+)\).*/\1/')" - echo "Device state: $state" - if echo "$state" | grep -q "Booted"; then - break - fi - sleep 1 - done - - - name: Run tests run: | - xcodebuild test -project Sample/Sample.xcodeproj -scheme Sample -destination "platform=iOS Simulator,id=${{ steps.create-simulator.outputs.device_id }}" + xcodebuild test -project Sample/Sample.xcodeproj -scheme Sample -destination "platform=iOS Simulator,OS=${{ matrix.config.version }},name=${{ matrix.config.device }}"