├── .github └── workflows │ └── CI.yml ├── .gitignore ├── Documentation ├── Flutter.md ├── React-Native.md └── Swift.md ├── LICENSE ├── Package.swift ├── README.md ├── Resources ├── AddingFiles.png ├── AddingFiles1.png ├── AddingFiles2.png ├── AddingViaSPM.png ├── AddingViaSPM1.png └── flutterExample.mov ├── SnapshotSafeView.podspec ├── SnapshotSafeView.xcodeproj ├── SnapshotSafeViewTests_Info.plist ├── SnapshotSafeView_Info.plist ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings └── xcshareddata │ └── xcschemes │ └── SnapshotSafeView-Package.xcscheme ├── Sources └── SnapshotSafeView │ ├── Core │ ├── HiddenContainerRecognizer.swift │ ├── ScreenshotInvincibleContainer.swift │ └── ScreenshotProtectController.swift │ ├── Library │ ├── UIView+SnapshotSafeController.swift │ └── ViewWithDisabledPointInsideCheck.swift │ ├── Multiplatform │ └── Flutter │ │ └── SnapshotSafeViewFlutterInterceptor.swift │ ├── MultiplatformBridgeView │ ├── MultiplatformBridgeView.swift │ └── MultiplatformContainer.swift │ ├── Protocols │ ├── ScreenshotInvincibleContainerProtocol.swift │ └── ScreenshotProtectControllerProtocol.swift │ ├── RootControllerInterceptor │ ├── RootControllerExpectation │ │ ├── Flutter │ │ │ └── FlutterExpectation.swift │ │ └── RootControllerExpectation.swift │ ├── RootControllerInterceptor.swift │ └── RootControllerInterceptorProtocol.swift │ └── SwiftUIBridge │ ├── SnaphotSafeViewSwiftUIBridge.swift │ ├── SnaphotSafeViewSwiftUIBridgeProtocol.swift │ └── View+SnaphotSafeViewSwiftUIBridge.swift └── Tests └── SnapshotSafeViewTests └── RootControllerInterceptorTests └── RootControllerInterceptorTests.swift /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: Swift 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: macos-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Build 13 | run: | 14 | sudo xcodebuild -scheme SnapshotSafeView-Package\ 15 | -project SnapshotSafeView.xcodeproj\ 16 | -derivedDataPath ./buildData\ 17 | -destination "platform=iOS Simulator,name=iPhone SE (3rd generation),OS=16.2"\ 18 | build-for-testing 19 | 20 | # Removed, because pipeline always fail in Github macOS node :( 21 | # 22 | # - name: Tests 23 | # run: | 24 | # sudo xcodebuild test -scheme SnapshotSafeView-Package\ 25 | # -project SnapshotSafeView.xcodeproj\ 26 | # -destination "platform=iOS Simulator,name=iPhone SE (3rd generation),OS=16.2"\ 27 | # -derivedDataPath ./buildData\ 28 | # test-without-building 29 | 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 7 | -------------------------------------------------------------------------------- /Documentation/Flutter.md: -------------------------------------------------------------------------------- 1 | ## Flutter 2 | >**Warning**: Experimental feature 3 | 4 | 1. In `Xcode`, go to `File` → `Add Packages...` OR select your project in the `Project Editor`, go to the `Package Dependencies` tab, and press the `+`. 5 | 6 | ![](/Resources/AddingViaSPM.png) 7 | 8 | 2. Enter a `Package URL` (e.g. a `GitHub` repository `URL`) or a search term in the search field in the upper right. 9 | 10 | ![](/Resources/AddingViaSPM1.png) 11 | 12 | 3. Select the package you want to add. Select a `Dependency Rule`. In most cases, you probably want to set this to `Up to Next Major Version`. 13 | Click `Add Package`. 14 | 15 | 4. Instantiate `SnapshotSafeViewFlutterInterceptor` and call `performSwitchView` for intercepting root view: 16 | 17 | ```swift 18 | @UIApplicationMain 19 | @objc class AppDelegate: FlutterAppDelegate { 20 | 21 | private let snapshotSafeViewInterceptor = SnapshotSafeViewFlutterInterceptor() 22 | 23 | override func application( 24 | _ application: UIApplication, 25 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 26 | ) -> Bool { 27 | GeneratedPluginRegistrant.register(with: self) 28 | snapshotSafeViewInterceptor.performSwitchView(in: window) 29 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 30 | } 31 | 32 | } 33 | ``` 34 | 35 | ## Example: 36 | 37 |

38 | 39 |

-------------------------------------------------------------------------------- /Documentation/React-Native.md: -------------------------------------------------------------------------------- 1 | ## React-Native 2 | 3 | 1. In `Xcode`, go to `File` → `Add Packages...` OR select your project in the `Project Editor`, go to the `Package Dependencies` tab, and press the `+`. 4 | 5 | ![](/Resources/AddingViaSPM.png) 6 | 7 | 2. Enter a `Package URL` (e.g. a `GitHub` repository `URL`) or a search term in the search field in the upper right. 8 | 9 | ![](/Resources/AddingViaSPM1.png) 10 | 11 | 3. Select the package you want to add. Select a `Dependency Rule`. In most cases, you probably want to set this to `Up to Next Major Version`. 12 | Click `Add Package`. 13 | 14 | 4. Add three files for bridging package to `React-Native`. 15 | 16 | ![](/Resources/AddingFiles.png) 17 | 18 | for `Swift` files: 19 | ![](/Resources/AddingFiles1.png) 20 | 21 | for `Obj-c` files: 22 | ![](/Resources/AddingFiles2.png) 23 | 24 | `Bridge`: 25 | ```swift 26 | import UIKit 27 | import SnapshotSafeView 28 | 29 | final class ReactNativeBridgeSnapshotSafeView: MultiplatformBridgeView { } 30 | ``` 31 | 32 | `Obj-c bridge`: 33 | ```objc 34 | #import 35 | 36 | @interface RCT_EXTERN_MODULE(RCTReactNativeBridgeSnapshotSafeViewManager, RCTViewManager) 37 | @end 38 | ``` 39 | 40 | `React-native fabric`: 41 | ```swift 42 | #if canImport(React) 43 | import React 44 | 45 | @objc (RCTReactNativeBridgeSnapshotSafeViewManager) 46 | class ReactNativeBridgeSnapshotSafeViewManager: RCTViewManager { 47 | 48 | override static func requiresMainQueueSetup() -> Bool { 49 | return true 50 | } 51 | 52 | override func view() -> UIView! { 53 | let view = ReactNativeBridgeSnapshotSafeView() 54 | return view 55 | } 56 | 57 | } 58 | #endif 59 | ``` 60 | 61 | 5. After build you can use the view in `React-Native` 62 | 63 | ```javascript 64 | import React, { Component } from 'react'; 65 | import { StyleSheet, View, Text, requireNativeComponent } from 'react-native'; 66 | 67 | const SnapshotSafeView = requireNativeComponent('RCTReactNativeBridgeSnapshotSafeView'); 68 | 69 | const ViewBoxesWithColorAndText = () => { 70 | return ( 71 | 72 | 78 | 79 | 80 | Hello World! 81 | 82 | 83 | ); 84 | }; 85 | 86 | export default ViewBoxesWithColorAndText; 87 | ``` -------------------------------------------------------------------------------- /Documentation/Swift.md: -------------------------------------------------------------------------------- 1 | ## Swift 2 | 3 | 1. In `Xcode`, go to `File` → `Add Packages...` OR select your project in the `Project Editor`, go to the `Package Dependencies` tab, and press the `+`. 4 | 5 | ![](/Resources/AddingViaSPM.png) 6 | 7 | 2. Enter a `Package URL` (e.g. a `GitHub` repository `URL`) or a search term in the search field in the upper right. 8 | 9 | ![](/Resources/AddingViaSPM1.png) 10 | 11 | 3. Select the package you want to add. Select a `Dependency Rule`. In most cases, you probably want to set this to `Up to Next Major Version`. 12 | Click `Add Package`. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Ilya knyazkov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.5 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "SnapshotSafeView", 8 | platforms: [ 9 | .iOS(.v12) 10 | ], 11 | products: [ 12 | .library(name: "SnapshotSafeView", targets: ["SnapshotSafeView"]) 13 | ], 14 | targets: [ 15 | .target(name: "SnapshotSafeView", dependencies: []), 16 | .testTarget(name: "SnapshotSafeViewTests", dependencies: ["SnapshotSafeView"]), 17 | ] 18 | ) 19 | 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SnapshotSafeView 2 | 3 | Used for hide view from system screenshots and video recording. 4 | 5 | ## Categories 6 | 7 | 1. [How install](#how-install) 8 | 2. [Requirements](#requirements) 9 | 3. [Progress](#progress) 10 | 4. [How usage](#how-usage) 11 | 12 | 4.1. [SwiftUI](#swiftui) 13 | 14 | 4.2. [UIKit](#uikit) 15 | 16 | ----------------------- 17 | 18 | ## How install 19 | 20 | - [React-Native](Documentation/React-Native.md) 21 | - [Flutter](Documentation/Flutter.md)(experimental) 22 | - [Swift](Documentation/Swift.md) 23 | 24 | ----------------------- 25 | 26 | ## Requirements 27 | 28 | - Xcode version: `12.5.1` or higher 29 | - Deployment target `iOS v12` or higher 30 | - Swift toolchain `5.5` 31 | 32 | ----------------------- 33 | 34 | ## Progress: 35 | 36 | - ~~Dinamicaly enable/disable hiding from system screenshots and videorecordings(disabling not working yet)~~ Done 37 | - Run from `init(coder:)` 38 | - ~~position inside content with autolayout(now frame based only)~~ Done 39 | - ~~Support SwiftUI~~ 40 | - Improve API 41 | - ~~Support `React-Native`~~ Done 42 | - Support `Flutter` 43 | 44 | ----------------------- 45 | 46 | ## How usage: 47 | ### `SwiftUI` 48 | ```swift 49 | import SwiftUI 50 | import SnapshotSafeView 51 | 52 | struct ContentView: View { 53 | 54 | @State var isNeedHiddenContentFromScreenshots: Bool = false 55 | 56 | var body: some View { 57 | Text("Hello, world!") 58 | .padding() 59 | .background(Color.brown) 60 | Text("Hello, world!") 61 | .padding() 62 | .background(Color.blue) 63 | .hiddenFromSystemSnaphotWithDefaultPadding(when: isNeedHiddenContentFromScreenshots) 64 | Text("Hello, world!") 65 | .padding() 66 | .background(Color.orange) 67 | Button("Toggle hide from screenshots condition") { 68 | isNeedHiddenContentFromScreenshots.toggle() 69 | } 70 | 71 | Spacer() 72 | 73 | Text(isNeedHiddenContentFromScreenshots ? "Will be hidden from snapshots" : "Will be appear in snapshots") 74 | .padding() 75 | .background(isNeedHiddenContentFromScreenshots ? Color.green : Color.red) 76 | 77 | } 78 | } 79 | 80 | struct ContentView_Previews: PreviewProvider { 81 | static var previews: some View { 82 | ContentView() 83 | } 84 | } 85 | ``` 86 | 87 | ### Example: 88 | 89 |

