基本は二重起動できない
すでに起動しているアプリを、LaunchPad や Finder から、再び クリック / ダブルクリックしても、二重起動しません。これは、macOS(の標準ランチャー)が二重起動を制御しているためです。よって、アプリ側でわざわざ二重起動に対する処理を持つ必要は「原則」ありません。
しかし、例えば、電卓アプリなどを複数起動したいと思っても、それができません。二重起動できないことが、返って不便なこともあります。

今回は、macOS標準の電卓アプリ(計算機.app
)を例に、アプリを二重起動(複数起動)する 2つの方法を紹介します。
(macOS 14.6.1 / Xcode 15.4 / Swift 5.10
で確認済み)
アプリケーションを二重起動する 2つの方法
- コマンドで二重起動する
- (プログラム)コードで二重起動する
1. コマンドで二重起動する
次のコマンドを実行すると、実行した数だけ電卓アプリを起動することができます。
open -n /System/Applications/Calculator.app
-n
オプション:新しいインスタンスで起動する
2. コードで二重起動する
次のコードを実行すると、実行した数だけ電卓アプリを起動することができます。
import AppKit
let path = "/System/Applications/Calculator.app"
let appURL = URL(fileURLWithPath: path)
let conf = NSWorkspace.OpenConfiguration()
conf.createsNewApplicationInstance = true
conf.addsToRecentItems = false
let newApp = NSWorkspace.shared.open(appURL, configuration: conf, completionHandler: { launchedApp, error in
if let error {
print("Could not launch new app: \(error.localizedDescription)")
} else if let launchedApp {
print("Launched app process id: \(launchedApp.processIdentifier)")
}
})
//Thread.sleep(forTimeInterval: 1) //required for Swift REPL
conf.createsNewApplicationInstance = true
を指定することで、新しいインスタンスで起動されます。
コマンド・コード共に、起動したアプリのウィンドウ位置が どれも同じ(ウィンドウが重なって起動される)ため、移動する必要があります。
電卓アプリを3つ起動した例 |
---|
![]() |
アプリ多重起動 & ウィンドウ位置調整
上記スクショのSwiftUIアプリは、指定したアプリを多重起動すると共に、自動的にウィンドウの位置を調整します(上記スクショのように、ウィンドウを少しずつずらす)。
All Close
ボタンで、起動したアプリを一斉に終了します。
コードは以下のとおり。
import SwiftUI
import AppKit
var apps = [NSRunningApplication]()
struct ContentView: View {
@State var appPath = "/System/Applications/Calculator.app"
var body: some View {
VStack {
HStack(alignment: .top) {
Text("App Path:")
TextEditor(text: $appPath)
.lineLimit(1)
.font(.system(size: 12))
.frame(height: 21)
Image(systemName: "x.circle")
.onTapGesture {
appPath = ""
}
Button("Lauch", action: {
if FileManager.default.fileExists(atPath: appPath.trimmingCharacters(in: .whitespacesAndNewlines), isDirectory: nil) {
launchApp(appPath)
} else {
print("file not found: \(appPath)")
}
})
}
HStack {
Spacer()
Button("All Close", action: {
if apps.isEmpty { return }
for app in apps.reversed() {
app.terminate()
}
apps.removeAll()
})
}
}
.padding()
}
func launchApp(_ path: String) {
let appURL = URL(fileURLWithPath: path)
let conf = NSWorkspace.OpenConfiguration()
conf.createsNewApplicationInstance = true
conf.addsToRecentItems = false
NSWorkspace.shared.open(appURL, configuration: conf, completionHandler: { runningApp, error in
if let error {
print("Cannot start new app: \(error.localizedDescription)")
} else if let runningApp {
var point: CGPoint? = nil
if !apps.isEmpty {
let lastApp = apps.last!
let lastAppPID = lastApp.processIdentifier
lastApp.activate()
Thread.sleep(forTimeInterval: 0.1) //wait for activate
if let window = getFocusedWindow(pid: lastAppPID) {
if let origin = getOrigin(element: window) {
point = origin
}
}
}
if var point {
point = CGPoint(x: point.x + 20, y: point.y + 20)
let appPID = runningApp.processIdentifier
runningApp.activate()
Thread.sleep(forTimeInterval: 0.1) //wait for activate
if let window = getFocusedWindow(pid: appPID) {
setOrigin(element: window, origin: point)
}
}
apps.append(runningApp)
print("Launch app: \(runningApp.processIdentifier)")
}
})
}
func copyAttributeValue(_ element: AXUIElement, attribute: String) -> CFTypeRef? {
var ref: CFTypeRef? = nil
let rtn = AXUIElementCopyAttributeValue(element, attribute as CFString, &ref)
return rtn == .success ? ref : nil
}
func setAttributeValue(_ element: AXUIElement, attribute: String, value: CFTypeRef) -> Bool {
AXUIElementSetAttributeValue(element, attribute as CFString, value) == .success
}
func getFocusedWindow(pid: pid_t) -> AXUIElement? {
let element = AXUIElementCreateApplication(pid)
if let window = copyAttributeValue(element, attribute: kAXFocusedWindowAttribute) {
return (window as! AXUIElement)
}
return nil
}
func getOrigin(element: AXUIElement) -> NSPoint? {
var origin: NSPoint = .zero
if let originRef = copyAttributeValue(element, attribute: kAXPositionAttribute) {
if AXValueGetType(originRef as! AXValue).rawValue == kAXValueCGPointType && AXValueGetValue(originRef as! AXValue, AXValueType(rawValue: kAXValueCGPointType)!, &origin) {
return origin
}
}
return nil
}
@discardableResult
func setOrigin(element: AXUIElement, origin: NSPoint) -> Bool {
var origin = origin
if let value = AXValueCreate(AXValueType.cgPoint, &origin) {
return setAttributeValue(element, attribute: kAXPositionAttribute, value: value)
}
return false
}
}
「App Path」フィールドをクリア後、アプリアイコンをドラッグ&ドロップすると、そのアプリのフルパスが設定されます。
SandBoxとアクセシビリティ
上のアプリを動作させるためには、SandBox環境のオフ
と、アクセシビリティ権限を与える必要があります。
1.アプリ名.entitlements
のApp SandBox
をNO
に変更する
2.「アクセシビリティ アクセス」ダイアログ
アプリを起動すると「アクセシビリティ アクセス」ダイアログが表示されるので、「システム設定を開く」をクリックする。

