DEV Community

Cover image for How to create an iOS app that takes secret photos while the iPhone screen seems to be turned off
Arik Segal
Arik Segal

Posted on

How to create an iOS app that takes secret photos while the iPhone screen seems to be turned off

Problem

Suppose you want to repeatedly take secret photos of someone, without being caught. What would be the best iPhone application for doing that? Would the built-in Camera app be sufficient?

There are may things to consider. For start, you can get caught if someone looks over your shoulder and notices that your screen is displaying a camera feed. So, you will need to make sure nobody is behind you when you shoot. Moreover, the tap gesture that is required in order to take a photo is distinguishable from other common gestures (scrolling, texting). A person can guess that you’re taking a photo, just from looking at your hands and body language.

Another thing to consider is the green camera-in-use indicator. When an iOS application is about to take photos, it must first display a prompt similar to this:

Image description

Once the user confirms, an application can take photos at any given moment, without having to supply any visible indication that a photo is being taken. Since taking photos of users secretly is morally wrong (we don’t want Gmail or Instagram to take secret photos of us), a green camera-in-use indicator was added since iOS 14:

Image description

The green indicator becomes green once the camera is active, even if the photos aren’t saved nor transmitted anywhere. So, even if we have a program that just takes a photo without displaying anything particular on screen, the green indicator would disclose the fact that the camera is active.

Finally, iOS has a special sound effect to let everyone know that a photo is taken:

The only way to take a photo without this sound is to totally mute the volume while shooting.


Solution

I’ve been studying this and finally created a small program that solves all of the above issues, and enables the user to take secret photos with more confidence.

  1. The program displays a black screen (no camera feed).
  2. The program repeatedly takes photos with a one second time interval (no user gesture is needed).
  3. Since the program controls the screen’s brightness and reduces it to the minimal possible value, the camera-in-use indicator is almost invisible, even if it is green, and by whole, the screen appears to be turned off.
  4. No need to mute the device: even when the device is set to high volume, the camera shooter sound is muted programatically.
  5. For a quick escape, touching the screen would immediately cause the program to exit back to the iPhone’s home screen.

Image description

It is important to note that I do not encourage anyone to actually use this program. After all, it is evil. The purpose of this project is just to demonstrate how easy it is to create such a program. Furthermore, the code only uses well known system API’s without accessing any private system methods, so theoretically, it can pass AppStore review (if embedded as a secret module within a bigger app that has some other valuable content).

The program’s code is publicly available on GitHub in the following address:

https://github.com/arixegal/BlackEye/tree/MediumTutorial

However, in this article I will demonstrate how to create this program, step by step. A basic knowledge of the Swift programming language is required.


General settings

We will need to create an xCode project, and a blank single page application.

  1. Open a modern version of Xcode.
  2. Choose “Create a new Xcode Project”.
  3. In the Available Templates window Choose the “iOS -> App” template

Image description

Enter a name for the project, a team and an Organization Identifier. The language should be Swift and the Interface should be Storyboard.

Image description

This will create an application that displays a blank screen. One step closer to the end game. In order to test it, type Command+R or select Product->Run.

How to display a black screen

Since our app’s interface is Storyboard-based, and we already have the view controller that manages the display of the main screen, in order to display a black screen, all we have to do is change the background of this screen to black. This can be done directly from Interface Builder.

  1. In Project Navigator, select the file Main.storyboard
  2. In the storyboard’s Document Outline, select the View element (under “View Controller Scene” -> “View Controller”)
  3. In the view’s Attributes Inspector, change the Background to black.

Image description

This will create an application that displays a black screen. In order to test it, type Command+R or select Product->Run. It is important to select the color Black, as opposed to other colors that appear to be black, such as Label Color. Choosing Label Color instead of Black as a background color, will result in a black or a white screen, depending on the iPhone’s Dark Mode settings. Choosing Black will always result in a black screen, regardless of the iPhone’s Dark or Light mode.

How to hide the Status Bar

If we run the app now, we will probably see a completely black screen. However, if the simulator or device we run the app on is set to Dark Mode, the screen wouldn’t be completely black. Dark Mode is achieved by selecting “Dark” under Settings -> Display & Brightness -> Appearance.

Image description

When we run our app now, while the device is in Dark mode, The top Status Bar displays various elements, such as the time, power and WiFi indicators. When in Light Mode, the status bar elements were all black, so we couldn’t see them, but now since we are in Dark mode, they are light and visible.

Image description

We want the screen to appear as if it is turned off. This means we have to hide the status bar. This can be achieved by adding two new key-value pairs to the target properties.

  1. In Project Navigator, select the top-most elements, which would be the project itself.
  2. While the project is selected, under TARGETS, select the main target and navigate to the Info tab.
  3. Click the Plus button which becomes visible while hovering the mouse over the list items, and add the following key: UIViewControllerBasedStatusBarAppearance
  4. Set the value of the newly added key to NO
  5. Add another key: UIStatusBarHidden
  6. Set the value of the above newly added key to YES