90 | 91 |

92 | 93 | 94 | ### `UIKit` 95 | 96 | >**Warning**: This is experimental API, be careful 97 | ```swift 98 | let someView = UIView() 99 | 100 | someView.translatesAutoresizingMaskIntoConstraints = false 101 | someView.topAnchor.constraint(superView.topAnchor, constant: 12).isActive = true 102 | someView.setupAsHiddenFromScreenshot() 103 | ``` 104 | 105 | or: 106 | 107 | ```swift 108 | final class ExampleViewController: UIViewController { 109 | 110 | var hiddenFromScreenshotButtonController = ScreenshotProtectController(content: UIButton()) 111 | 112 | override func viewDidLoad() { 113 | super.viewDidLoad() 114 | hiddenFromScreenshotButtonController.content.backgroundColor = .systemRed // UI customization apply to content 115 | hiddenFromScreenshotButtonController.content.layer.cornerRadius = 20 116 | 117 | view.addSubview(hiddenFromScreenshotButtonController.container) 118 | hiddenFromScreenshotButtonController.container.translatesAutoresizingMaskIntoConstraints = false 119 | 120 | [ 121 | hiddenFromScreenshotButtonController.container.topAnchor.constraint(equalTo: view.topAnchor, constant: 65), 122 | hiddenFromScreenshotButtonController.container.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 65), 123 | hiddenFromScreenshotButtonController.container.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -65), 124 | hiddenFromScreenshotButtonController.container.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -65) 125 | ].forEach { $0.isActive = true } // Layout control apply to container 126 | 127 | hiddenFromScreenshotButtonController.setupContentAsHiddenInScreenshotMode() // apply hidden mode 128 | // content will be removed from system screenshots and screen recording 129 | } 130 | 131 | } 132 | ``` 133 | 134 | ### Example: 135 | 136 |

137 | 138 |

