├── .gitignore ├── .swift-version ├── .travis.yml ├── Example ├── AppDelegate.swift ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Info.plist └── ViewController.swift ├── LICENSE ├── PIPKit.podspec ├── PIPKit.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ ├── Example.xcscheme │ └── PIPKit.xcscheme ├── PIPKit ├── Classes │ ├── AVPIPKit │ │ ├── AVPIPKitRenderer.swift │ │ ├── AVPIPKitUsable+UIKit.swift │ │ ├── AVPIPKitUsable.swift │ │ ├── AVPIPKitVideoController.swift │ │ └── AVPIPKitVideoProvider.swift │ ├── Extension │ │ ├── UIView+Associated.swift │ │ ├── UIViewController+Associated.swift │ │ └── UIWindow+Extension.swift │ └── PIPKit │ │ ├── KeyboardObserver.swift │ │ ├── PIPKit.swift │ │ ├── PIPKitEventDispatcher.swift │ │ └── PIPUsable.swift └── PIPKit.h ├── Package.swift ├── README.md └── Screenshot ├── default.gif ├── resize.gif └── transition.gif /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | # Package.resolved 41 | .build/ 42 | 43 | # CocoaPods 44 | # 45 | # We recommend against adding the Pods directory to your .gitignore. However 46 | # you should judge for yourself, the pros and cons are mentioned at: 47 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 48 | # 49 | # Pods/ 50 | 51 | # Carthage 52 | # 53 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 54 | # Carthage/Checkouts 55 | 56 | Carthage/Build 57 | 58 | # fastlane 59 | # 60 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 61 | # screenshots whenever they are needed. 62 | # For more information about the recommended setup visit: 63 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 64 | 65 | fastlane/report.xml 66 | fastlane/Preview.html 67 | fastlane/screenshots/**/*.png 68 | fastlane/test_output 69 | 70 | .DS_Store 71 | 72 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 5.0 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: osx 2 | osx_image: xcode11 3 | sudo: false 4 | language: objective-c 5 | 6 | env: 7 | - SDK="iphoneos13.0" 8 | 9 | before_install: 10 | - set -o pipefail 11 | 12 | script: 13 | - xcodebuild clean build test 14 | -workspace Example/Example.xcworkspace 15 | -scheme Example 16 | -destination 'platform=iOS Simulator,name=iPhone 11,OS=13.0' 17 | -sdk $SDK 18 | -configuration Debug 19 | -enableCodeCoverage YES 20 | CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO | xcpretty -c 21 | -------------------------------------------------------------------------------- /Example/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Example 4 | // 5 | // Created by Kofktu on 2022/01/03. 6 | // 7 | 8 | import UIKit 9 | 10 | @main 11 | class AppDelegate: UIResponder, UIApplicationDelegate { 12 | 13 | var window: UIWindow? 14 | 15 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 16 | // Override point for customization after application launch. 17 | return true 18 | } 19 | 20 | 21 | } 22 | 23 | -------------------------------------------------------------------------------- /Example/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Example/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Example/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 29 | 36 | 43 | 50 | 57 | 64 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 134 | 141 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | -------------------------------------------------------------------------------- /Example/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIBackgroundModes 6 | 7 | audio 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Example/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Example 4 | // 5 | // Created by Kofktu on 2022/01/03. 6 | // 7 | 8 | import UIKit 9 | import PIPKit 10 | 11 | #if canImport(Combine) 12 | import Combine 13 | #endif 14 | 15 | class ViewController: UIViewController { 16 | 17 | @IBOutlet private weak var textField: UITextField! 18 | 19 | private var cancellables: Any? 20 | 21 | class func viewController() -> ViewController { 22 | let storyboard = UIStoryboard(name: "Main", bundle: nil) 23 | guard let viewController = storyboard.instantiateViewController(withIdentifier: "ViewController") as? ViewController else { 24 | fatalError("ViewController is null") 25 | } 26 | return viewController 27 | } 28 | 29 | override func viewDidLoad() { 30 | super.viewDidLoad() 31 | // Do any additional setup after loading the view, typically from a nib. 32 | 33 | title = "PIPKit" 34 | } 35 | 36 | override func touchesEnded(_ touches: Set, with event: UIEvent?) { 37 | super.touchesEnded(touches, with: event) 38 | textField.resignFirstResponder() 39 | } 40 | 41 | // MARK: - Private 42 | private func setupDismissNavigationItem() { 43 | navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, 44 | target: self, 45 | action: #selector(onDismiss(_:))) 46 | } 47 | 48 | // MARK: - Action 49 | @objc 50 | private func onDismiss(_ sender: UIBarButtonItem) { 51 | dismiss(animated: true, completion: nil) 52 | } 53 | 54 | @IBAction private func onPIPViewController(_ sender: UIButton) { 55 | PIPKit.show(with: PIPViewController()) 56 | } 57 | 58 | @IBAction private func onPIPViewControllerWithXib(_ sender: UIButton) { 59 | PIPKit.show(with: PIPXibViewController.viewController()) 60 | } 61 | 62 | @IBAction private func onPIPDismiss() { 63 | PIPKit.dismiss(animated: true) 64 | } 65 | 66 | @IBAction private func onPushViewController(_ sender: UIButton) { 67 | let viewController = ViewController.viewController() 68 | navigationController?.pushViewController(viewController, animated: true) 69 | } 70 | 71 | @IBAction private func onPresentViewController(_ sender: UIButton) { 72 | let viewController = ViewController.viewController() 73 | let naviController = UINavigationController(rootViewController: viewController) 74 | present(naviController, animated: true) { [unowned viewController] in 75 | viewController.setupDismissNavigationItem() 76 | } 77 | } 78 | 79 | @IBAction private func onAVPIPKitStart() { 80 | guard #available(iOS 15.0, *), isAVKitPIPSupported else { 81 | print("AVPIPKit not supported") 82 | return 83 | } 84 | 85 | var cancellables = Set() 86 | exitPublisher 87 | .sink(receiveValue: { 88 | print("exit") 89 | }) 90 | .store(in: &cancellables) 91 | 92 | self.cancellables = cancellables 93 | startPictureInPicture() 94 | } 95 | 96 | @IBAction private func onAVPIPKitStop() { 97 | guard #available(iOS 15.0, *), isAVKitPIPSupported else { 98 | print("AVPIPKit not supported") 99 | return 100 | } 101 | 102 | stopPictureInPicture() 103 | } 104 | 105 | } 106 | 107 | class PIPViewController: UIViewController, PIPUsable { 108 | 109 | // var initialState: PIPState { return .pip } 110 | // var pipSize: CGSize { return CGSize(width: 200.0, height: 200.0) } 111 | 112 | override func viewDidLoad() { 113 | super.viewDidLoad() 114 | view.backgroundColor = .blue 115 | view.layer.borderColor = UIColor.red.cgColor 116 | view.layer.borderWidth = 1.0 117 | } 118 | 119 | override func touchesEnded(_ touches: Set, with event: UIEvent?) { 120 | super.touchesEnded(touches, with: event) 121 | 122 | if PIPKit.isPIP { 123 | stopPIPMode() 124 | } else { 125 | startPIPMode() 126 | } 127 | } 128 | 129 | func didChangedState(_ state: PIPState) { 130 | switch state { 131 | case .pip: 132 | print("PIPViewController.pip") 133 | case .full: 134 | print("PIPViewController.full") 135 | } 136 | } 137 | 138 | } 139 | 140 | class PIPXibViewController: UIViewController, PIPUsable { 141 | 142 | var initialState: PIPState { return .full } 143 | var initialPosition: PIPPosition { return .topRight } 144 | var pipEdgeInsets: UIEdgeInsets { return UIEdgeInsets(top: 30, left: 20, bottom: 30, right: 20) } 145 | var pipSize: CGSize = CGSize(width: 100.0, height: 100.0) 146 | var pipShadow: PIPShadow? = nil 147 | var pipCorner: PIPCorner? = nil 148 | 149 | class func viewController() -> PIPXibViewController { 150 | let storyboard = UIStoryboard(name: "Main", bundle: nil) 151 | guard let viewController = storyboard.instantiateViewController(withIdentifier: "PIPXibViewController") as? PIPXibViewController else { 152 | fatalError("PIPXibViewController is null") 153 | } 154 | return viewController 155 | } 156 | 157 | func didChangedState(_ state: PIPState) { 158 | switch state { 159 | case .pip: 160 | print("PIPXibViewController.pip") 161 | case .full: 162 | print("PIPXibViewController.full") 163 | } 164 | } 165 | 166 | func didChangePosition(_ position: PIPPosition) { 167 | switch position { 168 | case .topLeft: 169 | print("PIPXibViewController.topLeft") 170 | case .middleLeft: 171 | print("PIPXibViewController.middleLeft") 172 | case .bottomLeft: 173 | print("PIPXibViewController.bottomLeft") 174 | case .topRight: 175 | print("PIPXibViewController.topRight") 176 | case .middleRight: 177 | print("PIPXibViewController.middleRight") 178 | case .bottomRight: 179 | print("PIPXibViewController.bottomRight") 180 | } 181 | } 182 | 183 | // MARK: - Action 184 | @IBAction private func onFullAndPIP(_ sender: UIButton) { 185 | if PIPKit.isPIP { 186 | stopPIPMode() 187 | } else { 188 | startPIPMode() 189 | } 190 | } 191 | 192 | @IBAction private func onUpdatePIPSize(_ sender: UIButton) { 193 | pipSize = CGSize(width: 100 + Int(arc4random_uniform(100)), 194 | height: 100 + Int(arc4random_uniform(100))) 195 | setNeedsUpdatePIPFrame() 196 | } 197 | 198 | @IBAction private func onDismiss(_ sender: UIButton) { 199 | PIPKit.dismiss(animated: true) { 200 | print("PIPXibViewController.dismiss") 201 | } 202 | } 203 | } 204 | 205 | @available(iOS 15.0, *) 206 | extension ViewController: AVPIPUIKitUsable { 207 | 208 | var renderPolicy: AVPIPKitRenderPolicy { 209 | .once 210 | } 211 | 212 | var pipTargetView: UIView { 213 | view 214 | } 215 | 216 | } 217 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Taeun Kim 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 | -------------------------------------------------------------------------------- /PIPKit.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # Be sure to run `pod lib lint PIP.podspec' to ensure this is a 3 | # valid spec before submitting. 4 | # 5 | # Any lines starting with a # are optional, but their use is encouraged 6 | # To learn more about a Podspec see https://guides.cocoapods.org/syntax/podspec.html 7 | # 8 | 9 | Pod::Spec.new do |s| 10 | s.name = 'PIPKit' 11 | s.version = '1.1.0' 12 | s.summary = 'PIP(Picture in Picture) for iOS' 13 | 14 | # This description is used to generate tags and improve search results. 15 | # * Think: What does it do? Why did you write it? What is the focus? 16 | # * Try to keep it short, snappy and to the point. 17 | # * Write the description between the DESC delimiters below. 18 | # * Finally, don't worry about the indent, CocoaPods strips it! 19 | 20 | s.description = <<-DESC 21 | Picture in Picture to UIViewController 22 | DESC 23 | 24 | s.homepage = 'https://github.com/Kofktu/PIPKit' 25 | # s.screenshots = 'www.example.com/screenshots_1', 'www.example.com/screenshots_2' 26 | s.license = { :type => 'MIT', :file => 'LICENSE' } 27 | s.author = { 'kofktu@gmail.com' => 'kofktu@gmail.com' } 28 | s.source = { :git => 'https://github.com/Kofktu/PIPKit.git', :tag => s.version.to_s } 29 | # s.social_media_url = 'https://twitter.com/' 30 | 31 | s.ios.deployment_target = '12.0' 32 | s.swift_version = '5.0' 33 | 34 | s.source_files = 'PIPKit/Classes/**/*' 35 | 36 | # s.resource_bundles = { 37 | # 'PIP' => ['PIPKit/Assets/*.png'] 38 | # } 39 | 40 | # s.public_header_files = 'Pod/Classes/**/*.h' 41 | # s.frameworks = 'UIKit', 'MapKit' 42 | # s.dependency 'AFNetworking', '~> 2.3' 43 | end 44 | -------------------------------------------------------------------------------- /PIPKit.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 55; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | CE21D48128AA8CF900057736 /* KeyboardObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE21D48028AA8CF900057736 /* KeyboardObserver.swift */; }; 11 | CE96575E2782BB6D00303B7F /* AVPIPKitUsable.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE96575D2782BB6D00303B7F /* AVPIPKitUsable.swift */; }; 12 | CE9657602782BBCC00303B7F /* UIViewController+Associated.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE96575F2782BBCC00303B7F /* UIViewController+Associated.swift */; }; 13 | CE9657622782BC3900303B7F /* UIView+Associated.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE9657612782BC3900303B7F /* UIView+Associated.swift */; }; 14 | CE9657642782BC9100303B7F /* AVPIPKitVideoProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE9657632782BC9100303B7F /* AVPIPKitVideoProvider.swift */; }; 15 | CE9657662782BCAF00303B7F /* AVPIPKitVideoController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE9657652782BCAF00303B7F /* AVPIPKitVideoController.swift */; }; 16 | CE9657682782BEB500303B7F /* PIPKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CEE9972F2782A5CC002FA127 /* PIPKit.framework */; }; 17 | CE9657692782BEB500303B7F /* PIPKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = CEE9972F2782A5CC002FA127 /* PIPKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 18 | CEE5C8E027899B1500BEDD03 /* AVPIPKitRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE5C8DF27899B1500BEDD03 /* AVPIPKitRenderer.swift */; }; 19 | CEE5C8E22789A0CE00BEDD03 /* AVPIPKitUsable+UIKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE5C8E12789A0CE00BEDD03 /* AVPIPKitUsable+UIKit.swift */; }; 20 | CEE997332782A5CC002FA127 /* PIPKit.h in Headers */ = {isa = PBXBuildFile; fileRef = CEE997322782A5CC002FA127 /* PIPKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; 21 | CEE9973E2782A5F1002FA127 /* PIPKitEventDispatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE9973A2782A5F1002FA127 /* PIPKitEventDispatcher.swift */; }; 22 | CEE9973F2782A5F1002FA127 /* UIWindow+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE9973B2782A5F1002FA127 /* UIWindow+Extension.swift */; }; 23 | CEE997402782A5F1002FA127 /* PIPKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE9973C2782A5F1002FA127 /* PIPKit.swift */; }; 24 | CEE997412782A5F1002FA127 /* PIPUsable.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE9973D2782A5F1002FA127 /* PIPUsable.swift */; }; 25 | CEE997492782A62B002FA127 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE997482782A62B002FA127 /* AppDelegate.swift */; }; 26 | CEE9974D2782A62B002FA127 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE9974C2782A62B002FA127 /* ViewController.swift */; }; 27 | CEE997502782A62B002FA127 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = CEE9974E2782A62B002FA127 /* Main.storyboard */; }; 28 | CEE997522782A62B002FA127 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CEE997512782A62B002FA127 /* Assets.xcassets */; }; 29 | CEE997552782A62B002FA127 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = CEE997532782A62B002FA127 /* LaunchScreen.storyboard */; }; 30 | /* End PBXBuildFile section */ 31 | 32 | /* Begin PBXContainerItemProxy section */ 33 | CE9657582782A6AB00303B7F /* PBXContainerItemProxy */ = { 34 | isa = PBXContainerItemProxy; 35 | containerPortal = CEE997262782A5CC002FA127 /* Project object */; 36 | proxyType = 1; 37 | remoteGlobalIDString = CEE9972E2782A5CC002FA127; 38 | remoteInfo = PIPKit; 39 | }; 40 | CE96576A2782BEB500303B7F /* PBXContainerItemProxy */ = { 41 | isa = PBXContainerItemProxy; 42 | containerPortal = CEE997262782A5CC002FA127 /* Project object */; 43 | proxyType = 1; 44 | remoteGlobalIDString = CEE9972E2782A5CC002FA127; 45 | remoteInfo = PIPKit; 46 | }; 47 | /* End PBXContainerItemProxy section */ 48 | 49 | /* Begin PBXCopyFilesBuildPhase section */ 50 | CE96576C2782BEB500303B7F /* Embed Frameworks */ = { 51 | isa = PBXCopyFilesBuildPhase; 52 | buildActionMask = 2147483647; 53 | dstPath = ""; 54 | dstSubfolderSpec = 10; 55 | files = ( 56 | CE9657692782BEB500303B7F /* PIPKit.framework in Embed Frameworks */, 57 | ); 58 | name = "Embed Frameworks"; 59 | runOnlyForDeploymentPostprocessing = 0; 60 | }; 61 | /* End PBXCopyFilesBuildPhase section */ 62 | 63 | /* Begin PBXFileReference section */ 64 | CE21D48028AA8CF900057736 /* KeyboardObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardObserver.swift; sourceTree = ""; }; 65 | CE96575D2782BB6D00303B7F /* AVPIPKitUsable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPIPKitUsable.swift; sourceTree = ""; }; 66 | CE96575F2782BBCC00303B7F /* UIViewController+Associated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Associated.swift"; sourceTree = ""; }; 67 | CE9657612782BC3900303B7F /* UIView+Associated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Associated.swift"; sourceTree = ""; }; 68 | CE9657632782BC9100303B7F /* AVPIPKitVideoProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPIPKitVideoProvider.swift; sourceTree = ""; }; 69 | CE9657652782BCAF00303B7F /* AVPIPKitVideoController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPIPKitVideoController.swift; sourceTree = ""; }; 70 | CEE5C8DF27899B1500BEDD03 /* AVPIPKitRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPIPKitRenderer.swift; sourceTree = ""; }; 71 | CEE5C8E12789A0CE00BEDD03 /* AVPIPKitUsable+UIKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVPIPKitUsable+UIKit.swift"; sourceTree = ""; }; 72 | CEE9972F2782A5CC002FA127 /* PIPKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = PIPKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 73 | CEE997322782A5CC002FA127 /* PIPKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PIPKit.h; sourceTree = ""; }; 74 | CEE9973A2782A5F1002FA127 /* PIPKitEventDispatcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PIPKitEventDispatcher.swift; sourceTree = ""; }; 75 | CEE9973B2782A5F1002FA127 /* UIWindow+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIWindow+Extension.swift"; sourceTree = ""; }; 76 | CEE9973C2782A5F1002FA127 /* PIPKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PIPKit.swift; sourceTree = ""; }; 77 | CEE9973D2782A5F1002FA127 /* PIPUsable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PIPUsable.swift; sourceTree = ""; }; 78 | CEE997462782A62B002FA127 /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 79 | CEE997482782A62B002FA127 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 80 | CEE9974C2782A62B002FA127 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 81 | CEE9974F2782A62B002FA127 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 82 | CEE997512782A62B002FA127 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 83 | CEE997542782A62B002FA127 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 84 | CEE997562782A62B002FA127 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 85 | /* End PBXFileReference section */ 86 | 87 | /* Begin PBXFrameworksBuildPhase section */ 88 | CEE9972C2782A5CC002FA127 /* Frameworks */ = { 89 | isa = PBXFrameworksBuildPhase; 90 | buildActionMask = 2147483647; 91 | files = ( 92 | ); 93 | runOnlyForDeploymentPostprocessing = 0; 94 | }; 95 | CEE997432782A62B002FA127 /* Frameworks */ = { 96 | isa = PBXFrameworksBuildPhase; 97 | buildActionMask = 2147483647; 98 | files = ( 99 | CE9657682782BEB500303B7F /* PIPKit.framework in Frameworks */, 100 | ); 101 | runOnlyForDeploymentPostprocessing = 0; 102 | }; 103 | /* End PBXFrameworksBuildPhase section */ 104 | 105 | /* Begin PBXGroup section */ 106 | CE96575A2782BB3000303B7F /* AVPIPKit */ = { 107 | isa = PBXGroup; 108 | children = ( 109 | CE96575D2782BB6D00303B7F /* AVPIPKitUsable.swift */, 110 | CEE5C8E12789A0CE00BEDD03 /* AVPIPKitUsable+UIKit.swift */, 111 | CEE5C8DF27899B1500BEDD03 /* AVPIPKitRenderer.swift */, 112 | CE9657632782BC9100303B7F /* AVPIPKitVideoProvider.swift */, 113 | CE9657652782BCAF00303B7F /* AVPIPKitVideoController.swift */, 114 | ); 115 | path = AVPIPKit; 116 | sourceTree = ""; 117 | }; 118 | CE96575B2782BB4E00303B7F /* PIPKit */ = { 119 | isa = PBXGroup; 120 | children = ( 121 | CEE9973C2782A5F1002FA127 /* PIPKit.swift */, 122 | CEE9973D2782A5F1002FA127 /* PIPUsable.swift */, 123 | CEE9973A2782A5F1002FA127 /* PIPKitEventDispatcher.swift */, 124 | CE21D48028AA8CF900057736 /* KeyboardObserver.swift */, 125 | ); 126 | path = PIPKit; 127 | sourceTree = ""; 128 | }; 129 | CE96575C2782BB5900303B7F /* Extension */ = { 130 | isa = PBXGroup; 131 | children = ( 132 | CEE9973B2782A5F1002FA127 /* UIWindow+Extension.swift */, 133 | CE96575F2782BBCC00303B7F /* UIViewController+Associated.swift */, 134 | CE9657612782BC3900303B7F /* UIView+Associated.swift */, 135 | ); 136 | path = Extension; 137 | sourceTree = ""; 138 | }; 139 | CE9657672782BEB500303B7F /* Frameworks */ = { 140 | isa = PBXGroup; 141 | children = ( 142 | ); 143 | name = Frameworks; 144 | sourceTree = ""; 145 | }; 146 | CEE997252782A5CC002FA127 = { 147 | isa = PBXGroup; 148 | children = ( 149 | CEE997312782A5CC002FA127 /* PIPKit */, 150 | CEE997472782A62B002FA127 /* Example */, 151 | CEE997302782A5CC002FA127 /* Products */, 152 | CE9657672782BEB500303B7F /* Frameworks */, 153 | ); 154 | sourceTree = ""; 155 | }; 156 | CEE997302782A5CC002FA127 /* Products */ = { 157 | isa = PBXGroup; 158 | children = ( 159 | CEE9972F2782A5CC002FA127 /* PIPKit.framework */, 160 | CEE997462782A62B002FA127 /* Example.app */, 161 | ); 162 | name = Products; 163 | sourceTree = ""; 164 | }; 165 | CEE997312782A5CC002FA127 /* PIPKit */ = { 166 | isa = PBXGroup; 167 | children = ( 168 | CEE997392782A5F1002FA127 /* Classes */, 169 | CEE997322782A5CC002FA127 /* PIPKit.h */, 170 | ); 171 | path = PIPKit; 172 | sourceTree = ""; 173 | }; 174 | CEE997392782A5F1002FA127 /* Classes */ = { 175 | isa = PBXGroup; 176 | children = ( 177 | CE96575C2782BB5900303B7F /* Extension */, 178 | CE96575B2782BB4E00303B7F /* PIPKit */, 179 | CE96575A2782BB3000303B7F /* AVPIPKit */, 180 | ); 181 | path = Classes; 182 | sourceTree = ""; 183 | }; 184 | CEE997472782A62B002FA127 /* Example */ = { 185 | isa = PBXGroup; 186 | children = ( 187 | CEE997482782A62B002FA127 /* AppDelegate.swift */, 188 | CEE9974C2782A62B002FA127 /* ViewController.swift */, 189 | CEE9974E2782A62B002FA127 /* Main.storyboard */, 190 | CEE997512782A62B002FA127 /* Assets.xcassets */, 191 | CEE997532782A62B002FA127 /* LaunchScreen.storyboard */, 192 | CEE997562782A62B002FA127 /* Info.plist */, 193 | ); 194 | path = Example; 195 | sourceTree = ""; 196 | }; 197 | /* End PBXGroup section */ 198 | 199 | /* Begin PBXHeadersBuildPhase section */ 200 | CEE9972A2782A5CC002FA127 /* Headers */ = { 201 | isa = PBXHeadersBuildPhase; 202 | buildActionMask = 2147483647; 203 | files = ( 204 | CEE997332782A5CC002FA127 /* PIPKit.h in Headers */, 205 | ); 206 | runOnlyForDeploymentPostprocessing = 0; 207 | }; 208 | /* End PBXHeadersBuildPhase section */ 209 | 210 | /* Begin PBXNativeTarget section */ 211 | CEE9972E2782A5CC002FA127 /* PIPKit */ = { 212 | isa = PBXNativeTarget; 213 | buildConfigurationList = CEE997362782A5CC002FA127 /* Build configuration list for PBXNativeTarget "PIPKit" */; 214 | buildPhases = ( 215 | CEE9972A2782A5CC002FA127 /* Headers */, 216 | CEE9972B2782A5CC002FA127 /* Sources */, 217 | CEE9972C2782A5CC002FA127 /* Frameworks */, 218 | CEE9972D2782A5CC002FA127 /* Resources */, 219 | ); 220 | buildRules = ( 221 | ); 222 | dependencies = ( 223 | ); 224 | name = PIPKit; 225 | productName = PIPKit; 226 | productReference = CEE9972F2782A5CC002FA127 /* PIPKit.framework */; 227 | productType = "com.apple.product-type.framework"; 228 | }; 229 | CEE997452782A62B002FA127 /* Example */ = { 230 | isa = PBXNativeTarget; 231 | buildConfigurationList = CEE997572782A62B002FA127 /* Build configuration list for PBXNativeTarget "Example" */; 232 | buildPhases = ( 233 | CEE997422782A62B002FA127 /* Sources */, 234 | CEE997432782A62B002FA127 /* Frameworks */, 235 | CEE997442782A62B002FA127 /* Resources */, 236 | CE96576C2782BEB500303B7F /* Embed Frameworks */, 237 | ); 238 | buildRules = ( 239 | ); 240 | dependencies = ( 241 | CE9657592782A6AB00303B7F /* PBXTargetDependency */, 242 | CE96576B2782BEB500303B7F /* PBXTargetDependency */, 243 | ); 244 | name = Example; 245 | productName = Example; 246 | productReference = CEE997462782A62B002FA127 /* Example.app */; 247 | productType = "com.apple.product-type.application"; 248 | }; 249 | /* End PBXNativeTarget section */ 250 | 251 | /* Begin PBXProject section */ 252 | CEE997262782A5CC002FA127 /* Project object */ = { 253 | isa = PBXProject; 254 | attributes = { 255 | BuildIndependentTargetsInParallel = 1; 256 | LastSwiftUpdateCheck = 1320; 257 | LastUpgradeCheck = 1320; 258 | TargetAttributes = { 259 | CEE9972E2782A5CC002FA127 = { 260 | CreatedOnToolsVersion = 13.2.1; 261 | }; 262 | CEE997452782A62B002FA127 = { 263 | CreatedOnToolsVersion = 13.2.1; 264 | }; 265 | }; 266 | }; 267 | buildConfigurationList = CEE997292782A5CC002FA127 /* Build configuration list for PBXProject "PIPKit" */; 268 | compatibilityVersion = "Xcode 13.0"; 269 | developmentRegion = en; 270 | hasScannedForEncodings = 0; 271 | knownRegions = ( 272 | en, 273 | Base, 274 | ); 275 | mainGroup = CEE997252782A5CC002FA127; 276 | productRefGroup = CEE997302782A5CC002FA127 /* Products */; 277 | projectDirPath = ""; 278 | projectRoot = ""; 279 | targets = ( 280 | CEE9972E2782A5CC002FA127 /* PIPKit */, 281 | CEE997452782A62B002FA127 /* Example */, 282 | ); 283 | }; 284 | /* End PBXProject section */ 285 | 286 | /* Begin PBXResourcesBuildPhase section */ 287 | CEE9972D2782A5CC002FA127 /* Resources */ = { 288 | isa = PBXResourcesBuildPhase; 289 | buildActionMask = 2147483647; 290 | files = ( 291 | ); 292 | runOnlyForDeploymentPostprocessing = 0; 293 | }; 294 | CEE997442782A62B002FA127 /* Resources */ = { 295 | isa = PBXResourcesBuildPhase; 296 | buildActionMask = 2147483647; 297 | files = ( 298 | CEE997552782A62B002FA127 /* LaunchScreen.storyboard in Resources */, 299 | CEE997522782A62B002FA127 /* Assets.xcassets in Resources */, 300 | CEE997502782A62B002FA127 /* Main.storyboard in Resources */, 301 | ); 302 | runOnlyForDeploymentPostprocessing = 0; 303 | }; 304 | /* End PBXResourcesBuildPhase section */ 305 | 306 | /* Begin PBXSourcesBuildPhase section */ 307 | CEE9972B2782A5CC002FA127 /* Sources */ = { 308 | isa = PBXSourcesBuildPhase; 309 | buildActionMask = 2147483647; 310 | files = ( 311 | CEE9973E2782A5F1002FA127 /* PIPKitEventDispatcher.swift in Sources */, 312 | CEE9973F2782A5F1002FA127 /* UIWindow+Extension.swift in Sources */, 313 | CEE997402782A5F1002FA127 /* PIPKit.swift in Sources */, 314 | CE96575E2782BB6D00303B7F /* AVPIPKitUsable.swift in Sources */, 315 | CE9657622782BC3900303B7F /* UIView+Associated.swift in Sources */, 316 | CE9657642782BC9100303B7F /* AVPIPKitVideoProvider.swift in Sources */, 317 | CE9657662782BCAF00303B7F /* AVPIPKitVideoController.swift in Sources */, 318 | CE9657602782BBCC00303B7F /* UIViewController+Associated.swift in Sources */, 319 | CEE5C8E027899B1500BEDD03 /* AVPIPKitRenderer.swift in Sources */, 320 | CEE5C8E22789A0CE00BEDD03 /* AVPIPKitUsable+UIKit.swift in Sources */, 321 | CE21D48128AA8CF900057736 /* KeyboardObserver.swift in Sources */, 322 | CEE997412782A5F1002FA127 /* PIPUsable.swift in Sources */, 323 | ); 324 | runOnlyForDeploymentPostprocessing = 0; 325 | }; 326 | CEE997422782A62B002FA127 /* Sources */ = { 327 | isa = PBXSourcesBuildPhase; 328 | buildActionMask = 2147483647; 329 | files = ( 330 | CEE9974D2782A62B002FA127 /* ViewController.swift in Sources */, 331 | CEE997492782A62B002FA127 /* AppDelegate.swift in Sources */, 332 | ); 333 | runOnlyForDeploymentPostprocessing = 0; 334 | }; 335 | /* End PBXSourcesBuildPhase section */ 336 | 337 | /* Begin PBXTargetDependency section */ 338 | CE9657592782A6AB00303B7F /* PBXTargetDependency */ = { 339 | isa = PBXTargetDependency; 340 | target = CEE9972E2782A5CC002FA127 /* PIPKit */; 341 | targetProxy = CE9657582782A6AB00303B7F /* PBXContainerItemProxy */; 342 | }; 343 | CE96576B2782BEB500303B7F /* PBXTargetDependency */ = { 344 | isa = PBXTargetDependency; 345 | target = CEE9972E2782A5CC002FA127 /* PIPKit */; 346 | targetProxy = CE96576A2782BEB500303B7F /* PBXContainerItemProxy */; 347 | }; 348 | /* End PBXTargetDependency section */ 349 | 350 | /* Begin PBXVariantGroup section */ 351 | CEE9974E2782A62B002FA127 /* Main.storyboard */ = { 352 | isa = PBXVariantGroup; 353 | children = ( 354 | CEE9974F2782A62B002FA127 /* Base */, 355 | ); 356 | name = Main.storyboard; 357 | sourceTree = ""; 358 | }; 359 | CEE997532782A62B002FA127 /* LaunchScreen.storyboard */ = { 360 | isa = PBXVariantGroup; 361 | children = ( 362 | CEE997542782A62B002FA127 /* Base */, 363 | ); 364 | name = LaunchScreen.storyboard; 365 | sourceTree = ""; 366 | }; 367 | /* End PBXVariantGroup section */ 368 | 369 | /* Begin XCBuildConfiguration section */ 370 | CEE997342782A5CC002FA127 /* Debug */ = { 371 | isa = XCBuildConfiguration; 372 | buildSettings = { 373 | ALWAYS_SEARCH_USER_PATHS = NO; 374 | CLANG_ANALYZER_NONNULL = YES; 375 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 376 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 377 | CLANG_CXX_LIBRARY = "libc++"; 378 | CLANG_ENABLE_MODULES = YES; 379 | CLANG_ENABLE_OBJC_ARC = YES; 380 | CLANG_ENABLE_OBJC_WEAK = YES; 381 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 382 | CLANG_WARN_BOOL_CONVERSION = YES; 383 | CLANG_WARN_COMMA = YES; 384 | CLANG_WARN_CONSTANT_CONVERSION = YES; 385 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 386 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 387 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 388 | CLANG_WARN_EMPTY_BODY = YES; 389 | CLANG_WARN_ENUM_CONVERSION = YES; 390 | CLANG_WARN_INFINITE_RECURSION = YES; 391 | CLANG_WARN_INT_CONVERSION = YES; 392 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 393 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 394 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 395 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 396 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 397 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 398 | CLANG_WARN_STRICT_PROTOTYPES = YES; 399 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 400 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 401 | CLANG_WARN_UNREACHABLE_CODE = YES; 402 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 403 | COPY_PHASE_STRIP = NO; 404 | CURRENT_PROJECT_VERSION = 1; 405 | DEBUG_INFORMATION_FORMAT = dwarf; 406 | ENABLE_STRICT_OBJC_MSGSEND = YES; 407 | ENABLE_TESTABILITY = YES; 408 | GCC_C_LANGUAGE_STANDARD = gnu11; 409 | GCC_DYNAMIC_NO_PIC = NO; 410 | GCC_NO_COMMON_BLOCKS = YES; 411 | GCC_OPTIMIZATION_LEVEL = 0; 412 | GCC_PREPROCESSOR_DEFINITIONS = ( 413 | "DEBUG=1", 414 | "$(inherited)", 415 | ); 416 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 417 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 418 | GCC_WARN_UNDECLARED_SELECTOR = YES; 419 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 420 | GCC_WARN_UNUSED_FUNCTION = YES; 421 | GCC_WARN_UNUSED_VARIABLE = YES; 422 | IPHONEOS_DEPLOYMENT_TARGET = 15.2; 423 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 424 | MTL_FAST_MATH = YES; 425 | ONLY_ACTIVE_ARCH = YES; 426 | SDKROOT = iphoneos; 427 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 428 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 429 | VERSIONING_SYSTEM = "apple-generic"; 430 | VERSION_INFO_PREFIX = ""; 431 | }; 432 | name = Debug; 433 | }; 434 | CEE997352782A5CC002FA127 /* Release */ = { 435 | isa = XCBuildConfiguration; 436 | buildSettings = { 437 | ALWAYS_SEARCH_USER_PATHS = NO; 438 | CLANG_ANALYZER_NONNULL = YES; 439 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 440 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 441 | CLANG_CXX_LIBRARY = "libc++"; 442 | CLANG_ENABLE_MODULES = YES; 443 | CLANG_ENABLE_OBJC_ARC = YES; 444 | CLANG_ENABLE_OBJC_WEAK = YES; 445 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 446 | CLANG_WARN_BOOL_CONVERSION = YES; 447 | CLANG_WARN_COMMA = YES; 448 | CLANG_WARN_CONSTANT_CONVERSION = YES; 449 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 450 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 451 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 452 | CLANG_WARN_EMPTY_BODY = YES; 453 | CLANG_WARN_ENUM_CONVERSION = YES; 454 | CLANG_WARN_INFINITE_RECURSION = YES; 455 | CLANG_WARN_INT_CONVERSION = YES; 456 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 457 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 458 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 459 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 460 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 461 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 462 | CLANG_WARN_STRICT_PROTOTYPES = YES; 463 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 464 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 465 | CLANG_WARN_UNREACHABLE_CODE = YES; 466 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 467 | COPY_PHASE_STRIP = NO; 468 | CURRENT_PROJECT_VERSION = 1; 469 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 470 | ENABLE_NS_ASSERTIONS = NO; 471 | ENABLE_STRICT_OBJC_MSGSEND = YES; 472 | GCC_C_LANGUAGE_STANDARD = gnu11; 473 | GCC_NO_COMMON_BLOCKS = YES; 474 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 475 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 476 | GCC_WARN_UNDECLARED_SELECTOR = YES; 477 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 478 | GCC_WARN_UNUSED_FUNCTION = YES; 479 | GCC_WARN_UNUSED_VARIABLE = YES; 480 | IPHONEOS_DEPLOYMENT_TARGET = 15.2; 481 | MTL_ENABLE_DEBUG_INFO = NO; 482 | MTL_FAST_MATH = YES; 483 | SDKROOT = iphoneos; 484 | SWIFT_COMPILATION_MODE = wholemodule; 485 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 486 | VALIDATE_PRODUCT = YES; 487 | VERSIONING_SYSTEM = "apple-generic"; 488 | VERSION_INFO_PREFIX = ""; 489 | }; 490 | name = Release; 491 | }; 492 | CEE997372782A5CC002FA127 /* Debug */ = { 493 | isa = XCBuildConfiguration; 494 | buildSettings = { 495 | CODE_SIGN_STYLE = Automatic; 496 | CURRENT_PROJECT_VERSION = 1; 497 | DEFINES_MODULE = YES; 498 | DEVELOPMENT_TEAM = KLVLNTBFM4; 499 | DYLIB_COMPATIBILITY_VERSION = 1; 500 | DYLIB_CURRENT_VERSION = 1; 501 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 502 | GENERATE_INFOPLIST_FILE = YES; 503 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 504 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 505 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 506 | LD_RUNPATH_SEARCH_PATHS = ( 507 | "$(inherited)", 508 | "@executable_path/Frameworks", 509 | "@loader_path/Frameworks", 510 | ); 511 | MARKETING_VERSION = 1.0.7; 512 | PRODUCT_BUNDLE_IDENTIFIER = kr.kofktu.PIPKit; 513 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 514 | SKIP_INSTALL = YES; 515 | SWIFT_EMIT_LOC_STRINGS = YES; 516 | SWIFT_VERSION = 5.0; 517 | TARGETED_DEVICE_FAMILY = "1,2"; 518 | }; 519 | name = Debug; 520 | }; 521 | CEE997382782A5CC002FA127 /* Release */ = { 522 | isa = XCBuildConfiguration; 523 | buildSettings = { 524 | CODE_SIGN_STYLE = Automatic; 525 | CURRENT_PROJECT_VERSION = 1; 526 | DEFINES_MODULE = YES; 527 | DEVELOPMENT_TEAM = KLVLNTBFM4; 528 | DYLIB_COMPATIBILITY_VERSION = 1; 529 | DYLIB_CURRENT_VERSION = 1; 530 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 531 | GENERATE_INFOPLIST_FILE = YES; 532 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 533 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 534 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 535 | LD_RUNPATH_SEARCH_PATHS = ( 536 | "$(inherited)", 537 | "@executable_path/Frameworks", 538 | "@loader_path/Frameworks", 539 | ); 540 | MARKETING_VERSION = 1.0.7; 541 | PRODUCT_BUNDLE_IDENTIFIER = kr.kofktu.PIPKit; 542 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 543 | SKIP_INSTALL = YES; 544 | SWIFT_EMIT_LOC_STRINGS = YES; 545 | SWIFT_VERSION = 5.0; 546 | TARGETED_DEVICE_FAMILY = "1,2"; 547 | }; 548 | name = Release; 549 | }; 550 | CEE997582782A62B002FA127 /* Debug */ = { 551 | isa = XCBuildConfiguration; 552 | buildSettings = { 553 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 554 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 555 | CODE_SIGN_STYLE = Automatic; 556 | CURRENT_PROJECT_VERSION = 1; 557 | DEVELOPMENT_TEAM = KLVLNTBFM4; 558 | GENERATE_INFOPLIST_FILE = YES; 559 | INFOPLIST_FILE = Example/Info.plist; 560 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 561 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 562 | INFOPLIST_KEY_UIMainStoryboardFile = Main; 563 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 564 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 565 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 566 | LD_RUNPATH_SEARCH_PATHS = ( 567 | "$(inherited)", 568 | "@executable_path/Frameworks", 569 | ); 570 | MARKETING_VERSION = 1.0; 571 | PRODUCT_BUNDLE_IDENTIFIER = kr.kofktu.Example; 572 | PRODUCT_NAME = "$(TARGET_NAME)"; 573 | SWIFT_EMIT_LOC_STRINGS = YES; 574 | SWIFT_VERSION = 5.0; 575 | TARGETED_DEVICE_FAMILY = "1,2"; 576 | }; 577 | name = Debug; 578 | }; 579 | CEE997592782A62B002FA127 /* Release */ = { 580 | isa = XCBuildConfiguration; 581 | buildSettings = { 582 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 583 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 584 | CODE_SIGN_STYLE = Automatic; 585 | CURRENT_PROJECT_VERSION = 1; 586 | DEVELOPMENT_TEAM = KLVLNTBFM4; 587 | GENERATE_INFOPLIST_FILE = YES; 588 | INFOPLIST_FILE = Example/Info.plist; 589 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 590 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 591 | INFOPLIST_KEY_UIMainStoryboardFile = Main; 592 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 593 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 594 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 595 | LD_RUNPATH_SEARCH_PATHS = ( 596 | "$(inherited)", 597 | "@executable_path/Frameworks", 598 | ); 599 | MARKETING_VERSION = 1.0; 600 | PRODUCT_BUNDLE_IDENTIFIER = kr.kofktu.Example; 601 | PRODUCT_NAME = "$(TARGET_NAME)"; 602 | SWIFT_EMIT_LOC_STRINGS = YES; 603 | SWIFT_VERSION = 5.0; 604 | TARGETED_DEVICE_FAMILY = "1,2"; 605 | }; 606 | name = Release; 607 | }; 608 | /* End XCBuildConfiguration section */ 609 | 610 | /* Begin XCConfigurationList section */ 611 | CEE997292782A5CC002FA127 /* Build configuration list for PBXProject "PIPKit" */ = { 612 | isa = XCConfigurationList; 613 | buildConfigurations = ( 614 | CEE997342782A5CC002FA127 /* Debug */, 615 | CEE997352782A5CC002FA127 /* Release */, 616 | ); 617 | defaultConfigurationIsVisible = 0; 618 | defaultConfigurationName = Release; 619 | }; 620 | CEE997362782A5CC002FA127 /* Build configuration list for PBXNativeTarget "PIPKit" */ = { 621 | isa = XCConfigurationList; 622 | buildConfigurations = ( 623 | CEE997372782A5CC002FA127 /* Debug */, 624 | CEE997382782A5CC002FA127 /* Release */, 625 | ); 626 | defaultConfigurationIsVisible = 0; 627 | defaultConfigurationName = Release; 628 | }; 629 | CEE997572782A62B002FA127 /* Build configuration list for PBXNativeTarget "Example" */ = { 630 | isa = XCConfigurationList; 631 | buildConfigurations = ( 632 | CEE997582782A62B002FA127 /* Debug */, 633 | CEE997592782A62B002FA127 /* Release */, 634 | ); 635 | defaultConfigurationIsVisible = 0; 636 | defaultConfigurationName = Release; 637 | }; 638 | /* End XCConfigurationList section */ 639 | }; 640 | rootObject = CEE997262782A5CC002FA127 /* Project object */; 641 | } 642 | -------------------------------------------------------------------------------- /PIPKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /PIPKit.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /PIPKit.xcodeproj/xcshareddata/xcschemes/Example.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /PIPKit.xcodeproj/xcshareddata/xcschemes/PIPKit.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 57 | 58 | 59 | 60 | 62 | 63 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /PIPKit/Classes/AVPIPKit/AVPIPKitRenderer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AVPIPKitRenderer.swift 3 | // PIPKit 4 | // 5 | // Created by Kofktu on 2022/01/08. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | import UIKit 11 | 12 | @available(iOS 15.0, *) 13 | public protocol AVPIPKitRenderer { 14 | 15 | var policy: AVPIPKitRenderPolicy { get } 16 | var renderPublisher: AnyPublisher { get } 17 | 18 | func start() 19 | func stop() 20 | func exit() 21 | 22 | } 23 | 24 | @available(iOS 15.0, *) 25 | final class AVPIPUIKitRenderer: AVPIPKitRenderer { 26 | 27 | let policy: AVPIPKitRenderPolicy 28 | var renderPublisher: AnyPublisher { 29 | _render 30 | .filter { $0 != nil } 31 | .map { $0.unsafelyUnwrapped } 32 | .eraseToAnyPublisher() 33 | } 34 | var exitPublisher: AnyPublisher { 35 | _exit.eraseToAnyPublisher() 36 | } 37 | 38 | private var isRunning: Bool = false 39 | private weak var targetView: UIView? 40 | private var displayLink: CADisplayLink? 41 | private let _render = CurrentValueSubject(nil) 42 | private let _exit = PassthroughSubject() 43 | 44 | deinit { 45 | stop() 46 | } 47 | 48 | init(targetView: UIView, policy: AVPIPKitRenderPolicy) { 49 | self.targetView = targetView 50 | self.policy = policy 51 | } 52 | 53 | func start() { 54 | if isRunning { 55 | return 56 | } 57 | 58 | isRunning = true 59 | onRender() 60 | 61 | guard case .preferredFramesPerSecond(let preferredFramesPerSecond) = policy else { 62 | return 63 | } 64 | 65 | displayLink = CADisplayLink(target: self, selector: #selector(onRender)) 66 | displayLink?.preferredFrameRateRange = CAFrameRateRange(minimum: 1, maximum: Float(preferredFramesPerSecond), preferred: 0) 67 | displayLink?.add(to: .main, forMode: .default) 68 | } 69 | 70 | func stop() { 71 | guard isRunning else { 72 | return 73 | } 74 | 75 | displayLink?.invalidate() 76 | displayLink = nil 77 | isRunning = false 78 | } 79 | 80 | func exit() { 81 | _exit.send(()) 82 | } 83 | 84 | func render() { 85 | onRender() 86 | } 87 | 88 | // MARK: - Private 89 | @objc private func onRender() { 90 | guard let targetView = targetView else { 91 | stop() 92 | return 93 | } 94 | 95 | _render.send(targetView.uiImage) 96 | } 97 | 98 | } 99 | 100 | @available(iOS 15.0, *) 101 | private extension UIView { 102 | 103 | var uiImage: UIImage { 104 | UIGraphicsImageRenderer(bounds: bounds).image { context in 105 | layer.render(in: context.cgContext) 106 | } 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /PIPKit/Classes/AVPIPKit/AVPIPKitUsable+UIKit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AVPIPKitUsable+UIKit.swift 3 | // PIPKit 4 | // 5 | // Created by Kofktu on 2022/01/08. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | import Combine 11 | 12 | @available(iOS 15.0, *) 13 | public protocol AVPIPUIKitUsable: AVPIPKitUsable { 14 | 15 | var pipTargetView: UIView { get } 16 | var renderPolicy: AVPIPKitRenderPolicy { get } 17 | var exitPublisher: AnyPublisher { get } 18 | 19 | } 20 | 21 | @available(iOS 15.0, *) 22 | public extension AVPIPUIKitUsable { 23 | 24 | var renderPolicy: AVPIPKitRenderPolicy { 25 | .preferredFramesPerSecond(UIScreen.main.maximumFramesPerSecond) 26 | } 27 | 28 | } 29 | 30 | @available(iOS 15.0, *) 31 | public extension AVPIPUIKitUsable where Self: UIViewController { 32 | 33 | var pipTargetView: UIView { view } 34 | var renderer: AVPIPKitRenderer { 35 | setupRendererIfNeeded() 36 | return avUIKitRenderer.unsafelyUnwrapped 37 | } 38 | var exitPublisher: AnyPublisher { 39 | setupRendererIfNeeded() 40 | return avUIKitRenderer.unsafelyUnwrapped.exitPublisher 41 | } 42 | 43 | func startPictureInPicture() { 44 | setupIfNeeded() 45 | videoController?.start() 46 | } 47 | 48 | func stopPictureInPicture() { 49 | assert(videoController != nil) 50 | videoController?.stop() 51 | } 52 | 53 | // If you want to update the screen, execute the following additional code. 54 | func renderPictureInPicture() { 55 | setupRendererIfNeeded() 56 | avUIKitRenderer?.render() 57 | } 58 | 59 | // MARK: - Private 60 | private func setupRendererIfNeeded() { 61 | guard avUIKitRenderer == nil else { 62 | return 63 | } 64 | 65 | avUIKitRenderer = AVPIPUIKitRenderer(targetView: pipTargetView, policy: renderPolicy) 66 | } 67 | 68 | private func setupIfNeeded() { 69 | guard videoController == nil else { 70 | return 71 | } 72 | 73 | videoController = createVideoController() 74 | } 75 | 76 | } 77 | 78 | @available(iOS 15.0, *) 79 | public extension AVPIPUIKitUsable where Self: UIView { 80 | 81 | var pipTargetView: UIView { self } 82 | var renderer: AVPIPKitRenderer { 83 | setupRendererIfNeeded() 84 | return avUIKitRenderer.unsafelyUnwrapped 85 | } 86 | var exitPublisher: AnyPublisher { 87 | setupRendererIfNeeded() 88 | return avUIKitRenderer.unsafelyUnwrapped.exitPublisher 89 | } 90 | 91 | func startPictureInPicture() { 92 | setupIfNeeded() 93 | videoController?.start() 94 | } 95 | 96 | func stopPictureInPicture() { 97 | assert(videoController != nil) 98 | videoController?.stop() 99 | } 100 | 101 | // If you want to update the screen, execute the following additional code. 102 | func renderPictureInPicture() { 103 | setupRendererIfNeeded() 104 | avUIKitRenderer?.render() 105 | } 106 | 107 | // MARK: - Private 108 | private func setupRendererIfNeeded() { 109 | guard avUIKitRenderer == nil else { 110 | return 111 | } 112 | 113 | avUIKitRenderer = AVPIPUIKitRenderer(targetView: pipTargetView, policy: renderPolicy) 114 | } 115 | 116 | private func setupIfNeeded() { 117 | guard videoController == nil else { 118 | return 119 | } 120 | 121 | videoController = createVideoController() 122 | } 123 | 124 | } 125 | -------------------------------------------------------------------------------- /PIPKit/Classes/AVPIPKit/AVPIPKitUsable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AVPIPKitUsable.swift 3 | // PIPKit 4 | // 5 | // Created by Kofktu on 2022/01/03. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | import AVKit 11 | 12 | public extension PIPKit { 13 | 14 | static var isAVPIPKitSupported: Bool { 15 | guard #available(iOS 15.0, *) else { 16 | return false 17 | } 18 | 19 | return AVPictureInPictureController.isPictureInPictureSupported() 20 | } 21 | 22 | } 23 | 24 | @available(iOS 15.0, *) 25 | public enum AVPIPKitRenderPolicy { 26 | 27 | case once 28 | case preferredFramesPerSecond(Int) 29 | 30 | } 31 | 32 | @available(iOS 15.0, *) 33 | extension AVPIPKitRenderPolicy { 34 | 35 | var preferredFramesPerSecond: Int { 36 | switch self { 37 | case .once: 38 | return 1 39 | case .preferredFramesPerSecond(let preferredFramesPerSecond): 40 | return preferredFramesPerSecond 41 | } 42 | } 43 | 44 | } 45 | 46 | @available(iOS 15.0, *) 47 | public protocol AVPIPKitUsable { 48 | 49 | var renderer: AVPIPKitRenderer { get } 50 | 51 | func startPictureInPicture() 52 | func stopPictureInPicture() 53 | 54 | } 55 | 56 | @available(iOS 15.0, *) 57 | public extension AVPIPKitUsable { 58 | 59 | var isAVKitPIPSupported: Bool { 60 | PIPKit.isAVPIPKitSupported 61 | } 62 | 63 | } 64 | 65 | -------------------------------------------------------------------------------- /PIPKit/Classes/AVPIPKit/AVPIPKitVideoController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AVPIPKitVideoController.swift 3 | // PIPKit 4 | // 5 | // Created by Kofktu on 2022/01/03. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | import AVKit 11 | import AVFoundation 12 | import Combine 13 | 14 | @available(iOS 15.0, *) 15 | final class AVPIPKitVideoController: NSObject { 16 | 17 | var isPIPSupported: Bool { 18 | AVPictureInPictureController.isPictureInPictureSupported() 19 | } 20 | 21 | private let videoProvider: PIPVideoProvider 22 | private var pipController: AVPictureInPictureController? 23 | private var pipPossibleObservation: NSKeyValueObservation? 24 | 25 | private var audioSessionCategory: AVAudioSession.Category? 26 | private var audioSessionMode: AVAudioSession.Mode? 27 | private var audioSessionCategoryOptions: AVAudioSession.CategoryOptions? 28 | 29 | deinit { 30 | pipPossibleObservation?.invalidate() 31 | } 32 | 33 | init(renderer: AVPIPKitRenderer) { 34 | videoProvider = PIPVideoProvider(renderer: renderer) 35 | super.init() 36 | } 37 | 38 | func start() { 39 | dispatchPrecondition(condition: .onQueue(.main)) 40 | 41 | if audioSessionCategory == nil { 42 | cachedAndPrepareAudioSession() 43 | } 44 | 45 | if pipController == nil { 46 | prepareToPIPController() 47 | } 48 | 49 | if videoProvider.isRunning == false { 50 | videoProvider.start() 51 | } 52 | 53 | guard let pipController = pipController, pipController.isPictureInPicturePossible, 54 | pipController.isPictureInPictureActive == false else { 55 | return 56 | } 57 | 58 | pipController.startPictureInPicture() 59 | } 60 | 61 | func stop() { 62 | dispatchPrecondition(condition: .onQueue(.main)) 63 | 64 | defer { 65 | restoreAudioSession() 66 | } 67 | 68 | guard videoProvider.isRunning else { 69 | return 70 | } 71 | 72 | videoProvider.stop() 73 | pipController?.stopPictureInPicture() 74 | } 75 | 76 | // MARK: - Private 77 | private func cachedAndPrepareAudioSession() { 78 | guard AVAudioSession.sharedInstance().category != .playback else { 79 | return 80 | } 81 | 82 | audioSessionCategory = AVAudioSession.sharedInstance().category 83 | audioSessionMode = AVAudioSession.sharedInstance().mode 84 | audioSessionCategoryOptions = AVAudioSession.sharedInstance().categoryOptions 85 | 86 | do { 87 | try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default) 88 | try AVAudioSession.sharedInstance().setActive(true) 89 | } catch {} 90 | } 91 | 92 | private func restoreAudioSession() { 93 | defer { 94 | self.audioSessionCategory = nil 95 | self.audioSessionMode = nil 96 | self.audioSessionCategoryOptions = nil 97 | } 98 | 99 | guard let category = audioSessionCategory, 100 | let mode = audioSessionMode, 101 | let categoryOptions = audioSessionCategoryOptions else { 102 | return 103 | } 104 | 105 | do { 106 | try AVAudioSession.sharedInstance().setCategory(category, mode: mode, options: categoryOptions) 107 | try AVAudioSession.sharedInstance().setActive(true) 108 | } catch {} 109 | } 110 | 111 | private func prepareToPIPController() { 112 | guard isPIPSupported else { 113 | assertionFailure("not support PIP") 114 | return 115 | } 116 | 117 | pipController = AVPictureInPictureController( 118 | contentSource: .init( 119 | sampleBufferDisplayLayer: videoProvider.bufferDisplayLayer, 120 | playbackDelegate: self 121 | ) 122 | ) 123 | pipController?.delegate = self 124 | pipPossibleObservation = pipController?.observe( 125 | \AVPictureInPictureController.isPictureInPicturePossible, 126 | options: [.initial, .new], 127 | changeHandler: { [weak self] _, changed in 128 | DispatchQueue.main.async { 129 | if changed.newValue == true { 130 | self?.start() 131 | } 132 | } 133 | }) 134 | } 135 | 136 | private func exitPIPController() { 137 | pipPossibleObservation?.invalidate() 138 | pipController = nil 139 | videoProvider.renderer.exit() 140 | } 141 | 142 | } 143 | 144 | @available(iOS 15.0, *) 145 | extension AVPIPKitVideoController: AVPictureInPictureControllerDelegate { 146 | 147 | func pictureInPictureControllerWillStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { 148 | stop() 149 | } 150 | 151 | func pictureInPictureControllerDidStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { 152 | exitPIPController() 153 | } 154 | 155 | } 156 | 157 | @available(iOS 15.0, *) 158 | extension AVPIPKitVideoController: AVPictureInPictureSampleBufferPlaybackDelegate { 159 | 160 | func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, setPlaying playing: Bool) {} 161 | func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, didTransitionToRenderSize newRenderSize: CMVideoDimensions) {} 162 | 163 | func pictureInPictureControllerTimeRangeForPlayback(_ pictureInPictureController: AVPictureInPictureController) -> CMTimeRange { 164 | CMTimeRange(start: .negativeInfinity, duration: .positiveInfinity) 165 | } 166 | func pictureInPictureControllerIsPlaybackPaused(_ pictureInPictureController: AVPictureInPictureController) -> Bool { 167 | false 168 | } 169 | func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, skipByInterval skipInterval: CMTime, completion completionHandler: @escaping () -> Void) { 170 | completionHandler() 171 | } 172 | 173 | } 174 | 175 | -------------------------------------------------------------------------------- /PIPKit/Classes/AVPIPKit/AVPIPKitVideoProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AVPIPKitVideoProvider.swift 3 | // PIPKit 4 | // 5 | // Created by Kofktu on 2022/01/03. 6 | // 7 | 8 | import Foundation 9 | import QuartzCore 10 | import UIKit 11 | import AVKit 12 | import Combine 13 | 14 | @available(iOS 15.0, *) 15 | extension AVPIPKitUsable { 16 | 17 | func createVideoController() -> AVPIPKitVideoController { 18 | AVPIPKitVideoController(renderer: renderer) 19 | } 20 | 21 | } 22 | 23 | @available(iOS 15.0, *) 24 | final class PIPVideoProvider { 25 | 26 | private(set) var isRunning: Bool = false 27 | private(set) var bufferDisplayLayer = AVSampleBufferDisplayLayer() 28 | private(set) var renderer: AVPIPKitRenderer 29 | 30 | private let pipContainerView = UIView() 31 | private var cancellables = Set() 32 | 33 | deinit { 34 | stop() 35 | } 36 | 37 | init(renderer: AVPIPKitRenderer) { 38 | self.renderer = renderer 39 | } 40 | 41 | func start() { 42 | if isRunning { 43 | return 44 | } 45 | 46 | isRunning = true 47 | 48 | if let window = UIApplication.shared._keyWindow { 49 | pipContainerView.backgroundColor = .clear 50 | pipContainerView.alpha = 0.0 51 | window.addSubview(pipContainerView) 52 | window.sendSubviewToBack(pipContainerView) 53 | bufferDisplayLayer.backgroundColor = UIColor.clear.cgColor 54 | bufferDisplayLayer.videoGravity = .resizeAspect 55 | pipContainerView.layer.addSublayer(bufferDisplayLayer) 56 | } 57 | 58 | let preferredFramesPerSecond = renderer.policy.preferredFramesPerSecond 59 | let renderPublisher = renderer.renderPublisher 60 | .receive(on: DispatchQueue.main) 61 | .share() 62 | 63 | renderPublisher 64 | .map { $0.size } 65 | .removeDuplicates() 66 | .map { CGRect(origin: .zero, size: $0) } 67 | .sink(receiveValue: { [weak self] bounds in 68 | self?.pipContainerView.frame = bounds 69 | self?.bufferDisplayLayer.frame = bounds 70 | }) 71 | .store(in: &cancellables) 72 | 73 | renderPublisher 74 | .map { $0.cmSampleBuffer(preferredFramesPerSecond: preferredFramesPerSecond) } 75 | .filter { $0 != nil } 76 | .map { $0.unsafelyUnwrapped } 77 | .sink(receiveValue: { [weak self] buffer in 78 | if self?.bufferDisplayLayer.status == .failed { 79 | self?.bufferDisplayLayer.flush() 80 | } 81 | 82 | self?.bufferDisplayLayer.enqueue(buffer) 83 | }) 84 | .store(in: &cancellables) 85 | 86 | renderer.start() 87 | } 88 | 89 | func stop() { 90 | guard isRunning else { 91 | return 92 | } 93 | 94 | pipContainerView.removeFromSuperview() 95 | bufferDisplayLayer.removeFromSuperlayer() 96 | renderer.stop() 97 | isRunning = false 98 | } 99 | 100 | } 101 | 102 | private extension UIImage { 103 | 104 | func cmSampleBuffer(preferredFramesPerSecond: Int) -> CMSampleBuffer? { 105 | guard let jpegData = jpegData(compressionQuality: 1.0), 106 | let cgImage = cgImage else { 107 | return nil 108 | } 109 | 110 | let rawPixelSize = CGSize(width: cgImage.width, height: cgImage.height) 111 | var format: CMFormatDescription? 112 | 113 | CMVideoFormatDescriptionCreate( 114 | allocator: kCFAllocatorDefault, 115 | codecType: kCMVideoCodecType_JPEG, 116 | width: Int32(rawPixelSize.width), 117 | height: Int32(rawPixelSize.height), 118 | extensions: nil, 119 | formatDescriptionOut: &format 120 | ) 121 | 122 | guard let cmBlockBuffer = jpegData.toCMBlockBuffer() else { 123 | return nil 124 | } 125 | 126 | var size = jpegData.count 127 | var sampleBuffer: CMSampleBuffer? 128 | let presentationTimeStamp = CMTime(seconds: CACurrentMediaTime(), preferredTimescale: CMTimeScale(preferredFramesPerSecond)) 129 | let duration = CMTime(value: 1, timescale: CMTimeScale(preferredFramesPerSecond)) 130 | 131 | var timingInfo = CMSampleTimingInfo( 132 | duration: duration, 133 | presentationTimeStamp: presentationTimeStamp, 134 | decodeTimeStamp: .invalid 135 | ) 136 | 137 | CMSampleBufferCreateReady( 138 | allocator: kCFAllocatorDefault, 139 | dataBuffer: cmBlockBuffer, 140 | formatDescription: format, 141 | sampleCount: 1, 142 | sampleTimingEntryCount: 1, 143 | sampleTimingArray: &timingInfo, 144 | sampleSizeEntryCount: 1, 145 | sampleSizeArray: &size, 146 | sampleBufferOut: &sampleBuffer 147 | ) 148 | 149 | if sampleBuffer == nil { 150 | assertionFailure("SampleBuffer is null") 151 | } 152 | 153 | return sampleBuffer 154 | } 155 | 156 | } 157 | 158 | private func freeBlock(_ refCon: UnsafeMutableRawPointer?, doomedMemoryBlock: UnsafeMutableRawPointer, sizeInBytes: Int) -> Void { 159 | let unmanagedData = Unmanaged.fromOpaque(refCon!) 160 | unmanagedData.release() 161 | } 162 | 163 | private extension Data { 164 | 165 | func toCMBlockBuffer() -> CMBlockBuffer? { 166 | let data = NSMutableData(data: self) 167 | var source = CMBlockBufferCustomBlockSource() 168 | source.refCon = Unmanaged.passRetained(data).toOpaque() 169 | source.FreeBlock = freeBlock 170 | 171 | var blockBuffer: CMBlockBuffer? 172 | let result = CMBlockBufferCreateWithMemoryBlock( 173 | allocator: kCFAllocatorDefault, 174 | memoryBlock: data.mutableBytes, 175 | blockLength: data.length, 176 | blockAllocator: kCFAllocatorNull, 177 | customBlockSource: &source, 178 | offsetToData: 0, 179 | dataLength: data.length, 180 | flags: 0, 181 | blockBufferOut: &blockBuffer 182 | ) 183 | 184 | if OSStatus(result) != kCMBlockBufferNoErr { 185 | return nil 186 | } 187 | 188 | guard let buffer = blockBuffer else { 189 | return nil 190 | } 191 | 192 | assert(CMBlockBufferGetDataLength(buffer) == data.length) 193 | return buffer 194 | } 195 | 196 | } 197 | 198 | -------------------------------------------------------------------------------- /PIPKit/Classes/Extension/UIView+Associated.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+Associated.swift 3 | // PIPKit 4 | // 5 | // Created by Kofktu on 2022/01/03. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | extension UIView { 12 | 13 | enum AssociatedKeys { 14 | static var avUIKitRenderer: Void? 15 | static var pipVideoController: Void? 16 | } 17 | 18 | @available(iOS 15.0, *) 19 | var avUIKitRenderer: AVPIPUIKitRenderer? { 20 | get { objc_getAssociatedObject(self, &AssociatedKeys.avUIKitRenderer) as? AVPIPUIKitRenderer } 21 | set { objc_setAssociatedObject(self, &AssociatedKeys.avUIKitRenderer, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } 22 | } 23 | 24 | @available(iOS 15.0, *) 25 | var videoController: AVPIPKitVideoController? { 26 | get { objc_getAssociatedObject(self, &AssociatedKeys.pipVideoController) as? AVPIPKitVideoController } 27 | set { objc_setAssociatedObject(self, &AssociatedKeys.pipVideoController, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /PIPKit/Classes/Extension/UIViewController+Associated.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewController+Associated.swift 3 | // PIPKit 4 | // 5 | // Created by Kofktu on 2022/01/03. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | extension UIViewController { 12 | 13 | enum AssociatedKeys { 14 | static var pipEventDispatcher: Void? 15 | static var avUIKitRenderer: Void? 16 | static var pipVideoController: Void? 17 | } 18 | 19 | var pipEventDispatcher: PIPKitEventDispatcher? { 20 | get { objc_getAssociatedObject(self, &AssociatedKeys.pipEventDispatcher) as? PIPKitEventDispatcher } 21 | set { 22 | objc_setAssociatedObject( 23 | self, 24 | &AssociatedKeys.pipEventDispatcher, 25 | newValue, 26 | .OBJC_ASSOCIATION_RETAIN_NONATOMIC 27 | ) 28 | } 29 | } 30 | 31 | @available(iOS 15.0, *) 32 | var avUIKitRenderer: AVPIPUIKitRenderer? { 33 | get { objc_getAssociatedObject(self, &AssociatedKeys.avUIKitRenderer) as? AVPIPUIKitRenderer } 34 | set { 35 | objc_setAssociatedObject( 36 | self, 37 | &AssociatedKeys.avUIKitRenderer, 38 | newValue, 39 | .OBJC_ASSOCIATION_RETAIN_NONATOMIC 40 | ) 41 | } 42 | } 43 | 44 | @available(iOS 15.0, *) 45 | var videoController: AVPIPKitVideoController? { 46 | get { objc_getAssociatedObject(self, &AssociatedKeys.pipVideoController) as? AVPIPKitVideoController } 47 | set { 48 | objc_setAssociatedObject( 49 | self, 50 | &AssociatedKeys.pipVideoController, 51 | newValue, 52 | .OBJC_ASSOCIATION_RETAIN_NONATOMIC 53 | ) 54 | } 55 | } 56 | 57 | } 58 | 59 | -------------------------------------------------------------------------------- /PIPKit/Classes/Extension/UIWindow+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIWindow+Extension.swift 3 | // PIPKit 4 | // 5 | // Created by Kofktu on 2022/01/03. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UIApplication { 11 | 12 | var _keyWindow: UIWindow? { 13 | var sceneWindows: [UIWindow]? 14 | 15 | if #available(iOS 13.0, *) { 16 | sceneWindows = connectedScenes 17 | .filter { $0.activationState == .foregroundActive } 18 | .first { $0 is UIWindowScene } 19 | .flatMap { $0 as? UIWindowScene }?.windows 20 | } 21 | 22 | let windows = sceneWindows ?? self.windows 23 | return windows.first(where: \.isKeyWindow) ?? windows.first 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /PIPKit/Classes/PIPKit/KeyboardObserver.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyboardObserver.swift 3 | // PIPKit 4 | // 5 | // Created by Kofktu on 2022/08/15. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | protocol KeyboardObserverDelegate: AnyObject { 12 | 13 | func keyboard(_ observer: KeyboardObserver, changed visibleHeight: CGFloat) 14 | 15 | } 16 | 17 | final class KeyboardObserver: NSObject { 18 | 19 | var isVisible: Bool { 20 | visibleHeight > 0.0 21 | } 22 | var keyboardHeight: CGFloat { 23 | var height = keyboardFrame.height 24 | 25 | if isAdjustSafeAreaInset, #available(iOS 11.0, *) { 26 | if let window = UIApplication.shared._keyWindow, height > 0 { 27 | height -= window.safeAreaInsets.bottom 28 | } 29 | } 30 | 31 | return max(0, height) 32 | } 33 | 34 | private(set) var keyboardFrame = CGRect.zero { 35 | didSet { 36 | var height: CGFloat = max(0.0, screenHeight - keyboardFrame.minY) 37 | 38 | if isAdjustSafeAreaInset, #available(iOS 11.0, *) { 39 | if let window = UIApplication.shared._keyWindow, height > 0 { 40 | height -= window.safeAreaInsets.bottom 41 | } 42 | } 43 | 44 | visibleHeight = height 45 | } 46 | } 47 | private(set) var visibleHeight: CGFloat = 0.0 { 48 | didSet { 49 | if oldValue != visibleHeight { 50 | delegate?.keyboard(self, changed: visibleHeight) 51 | } 52 | } 53 | } 54 | 55 | private weak var delegate: KeyboardObserverDelegate? 56 | private let isAdjustSafeAreaInset: Bool 57 | private var isObserving: Bool = false 58 | private var observations: [NSObjectProtocol] = [] 59 | private var panGesture: UIPanGestureRecognizer? 60 | 61 | private let keyboardWillChangeFrame: Notification.Name = { 62 | #if swift(>=4.2) 63 | return UIResponder.keyboardWillChangeFrameNotification 64 | #else 65 | return NSNotification.Name.UIKeyboardWillChangeFrame 66 | #endif 67 | }() 68 | 69 | private let keyboardWillHide: Notification.Name = { 70 | #if swift(>=4.2) 71 | return UIResponder.keyboardWillHideNotification 72 | #else 73 | return NSNotification.Name.UIKeyboardWillHide 74 | #endif 75 | }() 76 | 77 | init(delegate: KeyboardObserverDelegate, 78 | adjustSafeAreaInset: Bool) { 79 | self.delegate = delegate 80 | self.isAdjustSafeAreaInset = adjustSafeAreaInset 81 | super.init() 82 | } 83 | 84 | func activate() { 85 | guard isObserving == false else { 86 | return 87 | } 88 | 89 | isObserving = true 90 | 91 | observations.append( 92 | NotificationCenter.default.addObserver(forName: keyboardWillChangeFrame, 93 | object: nil, 94 | queue: nil, 95 | using: { [weak self] noti in 96 | self?.onKeyboardHandler(noti) 97 | }) 98 | ) 99 | 100 | observations.append( 101 | NotificationCenter.default.addObserver(forName: keyboardWillHide, 102 | object: nil, 103 | queue: nil, 104 | using: { [weak self] noti in 105 | self?.onKeyboardHandler(noti) 106 | }) 107 | ) 108 | 109 | panGesture = UIPanGestureRecognizer(target: self, action: #selector(onPan(_:))) 110 | panGesture?.delegate = self 111 | UIApplication.shared._keyWindow?.addGestureRecognizer(panGesture.unsafelyUnwrapped) 112 | } 113 | 114 | func deactivate() { 115 | guard isObserving else { 116 | return 117 | } 118 | 119 | isObserving = false 120 | observations.removeAll() 121 | 122 | panGesture.flatMap { 123 | panGesture?.view?.removeGestureRecognizer($0) 124 | } 125 | panGesture = nil 126 | } 127 | 128 | // MARK: - Action 129 | private func onKeyboardHandler(_ noti: Notification) { 130 | #if swift(>=4.2) 131 | let keyboardFrameEndKey = UIResponder.keyboardFrameEndUserInfoKey 132 | #else 133 | let keyboardFrameEndKey = UIKeyboardFrameEndUserInfoKey 134 | #endif 135 | 136 | guard let rect = (noti.userInfo?[keyboardFrameEndKey] as? NSValue)?.cgRectValue else { 137 | return 138 | } 139 | 140 | var isLocal: Bool = true 141 | 142 | if #available(iOS 9.0, *) { 143 | #if swift(>=4.2) 144 | let isLocalKey = UIResponder.keyboardIsLocalUserInfoKey 145 | #else 146 | let isLocalKey = UIKeyboardIsLocalUserInfoKey 147 | #endif 148 | 149 | (noti.userInfo?[isLocalKey] as? Bool).flatMap { 150 | isLocal = $0 151 | } 152 | } 153 | 154 | guard isLocal else { 155 | return 156 | } 157 | 158 | var newFrame = rect 159 | 160 | switch noti.name { 161 | case keyboardWillChangeFrame: 162 | if rect.origin.y < 0 { 163 | newFrame.origin.y = screenHeight - newFrame.height 164 | } 165 | case keyboardWillHide: 166 | if rect.minY < 0.0 { 167 | newFrame.origin.y = screenHeight 168 | } 169 | default: 170 | break 171 | } 172 | 173 | keyboardFrame = newFrame 174 | } 175 | 176 | @objc 177 | private func onPan(_ gesture: UIPanGestureRecognizer) { 178 | guard let window = UIApplication.shared._keyWindow, 179 | gesture.state == .changed && isVisible else { 180 | return 181 | } 182 | 183 | let origin = gesture.location(in: window) 184 | var newFrame = keyboardFrame 185 | newFrame.origin.y = max(origin.y, screenHeight - keyboardFrame.height) 186 | keyboardFrame = newFrame 187 | } 188 | 189 | } 190 | 191 | extension KeyboardObserver: UIGestureRecognizerDelegate { 192 | 193 | func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { 194 | let point = touch.location(in: gestureRecognizer.view) 195 | var view = gestureRecognizer.view?.hitTest(point, with: nil) 196 | while let candidate = view { 197 | if let scrollView = candidate as? UIScrollView, scrollView.keyboardDismissMode == .interactive { 198 | return true 199 | } 200 | view = candidate.superview 201 | } 202 | return false 203 | } 204 | 205 | public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { 206 | gestureRecognizer == panGesture 207 | } 208 | 209 | } 210 | 211 | private extension KeyboardObserver { 212 | 213 | var screenHeight: CGFloat { 214 | UIScreen.main.bounds.height 215 | } 216 | 217 | } 218 | -------------------------------------------------------------------------------- /PIPKit/Classes/PIPKit/PIPKit.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | public struct PIPShadow { 5 | public let color: UIColor 6 | public let opacity: Float 7 | public let offset: CGSize 8 | public let radius: CGFloat 9 | 10 | public init(color: UIColor, 11 | opacity: Float, 12 | offset: CGSize, 13 | radius: CGFloat) { 14 | self.color = color 15 | self.opacity = opacity 16 | self.offset = offset 17 | self.radius = radius 18 | } 19 | } 20 | 21 | public struct PIPCorner { 22 | public let radius: CGFloat 23 | public let curve: Any? 24 | 25 | public init(radius: CGFloat) { 26 | self.radius = radius 27 | self.curve = nil 28 | } 29 | 30 | @available(iOS 13.0, *) 31 | public init( 32 | radius: CGFloat, 33 | curve: CALayerCornerCurve? = nil 34 | ) { 35 | self.radius = radius 36 | self.curve = curve 37 | } 38 | 39 | func apply(view: UIView) { 40 | view.clipsToBounds = radius > .zero 41 | view.layer.cornerRadius = radius 42 | 43 | guard 44 | #available(iOS 13.0, *), 45 | let curve = curve as? CALayerCornerCurve 46 | else { 47 | return 48 | } 49 | 50 | view.layer.cornerCurve = curve 51 | } 52 | 53 | } 54 | 55 | public enum PIPState { 56 | case pip 57 | case full 58 | } 59 | 60 | public enum PIPPosition { 61 | case topLeft 62 | case middleLeft 63 | case bottomLeft 64 | case topRight 65 | case middleRight 66 | case bottomRight 67 | } 68 | 69 | enum _PIPState { 70 | case none 71 | case pip 72 | case full 73 | case exit 74 | } 75 | 76 | public typealias PIPKitViewController = (UIViewController & PIPUsable) 77 | 78 | public final class PIPKit { 79 | 80 | static public var isActive: Bool { return rootViewController != nil } 81 | static public var isPIP: Bool { return state == .pip } 82 | static public var visibleViewController: PIPKitViewController? { return rootViewController } 83 | 84 | static internal var state: _PIPState = .none 85 | static private var rootViewController: PIPKitViewController? 86 | static private var pipWindow: UIWindow? 87 | 88 | public class func show(with viewController: PIPKitViewController, completion: (() -> Void)? = nil) { 89 | guard !isActive else { 90 | dismiss(animated: false) { 91 | PIPKit.show(with: viewController) 92 | } 93 | return 94 | } 95 | 96 | let newWindow = PIPKitWindow() 97 | newWindow.backgroundColor = .clear 98 | newWindow.rootViewController = viewController 99 | newWindow.windowLevel = .alert 100 | newWindow.makeKeyAndVisible() 101 | 102 | pipWindow = newWindow 103 | rootViewController = viewController 104 | state = (viewController.initialState == .pip) ? .pip : .full 105 | 106 | viewController.view.alpha = 0.0 107 | viewController.setupEventDispatcher() 108 | 109 | UIView.animate(withDuration: 0.25, animations: { 110 | PIPKit.rootViewController?.view.alpha = 1.0 111 | }) { (_) in 112 | completion?() 113 | } 114 | } 115 | 116 | public class func dismiss(animated: Bool, completion: (() -> Void)? = nil) { 117 | state = .exit 118 | rootViewController?.pipDismiss(animated: animated, completion: { 119 | PIPKit.reset() 120 | completion?() 121 | }) 122 | } 123 | 124 | // MARK: - Internal 125 | class func startPIPMode() { 126 | guard let rootViewController = rootViewController else { 127 | return 128 | } 129 | 130 | // PIP 131 | state = .pip 132 | rootViewController.pipEventDispatcher?.enterPIP() 133 | } 134 | 135 | class func stopPIPMode() { 136 | guard let rootViewController = rootViewController else { 137 | return 138 | } 139 | 140 | // fullScreen 141 | state = .full 142 | rootViewController.pipEventDispatcher?.enterFullScreen() 143 | } 144 | 145 | // MARK: - Private 146 | private static func reset() { 147 | PIPKit.state = .none 148 | PIPKit.pipWindow = nil 149 | PIPKit.rootViewController = nil 150 | UIApplication.shared._keyWindow?.makeKeyAndVisible() 151 | } 152 | 153 | } 154 | 155 | final private class PIPKitWindow: UIWindow { 156 | 157 | override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { 158 | guard let rootViewController = rootViewController else { 159 | return super.hitTest(point, with: event) 160 | } 161 | 162 | return rootViewController.view.frame.contains(point) ? super.hitTest(point, with: event) : nil 163 | } 164 | 165 | } 166 | -------------------------------------------------------------------------------- /PIPKit/Classes/PIPKit/PIPKitEventDispatcher.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PIPKitEventDispatcher.swift 3 | // PIPKit 4 | // 5 | // Created by Taeun Kim on 07/12/2018. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | final class PIPKitEventDispatcher { 12 | 13 | var pipPosition: PIPPosition 14 | 15 | private var window: UIWindow? { 16 | rootViewController?.view.window 17 | } 18 | private weak var rootViewController: PIPKitViewController? 19 | private lazy var transitionGesture: UIPanGestureRecognizer = { 20 | UIPanGestureRecognizer(target: self, action: #selector(onTransition(_:))) 21 | }() 22 | private lazy var keyboardObserver: KeyboardObserver = { 23 | KeyboardObserver(delegate: self, adjustSafeAreaInset: true) 24 | }() 25 | private var isMoveFromKeyboard: Bool = false 26 | 27 | private var startOffset: CGPoint = .zero 28 | private var deviceNotificationObserver: NSObjectProtocol? 29 | private var windowSubviewsObservation: NSKeyValueObservation? 30 | 31 | deinit { 32 | keyboardObserver.deactivate() 33 | windowSubviewsObservation?.invalidate() 34 | deviceNotificationObserver.flatMap { 35 | NotificationCenter.default.removeObserver($0) 36 | } 37 | } 38 | 39 | init(rootViewController: PIPKitViewController) { 40 | self.rootViewController = rootViewController 41 | self.pipPosition = rootViewController.initialPosition 42 | 43 | commonInit() 44 | updateFrame() 45 | 46 | switch rootViewController.initialState { 47 | case .full: 48 | didEnterFullScreen() 49 | case .pip: 50 | didEnterPIP() 51 | } 52 | } 53 | 54 | func enterFullScreen() { 55 | UIView.animate(withDuration: 0.25, animations: { [weak self] in 56 | self?.updateFrame() 57 | }) { [weak self] (_) in 58 | self?.didEnterFullScreen() 59 | } 60 | } 61 | 62 | func enterPIP() { 63 | UIView.animate(withDuration: 0.25, animations: { [weak self] in 64 | self?.updateFrame() 65 | }) { [weak self] (_) in 66 | self?.didEnterPIP() 67 | } 68 | } 69 | 70 | func updateFrame() { 71 | guard let window = window, 72 | let rootViewController = rootViewController else { 73 | return 74 | } 75 | 76 | switch PIPKit.state { 77 | case .full: 78 | rootViewController.view.frame = window.bounds 79 | case .pip: 80 | updatePIPFrame() 81 | default: 82 | break 83 | } 84 | 85 | rootViewController.view.setNeedsLayout() 86 | rootViewController.view.layoutIfNeeded() 87 | } 88 | 89 | 90 | // MARK: - Private 91 | private func commonInit() { 92 | rootViewController?.view.addGestureRecognizer(transitionGesture) 93 | 94 | if let pipShadow = rootViewController?.pipShadow { 95 | rootViewController?.view.layer.shadowColor = pipShadow.color.cgColor 96 | rootViewController?.view.layer.shadowOpacity = pipShadow.opacity 97 | rootViewController?.view.layer.shadowOffset = pipShadow.offset 98 | rootViewController?.view.layer.shadowRadius = pipShadow.radius 99 | } 100 | 101 | if let pipCorner = rootViewController?.pipCorner { 102 | rootViewController.flatMap { pipCorner.apply(view: $0.view) } 103 | } 104 | 105 | deviceNotificationObserver = NotificationCenter.default.addObserver(forName: UIDevice.orientationDidChangeNotification, 106 | object: nil, 107 | queue: nil) { [weak self] (noti) in 108 | UIView.animate(withDuration: 0.15, animations: { 109 | self?.updateFrame() 110 | }, completion:nil) 111 | } 112 | 113 | windowSubviewsObservation = window?.observe(\.subviews, 114 | options: [.initial, .new], 115 | changeHandler: { [weak self] window, _ in 116 | guard let rootViewController = self?.rootViewController else { 117 | return 118 | } 119 | 120 | window.bringSubviewToFront(rootViewController.view) 121 | }) 122 | } 123 | 124 | private func didEnterFullScreen() { 125 | transitionGesture.isEnabled = false 126 | rootViewController?.didChangedState(.full) 127 | keyboardObserver.deactivate() 128 | } 129 | 130 | private func didEnterPIP() { 131 | transitionGesture.isEnabled = true 132 | rootViewController?.didChangedState(.pip) 133 | keyboardObserver.activate() 134 | } 135 | 136 | private func updatePIPFrame() { 137 | guard let window = window, 138 | let rootViewController = rootViewController else { 139 | return 140 | } 141 | 142 | var origin = CGPoint.zero 143 | let pipSize = rootViewController.pipSize 144 | let pipEdgeInsets = rootViewController.pipEdgeInsets 145 | var edgeInsets = UIEdgeInsets.zero 146 | 147 | if #available(iOS 11.0, *) { 148 | if rootViewController.insetsPIPFromSafeArea { 149 | edgeInsets = window.safeAreaInsets 150 | } 151 | } 152 | 153 | switch pipPosition { 154 | case .topLeft: 155 | origin.x = edgeInsets.left + pipEdgeInsets.left 156 | origin.y = edgeInsets.top + pipEdgeInsets.top 157 | case .middleLeft: 158 | origin.x = edgeInsets.left + pipEdgeInsets.left 159 | let vh = (window.frame.height - (edgeInsets.top + edgeInsets.bottom)) / 3.0 160 | origin.y = edgeInsets.top + (vh * 2.0) - ((vh + pipSize.height) / 2.0) 161 | case .bottomLeft: 162 | origin.x = edgeInsets.left + pipEdgeInsets.left 163 | origin.y = window.frame.height - edgeInsets.bottom - pipEdgeInsets.bottom - pipSize.height 164 | case .topRight: 165 | origin.x = window.frame.width - edgeInsets.right - pipEdgeInsets.right - pipSize.width 166 | origin.y = edgeInsets.top + pipEdgeInsets.top 167 | case .middleRight: 168 | origin.x = window.frame.width - edgeInsets.right - pipEdgeInsets.right - pipSize.width 169 | let vh = (window.frame.height - (edgeInsets.top + edgeInsets.bottom)) / 3.0 170 | origin.y = edgeInsets.top + (vh * 2.0) - ((vh + pipSize.height) / 2.0) 171 | case .bottomRight: 172 | origin.x = window.frame.width - edgeInsets.right - pipEdgeInsets.right - pipSize.width 173 | origin.y = window.frame.height - edgeInsets.bottom - pipEdgeInsets.bottom - pipSize.height 174 | } 175 | 176 | rootViewController.view.frame = CGRect(origin: origin, size: pipSize) 177 | } 178 | 179 | private func updatePIPPosition() { 180 | guard let window = window, 181 | let rootViewController = rootViewController else { 182 | return 183 | } 184 | 185 | let center = rootViewController.view.center 186 | var safeAreaInsets = UIEdgeInsets.zero 187 | 188 | if #available(iOS 11.0, *) { 189 | safeAreaInsets = window.safeAreaInsets 190 | } 191 | 192 | let vh = (window.frame.height - (safeAreaInsets.top + safeAreaInsets.bottom)) / 3.0 193 | 194 | switch center.y { 195 | case let y where y < safeAreaInsets.top + vh: 196 | pipPosition = center.x < window.frame.width / 2.0 ? .topLeft : .topRight 197 | case let y where y > window.frame.height - safeAreaInsets.bottom - vh: 198 | pipPosition = center.x < window.frame.width / 2.0 ? .bottomLeft : .bottomRight 199 | default: 200 | pipPosition = center.x < window.frame.width / 2.0 ? .middleLeft : .middleRight 201 | } 202 | 203 | rootViewController.didChangePosition(pipPosition) 204 | } 205 | 206 | private func updatePIPPositionAndMove(from keyboardEvent: Bool) { 207 | guard PIPKit.isPIP, 208 | let rootViewController = rootViewController else { 209 | return 210 | } 211 | 212 | let keyboardFrame = keyboardObserver.keyboardFrame 213 | var isNeedUpdate: Bool = false 214 | 215 | if keyboardObserver.isVisible { 216 | guard keyboardFrame.contains(rootViewController.view.frame) else { 217 | return 218 | } 219 | 220 | var frame = rootViewController.view.frame 221 | frame.origin.y -= keyboardObserver.visibleHeight 222 | rootViewController.view.frame = frame 223 | isNeedUpdate = true 224 | isMoveFromKeyboard = keyboardEvent 225 | } else if isMoveFromKeyboard { 226 | var frame = rootViewController.view.frame 227 | frame.origin.y += keyboardObserver.keyboardHeight 228 | rootViewController.view.frame = frame 229 | isNeedUpdate = true 230 | isMoveFromKeyboard = false 231 | } 232 | 233 | if isNeedUpdate { 234 | updatePIPPosition() 235 | UIView.animate(withDuration: 0.15) { [weak self] in 236 | self?.updatePIPFrame() 237 | } 238 | } 239 | } 240 | 241 | // MARK: - Action 242 | @objc 243 | private func onTransition(_ gesture: UIPanGestureRecognizer) { 244 | guard PIPKit.isPIP else { 245 | return 246 | } 247 | guard let window = window, 248 | let rootViewController = rootViewController else { 249 | return 250 | } 251 | 252 | switch gesture.state { 253 | case .began: 254 | isMoveFromKeyboard = false 255 | startOffset = rootViewController.view.center 256 | case .changed: 257 | let transition = gesture.translation(in: window) 258 | let pipSize = rootViewController.pipSize 259 | let pipEdgeInsets = rootViewController.pipEdgeInsets 260 | var edgeInsets = UIEdgeInsets.zero 261 | 262 | if #available(iOS 11.0, *) { 263 | if rootViewController.insetsPIPFromSafeArea { 264 | edgeInsets = window.safeAreaInsets 265 | } 266 | } 267 | 268 | var offset = startOffset 269 | offset.x += transition.x 270 | offset.y += transition.y 271 | offset.x = max(edgeInsets.left + pipEdgeInsets.left + (pipSize.width / 2.0), 272 | min(offset.x, 273 | (window.frame.width - edgeInsets.right - pipEdgeInsets.right) - (pipSize.width / 2.0))) 274 | offset.y = max(edgeInsets.top + pipEdgeInsets.top + (pipSize.height / 2.0), 275 | min(offset.y, 276 | (window.frame.height - (edgeInsets.bottom) - pipEdgeInsets.bottom) - (pipSize.height / 2.0))) 277 | 278 | rootViewController.view.center = offset 279 | case .ended: 280 | updatePIPPositionAndMove(from: false) 281 | default: 282 | break 283 | } 284 | } 285 | 286 | } 287 | 288 | extension PIPKitEventDispatcher: KeyboardObserverDelegate { 289 | 290 | func keyboard(_ observer: KeyboardObserver, changed visibleHeight: CGFloat) { 291 | updatePIPPositionAndMove(from: true) 292 | } 293 | 294 | } 295 | 296 | extension PIPUsable where Self: UIViewController { 297 | 298 | func setupEventDispatcher() { 299 | pipEventDispatcher = PIPKitEventDispatcher(rootViewController: self) 300 | } 301 | 302 | } 303 | -------------------------------------------------------------------------------- /PIPKit/Classes/PIPKit/PIPUsable.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | public protocol PIPUsable { 5 | var initialState: PIPState { get } 6 | var initialPosition: PIPPosition { get } 7 | var insetsPIPFromSafeArea: Bool { get } 8 | var pipEdgeInsets: UIEdgeInsets { get } 9 | var pipSize: CGSize { get } 10 | var pipShadow: PIPShadow? { get } 11 | var pipCorner: PIPCorner? { get } 12 | func didChangedState(_ state: PIPState) 13 | func didChangePosition(_ position: PIPPosition) 14 | } 15 | 16 | public extension PIPUsable { 17 | var initialState: PIPState { return .pip } 18 | var initialPosition: PIPPosition { return .bottomRight } 19 | var insetsPIPFromSafeArea: Bool { return true } 20 | var pipEdgeInsets: UIEdgeInsets { return UIEdgeInsets(top: 15, left: 15, bottom: 15, right: 15) } 21 | var pipSize: CGSize { return CGSize(width: 200.0, height: (200.0 * 9.0) / 16.0) } 22 | var pipShadow: PIPShadow? { return PIPShadow(color: .black, opacity: 0.3, offset: CGSize(width: 0, height: 8), radius: 10) } 23 | var pipCorner: PIPCorner? { 24 | if #available(iOS 13.0, *) { 25 | return PIPCorner(radius: 6, curve: .continuous) 26 | } else { 27 | return PIPCorner(radius: 6) 28 | } 29 | } 30 | func didChangedState(_ state: PIPState) {} 31 | func didChangePosition(_ position: PIPPosition) {} 32 | } 33 | 34 | public extension PIPUsable where Self: UIViewController { 35 | 36 | func setNeedsUpdatePIPFrame() { 37 | guard PIPKit.isPIP else { 38 | return 39 | } 40 | pipEventDispatcher?.updateFrame() 41 | } 42 | 43 | func startPIPMode() { 44 | PIPKit.startPIPMode() 45 | } 46 | 47 | func stopPIPMode() { 48 | PIPKit.stopPIPMode() 49 | } 50 | 51 | } 52 | 53 | internal extension PIPUsable where Self: UIViewController { 54 | 55 | func pipDismiss(animated: Bool, completion: (() -> Void)?) { 56 | if animated { 57 | UIView.animate(withDuration: 0.24, delay: 0, options: .curveEaseOut, animations: { [weak self] in 58 | self?.view.alpha = 0.0 59 | self?.view.transform = CGAffineTransform(scaleX: 0.1, y: 0.1) 60 | }) { [weak self] (_) in 61 | self?.view.removeFromSuperview() 62 | completion?() 63 | } 64 | } else { 65 | view.removeFromSuperview() 66 | completion?() 67 | } 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /PIPKit/PIPKit.h: -------------------------------------------------------------------------------- 1 | // 2 | // PIPKit.h 3 | // PIPKit 4 | // 5 | // Created by Kofktu on 2022/01/03. 6 | // 7 | 8 | #import 9 | 10 | //! Project version number for PIPKit. 11 | FOUNDATION_EXPORT double PIPKitVersionNumber; 12 | 13 | //! Project version string for PIPKit. 14 | FOUNDATION_EXPORT const unsigned char PIPKitVersionString[]; 15 | 16 | // In this header, you should import all the public headers of your framework using statements like #import 17 | 18 | 19 | -------------------------------------------------------------------------------- /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 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "PIPKit", 7 | platforms: [ 8 | .iOS(.v12) 9 | ], 10 | products: [ 11 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 12 | .library(name: "PIPKit", targets: ["PIPKit"]), 13 | ], 14 | targets: [ 15 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 16 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 17 | .target(name: "PIPKit", path: "PIPKit/Classes") 18 | ] 19 | ) 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PIPKit 2 | 3 | ![Swift](https://img.shields.io/badge/Swift-5.0-orange.svg) 4 | [![CocoaPods](http://img.shields.io/cocoapods/v/PIPKit.svg?style=flat)](http://cocoapods.org/?q=name%3APIPKit%20author%3AKofktu) 5 | [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 6 | 7 | 8 | - Picture in Picture for iOS (iPhone, iPad) 9 | 10 | ![pip_default](/Screenshot/default.gif) 11 | ![pip_transition](/Screenshot/transition.gif) 12 | 13 | ## Requirements 14 | - iOS 8.0+ 15 | - Swift 5.0 16 | - Xcode 11 17 | 18 | ## Installation 19 | 20 | #### CocoaPods 21 | PIPKit is available through [CocoaPods](http://cocoapods.org). To install 22 | it, simply add the following line to your Podfile: 23 | 24 | ```ruby 25 | pod 'PIPKit' 26 | ``` 27 | 28 | #### Carthage 29 | For iOS 8+ projects with [Carthage](https://github.com/Carthage/Carthage) 30 | 31 | ``` 32 | github "Kofktu/PIPKit" 33 | ``` 34 | 35 | ### Swift Package Manager 36 | 37 | The [Swift Package Manager](https://swift.org/package-manager/) is a tool for automating the distribution of Swift code and is integrated into the `swift` compiler. It is in early development, but Alamofire does support its use on supported platforms. 38 | 39 | Once you have your Swift package set up, adding `PIPKit` as a dependency is as easy as adding it to the `dependencies` value of your `Package.swift`. 40 | 41 | ```swift 42 | dependencies: [ 43 | .package(url: "https://github.com/Kofktu/PIPKit.git", .upToNextMajor(from: "1.0.0")) 44 | ] 45 | ``` 46 | 47 | ## Usage 48 | 49 | #### PIPUsable 50 | 51 | ```swift 52 | public protocol PIPUsable { 53 | var initialState: PIPState { get } 54 | var initialPosition: PIPPosition { get } 55 | var insetsPIPFromSafeArea: Bool { get } 56 | var pipEdgeInsets: UIEdgeInsets { get } 57 | var pipSize: CGSize { get } 58 | var pipShadow: PIPShadow? { get } 59 | var pipCorner: PIPCorner? { get } 60 | func didChangedState(_ state: PIPState) 61 | func didChangePosition(_ position: PIPPosition) 62 | } 63 | 64 | ``` 65 | 66 | #### PIPKit 67 | 68 | ```swift 69 | class PIPKit { 70 | var isPIP: Bool 71 | var isActive: Bool 72 | var visibleViewController: PIPKitViewController? 73 | 74 | class func show(with viewController: PIPKitViewController, completion: (() -> Void)? = nil) 75 | class func dismiss(animated: Bool, completion: (() -> Void)? = nil) 76 | } 77 | ``` 78 | 79 | #### PIPKitViewController (UIViewController & PIPUsable) 80 | ```swift 81 | func setNeedsUpdatePIPFrame() 82 | func startPIPMode() 83 | func stopPIPMode() 84 | ``` 85 | 86 | ## At a Glance 87 | 88 | #### Show & Dismiss 89 | ```swift 90 | class PIPViewController: UIViewController, PIPUsable {} 91 | 92 | let viewController = PIPViewController() 93 | PIPKit.show(with: viewController) 94 | PIPKit.dismiss(animated: true) 95 | ``` 96 | 97 | #### Update PIPSize 98 | 99 | ![pip_resize](/Screenshot/resize.gif) 100 | 101 | ```swift 102 | class PIPViewController: UIViewController, PIPUsable { 103 | func updatePIPSize() { 104 | pipSize = CGSize() 105 | pipEdgeInsets = UIEdgeInsets() 106 | setNeedsUpdatePIPFrame() 107 | } 108 | } 109 | ``` 110 | 111 | #### FullScreen <-> PIP Mode 112 | ```swift 113 | class PIPViewController: UIViewController, PIPUsable { 114 | func fullScreenAndPIPMode() { 115 | if PIPKit.isPIP { 116 | stopPIPMode() 117 | } else { 118 | startPIPMode() 119 | } 120 | } 121 | 122 | func didChangedState(_ state: PIPState) {} 123 | } 124 | ``` 125 | 126 | ## AVPIPKitUsable 127 | UIView that is capable of Picture-in-Picture in iOS (AVKit.framework) 128 | 129 | 130 | 131 | ### Requirements 132 | - iOS 15 or higher 133 | - Info.plist - `Audio, AirPlay and Picture in Picture` in `Background Modes`. For more information, see [Apple Documentation](https://developer.apple.com/documentation/avfoundation/media_playback_and_selection/creating_a_basic_video_player_ios_and_tvos/enabling_background_audio) 134 | 135 | ### At a Glance 136 | 137 | ```swift 138 | var PIPKit.isAVPIPKitSupported: Bool 139 | ``` 140 | 141 | #### AVPIPKitRenderer 142 | 143 | ```swift 144 | protocol AVPIPKitRenderer { 145 | 146 | var policy: AVPIPKitRenderPolicy { get } 147 | var renderPublisher: AnyPublisher { get } 148 | 149 | func start() 150 | func stop() 151 | 152 | } 153 | ``` 154 | 155 | #### AVPIPUIKitUsable 156 | ```swift 157 | class View: UIView, AVPIPUIKitUsable { 158 | 159 | var pipTargetView: UIView { self } // Return the subview that you want to show. 160 | var renderPolicy: AVPIPKitRenderPolicy { 161 | // .once - only once render 162 | // .preferredFramesPerSecond - render in frames-per-second 163 | } 164 | } 165 | 166 | view.startPictureInPicture() 167 | view.stopPictureInPicture() 168 | 169 | class ViewController: UIViewController, AVPIPUIKitUsable { 170 | var pipTargetView: UIView { view } // Return the subview that you want to show. 171 | 172 | func start() { 173 | startPictureInPicture() 174 | } 175 | 176 | func stop() { 177 | stopPictureInPicture() 178 | } 179 | } 180 | ``` 181 | 182 | ## References 183 | 184 | - UIPiPDemo (https://github.com/uakihir0/UIPiPDemo) 185 | 186 | ## Authors 187 | 188 | Taeun Kim (kofktu), 189 | 190 | ## License 191 | 192 | PIPKit is available under the ```MIT``` license. See the ```LICENSE``` file for more info. 193 | -------------------------------------------------------------------------------- /Screenshot/default.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kofktu/PIPKit/377ff1b5b2d9554268c77ac2f8d0f10c5c59e129/Screenshot/default.gif -------------------------------------------------------------------------------- /Screenshot/resize.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kofktu/PIPKit/377ff1b5b2d9554268c77ac2f8d0f10c5c59e129/Screenshot/resize.gif -------------------------------------------------------------------------------- /Screenshot/transition.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kofktu/PIPKit/377ff1b5b2d9554268c77ac2f8d0f10c5c59e129/Screenshot/transition.gif --------------------------------------------------------------------------------