You will notice that once the keys are added and displayed, the name UIStatusBarHidden is displayed as “Status bar is initially hidden”, and the name UIViewControllerBasedStatusBarAppearance is displayed as “View controller-based status bar appearance”. This is OK.

Image description

When running the app now, you will see that the screen is completely black, and all the status bar elements are missing, regardless of the iPhone’s Dark or Light mode. This is exactly what we want.

How to respond to a tap event

When tapping the screen, we would like the app to completely exit. For that, we need to listen to taps. There are many ways of doing that. In this project we will add a black button programatically.

  1. In Project navigator, select ViewConroller.swift
  2. Add the following code right under the class declaration:
private lazy var curtainView: UIView = {
    let size = UIScreen.main.bounds.size
    let btn = UIButton(
        frame: CGRect(
            x: 0,
            y: 0,
            width: size.width,
            height: size.height
         )
    )
    btn.backgroundColor = UIColor.black 
    btn.addTarget(self, action: #selector(quit), for: .touchDown)     

    return btn
}()
Enter fullscreen mode Exit fullscreen mode
  1. Add the quit method
/// Will quit the application with animation
@objc private func quit() {
    UIApplication.shared.perform(#selector(NSXPCConnection.suspend))
    /// Sleep for a while to let the app goes in background
    sleep(2)
    exit(0)
}
Enter fullscreen mode Exit fullscreen mode
  1. Add the button to the view
override func viewDidLoad() {
    super.viewDidLoad()
    view.addSubview(curtainView)
}
Enter fullscreen mode Exit fullscreen mode

The above quit method is copied from one of the answers in the following discussion:

https://stackoverflow.com/questions/355168/proper-way-to-exit-iphone-application

Try running the app and test the behavior when the screen is tapped. If everything is done correctly, a tap on the screen would cause the app to exit.

How to take a photo

There are many online tutorials and examples of how to operate the iPhone cameras programatically. Most of them have too much code for our purpose, since the conventional way of operating the cameras involves displaying the camera feed on the screen, which in our case is not needed.

  1. In ViewController.swift, add the line import AVFoundation at the top of the file.
  2. Add the following two instances right under the class declaration:
private let photoOutput = AVCapturePhotoOutput()
private let session = AVCaptureSession()
Enter fullscreen mode Exit fullscreen mode

Add the following 2 methods:

private func setupCaptureSession() -> AVCaptureSession? {
    session.sessionPreset = .photo
    guard let cameraDevice = AVCaptureDevice.default(for: .video) else {           
        print("Unable to fetch default camera")
        return nil
    }
    guard let videoInput = try? AVCaptureDeviceInput(device: cameraDevice) else {
        print("Unable to establish video input")
        return nil
    }
    session.beginConfiguration()
        session.sessionPreset = AVCaptureSession.Preset.photo 
        guard session.canAddInput(videoInput) else {
            print("Unable to add videoInput to captureSession")
            return nil
        }

        session.addInput(videoInput)

        guard session.canAddOutput(photoOutput) else {
            print("Unable to add videoOutput to captureSession")
            return nil
        }

        session.addOutput(photoOutput)
        photoOutput.isHighResolutionCaptureEnabled = true
     session.commitConfiguration()
     DispatchQueue.global(qos: .background).async { [weak self] in
         self?.session.startRunning()
     }
     return session
}
private func takePhoto() {
    photoOutput.capturePhoto(with: AVCapturePhotoSettings(), delegate: self)
    DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .seconds(1)) {[weak self] in
        self?.takePhoto()
    }
}
Enter fullscreen mode Exit fullscreen mode

After adding the methods, you will notice that the project won’t compile. The problem is that we passed self as a delegate to the capturePhoto method, but self, which is in this case the UIViewController instance, doesn’t support the AVCapturePhotoCaptureDelegate protocol. We will need this support later on in order to save the photo and mute the sound. Keeping this in mind, in order to resolve the compilation error we will add the following stubs:

extension ViewController: AVCapturePhotoCaptureDelegate {
    func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {}
    func photoOutput(_ output: AVCapturePhotoOutput, willCapturePhotoFor resolvedSettings: AVCaptureResolvedPhotoSettings) {}
}
Enter fullscreen mode Exit fullscreen mode

So what do we have so far? two methods, one for configuring the camera session and the other for taking a photo (repeatedly with a one second interval). But we still need to connect them, so the final step is adding the first call:

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated) 
    if UIImagePickerController.isSourceTypeAvailable(.camera) { 
        if let _ = setupCaptureSession() {
            takePhoto()
        } else {
            print("Failed to establish capture session")
        }
    } else {
        print("Camera not available") // Would be true in Simulator
    }
}
Enter fullscreen mode Exit fullscreen mode

