如何创建一个iOS应用程序,该应用在iPhone屏幕似乎关闭时拍摄秘密照片
#ios #swift #privacy

问题

假设您想反复拍摄某人的秘密照片,而不会被抓住。最好的iPhone应用程序是什么?内置的相机应用程序足够吗?

有可能要考虑的事情。首先,如果有人看着您的肩膀并注意到您的屏幕正在显示相机供稿,您可能会被抓住。因此,您需要确保在拍摄时没有人在您身后。此外,为了拍照所需的水龙头手势与其他常见的手势(滚动,短信)可以区分。一个人可以猜测您只是从看手和肢体语言来拍照。

要考虑的另一件事是使用绿色摄像头指示器。当iOS应用程序即将拍照时,它必须首先显示与此类似的提示:

Image description

用户确认,应用程序可以在任何给定的时刻拍摄照片,而无需提供任何可见的迹象。由于在道德上秘密拍摄用户的照片是错误的(我们不希望Gmail或Instagram拍摄我们的秘密照片),因此自iOS 14:

以来添加了绿色摄像头指示器

Image description

一旦相机活跃,绿色指示器就会变为绿色,即使照片保存或在任何地方传输。因此,即使我们有一个只在屏幕上显示任何特殊内容的程序,绿色指标也会披露相机处于活动状态的事实。

最后,iOS具有特殊的声音效果,让所有人都知道拍摄照片:

没有这种声音的照片拍照的唯一方法是在拍摄时完全静音。


解决方案

我一直在研究这一点,并最终创建了一个解决上述所有问题的小程序,并使用户能够更加自信地拍摄秘密照片。

  1. 该程序显示黑屏(没有相机提要)。
  2. 该程序反复使用一秒钟间隔拍照(不需要用户手势)。
  3. 由于该程序控制屏幕的亮度并将其降低到最小可能的值,因此即使是绿色的,使用摄像机的指示器几乎是看不见的,而且总体上,屏幕似乎已关闭。
  4. 无需静音设备:即使设备设置为高音量,相机射击器声音也会在编程中被静音。
  5. 为了快速逃脱,触摸屏幕将立即导致该程序退回iPhone的主屏幕。

Image description

重要的是要注意,我不鼓励任何人实际使用此程序。毕竟,它是邪恶。该项目的目的只是证明创建这样的程序是多么容易。此外,该代码仅使用众所周知的系统API,而无需访问任何私人系统方法,因此从理论上讲,它可以通过AppStore评论(如果在具有其他有价值内容的较大应用程序中嵌入为秘密模块)。

该程序的代码在以下地址上在GitHub上公开可用:

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

但是,在本文中,我将演示如何逐步创建此程序。需要对快速编程语言的基本知识。


一般设置

我们需要创建一个Xcode项目和一个空白的单页应用程序。

  1. 打开现代版本的Xcode。
  2. 选择创建一个新的Xcode项目。
  3. 在“可用模板”窗口中,选择ios->应用模板

Image description

输入该项目,团队和组织标识符的名称。语言应为 swift ,界面应为故事板

Image description

这将创建一个显示空白屏幕的应用程序。接近最终游戏的一步。为了测试它,键入命令+r或选择product-> run。

如何显示黑屏

由于我们的应用程序接口是基于故事板的,并且我们已经拥有管理主屏幕显示的视图控制器,以显示黑屏,我们要做的就是更改此背景屏幕到黑色。这可以直接从接口构建器完成。

  1. 在项目导航器中,选择文件main.storyboard
  2. 在故事板的文档概述中,选择“视图元素”(在视图控制器场景下 - >视图控制器)
  3. 在视图的属性检查员中,将背景更改为黑色。

Image description

这将创建一个显示黑屏的应用程序。为了测试它,键入命令+R或选择“ product-> run”。重要的是选择颜色黑色,而不是其他似乎是黑色的颜色,例如标签颜色。选择标签颜色而不是黑色作为背景颜色,将导致黑色或白色屏幕,具体取决于iPhone的深色模式设置。无论iPhone的深色或轻度模式如何,选择黑屏总是会导致黑屏。

如何隐藏状态栏

如果我们现在运行该应用程序,我们可能会看到一个完全黑屏幕。但是,如果我们运行的模拟器或设备将应用程序设置为“暗模式”,则屏幕将不完全黑。黑暗模式是通过在设置 - >显示与亮度 - >外观下选择“黑暗”来实现的。

Image description

现在,当我们现在运行应用程序时,设备处于黑暗模式时,顶部状态栏将显示各种元素,例如时间,电源和WiFi指示器。在光模式下,状态栏元素都是黑色的,所以我们无法看到它们,但是现在由于我们处于黑暗模式,它们是轻巧的。

Image description

我们希望屏幕像关闭一样出现。这意味着我们必须隐藏状态栏。这可以通过在目标属性中添加两个新的键值对来实现。

  1. 在项目导航器中,选择最高的元素,这将是项目本身。
  2. 在选择该项目的情况下,在目标下下,选择主要目标并导航到 info tab。
  3. 单击 plus 按钮,在将鼠标悬停在列表项目上时,它变得可见,并添加以下键: uiviewControllerBaseedStatusBarappEarance
  4. 将新添加的键的值设置为 no
  5. 添加另一个键: uistatusbarhidden
  6. 将上述新添加的键设置为是YES

您会注意到,一旦添加并显示了键,名称 uistatusbarhiddend 将显示为 - 状态栏最初是隐藏的,而名称< strong> uiviewControllerBaseadStatusBarappeArance 显示为基于控制器的状态栏外观。没关系。

