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