Skip to content

Commit 4c323d8

Browse files
authored
feat(parser): use JSON output for iOS simulator parsing (#143)
* feat(parser): use JSON output for iOS simulator parsing Add DeviceFamily enum to properly identify device types (iPhone, iPad, Watch, TV, Vision) from simctl deviceTypeIdentifier. This enables correct icon selection without relying on name matching. - Switch simctl to JSON output (-j flag) - Parse deviceTypeIdentifier for reliable device family detection - Update menu item to use deviceFamily for icon selection - Add comprehensive tests for new parsing logic * refactor(ui): use visionpro SF Symbol instead of custom asset * test(parser): replace force unwraps with guard statements * style: apply swiftlint auto-fixes * test(parser): use XCTUnwrap to reduce function body length
1 parent bf28acd commit 4c323d8

14 files changed

Lines changed: 219 additions & 109 deletions

File tree

MiniSim.xcodeproj/project.pbxproj

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@
107107
76F2A914299050F9002D4EF6 /* UserDefaults+Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76F2A913299050F9002D4EF6 /* UserDefaults+Configuration.swift */; };
108108
76F2A9172991B7B6002D4EF6 /* ViewModifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76F2A9162991B7B6002D4EF6 /* ViewModifiers.swift */; };
109109
76FCABAB29B390D5003BBF9A /* Collection+get.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76FCABAA29B390D5003BBF9A /* Collection+get.swift */; };
110+
7D4142B4F3AF1D150D9222BD /* DeviceFamily.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F78DD26087D5448AAB2CC3B /* DeviceFamily.swift */; };
110111
9B225A9C2C7E360D002620BA /* DeviceType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B225A9B2C7E360D002620BA /* DeviceType.swift */; };
111112
/* End PBXBuildFile section */
112113

@@ -129,6 +130,7 @@
129130
52B363ED2AEC10B3006F515C /* ParametersTableFormViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParametersTableFormViewModel.swift; sourceTree = "<group>"; };
130131
551B88292B1385E900B8D325 /* Terminal.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Terminal.swift; sourceTree = "<group>"; };
131132
55CDB0772B1B6D24002418D7 /* TerminalApps.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalApps.swift; sourceTree = "<group>"; };
133+
5F78DD26087D5448AAB2CC3B /* DeviceFamily.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DeviceFamily.swift; sourceTree = "<group>"; };
132134
760554A22C085BEA001607FE /* Thread+Asserts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Thread+Asserts.swift"; sourceTree = "<group>"; };
133135
76059BF42AD4361C0008D38B /* SetupPreferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupPreferences.swift; sourceTree = "<group>"; };
134136
76059BF62AD449DC0008D38B /* OnboardingHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingHeader.swift; sourceTree = "<group>"; };
@@ -304,6 +306,7 @@
304306
7631218A2A12AFBC00EE7F48 /* Platform.swift */,
305307
76F269862A2A39D100424BDA /* Variables.swift */,
306308
9B225A9B2C7E360D002620BA /* DeviceType.swift */,
309+
5F78DD26087D5448AAB2CC3B /* DeviceFamily.swift */,
307310
);
308311
path = Model;
309312
sourceTree = "<group>";
@@ -721,6 +724,7 @@
721724
76BF0AEE2C905C43003BE568 /* IOSDeviceService.swift in Sources */,
722725
7645D4BE2982A1B100019227 /* DeviceService.swift in Sources */,
723726
765ABF382A8BECD900A063CB /* ExecuteCommand.swift in Sources */,
727+
7D4142B4F3AF1D150D9222BD /* DeviceFamily.swift in Sources */,
724728
);
725729
runOnlyForDeploymentPostprocessing = 0;
726730
};
@@ -745,7 +749,7 @@
745749
/* Begin PBXTargetDependency section */
746750
4A78928A2AF1A9A3004D3FC8 /* PBXTargetDependency */ = {
747751
isa = PBXTargetDependency;
748-
productRef = 4A7892892AF1A9A3004D3FC8 /* SwiftLintPlugin */;
752+
productRef = 4A7892892AF1A9A3004D3FC8 /* plugin:SwiftLintPlugin */;
749753
};
750754
76B70F792B0D359D009D87A4 /* PBXTargetDependency */ = {
751755
isa = PBXTargetDependency;
@@ -754,7 +758,7 @@
754758
};
755759
76B70F802B0D4F9D009D87A4 /* PBXTargetDependency */ = {
756760
isa = PBXTargetDependency;
757-
productRef = 76B70F7F2B0D4F9D009D87A4 /* SwiftLintPlugin */;
761+
productRef = 76B70F7F2B0D4F9D009D87A4 /* plugin:SwiftLintPlugin */;
758762
};
759763
/* End PBXTargetDependency section */
760764

