├── .gitignore
├── Touch-Tab
├── Assets.xcassets
│ ├── Contents.json
│ ├── AppIcon.appiconset
│ │ ├── AppIcon_16x16.png
│ │ ├── AppIcon_32x32.png
│ │ ├── AppIcon_64x64.png
│ │ ├── AppIcon_128x128.png
│ │ ├── AppIcon_256x256.png
│ │ ├── AppIcon_512x512.png
│ │ ├── AppIcon_1024x1024.png
│ │ └── Contents.json
│ ├── StatusIcon.imageset
│ │ ├── StatusIcon_16x16.png
│ │ ├── StatusIcon_32x32.png
│ │ └── Contents.json
│ ├── MenuItem-Warning.imageset
│ │ ├── MenuItem-Warning_16x16.png
│ │ ├── MenuItem-Warning_32x32.png
│ │ ├── MenuItem-Warning_48x48.png
│ │ └── Contents.json
│ └── StatusIcon-Warning.imageset
│ │ ├── StatusIcon-Warning_22x22.png
│ │ ├── StatusIcon-Warning_44x44.png
│ │ └── Contents.json
├── main.swift
├── Touch-Tab.entitlements
├── BundleInfo.swift
├── AboutView.swift
├── AppSwitcher.swift
├── PrivacyHelper.swift
├── AppDelegate.swift
└── SwipeManager.swift
├── README.md
└── Touch-Tab.xcodeproj
└── project.pbxproj
/.gitignore:
--------------------------------------------------------------------------------
1 | # Mac OS X
2 | .DS_Store
3 |
4 | # Xcode
5 | *.xcuserstate
6 | project.xcworkspace/
7 | xcuserdata/
8 |
--------------------------------------------------------------------------------
/Touch-Tab/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Touch-Tab/Assets.xcassets/AppIcon.appiconset/AppIcon_16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ris58h/Touch-Tab/HEAD/Touch-Tab/Assets.xcassets/AppIcon.appiconset/AppIcon_16x16.png
--------------------------------------------------------------------------------
/Touch-Tab/Assets.xcassets/AppIcon.appiconset/AppIcon_32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ris58h/Touch-Tab/HEAD/Touch-Tab/Assets.xcassets/AppIcon.appiconset/AppIcon_32x32.png
--------------------------------------------------------------------------------
/Touch-Tab/Assets.xcassets/AppIcon.appiconset/AppIcon_64x64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ris58h/Touch-Tab/HEAD/Touch-Tab/Assets.xcassets/AppIcon.appiconset/AppIcon_64x64.png
--------------------------------------------------------------------------------
/Touch-Tab/Assets.xcassets/AppIcon.appiconset/AppIcon_128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ris58h/Touch-Tab/HEAD/Touch-Tab/Assets.xcassets/AppIcon.appiconset/AppIcon_128x128.png
--------------------------------------------------------------------------------
/Touch-Tab/Assets.xcassets/AppIcon.appiconset/AppIcon_256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ris58h/Touch-Tab/HEAD/Touch-Tab/Assets.xcassets/AppIcon.appiconset/AppIcon_256x256.png
--------------------------------------------------------------------------------
/Touch-Tab/Assets.xcassets/AppIcon.appiconset/AppIcon_512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ris58h/Touch-Tab/HEAD/Touch-Tab/Assets.xcassets/AppIcon.appiconset/AppIcon_512x512.png
--------------------------------------------------------------------------------
/Touch-Tab/Assets.xcassets/AppIcon.appiconset/AppIcon_1024x1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ris58h/Touch-Tab/HEAD/Touch-Tab/Assets.xcassets/AppIcon.appiconset/AppIcon_1024x1024.png
--------------------------------------------------------------------------------
/Touch-Tab/Assets.xcassets/StatusIcon.imageset/StatusIcon_16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ris58h/Touch-Tab/HEAD/Touch-Tab/Assets.xcassets/StatusIcon.imageset/StatusIcon_16x16.png
--------------------------------------------------------------------------------
/Touch-Tab/Assets.xcassets/StatusIcon.imageset/StatusIcon_32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ris58h/Touch-Tab/HEAD/Touch-Tab/Assets.xcassets/StatusIcon.imageset/StatusIcon_32x32.png
--------------------------------------------------------------------------------
/Touch-Tab/main.swift:
--------------------------------------------------------------------------------
1 | import Cocoa
2 |
3 | let delegate = AppDelegate()
4 | NSApplication.shared.delegate = delegate
5 |
6 | _ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv)
7 |
--------------------------------------------------------------------------------
/Touch-Tab/Assets.xcassets/MenuItem-Warning.imageset/MenuItem-Warning_16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ris58h/Touch-Tab/HEAD/Touch-Tab/Assets.xcassets/MenuItem-Warning.imageset/MenuItem-Warning_16x16.png
--------------------------------------------------------------------------------
/Touch-Tab/Assets.xcassets/MenuItem-Warning.imageset/MenuItem-Warning_32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ris58h/Touch-Tab/HEAD/Touch-Tab/Assets.xcassets/MenuItem-Warning.imageset/MenuItem-Warning_32x32.png
--------------------------------------------------------------------------------
/Touch-Tab/Assets.xcassets/MenuItem-Warning.imageset/MenuItem-Warning_48x48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ris58h/Touch-Tab/HEAD/Touch-Tab/Assets.xcassets/MenuItem-Warning.imageset/MenuItem-Warning_48x48.png
--------------------------------------------------------------------------------
/Touch-Tab/Assets.xcassets/StatusIcon-Warning.imageset/StatusIcon-Warning_22x22.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ris58h/Touch-Tab/HEAD/Touch-Tab/Assets.xcassets/StatusIcon-Warning.imageset/StatusIcon-Warning_22x22.png
--------------------------------------------------------------------------------
/Touch-Tab/Assets.xcassets/StatusIcon-Warning.imageset/StatusIcon-Warning_44x44.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ris58h/Touch-Tab/HEAD/Touch-Tab/Assets.xcassets/StatusIcon-Warning.imageset/StatusIcon-Warning_44x44.png
--------------------------------------------------------------------------------
/Touch-Tab/Touch-Tab.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Touch-Tab/Assets.xcassets/StatusIcon.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "StatusIcon_16x16.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "filename" : "StatusIcon_32x32.png",
10 | "idiom" : "universal",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "scale" : "3x"
16 | }
17 | ],
18 | "info" : {
19 | "author" : "xcode",
20 | "version" : 1
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Touch-Tab/Assets.xcassets/StatusIcon-Warning.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "StatusIcon-Warning_22x22.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "filename" : "StatusIcon-Warning_44x44.png",
10 | "idiom" : "universal",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "scale" : "3x"
16 | }
17 | ],
18 | "info" : {
19 | "author" : "xcode",
20 | "version" : 1
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Touch-Tab/Assets.xcassets/MenuItem-Warning.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "MenuItem-Warning_16x16.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "filename" : "MenuItem-Warning_32x32.png",
10 | "idiom" : "universal",
11 | "scale" : "2x"
12 | },
13 | {
14 | "filename" : "MenuItem-Warning_48x48.png",
15 | "idiom" : "universal",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "author" : "xcode",
21 | "version" : 1
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Touch-Tab/BundleInfo.swift:
--------------------------------------------------------------------------------
1 | import Cocoa
2 |
3 | class BundleInfo {
4 | private static func bundleInfo(_ key: String) -> String {
5 | return Bundle.main.infoDictionary?[key] as? String ?? ""
6 | }
7 |
8 | static func iconName() -> String {
9 | return bundleInfo("CFBundleIconName")
10 | }
11 |
12 | static func displayName() -> String {
13 | return bundleInfo("CFBundleDisplayName")
14 | }
15 |
16 | static func version() -> String {
17 | return bundleInfo("CFBundleShortVersionString")
18 | }
19 |
20 | static func build() -> String {
21 | return bundleInfo("CFBundleVersion")
22 | }
23 |
24 | static func copyright() -> String {
25 | return bundleInfo("NSHumanReadableCopyright")
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Touch-Tab/AboutView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct AboutView: View {
4 | var body: some View {
5 | VStack(alignment: .center, spacing: 16) {
6 | Image(nsImage: NSImage(named: BundleInfo.iconName()) ?? NSImage())
7 | .resizable()
8 | .frame(width: 64, height: 64)
9 | Text(BundleInfo.displayName())
10 | .font(.system(size: 16, weight: .bold))
11 | Text("Version \(BundleInfo.version()) (\(BundleInfo.build()))")
12 | .font(.system(size: 12))
13 | Text("Copyright \(BundleInfo.copyright())")
14 | .font(.system(size: 12))
15 | }
16 | .padding(.horizontal, 32)
17 | .padding(.vertical, 16)
18 | }
19 | }
20 |
21 | struct AboutView_Previews: PreviewProvider {
22 | static var previews: some View {
23 | AboutView()
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Touch-Tab/AppSwitcher.swift:
--------------------------------------------------------------------------------
1 | import Cocoa
2 |
3 | class AppSwitcher {
4 | private static let keyboardEventSource = CGEventSource(stateID: CGEventSourceStateID.hidSystemState)
5 | private static let tabKey = CGKeyCode(0x30);
6 | private static let leftCommandKey = CGKeyCode(0x37);
7 |
8 | static func selectInAppSwitcher() {
9 | postKeyEvent(key: leftCommandKey, down: false)
10 | }
11 |
12 | static func cmdTab() {
13 | postKeyEvent(key: tabKey, down: true, flags: .maskCommand)
14 | postKeyEvent(key: tabKey, down: false, flags: .maskCommand)
15 | }
16 |
17 | static func cmdShiftTab() {
18 | postKeyEvent(key: tabKey, down: true, flags: [.maskCommand, .maskShift])
19 | postKeyEvent(key: tabKey, down: false, flags: [.maskCommand, .maskShift])
20 | }
21 |
22 | private static func postKeyEvent(key: CGKeyCode, down: Bool, flags: CGEventFlags = []) {
23 | let event = CGEvent(keyboardEventSource: keyboardEventSource, virtualKey: key, keyDown: down)
24 | event?.flags = flags
25 | event?.post(tap: CGEventTapLocation.cghidEventTap)
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Touch-Tab/PrivacyHelper.swift:
--------------------------------------------------------------------------------
1 | import Cocoa
2 |
3 | class PrivacyHelper {
4 | static func isProcessTrustedWithPrompt() -> Bool {
5 | //TODO: Investigation required. Calling AXIsProcessTrustedWithOptions in sandboxed app doesn't prompt user (at least in Ventura 13.4.1) but creating CGEventTap does it.
6 | let isAccessibilityPermissionGranted = AXIsProcessTrustedWithOptions([kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String : true] as CFDictionary)
7 | if isAccessibilityPermissionGranted {
8 | return true
9 | } else {
10 | PrivacyHelper.promptForAccessibilityPermissionFromSandbox()
11 | return false
12 | }
13 | }
14 |
15 | private static func promptForAccessibilityPermissionFromSandbox() {
16 | _ = CGEvent.tapCreate(
17 | tap: .cghidEventTap,
18 | place: .headInsertEventTap,
19 | options: .defaultTap,
20 | eventsOfInterest: NSEvent.EventTypeMask.gesture.rawValue,
21 | callback: dummyEventHandler,
22 | userInfo: nil
23 | )
24 | }
25 | }
26 |
27 | fileprivate func dummyEventHandler(proxy: CGEventTapProxy, eventType: CGEventType, cgEvent: CGEvent, userInfo: UnsafeMutableRawPointer?) -> Unmanaged? {
28 | debugPrint("Should never happen!")
29 | return Unmanaged.passUnretained(cgEvent)
30 | }
31 |
--------------------------------------------------------------------------------
/Touch-Tab/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "AppIcon_16x16.png",
5 | "idiom" : "mac",
6 | "scale" : "1x",
7 | "size" : "16x16"
8 | },
9 | {
10 | "filename" : "AppIcon_32x32.png",
11 | "idiom" : "mac",
12 | "scale" : "2x",
13 | "size" : "16x16"
14 | },
15 | {
16 | "filename" : "AppIcon_32x32.png",
17 | "idiom" : "mac",
18 | "scale" : "1x",
19 | "size" : "32x32"
20 | },
21 | {
22 | "filename" : "AppIcon_64x64.png",
23 | "idiom" : "mac",
24 | "scale" : "2x",
25 | "size" : "32x32"
26 | },
27 | {
28 | "filename" : "AppIcon_128x128.png",
29 | "idiom" : "mac",
30 | "scale" : "1x",
31 | "size" : "128x128"
32 | },
33 | {
34 | "filename" : "AppIcon_256x256.png",
35 | "idiom" : "mac",
36 | "scale" : "2x",
37 | "size" : "128x128"
38 | },
39 | {
40 | "filename" : "AppIcon_256x256.png",
41 | "idiom" : "mac",
42 | "scale" : "1x",
43 | "size" : "256x256"
44 | },
45 | {
46 | "filename" : "AppIcon_512x512.png",
47 | "idiom" : "mac",
48 | "scale" : "2x",
49 | "size" : "256x256"
50 | },
51 | {
52 | "filename" : "AppIcon_512x512.png",
53 | "idiom" : "mac",
54 | "scale" : "1x",
55 | "size" : "512x512"
56 | },
57 | {
58 | "filename" : "AppIcon_1024x1024.png",
59 | "idiom" : "mac",
60 | "scale" : "2x",
61 | "size" : "512x512"
62 | }
63 | ],
64 | "info" : {
65 | "author" : "xcode",
66 | "version" : 1
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Touch-Tab
2 |
3 | 
4 |
5 | Switch apps with trackpad on macOS.
6 | Use 3-fingers swipe right or 3-fingers swipe left to switch between apps.
7 | Hold after the swipe or swipe slowly to show App Switcher UI.
8 |
9 | Want to support? [Buy me a coffee](https://www.buymeacoffee.com/ris58h).
10 |
11 | ## Installation
12 | 1. Download the [latest](https://github.com/ris58h/Touch-Tab/releases/latest/download/Touch-Tab.zip) `Touch-Tab.zip` from [Releases](https://github.com/ris58h/Touch-Tab/releases) page.
13 | 2. Unzip the archive and move `Touch-Tab.app` into the `Applications` folder.
14 | 3. The app is ad-hoc signed so when you run the app macOS will warn you: `"Touch-Tab" can’t be opened because Apple cannot check it for malicious software`. Right-click the app and click `Open`, a
15 | pop-up will appear, click `Open` again.
16 | 4. The app needs access to global trackpad events. Allow Touch-Tab to control your computer in `System Settings > Privacy & Security > Accesibility`. If you had Touch-Tab installed before you may need to remove Touch-Tab from the `Accessibility` list first and add it again.
17 | 5. Disable 3-finger swipe between full-screen apps or make it 4-finger in `System Settings > Trackpad > More Gestures > Swipe between full-screen apps`.
18 |
19 | ## Usage
20 | - Use 3-fingers swipe right or 3-fingers swipe left to switch between apps.
21 | - Hold after the swipe or swipe slowly to show App Switcher UI. Pro tip: you can use 2-fingers scroll to switch apps in App Switcher faster.
22 |
23 | ### Hide Status Bar Item
24 | Holding ⌘ drag the item away from the status bar until you see ✖️ (cross icon) then let it go. To recover the item just open the app one more time.
25 |
26 | ## Troubleshooting
27 | ### "Touch-Tab" can’t be opened because Apple cannot check it for malicious software
28 | Right-click the app and click `Open`, a pop-up will appear, click `Open` again.
29 | ### It's running but doesn't work
30 | - Check that Touch-Tab is allowed to control your computer in `System Settings > Privacy & Security > Accesibility`. If you had Touch-Tab installed before you may need to remove Touch-Tab from the `Accessibility` list first and add it again.
31 | - Check that 3-finger swipe is disabled in `System Settings > Trackpad > More Gestures > Swipe between full-screen apps`.
32 | ### 3-finger swipe scrolls a content
33 | It's a [known issue](https://github.com/ris58h/Touch-Tab/issues/1). The workaround is to setup `Mission Control` or `App Expose` to use 3-finger swipe in `System Settings > Trackpad > More Gestures`.
34 | ### It still doesn't work
35 | Please create an [issue](https://github.com/ris58h/Touch-Tab/issues).
36 |
--------------------------------------------------------------------------------
/Touch-Tab/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | import Cocoa
2 | import SwiftUI
3 |
4 | class AppDelegate: NSObject, NSApplicationDelegate {
5 | private static let statusIcon = templateImage(named: "StatusIcon")
6 | private static let statusIconWarning = templateImage(named: "StatusIcon-Warning")
7 |
8 | private var statusBarItem: NSStatusItem!
9 | private var aboutWindow: NSWindow!
10 |
11 | private static func templateImage(named: String) -> NSImage? {
12 | let image = NSImage(named: named)
13 | image?.isTemplate = true
14 | return image
15 | }
16 |
17 | func applicationDidFinishLaunching(_ aNotification: Notification) {
18 | #if DEBUG
19 | if ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" {
20 | return
21 | }
22 | #endif
23 |
24 | createStatusBarItem()
25 | requestAccessibilityPermission() {
26 | SwipeManager.start()
27 | }
28 | }
29 |
30 | func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool {
31 | statusBarItem.isVisible=true
32 | return true
33 | }
34 |
35 | private func requestAccessibilityPermission(completion: @escaping ()->()) {
36 | let isAccessibilityPermissionGranted = PrivacyHelper.isProcessTrustedWithPrompt()
37 | debugPrint("Accessibility permission", isAccessibilityPermissionGranted)
38 | if isAccessibilityPermissionGranted {
39 | completion()
40 | } else {
41 | addAccessibilityWarning()
42 | Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [self] timer in
43 | if AXIsProcessTrusted() {
44 | debugPrint("Accessibility permission granted")
45 | removeAccessibilityWarning()
46 | timer.invalidate()
47 | completion()
48 | }
49 | }
50 | }
51 | }
52 |
53 | private func createStatusBarItem() {
54 | statusBarItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength)
55 | statusBarItem.button?.image = AppDelegate.statusIcon
56 | statusBarItem.button?.toolTip = BundleInfo.displayName()
57 | statusBarItem.behavior = .removalAllowed
58 |
59 | statusBarItem.menu = NSMenu()
60 | statusBarItem.menu?.addItem(
61 | withTitle: "About \(BundleInfo.displayName())",
62 | action: #selector(AppDelegate.showAbout),
63 | keyEquivalent: "")
64 | statusBarItem.menu?.addItem(
65 | withTitle: "Quit",
66 | action: #selector(AppDelegate.quit),
67 | keyEquivalent: "")
68 | }
69 |
70 | private func addAccessibilityWarning() {
71 | statusBarItem.button?.image = AppDelegate.statusIconWarning
72 | let warningDescriptionMenuItem = NSMenuItem(title: "No Accessibility Access", action: nil, keyEquivalent: "")
73 | warningDescriptionMenuItem.image = AppDelegate.templateImage(named: "MenuItem-Warning")
74 | warningDescriptionMenuItem.toolTip = "Grant access to this application in Privacy & Security settings, located in System Settings"
75 | warningDescriptionMenuItem.isEnabled = false
76 | let openPrivacyAccessibilityMenuItem = NSMenuItem(title: "Authorize...", action: #selector(openPrivacyAccessibility), keyEquivalent: "")
77 | statusBarItem.menu?.insertItem(warningDescriptionMenuItem, at: 0)
78 | statusBarItem.menu?.insertItem(openPrivacyAccessibilityMenuItem, at: 1)
79 | statusBarItem.menu?.insertItem(NSMenuItem.separator(), at: 2)
80 | }
81 |
82 | private func removeAccessibilityWarning() {
83 | statusBarItem.button?.image = AppDelegate.statusIcon
84 | statusBarItem.menu?.removeItem(at: 2)
85 | statusBarItem.menu?.removeItem(at: 1)
86 | statusBarItem.menu?.removeItem(at: 0)
87 | }
88 |
89 | @objc private func openPrivacyAccessibility() {
90 | let privacyAccessibilityURL = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility")!
91 | NSWorkspace.shared.open(privacyAccessibilityURL)
92 | }
93 |
94 | @objc private func quit() {
95 | NSApplication.shared.terminate(self)
96 | }
97 |
98 | @objc private func showAbout() {
99 | if aboutWindow == nil {
100 | aboutWindow = NSWindow(contentViewController: NSHostingController(rootView: AboutView().fixedSize()))
101 | aboutWindow.styleMask = [.closable, .titled]
102 | aboutWindow.title = ""
103 | }
104 | aboutWindow.center()
105 | aboutWindow.makeKeyAndOrderFront(nil)
106 | NSApp.activate(ignoringOtherApps: true)
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/Touch-Tab/SwipeManager.swift:
--------------------------------------------------------------------------------
1 | import Cocoa
2 |
3 | class SwipeManager {
4 | private static let accVelXThreshold: Float = 0.07
5 | // TODO: figure out the real value of the delay.
6 | private static let appSwitcherUIDelay: Double = 0.2
7 |
8 | private static var eventTap: CFMachPort? = nil
9 | // Event state.
10 | private static var accVelX: Float = 0
11 | private static var prevTouchPositions: [String: NSPoint] = [:]
12 | // Gesture state. Gesture may consists of multiple events.
13 | private static var startTime: Date? = nil
14 |
15 | //TODO: move it somewhere else?
16 | private static func listener(_ eventType: EventType) {
17 | switch eventType {
18 | case .startOrContinue(.left):
19 | AppSwitcher.cmdShiftTab()
20 | case .startOrContinue(.right):
21 | AppSwitcher.cmdTab()
22 | case .end:
23 | AppSwitcher.selectInAppSwitcher()
24 | }
25 | }
26 |
27 | static func start() {
28 | if eventTap != nil {
29 | debugPrint("SwipeManager is already started")
30 | return
31 | }
32 | debugPrint("SwipeManager start")
33 | eventTap = CGEvent.tapCreate(
34 | tap: .cghidEventTap,
35 | place: .headInsertEventTap,
36 | options: .defaultTap,
37 | eventsOfInterest: NSEvent.EventTypeMask.gesture.rawValue,
38 | callback: { proxy, type, cgEvent, userInfo in
39 | return SwipeManager.eventHandler(proxy: proxy, eventType: type, cgEvent: cgEvent, userInfo: userInfo)
40 | },
41 | userInfo: nil
42 | )
43 | if eventTap == nil {
44 | debugPrint("SwipeManager couldn't create event tap")
45 | return
46 | }
47 |
48 | let runLoopSource = CFMachPortCreateRunLoopSource(nil, eventTap, 0)
49 | CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, CFRunLoopMode.commonModes)
50 | CGEvent.tapEnable(tap: eventTap!, enable: true)
51 | }
52 |
53 | private static func eventHandler(proxy: CGEventTapProxy, eventType: CGEventType, cgEvent: CGEvent, userInfo: UnsafeMutableRawPointer?) -> Unmanaged? {
54 | if eventType.rawValue == NSEvent.EventType.gesture.rawValue, let nsEvent = NSEvent(cgEvent: cgEvent) {
55 | touchEventHandler(nsEvent)
56 | } else if (eventType == .tapDisabledByUserInput || eventType == .tapDisabledByTimeout) {
57 | debugPrint("SwipeManager tap disabled", eventType.rawValue)
58 | CGEvent.tapEnable(tap: eventTap!, enable: true)
59 | }
60 | return Unmanaged.passUnretained(cgEvent)
61 | }
62 |
63 | private static func touchEventHandler(_ nsEvent: NSEvent) {
64 | let touches = nsEvent.allTouches()
65 |
66 | // Sometimes there are empty touch events that we have to skip. There are no empty touch events if Mission Control or App Expose use 3-finger swipes though.
67 | if touches.isEmpty {
68 | return
69 | }
70 | let touchesCount = touches.allSatisfy({ $0.phase == .ended }) ? 0 : touches.count
71 |
72 | switch touchesCount {
73 | case 2: processTwoFingers()
74 | case 3: processThreeFingers(touches: touches)
75 | default: processOtherFingers()
76 | }
77 | }
78 |
79 | private static func processTwoFingers() {
80 | // Two fingers scrolling in App Switcher is OK but we shouldn't accumulate gesture velocity here.
81 | clearEventState()
82 | }
83 |
84 | private static func processThreeFingers(touches: Set) {
85 | let velX = SwipeManager.horizontalSwipeVelocity(touches: touches)
86 | // We don't care about non-horizontal swipes.
87 | if velX == nil {
88 | return
89 | }
90 |
91 | accVelX += velX!
92 | // Not enough swiping.
93 | if abs(accVelX) < accVelXThreshold {
94 | return
95 | }
96 |
97 | if startTime == nil {
98 | startTime = Date()
99 | } else {
100 | let interval = startTime!.timeIntervalSinceNow
101 | if -interval < appSwitcherUIDelay {
102 | // We skip subsequent events until App Switcher UI is shown.
103 | clearEventState()
104 | return
105 | }
106 | }
107 |
108 | startOrContinueGesture()
109 | clearEventState()
110 | }
111 |
112 | private static func processOtherFingers() {
113 | if startTime != nil {
114 | endGesture()
115 | clearEventState()
116 | startTime = nil
117 | }
118 | }
119 |
120 | private static func clearEventState() {
121 | accVelX = 0
122 | prevTouchPositions.removeAll()
123 | }
124 |
125 | private static func startOrContinueGesture() {
126 | let direction: EventType.Direction = accVelX < 0 ? .left : .right
127 | listener(.startOrContinue(direction: direction))
128 | }
129 |
130 | private static func endGesture() {
131 | listener(.end)
132 | }
133 |
134 | private static func horizontalSwipeVelocity(touches: Set) -> Float? {
135 | var allRight = true
136 | var allLeft = true
137 | var sumVelX = Float(0)
138 | var sumVelY = Float(0)
139 | for touch in touches {
140 | let (velX, velY) = touchVelocity(touch)
141 | allRight = allRight && velX >= 0
142 | allLeft = allLeft && velX <= 0
143 | sumVelX += velX
144 | sumVelY += velY
145 |
146 | if touch.phase == .ended {
147 | prevTouchPositions.removeValue(forKey: "\(touch.identity)")
148 | } else {
149 | prevTouchPositions["\(touch.identity)"] = touch.normalizedPosition
150 | }
151 | }
152 | // All fingers should move in the same direction.
153 | if !allRight && !allLeft {
154 | return nil
155 | }
156 |
157 | let velX = sumVelX / Float(touches.count)
158 | let velY = sumVelY / Float(touches.count)
159 | // Only horizontal swipes are interesting.
160 | if abs(velX) <= abs(velY) {
161 | return nil
162 | }
163 |
164 | return velX
165 | }
166 |
167 | private static func touchVelocity(_ touch: NSTouch) -> (Float, Float) {
168 | guard let prevPosition = prevTouchPositions["\(touch.identity)"] else {
169 | return (0, 0)
170 | }
171 | let position = touch.normalizedPosition
172 | return (Float(position.x - prevPosition.x), Float(position.y - prevPosition.y))
173 | }
174 |
175 | enum EventType {
176 | case startOrContinue(direction: Direction)
177 | case end
178 |
179 | enum Direction {
180 | case left
181 | case right
182 | }
183 | }
184 | }
185 |
--------------------------------------------------------------------------------
/Touch-Tab.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 55;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 588922D429BB490400F7F843 /* AppSwitcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 588922D329BB490400F7F843 /* AppSwitcher.swift */; };
11 | 58BC5B2C28B3C4C30013EAD3 /* SwipeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BC5B2828B3C4C30013EAD3 /* SwipeManager.swift */; };
12 | 58BC5B2D28B3C4C30013EAD3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 58BC5B2928B3C4C30013EAD3 /* Assets.xcassets */; };
13 | 58BC5B2E28B3C4C30013EAD3 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BC5B2A28B3C4C30013EAD3 /* AppDelegate.swift */; };
14 | 58BC5B2F28B3C4C30013EAD3 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BC5B2B28B3C4C30013EAD3 /* main.swift */; };
15 | 58D3F9F42A8E14C80021A532 /* PrivacyHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58D3F9F32A8E14C80021A532 /* PrivacyHelper.swift */; };
16 | 58DE49CA29D09BB70025BE44 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58DE49C929D09BB70025BE44 /* AboutView.swift */; };
17 | 58FBBBE529D20D3600A15F66 /* BundleInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FBBBE429D20D3600A15F66 /* BundleInfo.swift */; };
18 | /* End PBXBuildFile section */
19 |
20 | /* Begin PBXFileReference section */
21 | 588922D329BB490400F7F843 /* AppSwitcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSwitcher.swift; sourceTree = ""; };
22 | 58BC5B1528B3C37B0013EAD3 /* Touch-Tab.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Touch-Tab.app"; sourceTree = BUILT_PRODUCTS_DIR; };
23 | 58BC5B2828B3C4C30013EAD3 /* SwipeManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwipeManager.swift; sourceTree = ""; };
24 | 58BC5B2928B3C4C30013EAD3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
25 | 58BC5B2A28B3C4C30013EAD3 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
26 | 58BC5B2B28B3C4C30013EAD3 /* main.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; };
27 | 58D3F9F32A8E14C80021A532 /* PrivacyHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyHelper.swift; sourceTree = ""; };
28 | 58D3F9F52A8E15E50021A532 /* Touch-Tab.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Touch-Tab.entitlements"; sourceTree = ""; };
29 | 58DE49C929D09BB70025BE44 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = ""; };
30 | 58FBBBE429D20D3600A15F66 /* BundleInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleInfo.swift; sourceTree = ""; };
31 | /* End PBXFileReference section */
32 |
33 | /* Begin PBXGroup section */
34 | 58BC5B0C28B3C37B0013EAD3 = {
35 | isa = PBXGroup;
36 | children = (
37 | 58BC5B1728B3C37B0013EAD3 /* Touch-Tab */,
38 | 58BC5B1628B3C37B0013EAD3 /* Products */,
39 | );
40 | sourceTree = "";
41 | };
42 | 58BC5B1628B3C37B0013EAD3 /* Products */ = {
43 | isa = PBXGroup;
44 | children = (
45 | 58BC5B1528B3C37B0013EAD3 /* Touch-Tab.app */,
46 | );
47 | name = Products;
48 | sourceTree = "";
49 | };
50 | 58BC5B1728B3C37B0013EAD3 /* Touch-Tab */ = {
51 | isa = PBXGroup;
52 | children = (
53 | 58D3F9F52A8E15E50021A532 /* Touch-Tab.entitlements */,
54 | 58FBBBE429D20D3600A15F66 /* BundleInfo.swift */,
55 | 58BC5B2A28B3C4C30013EAD3 /* AppDelegate.swift */,
56 | 58BC5B2928B3C4C30013EAD3 /* Assets.xcassets */,
57 | 58BC5B2B28B3C4C30013EAD3 /* main.swift */,
58 | 58BC5B2828B3C4C30013EAD3 /* SwipeManager.swift */,
59 | 588922D329BB490400F7F843 /* AppSwitcher.swift */,
60 | 58DE49C929D09BB70025BE44 /* AboutView.swift */,
61 | 58D3F9F32A8E14C80021A532 /* PrivacyHelper.swift */,
62 | );
63 | path = "Touch-Tab";
64 | sourceTree = "";
65 | };
66 | /* End PBXGroup section */
67 |
68 | /* Begin PBXNativeTarget section */
69 | 58BC5B1428B3C37B0013EAD3 /* Touch-Tab */ = {
70 | isa = PBXNativeTarget;
71 | buildConfigurationList = 58BC5B2428B3C37E0013EAD3 /* Build configuration list for PBXNativeTarget "Touch-Tab" */;
72 | buildPhases = (
73 | 58BC5B1128B3C37B0013EAD3 /* Sources */,
74 | 58BC5B1328B3C37B0013EAD3 /* Resources */,
75 | );
76 | buildRules = (
77 | );
78 | dependencies = (
79 | );
80 | name = "Touch-Tab";
81 | productName = "Touch-Tab";
82 | productReference = 58BC5B1528B3C37B0013EAD3 /* Touch-Tab.app */;
83 | productType = "com.apple.product-type.application";
84 | };
85 | /* End PBXNativeTarget section */
86 |
87 | /* Begin PBXProject section */
88 | 58BC5B0D28B3C37B0013EAD3 /* Project object */ = {
89 | isa = PBXProject;
90 | attributes = {
91 | BuildIndependentTargetsInParallel = 1;
92 | LastSwiftUpdateCheck = 1340;
93 | LastUpgradeCheck = 1340;
94 | TargetAttributes = {
95 | 58BC5B1428B3C37B0013EAD3 = {
96 | CreatedOnToolsVersion = 13.4.1;
97 | LastSwiftMigration = 1340;
98 | };
99 | };
100 | };
101 | buildConfigurationList = 58BC5B1028B3C37B0013EAD3 /* Build configuration list for PBXProject "Touch-Tab" */;
102 | compatibilityVersion = "Xcode 13.0";
103 | developmentRegion = en;
104 | hasScannedForEncodings = 0;
105 | knownRegions = (
106 | en,
107 | Base,
108 | );
109 | mainGroup = 58BC5B0C28B3C37B0013EAD3;
110 | productRefGroup = 58BC5B1628B3C37B0013EAD3 /* Products */;
111 | projectDirPath = "";
112 | projectRoot = "";
113 | targets = (
114 | 58BC5B1428B3C37B0013EAD3 /* Touch-Tab */,
115 | );
116 | };
117 | /* End PBXProject section */
118 |
119 | /* Begin PBXResourcesBuildPhase section */
120 | 58BC5B1328B3C37B0013EAD3 /* Resources */ = {
121 | isa = PBXResourcesBuildPhase;
122 | buildActionMask = 2147483647;
123 | files = (
124 | 58BC5B2D28B3C4C30013EAD3 /* Assets.xcassets in Resources */,
125 | );
126 | runOnlyForDeploymentPostprocessing = 0;
127 | };
128 | /* End PBXResourcesBuildPhase section */
129 |
130 | /* Begin PBXSourcesBuildPhase section */
131 | 58BC5B1128B3C37B0013EAD3 /* Sources */ = {
132 | isa = PBXSourcesBuildPhase;
133 | buildActionMask = 2147483647;
134 | files = (
135 | 58BC5B2E28B3C4C30013EAD3 /* AppDelegate.swift in Sources */,
136 | 58BC5B2C28B3C4C30013EAD3 /* SwipeManager.swift in Sources */,
137 | 588922D429BB490400F7F843 /* AppSwitcher.swift in Sources */,
138 | 58FBBBE529D20D3600A15F66 /* BundleInfo.swift in Sources */,
139 | 58BC5B2F28B3C4C30013EAD3 /* main.swift in Sources */,
140 | 58DE49CA29D09BB70025BE44 /* AboutView.swift in Sources */,
141 | 58D3F9F42A8E14C80021A532 /* PrivacyHelper.swift in Sources */,
142 | );
143 | runOnlyForDeploymentPostprocessing = 0;
144 | };
145 | /* End PBXSourcesBuildPhase section */
146 |
147 | /* Begin XCBuildConfiguration section */
148 | 58BC5B2228B3C37E0013EAD3 /* Debug */ = {
149 | isa = XCBuildConfiguration;
150 | buildSettings = {
151 | ALWAYS_SEARCH_USER_PATHS = NO;
152 | CLANG_ANALYZER_NONNULL = YES;
153 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
154 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
155 | CLANG_ENABLE_MODULES = YES;
156 | CLANG_ENABLE_OBJC_ARC = YES;
157 | CLANG_ENABLE_OBJC_WEAK = YES;
158 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
159 | CLANG_WARN_BOOL_CONVERSION = YES;
160 | CLANG_WARN_COMMA = YES;
161 | CLANG_WARN_CONSTANT_CONVERSION = YES;
162 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
163 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
164 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
165 | CLANG_WARN_EMPTY_BODY = YES;
166 | CLANG_WARN_ENUM_CONVERSION = YES;
167 | CLANG_WARN_INFINITE_RECURSION = YES;
168 | CLANG_WARN_INT_CONVERSION = YES;
169 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
170 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
171 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
172 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
173 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
174 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
175 | CLANG_WARN_STRICT_PROTOTYPES = YES;
176 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
177 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
178 | CLANG_WARN_UNREACHABLE_CODE = YES;
179 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
180 | COPY_PHASE_STRIP = NO;
181 | DEBUG_INFORMATION_FORMAT = dwarf;
182 | ENABLE_STRICT_OBJC_MSGSEND = YES;
183 | ENABLE_TESTABILITY = YES;
184 | GCC_C_LANGUAGE_STANDARD = gnu11;
185 | GCC_DYNAMIC_NO_PIC = NO;
186 | GCC_NO_COMMON_BLOCKS = YES;
187 | GCC_OPTIMIZATION_LEVEL = 0;
188 | GCC_PREPROCESSOR_DEFINITIONS = (
189 | "DEBUG=1",
190 | "$(inherited)",
191 | );
192 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
193 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
194 | GCC_WARN_UNDECLARED_SELECTOR = YES;
195 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
196 | GCC_WARN_UNUSED_FUNCTION = YES;
197 | GCC_WARN_UNUSED_VARIABLE = YES;
198 | MACOSX_DEPLOYMENT_TARGET = 10.15;
199 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
200 | MTL_FAST_MATH = YES;
201 | ONLY_ACTIVE_ARCH = YES;
202 | SDKROOT = macosx;
203 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
204 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
205 | };
206 | name = Debug;
207 | };
208 | 58BC5B2328B3C37E0013EAD3 /* Release */ = {
209 | isa = XCBuildConfiguration;
210 | buildSettings = {
211 | ALWAYS_SEARCH_USER_PATHS = NO;
212 | CLANG_ANALYZER_NONNULL = YES;
213 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
214 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
215 | CLANG_ENABLE_MODULES = YES;
216 | CLANG_ENABLE_OBJC_ARC = YES;
217 | CLANG_ENABLE_OBJC_WEAK = YES;
218 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
219 | CLANG_WARN_BOOL_CONVERSION = YES;
220 | CLANG_WARN_COMMA = YES;
221 | CLANG_WARN_CONSTANT_CONVERSION = YES;
222 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
223 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
224 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
225 | CLANG_WARN_EMPTY_BODY = YES;
226 | CLANG_WARN_ENUM_CONVERSION = YES;
227 | CLANG_WARN_INFINITE_RECURSION = YES;
228 | CLANG_WARN_INT_CONVERSION = YES;
229 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
230 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
231 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
232 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
233 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
234 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
235 | CLANG_WARN_STRICT_PROTOTYPES = YES;
236 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
237 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
238 | CLANG_WARN_UNREACHABLE_CODE = YES;
239 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
240 | COPY_PHASE_STRIP = NO;
241 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
242 | ENABLE_NS_ASSERTIONS = NO;
243 | ENABLE_STRICT_OBJC_MSGSEND = YES;
244 | GCC_C_LANGUAGE_STANDARD = gnu11;
245 | GCC_NO_COMMON_BLOCKS = YES;
246 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
247 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
248 | GCC_WARN_UNDECLARED_SELECTOR = YES;
249 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
250 | GCC_WARN_UNUSED_FUNCTION = YES;
251 | GCC_WARN_UNUSED_VARIABLE = YES;
252 | MACOSX_DEPLOYMENT_TARGET = 10.15;
253 | MTL_ENABLE_DEBUG_INFO = NO;
254 | MTL_FAST_MATH = YES;
255 | SDKROOT = macosx;
256 | SWIFT_COMPILATION_MODE = wholemodule;
257 | SWIFT_OPTIMIZATION_LEVEL = "-O";
258 | };
259 | name = Release;
260 | };
261 | 58BC5B2528B3C37E0013EAD3 /* Debug */ = {
262 | isa = XCBuildConfiguration;
263 | buildSettings = {
264 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
265 | CLANG_ENABLE_MODULES = YES;
266 | CODE_SIGN_ENTITLEMENTS = "Touch-Tab/Touch-Tab.entitlements";
267 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
268 | CODE_SIGN_STYLE = Automatic;
269 | COMBINE_HIDPI_IMAGES = YES;
270 | CURRENT_PROJECT_VERSION = 10;
271 | DEVELOPMENT_TEAM = "";
272 | ENABLE_HARDENED_RUNTIME = YES;
273 | ENABLE_PREVIEWS = YES;
274 | GENERATE_INFOPLIST_FILE = YES;
275 | INFOPLIST_KEY_CFBundleDisplayName = "Touch-Tab";
276 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity";
277 | INFOPLIST_KEY_LSUIElement = YES;
278 | INFOPLIST_KEY_NSHumanReadableCopyright = "© Ilya Rodionov";
279 | LD_RUNPATH_SEARCH_PATHS = (
280 | "$(inherited)",
281 | "@executable_path/../Frameworks",
282 | );
283 | MARKETING_VERSION = 1.5.0;
284 | PRODUCT_BUNDLE_IDENTIFIER = "ris58h.Touch-Tab";
285 | PRODUCT_NAME = "$(TARGET_NAME)";
286 | SWIFT_EMIT_LOC_STRINGS = YES;
287 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
288 | SWIFT_VERSION = 5.0;
289 | };
290 | name = Debug;
291 | };
292 | 58BC5B2628B3C37E0013EAD3 /* Release */ = {
293 | isa = XCBuildConfiguration;
294 | buildSettings = {
295 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
296 | CLANG_ENABLE_MODULES = YES;
297 | CODE_SIGN_ENTITLEMENTS = "Touch-Tab/Touch-Tab.entitlements";
298 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
299 | CODE_SIGN_STYLE = Automatic;
300 | COMBINE_HIDPI_IMAGES = YES;
301 | CURRENT_PROJECT_VERSION = 10;
302 | DEVELOPMENT_TEAM = "";
303 | ENABLE_HARDENED_RUNTIME = YES;
304 | ENABLE_PREVIEWS = YES;
305 | GENERATE_INFOPLIST_FILE = YES;
306 | INFOPLIST_KEY_CFBundleDisplayName = "Touch-Tab";
307 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity";
308 | INFOPLIST_KEY_LSUIElement = YES;
309 | INFOPLIST_KEY_NSHumanReadableCopyright = "© Ilya Rodionov";
310 | LD_RUNPATH_SEARCH_PATHS = (
311 | "$(inherited)",
312 | "@executable_path/../Frameworks",
313 | );
314 | MARKETING_VERSION = 1.5.0;
315 | PRODUCT_BUNDLE_IDENTIFIER = "ris58h.Touch-Tab";
316 | PRODUCT_NAME = "$(TARGET_NAME)";
317 | SWIFT_EMIT_LOC_STRINGS = YES;
318 | SWIFT_VERSION = 5.0;
319 | };
320 | name = Release;
321 | };
322 | /* End XCBuildConfiguration section */
323 |
324 | /* Begin XCConfigurationList section */
325 | 58BC5B1028B3C37B0013EAD3 /* Build configuration list for PBXProject "Touch-Tab" */ = {
326 | isa = XCConfigurationList;
327 | buildConfigurations = (
328 | 58BC5B2228B3C37E0013EAD3 /* Debug */,
329 | 58BC5B2328B3C37E0013EAD3 /* Release */,
330 | );
331 | defaultConfigurationIsVisible = 0;
332 | defaultConfigurationName = Release;
333 | };
334 | 58BC5B2428B3C37E0013EAD3 /* Build configuration list for PBXNativeTarget "Touch-Tab" */ = {
335 | isa = XCConfigurationList;
336 | buildConfigurations = (
337 | 58BC5B2528B3C37E0013EAD3 /* Debug */,
338 | 58BC5B2628B3C37E0013EAD3 /* Release */,
339 | );
340 | defaultConfigurationIsVisible = 0;
341 | defaultConfigurationName = Release;
342 | };
343 | /* End XCConfigurationList section */
344 | };
345 | rootObject = 58BC5B0D28B3C37B0013EAD3 /* Project object */;
346 | }
347 |
--------------------------------------------------------------------------------