serve-sim 0.1.33 → 0.1.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -877,6 +877,8 @@ static void InstallCoreMotionSwizzles(void) {
|
|
|
877
877
|
|
|
878
878
|
#pragma mark - Install
|
|
879
879
|
|
|
880
|
+
static void SimCamInstallPickerSwizzles(void); // defined below
|
|
881
|
+
|
|
880
882
|
void SimCamInstallSwizzles(void) {
|
|
881
883
|
Class dev = [AVCaptureDevice class];
|
|
882
884
|
SwizzleClassMethod(dev,
|
|
@@ -1011,4 +1013,271 @@ void SimCamInstallSwizzles(void) {
|
|
|
1011
1013
|
@selector(simcam_imageWithActions:));
|
|
1012
1014
|
|
|
1013
1015
|
InstallCoreMotionSwizzles();
|
|
1016
|
+
SimCamInstallPickerSwizzles();
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
#pragma mark - UIImagePickerController native-UI bridge
|
|
1020
|
+
|
|
1021
|
+
// UIImagePickerController with sourceType=.camera renders Apple's own UI
|
|
1022
|
+
// in the simulator (CAMPreviewView, CAMDynamicShutterControl, flash & switch
|
|
1023
|
+
// buttons, etc.) but its viewfinder shows a gray "no camera" placeholder
|
|
1024
|
+
// (CAMSnapshotView) and the shutter is permanently disabled because there's
|
|
1025
|
+
// no real camera. We make the picker work by, on viewDidAppear:
|
|
1026
|
+
//
|
|
1027
|
+
// 1. Hiding CAMSnapshotView so our frames (already being pushed into the
|
|
1028
|
+
// picker's AVCaptureVideoPreviewLayer via the existing setSession:
|
|
1029
|
+
// swizzle) show through.
|
|
1030
|
+
// 2. Capturing CAMPreviewView's aspect so the captured photo can be
|
|
1031
|
+
// center-cropped to match the live framing.
|
|
1032
|
+
// 3. Wrapping CAMDynamicShutterControl.delegate to catch
|
|
1033
|
+
// shutterControlTouchAttemptedWhileDisabled: (fired even on a disabled
|
|
1034
|
+
// shutter) and deliver our current frame to picker.delegate.
|
|
1035
|
+
//
|
|
1036
|
+
// All private class names are looked up by string with try/catch — if Apple
|
|
1037
|
+
// renames them in a future iOS, the picker degrades to "back to gray + no
|
|
1038
|
+
// shutter" rather than crashing.
|
|
1039
|
+
//
|
|
1040
|
+
// Credit: this is the approach pioneered by tddworks/baguette's SimCamInject.
|
|
1041
|
+
|
|
1042
|
+
static NSString *const SimCamPickerUTImage = @"public.image";
|
|
1043
|
+
|
|
1044
|
+
// Set during the view-tree walk so SimCamShutterDelegateWrapper can reach
|
|
1045
|
+
// the host picker without changing its delegate-protocol signature.
|
|
1046
|
+
static __weak UIImagePickerController *gSimCamCurrentPicker = nil;
|
|
1047
|
+
static const void *kSimCamShutterWrappedDelegateKey = &kSimCamShutterWrappedDelegateKey;
|
|
1048
|
+
|
|
1049
|
+
static UIImage *SimCamPickerSnapshotImageMirrored(BOOL mirror) {
|
|
1050
|
+
CVPixelBufferRef pb = [[SimCamRegistry shared] currentPixelBuffer];
|
|
1051
|
+
if (!pb) return nil;
|
|
1052
|
+
CIImage *ci = [CIImage imageWithCVPixelBuffer:pb];
|
|
1053
|
+
if (mirror) ci = [ci imageByApplyingOrientation:kCGImagePropertyOrientationUpMirrored];
|
|
1054
|
+
static CIContext *ctx = nil; static dispatch_once_t once;
|
|
1055
|
+
dispatch_once(&once, ^{ ctx = [CIContext contextWithOptions:nil]; });
|
|
1056
|
+
CGImageRef cg = [ctx createCGImage:ci fromRect:ci.extent];
|
|
1057
|
+
CVPixelBufferRelease(pb);
|
|
1058
|
+
if (!cg) return nil;
|
|
1059
|
+
UIImage *img = [UIImage imageWithCGImage:cg];
|
|
1060
|
+
CGImageRelease(cg);
|
|
1061
|
+
return img;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
static void SimCamDeliverFrameToPicker(UIImagePickerController *picker) {
|
|
1065
|
+
if (!picker) return;
|
|
1066
|
+
AVCaptureDevicePosition pos =
|
|
1067
|
+
(picker.cameraDevice == UIImagePickerControllerCameraDeviceFront)
|
|
1068
|
+
? AVCaptureDevicePositionFront
|
|
1069
|
+
: AVCaptureDevicePositionBack;
|
|
1070
|
+
BOOL mirror = SimCamShouldMirror(pos);
|
|
1071
|
+
UIImage *image = SimCamPickerSnapshotImageMirrored(mirror);
|
|
1072
|
+
if (!image) {
|
|
1073
|
+
simcam_log(@"picker shutter: no frame available — skipping delivery");
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1076
|
+
id<UIImagePickerControllerDelegate, UINavigationControllerDelegate> delegate =
|
|
1077
|
+
(id<UIImagePickerControllerDelegate, UINavigationControllerDelegate>)picker.delegate;
|
|
1078
|
+
if (![delegate respondsToSelector:@selector(imagePickerController:didFinishPickingMediaWithInfo:)]) {
|
|
1079
|
+
simcam_log(@"picker shutter: delegate %@ doesn't implement didFinishPickingMediaWithInfo:",
|
|
1080
|
+
NSStringFromClass([(id)delegate class]));
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
NSMutableDictionary *info = [NSMutableDictionary dictionaryWithDictionary:@{
|
|
1084
|
+
UIImagePickerControllerOriginalImage: image,
|
|
1085
|
+
UIImagePickerControllerMediaType: SimCamPickerUTImage,
|
|
1086
|
+
}];
|
|
1087
|
+
// With allowsEditing, Apple shows an edit screen before delivery and
|
|
1088
|
+
// populates editedImage + cropRect. We skip the edit UI, but pass the
|
|
1089
|
+
// image through both keys with a full-image crop so apps that read
|
|
1090
|
+
// editedImage don't get nil.
|
|
1091
|
+
if (picker.allowsEditing) {
|
|
1092
|
+
info[UIImagePickerControllerEditedImage] = image;
|
|
1093
|
+
info[UIImagePickerControllerCropRect] =
|
|
1094
|
+
[NSValue valueWithCGRect:CGRectMake(0, 0, image.size.width, image.size.height)];
|
|
1095
|
+
}
|
|
1096
|
+
simcam_log(@"picker shutter → delivering %.0fx%.0f (edit=%d) to %@",
|
|
1097
|
+
image.size.width, image.size.height, (int)picker.allowsEditing,
|
|
1098
|
+
NSStringFromClass([(id)delegate class]));
|
|
1099
|
+
[delegate imagePickerController:picker didFinishPickingMediaWithInfo:info];
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
// Wraps CAMDynamicShutterControl's delegate. In the simulator the shutter
|
|
1103
|
+
// is always disabled (no real camera), so taps come through as
|
|
1104
|
+
// shutterControlTouchAttemptedWhileDisabled: rather than the usual
|
|
1105
|
+
// short-press selector. Catch both and deliver a frame; forward everything
|
|
1106
|
+
// else to the original delegate via message forwarding so Apple's chrome
|
|
1107
|
+
// keeps working.
|
|
1108
|
+
@interface SimCamShutterDelegateWrapper : NSObject
|
|
1109
|
+
@property (nonatomic, weak) id originalDelegate;
|
|
1110
|
+
@property (nonatomic, weak) UIImagePickerController *picker;
|
|
1111
|
+
@property (nonatomic, assign) BOOL hasDelivered;
|
|
1112
|
+
@end
|
|
1113
|
+
|
|
1114
|
+
@implementation SimCamShutterDelegateWrapper
|
|
1115
|
+
- (void)shutterControlTouchAttemptedWhileDisabled:(id)control {
|
|
1116
|
+
if (self.hasDelivered) return;
|
|
1117
|
+
self.hasDelivered = YES;
|
|
1118
|
+
simcam_log(@"intercepted shutterControlTouchAttemptedWhileDisabled");
|
|
1119
|
+
SimCamDeliverFrameToPicker(self.picker);
|
|
1120
|
+
}
|
|
1121
|
+
- (void)dynamicShutterControlDidShortPress:(id)control {
|
|
1122
|
+
if (self.hasDelivered) return;
|
|
1123
|
+
self.hasDelivered = YES;
|
|
1124
|
+
simcam_log(@"intercepted dynamicShutterControlDidShortPress");
|
|
1125
|
+
SimCamDeliverFrameToPicker(self.picker);
|
|
1126
|
+
}
|
|
1127
|
+
- (BOOL)respondsToSelector:(SEL)sel {
|
|
1128
|
+
return [super respondsToSelector:sel] || [self.originalDelegate respondsToSelector:sel];
|
|
1129
|
+
}
|
|
1130
|
+
- (id)forwardingTargetForSelector:(SEL)sel {
|
|
1131
|
+
if ([self.originalDelegate respondsToSelector:sel]) return self.originalDelegate;
|
|
1132
|
+
return nil;
|
|
1133
|
+
}
|
|
1134
|
+
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
|
|
1135
|
+
NSMethodSignature *sig = [super methodSignatureForSelector:sel];
|
|
1136
|
+
if (sig) return sig;
|
|
1137
|
+
return [(NSObject *)self.originalDelegate methodSignatureForSelector:sel];
|
|
1138
|
+
}
|
|
1139
|
+
@end
|
|
1140
|
+
|
|
1141
|
+
static void SimCamWalkPickerTree(UIView *view) {
|
|
1142
|
+
NSString *cls = NSStringFromClass([view class]);
|
|
1143
|
+
|
|
1144
|
+
// CAMPreviewView is where Apple shows the live viewfinder. On iOS 26
|
|
1145
|
+
// simulator it's force-hidden (no camera), and its inner CALayers have
|
|
1146
|
+
// nil contents (the AVCaptureVideoPreviewLayer was created but never
|
|
1147
|
+
// attached because the system errored). Unhide the view, hide the
|
|
1148
|
+
// "Live Preview" UILabel placeholder, and register the empty content
|
|
1149
|
+
// CALayer with the pump so our frames stream into it.
|
|
1150
|
+
if ([cls isEqualToString:@"CAMPreviewView"]) {
|
|
1151
|
+
if (view.hidden) {
|
|
1152
|
+
view.hidden = NO;
|
|
1153
|
+
simcam_log(@"un-hid CAMPreviewView (%@)", NSStringFromCGRect(view.frame));
|
|
1154
|
+
}
|
|
1155
|
+
// Hide the "Live Preview" placeholder label that Apple ships in the
|
|
1156
|
+
// sim build. Real device doesn't have it; simulator does.
|
|
1157
|
+
for (UIView *sub in view.subviews) {
|
|
1158
|
+
if ([sub isKindOfClass:[UILabel class]] && !sub.hidden) {
|
|
1159
|
+
sub.hidden = YES;
|
|
1160
|
+
simcam_log(@"hid CAMPreviewView UILabel placeholder");
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
// Register the full-size content layer with the pump so it gets
|
|
1164
|
+
// frames pushed into it via setContents:. Tag with the picker's
|
|
1165
|
+
// current cameraDevice so SimCamShouldMirror picks the right axis
|
|
1166
|
+
// (front → mirrored, back → not).
|
|
1167
|
+
for (CALayer *sub in view.layer.sublayers) {
|
|
1168
|
+
if (CGRectEqualToRect(sub.frame, view.bounds) ||
|
|
1169
|
+
(sub.frame.size.width >= view.bounds.size.width * 0.95 &&
|
|
1170
|
+
sub.frame.size.height >= view.bounds.size.height * 0.95)) {
|
|
1171
|
+
AVCaptureDevicePosition pos = AVCaptureDevicePositionBack;
|
|
1172
|
+
if (gSimCamCurrentPicker.cameraDevice ==
|
|
1173
|
+
UIImagePickerControllerCameraDeviceFront) {
|
|
1174
|
+
pos = AVCaptureDevicePositionFront;
|
|
1175
|
+
}
|
|
1176
|
+
SimCamSetPosition(sub, pos);
|
|
1177
|
+
[[SimCamRegistry shared] addPreviewLayer:(AVCaptureVideoPreviewLayer *)sub];
|
|
1178
|
+
simcam_log(@"registered CAMPreviewView content layer %@ pos=%d",
|
|
1179
|
+
NSStringFromClass([sub class]), (int)pos);
|
|
1180
|
+
break;
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
// CAMSnapshotView is a full-screen sibling that covers everything with
|
|
1186
|
+
// a gray "viewfinder closed" image. Hide it so the preview shows.
|
|
1187
|
+
if ([cls isEqualToString:@"CAMSnapshotView"] && !view.hidden) {
|
|
1188
|
+
view.hidden = YES;
|
|
1189
|
+
simcam_log(@"hid CAMSnapshotView to clear gray cover");
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
// CAMDynamicShutterControl — wrap its delegate so taps on the
|
|
1193
|
+
// (disabled) shutter still deliver a frame. Apple reuses the same
|
|
1194
|
+
// control instance across picker presentations, so re-seat picker and
|
|
1195
|
+
// reset hasDelivered on every walk; otherwise the second shot would be
|
|
1196
|
+
// silently dropped.
|
|
1197
|
+
if ([cls isEqualToString:@"CAMDynamicShutterControl"] && gSimCamCurrentPicker) {
|
|
1198
|
+
@try {
|
|
1199
|
+
SimCamShutterDelegateWrapper *existing =
|
|
1200
|
+
objc_getAssociatedObject(view, kSimCamShutterWrappedDelegateKey);
|
|
1201
|
+
id currentDelegate = [view valueForKey:@"delegate"];
|
|
1202
|
+
if (existing) {
|
|
1203
|
+
existing.picker = gSimCamCurrentPicker;
|
|
1204
|
+
existing.hasDelivered = NO;
|
|
1205
|
+
if (currentDelegate != existing) {
|
|
1206
|
+
existing.originalDelegate = currentDelegate;
|
|
1207
|
+
[view setValue:existing forKey:@"delegate"];
|
|
1208
|
+
simcam_log(@"re-seated shutter wrapper (orig: %@)",
|
|
1209
|
+
NSStringFromClass([currentDelegate class]));
|
|
1210
|
+
}
|
|
1211
|
+
} else {
|
|
1212
|
+
SimCamShutterDelegateWrapper *wrapper = [SimCamShutterDelegateWrapper new];
|
|
1213
|
+
wrapper.originalDelegate = currentDelegate;
|
|
1214
|
+
wrapper.picker = gSimCamCurrentPicker;
|
|
1215
|
+
objc_setAssociatedObject(view, kSimCamShutterWrappedDelegateKey, wrapper,
|
|
1216
|
+
OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
|
1217
|
+
[view setValue:wrapper forKey:@"delegate"];
|
|
1218
|
+
simcam_log(@"hijacked %@.delegate (orig: %@)",
|
|
1219
|
+
cls, NSStringFromClass([currentDelegate class]));
|
|
1220
|
+
}
|
|
1221
|
+
} @catch (NSException *e) {
|
|
1222
|
+
simcam_log(@"failed to hijack shutter delegate: %@", e);
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
for (UIView *child in view.subviews) SimCamWalkPickerTree(child);
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
@interface UIImagePickerController (SimCam)
|
|
1230
|
+
@end
|
|
1231
|
+
@implementation UIImagePickerController (SimCam)
|
|
1232
|
+
|
|
1233
|
+
+ (BOOL)simcam_isSourceTypeAvailable:(UIImagePickerControllerSourceType)t {
|
|
1234
|
+
if (t == UIImagePickerControllerSourceTypeCamera) return YES;
|
|
1235
|
+
return [self simcam_isSourceTypeAvailable:t];
|
|
1236
|
+
}
|
|
1237
|
+
+ (NSArray<NSString *> *)simcam_availableMediaTypesForSourceType:(UIImagePickerControllerSourceType)t {
|
|
1238
|
+
if (t == UIImagePickerControllerSourceTypeCamera) return @[SimCamPickerUTImage];
|
|
1239
|
+
return [self simcam_availableMediaTypesForSourceType:t];
|
|
1240
|
+
}
|
|
1241
|
+
+ (NSArray<NSNumber *> *)simcam_availableCaptureModesForCameraDevice:(UIImagePickerControllerCameraDevice)d {
|
|
1242
|
+
(void)d; return @[ @(UIImagePickerControllerCameraCaptureModePhoto) ];
|
|
1243
|
+
}
|
|
1244
|
+
+ (BOOL)simcam_isCameraDeviceAvailable:(UIImagePickerControllerCameraDevice)d { (void)d; return YES; }
|
|
1245
|
+
+ (BOOL)simcam_isFlashAvailableForCameraDevice:(UIImagePickerControllerCameraDevice)d { (void)d; return NO; }
|
|
1246
|
+
|
|
1247
|
+
- (void)simcam_viewDidAppear:(BOOL)animated {
|
|
1248
|
+
[self simcam_viewDidAppear:animated];
|
|
1249
|
+
if (self.sourceType != UIImagePickerControllerSourceTypeCamera) return;
|
|
1250
|
+
gSimCamCurrentPicker = self;
|
|
1251
|
+
SimCamWalkPickerTree(self.view);
|
|
1252
|
+
gSimCamCurrentPicker = nil;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
@end
|
|
1256
|
+
|
|
1257
|
+
static void SimCamInstallPickerSwizzles(void) {
|
|
1258
|
+
// method_exchangeImplementations is its own inverse — a second call
|
|
1259
|
+
// would un-install. Guard with dispatch_once.
|
|
1260
|
+
static dispatch_once_t once;
|
|
1261
|
+
dispatch_once(&once, ^{
|
|
1262
|
+
Class picker = [UIImagePickerController class];
|
|
1263
|
+
SwizzleClassMethod(picker,
|
|
1264
|
+
@selector(isSourceTypeAvailable:),
|
|
1265
|
+
@selector(simcam_isSourceTypeAvailable:));
|
|
1266
|
+
SwizzleClassMethod(picker,
|
|
1267
|
+
@selector(availableMediaTypesForSourceType:),
|
|
1268
|
+
@selector(simcam_availableMediaTypesForSourceType:));
|
|
1269
|
+
SwizzleClassMethod(picker,
|
|
1270
|
+
@selector(availableCaptureModesForCameraDevice:),
|
|
1271
|
+
@selector(simcam_availableCaptureModesForCameraDevice:));
|
|
1272
|
+
SwizzleClassMethod(picker,
|
|
1273
|
+
@selector(isCameraDeviceAvailable:),
|
|
1274
|
+
@selector(simcam_isCameraDeviceAvailable:));
|
|
1275
|
+
SwizzleClassMethod(picker,
|
|
1276
|
+
@selector(isFlashAvailableForCameraDevice:),
|
|
1277
|
+
@selector(simcam_isFlashAvailableForCameraDevice:));
|
|
1278
|
+
SwizzleInstanceMethod(picker,
|
|
1279
|
+
@selector(viewDidAppear:),
|
|
1280
|
+
@selector(simcam_viewDidAppear:));
|
|
1281
|
+
simcam_log(@"UIImagePickerController swizzles installed");
|
|
1282
|
+
});
|
|
1014
1283
|
}
|
package/bin/serve-sim-bin
CHANGED
|
Binary file
|