@@ -1106,7 +1110,7 @@
11061110
/* End XCRemoteSwiftPackageReference section */
11071111

11081112
/* Begin XCSwiftPackageProductDependency section */
1109-
4A7892892AF1A9A3004D3FC8 /* SwiftLintPlugin */ = {
1113+
4A7892892AF1A9A3004D3FC8 /* plugin:SwiftLintPlugin */ = {
11101114
isa = XCSwiftPackageProductDependency;
11111115
package = 4A7892862AF1A767004D3FC8 /* XCRemoteSwiftPackageReference "SwiftLint" */;
11121116
productName = "plugin:SwiftLintPlugin";
@@ -1141,7 +1145,7 @@
11411145
package = 76AC9AF72A0EB50800864A8B /* XCRemoteSwiftPackageReference "SymbolPicker" */;
11421146
productName = SymbolPicker;
11431147
};
1144-
76B70F7F2B0D4F9D009D87A4 /* SwiftLintPlugin */ = {
1148+
76B70F7F2B0D4F9D009D87A4 /* plugin:SwiftLintPlugin */ = {
11451149
isa = XCSwiftPackageProductDependency;
11461150
package = 4A7892862AF1A767004D3FC8 /* XCRemoteSwiftPackageReference "SwiftLint" */;
11471151
productName = "plugin:SwiftLintPlugin";

MiniSim/Assets.xcassets/vision_os.imageset/Contents.json

Lines changed: 0 additions & 21 deletions
This file was deleted.
-1.7 KB
Binary file not shown.

MiniSim/Extensions/NSMenuItem+ImageInit.swift

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,21 @@ extension NSMenuItem {
1313
action: Selector?,
1414
keyEquivalent: String,
1515
type: DeviceMenuItem,
16+
deviceFamily: DeviceFamily? = nil,
1617
image: NSImage? = nil
1718
) {
1819
self.init(title: title, action: action, keyEquivalent: keyEquivalent)
1920

2021
if let image {
2122
self.image = image
23+
} else if let deviceFamily {
24+
self.image = NSImage(
25+
systemSymbolName: deviceFamily.iconName,
26+
accessibilityDescription: title
27+
)
2228
} else {
23-
if title.contains("Vision") {
24-
self.image = NSImage(named: "vision_os")
25-
self.image?.isTemplate = true
26-
self.image?.size = NSSize(width: 15, height: 8.5)
27-
} else {
28-
let imageName = self.getSystemImageFromName(name: title)
29-
self.image = NSImage(systemSymbolName: imageName, accessibilityDescription: title)
30-
}
29+
let imageName = self.getSystemImageFromName(name: title)
30+
self.image = NSImage(systemSymbolName: imageName, accessibilityDescription: title)
3131
}
3232

3333
self.tag = type.rawValue
@@ -42,7 +42,7 @@ extension NSMenuItem {
4242
return "ipad.landscape"
4343
}
4444

45-
if name.contains("Watch") {
45+
if name.contains("Apple Watch") {
4646
return "applewatch"
4747
}
4848

MiniSim/Menu.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,8 @@ class Menu: NSMenu {
225225
title: device.displayName,
226226
action: #selector(deviceItemClick),
227227
keyEquivalent: "",
228-
type: device.platform == .ios ? .launchIOS : .launchAndroid
228+
type: device.platform == .ios ? .launchIOS : .launchAndroid,
229+
deviceFamily: device.deviceFamily
229230
)
230231

231232
menuItem.target = self

MiniSim/Model/Device.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ struct Device: Hashable, Codable {
77
var booted: Bool
88
var platform: Platform
99
var type: DeviceType
10+
var deviceFamily: DeviceFamily?
1011

1112
var displayName: String {
1213
switch platform {
@@ -22,7 +23,7 @@ struct Device: Hashable, Codable {
2223
}
2324

2425
enum CodingKeys: String, CodingKey {
25-
case name, version, identifier, booted, platform, displayName, type
26+
case name, version, identifier, booted, platform, displayName, type, deviceFamily
2627
}
2728

2829
init(
@@ -31,14 +32,16 @@ struct Device: Hashable, Codable {
3132
identifier: String?,
3233
booted: Bool = false,
3334
platform: Platform,
34-
type: DeviceType
35+
type: DeviceType,
36+
deviceFamily: DeviceFamily? = nil
3537
) {
3638
self.name = name
3739
self.version = version
3840
self.identifier = identifier
3941
self.booted = booted
4042
self.platform = platform
4143
self.type = type
44+
self.deviceFamily = deviceFamily
4245
}
4346

4447
init(from decoder: Decoder) throws {
@@ -49,6 +52,7 @@ struct Device: Hashable, Codable {
4952
booted = try values.decode(Bool.self, forKey: .booted)
5053
platform = try values.decode(Platform.self, forKey: .platform)
5154
type = try values.decode(DeviceType.self, forKey: .type)
55+
deviceFamily = try values.decodeIfPresent(DeviceFamily.self, forKey: .deviceFamily)
5256
}
5357

5458
func encode(to encoder: Encoder) throws {
@@ -60,5 +64,6 @@ struct Device: Hashable, Codable {
6064
try container.encode(platform, forKey: .platform)
6165
try container.encode(displayName, forKey: .displayName)
6266
try container.encode(type, forKey: .type)
67+
try container.encodeIfPresent(deviceFamily, forKey: .deviceFamily)
6368
}
6469
}

MiniSim/Model/DeviceFamily.swift

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
//
2+
// DeviceFamily.swift
3+
// MiniSim
4+
//
5+
// Created by Oskar Kwaśniewski on 17/01/2026.
6+
//
7+
8+
import Foundation
9+
10+
enum DeviceFamily: String, Codable {
11+
case iPhone
12+
case iPad
13+
case watch
14+
// swiftlint:disable:next identifier_name
15+
case tv
16+
case vision
17+
case unknown
18+
19+
var iconName: String {
20+
switch self {
21+
case .iPhone:
22+
return "iphone"
23+
case .iPad:
24+
return "ipad.landscape"
25+
case .watch:
26+
return "applewatch"
27+
case .tv:
28+
return "appletv.fill"
29+
case .vision:
30+
return "visionpro"
31+
case .unknown:
32+
return "iphone"
33+
}
34+
}
35+
36+
/// Parses `deviceTypeIdentifier` from simctl JSON output.
37+
/// Example: `com.apple.CoreSimulator.SimDeviceType.iPhone-14-Pro`
38+
init(fromDeviceTypeIdentifier identifier: String) {
39+
if identifier.contains("iPhone") {
40+
self = .iPhone
41+
} else if identifier.contains("iPad") {
42+
self = .iPad
43+
} else if identifier.contains("Apple-Watch") {
44+
self = .watch
45+
} else if identifier.contains("Apple-TV") {
46+
self = .tv
47+
} else if identifier.contains("Apple-Vision") {
48+
self = .vision
49+
} else {
50+
self = .unknown
51+
}
52+
}
53+
}

MiniSim/Service/ActionExecutor.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import Foundation
21
import AppKit
2+
import Foundation
33

44
class ActionExecutor {
55
private let queue: DispatchQueue

MiniSim/Service/ActionFactory.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,3 @@ class IOSActionFactory: ActionFactory {
4848
}
4949
}
5050
}
51-

MiniSim/Service/DeviceDiscoveryService.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ class IOSDeviceDiscovery: DeviceDiscoveryService {
8585
func getIOSSimulators() throws -> [Device] {
8686
let output = try shell.execute(
8787
command: DeviceConstants.ProcessPaths.xcrun.rawValue,
88-
arguments: ["simctl", "list", "devices", "available"]
88+
arguments: ["simctl", "list", "devices", "available", "-j"]
8989
)
9090
return DeviceParserFactory().getParser(.iosSimulator).parse(output)
9191
}

0 commit comments

Comments
 (0)