├── .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 | 
  4 | [](http://cocoapods.org/?q=name%3APIPKit%20author%3AKofktu)
  5 | [](https://github.com/Carthage/Carthage)
  6 |  7 | 
  8 | - Picture in Picture for iOS (iPhone, iPad)
  9 | 
 10 | 
 11 | 
 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 | 
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 |
  7 | 
  8 | - Picture in Picture for iOS (iPhone, iPad)
  9 | 
 10 | 
 11 | 
 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 | 
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
--------------------------------------------------------------------------------
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
--------------------------------------------------------------------------------