When running the project now the build should succeed. However, after adding the first call we have a runtime crush. This app has crashed because it attempted to access privacy-sensitive data without a usage description. To resolve this, we need to add some more key value pairs to the target. While we’re here, we will add a usage description for accessing the camera and another usage description for saving a photo into the photo album (which will be implemented later on).

  1. In Project Navigator, select the top-most elements, which would be the project itself.
  2. While the project is selected, under TARGETS, select the main target and navigate to the Info tab.
  3. Click the Plus button which becomes visible while hovering the mouse over the list items, and add the following (string) key: NSCameraUsageDescription
  4. Set the value of the newly added key to “In order to take secret photos”.
  5. Add another (string) key: NSPhotoLibraryAddUsageDescription
  6. Set the value of the above newly added key to “In order to store secret photos”

When running the app now, you should be able to see the Camera Access permission dialogue. Once authorized, you would be able to hear the camera shooter sound effect, and see the green camera in use indicator activated.

Image description

How to store the photo

In this program we will store each photo into the Recent photo album. If we wanted, we could have just as well store them on a remote server, but this requires configuring a back-end and is beyond the purpose of this demo.

Storing the photos is achieved by calling the system method UIImageWriteToSavedPhotosAlbum.

  1. Find the empty delegate method didFinishProcessingPhoto which was added earlier, and insert the following body:
func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
    guard let data = photo.fileDataRepresentation() else {
        print("Processing did finish with no data")
        return
    }
    guard let image = UIImage(data: data) else {
        print("Processing did finish with invalid image data")
        return
    }
    UIImageWriteToSavedPhotosAlbum(
        image, 
        self,  
        #selector(image(_:didFinishSavingWithError:contextInfo:)),           
        nil)

    print("Photo taken") 
}
Enter fullscreen mode Exit fullscreen mode

Add the following method to get a success / failure status:

@objc func image(_ image: UIImage, didFinishSavingWithError error: NSError?, contextInfo: UnsafeRawPointer) {
    if let error = error {
        print("Save error: \(error.localizedDescription)")
    } else {
        print("Saved!")
    }
}
Enter fullscreen mode Exit fullscreen mode

When running the app now, you should be able to see the Gallery Access permission dialogue. Once authorized, you will be able to see the new photos in the “Photos” app. Try running the app for a few seconds and see if the photos are added.

How to hide the Camera-in-Use indicator

iOS applications can control the screen brightness. Setting the screen brightness to the minimal possible value while the app is operating would cause the camera-in-use indicator to be barely-visible.

We will add a separate class dedicated to controlling the screen brightness:

final class DimUnDim {
    static let shared = DimUnDim()
    private var originalBrightness = UIScreen.main.brightness
    func dim() {
        print("dim")
        UIScreen.main.wantsSoftwareDimming = true
        UIScreen.main.brightness = 0.0
    }
    func unDim() {
        print("unDim")
        UIScreen.main.brightness = originalBrightness
    }
}
Enter fullscreen mode Exit fullscreen mode

We will call Dim when the app becomes active and call UnDim when the app is not active. Edit the file **SceneDelegate.swift **and add the following:

func sceneDidBecomeActive(_ scene: UIScene) {
    DimUnDim.shared.dim()
}
func sceneWillResignActive(_ scene: UIScene) {     
    DimUnDim.shared.unDim()
}
Enter fullscreen mode Exit fullscreen mode

This is good as long as the app is running. If it is active the screen is dark, but if the user switches to the home screen or to another app, the screen brightness retains the original value.

However, if the user manually terminates the app by touching the screen, the screen will remain dark because sceneWillResignActive is not called. This is inpolite. We will fix this by adding another call to UnDim before quitting.

Find the Quit method which as added earlier and add the call:

/// Will quit the application with animation
@objc private func quit() {
    DimUnDim.shared.unDim() // Restore normal screen brightness
    UIApplication.shared.perform(#selector(NSXPCConnection.suspend))
    /// Sleep for a while to let the app go in background
    sleep(2)
    exit(0)
}
Enter fullscreen mode Exit fullscreen mode

When running the project now, screen brightness should behave exactly as we need it to behave.

How to disable the shooter sound

Find the empty delegate method willCapturePhotoFor which was added earlier, and insert the following body:

func photoOutput(_ output: AVCapturePhotoOutput, willCapturePhotoFor resolvedSettings: AVCaptureResolvedPhotoSettings) {
    // dispose system shutter sound
    AudioServicesDisposeSystemSoundID(1108)
}
Enter fullscreen mode Exit fullscreen mode

The above method is copied from one of the answers in the following discussion:

https://stackoverflow.com/questions/4401232/avfoundation-how-to-turn-off-the-shutter-sound-when-capturestillimageasynchrono

This is it.


Final notes

The ability to take secret photos as demonstrated in this project goes against the current ongoing trend of protecting user privacy. As demonstrated, the green camera-in-use indicator can be manipulated into non-visibility by controlling the screen brightness. This can be prevented by Apple in future versions of iOS. There shouldn’t be any problem to crash an app that tries to reduce screen brightness while the camera is active (just as we saw that the app is crashed when we failed to produce the camera-usage-description).

Would it be possible to create a similar program that records a video instead of taking still photos? I don’t know, but it’s worth trying.

Top comments (0)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.