Image description

现在运行该应用程序时,您会发现屏幕是完全黑色的,并且所有状态栏元素都缺少,而不论iPhone的深色或光模式如何。这正是我们想要的。

如何响应点击事件

点击屏幕时,我们希望该应用完全退出。为此,我们需要收听水龙头。有很多方法。在此项目中,我们将以编程为黑色按钮。

  1. 在项目导航器中,选择 viewConroller.swift
  2. 在“类声明”下,添加以下代码:
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
}()
  1. 添加退出方法
/// 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)
}
  1. 将按钮添加到视图
override func viewDidLoad() {
    super.viewDidLoad()
    view.addSubview(curtainView)
}

上述退出方法是从以下讨论中的答案之一复制的:

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

尝试运行应用程序并测试屏幕时的行为。如果一切都正确完成,则在屏幕上进行点击会导致应用退出。

如何拍照

有许多在线教程和如何通过编程操作iPhone摄像机的示例。他们中的大多数人都有太多的代码用于我们的目的,因为操作相机的常规方式涉及在屏幕上显示相机馈电,在我们的情况下,这是不需要的。

  1. viewController.swift 中,在文件顶部添加行 import avoundation
  2. 在“班级声明”下添加以下两个实例:
private let photoOutput = AVCapturePhotoOutput()
private let session = AVCaptureSession()

添加以下2种方法:

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()
    }
}

添加方法后,您会注意到该项目将不会编译。问题在于,我们将 self 作为capturephoto方法的代表,但是 self ,在这种情况下,这是 uiviewController 实例t支持avcapturephotocaptredElegate协议。稍后我们将需要此支持,以保存照片并静音声音。请记住这一点,为了解决汇编错误,我们将添加以下存根:

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

那么到目前为止我们有什么?两种方法,一种用于配置相机会话,另一个用于拍照(以一秒钟的间隔反复)。但是我们仍然需要连接它们,因此最后一步是添加第一个调用:

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
    }
}

现在运行项目时,构建应该成功。但是,在添加了第一个电话后,我们有一个运行时迷。 此应用程序崩溃了,因为它试图在没有使用说明的情况下访问对隐私敏感的数据。为了解决这个问题,我们需要在目标中添加更多关键值对。当我们在这里时,我们将添加一个用法说明,以访问相机和另一个用法描述,以将照片保存到相册中(稍后将在稍后实施)。

  1. 在项目导航器中,选择最高的元素,这将是项目本身。
  2. 在选择该项目的情况下,在目标下下,选择主要目标并导航到 info tab。
  3. 单击 plus 按钮,在将鼠标悬停在列表项目上时,它变得可见,然后添加以下(字符串)键: nscamerausausagedescription
  4. 将新添加的键的值设置为â,以拍摄秘密照片
  5. 添加另一个(字符串)键: nsphotolibraryAddusagedescription
  6. 将上述新添加的键的值设置为â,以存储秘密照片

现在运行该应用程序时,您应该可以看到摄像机访问权限对话。授权后,您将能够听到相机射击器的声音效果,并看到激活指示器的绿色摄像头。

Image description

如何存储照片

在此程序中,我们将每张照片存储在最近的相册中。如果我们愿意,我们也可以将它们存储在远程服务器上,但这需要配置后端,并且超出了此演示的目的。

存储照片是通过调用系统方法 uiimagewritetosavedphotosalbum

来实现的。

  1. 找到空的代表方法dodfinishProcessingphoto,该方法已添加,并插入以下机构:
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") 
}

添加以下方法获得成功 /失败状态:

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

现在运行该应用程序时,您应该可以看到Gallery访问权限对话。授权后,您将可以在“照片应用程序”中看到新照片。尝试运行该应用程序几秒钟,看看是否添加了照片。

如何隐藏使用摄像头指示器

iOS应用程序可以控制屏幕亮度。在应用程序运行时将屏幕亮度设置为最小可能的值,这将导致使用摄像头指示器几乎无法可见。

我们将添加一个专门用于控制屏幕亮度的单独类:

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
    }
}

当应用程序不活动时,我们将在应用程序处于活动状态并调用UNDIM时致电DIM。编辑文件** scenedelegate.swift **并添加以下内容:

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

只要应用程序正在运行,这是很好的。如果它活跃,则屏幕是深色的,但是如果用户切换到主屏幕或另一个应用程序,则屏幕亮度保留了原始值。

但是,如果用户通过触摸屏幕手动终止应用程序,则屏幕将保持黑暗,因为未调用 scepencewillresignactive 。这是内在的。我们将通过在退出之前将另一个呼叫添加到undim来解决此问题。

找到戒烟方法,该方法如前所述,并添加呼叫:

/// 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)
}

现在运行项目时,屏幕亮度应完全按照我们需要的行为行为。

如何禁用射击器声音

找到前面添加的空委托方法Willcapturephotofor,并插入以下机构:

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

以下讨论中的答案之一复制了上述方法:

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

就是这样。


最终注释

如本项目所示,可以拍摄秘密照片的能力违背了保护用户隐私的当前持续趋势。如前所述,可以通过控制屏幕亮度来操纵使用绿色摄像头指示器。 Apple将来可以预防iOS版本。崩溃的应用程序在相机处于活动状态时试图降低屏幕亮度(正如我们看到我们无法生产摄像机 - 使用示例时崩溃的那样),没有任何问题。

是否可以创建一个类似的程序来记录视频而不是拍摄静物照片?我不知道,但值得尝试。