139 | -------------------------------------------------------------------------------- /Resources/AddingFiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stampoo/SnapshotSafeView/82825e5cab6c581070e50ca6a1d42d5e4f253ebc/Resources/AddingFiles.png -------------------------------------------------------------------------------- /Resources/AddingFiles1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stampoo/SnapshotSafeView/82825e5cab6c581070e50ca6a1d42d5e4f253ebc/Resources/AddingFiles1.png -------------------------------------------------------------------------------- /Resources/AddingFiles2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stampoo/SnapshotSafeView/82825e5cab6c581070e50ca6a1d42d5e4f253ebc/Resources/AddingFiles2.png -------------------------------------------------------------------------------- /Resources/AddingViaSPM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stampoo/SnapshotSafeView/82825e5cab6c581070e50ca6a1d42d5e4f253ebc/Resources/AddingViaSPM.png -------------------------------------------------------------------------------- /Resources/AddingViaSPM1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stampoo/SnapshotSafeView/82825e5cab6c581070e50ca6a1d42d5e4f253ebc/Resources/AddingViaSPM1.png -------------------------------------------------------------------------------- /Resources/flutterExample.mov: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stampoo/SnapshotSafeView/82825e5cab6c581070e50ca6a1d42d5e4f253ebc/Resources/flutterExample.mov -------------------------------------------------------------------------------- /SnapshotSafeView.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # Be sure to run `pod spec lint SnapshotSafeView.podspec' to ensure this is a 3 | # valid spec and to remove all comments including this before submitting the spec. 4 | # 5 | # To learn more about Podspec attributes see https://guides.cocoapods.org/syntax/podspec.html 6 | # To see working Podspecs in the CocoaPods repo see https://github.com/CocoaPods/Specs/ 7 | # 8 | 9 | Pod::Spec.new do |spec| 10 | 11 | # ――― Spec Metadata ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 12 | # 13 | # These will help people to find your library, and whilst it 14 | # can feel like a chore to fill in it's definitely to your advantage. The 15 | # summary should be tweet-length, and the description more in depth. 16 | # 17 | 18 | spec.name = "SnapshotSafeView" 19 | spec.version = "0.2.1" 20 | spec.summary = "Used for hide view from system screenshots and video recording " 21 | 22 | # This description is used to generate tags and improve search results. 23 | # * Think: What does it do? Why did you write it? What is the focus? 24 | # * Try to keep it short, snappy and to the point. 25 | # * Write the description between the DESC delimiters below. 26 | # * Finally, don't worry about the indent, CocoaPods strips it! 27 | spec.description = <<-DESC 28 | DESC 29 | 30 | spec.homepage = "https://github.com/Stampoo/SnapshotSafeView" 31 | # spec.screenshots = "www.example.com/screenshots_1.gif", "www.example.com/screenshots_2.gif" 32 | 33 | 34 | # ――― Spec License ――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 35 | # 36 | # Licensing your code is important. See https://choosealicense.com for more info. 37 | # CocoaPods will detect a license file if there is a named LICENSE* 38 | # Popular ones are 'MIT', 'BSD' and 'Apache License, Version 2.0'. 39 | # 40 | 41 | spec.license = "MIT" 42 | # spec.license = { :type => "MIT", :file => "FILE_LICENSE" } 43 | 44 | 45 | # ――― Author Metadata ――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 46 | # 47 | # Specify the authors of the library, with email addresses. Email addresses 48 | # of the authors are extracted from the SCM log. E.g. $ git log. CocoaPods also 49 | # accepts just a name if you'd rather not provide an email address. 50 | # 51 | # Specify a social_media_url where others can refer to, for example a twitter 52 | # profile URL. 53 | # 54 | 55 | spec.author = { "Ilya knyazkov" => "fivecoil@gmail.com" } 56 | # Or just: spec.author = "Ilya knyazkov" 57 | # spec.authors = { "Ilya knyazkov" => "email@address.com" } 58 | # spec.social_media_url = "https://twitter.com/Ilya knyazkov" 59 | 60 | # ――― Platform Specifics ――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 61 | # 62 | # If this Pod runs only on iOS or OS X, then specify the platform and 63 | # the deployment target. You can optionally include the target after the platform. 64 | # 65 | 66 | # spec.platform = :ios 67 | # spec.platform = :ios, "5.0" 68 | 69 | # When using multiple platforms 70 | # spec.ios.deployment_target = "5.0" 71 | # spec.osx.deployment_target = "10.7" 72 | # spec.watchos.deployment_target = "2.0" 73 | # spec.tvos.deployment_target = "9.0" 74 | 75 | 76 | # ――― Source Location ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 77 | # 78 | # Specify the location from where the source should be retrieved. 79 | # Supports git, hg, bzr, svn and HTTP. 80 | # 81 | 82 | spec.source = { :git => "https://github.com/Stampoo/SnapshotSafeView.git", :tag => "#{spec.version}" } 83 | 84 | 85 | # ――― Source Code ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 86 | # 87 | # CocoaPods is smart about how it includes source code. For source files 88 | # giving a folder will include any swift, h, m, mm, c & cpp files. 89 | # For header files it will include any header in the folder. 90 | # Not including the public_header_files will make all headers public. 91 | # 92 | 93 | spec.source_files = "Source/*.swift" 94 | 95 | # spec.public_header_files = "Classes/**/*.h" 96 | 97 | 98 | # ――― Resources ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 99 | # 100 | # A list of resources included with the Pod. These are copied into the 101 | # target bundle with a build phase script. Anything else will be cleaned. 102 | # You can preserve files from being cleaned, please don't preserve 103 | # non-essential files like tests, examples and documentation. 104 | # 105 | 106 | # spec.resource = "icon.png" 107 | # spec.resources = "Resources/*.png" 108 | 109 | # spec.preserve_paths = "FilesToSave", "MoreFilesToSave" 110 | 111 | 112 | # ――― Project Linking ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 113 | # 114 | # Link your library with frameworks, or libraries. Libraries do not include 115 | # the lib prefix of their name. 116 | # 117 | 118 | # spec.framework = "SomeFramework" 119 | # spec.frameworks = "SomeFramework", "AnotherFramework" 120 | 121 | # spec.library = "iconv" 122 | # spec.libraries = "iconv", "xml2" 123 | 124 | 125 | # ――― Project Settings ――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 126 | # 127 | # If your library depends on compiler flags you can set them in the xcconfig hash 128 | # where they will only apply to your library. If you depend on other Podspecs 129 | # you can include multiple dependencies to ensure it works. 130 | 131 | # spec.requires_arc = true 132 | 133 | # spec.xcconfig = { "HEADER_SEARCH_PATHS" => "$(SDKROOT)/usr/include/libxml2" } 134 | # spec.dependency "JSONKit", "~> 1.4" 135 | 136 | end 137 | -------------------------------------------------------------------------------- /SnapshotSafeView.xcodeproj/SnapshotSafeViewTests_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | BNDL 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /SnapshotSafeView.xcodeproj/SnapshotSafeView_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | FMWK 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /SnapshotSafeView.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = "1"; 4 | objectVersion = "46"; 5 | objects = { 6 | "OBJ_1" = { 7 | isa = "PBXProject"; 8 | attributes = { 9 | LastSwiftMigration = "9999"; 10 | LastUpgradeCheck = "9999"; 11 | }; 12 | buildConfigurationList = "OBJ_2"; 13 | compatibilityVersion = "Xcode 3.2"; 14 | developmentRegion = "en"; 15 | hasScannedForEncodings = "0"; 16 | knownRegions = ( 17 | "en" 18 | ); 19 | mainGroup = "OBJ_5"; 20 | productRefGroup = "OBJ_38"; 21 | projectDirPath = "."; 22 | targets = ( 23 | "snapshotsafeview::SnapshotSafeView", 24 | "snapshotsafeview::SwiftPMPackageDescription", 25 | "snapshotsafeview::SnapshotSafeViewPackageTests::ProductTarget", 26 | "snapshotsafeview::SnapshotSafeViewTests" 27 | ); 28 | }; 29 | "OBJ_10" = { 30 | isa = "PBXFileReference"; 31 | path = "HiddenContainerRecognizer.swift"; 32 | sourceTree = ""; 33 | }; 34 | "OBJ_11" = { 35 | isa = "PBXFileReference"; 36 | path = "ScreenshotInvincibleContainer.swift"; 37 | sourceTree = ""; 38 | }; 39 | "OBJ_12" = { 40 | isa = "PBXFileReference"; 41 | path = "ScreenshotProtectController.swift"; 42 | sourceTree = ""; 43 | }; 44 | "OBJ_13" = { 45 | isa = "PBXGroup"; 46 | children = ( 47 | "OBJ_14" 48 | ); 49 | name = "Library"; 50 | path = "Library"; 51 | sourceTree = ""; 52 | }; 53 | "OBJ_14" = { 54 | isa = "PBXFileReference"; 55 | path = "UIView+SnapshotSafeController.swift"; 56 | sourceTree = ""; 57 | }; 58 | "OBJ_15" = { 59 | isa = "PBXGroup"; 60 | children = ( 61 | "OBJ_16" 62 | ); 63 | name = "Multiplatform"; 64 | path = "Multiplatform"; 65 | sourceTree = ""; 66 | }; 67 | "OBJ_16" = { 68 | isa = "PBXGroup"; 69 | children = ( 70 | "OBJ_17" 71 | ); 72 | name = "Flutter"; 73 | path = "Flutter"; 74 | sourceTree = ""; 75 | }; 76 | "OBJ_17" = { 77 | isa = "PBXFileReference"; 78 | path = "SnapshotSafeViewFlutterInterceptor.swift"; 79 | sourceTree = ""; 80 | }; 81 | "OBJ_18" = { 82 | isa = "PBXGroup"; 83 | children = ( 84 | "OBJ_19" 85 | ); 86 | name = "MultiplatformBridgeView"; 87 | path = "MultiplatformBridgeView"; 88 | sourceTree = ""; 89 | }; 90 | "OBJ_19" = { 91 | isa = "PBXFileReference"; 92 | path = "MultiplatformBridgeView.swift"; 93 | sourceTree = ""; 94 | }; 95 | "OBJ_2" = { 96 | isa = "XCConfigurationList"; 97 | buildConfigurations = ( 98 | "OBJ_3", 99 | "OBJ_4" 100 | ); 101 | defaultConfigurationIsVisible = "0"; 102 | defaultConfigurationName = "Release"; 103 | }; 104 | "OBJ_20" = { 105 | isa = "PBXGroup"; 106 | children = ( 107 | "OBJ_21", 108 | "OBJ_22" 109 | ); 110 | name = "Protocols"; 111 | path = "Protocols"; 112 | sourceTree = ""; 113 | }; 114 | "OBJ_21" = { 115 | isa = "PBXFileReference"; 116 | path = "ScreenshotInvincibleContainerProtocol.swift"; 117 | sourceTree = ""; 118 | }; 119 | "OBJ_22" = { 120 | isa = "PBXFileReference"; 121 | path = "ScreenshotProtectControllerProtocol.swift"; 122 | sourceTree = ""; 123 | }; 124 | "OBJ_23" = { 125 | isa = "PBXGroup"; 126 | children = ( 127 | "OBJ_24", 128 | "OBJ_28", 129 | "OBJ_29" 130 | ); 131 | name = "RootControllerInterceptor"; 132 | path = "RootControllerInterceptor"; 133 | sourceTree = ""; 134 | }; 135 | "OBJ_24" = { 136 | isa = "PBXGroup"; 137 | children = ( 138 | "OBJ_25", 139 | "OBJ_27" 140 | ); 141 | name = "RootControllerExpectation"; 142 | path = "RootControllerExpectation"; 143 | sourceTree = ""; 144 | }; 145 | "OBJ_25" = { 146 | isa = "PBXGroup"; 147 | children = ( 148 | "OBJ_26" 149 | ); 150 | name = "Flutter"; 151 | path = "Flutter"; 152 | sourceTree = ""; 153 | }; 154 | "OBJ_26" = { 155 | isa = "PBXFileReference"; 156 | path = "FlutterExpectation.swift"; 157 | sourceTree = ""; 158 | }; 159 | "OBJ_27" = { 160 | isa = "PBXFileReference"; 161 | path = "RootControllerExpectation.swift"; 162 | sourceTree = ""; 163 | }; 164 | "OBJ_28" = { 165 | isa = "PBXFileReference"; 166 | path = "RootControllerInterceptor.swift"; 167 | sourceTree = ""; 168 | }; 169 | "OBJ_29" = { 170 | isa = "PBXFileReference"; 171 | path = "RootControllerInterceptorProtocol.swift"; 172 | sourceTree = ""; 173 | }; 174 | "OBJ_3" = { 175 | isa = "XCBuildConfiguration"; 176 | buildSettings = { 177 | CLANG_ENABLE_OBJC_ARC = "YES"; 178 | COMBINE_HIDPI_IMAGES = "YES"; 179 | COPY_PHASE_STRIP = "NO"; 180 | DEBUG_INFORMATION_FORMAT = "dwarf"; 181 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 182 | ENABLE_NS_ASSERTIONS = "YES"; 183 | GCC_OPTIMIZATION_LEVEL = "0"; 184 | GCC_PREPROCESSOR_DEFINITIONS = ( 185 | "$(inherited)", 186 | "SWIFT_PACKAGE=1", 187 | "DEBUG=1" 188 | ); 189 | MACOSX_DEPLOYMENT_TARGET = "10.10"; 190 | ONLY_ACTIVE_ARCH = "YES"; 191 | OTHER_SWIFT_FLAGS = ( 192 | "$(inherited)", 193 | "-DXcode" 194 | ); 195 | PRODUCT_NAME = "$(TARGET_NAME)"; 196 | SDKROOT = "macosx"; 197 | SUPPORTED_PLATFORMS = ( 198 | "$(AVAILABLE_PLATFORMS)" 199 | ); 200 | SUPPORTS_MACCATALYST = "YES"; 201 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = ( 202 | "$(inherited)", 203 | "SWIFT_PACKAGE", 204 | "DEBUG" 205 | ); 206 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 207 | USE_HEADERMAP = "NO"; 208 | }; 209 | name = "Debug"; 210 | }; 211 | "OBJ_30" = { 212 | isa = "PBXGroup"; 213 | children = ( 214 | "OBJ_31", 215 | "OBJ_32", 216 | "OBJ_33" 217 | ); 218 | name = "SwiftUIBridge"; 219 | path = "SwiftUIBridge"; 220 | sourceTree = ""; 221 | }; 222 | "OBJ_31" = { 223 | isa = "PBXFileReference"; 224 | path = "SnaphotSafeViewSwiftUIBridge.swift"; 225 | sourceTree = ""; 226 | }; 227 | "OBJ_32" = { 228 | isa = "PBXFileReference"; 229 | path = "SnaphotSafeViewSwiftUIBridgeProtocol.swift"; 230 | sourceTree = ""; 231 | }; 232 | "OBJ_33" = { 233 | isa = "PBXFileReference"; 234 | path = "View+SnaphotSafeViewSwiftUIBridge.swift"; 235 | sourceTree = ""; 236 | }; 237 | "OBJ_34" = { 238 | isa = "PBXGroup"; 239 | children = ( 240 | "OBJ_35" 241 | ); 242 | name = "Tests"; 243 | path = ""; 244 | sourceTree = "SOURCE_ROOT"; 245 | }; 246 | "OBJ_35" = { 247 | isa = "PBXGroup"; 248 | children = ( 249 | "OBJ_36" 250 | ); 251 | name = "SnapshotSafeViewTests"; 252 | path = "Tests/SnapshotSafeViewTests"; 253 | sourceTree = "SOURCE_ROOT"; 254 | }; 255 | "OBJ_36" = { 256 | isa = "PBXGroup"; 257 | children = ( 258 | "OBJ_37" 259 | ); 260 | name = "RootControllerInterceptorTests"; 261 | path = "RootControllerInterceptorTests"; 262 | sourceTree = ""; 263 | }; 264 | "OBJ_37" = { 265 | isa = "PBXFileReference"; 266 | path = "RootControllerInterceptorTests.swift"; 267 | sourceTree = ""; 268 | }; 269 | "OBJ_38" = { 270 | isa = "PBXGroup"; 271 | children = ( 272 | "snapshotsafeview::SnapshotSafeView::Product", 273 | "snapshotsafeview::SnapshotSafeViewTests::Product" 274 | ); 275 | name = "Products"; 276 | path = ""; 277 | sourceTree = "BUILT_PRODUCTS_DIR"; 278 | }; 279 | "OBJ_4" = { 280 | isa = "XCBuildConfiguration"; 281 | buildSettings = { 282 | CLANG_ENABLE_OBJC_ARC = "YES"; 283 | COMBINE_HIDPI_IMAGES = "YES"; 284 | COPY_PHASE_STRIP = "YES"; 285 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 286 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 287 | GCC_OPTIMIZATION_LEVEL = "s"; 288 | GCC_PREPROCESSOR_DEFINITIONS = ( 289 | "$(inherited)", 290 | "SWIFT_PACKAGE=1" 291 | ); 292 | MACOSX_DEPLOYMENT_TARGET = "10.10"; 293 | OTHER_SWIFT_FLAGS = ( 294 | "$(inherited)", 295 | "-DXcode" 296 | ); 297 | PRODUCT_NAME = "$(TARGET_NAME)"; 298 | SDKROOT = "macosx"; 299 | SUPPORTED_PLATFORMS = ( 300 | "$(AVAILABLE_PLATFORMS)" 301 | ); 302 | SUPPORTS_MACCATALYST = "YES"; 303 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = ( 304 | "$(inherited)", 305 | "SWIFT_PACKAGE" 306 | ); 307 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 308 | USE_HEADERMAP = "NO"; 309 | }; 310 | name = "Release"; 311 | }; 312 | "OBJ_41" = { 313 | isa = "PBXFileReference"; 314 | path = "Documentation"; 315 | sourceTree = "SOURCE_ROOT"; 316 | }; 317 | "OBJ_42" = { 318 | isa = "PBXFileReference"; 319 | path = "Crossplatform"; 320 | sourceTree = "SOURCE_ROOT"; 321 | }; 322 | "OBJ_43" = { 323 | isa = "PBXFileReference"; 324 | path = "Resources"; 325 | sourceTree = "SOURCE_ROOT"; 326 | }; 327 | "OBJ_44" = { 328 | isa = "PBXFileReference"; 329 | path = "SwiftGen"; 330 | sourceTree = "SOURCE_ROOT"; 331 | }; 332 | "OBJ_45" = { 333 | isa = "PBXFileReference"; 334 | path = "LICENSE"; 335 | sourceTree = ""; 336 | }; 337 | "OBJ_46" = { 338 | isa = "PBXFileReference"; 339 | path = "SnapshotSafeView.podspec"; 340 | sourceTree = ""; 341 | }; 342 | "OBJ_47" = { 343 | isa = "PBXFileReference"; 344 | path = "README.md"; 345 | sourceTree = ""; 346 | }; 347 | "OBJ_49" = { 348 | isa = "XCConfigurationList"; 349 | buildConfigurations = ( 350 | "OBJ_50", 351 | "OBJ_51" 352 | ); 353 | defaultConfigurationIsVisible = "0"; 354 | defaultConfigurationName = "Release"; 355 | }; 356 | "OBJ_5" = { 357 | isa = "PBXGroup"; 358 | children = ( 359 | "OBJ_6", 360 | "OBJ_7", 361 | "OBJ_34", 362 | "OBJ_38", 363 | "OBJ_41", 364 | "OBJ_42", 365 | "OBJ_43", 366 | "OBJ_44", 367 | "OBJ_45", 368 | "OBJ_46", 369 | "OBJ_47" 370 | ); 371 | path = ""; 372 | sourceTree = ""; 373 | }; 374 | "OBJ_50" = { 375 | isa = "XCBuildConfiguration"; 376 | buildSettings = { 377 | CURRENT_PROJECT_VERSION = "1"; 378 | DRIVERKIT_DEPLOYMENT_TARGET = "19.0"; 379 | ENABLE_TESTABILITY = "YES"; 380 | FRAMEWORK_SEARCH_PATHS = ( 381 | "$(inherited)", 382 | "$(PLATFORM_DIR)/Developer/Library/Frameworks" 383 | ); 384 | HEADER_SEARCH_PATHS = ( 385 | "$(inherited)" 386 | ); 387 | INFOPLIST_FILE = "SnapshotSafeView.xcodeproj/SnapshotSafeView_Info.plist"; 388 | IPHONEOS_DEPLOYMENT_TARGET = "12.0"; 389 | LD_RUNPATH_SEARCH_PATHS = ( 390 | "$(inherited)", 391 | "$(TOOLCHAIN_DIR)/usr/lib/swift/macosx" 392 | ); 393 | MACOSX_DEPLOYMENT_TARGET = "10.13"; 394 | OTHER_CFLAGS = ( 395 | "$(inherited)" 396 | ); 397 | OTHER_LDFLAGS = ( 398 | "$(inherited)" 399 | ); 400 | OTHER_SWIFT_FLAGS = ( 401 | "$(inherited)" 402 | ); 403 | PRODUCT_BUNDLE_IDENTIFIER = "SnapshotSafeView"; 404 | PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)"; 405 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 406 | SKIP_INSTALL = "YES"; 407 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = ( 408 | "$(inherited)" 409 | ); 410 | SWIFT_VERSION = "5.0"; 411 | TARGET_NAME = "SnapshotSafeView"; 412 | TVOS_DEPLOYMENT_TARGET = "11.0"; 413 | WATCHOS_DEPLOYMENT_TARGET = "4.0"; 414 | }; 415 | name = "Debug"; 416 | }; 417 | "OBJ_51" = { 418 | isa = "XCBuildConfiguration"; 419 | buildSettings = { 420 | CURRENT_PROJECT_VERSION = "1"; 421 | DRIVERKIT_DEPLOYMENT_TARGET = "19.0"; 422 | ENABLE_TESTABILITY = "YES"; 423 | FRAMEWORK_SEARCH_PATHS = ( 424 | "$(inherited)", 425 | "$(PLATFORM_DIR)/Developer/Library/Frameworks" 426 | ); 427 | HEADER_SEARCH_PATHS = ( 428 | "$(inherited)" 429 | ); 430 | INFOPLIST_FILE = "SnapshotSafeView.xcodeproj/SnapshotSafeView_Info.plist"; 431 | IPHONEOS_DEPLOYMENT_TARGET = "12.0"; 432 | LD_RUNPATH_SEARCH_PATHS = ( 433 | "$(inherited)", 434 | "$(TOOLCHAIN_DIR)/usr/lib/swift/macosx" 435 | ); 436 | MACOSX_DEPLOYMENT_TARGET = "10.13"; 437 | OTHER_CFLAGS = ( 438 | "$(inherited)" 439 | ); 440 | OTHER_LDFLAGS = ( 441 | "$(inherited)" 442 | ); 443 | OTHER_SWIFT_FLAGS = ( 444 | "$(inherited)" 445 | ); 446 | PRODUCT_BUNDLE_IDENTIFIER = "SnapshotSafeView"; 447 | PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)"; 448 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 449 | SKIP_INSTALL = "YES"; 450 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = ( 451 | "$(inherited)" 452 | ); 453 | SWIFT_VERSION = "5.0"; 454 | TARGET_NAME = "SnapshotSafeView"; 455 | TVOS_DEPLOYMENT_TARGET = "11.0"; 456 | WATCHOS_DEPLOYMENT_TARGET = "4.0"; 457 | }; 458 | name = "Release"; 459 | }; 460 | "OBJ_52" = { 461 | isa = "PBXSourcesBuildPhase"; 462 | files = ( 463 | "OBJ_53", 464 | "OBJ_54", 465 | "OBJ_55", 466 | "OBJ_56", 467 | "OBJ_57", 468 | "OBJ_58", 469 | "OBJ_59", 470 | "OBJ_60", 471 | "OBJ_61", 472 | "OBJ_62", 473 | "OBJ_63", 474 | "OBJ_64", 475 | "OBJ_65", 476 | "OBJ_66", 477 | "OBJ_67" 478 | ); 479 | }; 480 | "OBJ_53" = { 481 | isa = "PBXBuildFile"; 482 | fileRef = "OBJ_10"; 483 | }; 484 | "OBJ_54" = { 485 | isa = "PBXBuildFile"; 486 | fileRef = "OBJ_11"; 487 | }; 488 | "OBJ_55" = { 489 | isa = "PBXBuildFile"; 490 | fileRef = "OBJ_12"; 491 | }; 492 | "OBJ_56" = { 493 | isa = "PBXBuildFile"; 494 | fileRef = "OBJ_14"; 495 | }; 496 | "OBJ_57" = { 497 | isa = "PBXBuildFile"; 498 | fileRef = "OBJ_17"; 499 | }; 500 | "OBJ_58" = { 501 | isa = "PBXBuildFile"; 502 | fileRef = "OBJ_19"; 503 | }; 504 | "OBJ_59" = { 505 | isa = "PBXBuildFile"; 506 | fileRef = "OBJ_21"; 507 | }; 508 | "OBJ_6" = { 509 | isa = "PBXFileReference"; 510 | explicitFileType = "sourcecode.swift"; 511 | path = "Package.swift"; 512 | sourceTree = ""; 513 | }; 514 | "OBJ_60" = { 515 | isa = "PBXBuildFile"; 516 | fileRef = "OBJ_22"; 517 | }; 518 | "OBJ_61" = { 519 | isa = "PBXBuildFile"; 520 | fileRef = "OBJ_26"; 521 | }; 522 | "OBJ_62" = { 523 | isa = "PBXBuildFile"; 524 | fileRef = "OBJ_27"; 525 | }; 526 | "OBJ_63" = { 527 | isa = "PBXBuildFile"; 528 | fileRef = "OBJ_28"; 529 | }; 530 | "OBJ_64" = { 531 | isa = "PBXBuildFile"; 532 | fileRef = "OBJ_29"; 533 | }; 534 | "OBJ_65" = { 535 | isa = "PBXBuildFile"; 536 | fileRef = "OBJ_31"; 537 | }; 538 | "OBJ_66" = { 539 | isa = "PBXBuildFile"; 540 | fileRef = "OBJ_32"; 541 | }; 542 | "OBJ_67" = { 543 | isa = "PBXBuildFile"; 544 | fileRef = "OBJ_33"; 545 | }; 546 | "OBJ_68" = { 547 | isa = "PBXFrameworksBuildPhase"; 548 | files = ( 549 | ); 550 | }; 551 | "OBJ_7" = { 552 | isa = "PBXGroup"; 553 | children = ( 554 | "OBJ_8" 555 | ); 556 | name = "Sources"; 557 | path = ""; 558 | sourceTree = "SOURCE_ROOT"; 559 | }; 560 | "OBJ_70" = { 561 | isa = "XCConfigurationList"; 562 | buildConfigurations = ( 563 | "OBJ_71", 564 | "OBJ_72" 565 | ); 566 | defaultConfigurationIsVisible = "0"; 567 | defaultConfigurationName = "Release"; 568 | }; 569 | "OBJ_71" = { 570 | isa = "XCBuildConfiguration"; 571 | buildSettings = { 572 | LD = "/usr/bin/true"; 573 | OTHER_SWIFT_FLAGS = ( 574 | "-swift-version", 575 | "5", 576 | "-I", 577 | "$(TOOLCHAIN_DIR)/usr/lib/swift/pm/ManifestAPI", 578 | "-sdk", 579 | "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.0.sdk", 580 | "-package-description-version", 581 | "5.5.0" 582 | ); 583 | SWIFT_VERSION = "5.0"; 584 | }; 585 | name = "Debug"; 586 | }; 587 | "OBJ_72" = { 588 | isa = "XCBuildConfiguration"; 589 | buildSettings = { 590 | LD = "/usr/bin/true"; 591 | OTHER_SWIFT_FLAGS = ( 592 | "-swift-version", 593 | "5", 594 | "-I", 595 | "$(TOOLCHAIN_DIR)/usr/lib/swift/pm/ManifestAPI", 596 | "-sdk", 597 | "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.0.sdk", 598 | "-package-description-version", 599 | "5.5.0" 600 | ); 601 | SWIFT_VERSION = "5.0"; 602 | }; 603 | name = "Release"; 604 | }; 605 | "OBJ_73" = { 606 | isa = "PBXSourcesBuildPhase"; 607 | files = ( 608 | "OBJ_74" 609 | ); 610 | }; 611 | "OBJ_74" = { 612 | isa = "PBXBuildFile"; 613 | fileRef = "OBJ_6"; 614 | }; 615 | "OBJ_76" = { 616 | isa = "XCConfigurationList"; 617 | buildConfigurations = ( 618 | "OBJ_77", 619 | "OBJ_78" 620 | ); 621 | defaultConfigurationIsVisible = "0"; 622 | defaultConfigurationName = "Release"; 623 | }; 624 | "OBJ_77" = { 625 | isa = "XCBuildConfiguration"; 626 | buildSettings = { 627 | }; 628 | name = "Debug"; 629 | }; 630 | "OBJ_78" = { 631 | isa = "XCBuildConfiguration"; 632 | buildSettings = { 633 | }; 634 | name = "Release"; 635 | }; 636 | "OBJ_79" = { 637 | isa = "PBXTargetDependency"; 638 | target = "snapshotsafeview::SnapshotSafeViewTests"; 639 | }; 640 | "OBJ_8" = { 641 | isa = "PBXGroup"; 642 | children = ( 643 | "OBJ_9", 644 | "OBJ_13", 645 | "OBJ_15", 646 | "OBJ_18", 647 | "OBJ_20", 648 | "OBJ_23", 649 | "OBJ_30" 650 | ); 651 | name = "SnapshotSafeView"; 652 | path = "Sources/SnapshotSafeView"; 653 | sourceTree = "SOURCE_ROOT"; 654 | }; 655 | "OBJ_81" = { 656 | isa = "XCConfigurationList"; 657 | buildConfigurations = ( 658 | "OBJ_82", 659 | "OBJ_83" 660 | ); 661 | defaultConfigurationIsVisible = "0"; 662 | defaultConfigurationName = "Release"; 663 | }; 664 | "OBJ_82" = { 665 | isa = "XCBuildConfiguration"; 666 | buildSettings = { 667 | CLANG_ENABLE_MODULES = "YES"; 668 | CURRENT_PROJECT_VERSION = "1"; 669 | DRIVERKIT_DEPLOYMENT_TARGET = "19.0"; 670 | EMBEDDED_CONTENT_CONTAINS_SWIFT = "YES"; 671 | FRAMEWORK_SEARCH_PATHS = ( 672 | "$(inherited)", 673 | "$(PLATFORM_DIR)/Developer/Library/Frameworks" 674 | ); 675 | HEADER_SEARCH_PATHS = ( 676 | "$(inherited)" 677 | ); 678 | INFOPLIST_FILE = "SnapshotSafeView.xcodeproj/SnapshotSafeViewTests_Info.plist"; 679 | IPHONEOS_DEPLOYMENT_TARGET = "14.0"; 680 | LD_RUNPATH_SEARCH_PATHS = ( 681 | "$(inherited)", 682 | "@loader_path/../Frameworks", 683 | "@loader_path/Frameworks" 684 | ); 685 | MACOSX_DEPLOYMENT_TARGET = "11.0"; 686 | OTHER_CFLAGS = ( 687 | "$(inherited)" 688 | ); 689 | OTHER_LDFLAGS = ( 690 | "$(inherited)" 691 | ); 692 | OTHER_SWIFT_FLAGS = ( 693 | "$(inherited)" 694 | ); 695 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = ( 696 | "$(inherited)" 697 | ); 698 | SWIFT_VERSION = "5.0"; 699 | TARGET_NAME = "SnapshotSafeViewTests"; 700 | TVOS_DEPLOYMENT_TARGET = "14.0"; 701 | WATCHOS_DEPLOYMENT_TARGET = "7.0"; 702 | }; 703 | name = "Debug"; 704 | }; 705 | "OBJ_83" = { 706 | isa = "XCBuildConfiguration"; 707 | buildSettings = { 708 | CLANG_ENABLE_MODULES = "YES"; 709 | CURRENT_PROJECT_VERSION = "1"; 710 | DRIVERKIT_DEPLOYMENT_TARGET = "19.0"; 711 | EMBEDDED_CONTENT_CONTAINS_SWIFT = "YES"; 712 | FRAMEWORK_SEARCH_PATHS = ( 713 | "$(inherited)", 714 | "$(PLATFORM_DIR)/Developer/Library/Frameworks" 715 | ); 716 | HEADER_SEARCH_PATHS = ( 717 | "$(inherited)" 718 | ); 719 | INFOPLIST_FILE = "SnapshotSafeView.xcodeproj/SnapshotSafeViewTests_Info.plist"; 720 | IPHONEOS_DEPLOYMENT_TARGET = "14.0"; 721 | LD_RUNPATH_SEARCH_PATHS = ( 722 | "$(inherited)", 723 | "@loader_path/../Frameworks", 724 | "@loader_path/Frameworks" 725 | ); 726 | MACOSX_DEPLOYMENT_TARGET = "11.0"; 727 | OTHER_CFLAGS = ( 728 | "$(inherited)" 729 | ); 730 | OTHER_LDFLAGS = ( 731 | "$(inherited)" 732 | ); 733 | OTHER_SWIFT_FLAGS = ( 734 | "$(inherited)" 735 | ); 736 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = ( 737 | "$(inherited)" 738 | ); 739 | SWIFT_VERSION = "5.0"; 740 | TARGET_NAME = "SnapshotSafeViewTests"; 741 | TVOS_DEPLOYMENT_TARGET = "14.0"; 742 | WATCHOS_DEPLOYMENT_TARGET = "7.0"; 743 | }; 744 | name = "Release"; 745 | }; 746 | "OBJ_84" = { 747 | isa = "PBXSourcesBuildPhase"; 748 | files = ( 749 | "OBJ_85" 750 | ); 751 | }; 752 | "OBJ_85" = { 753 | isa = "PBXBuildFile"; 754 | fileRef = "OBJ_37"; 755 | }; 756 | "OBJ_86" = { 757 | isa = "PBXFrameworksBuildPhase"; 758 | files = ( 759 | "OBJ_87" 760 | ); 761 | }; 762 | "OBJ_87" = { 763 | isa = "PBXBuildFile"; 764 | fileRef = "snapshotsafeview::SnapshotSafeView::Product"; 765 | }; 766 | "OBJ_88" = { 767 | isa = "PBXTargetDependency"; 768 | target = "snapshotsafeview::SnapshotSafeView"; 769 | }; 770 | "OBJ_9" = { 771 | isa = "PBXGroup"; 772 | children = ( 773 | "OBJ_10", 774 | "OBJ_11", 775 | "OBJ_12" 776 | ); 777 | name = "Core"; 778 | path = "Core"; 779 | sourceTree = ""; 780 | }; 781 | "snapshotsafeview::SnapshotSafeView" = { 782 | isa = "PBXNativeTarget"; 783 | buildConfigurationList = "OBJ_49"; 784 | buildPhases = ( 785 | "OBJ_52", 786 | "OBJ_68" 787 | ); 788 | dependencies = ( 789 | ); 790 | name = "SnapshotSafeView"; 791 | productName = "SnapshotSafeView"; 792 | productReference = "snapshotsafeview::SnapshotSafeView::Product"; 793 | productType = "com.apple.product-type.framework"; 794 | }; 795 | "snapshotsafeview::SnapshotSafeView::Product" = { 796 | isa = "PBXFileReference"; 797 | path = "SnapshotSafeView.framework"; 798 | sourceTree = "BUILT_PRODUCTS_DIR"; 799 | }; 800 | "snapshotsafeview::SnapshotSafeViewPackageTests::ProductTarget" = { 801 | isa = "PBXAggregateTarget"; 802 | buildConfigurationList = "OBJ_76"; 803 | buildPhases = ( 804 | ); 805 | dependencies = ( 806 | "OBJ_79" 807 | ); 808 | name = "SnapshotSafeViewPackageTests"; 809 | productName = "SnapshotSafeViewPackageTests"; 810 | }; 811 | "snapshotsafeview::SnapshotSafeViewTests" = { 812 | isa = "PBXNativeTarget"; 813 | buildConfigurationList = "OBJ_81"; 814 | buildPhases = ( 815 | "OBJ_84", 816 | "OBJ_86" 817 | ); 818 | dependencies = ( 819 | "OBJ_88" 820 | ); 821 | name = "SnapshotSafeViewTests"; 822 | productName = "SnapshotSafeViewTests"; 823 | productReference = "snapshotsafeview::SnapshotSafeViewTests::Product"; 824 | productType = "com.apple.product-type.bundle.unit-test"; 825 | }; 826 | "snapshotsafeview::SnapshotSafeViewTests::Product" = { 827 | isa = "PBXFileReference"; 828 | path = "SnapshotSafeViewTests.xctest"; 829 | sourceTree = "BUILT_PRODUCTS_DIR"; 830 | }; 831 | "snapshotsafeview::SwiftPMPackageDescription" = { 832 | isa = "PBXNativeTarget"; 833 | buildConfigurationList = "OBJ_70"; 834 | buildPhases = ( 835 | "OBJ_73" 836 | ); 837 | dependencies = ( 838 | ); 839 | name = "SnapshotSafeViewPackageDescription"; 840 | productName = "SnapshotSafeViewPackageDescription"; 841 | productType = "com.apple.product-type.framework"; 842 | }; 843 | }; 844 | rootObject = "OBJ_1"; 845 | } 846 | -------------------------------------------------------------------------------- /SnapshotSafeView.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /SnapshotSafeView.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /SnapshotSafeView.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded 6 | 7 | 8 | -------------------------------------------------------------------------------- /SnapshotSafeView.xcodeproj/xcshareddata/xcschemes/SnapshotSafeView-Package.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 54 | 60 | 61 | 63 | 64 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /Sources/SnapshotSafeView/Core/HiddenContainerRecognizer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HiddenContainerRecognizer.swift 3 | // 4 | // 5 | // Created by Князьков Илья on 23.07.2022. 6 | // 7 | 8 | import UIKit 9 | 10 | /// Recognize view, which can be hidden before system screenshot event did triggered, depend on `iOS version` 11 | struct HiddenContainerRecognizer { 12 | 13 | // MARK: - Nested Types 14 | 15 | private enum Error: Swift.Error { 16 | case unsupportedIosVersion(version: Float) 17 | case desiredContainerWasNotFound(_ containerName: String) 18 | } 19 | 20 | // MARK: - Internal Methods 21 | 22 | func getHiddenContainer(from view: UIView) throws -> UIView { 23 | let containerName = try getHiddenContainerTypeInStringRepresentation() 24 | let containers = view.subviews.filter { subview in 25 | type(of: subview).description() == containerName 26 | } 27 | 28 | guard let container = containers.first else { 29 | throw Error.desiredContainerWasNotFound(containerName) 30 | } 31 | 32 | return container 33 | } 34 | 35 | func getHiddenContainerTypeInStringRepresentation() throws -> String { 36 | 37 | if #available(iOS 15, *) { 38 | return "_UITextLayoutCanvasView" 39 | } 40 | 41 | if #available(iOS 14, *) { 42 | return "_UITextFieldCanvasView" 43 | } 44 | 45 | if #available(iOS 13, *) { 46 | return "_UITextFieldCanvasView" 47 | } 48 | 49 | if #available(iOS 12, *) { 50 | return "_UITextFieldContentView" 51 | } 52 | 53 | let currentIOSVersion = (UIDevice.current.systemVersion as NSString).floatValue 54 | throw Error.unsupportedIosVersion(version: currentIOSVersion) 55 | } 56 | 57 | func viewIsAlreadyInHiddenContainer(_ view: UIView) -> Bool { 58 | guard 59 | let containerClassName = try? getHiddenContainerTypeInStringRepresentation(), 60 | let superViewInspectableView = view.superview 61 | else { 62 | return false 63 | } 64 | 65 | let typeOfClassContainer = type(of: superViewInspectableView) 66 | let stringRepresentationOfClassContainer = String(describing: typeOfClassContainer.self) 67 | 68 | return stringRepresentationOfClassContainer == containerClassName 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /Sources/SnapshotSafeView/Core/ScreenshotInvincibleContainer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScreenshotInvincibleContainer.swift 3 | // 4 | // 5 | // Created by Князьков Илья on 01.03.2022. 6 | // 7 | 8 | import UIKit 9 | 10 | class ScreenshotInvincibleContainer: UITextField { 11 | 12 | // MARK: - Private Properties 13 | 14 | private let hiddenContainerRecognizer = HiddenContainerRecognizer() 15 | private var container: UIView? { 16 | try? hiddenContainerRecognizer.getHiddenContainer(from: self) 17 | } 18 | 19 | // MARK: - Internal Properties 20 | 21 | /// - View, which will be hidden on screenshots and screen recording 22 | private(set) var content: UIView 23 | 24 | // MARK: - Initialization 25 | 26 | public init(content: UIView) { 27 | self.content = content 28 | super.init(frame: .zero) 29 | setupInitialState() 30 | } 31 | 32 | public required init?(coder: NSCoder) { 33 | self.content = UIView() 34 | super.init(coder: coder) 35 | setupInitialState() 36 | } 37 | 38 | // MARK: - UIView 39 | 40 | override var canBecomeFocused: Bool { 41 | false 42 | } 43 | 44 | override var canBecomeFirstResponder: Bool { 45 | false 46 | } 47 | 48 | override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { 49 | return container?.hitTest(point, with: event) 50 | } 51 | 52 | override func layoutSubviews() { 53 | super.layoutSubviews() 54 | isUserInteractionEnabled = content.isUserInteractionEnabled 55 | } 56 | 57 | // MARK: - Private methods 58 | 59 | private func setupInitialState() { 60 | appendContent(to: container) 61 | 62 | backgroundColor = .clear 63 | isUserInteractionEnabled = content.isUserInteractionEnabled 64 | } 65 | 66 | private func activateLayoutConstraintsOfContent(to view: UIView) { 67 | [ 68 | content.topAnchor.constraint(equalTo: view.topAnchor), 69 | content.bottomAnchor.constraint(equalTo: view.bottomAnchor), 70 | content.leftAnchor.constraint(equalTo: view.leftAnchor), 71 | content.rightAnchor.constraint(equalTo: view.rightAnchor) 72 | ].forEach { $0.isActive = true } 73 | } 74 | 75 | private func appendContent(to view: UIView?) { 76 | guard let view = view else { 77 | return 78 | } 79 | view.addSubview(content) 80 | view.isUserInteractionEnabled = true 81 | content.translatesAutoresizingMaskIntoConstraints = false 82 | activateLayoutConstraintsOfContent(to: view) 83 | } 84 | 85 | } 86 | 87 | // MARK: - ScreenshotInvincibleContainerProtocol 88 | 89 | extension ScreenshotInvincibleContainer: ScreenshotInvincibleContainerProtocol { 90 | 91 | func eraseOldAndAddnewContent(_ newContent: UIView) { 92 | content.removeFromSuperview() 93 | content = newContent 94 | appendContent(to: container) 95 | } 96 | 97 | func setupContanerAsHideContentInScreenshots() { 98 | isSecureTextEntry = true 99 | } 100 | 101 | func setupContanerAsDisplayContentInScreenshots() { 102 | isSecureTextEntry = false 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /Sources/SnapshotSafeView/Core/ScreenshotProtectController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScreenshotProtectController.swift 3 | // 4 | // 5 | // Created by Князьков Илья on 02.03.2022. 6 | // 7 | 8 | import class UIKit.UIView 9 | 10 | /** 11 | Controller on hide from screenshot and screen recording states of content 12 | 13 | Example usage: 14 | ``` 15 | final class ExampleSecureViewController: UIViewController { 16 | 17 | let hiddenFromScreenshotButtonController = ScreenshotProtectController(content: UIButton()) 18 | 19 | override func viewDidLoad() { 20 | super.viewDidLoad() 21 | hiddenFromScreenshotButtonController.content.backgroundColor = .red // UI customization apply to content 22 | hiddenFromScreenshotButtonController.content.layer.cornerRadius = 16 23 | 24 | view.addSubview(hiddenFromScreenshotButtonController.container) 25 | hiddenFromScreenshotButtonController.container // Layout control apply to container 26 | .position 27 | .pin(to: view.safeAreaLayoutGuide, const: 65) 28 | 29 | hiddenFromScreenshotButtonController.setupContentAsHiddenInScreenshotMode() // apply hidden mode 30 | // content will be removed from system screenshots and screen recording 31 | } 32 | 33 | } 34 | 35 | ``` 36 | */ 37 | open class ScreenshotProtectController: ScreenshotProtectControllerProtocol { 38 | 39 | public typealias ProtectiveContainer = ScreenshotInvincibleContainerProtocol 40 | 41 | /// - View, which will be hidden on screenshots and screen recording 42 | /// - All operation with UI customization need perform at content 43 | public var content: Content 44 | 45 | /// - Container view, all operation with layout need perform at container 46 | public lazy var container: ProtectiveContainer = ScreenshotInvincibleContainer(content: content) 47 | 48 | public init(content: Content) { 49 | self.content = content 50 | } 51 | 52 | public func eraseOldAndAddnewContent(_ newContent: Content) { 53 | container.eraseOldAndAddnewContent(newContent) 54 | } 55 | 56 | public func setupContentAsHiddenInScreenshotMode() { 57 | container.setupContanerAsHideContentInScreenshots() 58 | } 59 | 60 | public func setupContentAsDisplayedInScreenshotMode() { 61 | container.setupContanerAsDisplayContentInScreenshots() 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /Sources/SnapshotSafeView/Library/UIView+SnapshotSafeController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+SnapshotSafeController.swift 3 | // 4 | // 5 | // Created by Илья Князьков on 10.11.2022. 6 | // 7 | 8 | import UIKit 9 | import Foundation 10 | 11 | public extension UIView { 12 | 13 | /// Wraps view in ``SnpashotSafeController`` with saving ``Auto Layout`` properties. 14 | /// But this is irreversible process and for much control use directly ``SnapshotSaveController`` instead. 15 | /// - Warning: May destroy layout of view! 16 | /** 17 | # Example 18 | ```swift 19 | let someView = UIView() 20 | 21 | someView.translatesAutoresizingMaskIntoConstraints = false 22 | someView.topAnchor.constraint(superView.topAnchor, constant: 12).isActive = true 23 | someView.setupAsHiddenFromScreenshot() 24 | ``` 25 | */ 26 | func setupAsHiddenFromSystemScreenshotsAndVideoRecordings() { 27 | guard 28 | let superview = self.superview, 29 | !HiddenContainerRecognizer().viewIsAlreadyInHiddenContainer(self) 30 | else { 31 | return 32 | } 33 | 34 | let alreadyUsedConstraints = copiedConstraints(from: self) 35 | let snapshotSafeController = ScreenshotProtectController(content: self) 36 | 37 | snapshotSafeController.setupContentAsHiddenInScreenshotMode() 38 | 39 | superview.addSubview(snapshotSafeController.container) 40 | snapshotSafeController.container.translatesAutoresizingMaskIntoConstraints = false 41 | 42 | alreadyUsedConstraints 43 | .fromRelationShip 44 | .map { constraint in 45 | generateConstraint(for: constraint.firstItem, to: snapshotSafeController.container, from: constraint) 46 | } 47 | .forEach(superview.addConstraint) 48 | 49 | alreadyUsedConstraints 50 | .toRelationship 51 | .map { constraint in 52 | generateConstraint(for: snapshotSafeController.container, to: constraint.secondItem, from: constraint) 53 | } 54 | .forEach(superview.addConstraint) 55 | } 56 | 57 | } 58 | 59 | private extension UIView { 60 | 61 | // MARK: - Nested Types 62 | 63 | struct Constraints { 64 | 65 | let toRelationship: [NSLayoutConstraint] 66 | let fromRelationShip: [NSLayoutConstraint] 67 | 68 | func insertedToRelatinshipConstraint(_ constraint: NSLayoutConstraint) -> Self { 69 | return Constraints(toRelationship: toRelationship + [constraint], fromRelationShip: fromRelationShip) 70 | } 71 | 72 | func insertedfromRelatinshipConstraint(_ constraint: NSLayoutConstraint) -> Self { 73 | return Constraints(toRelationship: toRelationship, fromRelationShip: fromRelationShip + [constraint]) 74 | } 75 | 76 | } 77 | 78 | // MARK: - Private Methods 79 | 80 | func copiedConstraints(from view: UIView) -> Constraints { 81 | let constraints = Constraints(toRelationship: [], fromRelationShip: []) 82 | guard let superView = view.superview else { 83 | return constraints 84 | } 85 | 86 | return superView 87 | .constraints 88 | .reduce(constraints) { partialResult, constraint in 89 | if let firstItem = constraint.firstItem as? UIView, firstItem == view { 90 | return partialResult.insertedToRelatinshipConstraint(constraint) 91 | } else if let secondItem = constraint.secondItem as? UIView, secondItem == view { 92 | return partialResult.insertedfromRelatinshipConstraint(constraint) 93 | } 94 | return partialResult 95 | } 96 | } 97 | 98 | func generateConstraint( 99 | for forItem: Any?, 100 | to toItem: Any?, 101 | from constraint: NSLayoutConstraint 102 | ) -> NSLayoutConstraint { 103 | return NSLayoutConstraint( 104 | item: forItem ?? Void(), 105 | attribute: constraint.firstAttribute, 106 | relatedBy: constraint.relation, 107 | toItem: toItem, 108 | attribute: constraint.secondAttribute, 109 | multiplier: constraint.multiplier, 110 | constant: constraint.constant 111 | ) 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /Sources/SnapshotSafeView/Library/ViewWithDisabledPointInsideCheck.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewWithDisabledPointInsideCheck.swift 3 | // 4 | // 5 | // Created by Илья Князьков on 21.02.2023. 6 | // 7 | 8 | import UIKit 9 | 10 | final class ViewWithDisabledPointInsideCheck: UIView { 11 | 12 | override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { 13 | return true 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /Sources/SnapshotSafeView/Multiplatform/Flutter/SnapshotSafeViewFlutterInterceptor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SnapshotSafeViewFlutterInterceptor.swift 3 | // 4 | // 5 | // Created by Илья Князьков on 30.01.2023. 6 | // 7 | 8 | import UIKit 9 | 10 | /** - Note: Should be retained, because the desired controller may not appear immediately 11 | 12 | **Usage:** 13 | ```swift 14 | import UIKit 15 | import Flutter 16 | import SnapshotSafeView 17 | 18 | @UIApplicationMain 19 | @objc class AppDelegate: FlutterAppDelegate { 20 | 21 | private let snapshotSafeViewInterceptor = SnapshotSafeViewFlutterInterceptor() 22 | 23 | override func application( 24 | _ application: UIApplication, 25 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 26 | ) -> Bool { 27 | GeneratedPluginRegistrant.register(with: self) 28 | snapshotSafeViewInterceptor.performSwitchView(in: window) /// Wait for ``FlutterViewController`` 29 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 30 | } 31 | 32 | } 33 | ``` 34 | */ 35 | public final class SnapshotSafeViewFlutterInterceptor { 36 | 37 | // MARK: - Private Properties 38 | 39 | private let multiplatformSnapshotSafeView: UIView 40 | private let rootControllerInterceptor: RootControllerInterceptorProtocol 41 | 42 | // MARK: - Initialization 43 | 44 | public init() { 45 | self.multiplatformSnapshotSafeView = MultiplatformBridgeView() 46 | self.rootControllerInterceptor = RootControllerInterceptor() 47 | } 48 | 49 | // MARK: - Public Methods 50 | 51 | public func performSwitchView(in window: UIWindow) { 52 | rootControllerInterceptor.performSwitchView( 53 | in: window, 54 | expectedContainer: multiplatformSnapshotSafeView, 55 | waitWhenRootBecome: FlutterExpectation() 56 | ) 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /Sources/SnapshotSafeView/MultiplatformBridgeView/MultiplatformBridgeView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MultiplatformBridgeView.swift 3 | // 4 | // 5 | // Created by Илья Князьков on 19.01.2023. 6 | // 7 | 8 | import UIKit 9 | 10 | open class MultiplatformBridgeView: UIView { 11 | 12 | // MARK: - Private Properties 13 | 14 | private let screenshotSafeContainer: MultiplatformContainer 15 | private var setupInitialStateIsPerformed: Bool = false 16 | 17 | // MARK: - Initialization 18 | 19 | public init() { 20 | self.screenshotSafeContainer = MultiplatformContainer(content: ViewWithDisabledPointInsideCheck()) 21 | super.init(frame: .zero) 22 | 23 | configureScreenshotSafeContainer() 24 | } 25 | 26 | public required init?(coder: NSCoder) { 27 | fatalError("init(coder:) has not been implemented") 28 | } 29 | 30 | // MARK: - UIView 31 | 32 | override open func addSubview(_ view: UIView) { 33 | guard setupInitialStateIsPerformed else { 34 | return super.addSubview(view) 35 | } 36 | screenshotSafeContainer.content.addSubview(view) 37 | } 38 | 39 | } 40 | 41 | // MARK: - Private Methods 42 | 43 | private extension MultiplatformBridgeView { 44 | 45 | func configureScreenshotSafeContainer() { 46 | screenshotSafeContainer.setupContanerAsHideContentInScreenshots() 47 | addSubview(screenshotSafeContainer) 48 | [ 49 | screenshotSafeContainer.topAnchor.constraint(equalTo: topAnchor), 50 | screenshotSafeContainer.bottomAnchor.constraint(equalTo: bottomAnchor), 51 | screenshotSafeContainer.leftAnchor.constraint(equalTo: leftAnchor), 52 | screenshotSafeContainer.rightAnchor.constraint(equalTo: rightAnchor) 53 | ].forEach { $0.isActive = true } 54 | setupInitialStateIsPerformed = true 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /Sources/SnapshotSafeView/MultiplatformBridgeView/MultiplatformContainer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MultiplatformContainer.swift 3 | // 4 | // 5 | // Created by Илья Князьков on 21.02.2023. 6 | // 7 | 8 | import UIKit 9 | 10 | final class MultiplatformContainer: ScreenshotInvincibleContainer { 11 | 12 | override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { 13 | return content.hitTest(point, with: event) 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /Sources/SnapshotSafeView/Protocols/ScreenshotInvincibleContainerProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScreenshotInvincibleContainerProtocol.swift.swift 3 | // 4 | // 5 | // Created by Князьков Илья on 02.03.2022. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | public protocol ScreenshotInvincibleContainerProtocol: UIView { 12 | 13 | func eraseOldAndAddnewContent(_ newContent: UIView) 14 | func setupContanerAsHideContentInScreenshots() 15 | func setupContanerAsDisplayContentInScreenshots() 16 | 17 | } 18 | -------------------------------------------------------------------------------- /Sources/SnapshotSafeView/Protocols/ScreenshotProtectControllerProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScreenshotProtectControllerProtocol.swift 3 | // 4 | // 5 | // Created by Князьков Илья on 02.03.2022. 6 | // 7 | 8 | import UIKit 9 | 10 | public protocol ScreenshotProtectControllerProtocol { 11 | 12 | associatedtype Content 13 | associatedtype ProtectiveContainer 14 | 15 | var content: Content { get set } 16 | var container: ProtectiveContainer { get set } 17 | 18 | func eraseOldAndAddnewContent(_ newContent: Content) 19 | func setupContentAsHiddenInScreenshotMode() 20 | func setupContentAsDisplayedInScreenshotMode() 21 | 22 | } 23 | 24 | extension ScreenshotProtectControllerProtocol where Self: ScreenshotInvincibleContainerProtocol { 25 | 26 | func setupContentAsHiddenInScreenshotMode() { 27 | setupContentAsDisplayedInScreenshotMode() 28 | } 29 | 30 | func setupContentAsDisplayedInScreenshotMode() { 31 | setupContentAsDisplayedInScreenshotMode() 32 | } 33 | 34 | } 35 | 36 | 37 | -------------------------------------------------------------------------------- /Sources/SnapshotSafeView/RootControllerInterceptor/RootControllerExpectation/Flutter/FlutterExpectation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FlutterExpectation.swift 3 | // 4 | // 5 | // Created by Илья Князьков on 30.01.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct FlutterExpectation: RootControllerExpectation { 11 | 12 | public var typeInStringRepresentation: String = "FlutterViewController" 13 | 14 | public init() { } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /Sources/SnapshotSafeView/RootControllerInterceptor/RootControllerExpectation/RootControllerExpectation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RootControllerExpectation.swift 3 | // 4 | // 5 | // Created by Илья Князьков on 30.01.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol RootControllerExpectation { 11 | 12 | var typeInStringRepresentation: String { get } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /Sources/SnapshotSafeView/RootControllerInterceptor/RootControllerInterceptor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RootControllerInterceptor.swift 3 | // 4 | // 5 | // Created by Илья Князьков on 30.01.2023. 6 | // 7 | 8 | import UIKit 9 | 10 | public final class RootControllerInterceptor: RootControllerInterceptorProtocol { 11 | 12 | // MARK: - Private Properties 13 | 14 | private var timerWhichListenWindowsRoot: DispatchSourceTimer? 15 | 16 | // MARK: - Deinitialization 17 | 18 | deinit { 19 | stopWaitingRootInWindow() 20 | } 21 | 22 | // MARK: - RootControllerInterceptorProtocol 23 | 24 | public func performSwitchView( 25 | in window: UIWindow, 26 | expectedContainer view: UIView, 27 | waitWhenRootBecome rootExpectation: RootControllerExpectation 28 | ) { 29 | guard 30 | isNeedPerformSwitching(window.rootViewController, expected: rootExpectation), 31 | let viewController = window.rootViewController 32 | else { 33 | return startWaitingExpectedController(in: window, expectedContainer: view, waitWhenRootBecome: rootExpectation) 34 | } 35 | moveContainerBehindDesiredView(view, desired: viewController.view, in: viewController) 36 | } 37 | 38 | } 39 | 40 | 41 | // MARK: - Private Methods 42 | 43 | private extension RootControllerInterceptor { 44 | 45 | func startWaitingExpectedController( 46 | in window: UIWindow, 47 | expectedContainer view: UIView, 48 | waitWhenRootBecome rootExpectation: RootControllerExpectation 49 | ) { 50 | stopWaitingRootInWindow() 51 | 52 | timerWhichListenWindowsRoot = DispatchSource.makeTimerSource(queue: .main) 53 | timerWhichListenWindowsRoot?.setEventHandler { [weak self] in 54 | guard 55 | self?.isNeedPerformSwitching(window.rootViewController, expected: rootExpectation) == .some(true), 56 | let viewController = window.rootViewController 57 | else { 58 | return 59 | } 60 | self?.moveContainerBehindDesiredView(view, desired: viewController.view, in: viewController) 61 | self?.stopWaitingRootInWindow() 62 | } 63 | 64 | timerWhichListenWindowsRoot?.schedule(deadline: .now(), repeating: 0.005) 65 | timerWhichListenWindowsRoot?.resume() 66 | } 67 | 68 | func isNeedPerformSwitching(_ controller: UIViewController?, expected rootExpectation: RootControllerExpectation) -> Bool { 69 | guard let rootViewController = controller else { 70 | return false 71 | } 72 | let typeOfRootInString = String(describing: type(of: rootViewController)) 73 | return typeOfRootInString == rootExpectation.typeInStringRepresentation 74 | } 75 | 76 | func moveContainerBehindDesiredView(_ container: UIView, desired view: UIView, in controller: UIViewController) { 77 | DispatchQueue.main.async { 78 | container.autoresizingMask = [.flexibleWidth, .flexibleHeight] 79 | controller.view = container 80 | 81 | container.addSubview(view) 82 | 83 | view.translatesAutoresizingMaskIntoConstraints = false 84 | [ 85 | view.topAnchor.constraint(equalTo: container.topAnchor), 86 | view.bottomAnchor.constraint(equalTo: container.bottomAnchor), 87 | view.leadingAnchor.constraint(equalTo: container.leadingAnchor), 88 | view.trailingAnchor.constraint(equalTo: container.trailingAnchor) 89 | ].forEach { constraint in 90 | constraint.isActive = true 91 | } 92 | } 93 | } 94 | 95 | func stopWaitingRootInWindow() { 96 | timerWhichListenWindowsRoot?.cancel() 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /Sources/SnapshotSafeView/RootControllerInterceptor/RootControllerInterceptorProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RootControllerInterceptorProtocol.swift 3 | // 4 | // 5 | // Created by Илья Князьков on 30.01.2023. 6 | // 7 | 8 | import UIKit 9 | import Foundation 10 | 11 | /// - Note: Should be retained, because the desired controller may not appear immediately 12 | public protocol RootControllerInterceptorProtocol { 13 | 14 | /// Method waiting desired controller in ``UIWindow.rootViewController`` and will try to switch him at `view` 15 | /// 16 | /// - Parameters: 17 | /// - window: ``UIWindow`` 18 | /// - view: ``UIView`` for switching 19 | /// - rootExpectation: Swappable view ``RootControllerExpectation`` 20 | func performSwitchView( 21 | in window: UIWindow, 22 | expectedContainer view: UIView, 23 | waitWhenRootBecome rootExpectation: RootControllerExpectation 24 | ) 25 | 26 | } 27 | -------------------------------------------------------------------------------- /Sources/SnapshotSafeView/SwiftUIBridge/SnaphotSafeViewSwiftUIBridge.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SnaphotSafeViewSwiftUIBridge.swift 3 | // 4 | // 5 | // Created by Илья Князьков on 10.09.2022. 6 | // 7 | 8 | 9 | #if canImport(SwiftUI) 10 | import SwiftUI 11 | 12 | @available(iOS 13, *) 13 | struct SnaphotSafeViewSwiftUIBridge: SnaphotSafeViewSwiftUIBridgeProtocol { 14 | 15 | // MARK: - Private Properties 16 | 17 | private let content: () -> Content 18 | 19 | // MARK: - Initialization 20 | 21 | init(content: @autoclosure @escaping () -> Content) { 22 | self.content = content 23 | } 24 | 25 | // MARK: - SnaphotSafeViewSwiftUIBridgeProtocol 26 | 27 | typealias ProtectedView = ProtectedViewRepresentable 28 | 29 | func hiddenFromSystemSnaphot() -> ProtectedView { 30 | let hostingController = UIHostingController(rootView: content()) 31 | return ProtectedViewRepresentable(content: hostingController.view) 32 | } 33 | 34 | } 35 | 36 | // MARK: - Nested Types 37 | 38 | @available(iOS 13, *) 39 | extension SnaphotSafeViewSwiftUIBridge { 40 | 41 | struct ProtectedViewRepresentable: UIViewRepresentable { 42 | 43 | // MARK: - Internal Properties 44 | 45 | let viewContent: UIView 46 | 47 | // MARK: - Initialization 48 | 49 | init(content: UIView) { 50 | self.viewContent = content 51 | } 52 | 53 | // MARK: - UIViewRepresentable 54 | 55 | func makeUIView(context: Context) -> UIView { 56 | let snapshotController = ScreenshotProtectController(content: viewContent) 57 | snapshotController.setupContentAsHiddenInScreenshotMode() 58 | snapshotController.container.setContentHuggingPriority(.required, for: .vertical) 59 | snapshotController.container.setContentHuggingPriority(.fittingSizeLevel, for: .horizontal) 60 | return snapshotController.container 61 | } 62 | 63 | func updateUIView(_ uiView: UIView, context: Context) { } 64 | 65 | } 66 | 67 | } 68 | #endif 69 | -------------------------------------------------------------------------------- /Sources/SnapshotSafeView/SwiftUIBridge/SnaphotSafeViewSwiftUIBridgeProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SnaphotSafeViewSwiftUIBridgeProtocol.swift 3 | // 4 | // 5 | // Created by Илья Князьков on 10.09.2022. 6 | // 7 | 8 | #if canImport(SwiftUI) 9 | import SwiftUI 10 | 11 | @available(iOS 13, *) 12 | protocol SnaphotSafeViewSwiftUIBridgeProtocol { 13 | 14 | associatedtype ProtectedView: View 15 | 16 | func hiddenFromSystemSnaphot() -> ProtectedView 17 | 18 | } 19 | #endif 20 | -------------------------------------------------------------------------------- /Sources/SnapshotSafeView/SwiftUIBridge/View+SnaphotSafeViewSwiftUIBridge.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+SnaphotSafeViewSwiftUIBridge.swift 3 | // 4 | // 5 | // Created by Илья Князьков on 10.09.2022. 6 | // 7 | 8 | #if canImport(SwiftUI) 9 | import SwiftUI 10 | 11 | @available(iOS 13, *) 12 | public extension View { 13 | 14 | // MARK: - Private Properties 15 | 16 | private var snapshotBridge: SnaphotSafeViewSwiftUIBridge { 17 | SnaphotSafeViewSwiftUIBridge(content: self) 18 | } 19 | 20 | // MARK: - Public Methods 21 | 22 | /// Wrapped `View` in`ScreenshotProtectController`, will be hidden from system screenshots, during ``condition`` is true 23 | /// - Parameter condition: During condittion is true, return wrapped `View` 24 | /// - Returns: Wrapped `View` or just `Self`, depend at condition 25 | @ViewBuilder 26 | func hiddenFromSystemSnaphot(when condition: @autoclosure () -> Bool) -> some View { 27 | if condition() { 28 | snapshotBridge.hiddenFromSystemSnaphot() 29 | } else { 30 | self 31 | } 32 | } 33 | 34 | /// Wrapped `View` in`ScreenshotProtectController` with default padding, will be hidden from system screenshots, during ``condition`` is true 35 | /// - Parameter condition: During condittion is true, return wrapped `View` 36 | /// - Returns: Wrapped `View` or just `Self`, depend at condition 37 | @ViewBuilder 38 | func hiddenFromSystemSnaphotWithDefaultPadding(when condition: @autoclosure () -> Bool) -> some View { 39 | if condition() { 40 | snapshotBridge.hiddenFromSystemSnaphot() 41 | .padding(.vertical) 42 | } else { 43 | self 44 | } 45 | } 46 | 47 | } 48 | #endif 49 | -------------------------------------------------------------------------------- /Tests/SnapshotSafeViewTests/RootControllerInterceptorTests/RootControllerInterceptorTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RootControllerInterceptorTests.swift 3 | // 4 | // 5 | // Created by Илья Князьков on 31.01.2023. 6 | // 7 | 8 | import XCTest 9 | @testable import SnapshotSafeView 10 | 11 | final class RootControllerInterceptorTests: XCTestCase { 12 | 13 | var interceptor: RootControllerInterceptorProtocol { 14 | RootControllerInterceptor() 15 | } 16 | 17 | func testOnCorrectInterceptingAndSwitchingViews() { 18 | let expectation = expectation(description: "Wait for switching") 19 | 20 | let expectedView = TestableView() 21 | let controller = TestableController() 22 | let frontView = controller.view 23 | 24 | let window = UIWindow() 25 | window.rootViewController = controller 26 | 27 | interceptor.performSwitchView(in: window, expectedContainer: expectedView, waitWhenRootBecome: TestableExpectation()) 28 | 29 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { 30 | XCTAssertEqual(controller.view, expectedView) 31 | XCTAssertEqual(controller.view.subviews.first, frontView) 32 | expectation.fulfill() 33 | } 34 | 35 | wait(for: [expectation], timeout: 0.1) 36 | } 37 | 38 | func testOnCorrectWaitingForInterceptingAndSwitchingViews() { 39 | let expectation = expectation(description: "Wait for switching") 40 | 41 | let interceptor = self.interceptor 42 | let expectedView = TestableView() 43 | let controller = TestableController() 44 | let frontView = controller.view 45 | 46 | let window = UIWindow() 47 | 48 | interceptor.performSwitchView(in: window, expectedContainer: expectedView, waitWhenRootBecome: TestableExpectation()) 49 | 50 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { 51 | window.rootViewController = controller 52 | } 53 | 54 | DispatchQueue.main.asyncAfter(deadline: .now() + 1) { 55 | XCTAssertEqual(controller.view, expectedView) 56 | XCTAssertEqual(controller.view.subviews.first, frontView) 57 | expectation.fulfill() 58 | } 59 | 60 | wait(for: [expectation], timeout: 2) 61 | } 62 | 63 | } 64 | 65 | // MARK: - Mocks 66 | 67 | fileprivate final class TestableController: UIViewController { } 68 | 69 | fileprivate struct TestableExpectation: RootControllerExpectation { 70 | 71 | var typeInStringRepresentation: String { 72 | String(describing: type(of: TestableController())) 73 | } 74 | 75 | } 76 | 77 | fileprivate final class TestableView: UIView { } 78 | --------------------------------------------------------------------------------