「システム設定」→「プライバシーとセキュリティ」→「アクセシビリティ」が開くので、当アプリを「ON」にする。

以上の設定により、以降のウィンドウのずらしが機能するはずです。
アプリの二重起動をチェックする方法
一方、アプリによっては、複数のインスタンスが起動されては困る(1つのインスタンスで処理する必要があるアプリの)場合は、アプリ自身で二重起動をチェックする必要があります。
アプリの二重起動をチェックする方法として、次のブログ記事がヒットします。
上記ブログ記事に掲載されている Objective-C コード
//自分のプロセスID
pid_t myPID = [[NSProcessInfo processInfo] processIdentifier];
//自分のBundleID
NSString* myBundleID = [[NSBundle mainBundle] bundleIdentifier];
//自分の起動パス
NSURL* myURL = [[NSBundle mainBundle] executableURL];
//現在起動中のアプリケーション群を取得
NSArray* arr = [[NSWorkspace sharedWorkspace] runningApplications];
for (NSRunningApplication* app in arr) {
//自分自身。これは無視した方が良い
if (myPID == app.processIdentifier) {
NSLog(@"It's me!");
}
//同じパスから起動したプロセス。ここで一致するようなら自分自身を終了する
if ([myURL app.executableURL]) {
NSLog(@"It's same File!");
}
//起動パスは違うが同一アプリケーション。必要によってはこのケースでも終了する
if ([app.bundleIdentifier isEqualToString:myBundleID]) {
NSLog(@"It's same App!");
}
}
Swift が登場する前の 2013年の発信ですから、Objective-C で書かれています。これを Swift に書き直したのが次のコードです。
//自分のプロセスID
let myPID = ProcessInfo.processInfo.processIdentifier
//自分のBundleID
let myBundleID = Bundle.main.bundleIdentifier
//自分の起動パス
let myURL = Bundle.main.executableURL
//現在起動中のアプリケーション群を取得
let arr = NSWorkspace.shared.runningApplications
for app in arr {
//自分自身。これは無視した方が良い
if myPID == app.processIdentifier {
NSLog("It's me!")
}
//同じパスから起動したプロセス。ここで一致するようなら自分自身を終了する
if myURL == app.executableURL {
NSLog("It's same File!")
}
//起動パスは違うが同一アプリケーション。必要によってはこのケースでも終了する
if myBundleID == app.bundleIdentifier {
NSLog("It's same App!")
}
}
海外を含むブログやQ&Aのサイトをいろいろ探しましたが、どれも基本は、NSWorkspace.runningApplications
を使用したものでした。
これの使い方については、別な記事で紹介したいと思います。
参考
Windowsアプリの二重起動を防止する方法については、次の記事が参考になります。
以上